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
- Model:
openai/gpt-5.6-sol-pro(OpenRouter), resolved toopenai/gpt-5.6-sol-pro-20260709,reasoning.mode: pro. - One real API call, made after two earlier attempts failed on OpenRouter's default routing: the default endpoint (Azure-hosted) returned a consistent
500 Internal Server Errorfrom the Azure backend ("provider": "Azure",status: -2, degraded) after ~331.5s on three consecutive attempts regardless ofmax_tokens. Diagnosed via/api/v1/models/openai/gpt-5.6-sol-pro/endpoints, which showed the Azure endpoint in a degraded state alongside a healthyOpenAI-hosted endpoint for the same model. The successful call explicitly routed to the OpenAI-hosted endpoint ("provider": {"order": ["openai"], "allow_fallbacks": false}) and streamed the response to avoid any intermediate buffering timeout. A first attempt at this routing completed withfinish_reason: length(truncated mid-sentence at ~49K completion tokens despite a requestedmax_tokens: 16000, which this endpoint did not honor as a hard cap) — the final, complete run below usedmax_tokens: 100000and finished cleanly withfinish_reason: stop. - Final successful call: prompt_tokens=85,933 (26,394 cached), completion_tokens=51,154 (6,966 reasoning tokens), total cost $1.845512. Completed in ~300s.
- Grounding material provided: GPT-5.6's own prior Call-2 production-readiness report (pasted in full as the seed to expand, not re-derive), the real current state of
/opt/oimy-engine/api.pyverified directly viagrep/git log/crontab -lon the Hetzner production box immediately beforehand (exact line numbers for the already-fixedthreading.local()concurrency fix, the already-fixed session-boundary fix, the already-fixed bridge-staleness guard, and — newly discovered during this grounding pass — a still-live, unauthenticatedGET /tokenJWT-minting vulnerability at line ~6001, confirmed not yet fixed), the full/context+/context/writebackdesign spec from the 2026-07-10 architecture doc (§3 verbatim), the complete real bug history from the overnight session log, the OpenClaw six-week silent-outage incident as the concrete monitoring bar, and a list of existing cron/monitoring scripts already running on the box (container-monitor.sh,health-check.sh,hourly-health-report.sh,deployment_monitor.sh,oimy_engine_eval_watchdog.sh, proactive-outreach cron jobs) to audit-and-extend rather than blindly duplicate. The two newly-locked architectural decisions (stabilize-first sequencing; Hermes scoped to channel-gateway only,api.pypermanent for web/Rork) were given as fixed constraints, not open questions. - The full raw response is reproduced verbatim below, unedited.
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:
- Authentication determines user and family identity. Request bodies and proxy headers never do.
- Every accepted user message has one immutable
turn_id. - Every turn has an authoritative session identity and context revision.
- Every asynchronous job carries the originating turn/session/revision.
- Every accepted turn reaches a durable terminal disposition.
- Every channel delivery is represented by a durable outbox row and an acknowledged or terminal-failure result.
- Direct HTTP delivery and channel delivery have different, honest terminal states:
- Direct API:
response_committed, optionally followed byclient_acknowledged. - Telegram/iMessage:provider_accepted, optionally followed by user/channel acknowledgment. - No generated answer exists solely in process memory.
- Every production instance exposes its exact code/image/config/schema identity.
- 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:
- Validate an upstream identity token against an allowlisted issuer and audience.
- Resolve the upstream identity to an OiMy user in the database.
- Load family membership and current role from the database.
- Mint only for that resolved user. It must not accept
user_idas an authority. - 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:
- Web/Rork access token: 60 minutes.
- Gateway service token: 10 minutes, obtained with service authentication.
- Do not create indefinitely valid per-user tokens.
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:
- Generate a new JWT signing key or key pair.
- Deploy verification that accepts both old and new keys for no more than a tightly controlled migration window if necessary.
- Switch all legitimate clients/gateways to the new key.
- Remove the old verification key.
- Record the security event and affected time window.
- Search access logs for
/token, but treat query strings as sensitive and restrict the incident artifact. - 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:
- Commit all source changes.
- Pin dependencies in a lockfile.
- Build an immutable image.
- Record:
- Git commit;
- lockfile SHA-256;
- image digest;
- configuration revision;
- schema version.
- Back up the database.
- Run authentication regressions.
- Deploy canary first.
- Rotate keys only after canary validation.
Phase 0 exit gate:
GET /token?user_id=...returns410.- Old minted tokens are rejected.
- Cross-user body/JWT mismatch returns
403. - Missing authentication returns
401. - External spoofed
X-User-IDandX-Family-IDheaders have no effect. - All legitimate web/Rork/Telegram clients still authenticate.
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:
- The outbox and turn ledger require the same identity and turn metadata at every write point.
/contextand/context/writebackcreate a second invocation path into the knowledge layer. Continuing implicit state would make those endpoints easy to implement inconsistently.- Replacing
ThreadingHTTPServerlater 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:
- Use UUIDv7/ULID-style sortable identifiers generated by trusted server code.
turn_idis immutable.- Do not put raw IP addresses, tokens, user messages, or secrets in
RequestContext. feature_flagsmust be copied into an immutable mapping.context_revisionis a durable monotonic revision, not an in-process counter.
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:
request_idgeneration.- Ingress idempotency resolution.
- Server-side user/family/conversation lookup.
- Session resolution:
- direct
/chat:_session_start_index()and_SESSION_GAP_S; - Hermes/context: validatedsession_meta. - Durable context revision lookup.
- Deadline assignment.
- 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:
- Gap
< 1800 seconds: same session. - Gap
>= 1800 seconds: new session.
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:
- Entry points.
- Session/history selection.
- Constraint ledger.
- Anchor retrieval/demotion.
- Topic cache.
- entity facts.
- H3.0/deep profile.
_kimi_jobs.- persistence/writeback.
- 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:
- All route entry points construct
RequestContext. _call_companionreceives it.- Session ID, turn ID, user ID, family ID, and context revision appear in structured events.
- Existing thread-local properties remain only as a compatibility layer.
- The adversarial A/B concurrency suite passes with no canary leakage.
- Existing live behavior remains unchanged behind a feature flag.
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
- Authenticate.
- Validate body and size.
- Resolve server-side identity.
- Derive or validate
Idempotency-Key. - In one transaction:
- insert
turns(status='accepted'); - insertengine_accepted; - store inbound message in the protected conversation store. - Select session/context and record
context_selected. - Set
generation_started. - Generate.
- Persist the generated assistant message immediately, keyed by
turn_id. - Set
generation_completed. - Apply entity facts, profile, ledger, topic, and related writes transactionally where possible.
- Set
durable_write_completedwith the resultingcontext_revision_write. - Serialize HTTP response including
turn_id. - Immediately before writing response bytes, record
response_commit_started. - After the server successfully flushes the response without a socket error, record
response_committed. - Set terminal direct-delivery status:
-
response_committedis success at the API transport boundary; - it does not mean the human saw the answer. - 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:
- If processing: return
202with the existingturn_id. - If complete: return the exact stored response.
- If terminally failed: return the recorded failure safely.
- Never generate twice.
2.3 Exact lifecycle for Telegram/iMessage/Hermes channels
At channel ingress:
- Authenticate webhook/provider signature.
- Compute
(gateway_id, external_ingress_id). - Insert
inbound_dedupandturnsatomically. - A duplicate returns the existing turn state and does not regenerate.
- Generation and durable knowledge writes complete.
- In the same transaction that makes the assistant response available:
- insert
outbound_messages(status='pending'); - insertoutbox_enqueued; - set the turn todelivery_pending. - A sender worker claims the row using a lease.
- Record each provider attempt before making the external call.
- Interpret provider result:
- success: record provider message ID and
provider_accepted; -429: use providerretry_afterplus jitter; - timeout/reset/5xx: retry with bounded exponential backoff; - permanent destination/auth failure: terminal failure and alert. - 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:
- Ordinary reply: retry up to 2 hours, then terminal failure.
- Proactive conversational outreach: suppress if stale after its configured window.
- Medication reminder: use the separately defined late-send policy; do not apply generic reply behavior.
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:
- Every accepted turn has a durable row.
- Every row reaches a terminal status or triggers an age alert.
- Telegram/provider sends originate only from the outbox.
- A crash at every lifecycle boundary recovers without losing the response.
- Duplicate ingress does not duplicate generation or send.
- No proactive script bypasses the outbox.
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:
/context:context:read/context/writeback:context:write
Enforce body.user_id == JWT sub for per-sender tokens. For gateway service tokens, resolve the external sender through an authorized mapping.
Body limits:
- HTTP body maximum: 64 KiB.
message: maximum 16 KiB UTF-8.assistant_message: maximum 64 KiB UTF-8.max_bytes: clamp to1..16000.- Reject unknown
includevalues. - Limit array sizes and string lengths before doing model or embedding work.
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:
- Authenticate and deduplicate ingress.
- Construct Hermes-authority
RequestContext. - Insert/resolve the durable turn.
- Run the existing router for
skill_hint. - Call
h30.update_constraint_ledger(ctx, message)before reading the ledger. - Increment the durable context revision if that update changes state.
- Retrieve requested blocks through existing engine functions.
- Apply per-block and total byte caps.
- Record the revision and content hashes, not content, in the turn events.
- 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:
- Ledger.
- Family.
- Topic.
- Profile.
- Anchors.
- 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:
- Lock/load the turn.
- Validate user, family, gateway, session, and source.
- 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. - Persist user and assistant messages keyed by
turn_id. - Run or enqueue:
- entity extraction;
- deep-profile
store_message; - H3.0 ledger/openings update; - Honcho append. - Confirm delivered anchor reservations; release unused reservations.
- Advance context revision atomically.
- Insert the channel outbox row in the same transaction.
- Mark writeback complete.
- 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
- Deleted user:
410 subject_deleted; no context returned and no send. - Revoked family role:
403. - Duplicate writeback: return original success.
- Same
turn_id, different assistant hash:409 idempotency_conflictand alert. - Stale context revision: explicit
409 context_revision_conflictunless the server safely merges/recomputes. - Missing turn:
404, never silently create a writeback-only turn in production. - H3.0/Honcho failure: mark that subsystem failure and retry durably; do not pretend full writeback succeeded.
- A nonessential profile enrichment failure may allow delivery only if the core conversation write and safety-relevant ledger write succeeded. Record
persisted_with_deferred_enrichment.
3.7 Performance targets
For local production traffic:
/contextp50< 700 ms.- p95
< 1.5 s. - p99
< 3 s. - Error ratio
< 0.5%excluding caller validation failures. - Router must have a bounded timeout and fallback skill.
- SQLite lock waits must be measured separately.
- No unbounded embedding or model call may run on the request thread.
Phase 3 exit gate:
- Hermes test gateway preserves one
turn_idacross context, writeback, outbox, and provider confirmation. - Duplicate context/writeback calls are idempotent.
- Anchor reservation and confirmation work across send failures.
- Both engine-gap and Hermes session authorities pass their own boundary suites.
- Direct
/chatregressions remain green.
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:
- FastAPI/Starlette or equivalent.
- One application worker initially if the engine has process-local caches or SQLite.
- Bounded synchronous worker/thread capacity, e.g. 32.
- Bounded request queue.
- Reverse proxy with:
- TLS;
- maximum body size;
- header timeout;
- request-body timeout;
- idle timeout;
- rate limits;
- connection limits.
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:
- Maximum body: 64 KiB.
- Header timeout: 10 seconds.
- Body-read timeout: 15 seconds.
- Keep-alive idle: 15 seconds.
- Maximum concurrent engine generations: 16.
- Maximum concurrent non-model context requests: 32.
- Queue wait maximum: 5 seconds, then
503withRetry-After. - Graceful shutdown: 120 seconds.
- Outbox workers stop claiming new work on shutdown and finish/release leases.
- Model provider connect timeout: 5 seconds.
- Model total deadline: product-specific, initially 120 seconds.
- Database busy timeout: 5 seconds, with metrics.
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:
- database read/write probe;
- migrations current;
- outbox worker heartbeat if the instance owns sending;
- required secrets loaded;
- engine initialized;
- disk not critically full.
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
- Run the new server on a separate internal port.
- Replay sanitized production-shaped requests.
- Shadow only read-only operations initially.
- Route synthetic traffic.
- Route Bharath’s test account.
- Route 5% of alpha traffic.
- Increase to 25%, 50%, 100% after fixed observation windows.
- 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:
/opt/oimy/container-monitor.sh/opt/oimy/health-check.sh/opt/oimy/hourly-health-report.sh/opt/oimy-engine/scripts/deployment_monitor.sh/opt/oimy-engine/scripts/oimy_engine_eval_watchdog.sh/opt/oimy-engine/scripts/rhythm_all_users.sh/opt/oimy-engine/scripts/run_proactive_outreach.sh/opt/oimy-engine/scripts/voice_outreach.sh
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:
- use
set -euo pipefail; - have a timeout;
- use a non-overlapping lock;
- emit start/success/failure metrics;
- avoid secrets and user content in logs;
- alert after consecutive failures;
- have a synthetic test mode.
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:
- OOM kill occurs once in production.
- Restart count increases by more than 3 in 10 minutes.
- Process uptime repeatedly remains below 10 minutes.
- Readiness fails for 5 consecutive minutes.
- Memory exceeds 90% of limit for 10 minutes.
- Open FDs exceed 80% of limit.
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:
- expected;
- running;
- ready;
- publicly routable;
- correct version;
- synthetic success.
Alerts:
- Any real-user instance unready for 10 minutes: page.
- Healthy count differs from expected count: page after 10 minutes.
- Fleet health below 95%: immediate page.
- Version/config drift: ticket immediately; page if traffic-serving.
- Unknown traffic-serving instance: immediate security page.
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
- Resolve DNS.
- Validate TLS/SNI and certificate lifetime.
- Call
/health/version. - Verify expected route, commit cohort, and latency.
Synthetic B: direct /chat
Use a dedicated synthetic family with no real-family access:
- Obtain a synthetic-scoped token.
- Send a deterministic message with a unique idempotency key.
- Verify a valid response and
turn_id. - Query the internal synthetic verifier for: - turn accepted; - generation complete; - durable write complete; - response committed.
- Verify expected logging/cost records.
Synthetic C: Telegram end-to-end
Use a dedicated private bot/chat:
- Send a unique synthetic update through the public webhook.
- Verify ingress dedup row.
- Verify turn generation.
- Verify outbox row.
- Verify Telegram provider message ID.
- If feasible, have a bot-side observer confirm receipt.
Synthetic D: /context bridge
- Call
/contextwith a Hermes-authority synthetic session. - Generate a fixed test reply or use a deterministic test model.
- Call
/context/writeback. - 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:
- Any real-user turn without a terminal state for 10 minutes: page.
- Generation completed but no writeback after 2 minutes: page.
- Ordinary reply outbox pending over 2 minutes: urgent alert.
- Terminal delivery failure: immediate ticket plus page if more than one or safety-related.
- Five-minute send failure ratio over 5% with at least 10 attempts: page.
- Inbound traffic nonzero but accepted engine traffic zero for 5 minutes: page.
- Generation traffic nonzero but outbox enqueue zero for channel traffic: page.
5.6 Logging-pipeline canary
After every scheduled synthetic:
- Verify exactly one expected turn row.
- Verify event sequence is complete.
- Verify token/cost fields are populated when applicable.
- Verify outbox attempt and provider ID.
- Verify timestamps are ordered.
- Verify user/family values correspond only to the synthetic tenant.
Alerts:
- Last successful synthetic ledger row older than 10 minutes.
- Application traffic nonzero while turn inserts remain zero.
- Outbox provider traffic nonzero while attempt rows remain zero.
- Database insert failure counter greater than zero.
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:
- Image digest matches deployment manifest.
- Schema migration succeeds.
- All instances become ready.
- No restart spike.
- Direct synthetic passes.
- Context synthetic passes if enabled.
- Outbox/provider synthetic passes.
- Funnel rows exist.
- Error and latency rates remain within baseline.
- Roll back automatically if the healthy count falls or critical synthetics fail.
Post-deploy observation:
- Minimum 30 minutes for normal releases.
- Minimum 60 minutes for authentication, database, session, or outbox changes.
5.8 Operational ownership
Define:
- one primary and one backup on-call;
- alert destination with acknowledgment;
- page escalation after 10 minutes;
- runbooks for authentication outage, model outage, DB lock, outbox backlog, Telegram 429, invalid provider token, OOM, disk full, schema mismatch, and synthetic failure;
- a weekly review of nonterminal turns and terminal delivery failures.
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:
- raw messages;
- assistant messages;
- entity facts;
- H3.0 profiles;
- constraint ledgers;
- topic caches;
- anchor reservations/deliveries;
- Honcho records;
- async jobs and results;
- outbound pending messages;
- medication reminders and occurrences;
- gateway mappings;
- exports;
- search/embedding indexes;
- derived caches.
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:
- Restore the latest backup into an isolated environment.
- Verify schema version and checksums.
- Compare table counts.
- Run foreign-key checks.
- Verify encrypted data can be decrypted with recovery-managed keys.
- Disable all outbound delivery before restore.
- Mark restored outbox rows and async jobs as quarantined.
- Prove no historical messages are sent.
- Run synthetic reads and a new post-restore turn.
Phase 7 — Testing program
7.1 Unit and contract tests
Cover:
- JWT issuer/audience/expiry/key rotation.
- Body user mismatch.
- Gateway mapping authorization.
- RequestContext immutability.
- 29:59 vs 30:00 session boundary.
- Hermes session namespace and turn ordering.
- Context byte caps and UTF-8 truncation.
- Idempotency conflicts.
- Anchor reservation/confirmation/expiration.
- Context revision conflicts.
- Outbox claiming and lease expiration.
429 retry_after.- duplicate provider callbacks.
- deleted-user behavior.
- reminder late-send suppression.
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:
- identity resolution;
- skill selection;
- constraint lookup;
- prompt construction;
- anchor retrieval;
- topic lookup;
- model completion;
- entity extraction;
- profile write;
- ledger write;
- async completion.
Run thousands of interleavings, including exceptions at every barrier.
Inspect:
- prompts;
- retrieved facts;
- ledger content;
- anchors;
- topic cache;
- entity writes;
- profile jobs;
- turn events;
- outbox records;
- final responses.
Pass condition: no foreign canary at any layer.
7.3 Session matrix
Run for both authority types where applicable:
- 29:59 gap: same engine session.
- 30:00 gap: new engine session.
- 30:01 gap: new engine session.
- restart mid-session;
- duplicate delayed update crossing boundary;
- more than 24 messages;
- four-hour active conversation;
- repeated topic in a new session;
- concurrent messages from one user;
- two family members near the boundary;
- out-of-order Hermes turn index;
- Hermes session restart with same external session ID.
Verify one session ID/revision across:
- model history;
- profile input;
- topic cache;
- anchors;
- ledger;
- jobs;
- entity facts;
- writeback.
7.4 Crash-point testing
Inject process termination after:
- ingress row commit;
- model call start;
- model response receipt;
- assistant-message persistence;
- context writes;
- outbox enqueue;
- outbox lease;
- provider acceptance;
- before provider confirmation commit.
For each point, prove:
- no turn disappears;
- no duplicate generation unless explicitly unavoidable and detected;
- no unbounded duplicate sends;
- reconciliation reaches a terminal state;
- operators can identify the exact stranded stage.
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:
- two-parent household;
- separated/custody transition;
- single parent;
- multiple children;
- adult-only family;
- guardian plus limited caregiver;
- medication reminder user;
- multilingual user;
- high-frequency and low-frequency users.
Run compressed multi-day scenarios plus real wall-clock scenarios.
Each simulated family should include:
- onboarding;
- durable fact creation;
- corrections to prior facts;
- explicit hard constraints;
- topic changes;
- unresolved and later-resolved questions;
- assistant follow-ups;
- proactive outreach;
- ignored outreach;
- resumption after more than 30 minutes;
- concurrent users;
- gateway retries;
- provider outage;
- profile evolution;
- deletion/revocation.
Assertions:
- prior assistant replies appear exactly once in history;
- answered questions are not repeatedly re-answered;
- new sessions retrieve durable facts but not stale conversational obligations;
- deliver-once anchors are not repeated improperly;
- stale background outputs are not emitted raw;
- every turn has a terminal ledger state;
- every channel reply has an outbox terminal state;
- no tenant canary crosses families.
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:
- separate synthetic/test family;
- same image and schema as production;
- same public webhook/TLS/proxy path;
- same engine;
- same outbox sender;
- same monitoring;
- test-tagged data, never mixed with real families.
Required demonstration over at least seven days:
- Multi-turn conversation with at least 50 total turns.
- Session continuation within 30 minutes.
- New-session behavior after 30 minutes.
- Follow-up based on an earlier fact.
- Constraint stated and honored on the same turn.
- Anchor delivery and non-repetition.
- Proactive outreach at least once daily.
- User ignores one outreach, then resumes later.
- Outbox retry during an induced Telegram failure.
- Duplicate update replay without duplicate reply.
- Engine restart during active use.
- A pending background result becomes stale and is safely withheld/recomputed.
- Every turn visible in the ledger with provider message IDs.
- 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:
- slow headers/body;
- malformed JSON;
- oversized body;
- chunked requests;
- client disconnect;
- model latency of several minutes;
- burst concurrency;
- DB lock contention;
- file descriptor exhaustion;
- graceful shutdown;
- outbox backlog;
- rate-limit abuse;
- repeated invalid JWTs.
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:
- Protect new behavior with server-side feature flags.
- Cohort by test account/user, never by random code path alone.
- Do not modify live Telegram provider sending until outbox canary passes.
- Keep
DISABLE_BRIDGEoperational and tested. - Do not enable Hermes-heavy traffic during stabilization.
- Avoid schema changes requiring long exclusive locks.
- Use additive migrations first; remove old columns only in a later release.
- Back up before migrations.
- Define rollback behavior for both code and schema.
- Ensure rollback does not cause newer outbox rows to be ignored.
Highest-risk releases:
- JWT key rotation.
- Turn/outbox ownership cutover.
- HTTP server replacement.
/context/writebackenabling 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
- [ ] Unauthenticated
/tokenis removed. - [ ] Vulnerable signing key is retired.
- [ ] JWT issuer, audience, expiry, scope, and subject are enforced.
- [ ] Body/JWT user mismatch returns
403. - [ ] Proxy identity headers cannot be spoofed.
- [ ] Cross-family authorization tests pass for conversations, facts, profiles, ledgers, anchors, jobs, reminders, and context endpoints.
- [ ] Custody/guardian role revocation cancels stale authorization and pending work.
- [ ] Tokens and secrets are absent from logs, images, prompts, and responses.
Request/session correctness
- [ ] Every accepted request has
request_id,turn_id,session_id, andcontext_revision. - [ ] Direct traffic uses the 30-minute engine-gap authority.
- [ ] Hermes traffic uses namespaced Hermes session authority.
- [ ] 29:59/30:00/30:01 tests pass.
- [ ] Request identity no longer relies solely on implicit thread-local fields.
- [ ] Cross-tenant stress testing reports zero leakage.
- [ ] Assistant history, repeated-question, and stale-thread regressions pass.
Durable accountability
- [ ] Every accepted turn has a durable ledger row.
- [ ] No real-user turn remains nonterminal beyond the alert SLA without a page.
- [ ] Generated answers are persisted before delivery.
- [ ] Channel sends use the outbox exclusively.
- [ ] Provider message IDs and attempts are recorded.
- [ ] Duplicate ingress and retries are idempotent.
- [ ] Crash-point tests pass at every lifecycle boundary.
- [ ] Proactive workflows enqueue through the same ledger/outbox.
- [ ] Medication reminders have separate scheduling, authorization, audit, late-send, and acknowledgment semantics.
/context bridge
- [ ]
/contextand/context/writebackrequire scoped authentication. - [ ] They preserve
turn_id, session metadata, and context revision. - [ ] Same-turn constraints bind before context is read.
- [ ] Byte caps and allowlists are enforced.
- [ ] Writeback is idempotent and conflict-aware.
- [ ] Anchor delivery uses reservation then confirmation.
- [ ] Writeback commits before channel send.
- [ ] Direct
/chatremains fully operational and independent.
Server and capacity
- [ ] Bare unbounded
ThreadingHTTPServeris no longer the production boundary. - [ ] Concurrency, queue, body, connection, and deadline limits are enforced.
- [ ] Graceful shutdown drains or releases work safely.
- [ ] Production load test demonstrates at least 2× planned alpha peak.
- [ ] p95/p99 latency, memory, FD, thread, queue, and DB lock thresholds are documented.
- [ ] Load shedding returns explicit retryable errors rather than hanging.
Monitoring and operations
- [ ] Every existing monitor/script has an owner and tested failure path.
- [ ] Restart, OOM, readiness, memory, disk, and FD alerts are active.
- [ ] Expected-instance reconciliation runs every 2 minutes.
- [ ] External synthetics run every 5 minutes.
- [ ] Direct chat, context, writeback, outbox, and Telegram synthetics pass.
- [ ] Funnel reconciliation alerts on missing stages.
- [ ] Logging-pipeline canary proves rows are actually written.
- [ ] Deployment automatically halts/rolls back on critical synthetic failure.
- [ ] On-call alerts require acknowledgment and escalate.
- [ ] A deliberate test alert has reached Bharath/on-call successfully.
Data recovery and deletion
- [ ] Family export works.
- [ ] Family deletion covers raw and derived stores.
- [ ] Pending jobs/sends are cancelled on deletion.
- [ ] Backup restore succeeds in isolation.
- [ ] Restored historical jobs/outbox rows cannot send.
- [ ] Retention rules for child and family data are documented and enforced.
Final validation
- [ ] Longitudinal simulator has run for at least seven continuous days.
- [ ] Bharath’s Telegram test instance has completed the seven-day engagement scenario.
- [ ] No unexplained nonterminal turns remain.
- [ ] No unexplained terminal delivery failures remain.
- [ ] No cross-tenant canary appears anywhere.
- [ ] No critical alert is muted or unowned.
- [ ] The release manifest records commit, lock hash, image digest, config revision, and schema version.
- [ ]
DISABLE_BRIDGEand routing rollback have been tested, not merely documented.
Only after this bar is met should alpha enrollment expand or medication reminders be enabled for real families.