OiMy Stabilization Plan — Authored by GPT-5.6 (Independent Model)

Date: 2026-07-11 Purpose: Bharath reviewed the independent GPT-5.6 cross-check of Fable's Hetzner audit and Goal-2 plan (see GPT56-INDEPENDENT-REVIEW-20260711.md) and made a real decision: stabilize api.py + the full OiMy data/knowledge layer + the /context endpoint to real alpha-rollout production-grade reliability FIRST, before adopting Hermes-heavy. He also locked a new architectural clarification: api.py stays the permanent direct handler for the web app and Rork mobile app; Hermes is scoped specifically to the channel-gateway layer (iMessage via Photon, and Telegram). This document is the actual, execution-ready engineering plan for that stabilization work, authored by GPT-5.6 itself (not summarized or paraphrased by Claude) — the same independent model whose prior review informed this decision, now asked to turn its own recommendations into a real build plan.

Methods note


OiMy production-stabilization execution plan

0. Target architecture and non-negotiable invariants

The permanent serving model is:

Web app / Rork
    └── authenticated POST /chat
            └── api.py product engine
                    ├── OiMy durable family/knowledge layer
                    ├── turn ledger
                    └── HTTP response accountability

Telegram today
    └── thin Telegram gateway
            ├── ingress deduplication
            ├── api.py /chat today, or Hermes channel path later
            └── durable outbound outbox + Telegram delivery confirmation

Telegram / iMessage later
    └── Hermes channel gateway
            ├── POST /context
            ├── model generation
            ├── POST /context/writeback
            └── durable outbound outbox + channel delivery confirmation

api.py remains the direct web/Rork API permanently. Hermes never fronts those clients.

The stabilization must establish these invariants:

  1. Authentication determines user and family identity. Request bodies and proxy headers never do.
  2. Every accepted user message has one immutable turn_id.
  3. Every turn has an authoritative session identity and context revision.
  4. Every asynchronous job carries the originating turn/session/revision.
  5. Every accepted turn reaches a durable terminal disposition.
  6. Every channel delivery is represented by a durable outbox row and an acknowledged or terminal-failure result.
  7. Direct HTTP delivery and channel delivery have different, honest terminal states: - Direct API: response_committed, optionally followed by client_acknowledged. - Telegram/iMessage: provider_accepted, optionally followed by user/channel acknowledgment.
  8. No generated answer exists solely in process memory.
  9. Every production instance exposes its exact code/image/config/schema identity.
  10. An OpenClaw-equivalent failure is detected within minutes, not through user complaints.

Phase 0 — Contain the live authentication vulnerability and establish deployment control

This phase precedes all schema or endpoint work. It is safe to deploy independently and should be treated as an emergency security release.

0.1 Remove unauthenticated GET /token

The live implementation near api.py:6001 must be removed:

elif parsed.path == "/token":
    params = parse_qs(parsed.query)
    uid = params.get("user_id", ["default"])[0]
    token = generate_token(uid)
    self._json_response({"token": token, "user_id": uid})

Replace it with an unconditional response that does not reveal whether a user exists:

elif parsed.path == "/token":
    self._json_response(
        {"error": "endpoint_removed"},
        status=410,
        headers={"Cache-Control": "no-store"},
    )

Do not leave a compatibility mode accepting a shared query-string secret. URLs are logged by proxies, shells, analytics, and browser history.

0.2 Replace token issuance with authenticated exchange

If web/Rork require OiMy-issued JWTs, add:

POST /auth/token
Authorization: Bearer <upstream identity token>
Content-Type: application/json

The endpoint must:

  1. Validate an upstream identity token against an allowlisted issuer and audience.
  2. Resolve the upstream identity to an OiMy user in the database.
  3. Load family membership and current role from the database.
  4. Mint only for that resolved user. It must not accept user_id as an authority.
  5. Return a short-lived access token.

JWT claims:

{
  "iss": "https://api.oimyai.com",
  "aud": "oimy-api",
  "sub": "user_01...",
  "family_id": "family_01...",
  "roles": ["guardian"],
  "scope": ["chat", "context:read", "context:write"],
  "iat": 1783728000,
  "exp": 1783731600,
  "jti": "jwt_01..."
}

Recommended lifetime:

For Telegram, the gateway authenticates as a service and submits the externally mapped sender identifier. The engine resolves that mapping server-side. A user-controlled Telegram field must not become an arbitrary OiMy user_id.

0.3 Invalidate tokens minted through the vulnerable endpoint

Merely deleting /token does not invalidate already minted JWTs.

Perform, in this order:

  1. Generate a new JWT signing key or key pair.
  2. Deploy verification that accepts both old and new keys for no more than a tightly controlled migration window if necessary.
  3. Switch all legitimate clients/gateways to the new key.
  4. Remove the old verification key.
  5. Record the security event and affected time window.
  6. Search access logs for /token, but treat query strings as sensitive and restrict the incident artifact.
  7. Review accesses to /chat, /profile/*, and other JWT-gated routes during that window for suspicious subjects, IPs, or impossible identity combinations.

Use kid headers and a versioned key set so future rotations do not require ad hoc code edits.

0.4 Centralize authorization immediately

Introduce one function used by every protected route:

@dataclass(frozen=True)
class AuthenticatedPrincipal:
    subject_user_id: str
    family_id: str
    roles: frozenset[str]
    scopes: frozenset[str]
    token_jti: str
    auth_method: str  # "user_jwt", "gateway_service", "synthetic"
    gateway_id: str | None = None

def authenticate_request(
    headers: Mapping[str, str],
    *,
    required_scopes: frozenset[str],
) -> AuthenticatedPrincipal:
    ...

Then enforce body matching through a separate helper:

def require_subject_match(
    principal: AuthenticatedPrincipal,
    body_user_id: str | None,
) -> str:
    if body_user_id is not None and body_user_id != principal.subject_user_id:
        raise HTTPError(403, "subject_mismatch")
    return principal.subject_user_id

For service-authenticated gateways, resolve external sender mappings through a database table; do not allow a service to address every user unless its policy explicitly grants that capability.

0.5 Release controls

Before this deploy:

Phase 0 exit gate:


Phase 1 — Introduce explicit RequestContext without destabilizing the current engine

1.1 Verdict: build it now

The threading.local() repair at api.py:1859–1884 is technically correct for the current synchronous ThreadingHTTPServer. It should not be described as still broken.

Nevertheless, RequestContext should be built now, before /context, the outbox, or server replacement, for three concrete reasons:

  1. The outbox and turn ledger require the same identity and turn metadata at every write point.
  2. /context and /context/writeback create a second invocation path into the knowledge layer. Continuing implicit state would make those endpoints easy to implement inconsistently.
  3. Replacing ThreadingHTTPServer later is unsafe while request identity depends on thread affinity.

This is not a big-bang rewrite. Keep the current thread-local properties as a compatibility shim while migrating helpers incrementally.

1.2 Core types

Create oimy/request_context.py or an equivalent module, rather than adding more unstructured code to api.py.

from dataclasses import dataclass, field
from datetime import datetime
from enum import Enum
from typing import Literal, Mapping

class SessionAuthority(str, Enum):
    ENGINE_GAP = "engine_gap"
    HERMES = "hermes"

class Channel(str, Enum):
    WEB = "web"
    RORK = "rork"
    TELEGRAM = "telegram"
    IMESSAGE = "imessage"
    SYNTHETIC = "synthetic"
    INTERNAL = "internal"

@dataclass(frozen=True)
class SessionContext:
    session_id: str
    authority: SessionAuthority
    started_at: datetime
    history_start_index: int | None
    history_watermark: int
    context_revision: int
    external_session_id: str | None = None
    external_turn_index: int | None = None

@dataclass(frozen=True)
class RequestContext:
    request_id: str
    turn_id: str
    idempotency_key: str
    principal: AuthenticatedPrincipal

    user_id: str
    family_id: str
    conversation_id: str
    channel: Channel
    gateway_id: str | None

    session: SessionContext
    skill_id: str | None
    locale: str | None
    timezone: str | None

    received_at: datetime
    deadline_at: datetime
    source_ip_hash: str | None
    trace_id: str
    parent_turn_id: str | None = None

    feature_flags: Mapping[str, bool] = field(default_factory=dict)

Requirements:

1.3 Where it is constructed

Do not construct a trusted context at the first line of do_POST, because identity has not yet been authenticated.

The request flow should be:

def do_POST(self) -> None:
    transport = parse_transport_request(self)
    principal = authenticate_request(
        transport.headers,
        required_scopes=scopes_for_route(transport.path),
    )
    body = parse_and_validate_body(transport)
    identity = resolve_route_identity(principal, body, transport.path)
    ctx = self.server.context_factory.create(
        transport=transport,
        principal=principal,
        identity=identity,
        body=body,
    )
    return self.dispatch(ctx, transport.path, body)

ContextFactory.create() performs:

  1. request_id generation.
  2. Ingress idempotency resolution.
  3. Server-side user/family/conversation lookup.
  4. Session resolution: - direct /chat: _session_start_index() and _SESSION_GAP_S; - Hermes /context: validated session_meta.
  5. Durable context revision lookup.
  6. Deadline assignment.
  7. Trace binding.

Authentication failures get a request_id for logs but do not get a user-scoped RequestContext.

1.4 Preserve both session authorities

For direct /chat:

SessionContext(
    session_id=engine_session_id,
    authority=SessionAuthority.ENGINE_GAP,
    started_at=...,
    history_start_index=_session_start_index(history, now_s),
    history_watermark=...,
    context_revision=...,
)

Boundary definition must be explicit:

This resolves the 30:00 ambiguity and should become the documented rule.

For Hermes-routed traffic:

SessionContext(
    session_id=internal_stable_session_id,
    authority=SessionAuthority.HERMES,
    started_at=parse_timestamp(session_meta["started_at"]),
    history_start_index=None,
    history_watermark=session_meta["history_watermark"],
    context_revision=current_engine_revision,
    external_session_id=session_meta["session_id"],
    external_turn_index=session_meta["turn_index"],
)

The engine must namespace external sessions by gateway:

hermes:{gateway_id}:{external_session_id}

A client cannot select ENGINE_GAP or manufacture an internal session ID.

1.5 Compatibility migration inside OiMyEngine

Add:

class OiMyEngine:
    def call_companion(
        self,
        ctx: RequestContext,
        message: str,
        *,
        history: Sequence[Message],
    ) -> CompanionResult:
        with self._bind_legacy_context(ctx):
            return self._call_companion(
                message,
                history=history,
                request_ctx=ctx,
            )

Compatibility binder:

@contextmanager
def _bind_legacy_context(self, ctx: RequestContext):
    previous_user = getattr(self, "_current_user_id", None)
    previous_skill = getattr(self, "_current_skill_id", None)
    self._current_user_id = ctx.user_id
    self._current_skill_id = ctx.skill_id
    try:
        yield
    finally:
        self._current_user_id = previous_user
        self._current_skill_id = previous_skill

Also bind the full context in the existing thread-local object:

@property
def request_context(self) -> RequestContext:
    ctx = getattr(self._request_local, "request_context", None)
    if ctx is None:
        raise RuntimeError("request context not bound")
    return ctx

This gives old helpers a safe transitional access path while new code accepts ctx explicitly.

Migration order:

  1. Entry points.
  2. Session/history selection.
  3. Constraint ledger.
  4. Anchor retrieval/demotion.
  5. Topic cache.
  6. entity facts.
  7. H3.0/deep profile.
  8. _kimi_jobs.
  9. persistence/writeback.
  10. logging/metrics.

New or edited functions must accept ctx; no new uses of _current_user_id or _current_skill_id are allowed.

Representative signatures:

def build_family_context(ctx: RequestContext, message: str) -> str: ...
def retrieve_anchors(ctx: RequestContext, message: str, limit: int = 3) -> list[Anchor]: ...
def read_constraint_ledger(ctx: RequestContext) -> ConstraintLedger: ...
def update_constraint_ledger(ctx: RequestContext, message: str) -> LedgerUpdate: ...
def build_topic_context(ctx: RequestContext, message: str) -> TopicContext: ...
def persist_turn(ctx: RequestContext, result: CompanionResult) -> WriteResult: ...

Add a CI check rejecting newly introduced references to legacy fields outside the compatibility section.

1.6 Async job envelope

Replace untyped _kimi_jobs values incrementally with:

@dataclass(frozen=True)
class JobEnvelope:
    job_id: str
    job_type: str
    user_id: str
    family_id: str
    conversation_id: str
    session_id: str
    session_authority: str
    originating_turn_id: str
    history_watermark: int
    context_revision: int
    created_at: datetime
    payload_version: int
    idempotency_key: str
    held_turns: int

Completion policy:

Condition Disposition
Same session, same required revision, next eligible turn Reconcile and use
Topic changed Store as candidate fact if appropriate; do not send
New session Do not inject as current conversational content
Out-of-order result Apply only through idempotent version checks
Duplicate retry Return prior stored result
Reconciliation exception Mark reconciliation_failed; never emit raw result
User/conversation deleted Mark subject_deleted; do not write or send
Restart after computation Resume from durable job/result row

Keep the current held_turns guard, but make it one validation input rather than the entire protocol.

Phase 1 exit gate:


Phase 2 — Durable turn ledger, ingress deduplication, jobs, and outbound outbox

This closes the primary reliability finding. It must precede /context production use because /context/writeback otherwise creates another unaccountable generation/delivery path.

2.1 Database schema

Use the current authoritative transactional database. If that is SQLite, enable WAL mode, enforce foreign keys, set a busy timeout, use short transactions, and use one migration runner. Do not introduce a second database solely for the outbox.

turns

CREATE TABLE turns (
    turn_id                 TEXT PRIMARY KEY,
    request_id              TEXT NOT NULL UNIQUE,
    idempotency_key         TEXT NOT NULL,
    family_id               TEXT NOT NULL,
    user_id                 TEXT NOT NULL,
    conversation_id         TEXT NOT NULL,
    channel                 TEXT NOT NULL,
    gateway_id              TEXT,
    external_ingress_id     TEXT,

    session_id              TEXT NOT NULL,
    session_authority       TEXT NOT NULL,
    originating_turn_id     TEXT,
    history_watermark       INTEGER NOT NULL,
    context_revision_read   INTEGER NOT NULL,
    context_revision_write  INTEGER,

    status                  TEXT NOT NULL,
    generation_status       TEXT NOT NULL DEFAULT 'not_started',
    writeback_status        TEXT NOT NULL DEFAULT 'not_started',
    delivery_status         TEXT NOT NULL DEFAULT 'not_required',

    model_provider          TEXT,
    model_name              TEXT,
    prompt_hash             TEXT,
    response_hash           TEXT,
    input_tokens            INTEGER,
    output_tokens           INTEGER,
    cost_microunits         INTEGER,

    accepted_at             TEXT NOT NULL,
    generation_started_at   TEXT,
    generation_completed_at TEXT,
    durable_write_at        TEXT,
    response_committed_at   TEXT,
    client_acknowledged_at  TEXT,
    terminal_at             TEXT,

    error_class             TEXT,
    error_code              TEXT,
    retryable               INTEGER,
    created_at              TEXT NOT NULL,
    updated_at              TEXT NOT NULL,

    UNIQUE(gateway_id, external_ingress_id),
    UNIQUE(user_id, channel, idempotency_key)
);

Do not store message text in this operational table. Store content in the existing protected conversation store and reference it through turn_id.

Valid top-level statuses:

accepted
processing
generated
persisted
delivery_pending
terminal_success
terminal_failure
cancelled

turn_events

CREATE TABLE turn_events (
    event_id        TEXT PRIMARY KEY,
    turn_id         TEXT NOT NULL REFERENCES turns(turn_id),
    sequence_no     INTEGER NOT NULL,
    event_type      TEXT NOT NULL,
    event_at        TEXT NOT NULL,
    actor           TEXT NOT NULL,
    attempt_no      INTEGER,
    metadata_json   TEXT NOT NULL DEFAULT '{}',
    UNIQUE(turn_id, sequence_no)
);

Allowed event types include:

ingress_received
ingress_authenticated
ingress_duplicate
identity_resolved
engine_accepted
context_selected
generation_started
generation_completed
generation_failed
durable_write_started
durable_write_completed
durable_write_failed
outbox_enqueued
response_commit_started
response_committed
client_acknowledged
send_claimed
send_attempted
provider_accepted
send_retry_scheduled
delivery_terminal_failure
turn_terminal_success
turn_terminal_failure

Metadata must be allowlisted and content-free.

inbound_dedup

CREATE TABLE inbound_dedup (
    gateway_id          TEXT NOT NULL,
    external_ingress_id TEXT NOT NULL,
    user_id             TEXT NOT NULL,
    turn_id             TEXT NOT NULL REFERENCES turns(turn_id),
    payload_hash        TEXT NOT NULL,
    first_seen_at       TEXT NOT NULL,
    last_seen_at        TEXT NOT NULL,
    duplicate_count     INTEGER NOT NULL DEFAULT 0,
    PRIMARY KEY(gateway_id, external_ingress_id)
);

If the same ingress ID arrives with a different payload hash, reject it and alert.

outbound_messages

CREATE TABLE outbound_messages (
    outbox_id               TEXT PRIMARY KEY,
    turn_id                 TEXT NOT NULL REFERENCES turns(turn_id),
    family_id               TEXT NOT NULL,
    user_id                 TEXT NOT NULL,
    channel                 TEXT NOT NULL,
    gateway_id              TEXT NOT NULL,
    destination_ref         TEXT NOT NULL,
    content_ref             TEXT NOT NULL,
    content_hash            TEXT NOT NULL,

    idempotency_key         TEXT NOT NULL UNIQUE,
    status                  TEXT NOT NULL,
    priority                INTEGER NOT NULL DEFAULT 100,
    not_before              TEXT NOT NULL,
    expires_at              TEXT,
    attempt_count           INTEGER NOT NULL DEFAULT 0,
    max_attempts            INTEGER NOT NULL DEFAULT 12,

    lease_owner             TEXT,
    lease_expires_at        TEXT,
    last_attempt_at         TEXT,
    next_attempt_at         TEXT,

    provider_message_id     TEXT,
    provider_status         TEXT,
    provider_accepted_at    TEXT,
    acknowledged_at         TEXT,

    last_error_class        TEXT,
    last_error_code         TEXT,
    last_error_detail_safe  TEXT,

    created_at              TEXT NOT NULL,
    updated_at              TEXT NOT NULL,
    terminal_at             TEXT
);

destination_ref should preferably reference an encrypted channel-address record rather than contain a raw phone number/chat ID.

outbound_attempts

CREATE TABLE outbound_attempts (
    attempt_id              TEXT PRIMARY KEY,
    outbox_id               TEXT NOT NULL REFERENCES outbound_messages(outbox_id),
    attempt_no              INTEGER NOT NULL,
    worker_id               TEXT NOT NULL,
    started_at              TEXT NOT NULL,
    finished_at             TEXT,
    outcome                 TEXT,
    provider_http_status    INTEGER,
    provider_message_id     TEXT,
    retry_after_seconds     INTEGER,
    error_class             TEXT,
    error_code              TEXT,
    UNIQUE(outbox_id, attempt_no)
);

async_jobs

CREATE TABLE async_jobs (
    job_id                  TEXT PRIMARY KEY,
    job_type                TEXT NOT NULL,
    idempotency_key         TEXT NOT NULL UNIQUE,
    family_id               TEXT NOT NULL,
    user_id                 TEXT NOT NULL,
    conversation_id         TEXT NOT NULL,
    session_id              TEXT NOT NULL,
    originating_turn_id     TEXT NOT NULL,
    history_watermark       INTEGER NOT NULL,
    context_revision        INTEGER NOT NULL,
    held_turns              INTEGER NOT NULL DEFAULT 0,
    status                  TEXT NOT NULL,
    attempt_count           INTEGER NOT NULL DEFAULT 0,
    result_ref              TEXT,
    terminal_disposition    TEXT,
    created_at              TEXT NOT NULL,
    updated_at              TEXT NOT NULL
);

2.2 Exact lifecycle for direct /chat

  1. Authenticate.
  2. Validate body and size.
  3. Resolve server-side identity.
  4. Derive or validate Idempotency-Key.
  5. In one transaction: - insert turns(status='accepted'); - insert engine_accepted; - store inbound message in the protected conversation store.
  6. Select session/context and record context_selected.
  7. Set generation_started.
  8. Generate.
  9. Persist the generated assistant message immediately, keyed by turn_id.
  10. Set generation_completed.
  11. Apply entity facts, profile, ledger, topic, and related writes transactionally where possible.
  12. Set durable_write_completed with the resulting context_revision_write.
  13. Serialize HTTP response including turn_id.
  14. Immediately before writing response bytes, record response_commit_started.
  15. After the server successfully flushes the response without a socket error, record response_committed.
  16. Set terminal direct-delivery status: - response_committed is success at the API transport boundary; - it does not mean the human saw the answer.
  17. Web/Rork optionally calls:
POST /turns/{turn_id}/ack
Idempotency-Key: ...
{"ack_type": "rendered", "client_timestamp": "..."}

This transitions to client_acknowledged.

If a duplicate direct request has the same idempotency key:

2.3 Exact lifecycle for Telegram/iMessage/Hermes channels

At channel ingress:

  1. Authenticate webhook/provider signature.
  2. Compute (gateway_id, external_ingress_id).
  3. Insert inbound_dedup and turns atomically.
  4. A duplicate returns the existing turn state and does not regenerate.
  5. Generation and durable knowledge writes complete.
  6. In the same transaction that makes the assistant response available: - insert outbound_messages(status='pending'); - insert outbox_enqueued; - set the turn to delivery_pending.
  7. A sender worker claims the row using a lease.
  8. Record each provider attempt before making the external call.
  9. Interpret provider result: - success: record provider message ID and provider_accepted; - 429: use provider retry_after plus jitter; - timeout/reset/5xx: retry with bounded exponential backoff; - permanent destination/auth failure: terminal failure and alert.
  10. If the process crashes after provider acceptance but before recording it, retry with a provider idempotency key where supported. Where unsupported, reconcile by provider message ID or accept a narrowly bounded duplicate risk. The outbox remains the source of truth.

Suggested retry schedule:

0s, 5s, 15s, 30s, 60s, 2m, 5m, 10m, 20m, 30m, 60m, 120m

Apply jitter. Respect provider retry_after. Expiration depends on message type:

2.4 Sender ownership

There must be one accountable sender implementation per channel.

Preferred interface:

class ChannelSender(Protocol):
    def send(self, message: OutboundMessage) -> ProviderSendResult:
        ...

Workers claim rows with:

def claim_outbox_batch(
    worker_id: str,
    channel: str,
    limit: int,
    lease_seconds: int = 60,
) -> list[OutboundMessage]:
    ...

A crashed worker’s lease expires and another worker retries.

If Telegram remains outside the engine process, expose service-authenticated operations:

POST /internal/outbox/claim
POST /internal/outbox/{outbox_id}/attempt
POST /internal/outbox/{outbox_id}/confirm
POST /internal/outbox/{outbox_id}/fail

These must require mTLS or a narrowly scoped service JWT. They are not user endpoints.

2.5 Proactive messages

rhythm_all_users.sh, run_proactive_outreach.sh, and voice_outreach.sh must no longer call providers directly.

They should create a synthetic/proactive turn and enqueue an outbox row:

turn.origin = proactive
turn.parent_turn_id = nullable
outbox.idempotency_key =
  proactive:{campaign_id}:{user_id}:{scheduled_window}

This prevents duplicate sends when cron overlaps or restarts.

2.6 Medication reminders

Medication reminders use the same outbox but a separate authoritative reminder schema and policy. At minimum:

CREATE TABLE medication_reminders (
    reminder_id            TEXT PRIMARY KEY,
    family_id              TEXT NOT NULL,
    subject_user_id        TEXT NOT NULL,
    medication_display     TEXT NOT NULL,
    timezone               TEXT NOT NULL,
    schedule_rule          TEXT NOT NULL,
    status                 TEXT NOT NULL,
    created_by_user_id     TEXT NOT NULL,
    authorization_revision INTEGER NOT NULL,
    version                INTEGER NOT NULL,
    created_at             TEXT NOT NULL,
    updated_at             TEXT NOT NULL
);

CREATE TABLE medication_occurrences (
    occurrence_id          TEXT PRIMARY KEY,
    reminder_id            TEXT NOT NULL,
    reminder_version       INTEGER NOT NULL,
    scheduled_for          TEXT NOT NULL,
    late_send_deadline     TEXT NOT NULL,
    idempotency_key        TEXT NOT NULL UNIQUE,
    status                 TEXT NOT NULL,
    outbox_id              TEXT,
    acknowledged_at        TEXT,
    acknowledgment_type    TEXT
);

A sent reminder means only “the provider accepted a reminder message.” It must never be recorded or worded as medication taken. “Taken” requires a separate explicit acknowledgment.

Phase 2 exit gate:


Phase 3 — Build /context and /context/writeback

These endpoints come after RequestContext and the ledger/outbox foundations because they must not establish a second untracked path.

3.1 Versioning and authentication

Use versioned media types or a schema header:

Content-Type: application/json
X-OiMy-Protocol-Version: 1
Authorization: Bearer <scoped token>
Idempotency-Key: <required>

Scopes:

Enforce body.user_id == JWT sub for per-sender tokens. For gateway service tokens, resolve the external sender through an authorized mapping.

Body limits:

3.2 Required protocol extensions

The agreed design is sound but lacks enough metadata for idempotency, session accountability, and durable delivery. Add optional fields now and make them mandatory for production gateways:

{
  "user_id": "8040940969",
  "message": "raw user message",
  "include": ["family", "profile", "anchors", "ledger", "honcho", "topic"],
  "max_bytes": 16000,
  "turn_id": "turn_01...",
  "conversation_id": "conv_01...",
  "external_ingress_id": "telegram-update-12345",
  "session_meta": {
    "session_id": "hermes-session-...",
    "started_at": "2026-07-11T10:00:00Z",
    "turn_index": 12,
    "history_watermark": 11
  }
}

The server may mint turn_id when absent for development compatibility, but production Hermes must preserve the returned value into writeback and delivery.

Response additions:

{
  "turn_id": "turn_01...",
  "context_revision": 481,
  "blocks": { "...": "..." },
  "anchors_delivered": ["becky-kennedy-good-inside"],
  "meta": {
    "skill_hint": "parenting-coach",
    "router_confidence": 0.86,
    "session_id": "hermes:telegram-gateway:...",
    "protocol_version": 1
  }
}

3.3 /context implementation order

Implement as a service function first:

@dataclass(frozen=True)
class ContextRequest:
    message: str
    include: frozenset[str]
    max_bytes: int

@dataclass(frozen=True)
class ContextResponse:
    turn_id: str
    context_revision: int
    blocks: Mapping[str, str]
    anchor_ids: tuple[str, ...]
    skill_hint: str
    router_confidence: float

def get_grounding_context(
    ctx: RequestContext,
    req: ContextRequest,
) -> ContextResponse:
    ...

Execution:

  1. Authenticate and deduplicate ingress.
  2. Construct Hermes-authority RequestContext.
  3. Insert/resolve the durable turn.
  4. Run the existing router for skill_hint.
  5. Call h30.update_constraint_ledger(ctx, message) before reading the ledger.
  6. Increment the durable context revision if that update changes state.
  7. Retrieve requested blocks through existing engine functions.
  8. Apply per-block and total byte caps.
  9. Record the revision and content hashes, not content, in the turn events.
  10. Return labeled blocks.

Assembly order and hard caps:

Block Maximum
family 600 bytes
profile 2,500 bytes
anchors 12,288 bytes and max 3 anchors
ledger 2,000 bytes
honcho 2,000 bytes
topic 1,000 bytes
Total caller max_bytes, hard maximum 16,000

Because individual maxima exceed the total, use priority-based truncation:

  1. Ledger.
  2. Family.
  3. Topic.
  4. Profile.
  5. Anchors.
  6. Honcho.

Never truncate by raw byte slicing that produces invalid UTF-8. Truncate whole records/sections and add a safe [TRUNCATED] marker.

3.4 Anchor delivery bookkeeping correction

The existing field name anchors_delivered in the /context response is retained for compatibility, but retrieving an anchor is not proof it reached the user.

Implement an anchor reservation:

CREATE TABLE anchor_deliveries (
    reservation_id    TEXT PRIMARY KEY,
    anchor_id         TEXT NOT NULL,
    user_id           TEXT NOT NULL,
    session_id        TEXT NOT NULL,
    turn_id           TEXT NOT NULL,
    status            TEXT NOT NULL, -- reserved/delivered/released/expired
    reserved_at       TEXT NOT NULL,
    expires_at        TEXT NOT NULL,
    delivered_at      TEXT,
    UNIQUE(anchor_id, user_id, session_id, turn_id)
);

/context reserves returned anchors. /context/writeback confirms only IDs actually used in the generated reply. A failed/abandoned generation releases or expires the reservation. This preserves deliver-once behavior without permanently suppressing anchors after a gateway failure.

3.5 /context/writeback

Production request:

{
  "user_id": "8040940969",
  "turn_id": "turn_01...",
  "context_revision_read": 481,
  "user_message": "raw user message",
  "assistant_message": "generated reply",
  "anchors_delivered": ["becky-kennedy-good-inside"],
  "source": "hermes-heavy",
  "model": {
    "provider": "x-ai",
    "name": "grok-4.3"
  },
  "session_meta": {
    "session_id": "hermes-session-...",
    "started_at": "2026-07-11T10:00:00Z",
    "turn_index": 12,
    "history_watermark": 11
  },
  "delivery": {
    "channel": "telegram",
    "gateway_id": "telegram-hermes",
    "destination_ref": "channel_address_01..."
  }
}

Implementation function:

@dataclass(frozen=True)
class WritebackResult:
    turn_id: str
    status: Literal["persisted", "already_persisted", "conflict"]
    context_revision_write: int
    outbox_id: str | None

def writeback_context_turn(
    ctx: RequestContext,
    request: ContextWritebackRequest,
) -> WritebackResult:
    ...

Transaction behavior:

  1. Lock/load the turn.
  2. Validate user, family, gateway, session, and source.
  3. Validate context_revision_read: - If current revision is unchanged, proceed. - If changed, merge commutative/entity writes where safe. - For conflicting ledger/profile state, queue recomputation against current state; do not overwrite blindly.
  4. Persist user and assistant messages keyed by turn_id.
  5. Run or enqueue: - entity extraction; - deep-profile store_message; - H3.0 ledger/openings update; - Honcho append.
  6. Confirm delivered anchor reservations; release unused reservations.
  7. Advance context revision atomically.
  8. Insert the channel outbox row in the same transaction.
  9. Mark writeback complete.
  10. Return the existing result on duplicate idempotency key.

Do not permit the gateway to send before successful writeback/outbox creation. The sequence is:

/context → generation → /context/writeback commits → sender claims outbox → provider send

This is stricter than a loose “post-turn hook” and ensures generated replies cannot disappear between generation and delivery.

If Hermes must own the actual provider API call, it should act as the outbox sender and report the provider result through the internal confirmation endpoints. It must not independently send from transient hook memory.

3.6 Deletion/conflict behavior

3.7 Performance targets

For local production traffic:

Phase 3 exit gate:


Phase 4 — Replace the bare ThreadingHTTPServer boundary safely

Do this only after explicit context is present and mutable request identity no longer depends on thread-local behavior.

4.1 Extract route functions before replacing the server

Create transport-independent handlers:

def handle_chat(ctx: RequestContext, req: ChatRequest) -> ChatResponse: ...
def handle_context(ctx: RequestContext, req: ContextRequest) -> ContextResponse: ...
def handle_context_writeback(
    ctx: RequestContext,
    req: ContextWritebackRequest,
) -> WritebackResponse: ...

Then place a conventional bounded server around them.

Recommended initial deployment:

Do not deploy multiple workers until caches, job ownership, and SQLite write behavior are tested across processes.

4.2 Resource and timeout policy

Initial limits, tuned by load test:

Use semaphores/bulkheads per dependency so a slow model provider cannot consume all context/read capacity.

4.3 Health endpoints

GET /health/live
GET /health/ready
GET /health/version

/health/live checks only that the process loop is functioning.

/health/ready checks:

It must not invoke a paid model on every probe.

/health/version returns:

{
  "service": "oimy-engine",
  "git_commit": "00ac059...",
  "dependency_lock_sha256": "...",
  "image_digest": "sha256:...",
  "config_revision": "cfg-...",
  "schema_version": 17,
  "build_time": "...",
  "instance_id": "...",
  "protocol_versions": {
    "chat": 1,
    "context": 1,
    "writeback": 1
  }
}

No secrets, filesystem paths, tokens, or user data.

4.4 Safe migration

  1. Run the new server on a separate internal port.
  2. Replay sanitized production-shaped requests.
  3. Shadow only read-only operations initially.
  4. Route synthetic traffic.
  5. Route Bharath’s test account.
  6. Route 5% of alpha traffic.
  7. Increase to 25%, 50%, 100% after fixed observation windows.
  8. Keep old server available for immediate routing rollback, but ensure only one path owns each ingress idempotency key.

Phase 5 — Monitoring, alerting, and operational accountability

5.1 Audit the existing scripts rather than assume coverage

Audit and commit these scripts into the production repository or a dedicated infrastructure repository:

For each script, record:

owner
source repository and commit
execution mechanism
schedule
timeout
lock/concurrency behavior
inputs/secrets
outputs
metrics emitted
alert destination
alert deduplication
last successful execution
failure behavior
test procedure

A cron entry is not monitoring unless failure reaches an actively monitored destination.

Every scheduled script must:

5.2 Required metrics

Process and host

oimy_process_start_total
oimy_process_uptime_seconds
oimy_process_restart_total
oimy_process_oom_total
oimy_memory_working_set_bytes
oimy_memory_limit_bytes
oimy_open_fds
oimy_thread_count
oimy_active_requests
oimy_request_queue_depth
oimy_disk_free_bytes
oimy_db_busy_seconds_total

HTTP

oimy_http_requests_total{route,method,status_class}
oimy_http_request_duration_seconds{route}
oimy_http_rejected_total{reason}
oimy_auth_failures_total{reason}
oimy_idempotency_conflicts_total{route}

Do not label metrics by user, family, raw status text, or turn ID.

Turn funnel

oimy_turns_total{channel,stage,outcome}
oimy_turn_age_seconds{stage}
oimy_turns_nonterminal
oimy_generation_duration_seconds{provider,model}
oimy_writeback_failures_total{subsystem}
oimy_context_revision_conflicts_total

Outbox

oimy_outbox_depth{channel,status}
oimy_outbox_oldest_pending_seconds{channel}
oimy_outbox_attempts_total{channel,outcome}
oimy_outbox_delivery_duration_seconds{channel}
oimy_outbox_terminal_failures_total{channel,error_class}
oimy_outbox_lease_expirations_total{channel}

Jobs and proactive workflows

oimy_async_jobs{job_type,status}
oimy_async_job_oldest_seconds{job_type,status}
oimy_proactive_expected_total{campaign}
oimy_proactive_enqueued_total{campaign}
oimy_proactive_delivered_total{campaign}
oimy_reminder_due_total
oimy_reminder_enqueued_total
oimy_reminder_terminal_failure_total

5.3 Concrete alert rules

Restart/OOM

Page immediately when:

Fleet/instance reconciliation

Maintain an authoritative expected_instances inventory:

instance_id
service
environment
traffic_cohort
expected_image_digest
expected_config_revision
expected_schema_version
route
owner
enabled

Run every 2 minutes and compare:

Alerts:

This is the control that would have caught 18 of 22 OpenClaw containers failing.

5.4 External synthetic checks

Run from outside the production host/network every 5 minutes.

Synthetic A: transport/version

  1. Resolve DNS.
  2. Validate TLS/SNI and certificate lifetime.
  3. Call /health/version.
  4. Verify expected route, commit cohort, and latency.

Synthetic B: direct /chat

Use a dedicated synthetic family with no real-family access:

  1. Obtain a synthetic-scoped token.
  2. Send a deterministic message with a unique idempotency key.
  3. Verify a valid response and turn_id.
  4. Query the internal synthetic verifier for: - turn accepted; - generation complete; - durable write complete; - response committed.
  5. Verify expected logging/cost records.

Synthetic C: Telegram end-to-end

Use a dedicated private bot/chat:

  1. Send a unique synthetic update through the public webhook.
  2. Verify ingress dedup row.
  3. Verify turn generation.
  4. Verify outbox row.
  5. Verify Telegram provider message ID.
  6. If feasible, have a bot-side observer confirm receipt.

Synthetic D: /context bridge

  1. Call /context with a Hermes-authority synthetic session.
  2. Generate a fixed test reply or use a deterministic test model.
  3. Call /context/writeback.
  4. Verify context revision, writeback, outbox, and delivery confirmation.

Alert if any expected stage is missing beyond its SLA.

5.5 Message-funnel reconciliation

Run every minute:

-- Accepted but no generation/failure record after 5 minutes
SELECT turn_id
FROM turns
WHERE status IN ('accepted', 'processing')
  AND accepted_at < now_minus_5_minutes;

-- Generated but not durably written after 2 minutes
SELECT turn_id
FROM turns
WHERE generation_status = 'completed'
  AND writeback_status NOT IN ('completed', 'deferred')
  AND generation_completed_at < now_minus_2_minutes;

-- Pending channel delivery too old
SELECT outbox_id
FROM outbound_messages
WHERE status IN ('pending', 'leased', 'retry')
  AND created_at < channel_specific_threshold;

Alert thresholds:

5.6 Logging-pipeline canary

After every scheduled synthetic:

  1. Verify exactly one expected turn row.
  2. Verify event sequence is complete.
  3. Verify token/cost fields are populated when applicable.
  4. Verify outbox attempt and provider ID.
  5. Verify timestamps are ordered.
  6. Verify user/family values correspond only to the synthetic tenant.

Alerts:

This specifically prevents a repeat of schemas existing with zero actual writes.

5.7 Deployment monitor extension

deployment_monitor.sh must enforce, not merely report:

  1. Image digest matches deployment manifest.
  2. Schema migration succeeds.
  3. All instances become ready.
  4. No restart spike.
  5. Direct synthetic passes.
  6. Context synthetic passes if enabled.
  7. Outbox/provider synthetic passes.
  8. Funnel rows exist.
  9. Error and latency rates remain within baseline.
  10. Roll back automatically if the healthy count falls or critical synthetics fail.

Post-deploy observation:

5.8 Operational ownership

Define:

An alert that nobody must acknowledge is not a production control.


Phase 6 — Data authorization, retention, backup, and safety gates

Because the product handles children’s data, custody-adjacent content, mental-health-adjacent content, and medication reminders, alpha expansion also requires a concrete authorization model.

6.1 Authorization scopes

At minimum:

family
user_private
guardian_only
subject_and_guardians
system_safety

Every durable fact, profile item, conversation, constraint, and reminder must carry:

family_id
subject_user_id
visibility_scope
created_by_user_id
authorization_revision

Roles:

guardian
adult_member
child_member
caregiver_limited
system_operator

Custody or guardian revocation increments authorization_revision. Subsequent reads and queued jobs re-check the current revision. Pending sends based on stale authorization are cancelled.

6.2 Deletion

A deletion job must enumerate and remove or tombstone:

Operational turn metadata may be retained only according to documented legal/security retention and must be de-identified where possible.

6.3 Backup/restore

At least monthly before alpha expansion, and after major schema changes:

  1. Restore the latest backup into an isolated environment.
  2. Verify schema version and checksums.
  3. Compare table counts.
  4. Run foreign-key checks.
  5. Verify encrypted data can be decrypted with recovery-managed keys.
  6. Disable all outbound delivery before restore.
  7. Mark restored outbox rows and async jobs as quarantined.
  8. Prove no historical messages are sent.
  9. Run synthetic reads and a new post-restore turn.

Phase 7 — Testing program

7.1 Unit and contract tests

Cover:

7.2 Adversarial tenant isolation

Use:

Family A: PRIVATE_A_7391
Family B: PRIVATE_B_2846
Same-family user A2: PRIVATE_A2_9144

Insert barriers before and after:

Run thousands of interleavings, including exceptions at every barrier.

Inspect:

Pass condition: no foreign canary at any layer.

7.3 Session matrix

Run for both authority types where applicable:

Verify one session ID/revision across:

7.4 Crash-point testing

Inject process termination after:

  1. ingress row commit;
  2. model call start;
  3. model response receipt;
  4. assistant-message persistence;
  5. context writes;
  6. outbox enqueue;
  7. outbox lease;
  8. provider acceptance;
  9. before provider confirmation commit.

For each point, prove:

7.5 Longitudinal multi-day simulator

Extend the existing loop-phase0 family-simulator pattern to run against actual deployed instances, not imported functions.

Create at least 20 synthetic families with varied compositions:

Run compressed multi-day scenarios plus real wall-clock scenarios.

Each simulated family should include:

Assertions:

Run continuously for at least seven days before alpha expansion, with no unexplained nonterminal turn.

7.6 Bharath’s working Telegram test instance

Create a dedicated production-like cohort for Bharath’s existing Telegram bot:

Required demonstration over at least seven days:

  1. Multi-turn conversation with at least 50 total turns.
  2. Session continuation within 30 minutes.
  3. New-session behavior after 30 minutes.
  4. Follow-up based on an earlier fact.
  5. Constraint stated and honored on the same turn.
  6. Anchor delivery and non-repetition.
  7. Proactive outreach at least once daily.
  8. User ignores one outreach, then resumes later.
  9. Outbox retry during an induced Telegram failure.
  10. Duplicate update replay without duplicate reply.
  11. Engine restart during active use.
  12. A pending background result becomes stale and is safely withheld/recomputed.
  13. Every turn visible in the ledger with provider message IDs.
  14. Alerts intentionally triggered and received for one test failure.

A single successful reply is not acceptance evidence.

7.7 Load and abuse testing

Test the actual deployed HTTP server/proxy:

Release capacity limits based on measured p95/p99 latency, memory, thread count, queue depth, and DB lock time. Alpha enrollment must remain below tested sustainable capacity with at least 2× headroom.


Execution sequencing and dependency graph

Phase 0: token closure/key rotation
    └── required before any expansion

Repository/deployment inventory ─────────────┐
Monitoring-script audit ────────────────────┤ can run in parallel with Phase 1
Authorization-model implementation ─────────┘

Phase 1: RequestContext + explicit sessions
    ├── required before new server concurrency
    ├── required before robust /context
    └── feeds turn ledger metadata

Phase 2: turn ledger + ingress dedup + jobs + outbox
    ├── required before production /context/writeback
    ├── required before proactive workflow approval
    └── required before medication reminders

Phase 3: /context + /context/writeback
    └── uses RequestContext, revisions, ledger, outbox

Phase 4: bounded production server
    └── requires explicit request state and load tests

Phase 5: full monitoring/alerts
    ├── basic restart/external checks begin during Phase 0
    └── funnel alerts complete after Phase 2 schema exists

Phase 6: deletion/backup/authorization safety gates
    └── may develop in parallel, must finish before alpha expansion

Phase 7: longitudinal/canary/load testing
    ├── starts as soon as Phase 1 canary exists
    └── final run occurs on the complete release candidate

Live-traffic risk controls

For every phase:

Highest-risk releases:

  1. JWT key rotation.
  2. Turn/outbox ownership cutover.
  3. HTTP server replacement.
  4. /context/writeback enabling real sends.

Each requires a dedicated canary and at least a 60-minute observation window.


Alpha-rollout production-grade release bar

Bharath should not expand alpha until every item below is checked with retained evidence.

Security and identity

Request/session correctness

Durable accountability

/context bridge

Server and capacity

Monitoring and operations

Data recovery and deletion

Final validation

Only after this bar is met should alpha enrollment expand or medication reminders be enabled for real families.