(MOT-4150) fix(harness,context-manager,shell): context-overflow accounting, resume budget, and loud child failures - #552
Conversation
The chars/4 estimator counted the full serialized FunctionResult message, but no provider wire adapter ever sends `details` (content text/images only; the denied envelope is the one exception and stays counted). Fat tool results — a file read carries the whole file in BOTH content and details — were double-billed, halving the effective usable window. Observed live (session scan-x7k2-c11-l2): 22 file reads inflated an ~88k wire request to 148602 estimated > 148000 usable and the turn died terminally in a 200k-window model.
…embly select() legally returns a whole-head selection (tail_turns 0, or a view with no user turn — e.g. a harness candidate window opened at a prior compaction's assistant-boundary tail_start), and try_compact only guarded against an empty head. Compaction then summarized everything, split_off emptied the messages, and assemble returned an empty model-facing context — which providers hard-reject and the harness turns into a terminal context_overflow. Skip compaction whenever it would consume the whole working set; emergency reduction (replaces-but-never-removes) does the shrinking instead. The guard lives in assemble, not select(), because context::compact legitimately summarizes whole histories. Observed live (session scan-batch-4-7x2k): second compaction on a user-less window killed the turn with "context::assemble returned an empty model-facing context". BDD scenario fails without the guard.
Two turn-loop gaps observed live on 2026-07-21, one commit because the changes share turn_loop.rs hunks: Re-assembly deficit vs believed size: the one-shot re-assembly folded final - usable into the reservation, but post-assembly additions (the fp::inject-guidance prompt append is ~2.5k tokens vs the 256 allowance) can dwarf that overshoot. Assembly stayed under its own ceiling, rebuilt the identical request, and the turn died twice on the same count (148602 > 148000, session scan-x7k2-c11-l2). Measure the deficit against assembled.token_count — everything the request grew beyond what assembly believed — via reassembly_deficit(). Loud child failures: a fire-and-forget spawn settles its call Done at spawn time and the parent turn has usually completed, so resolve_parent(failed) no-oped at both resolve() gates and a dead child was structurally silent — it can never write the state keys its spawner watches, and turn-completed events reach only registered subscriptions (session console-42ae032b: two dead scanners, coordinator waited forever). resolve_parent now reports delivery; when unconsumed, finalize_failed sends an idempotent [child-failure] message to the spawner session (model/provider inherited, loop-safe: send-created turns carry no ParentLink). The spawn result text documents the automatic failure delivery.
Provider overloads (Anthropic 529s) cluster in bursts, and only mid-stream errors burn this budget — pre-stream errors already get llm-router backoff retries. A budget of 1 killed a turn one generation step from done when a second overload landed after the first resume (session dcmcp-scan-p6w4-c-aq: both findings INSERTed, bookkeeping never ran). Config and per-turn serde defaults stay in lockstep.
shell::fs::read returns a ContentRef handle, not file text, but the coder::read-file hint sat at the docstring tail where weak models miss it — a haiku scanner read 22 files through it, got useless stubs, and re-read everything via coder::read-file, doubling round-trips on the way to a context-overflow death. State the handle-not-text nature and the text-file redirect in the first two sentences.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
skill-check — worker0 verified, 47 skipped (no docs/).
Four for four. Nicely done. |
📝 WalkthroughWalkthroughThe PR adjusts context token accounting and compaction safeguards, improves child-failure propagation and reassembly recovery, raises transient resume defaults, and clarifies the shell file-read response description. ChangesContext management
Harness reliability
Shell API documentation
Estimated code review effort: 4 (Complex) | ~45 minutes Sequence Diagram(s)sequenceDiagram
participant ChildTurn
participant resolve_parent
participant notify_parent_of_child_failure
participant ParentSession
ChildTurn->>resolve_parent: finalize failed turn
resolve_parent-->>ChildTurn: resolved false
ChildTurn->>notify_parent_of_child_failure: child failure details
notify_parent_of_child_failure->>ParentSession: send [child-failure] notice
Possibly related PRs
Suggested reviewers: Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Four live incidents from parallel-scan subagent runs on 2026-07-21, each traced to a distinct harness-stack defect. All four are fixed here; each carries a regression test that fails without its fix.
1. Phantom token accounting (
context-manager/src/core/estimate.rs)The chars/4 estimator counted the full serialized
FunctionResultincludingdetails, but no provider wire adapter ever sendsdetails(content text/images only; the denied envelope is the exception and stays counted). File reads carry the whole file in BOTHcontentanddetails, so tool-heavy sessions were double-billed and lost ~half their usable window.Incident: session
scan-x7k2-c11-l2— 22 file reads, ~88k real wire request, estimated 148,602 > 148,000 usable in a 200k-window model, terminalharness.context_overflow.2. Futile re-assembly (
harness/src/turn_loop.rs)The one-shot re-assembly folded
final − usableinto the reservation, but post-assembly additions (thefp::inject-guidanceprompt append is ~2.5k tokens vs the 256-token allowance) can dwarf that overshoot: assembly stayed under its own ceiling, rebuilt the identical request, and the turn died twice on the same count. The fold now measuresfinal − assembled.token_count— everything the request grew beyond what assembly believed (reassembly_deficit()).3. Transient resume budget of 1 (
harness/src/config.rs,harness/src/types/turn.rs)Anthropic 529s cluster in bursts and only mid-stream errors burn harness resumes (pre-stream errors already get llm-router backoff retries). Budget 1 killed a turn one generation step from done when a second overload landed after the first resume.
Incident: session
dcmcp-scan-p6w4-c-aq— both findings INSERTed (rows 292/293), the final bookkeeping calls never ran. Default is now 3; config and per-turn serde defaults stay in lockstep.4a. Empty-context compaction (
context-manager/src/functions/assemble.rs)select()legally returns a whole-head selection for a view with no user turn (a harness candidate window opened at a prior compaction's assistant-boundarytail_start) ortail_turns == 0, andtry_compactonly guarded the head. Compaction then summarized the entire working set and assemble returned an empty model-facing context — providers hard-reject that, and the harness fails the turn terminally. The guard skips compaction whenever it would consume the whole working set; emergency reduction (replaces-but-never-removes) shrinks instead. Guard lives in assemble, notselect(), becausecontext::compactlegitimately summarizes whole histories.Incident: session
scan-batch-4-7x2k— "context::assemble returned an empty model-facing context" after a second compaction on a user-less window. New BDD scenario fails 84/85 without the guard.4b. Structurally silent child failures (
harness/src/deferred.rs,harness/src/turn_loop.rs,harness/src/subagent.rs)A fire-and-forget spawn settles its call
Doneat spawn time and the parent turn has usually completed, soresolve_parent(failed)no-oped at bothresolve()gates — and a dead child can never write the state keys its spawner watches, while turn-completed events reach only registered subscriptions. Child death was silent by construction.Incident: session
console-42ae032b— two spawned scanners died instantly; the coordinator waited forever on success-path state triggers.resolve_parentnow reports delivery; when unconsumed,finalize_failedsends an idempotent[child-failure]message to the spawner session (model/provider inherited; loop-safe — send-created turns carry no ParentLink; react-spawned children unaffected). The spawn result text documents the automatic failure delivery.Also
shell::fs::readdocs now lead with "returns a ContentRef handle, NOT file text — use coder::read-file for text": the tail-position hint caused a haiku scanner to double-read all 22 files (incident 1's trigger).Tests
Deploy note
These crates ship as long-running workers — rebuild + restart
context-manager,harness, andshellfor the fixes to take effect.Summary by CodeRabbit
Bug Fixes
Improvements
Fixes MOT-4150