Repository navigation
feat(server): direct child threads resume on their own after a usage limit - #72
Conversation
Thread transfer impact✅ Thread transfer remains within every enforced ceiling.
Baseline: Scenario and decoded snapshot size10 historical turns, 5 command tools per turn, 878.9 KiB retained MCP result per historical turn, and a 1.05 MiB retained result in the measured turn.
Updated in place by a trusted workflow. PR artifacts are strictly validated and never executed. |
|
Merge plan (user approved 2026-10-01): after the #92 stack (#101 merged, #102 and #103 in review) lands, rebase this onto main (it now conflicts), rerun CI, get an independent review on the exact head, then squash-merge and stage the build. The check that tells Model Router job threads apart from direct spawns stays until #97 removes the runner hooks; #97 deletes it here too. |
52da693 to
dc908d2
Compare
|
Independent review of dc908d2: request changes
|
…limit (#71) A spawn_thread child that stopped on a usage limit waited until the user typed "continue" to the planner and the planner relayed it: #63 skipped every sub.* thread to leave Prism jobs to Model Router. The resume watcher now skips only Prism job threads, recognized by Model Router's "[model-router job ...]" first user message (titles get regenerated, so they are no marker). A resume fires at the earlier of the displayed reset plus a minute, or the first reply any thread gets from the same provider instance after the settle delay: on 2026-09-29 the displayed reset was 2.5 hours later than when requests worked again. A thread whose early resume fails again waits for the reset, so a per-model limit cannot turn replies elsewhere into a retry loop. A resumed child's parent gets one line saying so. Done by Claude Opus 5.5 in Claude Code. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A dispatcher interrupts a limit-hit child before replacing it, but that child is already failed, so interrupt_thread had nothing to stop while the pending automatic continue would still restart it next to its replacement. interrupt_thread now cancels that pending continue and marks the stopped turn so a later limit error for it schedules none. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…atching (#71) Only a turn whose own error reads as a usage limit schedules a continue; an unrelated runtime error while a usage window is exhausted is left alone. An exhausted window whose reset time already passed no longer ends the watch: it keeps waiting for a reply on the instance and re-reads the windows every 5 minutes, resuming at a future reset or once none is exhausted, so a stale reading cannot turn into a continue loop. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
0641c6f to
b201a83
Compare
|
Independent review of b201a83: request changes
The new error-text gate matches the current Claude, Codex, and Grok adapter messages. OpenCode passes through its server-provided |
) At a future reset plus the margin the watcher resumed without reading the usage windows again, so a window still at 100 % got a continue anyway. It now re-reads them and resumes only when none is exhausted; otherwise it keeps re-reading every 5 minutes, and a reply on the instance can still resume it early. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
|
Independent review of 5bc0e4c: approve |
What: A
spawn_threadchild that stops on a usage limit now gets one automatic "continue" once the limit lifts, and its parent gets one line saying so. Prism jobs are still left to Model Router.Why: #63 skipped every
sub.*thread, so a direct child waited for the user to relay "continue". The displayedresetsAtcan also be hours later than when requests work again.So what: The planner gets an independent review of the exact head and merges (the user approved the merge once CI and review pass).
Closes #71.
Rebased onto main after #101-#103 (2026-10-01)
handlers.ts: kept both imports (Deferred,Duration), and added the reply-waiter step to the bridge loop fix(server): report-back reaches the parent exactly once across a restart (#94) #103 introduced (subscribe, restart catch-up, then drain). Every event still passes through bridge, usage-limit resume, then reply waiters, in that order. The rest of feat(server): direct child threads resume on their own after a usage limit #72 merged unchanged: the resume watcher, the Model Router job-thread check (it stays until Remove the Model Router runner hooks from Chromeria #97) and the parent notice.docs/fork-features.md: kept Report-back reaches the parent exactly once across a server restart #94/interrupt_thread: stop a thread's running turn #95 and added Resume direct-spawn child threads after a usage limit #71 to the child-threads entry. The Issue link now points attoolboxmd/chromeria/issues/71, where Resume direct-spawn child threads after a usage limit #71 lives.childUsageLimitResume.test.tssits next tohandlers.testFixtures.ts.interrupt_threadcancels a pending auto-continue. A dispatcher interrupts a limit-hit child before replacing it (that is what the tool description tells it to do). That child is already failed, so there is no turn to stop, but the pending continue would still have restarted it next to its replacement.interrupt_threadnow interrupts that pending resume and marks the stopped turn, so a later limit error for that turn schedules none. The tool description says so. A running child thatinterrupt_threadstops endsinterrupted, noterror, so the watcher never sees it, and the marked turn covers a provider that reports an error for it anyway.dc908d278:lastErrorreads as a usage limit (isUsageLimitError: the adapters' "Claude usage limit reached", "Claude stopped: a usage limit blocked the request", "Codex usage limit reached", "Grok usage limit reached"). The exhausted-window check still applies on top. An unrelated runtime error while a window is exhausted schedules nothing.b201a83b4). At a future reset plus the margin, the watcher re-reads the usage windows and resumes only when none is exhausted. A window still at 100 % is re-read every 5 minutes, and a reply on the instance can still resume it early when allowed.resetsAtno longer ends the watch. An exhausted window whose reset time has already passed is a stale reading. The watcher keeps waiting for a reply on the instance (when an early resume is allowed) and re-reads the windows every 5 minutes (RESET_RECHECK_MS). It resumes at a future reset or once no window is exhausted, never while the limit still shows, so a stale reading cannot loop. A provider with no exhausted window at all is stillnot-limited.How direct children are told apart from Prism jobs
Model Router opens every job thread with a first user message
[model-router job <id> <kind> on route <route>; planner thread <id>]. The web Agents panel already groups "Prism spawns" by this tag (apps/web/src/components/AgentThreadTree.logic.ts). The server now reads the same tag (isRouterJobMessage,usageLimitResume.ts).I checked the live DB read-only on 2026-09-29: 42
sub.*threads carry the tag. None of them still has amodel-router …title, because titles get regenerated. So the title regex thatstaleTurnMonitor.tsuses would match no live job, and it can't be the marker. Children spawned by a planner with a role (for example…reviewer-…) are directspawn_threadchildren, so they get resumed.Behaviour
resetsAtplus 1 minute, or (b) the first finished assistant reply that any thread gets from the same provider instance after the 5 s settle delay. The settle delay filters out replies that were already in flight when the limit hit.resetsAtaffects top-level threads the same way.[Subagent <title> (thread <id>) resumed automatically: …]message per resume.Proof
Node v24.13.1, at head
5bc0e4c12(onorigin/main5f2dae583).usageLimitResume.test.tsandchildUsageLimitResume.test.ts:Tests 17 passed (17), three runs in a row.5bc0e4c12:sends nothing at the reset while the window still shows exhausted. The window keeps showing 100 % past its reset; after three rechecks there is still no continue. Failing-before: with the watcher fromb201a83b4it fails withexpected [ { threadId: 'thread-user', …(2) } ] to deeply equal [](it resumed at the reset anyway).resumes once, a minute after the limit resetsand the spent-early-resume case now have the window drop at its reset and resume at 21 minutes after re-reading it. The engine-level stub shows the window lifted after its reset, so the continued-child test resumes on a fresh reading.b201a83b4:never continues a turn whose error is not a usage limit, even while a window is exhausted(real engine): the child fails with "Provider process exited with code 1." while the session window is exhausted; after a reply on the instance and four hours, no continue and no parent notice. Failing-before: without the error check it fails withexpected [ Array(1) ] to deeply equal [].keeps watching and resumes at the next reply on the instance(exhausted window with a reset 10 minutes in the past): resumesliftedat the 3-minute reply. Failing-before: with the old watcher it fails withexpected 'not-limited' to equal 'lifted'.sends nothing while the window still shows exhausted, then resumes once it lifts(early resume spent): nothing is sent for two hours of replies elsewhere; once the windows stop being exhausted, it resumes once at the next recheck. Failing-before:expected 'not-limited' to equal 'reset'.isUsageLimitErroraccepts the four adapter messages and rejects other errors.expected [ Array(1) ] to deeply equal []); a continued child's reply is reported once, also after a restart; a direct child resumes at the first reply on its instance and the Prism job is left alone.vp test run src/mcp/ src/orchestration/Layers/ProviderCommandReactor.test.ts src/server.test.ts src/prism(apps/server):Test Files 28 passed (28),Tests 472 passed (472)tsc --noEmit(apps/server): exit 0.vp fmt --checkandvp lint --report-unused-disable-directiveson the changed files: clean.vp run knip:check: clean.bash scripts/fork-check.sh --upstream upstream:fork-features: OK (19 features).fork-check: OK.Not done: no resume was observed on the running app, because it was not restarted or redeployed.
Elon record
Requirements and who asked: The user (2026-09-29, #71) never wants to relay "continue" to a direct worker after a limit. The planner (2026-10-01): an interrupted child must not be auto-continued, there must be no duplicate report after a continue, and (independent review) only usage-limit errors are continued, a past reset time must not end the watch, and a reset resumes only once the window shows it lifted. Resume at the earlier of
resetsAt+ about 1 minute or the first success on the same provider instance (user's brief). Prism jobs stay with Model Router (#61).Deleted: The title-based job marker (it matches no live job). A new layer,
server.tsedit, contracts change, UI, settings and restart persistence. A separate code path for top-level threads. Per-model matching for the early trigger: the loop guard covers per-model limits without it.Bottleneck: After a limit, a direct child idles until a human notices and relays "continue". The displayed reset (15:40Z) was 2.5 h behind the first working request on the same instance (13:10:41Z).
Checked myself: Read the adapters' usage-limit error texts (
ClaudeAdapter.ts,codexUsageLimits.ts,XAiAcpExtension.ts) for the classifier; reverted each review fix to see its test fail. After the rebase, reproduced the interrupt-then-auto-continue double writer as a test (red without the cancel, green with it) and the continue-then-report path across a restart. Earlier: live DB (read-only): the child…01b1a94bbcfdfailed at 13:10:29.552Z onclaudeAgent/claude-opus-5-5. Its parent on the same instance and model finished replies at 13:10:30Z and 13:10:41Z. The first user message tag marks 42 of 42 router threads, and the title marks 0. I ran the tests, the mutation checks, the typecheck, lint andfork-check.shshown above.Done by Claude Opus 5.5 in Claude Code.
🤖 Generated with Claude Code