[Bug]: auto-resume never armed despite 3 rejected five_hour events - overage-disabled spend limit, graceful session exit, 6h of an overnight loop lost #118

Description

@eddy-curly

Summary

An overnight self-paced /loop run died at 06:34:49Z and never came back. Auto-resume was enabled for the thread and its poller was alive the whole time, but the durable store shows the thread never armed a pending resume and never fired one, despite receiving three account.rate-limits.updated events with status:"rejected" — the exact signal AutoResumeReactor's detection fiber documents as its trigger.

The distinguishing feature of this episode versus #6 and #39: the terminal block was not a plain five_hour exhaustion. It was five_hour rejected plus overage rejected with overageDisabledReason:"org_level_disabled_until", surfacing to the model as You've hit your individual spend limit · run /usage-credits to ask your admin for a higher limit.

Net cost: ~6 hours of wall clock, and an overnight build that stopped at item 5 of 6 in its first wave. The user found out the next morning.

Environment

  • T3 Code desktop, Windows 11 Pro 10.0.26200. Build installed 2026-08-14 (resources/app.asar, resources/server.asar mtime 20:31).
  • Thread 57e2846c-9cb1-40d4-8776-e5f7db3fe7b9, provider session 4def7c10-494d-40ef-a685-36ff485c36d6.
  • claude-opus-5[1m], effort high, runtime mode full-access.
  • Auto-resume state file: ~/.t3/userdata/t3x-auto-resume.json.
  • Provider log: ~/.t3/userdata/logs/provider/events.57e2846c-9cb1-40d4-8776-e5f7db3fe7b9.log.

What the user was doing

A /loop in dynamic mode (self-paced via ScheduleWakeup) driving a multi-wave prototype build, with work fanned out through background Workflow subagents. Explicitly an unattended overnight run.

Confirmed timeline

All times UTC. Local was UTC-4.

TimeEventEvidence
06:23:55.882account.rate-limits.updated, status:"rejected", rateLimitType:"five_hour", resetsAt:1786963200 (10:40:00Z), overageStatus:"allowed", isUsingOverage:trueprovider log line 1170
06:30:24.452same, overageStatus:"allowed_warning", isUsingOverage:trueline 1261
06:34:49.391same, but overageStatus:"rejected", overageDisabledReason:"org_level_disabled_until", overageResetsAt:1788220800 (2026-09-01), isUsingOverage:falseline 1343
06:34:49.392Workflow subagent UX-06 dies: "error":"You've hit your individual spend limit · run /usage-credits…"line 1344
06:34:50.354Synthetic assistant message, "model":"<synthetic>", content = the spend-limit string, stop_reason:"stop_sequence"line 1356
06:34:50.355item.completeditemType:"assistant_message"status:"completed"line 1358
06:34:50.362resultis_error:true together with subtype:"success", num_turns:1line 1359
06:34:50.876turn.completedline 1361
06:35:00.922The loop's ScheduleWakeup fires: command_lifecyclestate:"started", command_uuid 9663690cline 1363
06:35:01.161systemsubtype:"init"status:"failed"line 1364
06:35:01.855Second synthetic spend-limit assistant messageline 1368
06:35:01.857result again is_error:true + subtype:"success"line 1371
06:35:02.335Same command_uuidstate:"cancelled"line 1375
06:39:37.260session.exited, reason:"Session stopped", exitKind:"graceful"line 1376
10:40:00five_hour window reset. Nothing happened.
12:44:46session.started — the human came back and resumed manuallyline 1377

The core defect

Detection is documented as:

subscribes once to providerService.streamEvents and, on a Claude account.rate-limits.updated event with status:"rejected", schedules a resume at the structured resetsAt (+ margin)

That precondition was met three times. account.rate-limits.updated appears 25 times in this thread's log; the three carrying status:"rejected" are confirmed to be of that canonical type.

Walking planSchedule with the 06:23:55 event: verdict.rejected true, pendingResumeAtMs null, windowOpensInFuture true (10:40:00Z > 06:23:55Z), firedInCapWindow 0. That returns {kind:"schedule"}. Yet the store reads:

"57e2846c-9cb1-40d4-8776-e5f7db3fe7b9": {
"pending": null, "firedAtMs": [], "overridePrompt": null, "enabled": true
}

enabled:true, so the per-thread switch was on. firedAtMs is empty and FIRED_HISTORY_RETENTION_MS is 25h, so a fire anywhere in this episode would still be visible — it never fired. And pending is null, so either it never armed or it armed and was cancelled.

The wake fiber was demonstrably alive: GET /api/coil/auto-resume?threadId=57e2846c… polls run continuously from 06:07:06Z to 12:52:04Z with no gap over 5 minutes, straight across the 10:40:00Z reset. So this is not "the machine was asleep".

I could not narrow it further from the outside, because there is no telemetry for the auto-resume decision at all (see item 3).

Narrowed hypotheses

  1. Armed, then cancelled pre-dispatch.cancelReason runs against a fresh snapshot. thread-advanced should be excluded (the fix from Auto-resume never fires in practice: 'thread-advanced' false cancellation when the limited turn settles during the wait #6: latestTurn is null once the turn settles, and null is not treated as advancement) and progressing should be false after session.exited. That leaves thread-gone — is settledOverride set to "settled", or archivedAt populated, by a gracefulsession.exited? If so, every unattended run that exits cleanly while limited loses its pending resume. That would be the same class of bug as Auto-resume never fires in practice: 'thread-advanced' false cancellation when the limited turn settles during the wait #6/[Bug]: auto-resume is cancelled as "user-took-over" by any user message — including one that was itself rejected by a limit #39: a guard meant to detect human takeover firing on an ordinary lifecycle transition.
  2. Never armed, because the parser did not read this payload shape as rejected. The rate_limit_info here carries the newer overage fields (overageStatus, overageResetsAt, overageDisabledReason, isUsingOverage). If the verdict parser reads a composite/effective status rather than rate_limit_info.status, an overageStatus:"rejected" variant may not map to verdict.rejected.
  3. Armed correctly, but at a useless time.resetsAt (10:40:00Z) describes the five_hour bucket. The actual blocker was the individual spend limitorg_level_disabled_until, with overageResetsAt on 2026-09-01. A resume at 10:40 would have fired into a still-closed door and burned a ladder attempt. Auto-resume currently has no representation for "blocked until a human raises a spend cap", which is not a time-based condition at all.

Hypothesis 3 holds regardless of which of 1 or 2 is true, and is the one worth designing for.

Three separable defects

1. A limit stop is recorded as a successful turn.result carries is_error:true and subtype:"success" simultaneously, and the item is status:"completed". Any consumer keyed on subtype sees success. The reactor docstring already acknowledges the upstream shape ("a usage limit does not produce a failed turn (the SDK has no rate-limit result subtype)") and deliberately reads the structured signal instead — but nothing else in the fork does, so the "did my overnight run actually stop?" question has no reliable answer outside the rate-limit stream.

2. A scheduled wake tick burns a turn into a closed limit, then the session exits silently. At 06:35:00 the loop's own ScheduleWakeup started a turn; init reported status:"failed", the model never ran, the command was cancelled, and 4m35s later the session exited gracefully. From the model's side the turn produced no output, so no follow-up ScheduleWakeup was armed and the loop was permanently dead. Two asks: (a) don't dispatch a scheduled wake while a rejection is in force and its window has not opened — defer it to the same resetsAt machinery; (b) a graceful exit while limited with a live loop should raise something the user can see the next morning. It produced no notification of any kind. This is closely related to #38 and #42.

3. Auto-resume decisions are invisible. Grepping every rotated server.trace.ndjson* for already-pending, not-rejected, capped, autoResume, auto-resume scheduled, and rate_limit_event returns zero hits — the only auto-resume evidence in the traces is the HTTP poll route. There is no record of a planSchedule verdict, a cancelReason, or a fire attempt. #6 and #39 were both "auto-resume silently didn't fire" bugs; a third one is now unresolvable from a full set of logs. Emitting the arm/skip/cancel decision (with reason and resumeAtMs) as a domain event or timeline activity would have made this issue a one-line diagnosis.

Suggested direction

  • Log every planSchedule and cancelReason outcome with its reason. Highest value per line of code here.
  • Assert which of hypotheses 1/2 applies, using the timeline above as the fixture.
  • Treat "overage rejected / spend limit / org_level_disabled_until" as a distinct terminal class from a windowed rate limit: do not schedule a time-based resume, mark the thread blocked-on-human, and notify. overageResetsAt two weeks out is not a retry target.
  • Consider whether a graceful session.exited should preserve rather than invalidate a pending resume.

Repro sketch

  1. Account where five-hour usage is exhausted and org-level overage is disabled (spend cap reached).
  2. Start a dynamic /loop that fans work out through background Workflow subagents, so turns are long and rate-limit rejections land mid-turn.
  3. Let it run into the limit unattended.
  4. Observe: synthetic spend-limit assistant message, resultis_error:true/subtype:"success", session exits gracefully, t3x-auto-resume.json shows pending:null and firedAtMs:[] for the thread, and no resume at the resetsAt boundary.

Related

Both #6 and #39 were "the guard cancelled a resume that should have fired". This is the third instance of that shape, plus a limit class the feature has no model for.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions

      , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
       blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
      }
      } catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
      })();
      (function(){
      try {
      var __m = "github.com";
      var __re = new RegExp('^' + "github\\.com" + '
      
      Skip to content

      [Bug]: auto-resume never armed despite 3 rejected five_hour events - overage-disabled spend limit, graceful session exit, 6h of an overnight loop lost #118

      Description

      @eddy-curly

      Summary

      An overnight self-paced /loop run died at 06:34:49Z and never came back. Auto-resume was enabled for the thread and its poller was alive the whole time, but the durable store shows the thread never armed a pending resume and never fired one, despite receiving three account.rate-limits.updated events with status:"rejected" — the exact signal AutoResumeReactor's detection fiber documents as its trigger.

      The distinguishing feature of this episode versus #6 and #39: the terminal block was not a plain five_hour exhaustion. It was five_hour rejected plus overage rejected with overageDisabledReason:"org_level_disabled_until", surfacing to the model as You've hit your individual spend limit · run /usage-credits to ask your admin for a higher limit.

      Net cost: ~6 hours of wall clock, and an overnight build that stopped at item 5 of 6 in its first wave. The user found out the next morning.

      Environment

      • T3 Code desktop, Windows 11 Pro 10.0.26200. Build installed 2026-08-14 (resources/app.asar, resources/server.asar mtime 20:31).
      • Thread 57e2846c-9cb1-40d4-8776-e5f7db3fe7b9, provider session 4def7c10-494d-40ef-a685-36ff485c36d6.
      • claude-opus-5[1m], effort high, runtime mode full-access.
      • Auto-resume state file: ~/.t3/userdata/t3x-auto-resume.json.
      • Provider log: ~/.t3/userdata/logs/provider/events.57e2846c-9cb1-40d4-8776-e5f7db3fe7b9.log.

      What the user was doing

      A /loop in dynamic mode (self-paced via ScheduleWakeup) driving a multi-wave prototype build, with work fanned out through background Workflow subagents. Explicitly an unattended overnight run.

      Confirmed timeline

      All times UTC. Local was UTC-4.

      TimeEventEvidence
      06:23:55.882account.rate-limits.updated, status:"rejected", rateLimitType:"five_hour", resetsAt:1786963200 (10:40:00Z), overageStatus:"allowed", isUsingOverage:trueprovider log line 1170
      06:30:24.452same, overageStatus:"allowed_warning", isUsingOverage:trueline 1261
      06:34:49.391same, but overageStatus:"rejected", overageDisabledReason:"org_level_disabled_until", overageResetsAt:1788220800 (2026-09-01), isUsingOverage:falseline 1343
      06:34:49.392Workflow subagent UX-06 dies: "error":"You've hit your individual spend limit · run /usage-credits…"line 1344
      06:34:50.354Synthetic assistant message, "model":"<synthetic>", content = the spend-limit string, stop_reason:"stop_sequence"line 1356
      06:34:50.355item.completeditemType:"assistant_message"status:"completed"line 1358
      06:34:50.362resultis_error:true together with subtype:"success", num_turns:1line 1359
      06:34:50.876turn.completedline 1361
      06:35:00.922The loop's ScheduleWakeup fires: command_lifecyclestate:"started", command_uuid 9663690cline 1363
      06:35:01.161systemsubtype:"init"status:"failed"line 1364
      06:35:01.855Second synthetic spend-limit assistant messageline 1368
      06:35:01.857result again is_error:true + subtype:"success"line 1371
      06:35:02.335Same command_uuidstate:"cancelled"line 1375
      06:39:37.260session.exited, reason:"Session stopped", exitKind:"graceful"line 1376
      10:40:00five_hour window reset. Nothing happened.
      12:44:46session.started — the human came back and resumed manuallyline 1377

      The core defect

      Detection is documented as:

      subscribes once to providerService.streamEvents and, on a Claude account.rate-limits.updated event with status:"rejected", schedules a resume at the structured resetsAt (+ margin)

      That precondition was met three times. account.rate-limits.updated appears 25 times in this thread's log; the three carrying status:"rejected" are confirmed to be of that canonical type.

      Walking planSchedule with the 06:23:55 event: verdict.rejected true, pendingResumeAtMs null, windowOpensInFuture true (10:40:00Z > 06:23:55Z), firedInCapWindow 0. That returns {kind:"schedule"}. Yet the store reads:

      "57e2846c-9cb1-40d4-8776-e5f7db3fe7b9": {
      "pending": null, "firedAtMs": [], "overridePrompt": null, "enabled": true
      }

      enabled:true, so the per-thread switch was on. firedAtMs is empty and FIRED_HISTORY_RETENTION_MS is 25h, so a fire anywhere in this episode would still be visible — it never fired. And pending is null, so either it never armed or it armed and was cancelled.

      The wake fiber was demonstrably alive: GET /api/coil/auto-resume?threadId=57e2846c… polls run continuously from 06:07:06Z to 12:52:04Z with no gap over 5 minutes, straight across the 10:40:00Z reset. So this is not "the machine was asleep".

      I could not narrow it further from the outside, because there is no telemetry for the auto-resume decision at all (see item 3).

      Narrowed hypotheses

      1. Armed, then cancelled pre-dispatch.cancelReason runs against a fresh snapshot. thread-advanced should be excluded (the fix from Auto-resume never fires in practice: 'thread-advanced' false cancellation when the limited turn settles during the wait #6: latestTurn is null once the turn settles, and null is not treated as advancement) and progressing should be false after session.exited. That leaves thread-gone — is settledOverride set to "settled", or archivedAt populated, by a gracefulsession.exited? If so, every unattended run that exits cleanly while limited loses its pending resume. That would be the same class of bug as Auto-resume never fires in practice: 'thread-advanced' false cancellation when the limited turn settles during the wait #6/[Bug]: auto-resume is cancelled as "user-took-over" by any user message — including one that was itself rejected by a limit #39: a guard meant to detect human takeover firing on an ordinary lifecycle transition.
      2. Never armed, because the parser did not read this payload shape as rejected. The rate_limit_info here carries the newer overage fields (overageStatus, overageResetsAt, overageDisabledReason, isUsingOverage). If the verdict parser reads a composite/effective status rather than rate_limit_info.status, an overageStatus:"rejected" variant may not map to verdict.rejected.
      3. Armed correctly, but at a useless time.resetsAt (10:40:00Z) describes the five_hour bucket. The actual blocker was the individual spend limitorg_level_disabled_until, with overageResetsAt on 2026-09-01. A resume at 10:40 would have fired into a still-closed door and burned a ladder attempt. Auto-resume currently has no representation for "blocked until a human raises a spend cap", which is not a time-based condition at all.

      Hypothesis 3 holds regardless of which of 1 or 2 is true, and is the one worth designing for.

      Three separable defects

      1. A limit stop is recorded as a successful turn.result carries is_error:true and subtype:"success" simultaneously, and the item is status:"completed". Any consumer keyed on subtype sees success. The reactor docstring already acknowledges the upstream shape ("a usage limit does not produce a failed turn (the SDK has no rate-limit result subtype)") and deliberately reads the structured signal instead — but nothing else in the fork does, so the "did my overnight run actually stop?" question has no reliable answer outside the rate-limit stream.

      2. A scheduled wake tick burns a turn into a closed limit, then the session exits silently. At 06:35:00 the loop's own ScheduleWakeup started a turn; init reported status:"failed", the model never ran, the command was cancelled, and 4m35s later the session exited gracefully. From the model's side the turn produced no output, so no follow-up ScheduleWakeup was armed and the loop was permanently dead. Two asks: (a) don't dispatch a scheduled wake while a rejection is in force and its window has not opened — defer it to the same resetsAt machinery; (b) a graceful exit while limited with a live loop should raise something the user can see the next morning. It produced no notification of any kind. This is closely related to #38 and #42.

      3. Auto-resume decisions are invisible. Grepping every rotated server.trace.ndjson* for already-pending, not-rejected, capped, autoResume, auto-resume scheduled, and rate_limit_event returns zero hits — the only auto-resume evidence in the traces is the HTTP poll route. There is no record of a planSchedule verdict, a cancelReason, or a fire attempt. #6 and #39 were both "auto-resume silently didn't fire" bugs; a third one is now unresolvable from a full set of logs. Emitting the arm/skip/cancel decision (with reason and resumeAtMs) as a domain event or timeline activity would have made this issue a one-line diagnosis.

      Suggested direction

      • Log every planSchedule and cancelReason outcome with its reason. Highest value per line of code here.
      • Assert which of hypotheses 1/2 applies, using the timeline above as the fixture.
      • Treat "overage rejected / spend limit / org_level_disabled_until" as a distinct terminal class from a windowed rate limit: do not schedule a time-based resume, mark the thread blocked-on-human, and notify. overageResetsAt two weeks out is not a retry target.
      • Consider whether a graceful session.exited should preserve rather than invalidate a pending resume.

      Repro sketch

      1. Account where five-hour usage is exhausted and org-level overage is disabled (spend cap reached).
      2. Start a dynamic /loop that fans work out through background Workflow subagents, so turns are long and rate-limit rejections land mid-turn.
      3. Let it run into the limit unattended.
      4. Observe: synthetic spend-limit assistant message, resultis_error:true/subtype:"success", session exits gracefully, t3x-auto-resume.json shows pending:null and firedAtMs:[] for the thread, and no resume at the resetsAt boundary.

      Related

      Both #6 and #39 were "the guard cancelled a resume that should have fired". This is the third instance of that shape, plus a limit class the feature has no model for.

      Metadata

      Metadata

      Assignees

      No one assigned

        Labels

        No labels
        No labels

        Projects

        No projects

          Milestone

          No milestone

          Relationships

          None yet

          Development

          No branches or pull requests

          Issue actions

          , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
          Skip to content

          [Bug]: auto-resume never armed despite 3 rejected five_hour events - overage-disabled spend limit, graceful session exit, 6h of an overnight loop lost #118

          Description

          @eddy-curly

          Summary

          An overnight self-paced /loop run died at 06:34:49Z and never came back. Auto-resume was enabled for the thread and its poller was alive the whole time, but the durable store shows the thread never armed a pending resume and never fired one, despite receiving three account.rate-limits.updated events with status:"rejected" — the exact signal AutoResumeReactor's detection fiber documents as its trigger.

          The distinguishing feature of this episode versus #6 and #39: the terminal block was not a plain five_hour exhaustion. It was five_hour rejected plus overage rejected with overageDisabledReason:"org_level_disabled_until", surfacing to the model as You've hit your individual spend limit · run /usage-credits to ask your admin for a higher limit.

          Net cost: ~6 hours of wall clock, and an overnight build that stopped at item 5 of 6 in its first wave. The user found out the next morning.

          Environment

          • T3 Code desktop, Windows 11 Pro 10.0.26200. Build installed 2026-08-14 (resources/app.asar, resources/server.asar mtime 20:31).
          • Thread 57e2846c-9cb1-40d4-8776-e5f7db3fe7b9, provider session 4def7c10-494d-40ef-a685-36ff485c36d6.
          • claude-opus-5[1m], effort high, runtime mode full-access.
          • Auto-resume state file: ~/.t3/userdata/t3x-auto-resume.json.
          • Provider log: ~/.t3/userdata/logs/provider/events.57e2846c-9cb1-40d4-8776-e5f7db3fe7b9.log.

          What the user was doing

          A /loop in dynamic mode (self-paced via ScheduleWakeup) driving a multi-wave prototype build, with work fanned out through background Workflow subagents. Explicitly an unattended overnight run.

          Confirmed timeline

          All times UTC. Local was UTC-4.

          TimeEventEvidence
          06:23:55.882account.rate-limits.updated, status:"rejected", rateLimitType:"five_hour", resetsAt:1786963200 (10:40:00Z), overageStatus:"allowed", isUsingOverage:trueprovider log line 1170
          06:30:24.452same, overageStatus:"allowed_warning", isUsingOverage:trueline 1261
          06:34:49.391same, but overageStatus:"rejected", overageDisabledReason:"org_level_disabled_until", overageResetsAt:1788220800 (2026-09-01), isUsingOverage:falseline 1343
          06:34:49.392Workflow subagent UX-06 dies: "error":"You've hit your individual spend limit · run /usage-credits…"line 1344
          06:34:50.354Synthetic assistant message, "model":"<synthetic>", content = the spend-limit string, stop_reason:"stop_sequence"line 1356
          06:34:50.355item.completeditemType:"assistant_message"status:"completed"line 1358
          06:34:50.362resultis_error:true together with subtype:"success", num_turns:1line 1359
          06:34:50.876turn.completedline 1361
          06:35:00.922The loop's ScheduleWakeup fires: command_lifecyclestate:"started", command_uuid 9663690cline 1363
          06:35:01.161systemsubtype:"init"status:"failed"line 1364
          06:35:01.855Second synthetic spend-limit assistant messageline 1368
          06:35:01.857result again is_error:true + subtype:"success"line 1371
          06:35:02.335Same command_uuidstate:"cancelled"line 1375
          06:39:37.260session.exited, reason:"Session stopped", exitKind:"graceful"line 1376
          10:40:00five_hour window reset. Nothing happened.
          12:44:46session.started — the human came back and resumed manuallyline 1377

          The core defect

          Detection is documented as:

          subscribes once to providerService.streamEvents and, on a Claude account.rate-limits.updated event with status:"rejected", schedules a resume at the structured resetsAt (+ margin)

          That precondition was met three times. account.rate-limits.updated appears 25 times in this thread's log; the three carrying status:"rejected" are confirmed to be of that canonical type.

          Walking planSchedule with the 06:23:55 event: verdict.rejected true, pendingResumeAtMs null, windowOpensInFuture true (10:40:00Z > 06:23:55Z), firedInCapWindow 0. That returns {kind:"schedule"}. Yet the store reads:

          "57e2846c-9cb1-40d4-8776-e5f7db3fe7b9": {
          "pending": null, "firedAtMs": [], "overridePrompt": null, "enabled": true
          }

          enabled:true, so the per-thread switch was on. firedAtMs is empty and FIRED_HISTORY_RETENTION_MS is 25h, so a fire anywhere in this episode would still be visible — it never fired. And pending is null, so either it never armed or it armed and was cancelled.

          The wake fiber was demonstrably alive: GET /api/coil/auto-resume?threadId=57e2846c… polls run continuously from 06:07:06Z to 12:52:04Z with no gap over 5 minutes, straight across the 10:40:00Z reset. So this is not "the machine was asleep".

          I could not narrow it further from the outside, because there is no telemetry for the auto-resume decision at all (see item 3).

          Narrowed hypotheses

          1. Armed, then cancelled pre-dispatch.cancelReason runs against a fresh snapshot. thread-advanced should be excluded (the fix from Auto-resume never fires in practice: 'thread-advanced' false cancellation when the limited turn settles during the wait #6: latestTurn is null once the turn settles, and null is not treated as advancement) and progressing should be false after session.exited. That leaves thread-gone — is settledOverride set to "settled", or archivedAt populated, by a gracefulsession.exited? If so, every unattended run that exits cleanly while limited loses its pending resume. That would be the same class of bug as Auto-resume never fires in practice: 'thread-advanced' false cancellation when the limited turn settles during the wait #6/[Bug]: auto-resume is cancelled as "user-took-over" by any user message — including one that was itself rejected by a limit #39: a guard meant to detect human takeover firing on an ordinary lifecycle transition.
          2. Never armed, because the parser did not read this payload shape as rejected. The rate_limit_info here carries the newer overage fields (overageStatus, overageResetsAt, overageDisabledReason, isUsingOverage). If the verdict parser reads a composite/effective status rather than rate_limit_info.status, an overageStatus:"rejected" variant may not map to verdict.rejected.
          3. Armed correctly, but at a useless time.resetsAt (10:40:00Z) describes the five_hour bucket. The actual blocker was the individual spend limitorg_level_disabled_until, with overageResetsAt on 2026-09-01. A resume at 10:40 would have fired into a still-closed door and burned a ladder attempt. Auto-resume currently has no representation for "blocked until a human raises a spend cap", which is not a time-based condition at all.

          Hypothesis 3 holds regardless of which of 1 or 2 is true, and is the one worth designing for.

          Three separable defects

          1. A limit stop is recorded as a successful turn.result carries is_error:true and subtype:"success" simultaneously, and the item is status:"completed". Any consumer keyed on subtype sees success. The reactor docstring already acknowledges the upstream shape ("a usage limit does not produce a failed turn (the SDK has no rate-limit result subtype)") and deliberately reads the structured signal instead — but nothing else in the fork does, so the "did my overnight run actually stop?" question has no reliable answer outside the rate-limit stream.

          2. A scheduled wake tick burns a turn into a closed limit, then the session exits silently. At 06:35:00 the loop's own ScheduleWakeup started a turn; init reported status:"failed", the model never ran, the command was cancelled, and 4m35s later the session exited gracefully. From the model's side the turn produced no output, so no follow-up ScheduleWakeup was armed and the loop was permanently dead. Two asks: (a) don't dispatch a scheduled wake while a rejection is in force and its window has not opened — defer it to the same resetsAt machinery; (b) a graceful exit while limited with a live loop should raise something the user can see the next morning. It produced no notification of any kind. This is closely related to #38 and #42.

          3. Auto-resume decisions are invisible. Grepping every rotated server.trace.ndjson* for already-pending, not-rejected, capped, autoResume, auto-resume scheduled, and rate_limit_event returns zero hits — the only auto-resume evidence in the traces is the HTTP poll route. There is no record of a planSchedule verdict, a cancelReason, or a fire attempt. #6 and #39 were both "auto-resume silently didn't fire" bugs; a third one is now unresolvable from a full set of logs. Emitting the arm/skip/cancel decision (with reason and resumeAtMs) as a domain event or timeline activity would have made this issue a one-line diagnosis.

          Suggested direction

          • Log every planSchedule and cancelReason outcome with its reason. Highest value per line of code here.
          • Assert which of hypotheses 1/2 applies, using the timeline above as the fixture.
          • Treat "overage rejected / spend limit / org_level_disabled_until" as a distinct terminal class from a windowed rate limit: do not schedule a time-based resume, mark the thread blocked-on-human, and notify. overageResetsAt two weeks out is not a retry target.
          • Consider whether a graceful session.exited should preserve rather than invalidate a pending resume.

          Repro sketch

          1. Account where five-hour usage is exhausted and org-level overage is disabled (spend cap reached).
          2. Start a dynamic /loop that fans work out through background Workflow subagents, so turns are long and rate-limit rejections land mid-turn.
          3. Let it run into the limit unattended.
          4. Observe: synthetic spend-limit assistant message, resultis_error:true/subtype:"success", session exits gracefully, t3x-auto-resume.json shows pending:null and firedAtMs:[] for the thread, and no resume at the resetsAt boundary.

          Related

          Both #6 and #39 were "the guard cancelled a resume that should have fired". This is the third instance of that shape, plus a limit class the feature has no model for.

          Metadata

          Metadata

          Assignees

          No one assigned

            Labels

            No labels
            No labels

            Projects

            No projects

              Milestone

              No milestone

              Relationships

              None yet

              Development

              No branches or pull requests

              Issue actions

              , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
              Skip to content

              [Bug]: auto-resume never armed despite 3 rejected five_hour events - overage-disabled spend limit, graceful session exit, 6h of an overnight loop lost #118

              Description

              @eddy-curly

              Summary

              An overnight self-paced /loop run died at 06:34:49Z and never came back. Auto-resume was enabled for the thread and its poller was alive the whole time, but the durable store shows the thread never armed a pending resume and never fired one, despite receiving three account.rate-limits.updated events with status:"rejected" — the exact signal AutoResumeReactor's detection fiber documents as its trigger.

              The distinguishing feature of this episode versus #6 and #39: the terminal block was not a plain five_hour exhaustion. It was five_hour rejected plus overage rejected with overageDisabledReason:"org_level_disabled_until", surfacing to the model as You've hit your individual spend limit · run /usage-credits to ask your admin for a higher limit.

              Net cost: ~6 hours of wall clock, and an overnight build that stopped at item 5 of 6 in its first wave. The user found out the next morning.

              Environment

              • T3 Code desktop, Windows 11 Pro 10.0.26200. Build installed 2026-08-14 (resources/app.asar, resources/server.asar mtime 20:31).
              • Thread 57e2846c-9cb1-40d4-8776-e5f7db3fe7b9, provider session 4def7c10-494d-40ef-a685-36ff485c36d6.
              • claude-opus-5[1m], effort high, runtime mode full-access.
              • Auto-resume state file: ~/.t3/userdata/t3x-auto-resume.json.
              • Provider log: ~/.t3/userdata/logs/provider/events.57e2846c-9cb1-40d4-8776-e5f7db3fe7b9.log.

              What the user was doing

              A /loop in dynamic mode (self-paced via ScheduleWakeup) driving a multi-wave prototype build, with work fanned out through background Workflow subagents. Explicitly an unattended overnight run.

              Confirmed timeline

              All times UTC. Local was UTC-4.

              TimeEventEvidence
              06:23:55.882account.rate-limits.updated, status:"rejected", rateLimitType:"five_hour", resetsAt:1786963200 (10:40:00Z), overageStatus:"allowed", isUsingOverage:trueprovider log line 1170
              06:30:24.452same, overageStatus:"allowed_warning", isUsingOverage:trueline 1261
              06:34:49.391same, but overageStatus:"rejected", overageDisabledReason:"org_level_disabled_until", overageResetsAt:1788220800 (2026-09-01), isUsingOverage:falseline 1343
              06:34:49.392Workflow subagent UX-06 dies: "error":"You've hit your individual spend limit · run /usage-credits…"line 1344
              06:34:50.354Synthetic assistant message, "model":"<synthetic>", content = the spend-limit string, stop_reason:"stop_sequence"line 1356
              06:34:50.355item.completeditemType:"assistant_message"status:"completed"line 1358
              06:34:50.362resultis_error:true together with subtype:"success", num_turns:1line 1359
              06:34:50.876turn.completedline 1361
              06:35:00.922The loop's ScheduleWakeup fires: command_lifecyclestate:"started", command_uuid 9663690cline 1363
              06:35:01.161systemsubtype:"init"status:"failed"line 1364
              06:35:01.855Second synthetic spend-limit assistant messageline 1368
              06:35:01.857result again is_error:true + subtype:"success"line 1371
              06:35:02.335Same command_uuidstate:"cancelled"line 1375
              06:39:37.260session.exited, reason:"Session stopped", exitKind:"graceful"line 1376
              10:40:00five_hour window reset. Nothing happened.
              12:44:46session.started — the human came back and resumed manuallyline 1377

              The core defect

              Detection is documented as:

              subscribes once to providerService.streamEvents and, on a Claude account.rate-limits.updated event with status:"rejected", schedules a resume at the structured resetsAt (+ margin)

              That precondition was met three times. account.rate-limits.updated appears 25 times in this thread's log; the three carrying status:"rejected" are confirmed to be of that canonical type.

              Walking planSchedule with the 06:23:55 event: verdict.rejected true, pendingResumeAtMs null, windowOpensInFuture true (10:40:00Z > 06:23:55Z), firedInCapWindow 0. That returns {kind:"schedule"}. Yet the store reads:

              "57e2846c-9cb1-40d4-8776-e5f7db3fe7b9": {
              "pending": null, "firedAtMs": [], "overridePrompt": null, "enabled": true
              }

              enabled:true, so the per-thread switch was on. firedAtMs is empty and FIRED_HISTORY_RETENTION_MS is 25h, so a fire anywhere in this episode would still be visible — it never fired. And pending is null, so either it never armed or it armed and was cancelled.

              The wake fiber was demonstrably alive: GET /api/coil/auto-resume?threadId=57e2846c… polls run continuously from 06:07:06Z to 12:52:04Z with no gap over 5 minutes, straight across the 10:40:00Z reset. So this is not "the machine was asleep".

              I could not narrow it further from the outside, because there is no telemetry for the auto-resume decision at all (see item 3).

              Narrowed hypotheses

              1. Armed, then cancelled pre-dispatch.cancelReason runs against a fresh snapshot. thread-advanced should be excluded (the fix from Auto-resume never fires in practice: 'thread-advanced' false cancellation when the limited turn settles during the wait #6: latestTurn is null once the turn settles, and null is not treated as advancement) and progressing should be false after session.exited. That leaves thread-gone — is settledOverride set to "settled", or archivedAt populated, by a gracefulsession.exited? If so, every unattended run that exits cleanly while limited loses its pending resume. That would be the same class of bug as Auto-resume never fires in practice: 'thread-advanced' false cancellation when the limited turn settles during the wait #6/[Bug]: auto-resume is cancelled as "user-took-over" by any user message — including one that was itself rejected by a limit #39: a guard meant to detect human takeover firing on an ordinary lifecycle transition.
              2. Never armed, because the parser did not read this payload shape as rejected. The rate_limit_info here carries the newer overage fields (overageStatus, overageResetsAt, overageDisabledReason, isUsingOverage). If the verdict parser reads a composite/effective status rather than rate_limit_info.status, an overageStatus:"rejected" variant may not map to verdict.rejected.
              3. Armed correctly, but at a useless time.resetsAt (10:40:00Z) describes the five_hour bucket. The actual blocker was the individual spend limitorg_level_disabled_until, with overageResetsAt on 2026-09-01. A resume at 10:40 would have fired into a still-closed door and burned a ladder attempt. Auto-resume currently has no representation for "blocked until a human raises a spend cap", which is not a time-based condition at all.

              Hypothesis 3 holds regardless of which of 1 or 2 is true, and is the one worth designing for.

              Three separable defects

              1. A limit stop is recorded as a successful turn.result carries is_error:true and subtype:"success" simultaneously, and the item is status:"completed". Any consumer keyed on subtype sees success. The reactor docstring already acknowledges the upstream shape ("a usage limit does not produce a failed turn (the SDK has no rate-limit result subtype)") and deliberately reads the structured signal instead — but nothing else in the fork does, so the "did my overnight run actually stop?" question has no reliable answer outside the rate-limit stream.

              2. A scheduled wake tick burns a turn into a closed limit, then the session exits silently. At 06:35:00 the loop's own ScheduleWakeup started a turn; init reported status:"failed", the model never ran, the command was cancelled, and 4m35s later the session exited gracefully. From the model's side the turn produced no output, so no follow-up ScheduleWakeup was armed and the loop was permanently dead. Two asks: (a) don't dispatch a scheduled wake while a rejection is in force and its window has not opened — defer it to the same resetsAt machinery; (b) a graceful exit while limited with a live loop should raise something the user can see the next morning. It produced no notification of any kind. This is closely related to #38 and #42.

              3. Auto-resume decisions are invisible. Grepping every rotated server.trace.ndjson* for already-pending, not-rejected, capped, autoResume, auto-resume scheduled, and rate_limit_event returns zero hits — the only auto-resume evidence in the traces is the HTTP poll route. There is no record of a planSchedule verdict, a cancelReason, or a fire attempt. #6 and #39 were both "auto-resume silently didn't fire" bugs; a third one is now unresolvable from a full set of logs. Emitting the arm/skip/cancel decision (with reason and resumeAtMs) as a domain event or timeline activity would have made this issue a one-line diagnosis.

              Suggested direction

              • Log every planSchedule and cancelReason outcome with its reason. Highest value per line of code here.
              • Assert which of hypotheses 1/2 applies, using the timeline above as the fixture.
              • Treat "overage rejected / spend limit / org_level_disabled_until" as a distinct terminal class from a windowed rate limit: do not schedule a time-based resume, mark the thread blocked-on-human, and notify. overageResetsAt two weeks out is not a retry target.
              • Consider whether a graceful session.exited should preserve rather than invalidate a pending resume.

              Repro sketch

              1. Account where five-hour usage is exhausted and org-level overage is disabled (spend cap reached).
              2. Start a dynamic /loop that fans work out through background Workflow subagents, so turns are long and rate-limit rejections land mid-turn.
              3. Let it run into the limit unattended.
              4. Observe: synthetic spend-limit assistant message, resultis_error:true/subtype:"success", session exits gracefully, t3x-auto-resume.json shows pending:null and firedAtMs:[] for the thread, and no resume at the resetsAt boundary.

              Related

              Both #6 and #39 were "the guard cancelled a resume that should have fired". This is the third instance of that shape, plus a limit class the feature has no model for.

              Metadata

              Metadata

              Assignees

              No one assigned

                Labels

                No labels
                No labels

                Projects

                No projects

                  Milestone

                  No milestone

                  Relationships

                  None yet

                  Development

                  No branches or pull requests

                  Issue actions

                  , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
                  Skip to content

                  [Bug]: auto-resume never armed despite 3 rejected five_hour events - overage-disabled spend limit, graceful session exit, 6h of an overnight loop lost #118

                  Description

                  @eddy-curly

                  Summary

                  An overnight self-paced /loop run died at 06:34:49Z and never came back. Auto-resume was enabled for the thread and its poller was alive the whole time, but the durable store shows the thread never armed a pending resume and never fired one, despite receiving three account.rate-limits.updated events with status:"rejected" — the exact signal AutoResumeReactor's detection fiber documents as its trigger.

                  The distinguishing feature of this episode versus #6 and #39: the terminal block was not a plain five_hour exhaustion. It was five_hour rejected plus overage rejected with overageDisabledReason:"org_level_disabled_until", surfacing to the model as You've hit your individual spend limit · run /usage-credits to ask your admin for a higher limit.

                  Net cost: ~6 hours of wall clock, and an overnight build that stopped at item 5 of 6 in its first wave. The user found out the next morning.

                  Environment

                  • T3 Code desktop, Windows 11 Pro 10.0.26200. Build installed 2026-08-14 (resources/app.asar, resources/server.asar mtime 20:31).
                  • Thread 57e2846c-9cb1-40d4-8776-e5f7db3fe7b9, provider session 4def7c10-494d-40ef-a685-36ff485c36d6.
                  • claude-opus-5[1m], effort high, runtime mode full-access.
                  • Auto-resume state file: ~/.t3/userdata/t3x-auto-resume.json.
                  • Provider log: ~/.t3/userdata/logs/provider/events.57e2846c-9cb1-40d4-8776-e5f7db3fe7b9.log.

                  What the user was doing

                  A /loop in dynamic mode (self-paced via ScheduleWakeup) driving a multi-wave prototype build, with work fanned out through background Workflow subagents. Explicitly an unattended overnight run.

                  Confirmed timeline

                  All times UTC. Local was UTC-4.

                  TimeEventEvidence
                  06:23:55.882account.rate-limits.updated, status:"rejected", rateLimitType:"five_hour", resetsAt:1786963200 (10:40:00Z), overageStatus:"allowed", isUsingOverage:trueprovider log line 1170
                  06:30:24.452same, overageStatus:"allowed_warning", isUsingOverage:trueline 1261
                  06:34:49.391same, but overageStatus:"rejected", overageDisabledReason:"org_level_disabled_until", overageResetsAt:1788220800 (2026-09-01), isUsingOverage:falseline 1343
                  06:34:49.392Workflow subagent UX-06 dies: "error":"You've hit your individual spend limit · run /usage-credits…"line 1344
                  06:34:50.354Synthetic assistant message, "model":"<synthetic>", content = the spend-limit string, stop_reason:"stop_sequence"line 1356
                  06:34:50.355item.completeditemType:"assistant_message"status:"completed"line 1358
                  06:34:50.362resultis_error:true together with subtype:"success", num_turns:1line 1359
                  06:34:50.876turn.completedline 1361
                  06:35:00.922The loop's ScheduleWakeup fires: command_lifecyclestate:"started", command_uuid 9663690cline 1363
                  06:35:01.161systemsubtype:"init"status:"failed"line 1364
                  06:35:01.855Second synthetic spend-limit assistant messageline 1368
                  06:35:01.857result again is_error:true + subtype:"success"line 1371
                  06:35:02.335Same command_uuidstate:"cancelled"line 1375
                  06:39:37.260session.exited, reason:"Session stopped", exitKind:"graceful"line 1376
                  10:40:00five_hour window reset. Nothing happened.
                  12:44:46session.started — the human came back and resumed manuallyline 1377

                  The core defect

                  Detection is documented as:

                  subscribes once to providerService.streamEvents and, on a Claude account.rate-limits.updated event with status:"rejected", schedules a resume at the structured resetsAt (+ margin)

                  That precondition was met three times. account.rate-limits.updated appears 25 times in this thread's log; the three carrying status:"rejected" are confirmed to be of that canonical type.

                  Walking planSchedule with the 06:23:55 event: verdict.rejected true, pendingResumeAtMs null, windowOpensInFuture true (10:40:00Z > 06:23:55Z), firedInCapWindow 0. That returns {kind:"schedule"}. Yet the store reads:

                  "57e2846c-9cb1-40d4-8776-e5f7db3fe7b9": {
                  "pending": null, "firedAtMs": [], "overridePrompt": null, "enabled": true
                  }

                  enabled:true, so the per-thread switch was on. firedAtMs is empty and FIRED_HISTORY_RETENTION_MS is 25h, so a fire anywhere in this episode would still be visible — it never fired. And pending is null, so either it never armed or it armed and was cancelled.

                  The wake fiber was demonstrably alive: GET /api/coil/auto-resume?threadId=57e2846c… polls run continuously from 06:07:06Z to 12:52:04Z with no gap over 5 minutes, straight across the 10:40:00Z reset. So this is not "the machine was asleep".

                  I could not narrow it further from the outside, because there is no telemetry for the auto-resume decision at all (see item 3).

                  Narrowed hypotheses

                  1. Armed, then cancelled pre-dispatch.cancelReason runs against a fresh snapshot. thread-advanced should be excluded (the fix from Auto-resume never fires in practice: 'thread-advanced' false cancellation when the limited turn settles during the wait #6: latestTurn is null once the turn settles, and null is not treated as advancement) and progressing should be false after session.exited. That leaves thread-gone — is settledOverride set to "settled", or archivedAt populated, by a gracefulsession.exited? If so, every unattended run that exits cleanly while limited loses its pending resume. That would be the same class of bug as Auto-resume never fires in practice: 'thread-advanced' false cancellation when the limited turn settles during the wait #6/[Bug]: auto-resume is cancelled as "user-took-over" by any user message — including one that was itself rejected by a limit #39: a guard meant to detect human takeover firing on an ordinary lifecycle transition.
                  2. Never armed, because the parser did not read this payload shape as rejected. The rate_limit_info here carries the newer overage fields (overageStatus, overageResetsAt, overageDisabledReason, isUsingOverage). If the verdict parser reads a composite/effective status rather than rate_limit_info.status, an overageStatus:"rejected" variant may not map to verdict.rejected.
                  3. Armed correctly, but at a useless time.resetsAt (10:40:00Z) describes the five_hour bucket. The actual blocker was the individual spend limitorg_level_disabled_until, with overageResetsAt on 2026-09-01. A resume at 10:40 would have fired into a still-closed door and burned a ladder attempt. Auto-resume currently has no representation for "blocked until a human raises a spend cap", which is not a time-based condition at all.

                  Hypothesis 3 holds regardless of which of 1 or 2 is true, and is the one worth designing for.

                  Three separable defects

                  1. A limit stop is recorded as a successful turn.result carries is_error:true and subtype:"success" simultaneously, and the item is status:"completed". Any consumer keyed on subtype sees success. The reactor docstring already acknowledges the upstream shape ("a usage limit does not produce a failed turn (the SDK has no rate-limit result subtype)") and deliberately reads the structured signal instead — but nothing else in the fork does, so the "did my overnight run actually stop?" question has no reliable answer outside the rate-limit stream.

                  2. A scheduled wake tick burns a turn into a closed limit, then the session exits silently. At 06:35:00 the loop's own ScheduleWakeup started a turn; init reported status:"failed", the model never ran, the command was cancelled, and 4m35s later the session exited gracefully. From the model's side the turn produced no output, so no follow-up ScheduleWakeup was armed and the loop was permanently dead. Two asks: (a) don't dispatch a scheduled wake while a rejection is in force and its window has not opened — defer it to the same resetsAt machinery; (b) a graceful exit while limited with a live loop should raise something the user can see the next morning. It produced no notification of any kind. This is closely related to #38 and #42.

                  3. Auto-resume decisions are invisible. Grepping every rotated server.trace.ndjson* for already-pending, not-rejected, capped, autoResume, auto-resume scheduled, and rate_limit_event returns zero hits — the only auto-resume evidence in the traces is the HTTP poll route. There is no record of a planSchedule verdict, a cancelReason, or a fire attempt. #6 and #39 were both "auto-resume silently didn't fire" bugs; a third one is now unresolvable from a full set of logs. Emitting the arm/skip/cancel decision (with reason and resumeAtMs) as a domain event or timeline activity would have made this issue a one-line diagnosis.

                  Suggested direction

                  • Log every planSchedule and cancelReason outcome with its reason. Highest value per line of code here.
                  • Assert which of hypotheses 1/2 applies, using the timeline above as the fixture.
                  • Treat "overage rejected / spend limit / org_level_disabled_until" as a distinct terminal class from a windowed rate limit: do not schedule a time-based resume, mark the thread blocked-on-human, and notify. overageResetsAt two weeks out is not a retry target.
                  • Consider whether a graceful session.exited should preserve rather than invalidate a pending resume.

                  Repro sketch

                  1. Account where five-hour usage is exhausted and org-level overage is disabled (spend cap reached).
                  2. Start a dynamic /loop that fans work out through background Workflow subagents, so turns are long and rate-limit rejections land mid-turn.
                  3. Let it run into the limit unattended.
                  4. Observe: synthetic spend-limit assistant message, resultis_error:true/subtype:"success", session exits gracefully, t3x-auto-resume.json shows pending:null and firedAtMs:[] for the thread, and no resume at the resetsAt boundary.

                  Related

                  Both #6 and #39 were "the guard cancelled a resume that should have fired". This is the third instance of that shape, plus a limit class the feature has no model for.

                  Metadata

                  Metadata

                  Assignees

                  No one assigned

                    Labels

                    No labels
                    No labels

                    Projects

                    No projects

                      Milestone

                      No milestone

                      Relationships

                      None yet

                      Development

                      No branches or pull requests

                      Issue actions

                      , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
                      Skip to content

                      [Bug]: auto-resume never armed despite 3 rejected five_hour events - overage-disabled spend limit, graceful session exit, 6h of an overnight loop lost #118

                      Description

                      @eddy-curly

                      Summary

                      An overnight self-paced /loop run died at 06:34:49Z and never came back. Auto-resume was enabled for the thread and its poller was alive the whole time, but the durable store shows the thread never armed a pending resume and never fired one, despite receiving three account.rate-limits.updated events with status:"rejected" — the exact signal AutoResumeReactor's detection fiber documents as its trigger.

                      The distinguishing feature of this episode versus #6 and #39: the terminal block was not a plain five_hour exhaustion. It was five_hour rejected plus overage rejected with overageDisabledReason:"org_level_disabled_until", surfacing to the model as You've hit your individual spend limit · run /usage-credits to ask your admin for a higher limit.

                      Net cost: ~6 hours of wall clock, and an overnight build that stopped at item 5 of 6 in its first wave. The user found out the next morning.

                      Environment

                      • T3 Code desktop, Windows 11 Pro 10.0.26200. Build installed 2026-08-14 (resources/app.asar, resources/server.asar mtime 20:31).
                      • Thread 57e2846c-9cb1-40d4-8776-e5f7db3fe7b9, provider session 4def7c10-494d-40ef-a685-36ff485c36d6.
                      • claude-opus-5[1m], effort high, runtime mode full-access.
                      • Auto-resume state file: ~/.t3/userdata/t3x-auto-resume.json.
                      • Provider log: ~/.t3/userdata/logs/provider/events.57e2846c-9cb1-40d4-8776-e5f7db3fe7b9.log.

                      What the user was doing

                      A /loop in dynamic mode (self-paced via ScheduleWakeup) driving a multi-wave prototype build, with work fanned out through background Workflow subagents. Explicitly an unattended overnight run.

                      Confirmed timeline

                      All times UTC. Local was UTC-4.

                      TimeEventEvidence
                      06:23:55.882account.rate-limits.updated, status:"rejected", rateLimitType:"five_hour", resetsAt:1786963200 (10:40:00Z), overageStatus:"allowed", isUsingOverage:trueprovider log line 1170
                      06:30:24.452same, overageStatus:"allowed_warning", isUsingOverage:trueline 1261
                      06:34:49.391same, but overageStatus:"rejected", overageDisabledReason:"org_level_disabled_until", overageResetsAt:1788220800 (2026-09-01), isUsingOverage:falseline 1343
                      06:34:49.392Workflow subagent UX-06 dies: "error":"You've hit your individual spend limit · run /usage-credits…"line 1344
                      06:34:50.354Synthetic assistant message, "model":"<synthetic>", content = the spend-limit string, stop_reason:"stop_sequence"line 1356
                      06:34:50.355item.completeditemType:"assistant_message"status:"completed"line 1358
                      06:34:50.362resultis_error:true together with subtype:"success", num_turns:1line 1359
                      06:34:50.876turn.completedline 1361
                      06:35:00.922The loop's ScheduleWakeup fires: command_lifecyclestate:"started", command_uuid 9663690cline 1363
                      06:35:01.161systemsubtype:"init"status:"failed"line 1364
                      06:35:01.855Second synthetic spend-limit assistant messageline 1368
                      06:35:01.857result again is_error:true + subtype:"success"line 1371
                      06:35:02.335Same command_uuidstate:"cancelled"line 1375
                      06:39:37.260session.exited, reason:"Session stopped", exitKind:"graceful"line 1376
                      10:40:00five_hour window reset. Nothing happened.
                      12:44:46session.started — the human came back and resumed manuallyline 1377

                      The core defect

                      Detection is documented as:

                      subscribes once to providerService.streamEvents and, on a Claude account.rate-limits.updated event with status:"rejected", schedules a resume at the structured resetsAt (+ margin)

                      That precondition was met three times. account.rate-limits.updated appears 25 times in this thread's log; the three carrying status:"rejected" are confirmed to be of that canonical type.

                      Walking planSchedule with the 06:23:55 event: verdict.rejected true, pendingResumeAtMs null, windowOpensInFuture true (10:40:00Z > 06:23:55Z), firedInCapWindow 0. That returns {kind:"schedule"}. Yet the store reads:

                      "57e2846c-9cb1-40d4-8776-e5f7db3fe7b9": {
                      "pending": null, "firedAtMs": [], "overridePrompt": null, "enabled": true
                      }

                      enabled:true, so the per-thread switch was on. firedAtMs is empty and FIRED_HISTORY_RETENTION_MS is 25h, so a fire anywhere in this episode would still be visible — it never fired. And pending is null, so either it never armed or it armed and was cancelled.

                      The wake fiber was demonstrably alive: GET /api/coil/auto-resume?threadId=57e2846c… polls run continuously from 06:07:06Z to 12:52:04Z with no gap over 5 minutes, straight across the 10:40:00Z reset. So this is not "the machine was asleep".

                      I could not narrow it further from the outside, because there is no telemetry for the auto-resume decision at all (see item 3).

                      Narrowed hypotheses

                      1. Armed, then cancelled pre-dispatch.cancelReason runs against a fresh snapshot. thread-advanced should be excluded (the fix from Auto-resume never fires in practice: 'thread-advanced' false cancellation when the limited turn settles during the wait #6: latestTurn is null once the turn settles, and null is not treated as advancement) and progressing should be false after session.exited. That leaves thread-gone — is settledOverride set to "settled", or archivedAt populated, by a gracefulsession.exited? If so, every unattended run that exits cleanly while limited loses its pending resume. That would be the same class of bug as Auto-resume never fires in practice: 'thread-advanced' false cancellation when the limited turn settles during the wait #6/[Bug]: auto-resume is cancelled as "user-took-over" by any user message — including one that was itself rejected by a limit #39: a guard meant to detect human takeover firing on an ordinary lifecycle transition.
                      2. Never armed, because the parser did not read this payload shape as rejected. The rate_limit_info here carries the newer overage fields (overageStatus, overageResetsAt, overageDisabledReason, isUsingOverage). If the verdict parser reads a composite/effective status rather than rate_limit_info.status, an overageStatus:"rejected" variant may not map to verdict.rejected.
                      3. Armed correctly, but at a useless time.resetsAt (10:40:00Z) describes the five_hour bucket. The actual blocker was the individual spend limitorg_level_disabled_until, with overageResetsAt on 2026-09-01. A resume at 10:40 would have fired into a still-closed door and burned a ladder attempt. Auto-resume currently has no representation for "blocked until a human raises a spend cap", which is not a time-based condition at all.

                      Hypothesis 3 holds regardless of which of 1 or 2 is true, and is the one worth designing for.

                      Three separable defects

                      1. A limit stop is recorded as a successful turn.result carries is_error:true and subtype:"success" simultaneously, and the item is status:"completed". Any consumer keyed on subtype sees success. The reactor docstring already acknowledges the upstream shape ("a usage limit does not produce a failed turn (the SDK has no rate-limit result subtype)") and deliberately reads the structured signal instead — but nothing else in the fork does, so the "did my overnight run actually stop?" question has no reliable answer outside the rate-limit stream.

                      2. A scheduled wake tick burns a turn into a closed limit, then the session exits silently. At 06:35:00 the loop's own ScheduleWakeup started a turn; init reported status:"failed", the model never ran, the command was cancelled, and 4m35s later the session exited gracefully. From the model's side the turn produced no output, so no follow-up ScheduleWakeup was armed and the loop was permanently dead. Two asks: (a) don't dispatch a scheduled wake while a rejection is in force and its window has not opened — defer it to the same resetsAt machinery; (b) a graceful exit while limited with a live loop should raise something the user can see the next morning. It produced no notification of any kind. This is closely related to #38 and #42.

                      3. Auto-resume decisions are invisible. Grepping every rotated server.trace.ndjson* for already-pending, not-rejected, capped, autoResume, auto-resume scheduled, and rate_limit_event returns zero hits — the only auto-resume evidence in the traces is the HTTP poll route. There is no record of a planSchedule verdict, a cancelReason, or a fire attempt. #6 and #39 were both "auto-resume silently didn't fire" bugs; a third one is now unresolvable from a full set of logs. Emitting the arm/skip/cancel decision (with reason and resumeAtMs) as a domain event or timeline activity would have made this issue a one-line diagnosis.

                      Suggested direction

                      • Log every planSchedule and cancelReason outcome with its reason. Highest value per line of code here.
                      • Assert which of hypotheses 1/2 applies, using the timeline above as the fixture.
                      • Treat "overage rejected / spend limit / org_level_disabled_until" as a distinct terminal class from a windowed rate limit: do not schedule a time-based resume, mark the thread blocked-on-human, and notify. overageResetsAt two weeks out is not a retry target.
                      • Consider whether a graceful session.exited should preserve rather than invalidate a pending resume.

                      Repro sketch

                      1. Account where five-hour usage is exhausted and org-level overage is disabled (spend cap reached).
                      2. Start a dynamic /loop that fans work out through background Workflow subagents, so turns are long and rate-limit rejections land mid-turn.
                      3. Let it run into the limit unattended.
                      4. Observe: synthetic spend-limit assistant message, resultis_error:true/subtype:"success", session exits gracefully, t3x-auto-resume.json shows pending:null and firedAtMs:[] for the thread, and no resume at the resetsAt boundary.

                      Related

                      Both #6 and #39 were "the guard cancelled a resume that should have fired". This is the third instance of that shape, plus a limit class the feature has no model for.

                      Metadata

                      Metadata

                      Assignees

                      No one assigned

                        Labels

                        No labels
                        No labels

                        Projects

                        No projects

                          Milestone

                          No milestone

                          Relationships

                          None yet

                          Development

                          No branches or pull requests

                          Issue actions

                          , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
                          Skip to content

                          [Bug]: auto-resume never armed despite 3 rejected five_hour events - overage-disabled spend limit, graceful session exit, 6h of an overnight loop lost #118

                          Description

                          @eddy-curly

                          Summary

                          An overnight self-paced /loop run died at 06:34:49Z and never came back. Auto-resume was enabled for the thread and its poller was alive the whole time, but the durable store shows the thread never armed a pending resume and never fired one, despite receiving three account.rate-limits.updated events with status:"rejected" — the exact signal AutoResumeReactor's detection fiber documents as its trigger.

                          The distinguishing feature of this episode versus #6 and #39: the terminal block was not a plain five_hour exhaustion. It was five_hour rejected plus overage rejected with overageDisabledReason:"org_level_disabled_until", surfacing to the model as You've hit your individual spend limit · run /usage-credits to ask your admin for a higher limit.

                          Net cost: ~6 hours of wall clock, and an overnight build that stopped at item 5 of 6 in its first wave. The user found out the next morning.

                          Environment

                          • T3 Code desktop, Windows 11 Pro 10.0.26200. Build installed 2026-08-14 (resources/app.asar, resources/server.asar mtime 20:31).
                          • Thread 57e2846c-9cb1-40d4-8776-e5f7db3fe7b9, provider session 4def7c10-494d-40ef-a685-36ff485c36d6.
                          • claude-opus-5[1m], effort high, runtime mode full-access.
                          • Auto-resume state file: ~/.t3/userdata/t3x-auto-resume.json.
                          • Provider log: ~/.t3/userdata/logs/provider/events.57e2846c-9cb1-40d4-8776-e5f7db3fe7b9.log.

                          What the user was doing

                          A /loop in dynamic mode (self-paced via ScheduleWakeup) driving a multi-wave prototype build, with work fanned out through background Workflow subagents. Explicitly an unattended overnight run.

                          Confirmed timeline

                          All times UTC. Local was UTC-4.

                          TimeEventEvidence
                          06:23:55.882account.rate-limits.updated, status:"rejected", rateLimitType:"five_hour", resetsAt:1786963200 (10:40:00Z), overageStatus:"allowed", isUsingOverage:trueprovider log line 1170
                          06:30:24.452same, overageStatus:"allowed_warning", isUsingOverage:trueline 1261
                          06:34:49.391same, but overageStatus:"rejected", overageDisabledReason:"org_level_disabled_until", overageResetsAt:1788220800 (2026-09-01), isUsingOverage:falseline 1343
                          06:34:49.392Workflow subagent UX-06 dies: "error":"You've hit your individual spend limit · run /usage-credits…"line 1344
                          06:34:50.354Synthetic assistant message, "model":"<synthetic>", content = the spend-limit string, stop_reason:"stop_sequence"line 1356
                          06:34:50.355item.completeditemType:"assistant_message"status:"completed"line 1358
                          06:34:50.362resultis_error:true together with subtype:"success", num_turns:1line 1359
                          06:34:50.876turn.completedline 1361
                          06:35:00.922The loop's ScheduleWakeup fires: command_lifecyclestate:"started", command_uuid 9663690cline 1363
                          06:35:01.161systemsubtype:"init"status:"failed"line 1364
                          06:35:01.855Second synthetic spend-limit assistant messageline 1368
                          06:35:01.857result again is_error:true + subtype:"success"line 1371
                          06:35:02.335Same command_uuidstate:"cancelled"line 1375
                          06:39:37.260session.exited, reason:"Session stopped", exitKind:"graceful"line 1376
                          10:40:00five_hour window reset. Nothing happened.
                          12:44:46session.started — the human came back and resumed manuallyline 1377

                          The core defect

                          Detection is documented as:

                          subscribes once to providerService.streamEvents and, on a Claude account.rate-limits.updated event with status:"rejected", schedules a resume at the structured resetsAt (+ margin)

                          That precondition was met three times. account.rate-limits.updated appears 25 times in this thread's log; the three carrying status:"rejected" are confirmed to be of that canonical type.

                          Walking planSchedule with the 06:23:55 event: verdict.rejected true, pendingResumeAtMs null, windowOpensInFuture true (10:40:00Z > 06:23:55Z), firedInCapWindow 0. That returns {kind:"schedule"}. Yet the store reads:

                          "57e2846c-9cb1-40d4-8776-e5f7db3fe7b9": {
                          "pending": null, "firedAtMs": [], "overridePrompt": null, "enabled": true
                          }

                          enabled:true, so the per-thread switch was on. firedAtMs is empty and FIRED_HISTORY_RETENTION_MS is 25h, so a fire anywhere in this episode would still be visible — it never fired. And pending is null, so either it never armed or it armed and was cancelled.

                          The wake fiber was demonstrably alive: GET /api/coil/auto-resume?threadId=57e2846c… polls run continuously from 06:07:06Z to 12:52:04Z with no gap over 5 minutes, straight across the 10:40:00Z reset. So this is not "the machine was asleep".

                          I could not narrow it further from the outside, because there is no telemetry for the auto-resume decision at all (see item 3).

                          Narrowed hypotheses

                          1. Armed, then cancelled pre-dispatch.cancelReason runs against a fresh snapshot. thread-advanced should be excluded (the fix from Auto-resume never fires in practice: 'thread-advanced' false cancellation when the limited turn settles during the wait #6: latestTurn is null once the turn settles, and null is not treated as advancement) and progressing should be false after session.exited. That leaves thread-gone — is settledOverride set to "settled", or archivedAt populated, by a gracefulsession.exited? If so, every unattended run that exits cleanly while limited loses its pending resume. That would be the same class of bug as Auto-resume never fires in practice: 'thread-advanced' false cancellation when the limited turn settles during the wait #6/[Bug]: auto-resume is cancelled as "user-took-over" by any user message — including one that was itself rejected by a limit #39: a guard meant to detect human takeover firing on an ordinary lifecycle transition.
                          2. Never armed, because the parser did not read this payload shape as rejected. The rate_limit_info here carries the newer overage fields (overageStatus, overageResetsAt, overageDisabledReason, isUsingOverage). If the verdict parser reads a composite/effective status rather than rate_limit_info.status, an overageStatus:"rejected" variant may not map to verdict.rejected.
                          3. Armed correctly, but at a useless time.resetsAt (10:40:00Z) describes the five_hour bucket. The actual blocker was the individual spend limitorg_level_disabled_until, with overageResetsAt on 2026-09-01. A resume at 10:40 would have fired into a still-closed door and burned a ladder attempt. Auto-resume currently has no representation for "blocked until a human raises a spend cap", which is not a time-based condition at all.

                          Hypothesis 3 holds regardless of which of 1 or 2 is true, and is the one worth designing for.

                          Three separable defects

                          1. A limit stop is recorded as a successful turn.result carries is_error:true and subtype:"success" simultaneously, and the item is status:"completed". Any consumer keyed on subtype sees success. The reactor docstring already acknowledges the upstream shape ("a usage limit does not produce a failed turn (the SDK has no rate-limit result subtype)") and deliberately reads the structured signal instead — but nothing else in the fork does, so the "did my overnight run actually stop?" question has no reliable answer outside the rate-limit stream.

                          2. A scheduled wake tick burns a turn into a closed limit, then the session exits silently. At 06:35:00 the loop's own ScheduleWakeup started a turn; init reported status:"failed", the model never ran, the command was cancelled, and 4m35s later the session exited gracefully. From the model's side the turn produced no output, so no follow-up ScheduleWakeup was armed and the loop was permanently dead. Two asks: (a) don't dispatch a scheduled wake while a rejection is in force and its window has not opened — defer it to the same resetsAt machinery; (b) a graceful exit while limited with a live loop should raise something the user can see the next morning. It produced no notification of any kind. This is closely related to #38 and #42.

                          3. Auto-resume decisions are invisible. Grepping every rotated server.trace.ndjson* for already-pending, not-rejected, capped, autoResume, auto-resume scheduled, and rate_limit_event returns zero hits — the only auto-resume evidence in the traces is the HTTP poll route. There is no record of a planSchedule verdict, a cancelReason, or a fire attempt. #6 and #39 were both "auto-resume silently didn't fire" bugs; a third one is now unresolvable from a full set of logs. Emitting the arm/skip/cancel decision (with reason and resumeAtMs) as a domain event or timeline activity would have made this issue a one-line diagnosis.

                          Suggested direction

                          • Log every planSchedule and cancelReason outcome with its reason. Highest value per line of code here.
                          • Assert which of hypotheses 1/2 applies, using the timeline above as the fixture.
                          • Treat "overage rejected / spend limit / org_level_disabled_until" as a distinct terminal class from a windowed rate limit: do not schedule a time-based resume, mark the thread blocked-on-human, and notify. overageResetsAt two weeks out is not a retry target.
                          • Consider whether a graceful session.exited should preserve rather than invalidate a pending resume.

                          Repro sketch

                          1. Account where five-hour usage is exhausted and org-level overage is disabled (spend cap reached).
                          2. Start a dynamic /loop that fans work out through background Workflow subagents, so turns are long and rate-limit rejections land mid-turn.
                          3. Let it run into the limit unattended.
                          4. Observe: synthetic spend-limit assistant message, resultis_error:true/subtype:"success", session exits gracefully, t3x-auto-resume.json shows pending:null and firedAtMs:[] for the thread, and no resume at the resetsAt boundary.

                          Related

                          Both #6 and #39 were "the guard cancelled a resume that should have fired". This is the third instance of that shape, plus a limit class the feature has no model for.

                          Metadata

                          Metadata

                          Assignees

                          No one assigned

                            Labels

                            No labels
                            No labels

                            Projects

                            No projects

                              Milestone

                              No milestone

                              Relationships

                              None yet

                              Development

                              No branches or pull requests

                              Issue actions

                              , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
                              Skip to content

                              [Bug]: auto-resume never armed despite 3 rejected five_hour events - overage-disabled spend limit, graceful session exit, 6h of an overnight loop lost #118

                              Description

                              @eddy-curly

                              Summary

                              An overnight self-paced /loop run died at 06:34:49Z and never came back. Auto-resume was enabled for the thread and its poller was alive the whole time, but the durable store shows the thread never armed a pending resume and never fired one, despite receiving three account.rate-limits.updated events with status:"rejected" — the exact signal AutoResumeReactor's detection fiber documents as its trigger.

                              The distinguishing feature of this episode versus #6 and #39: the terminal block was not a plain five_hour exhaustion. It was five_hour rejected plus overage rejected with overageDisabledReason:"org_level_disabled_until", surfacing to the model as You've hit your individual spend limit · run /usage-credits to ask your admin for a higher limit.

                              Net cost: ~6 hours of wall clock, and an overnight build that stopped at item 5 of 6 in its first wave. The user found out the next morning.

                              Environment

                              • T3 Code desktop, Windows 11 Pro 10.0.26200. Build installed 2026-08-14 (resources/app.asar, resources/server.asar mtime 20:31).
                              • Thread 57e2846c-9cb1-40d4-8776-e5f7db3fe7b9, provider session 4def7c10-494d-40ef-a685-36ff485c36d6.
                              • claude-opus-5[1m], effort high, runtime mode full-access.
                              • Auto-resume state file: ~/.t3/userdata/t3x-auto-resume.json.
                              • Provider log: ~/.t3/userdata/logs/provider/events.57e2846c-9cb1-40d4-8776-e5f7db3fe7b9.log.

                              What the user was doing

                              A /loop in dynamic mode (self-paced via ScheduleWakeup) driving a multi-wave prototype build, with work fanned out through background Workflow subagents. Explicitly an unattended overnight run.

                              Confirmed timeline

                              All times UTC. Local was UTC-4.

                              TimeEventEvidence
                              06:23:55.882account.rate-limits.updated, status:"rejected", rateLimitType:"five_hour", resetsAt:1786963200 (10:40:00Z), overageStatus:"allowed", isUsingOverage:trueprovider log line 1170
                              06:30:24.452same, overageStatus:"allowed_warning", isUsingOverage:trueline 1261
                              06:34:49.391same, but overageStatus:"rejected", overageDisabledReason:"org_level_disabled_until", overageResetsAt:1788220800 (2026-09-01), isUsingOverage:falseline 1343
                              06:34:49.392Workflow subagent UX-06 dies: "error":"You've hit your individual spend limit · run /usage-credits…"line 1344
                              06:34:50.354Synthetic assistant message, "model":"<synthetic>", content = the spend-limit string, stop_reason:"stop_sequence"line 1356
                              06:34:50.355item.completeditemType:"assistant_message"status:"completed"line 1358
                              06:34:50.362resultis_error:true together with subtype:"success", num_turns:1line 1359
                              06:34:50.876turn.completedline 1361
                              06:35:00.922The loop's ScheduleWakeup fires: command_lifecyclestate:"started", command_uuid 9663690cline 1363
                              06:35:01.161systemsubtype:"init"status:"failed"line 1364
                              06:35:01.855Second synthetic spend-limit assistant messageline 1368
                              06:35:01.857result again is_error:true + subtype:"success"line 1371
                              06:35:02.335Same command_uuidstate:"cancelled"line 1375
                              06:39:37.260session.exited, reason:"Session stopped", exitKind:"graceful"line 1376
                              10:40:00five_hour window reset. Nothing happened.
                              12:44:46session.started — the human came back and resumed manuallyline 1377

                              The core defect

                              Detection is documented as:

                              subscribes once to providerService.streamEvents and, on a Claude account.rate-limits.updated event with status:"rejected", schedules a resume at the structured resetsAt (+ margin)

                              That precondition was met three times. account.rate-limits.updated appears 25 times in this thread's log; the three carrying status:"rejected" are confirmed to be of that canonical type.

                              Walking planSchedule with the 06:23:55 event: verdict.rejected true, pendingResumeAtMs null, windowOpensInFuture true (10:40:00Z > 06:23:55Z), firedInCapWindow 0. That returns {kind:"schedule"}. Yet the store reads:

                              "57e2846c-9cb1-40d4-8776-e5f7db3fe7b9": {
                              "pending": null, "firedAtMs": [], "overridePrompt": null, "enabled": true
                              }

                              enabled:true, so the per-thread switch was on. firedAtMs is empty and FIRED_HISTORY_RETENTION_MS is 25h, so a fire anywhere in this episode would still be visible — it never fired. And pending is null, so either it never armed or it armed and was cancelled.

                              The wake fiber was demonstrably alive: GET /api/coil/auto-resume?threadId=57e2846c… polls run continuously from 06:07:06Z to 12:52:04Z with no gap over 5 minutes, straight across the 10:40:00Z reset. So this is not "the machine was asleep".

                              I could not narrow it further from the outside, because there is no telemetry for the auto-resume decision at all (see item 3).

                              Narrowed hypotheses

                              1. Armed, then cancelled pre-dispatch.cancelReason runs against a fresh snapshot. thread-advanced should be excluded (the fix from Auto-resume never fires in practice: 'thread-advanced' false cancellation when the limited turn settles during the wait #6: latestTurn is null once the turn settles, and null is not treated as advancement) and progressing should be false after session.exited. That leaves thread-gone — is settledOverride set to "settled", or archivedAt populated, by a gracefulsession.exited? If so, every unattended run that exits cleanly while limited loses its pending resume. That would be the same class of bug as Auto-resume never fires in practice: 'thread-advanced' false cancellation when the limited turn settles during the wait #6/[Bug]: auto-resume is cancelled as "user-took-over" by any user message — including one that was itself rejected by a limit #39: a guard meant to detect human takeover firing on an ordinary lifecycle transition.
                              2. Never armed, because the parser did not read this payload shape as rejected. The rate_limit_info here carries the newer overage fields (overageStatus, overageResetsAt, overageDisabledReason, isUsingOverage). If the verdict parser reads a composite/effective status rather than rate_limit_info.status, an overageStatus:"rejected" variant may not map to verdict.rejected.
                              3. Armed correctly, but at a useless time.resetsAt (10:40:00Z) describes the five_hour bucket. The actual blocker was the individual spend limitorg_level_disabled_until, with overageResetsAt on 2026-09-01. A resume at 10:40 would have fired into a still-closed door and burned a ladder attempt. Auto-resume currently has no representation for "blocked until a human raises a spend cap", which is not a time-based condition at all.

                              Hypothesis 3 holds regardless of which of 1 or 2 is true, and is the one worth designing for.

                              Three separable defects

                              1. A limit stop is recorded as a successful turn.result carries is_error:true and subtype:"success" simultaneously, and the item is status:"completed". Any consumer keyed on subtype sees success. The reactor docstring already acknowledges the upstream shape ("a usage limit does not produce a failed turn (the SDK has no rate-limit result subtype)") and deliberately reads the structured signal instead — but nothing else in the fork does, so the "did my overnight run actually stop?" question has no reliable answer outside the rate-limit stream.

                              2. A scheduled wake tick burns a turn into a closed limit, then the session exits silently. At 06:35:00 the loop's own ScheduleWakeup started a turn; init reported status:"failed", the model never ran, the command was cancelled, and 4m35s later the session exited gracefully. From the model's side the turn produced no output, so no follow-up ScheduleWakeup was armed and the loop was permanently dead. Two asks: (a) don't dispatch a scheduled wake while a rejection is in force and its window has not opened — defer it to the same resetsAt machinery; (b) a graceful exit while limited with a live loop should raise something the user can see the next morning. It produced no notification of any kind. This is closely related to #38 and #42.

                              3. Auto-resume decisions are invisible. Grepping every rotated server.trace.ndjson* for already-pending, not-rejected, capped, autoResume, auto-resume scheduled, and rate_limit_event returns zero hits — the only auto-resume evidence in the traces is the HTTP poll route. There is no record of a planSchedule verdict, a cancelReason, or a fire attempt. #6 and #39 were both "auto-resume silently didn't fire" bugs; a third one is now unresolvable from a full set of logs. Emitting the arm/skip/cancel decision (with reason and resumeAtMs) as a domain event or timeline activity would have made this issue a one-line diagnosis.

                              Suggested direction

                              • Log every planSchedule and cancelReason outcome with its reason. Highest value per line of code here.
                              • Assert which of hypotheses 1/2 applies, using the timeline above as the fixture.
                              • Treat "overage rejected / spend limit / org_level_disabled_until" as a distinct terminal class from a windowed rate limit: do not schedule a time-based resume, mark the thread blocked-on-human, and notify. overageResetsAt two weeks out is not a retry target.
                              • Consider whether a graceful session.exited should preserve rather than invalidate a pending resume.

                              Repro sketch

                              1. Account where five-hour usage is exhausted and org-level overage is disabled (spend cap reached).
                              2. Start a dynamic /loop that fans work out through background Workflow subagents, so turns are long and rate-limit rejections land mid-turn.
                              3. Let it run into the limit unattended.
                              4. Observe: synthetic spend-limit assistant message, resultis_error:true/subtype:"success", session exits gracefully, t3x-auto-resume.json shows pending:null and firedAtMs:[] for the thread, and no resume at the resetsAt boundary.

                              Related

                              Both #6 and #39 were "the guard cancelled a resume that should have fired". This is the third instance of that shape, plus a limit class the feature has no model for.

                              Metadata

                              Metadata

                              Assignees

                              No one assigned

                                Labels

                                No labels
                                No labels

                                Projects

                                No projects

                                  Milestone

                                  No milestone

                                  Relationships

                                  None yet

                                  Development

                                  No branches or pull requests

                                  Issue actions