Repository navigation
[Bug]: Usage-limited Claude thread is never armed for auto-resume when background-task wake runs finish as completed right after the limit stop #16815
Description
Before submitting
- I searched existing issues and did not find a duplicate.
- I included enough detail to reproduce or investigate the problem.
Related but different: #15665 (a follow-up turn fails with resetAt: null) and #16233 (a queued background notification blocks snooze). Here the follow-up turns are not failures at all: they finish as completed, so the thread is never armed for auto-resume.
Filed by the user's Claude Code agent at their request. Everything below is sanitized: no conversation content, project names, paths or account details. Thread labels A and B are arbitrary.
Area
apps/server
Summary
With Auto-resume limited threads and Snooze limited threads both on, a Claude thread that hit its five-hour limit while background subagents were running was never scheduled to resume. Its limited run stored a valid future reset, but about 2 seconds later a background-task notification started a new root run that finished as completed in under 2 seconds. Five more such runs followed. The thread's latest executed run was therefore completed, limitRecovery stayed null, no limit-resume: message was ever dispatched, and the thread sat idle for 4 h 42 m after the reset until the user sent a message.
A second Claude thread on the same account, where no further run followed the limited one, hit the same limit window about 73 seconds later. It was armed and resumed 1 second after the reset.
Steps to reproduce
Observed incident on an unmodified official nightly. Not a minimized reproduction, and no fresh quota-consuming run was made.
- Settings:
autoResumeLimitedThreads: true,snoozeLimitedThreads: true. - In a Claude (
claudeAgent, Orchestrator V2) thread, start several background subagents (11 in this incident) and let the root turn end normally while they work. - Exhaust the five-hour usage window. A background-task notification starts a root run that fails with
usage_limit. - Other background subagents die on the same limit. Each one's completion notification starts another root run within seconds.
- Wait for the reported reset time.
The reproduction in #15665 (real CLI against a mock Messages API) should be adaptable: after the first limited turn, let a background task complete so its wake turn starts within a few seconds.
Expected behavior
The limited run is armed for recovery and the thread resumes at the reset time, exactly as the comparison thread did. A wake turn that makes no progress because the account is limited should not cancel or hide a pending recovery.
Actual behavior
Thread A, read-only from the V2 projection tables. t0 is the moment the limited run failed. R is the stored reset, t0 + 4 h 20 m.
| Run | Started by | Ended | Ran for | Status | Turn items |
|---|---|---|---|---|---|
| 2 | background-task notification | t0 |
30.5 s | failed |
..., assistant_message (the provider's limit text), error with failure.class: usage_limit, failure.code: api_error_429, failure.resetAt: R |
| 3 | background-task notification | t0 + 2.1 s |
1.8 s | completed |
notification, checkpoint |
| 4 | background-task notification | t0 + 6.8 s |
0.6 s | completed |
notification, checkpoint |
| 5 | background-task notification | t0 + 9.8 s |
0.8 s | completed |
notification, checkpoint |
| 6 | background-task notification | t0 + 34.3 s |
0.7 s | completed |
notification, system_notice, checkpoint |
| 7 | background-task notification | t0 + 41.0 s |
1.3 s | completed |
notification, assistant_message (61 characters: You've hit your session limit · resets 3am (America/New_York)), checkpoint |
| 8 | background-task notification | t0 + 5 m 14 s |
0.6 s | completed |
notification, system_notice, checkpoint |
- Thread A
limitRecovery:null. It was never armed. - Thread A turn items matching
limit-resume: none. - Thread A next activity: a user message at
R + 4 h 42 m. - Runs 3 to 8 carry no
erroritem. Run 7 stores the provider's limit message as an ordinary completed assistant message.
Thread B, same account and same limit window, no run after the limited one:
| Event | Time |
|---|---|
Limited run failed (usage_limit, resetAt: R) |
about t0 + 73 s |
limitRecovery armed |
{ autoResume: true, snooze: true, resetAt: R } |
limit-resume run requested |
R + 1 s |
| That run | completed, and the thread kept working |
Analysis
Read from the installed server bundle of this build, not from a source checkout.
ProjectionStore.getLimitRecoveryCandidatesselects each thread's latest executed run (the run that ended last) and requiresr.status = 'failed'plus ausage_limiterror item with a non-nullresetAton that same run.limitRecoveryCommandin the usage-limit recovery worker returnsnullunlessthread.status === "failed"andthread.lastErrorClass === "usage_limit".- So a wake run that ends as
completedbecomes the latest executed run and removes the thread from the candidate set. In this incident that happened 2.1 s after the limit stop, before the limited run had been armed. - Separately, a turn that could not do any work because the account was limited was classified
completed. In run 7 the only output is the provider's limit text.
Why this is not covered by a fix for #15665: carrying the reset onto follow-up failures does not help when the follow-up turns are not recorded as failures.
Possible directions, for the maintainers to judge:
- Classify a wake turn whose provider result is the limit message or a 429 as a
usage_limitfailure, including when a background-task notification started it. - Let candidate selection use the most recent
usage_limitfailure whose reset is still in the future, ignoring later notification-only runs that produced no work. - Hold background-task wake turns while the thread is limited, and release them with the resume.
Impact
Major degradation or frequent failure.
Auto-resume silently does nothing for threads that have background agents running when the limit hits, which is the pattern most likely to exhaust a limit. Nothing resumed the thread and its work idled overnight. I did not capture how the thread was presented in the UI at the time.
Version or commit
T3 Code (Nightly) 0.0.46-nightly.20261005.2702, official Windows build. Claude Code CLI 2.1.292.
Environment
- Windows 11 Pro 10.0.26200, desktop app with its local server.
- Provider
claudeAgent, modelclaude-opus-5-5, Claude subscription login, Orchestrator V2. - The machine did not sleep during the window (no Kernel-Power sleep or wake events), and the same server process was running before the limit and after the reset.
Logs or stack traces
None retained. server.trace.ndjson rotates at 10 MB roughly every three minutes here, so the files covering the limit stop and the reset were gone by morning. The evidence above comes from orchestration_v2_projection_runs, orchestration_v2_projection_turn_items and orchestration_v2_projection_threads, opened read-only.
Workaround
Send a message to the thread after the reset. Avoid launching many background agents when the usage window is nearly spent.
Activity
juliusmarminge commentedon Oct 7, 2026
Note
Grok responding on behalf of Julius.
Thanks for the detailed write-up. I checked it against
mainand it's a real bug.Root cause
ProjectionStore.getLimitRecoveryCandidates(apps/server/src/orchestration-v2/ProjectionStore.ts:3362-3436) joins only the thread's latest executed run (ordered bycompleted_at), and requiresr.status = 'failed'plus a rooterroritem on that same run withfailure.class = 'usage_limit'and a non-nullfailure.resetAtthat's later than the run'scompleted_at.limitRecoveryCommand(apps/server/src/orchestration-v2/UsageLimitRecoveryWorker.ts:11-25) returnsnullunlessthread.status === "failed"andlastErrorClass === "usage_limit". Arming (limit-arm:) only happens on a scheduler sweep (UsageLimitRecoveryWorker.ts:78-113), so it isn't immediate.ClaudeAdapterV2(Adapters/ClaudeAdapterV2.ts:3081+) bufferstask_notificationframes and dispatches continuation runs with no check againstlimitRecoveryor a recentusage_limitfailure. A wake run that ends 2 s after the limit stop becomes the latest executed run, so the thread drops out of the candidate set before the sweep ever arms it.completedbecauseproviderFailureFromResult(ClaudeAdapterV2.ts:2517-2555) returnsnullfor asuccessresult withis_error: false. TheusageLimitedheuristic (ClaudeAdapterV2.ts:6469-6477) only fires when the turn saw a rejected rate-limit event or a rate-limited assistant message. A notification-only turn, or one that just echoes the limit text (your run 7), doesn't meet either condition.Fix options
usage_limit. This is worth doing, but it isn't enough alone. Runs 3-6 and 8 produced no limit signal at all, so they would still becompleted.usage_limitfailure with a future reset, skipping later runs that did no work (notification/checkpoint/system_notice only). Recommended. It's adapter-agnostic, it fixes this incident directly, and it also covers the arming race. Both the SQL andlimitRecoveryCommandneed to stop relying onthread.status === "failed"for this case, keyed off the limited run's id.Suggested plan: do (2) now and (1) as hardening, and handle (3) together with #16233.
Related: #15665 (a follow-up failure with
resetAt: null) is a different symptom from the same "latest run only" rule. #16233, #15130 and #14169 are adjacent but not duplicates. PR #15553 is UI-only and doesn't change arming.Workaround: after the reset, send any message to the thread. Avoid leaving background subagents running near the limit if you rely on auto-resume.
Neonsy commentedon Oct 9, 2026
Still reproduces on
0.0.46-nightly.20261009.2861(3b6af0bd1600), Windows 10 Pro 10.0.19045, Claude Code CLI 2.1.295,autoResumeLimitedThreads: true. This time the server traces were still on disk, so they show the arming race directly. Times are UTCrate_limit_event:rejected,five_hour, reset 12:40:00usage_limitandresetAt12:40:00message.dispatchwith aprovider-continuation:command), 515 ms after the failurecompleted. Provider resultsuccess,is_error: false,num_turns: 0, 14 mscompleted), so nothing is armedlimit-arm:orlimit-resume:command and no new run (checked through 12:41)limitRecovery. Its latest run stayed run 52 (completed) until a manual message at 12:43notificationand an emptycheckpoint, so the recommended option above (skip later runs that did no work) would cover themClient side, from the installed bundle. The composer banner reads the same latest-run status (
Pi.status === "failed" && Pi.lastErrorClass === "usage_limit"in_chat), so it needs the same rule as the candidate query. Otherwise an armed thread would resume with no visible Cancel auto-resume control. In this incident the banner condition held for about 0.5 s, from the push after the failure (12:21:06.372) to the run 50 update (about 12:21:07.10). The client did process that state, since it sent athread.visitfor this thread at 12:21:06.590. In that window the banner could only offer Resume at reset, because Cancel auto-resume requires an armed recoveryAfter the reset the subagents were resumed by hand, and their failure notification rows kept a red Failed badge while they ran. That part is filed separately as #17486