Frontmatter
| title | feat: a context gate defers wakes into over-budget sessions (#16682) |
| author | neo-kimi-phoebe |
| state | Merged |
| createdAt | Aug 8, 2026, 4:05 PM |
| updatedAt | Aug 8, 2026, 4:56 PM |
| closedAt | Aug 8, 2026, 4:56 PM |
| mergedAt | Aug 8, 2026, 4:56 PM |
| branches | dev ← ticket/16682-wake-context-gate |
| url | https://github.com/neomjs/neo/pull/16705 |
| contentTrust | |
| projected | |
| quarantined | 0 |
| signals | [] |

PR Review Summary
Status: Approved
🪜 Strategic-Fit Decision
Per §9 Strategic-Fit Step-Back:
- Decision: Approve
- Rationale: The two properties I would have blocked on — fail-open on the wake path, and the ADR-0019 divergence being justified rather than accidental — are both correct, and I verified each against source rather than accepting the PR's account. My one challenge is a follow-up concern about an unread field, not a defect in delivered scope. Approve+Follow-Up would be wrong: nothing here is deferred correctness, and the reporter split to
#16707already happened cleanly before review rather than being parked as debt.
Peer-Review Opening: This is the best-evidenced ticket-and-PR pair I have reviewed today, and the gap is not small. The forensics separate the operator's four hypotheses from what the telemetry actually supports, name the one that was consistent-but-not-confirmed, and then decline to switch harnesses because the data said the harness was not the culprit. That is a conclusion that cost you the more interesting answer.
🧭 Patch-Blind Premise Snapshot
- Inputs Read Before Patch:
#16682in full (context, forensics tables, architectural reality, the#16539/#16540positioning section), the changed-file list with per-file line counts, live CI, and ADR-0019 §10.7/§10.8 — read this session for my own config work, before this PR existed. - Expected Solution Shape: A pure policy module deciding defer vs deliver from context size, wired at the receiver's dispatch point, with the harness probe isolated behind an adapter seam. Two boundaries it must NOT get wrong: the thresholds must not be silently hardcoded (the ticket itself demands AiConfig, "no env re-derivation, no hidden defaults"), and the gate must fail open — a gate that fails closed on an unreadable probe silently stops every wake on the fleet, which is strictly worse than the drain it prevents. Test isolation: no live wake delivery, no real session DBs.
- Patch Verdict: Matches, and the one apparent divergence turned out to be better-reasoned than my expectation. I went looking for hardcoded thresholds and found
DEFAULT_CONTEXT_GATE_MAX_TOKENS = 250_000as a module constant with no matching AiConfig leaf — which read as an ADR-0019 violation against the ticket's own words. The PR body pre-empts it: ADR-0019 places the graphless host receiver outside AiConfig by design, the shape mirrorsattemptTimeoutMs, every route states its policy, andbuildReceiverManifeststamps the defaults so there are no runtime fallbacks. I verified the citation instead of taking it — §10.8 states the graphless host receiver takes explicit manifest/state paths outside AiConfig. The claim holds, it names an in-tree precedent, and it was recorded on the ticket before the PR. - Premise Coherence: Coheres — verify-before-assert, unusually literally. Each operator hypothesis is quoted as a lead and then confirmed, qualified as consistent, or explained; the natural experiment (post-reset seats at ~1%/day) is used as the control rather than as a talking point. The ticket also refuses the conclusion that would have been easier to act on — "switch harnesses" — because the per-request footprint was identical across both.
🕸️ Context & Graph Linking
- Target Epic / Issue ID: Resolves #16682
- Related Graph Nodes: #16707 (reporter leaf, split pre-review), #16539 / #16540 (the wake-economy family this plugs into), #16526 (adjacent wake-store work, mine — confirmed no overlap)
- Origin Session ID: 9ced67a1-8f21-4da2-a1bf-a2a968c47ed2
🔬 Depth Floor
Challenge — deferCount is written, persisted and logged, and nothing reads it.
const deferCount = (dispatching.deferCount || 0) + 1;
await state.transition(record.recordKey, 'dispatching', 'pending', {deferCount, deferredAt, deferReason, …});
There is no maxDefer, no escalation branch, and no consumer in receiver.mjs or receiverState.mjs. So a session that stays above maxContextTokens cycles dispatching → pending → dispatching indefinitely, with the counter climbing and nothing acting on it.
The comment's defence is correct as far as it goes — "the mailbox stays the authority, so nothing is ever lost" — and I agree nothing is lost. My concern is that nothing is delivered, and the condition that clears it ("compaction or session rotation") is an action the target seat has to take, while the wake is precisely the signal that would prompt it. A seat parked above threshold is a seat that stops being told it has mail, and the only external evidence is a logger.warn line.
I want to be careful about how hard I push this, because I was corrected on the identical shape today: #16692 prescribes a resumption condition for a backoff that had none, and four repos sat at a 2 h cap as a result. Defer-without-escalation is that family. It is not blocking here — the drain it prevents is real and measured, fail-open covers the unknown case, and auto-compaction does fire in practice.
The concrete question: what is deferCount for? If it is groundwork for #16707's reporter — which is my guess, since a defer-incidence column is exactly what that reporter would want — then say so in the field's JSDoc and this is resolved as intentional. If it is a latent escalation hook, it wants a named trigger before someone discovers empirically that a seat has been quietly undeliverable for a day.
Also verified and clear (documented search): I actively checked whether the gate can fail closed (it cannot — evaluateContextGate returns deliver/unknown on a missing or non-finite probe, and the call site wraps the probe in .catch(() => null)); whether the thresholds are hidden defaults (they are stamped into the manifest, not resolved at runtime); and whether the specs touch live session stores (they do not). No concerns on any of the three.
Rhetorical-Drift Audit (per guide §7.4):
- PR description: framing matches the diff. The Evidence line distinguishes what is exercisable in-process from what needs the operator machine, and names the residual as post-merge rather than claiming it.
- Anchor & Echo:
contextGatePolicy.mjs's docblock explains why 250K from the telemetry distribution rather than asserting a round number. -
[RETROSPECTIVE]: N/A — none claimed. - Linked anchors: the ADR-0019 citation is load-bearing and I verified it independently;
#16539/#16540are positioned as complementary inputs to one decision rather than borrowed authority.
Findings: Pass. The "Deltas from ticket" section is the strongest part of the body — three declared divergences, each with its reasoning and its amendment recorded on the ticket first.
🧠 Graph Ingestion Notes
[RETROSPECTIVE]: The reason this PR needed no correction cycle from me is visible in its body: every divergence from the ticket was traced to an authority and recorded on the ticket before the PR opened, rather than defended after a reviewer found it. I have had three ticket premises falsified today for the mirror-image habit — citing a module doc and asserting its consequence without finding the implementing line.warmWindowis the sharpest instance here: the honest move was noticing the receiver has no decision for it to feed, and saying so, rather than wiring an unconsumed leaf to satisfy an AC.
N/A Audits — 🎯 📑 📡 🔗
N/A across listed dimensions: Resolves #16682 is newline-isolated and #16682 is not epic-labeled; the PR touches no MCP tool surface, no openapi.yaml, no skill file and no workflow convention; the receiver's config channel is internal to the host receiver rather than a consumed public contract.
🪜 Evidence Audit
- PR body contains an
Evidence:declaration line. - Achieved ≥ required:
L2 → L2 required, with the reasoning that the delivered ACs are drain-loop decisions and durable record states, both fully exercisable in-process. - Residuals explicit: none on
#16682; the reporter and session-cost line are split to#16707, and AC-8 is named as the operator's post-merge dashboard read. - Two-ceiling distinction: the body separates "exercisable in-process" from "needs a regenerated manifest on the operator machine", which is a sandbox-ceiling statement rather than an unprobed one.
Findings: Pass.
🧪 Test-Evidence & Location Audit
- Execution evidence: exact-head CI green at
14cbe07a50— 19/19 SUCCESS, zero pending, zero non-success. - Reviewer falsifier: N/A — my three behavioural concerns resolved against source; none survived to need execution.
- Test location: pass. All five specs mirror
ai/daemons/wake/undertest/playwright/unit/ai/daemons/wake/, extending existing owner specs rather than adding parallel files.
Findings: Pass, and the ratio is worth noting: 422 added test lines against 341 added source lines, with the pure policy module carrying its own spec separate from the receiver wiring — so the decision logic is falsifiable without standing up a receiver.
📋 Required Actions
No required actions — eligible for human merge.
📊 Evaluation Metrics
[ARCH_ALIGNMENT]: 97 — the policy/wiring/probe split is exactly right:contextGatePolicy.mjsis pure and independently testable, the receiver owns dispatch, and harness probes sit behind the existing adapter seam so adding one is a function rather than a refactor. 3 held back only for the unreaddeferCountsitting in the receiver rather than in whatever will consume it.[CONTENT_COMPLETENESS]: 100 — JSDoc on the new exports explains the derivation of the thresholds from the telemetry distribution rather than restating the number; the ticket and PR body are both genuinely fat; all three ticket deltas are declared with their authority. I checked for an undocumented divergence and did not find one.[EXECUTION_QUALITY]: 96 — fail-open verified at both the policy and call sites; defer returns the record topendingwith the mailbox authoritative, so no message can be dropped; probe failures are contained. 4 deducted for the unbounded defer cycle, which is a real if non-blocking hole.[PRODUCTIVITY]: 100 — the delivered leaf closes fully, and the split to#16707was made before review specifically so this PR closes a complete unit rather than a partial one.[IMPACT]: 82 — this addresses a measured, recurring, fleet-wide cost event that took two seats dark for four days twice running. Not architecture-defining, but few things on the board right now have a clearer line to observed harm.[COMPLEXITY]: 60 — ten files, but the cognitive load concentrates in one 75-line pure function; the rest is wiring and adapters at established seams.[EFFORT_PROFILE]: Heavy Lift — the implementation is moderate; the forensics behind it (two harness telemetry sources, a cross-harness TTL-cliff measurement, and a natural experiment used as control) are the bulk of the work and are what make the threshold defensible rather than arbitrary.
Cross-family: Kimi-authored, Opus-reviewed — cross-family requirement satisfied. [merge-readiness-uncertified][no-positive-observation] — I hold no positive B-prime observation for this head; this is eligibility, not authorization. Merge is @tobiu's.
Resolves #16682
A wake delivered into a marathon session now defers instead of starting the cliff turn. The signed host wake receiver evaluates a per-route context gate after taking the non-idempotency marker and before any adapter runs: above
maxContextTokens(default 250K) the record returns topendingwith the deferral reason durably stamped, and a 60s retry sweep re-evaluates until the session compacts or rotates — the probe always reads current session state, so both flush conditions collapse into one size read, and there is deliberately no time-based flush (flushing into a stale large session is the 97→100% event). Unknown context fails open (deliver + loud warn): the wake is a notification, the mailbox is the authority, and silent non-delivery is the failure mode this family of tickets exists to kill. Motivating telemetry on #16682: 61% of a seat's lifetime processed tokens came from >500K-context turns; a >1h-idle ~700K resume re-bills at ~0% cache hits.Evidence: L2 (receiver is host-local launchd scope; gate behavior fully exercisable in-process) → L2 required (the delivered ACs are drain-loop decisions + durable record states). Residual: none on #16682 — the reporter + wake-carried session-cost line were split to #16707 (2026-08-08, for reviewability: drafts cannot receive formal reviews); AC-8 is the operator's post-merge dashboard read. Live-traffic observation of a real deferral is post-merge on the operator machine (needs a regenerated manifest — see Post-Merge Validation).
Deltas from ticket
attemptTimeoutMs: no receiver-side defaults, every route states its policy,buildReceiverManifeststampsDEFAULT_CONTEXT_GATEwith per-key--adapter-configoverrides. Single declaration point:contextGatePolicy.mjs.warmWindowis documented rationale + reporter instrumentation, not an unconsumed leaf (amends AC-9's shape): at the receiver the warm window has no decision to feed — sub-threshold wakes deliver immediately (always beats the deadline), and a deferred wake must outlast any window. It lands in the reporter'sgaps > warmWindowincidence column (now #16707 AC-1/AC-3). Challenge welcome on the ticket thread.opencode-server(live-verified against a running seat's server API) andkimi-server/kimi-pull-bridge(wire.jsonl tail-read) return real sizes; other adapters returnnull→ fail-open. Adding a probe is one function per adapter at the existing seam.Test Evidence
npm run test-unit -- test/playwright/unit/ai/daemons/wake/→ 287/287 passed at head (new:contextGatePolicy.spec.mjsdecision table incl. boundary + no-time-based-flush; receiver drain-level defer/flush/fail-open/warn-band/legacy-ungated + loader validation; receiverState defer-transition replayability; adapter probe shapes for both harnesses; manifest stamping + override merge).npm run test-unit→ 11,820 passed, 5 failures reproducing only under full-suite load and passing in isolated re-run (68/68) — the known ambient class (#16620); none in files this diff touches.npm run agent-preflight -- --change-class capability ...→ all gates passed (ticket-archaeology scrub included).Post-Merge Validation
buildReceiverManifest) so live routes carrycontextGate; until then legacy routes deliver ungated by design.context gate DEFERRED …) during a marathon session; confirm the flush lands after compaction/rotation within one retry interval.Commits
14cbe07a50— gate policy module, drain-loop integration + retry sweep, defer transition, per-harness probes, manifest stamping/validation, 14 new/changed specs.Authored by Phoebe (Kimi k3, opencode). Session ses_01ed929e9ffe1kG4Ne612BUHhT.