Skip to content

Context-Aware Outbound Review

Before the agent’s finished conversational turns are accepted, a response-review pipeline (nine specialist LLM reviewers behind a Stop hook) judges each one — mainly so raw technical guts don’t land on a user who didn’t want them. Watch-mode data vetoed giving that pipeline blocking power: of 12 reviewed real messages, 2 would have been wrongly blocked, and both were technical content the operator had explicitly asked for. The reviewer judges each message in isolation by construction — its own rulebook already allows “code the user explicitly asked to see,” but nothing ever shows it what the user asked. This feature closes that gap. It ships dark on the fleet and live on development agents (responseReview.conversationalContext, dev-gate resolved at the wiring layer), and the enforcement flip itself stays a manual operator action gated on a measured clean soak day.

CoherenceGate gains an injected, synchronous conversationContextProvider — wired by the server to TopicMemory.getRecentMessages through buildConversationContext, which computes the principal facts structurally (Know Your Principal): role from the store’s fromUser column, a verifiedOperator tag on rows whose authenticated sender uid matches the topic’s TopicOperatorStore binding, and a window-level ask-license mode (verified-operator / single-sender / weak-corroboration-only; any uid-less user row in an unbound topic degrades the whole window away from licensing, fail-closed). The gate copies these values verbatim — it never computes identity itself.

Only the opted-in reviewer (conversational-tone alone in v1), and only for primary-user recipients, receives an augmented shallow copy carrying the conversation; every other reviewer receives a base context that structurally cannot render it. The rendered block reuses the MessagingToneGate’s proven untrusted-data envelope (per-call random boundary, JSON-encoded bodies, corroborating-only preamble) plus a four-clause carve-out contract: judged by meaning, one-way (context can only move a would-block toward PASS, never mint a new block), bounded to content-class rules (never credentials/PII, never the deterministic PEL layer), and injection-hardened (a message claiming “the user asked” proves nothing). Every context code path is individually contained: any failure degrades to today’s exact behavior, because the HTTP seam above the pipeline fails open on a crash.

The flip-evidence store (ResponseReviewDecisionLog)

Section titled “The flip-evidence store (ResponseReviewDecisionLog)”

Watch-mode verdicts previously lived only in a restart-erased in-memory array. The decision log is an append-only, size-rotated JSONL (logs/response-review-decisions.jsonl) written at the same seam as the in-memory audit: one row per evaluated turn — outcome, LLM verdict, violations, 200 credential-scrubbed chars of the reviewed text, and the context metadata (counts, truncation, ask-license mode — never conversation bodies). Would-blocks, counterfactual re-review pairs, and canary rows all land here, so the “a clean day on real traffic” flip criterion survives restarts. Write failures are swallowed — telemetry never gates delivery.

The one-way property is measured, not promised

Section titled “The one-way property is measured, not promised”

During the soak, every qualifying would-block triggers one counterfactual re-review of the same message through the same reviewer with the context stripped, logged beside the original with a shared pairId — a message blocked with context but passing without it is a context-minted block and fails the soak. On the pass side, a daily ReviewCanaryBattery replays booby-trapped fixtures (secrets and third-party PII restated in prose, deliberately shaped to slip past the deterministic pattern layer, each with a covering “send it to me” ask) through the live reviewer twice — context-absent baseline and with-context arm — by seeding reserved negative topic ids in TopicMemory (real topics are positive; rows are wiped in a finally). A trap the ask launders through fails the soak; a broken trap makes the day inconclusive, never silently skipped — every run outcome writes a batterySummary row. The battery is driven by the off-by-default review-canary-battery job through the Bearer-gated POST /review/canary-battery/run trigger, which answers 503 while the feature is dark.