Repository navigation
[Bug]: Claude usage-limit reset shown in activity but missing auto-resume controls during subagent workflow #15665
Description
Summary
A Claude thread using Orchestrator V2 subagents displays a reset time in its conversation activity, but the composer banner says Reset time unavailable; retry manually and offers no auto-resume or snooze controls. Another Claude thread in the same setup correctly displays the reset time and Cancel auto-resume / Snooze until reset.
Environment
- macOS desktop client connected through T3 Connect to a macOS backend.
- Custom build based on upstream nightly .2623, version
0.0.46-preview.20261003.1(local commit5c30ddb9). Customizations include reply suggestions and provider-inactivity UI. Not reproduced on an unmodified official build yet. - Claude Code subscription login; Orchestrator V2.
- Observed 4 October 2026, Europe/London.
Observed sequence
- Run Claude work that delegates to multiple subagents.
- Exhaust the Claude subscription usage window.
- Activity shows a failed subagent and a Claude usage-limit warning with a reset countdown. Messages also say
You've hit your session limit · resets 4:40pm (Europe/London)andUsage limit reached. Retry after 16:40. - The same thread's composer shows
Usage limit reached Reset time unavailable; retry manually, with no recovery scheduling actions. - Another Claude thread shows a reset of 16:40 on the same date and exposes recovery controls, including
Cancel auto-resume.
This is an observed case, not a deterministic minimal reproduction. Subagents were present, but they have not been established as the cause. A Waiting on subagent row also remained visible; this may be a separate state-display issue.
Expected behavior
When a trustworthy reset timestamp for the affected account and exhausted window is available, the failed turn should retain it and offer Resume at reset / Snooze until reset consistently. If only human-readable text is available and recovery cannot safely be scheduled, clearer explanation would help distinguish that from a thread/subagent failure. Do not guess a reset time or automatically enable recovery without the user's setting.
Investigation so far (not a confirmed root cause)
In the installed source, UsageLimitRecoveryBanner.tsx exposes scheduling actions only when a non-null reset time is later than the stop time. ClaudeAdapterV2.ts can emit a usage-limit failure with resetAt: null when its collected reset-time data is incomplete. That explains the UI condition, but we have not traced the raw SDK events for this particular failed run or established why its structured reset time is missing despite the displayed messages.
Related reports
- [Bug]: Orchestrator V2 Usage Reset Tracker Broken #14169: similar missing reset/recovery controls for Codex; filing separately because this report concerns Claude's adapter and subagent workflow.
- Five bugs found while running the orchestrator v2 branch (#2829) day to day #13331: potentially related waiting-state presentation after failed turns.
Workaround / privacy
Manual retry after the subscription resets. No transcripts, private project names, account identifiers, or screenshots are included in this report.
Activity
juliusmarminge commentedon Oct 4, 2026
Note
Grok responding on behalf of Julius.
Triage
Thanks for the careful write-up, @wobby13, and for being upfront about the custom build and what you haven't confirmed yet. That made this much easier to look into.
What I found
The composer symptom also exists in unmodified source, both in nightly
.2623and onmainat4ee6bfd50e. Subagents don't appear to be required. This isn't a duplicate of #14169 or #13331.UsageLimitRecoveryBannershowsReset time unavailable; retry manuallyand hides Resume at reset / Snooze until reset whenever the latest root error'sfailure.resetAtis null. That value comes fromthreadErrorSummaryinpackages/shared/src/orchestrationV2ThreadError.ts. The class is stillusage_limit, which is why the title still reads "Usage limit reached".resetAtends up null.ClaudeAdapterV2setsfailure.resetAtonly when every recorded rate-limit window has a parseableresetsAt. One rejected window withoutresetsAtnulls the whole value, and so does an assistantrate_limitframe with no rejected events. The SDK textYou've hit your session limit · resets 4:40pmis shown as provider text and isn't parsed intoresetAt.resetsAtemits its own per-window notice, so the activity can show a wait while the banner says the reset is unavailable. Your other Claude thread is most likely the case where every rejected window had a reset time.failure.resetAtis set, so it can't come from the same root error the banner is reading. It probably belongs to an earlier error item or to your custom UI.Waiting on subagentrow. That comes from a separate surface (presentPendingBackgroundWork) and doesn't affectresetAt.Likely fix area
resetAtfrom the windows that do have a reset time, or using the latest known reset for the exhausted window, instead of the current all-or-nothing rule inClaudeAdapterV2.rate_limit_eventframes (rateLimitType,status,resetsAt,overageStatus) would show exactly which window dropped its reset time.A maintainer will decide on the fix direction.
wobby13 commentedon Oct 4, 2026
I checked the retained provider logs (including rotated files) and the V2 database read-only. Here is an allowlisted extract; thread/run labels below are arbitrary, and no conversation content, project names, paths, account details or session identifiers are included.
Affected thread A
Shortly before the failures, the retained SDK
rate_limit_event.rate_limit_infowas:{ "rateLimitType": "five_hour", "status": "rejected", "resetsAt": 1791128400, "overageStatus": "rejected" }1791128400is 2026-10-04 15:40 UTC / 16:40 Europe/London, matching the reset displayed in the report.Two successive persisted failed runs in this same thread differ:
For the following run's requested-to-completed interval, the retained provider log contains an SDK frame with
type: "assistant",error: "rate_limit", but norate_limit_eventframe. I am not substituting invented null values for its requested fields: there is no corresponding event from which to extract them. The interval is present in the log, including the assistant error frame immediately before completion.Comparison thread B
The comparison thread's rejected event contains the same four allowlisted fields/values shown above. Its persisted failure has
resetAt: "2026-10-04T15:40:00.000Z", consistent with its available recovery controls.What this narrows down
For this specific occurrence, the evidence fits the assistant-rate-limit-without-a-new-rejected-event path, rather than demonstrating a rejected frame with a missing
resetsAt. The known reset was present earlier in the affected thread, but the next failed run persisted null. This is a log/database observation, not proof of SDK delivery beyond what was retained, nor a deterministic reproduction.Your point about
Retry after 16:40belonging to an earlier error is consistent with the two successive stored failures: the earlier one has a reset and the later one does not. I have not independently mapped the screenshot's exact activity row to its stored item, so that identification remains an inference.No production settings were changed and no quota-consuming reproduction was run. This is still the custom build disclosed in the original report.
coygeek commentedon Oct 6, 2026
Additional investigated occurrence on a nightly macOS installation. I am the user's Codex agent, posting these sanitized findings with their authorization.
Summary
A Claude thread's first usage-limit failure retains a future structured reset, but two automatic background continuations immediately afterward fail with resetAt=null. The assistant text still reports the same reset, while the composer says
Reset time unavailable; retry manuallyand there is no current saved snooze. This independently corroborates the known-reset-to-null sequence already reported here.Steps to reproduce
Background task completedmessages started subsequent automatic continuation runs.These are supplied and reconstructed incident conditions, not a deterministic minimal recipe. No fresh quota-consuming reproduction was run. The exact raw SDK frames responsible for the null reset remain uninspected.
Expected behavior
With reset-aware recovery enabled, a continuation of the same exhausted window should retain trustworthy applicable reset information and consistent pause/recovery behavior. New provider information may invalidate a reset or add an unknown exhausted window; that case must not be handled by guessing a timestamp.
Actual behavior
Read-only V2 snapshots establish this sequence in one affected thread. All three root failures have class=usage_limit, code=api_error_429, and retryable=true. R below denotes the observed future timestamp; it is an anonymous label, not an invented replacement value in a raw log.
The assistant text on all three failures names the same human-readable reset. The affected thread's snoozedUntil and snoozedAt are null. Its retained limitRecovery references an earlier expired usage window, rather than its current failed run.
A comparison thread also stores a known reset, then null on four successive failures, then the known reset again on a later failure. Its current recovery/snooze become valid and agree with the Snoozed sidebar presentation. This is one inspected incident; these sequence counts are not a population-frequency claim.
Evidence
Local verification means no new provider execution was attempted. The saved sequence was directly inspected. The null reset is established; the specific SDK path that produced it is not.
The installed adapter starts a fresh per-turn reset map. Its terminal reset calculation yields null for an empty map or any unknown exhausted-window reset. It does not fill this map from human-readable reset text. The adapter also buffers pre-continuation rate-limit events, as expressed by its background-wake regression test, so this is not a claim that every background continuation drops reset metadata.
Restoration check
Replay the captured SDK sequence once available. A known future reset should remain applicable through a continuation of the same exhausted window when no conflicting, expired, or newly unknown window invalidates it. The following failed turn should expose consistent recovery controls and honor the enabled reset-aware pause. Keep invalidated/unknown resets and disabled recovery settings as controls; do not pass by guessing reset times or discarding background-task notifications.
Runtime or environment
The inspected macOS desktop and two installed servers report 0.0.46-nightly.20261005.2702; the installed package pins cfa4f76. Provider: Claude Agent SDK, Claude Opus 5.5. No live store or settings were changed during investigation.
Triage assessment
bitconym commentedon Oct 6, 2026
Another occurrence on 6 October 2026, with the redacted screenshot below. This matches the earlier report of a known reset disappearing on a subsequent Claude usage-limit failure:
Both failures are shown at 12:15 PM in the screenshot. This is visual corroboration of the known-reset → missing-reset symptom; we have not inspected the raw SDK events or persisted failure fields for this occurrence, so we cannot establish which adapter path caused it. We have not tested #15697 locally.
Posted with the reporter's authorization by their Codex agent.
EhsanulHaqueSiam commentedon Oct 7, 2026
Root cause and a deterministic reproduction for the follow-up failures that lose
resetAt. Verified on an unmodified official nightly (0.0.46-nightly.20261005.2702, Claude CLI 2.1.292, macOS) and onmainatbfec2387b.No exhausted subscription is needed. The real CLI runs against a local mock of the Messages API that answers like a used-up five-hour window.
Steps to reproduce (verified)
mock-api.mjs, below) with a reset two minutes away:node mock-api.mjs 18787 $(( $(date +%s) + 120 ))fake-claude.sh, which runs the real CLI against the mock with a throwaway token and config dir:/tmp/t3-limit-home/userdata/settings.json, add a scratch project, and start an isolated dev server:{ "autoResumeLimitedThreads": true, "snoozeLimitedThreads": true, "providerInstances": { "claudeAgent": { "driver": "claudeAgent", "enabled": true, "config": { "binaryPath": "/path/to/fake-claude.sh" } } } }Usage limit reached, and the banner showsResets …withCancel auto-resume.Expected behavior
The second failure keeps the reset, the banner keeps
Cancel auto-resume, and the thread auto-resumes at the reset.Actual behavior
Usage limit reached., and the banner changes toReset time unavailable; retry manually.failure.resetAt: null.limit-resume:message is dispatched and the mock receives no request.Measured on
main, one thread:rate_limit_eventin turnfailure.resetAt2026-10-07T19:06:40Znull2026-10-07T19:06:40ZWith the reset two minutes out,
mainhad dispatched nolimit-resume:message 40 s after the reset. With the fix below, the same steps kept the reset on turn 2, andContinue where you left off.was dispatched 2 s after the reset.In real use, the second limited turn usually starts by itself. A background task, subagent, or overdue
ScheduleWakeupfires its wake turn the moment the limit ends the current turn, which matches the reports above. In my incident the wake turn came 1.5 s after the first limit hit.Root cause
The CLI's headless stream does not send a
rate_limit_eventfor every 429. It sends one when the limit state changes, and repeats an unchanged state at most once every 30 seconds for the sameresetsAt. A limited turn that starts within 30 s of the last reported event, in the same CLI process, therefore gets no event.cli-throttle.mjs(below) shows this without T3, driving the CLI headless against the mock:ClaudeAdapterV2takes the reset only fromrate_limit_events seen during the turn, so that turn fails withresetAt: null. It becomes the thread's latest run and hides the earlier reset.UsageLimitRecoveryWorkerskips any thread whose latest failure has no reset, even if it had already armed recovery for the earlier run.The reset is still on the blocked assistant message, in a field the SDK types omit. This is the incident's wake turn; every mock turn above, turn 2 included, carried the same field:
{ "type": "assistant", "error": "rate_limit", "api_error": "usage_limit_reached", "api_error_params": { "rate_limit_info": { "status": "rejected", "rateLimitType": "five_hour", "resetsAt": 1791324000, "overageStatus": "rejected" } } }That answers the triage question about this turn's frames: it has no
rate_limit_eventat all, but its root assistant frame names the window and reset.Possible fix
Read
api_error_params.rate_limit_infowhen the root assistant frame haserror: "rate_limit". This works per turn with no cross-turn state, and an event-supplied reset still wins:if (message.parent_tool_use_id === null) { context.latestAssistantRateLimited = message.error === "rate_limit"; + if (message.error === "rate_limit" && isClaudeBlockedAssistantRateLimit(message)) { + const info = message.api_error_params.rate_limit_info; + const limitType = info.rateLimitType ?? "unknown"; + context.rejectedRateLimitTypes.add(limitType); + context.rateLimitResetTimes.set( + limitType, + context.rateLimitResetTimes.get(limitType) ?? claudeUsageLimitResetAt(info.resetsAt), + ); + }isClaudeBlockedAssistantRateLimitis aSchema.isguard for{ api_error_params: { rate_limit_info: { status: "rejected", rateLimitType?, resetsAt? } } }.claudeUsageLimitResetAtis the existing epoch-seconds conversion from therate_limit_eventbranch, moved into a helper.A wake-harness test in
ClaudeAdapterV2.test.tsreplays the incident's frames: a limited user turn with a rejected event, then a wake turn with only the blocked assistant frame and a 429 result. It fails onmainwithexpected null to equal '2026-10-06T22:00:00.000Z'and passes with the change. All 144 tests in that file pass, and the server typecheck is clean.This may also simplify #15697. Each wake turn's blocked frame names its own reset, so the per-conversation cache and its replay invalidation may not be needed. If maintainers prefer this direction, the test and change are ready to open as a PR.
mock-api.mjscli-throttle.mjs(CLI-only check:node cli-throttle.mjs "$(command -v claude)" 18787 1 35)realhasanshoaib commentedon Oct 12, 2026
Confirmed the embedded-only path on today's nightly with Claude Code 2.1.295. Read the retained provider protocol log and V2 projection rows read-only; no live state was changed.
Two root failures in the same conversation, four seconds apart:
rate_limit_eventfive_hour, rejected,resetsAt: 1791804000api_error_params.rate_limit_infoapi_error_status: 429,terminal_reason: api_errorfailure.resetAt2026-10-12T11:20:00.000ZnullThat is 3:20 PM in Asia/Dubai, matching Claude's message. Other inspected threads retained the same reset. The follow-up became the latest failure, so recovery controls disappeared and automatic snoozing did not arm for that run.
Also reproduced through the real web client on unchanged
mainat58d253231d, with the real Claude CLI pointed at a local mock subscription limit. A retry 16 seconds after the first failure lost the reset. No automatic continuation was sent after the mock reset passed. No real subscription quota or credentials were used.I'm preparing a focused fix that reads the current root assistant's structured payload rather than caching reset times across turns. This follows the reproduction and proposed direction in @EhsanulHaqueSiam's comment above. It preserves event-provided timestamps and the existing recovery settings.
realhasanshoaib commentedon Oct 12, 2026
Fix and verification: #18616
The real CLI follow-up failed 16 seconds after the first limit, without a standalone rate-limit event. With the fix it kept its reset, snoozed, and completed automatically three seconds after reset. Baseline main did not resume. Before is left; after is right. The 32-second browser recording uses a synthetic project and a local mock limit.
auto-resume.mp4