# fix\(server\): preserve usage-limit recovery across empty wake runs · PR \#22775 · pingdotgg/t3code

[View on GitCafe](https://git.cafe/pingdotgg/t3code/pull/22775)

Repository: [pingdotgg/t3code](https://git.cafe/pingdotgg/t3code)

Visibility: public

State: open

Source repository: pingdotgg/t3code

Source branch: fix/issue-16815

Target branch: main

Head commit: 824b0ba8ec6ef6999b3da40106f381dc7825db94

[Discussion and reviews](https://git.cafe/pingdotgg/t3code/pull/22775)

[Commits](https://git.cafe/pingdotgg/t3code/pull/22775/commits?format=markdown)

[Files changed](https://git.cafe/pingdotgg/t3code/pull/22775/diffs?format=markdown)

## Description

---

## Problem

A usage-limited thread loses recovery when a notification wake completes before the reported reset. Fixes [#16815](https://github.com/pingdotgg/t3code/issues/16815).

## Change

Recovery now keeps the limited source through notification wakes completed at or before its reset. Automatic arming, shell state, manual Resume, and Stop select that same source. A wake completed after reset supersedes the limit. This uses completion time, not output-text parsing: even a productive pre-reset notification retains recovery; post-reset completion is authoritative.

## Scope and approval

Fixes [#16815](https://github.com/pingdotgg/t3code/issues/16815)

Triaged bug via the automated issue flow.

Baseline [21da3061aa](https://git.cafe/pingdotgg/t3code/commit/21da3061aa006fc852ea4bd84c31ec084a3afa26): synthetic usage-limit failure followed by completed background wake removes the candidate before arming and after arming. Both regression cases fail. Final [9fd419a6e9](https://git.cafe/pingdotgg/t3code/commit/9fd419a6e9b455f17958325ad3ed696f08f00722): 99/99 focused tests pass, including 22 recovery cases. Scoped server/web/client typechecks, targeted lint and format checks pass.

## Verification

### Reproduction and validation

CI fix: removed the PR-only capture runner, offline fixture, and recipe; uploaded evidence links remain below. The failing assertion counts query-plan lookups, not persisted rows: selecting the latest limited source adds one indexed read per query. Added explicit thread predicates and updated exact lookup counts while retaining all composite-index assertions; no failure write path changed. Claude Opus 5.5 adversarial review: MERGE. Merged current main; [866e273265](https://git.cafe/pingdotgg/t3code/commit/866e2732659877c96e811527095aa3ee84b7481b) passes `check:changed`: format, lint, both knip passes, server GraphQL check, 28 affected-package typechecks, and 185 tests across six files (including the complete ProjectionStore file and round 2 recovery tests). Existing lint warnings and typecheck suggestions only.

Two independent review rounds, Codex GPT-6.1-Sol and Claude Opus 5.5: round 1 found Resume and Stop selection mismatches; both fixed. Round 2 found no confirmed blockers. Proof audit pass 1 derived the matrix; pass 2 found sufficient proof and independently rebuilt R1 with matching core events/states and duplicate receipts.

### Evidence

[Fixture matrix](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/10444e208cf49342/matrix.md). Capture evidence remains in the uploaded artifacts below. The PR-only runner, offline fixture, and recipe were removed from the branch during the CI fix.

Paired actual isolated dev-server captures use BEFORE 21da3061aa006fc852ea4bd84c31ec084a3afa26 and AFTER 9fd419a6e9b455f17958325ad3ed696f08f00722, identical runner hashes, Linux x86_64, Node v24.21.0, UTC, SQLite, disposable homes/ports, synthetic provider events and existing mock peer. Offline EventSink staging is explicitly distinguished from live server observations. No real credentials/quota used.

| History | Before | After |
| --- | --- | --- |
| R1 wake before arm | No candidate/arm/continuation | Limited source armed; duplicate delivery creates one continuation |
| R2 already armed | Stored arm becomes inert | Arm identity survives; one continuation |
| R3 Stop before arm | Limited source not marked stopped | Stop disarms source; sweep preserves Stop; explicit rearm works |
| R4 manual Resume with held queue | Selected/forced continuation rejected | Source accepted once; queued message stays behind continuation |
| R5 successful wake after reset | Completed, stale requests inert | Same; no stale recovery |

R6–R10 ordering, active-wake, input/root identity, date boundaries and cancellation guards use regression tests and supplemental parity diagnostics. R4 active-wake rejection is harness-only, not live-server proof. UI rendering, real Claude quota exhaustion, Windows/macOS/native mobile runtime behavior remain unverified. No visual layout changed.

Public evidence uploaded in round 3; original capture notes retain their historical publication status.

| Evidence | Public file |
| --- | --- |
| capture-index.md | [capture-index.md](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/b6b9c3b20f1d9674/capture-index.md) |
| matrix.md | [matrix.md](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/7a87373aba6f351a/matrix.md) |
| review-1-codex.md | [review-1-codex.md](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/76a93a84f920283e/review-1-codex.md) |
| review-1-claude.md | [review-1-claude.md](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/d0198f153b71ad4c/review-1-claude.md) |
| review-2-codex.md | [review-2-codex.md](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/f9e9ae9c4af06bd2/review-2-codex.md) |
| review-2-claude.md | [review-2-claude.md](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/e355ae41ed3746e3/review-2-claude.md) |
| proof-audit-1.md | [proof-audit-1.md](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/588b9000b39b483e/proof-audit-1.md) |
| proof-audit-2.md | [proof-audit-2.md](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/997e83cb0004574e/proof-audit-2.md) |
| before-transcript.log | [before-transcript.log](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/f5ac8ba8a6da2675/before-transcript.log) |
| after-transcript.log | [after-transcript.log](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/e1b36052e6c315b9/after-transcript.log) |
| capture-summary.json | [capture-summary.json](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/e2d7ea304a475838/capture-summary.json) |
| capture-manifest.json | [capture-manifest.json](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/424661c0c40c7edb/capture-manifest.json) |
| regression-provenance.md | [regression-provenance.md](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/62b6b38092cbdb8c/regression-provenance.md) |
| followup-tests.log | [followup-tests.log](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/6ca12fa0acd1bbc5/followup-tests.log) |
| audit2-tests.log | [audit2-tests.log](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/f0025c9243b70713/audit2-tests.log) |

What else changed: aligned manual Resume and Stop with recovery selection, shared client presentation, successful post-reset supersession, focused regressions, and indexed recovery-source lookups. No schema/provider protocol or production transport changes. All capture servers, disposable homes and throwaway worktrees were removed.

Model and harness: GPT-6.1-Sol through Codex in T3 Code; independent Claude Opus 5.5 review.

## Evidence

_Evidence lead pass at product head [e8fbc9baea](https://git.cafe/pingdotgg/t3code/commit/e8fbc9baea51552c05e76828bb06e36f4a33c60c); PR head [824b0ba8ec](https://git.cafe/pingdotgg/t3code/commit/824b0ba8ec6ef6999b3da40106f381dc7825db94) adds only the evidence scripts. This supersedes the line above saying "UI rendering … remain unverified" and "No visual layout changed": the web banner, Resume thread button and Limited status now have before/after captures. Server rows R1–R5 above still stand: the server files are byte-identical between [866e273265](https://git.cafe/pingdotgg/t3code/commit/866e2732659877c96e811527095aa3ee84b7481b) and the head._

Before = merge-base [8acbefefc5](https://git.cafe/pingdotgg/t3code/commit/8acbefefc51b4d281dcd04108c82baa1ab902b0e), after = [e8fbc9baea](https://git.cafe/pingdotgg/t3code/commit/e8fbc9baea51552c05e76828bb06e36f4a33c60c). Both run the real dev server and web app on isolated, seeded homes. **Every usage-limit failure is a synthetic fixture**, and continuations ran on the Codex mock peer, never a real account. Environment: Ubuntu 24.04.5, Linux 6.1.102 x86_64, Node v26.12.1, headless Chromium 148.0.7778.96 (Playwright 1.60.0), UTC. Per-capture environment lines are in the [capture index](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/92c23f59824462fd/capture-index.md). [Fixture matrix (web/shared)](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/ac367204c2204aa9/matrix.md).

| Row | Before | After |
| --- | --- | --- |
| W0 limited, no wake | Banner, Limited | Same |
| W0a Resume at reset clicked | Shows Cancel auto-resume | Same |
| **W1 notification wake completes before reset** | Banner, Resume thread and Limited all disappear (sidebar: Done) | Banner, Resume at reset, Snooze, Resume thread and Limited all stay |
| **W2 same, auto-resume armed** | No banner; armed state invisible | Banner shows **Cancel auto-resume**; Limited |
| W4 click Resume thread | Button absent | Adds "Continue where you left off."; the run starts and the banner clears |
| W4q Resume with a held queued message | Runs the queued message; the limited run never continues | Continues the limited run first; the queued message stays queued |
| W5 armed thread at reset | No continuation | Exactly one continuation, requested 0.6 s after reset |
| W6 reset passes on screen | No banner throughout | Banner stays; Snooze disables at reset; Resume at reset stays enabled |
| W7 wake completes after reset | No banner | Same (post-reset success supersedes) |
| W8 user turn completes before reset | No banner | Same |

W1, before | after, 1440 dark:
![W1 1440 dark, before left, after right](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/32bdd685d4d8a0a3/W1-1440-dark-side.png)
![W1 1440 dark pixel diff](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/0516fcaf22e52910/W1-1440-dark-diff.png)

W2, before | after, 1440 light:
![W2 1440 light, before left, after right](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/6557dd3f8487a3ff/W2-1440-light-side.png)
![W2 1440 light pixel diff](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/01bdd849dbe35dac/W2-1440-light-diff.png)

W1, before | after, 390 dark:
![W1 390 dark, before left, after right](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/240f6658999740c5/W1-390-dark-side.png)

W6, after, Snooze before vs after reset:
![W6 after: Snooze enabled before reset, disabled after](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/5b2218f8d2bb15bf/W6-after-snooze-pre-vs-post-reset.png)

Recordings, with timestamped `.srt` narration giving UTC wall-clock and the reset time:
- W4: [before](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/68aff7358f81854d/W4-before.mp4) ([srt](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/fc3daafd81a0e00f/W4-before.srt)) · [after](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/a36378ebebefbd4f/W4-after.mp4) ([srt](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/e6f8ed7fd410a947/W4-after.srt))
- W4q: [before](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/4a42445d2231513a/W4q-before.mp4) ([srt](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/c4b310bedbe4405e/W4q-before.srt)) · [after](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/6b4a199f862b1ab7/W4q-after.mp4) ([srt](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/8d9d3c983c989c79/W4q-after.srt))
- W5: [before](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/628f6941746604a9/W5-before.mp4) ([srt](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/aa5da2539c13960e/W5-before.srt)) · [after](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/bf99287c81b7909e/W5-after.mp4) ([srt](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/c2da4111cd7f699b/W5-after.srt)) · [db-evidence.txt](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/144025cd061a9aa0/db-evidence.txt)
- W6: [before](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/f88628470cb3a03e/W6-before.mp4) ([srt](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/ab333cebf66c01e2/W6-before.srt)) · [after](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/96312d4239526552/W6-after.mp4) ([srt](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/6bf37274f8dcc122/W6-after.srt))
- W0a: [before](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/cab284fc173f5f2e/W0a-before.mp4) ([srt](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/1f33c7dece7d4753/W0a-before.srt)) · [after](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/60f8d889c95cd2a4/W0a-after.mp4) ([srt](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/2a394d5a35b527ba/W0a-after.srt))

The capture index has every still (W0, W1, W2, W7, W8 at 1440 and 390, light and dark), each with a side-by-side and a pixel diff, plus [pixel counts](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/146c4cfdf04c9278/diff-pixel-counts.txt).

**What else changed:** nothing beyond the rows above. Every non-zero diff on a control row has a cause outside this PR:
- Other seeded threads' sidebar pills differ (Limited vs Done, and later Working).
- Reset-time seconds differ because the two homes were seeded about 4 s apart.
- At 390, W7/W8 show a focus ring on a toast button.

Seed and driver scripts: `apps/server/scripts/usage-limit-web-evidence-{seed.ts,seed-all.sh,stills.mjs,record.mjs}`, added in [824b0ba8ec](https://git.cafe/pingdotgg/t3code/commit/824b0ba8ec6ef6999b3da40106f381dc7825db94). Rebuild W1 with `node apps/server/scripts/usage-limit-web-evidence-seed.ts --home <h> --row W1 --stage init` and then `--stage wake`, run `vp run dev --home-dir <h>`, then `usage-limit-web-evidence-stills.mjs <pairingUrl> <out> after W1`.

<details><summary>Verification log</summary>

- Proof audit pass 1 (Claude Opus 5.5) derived the web/shared matrix: [matrix.md](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/ac367204c2204aa9/matrix.md).
- Proof audit pass 2 (Claude Opus 5.5): **SUFFICIENT** for server (carried over), web and shared/client-runtime. Mobile is N/A by path because no `apps/mobile` file changed. It checked SHAs row by row and matched narration against frames extracted at the `.srt` timestamps. It rebuilt W1 from the committed scripts: same run state, and the 1440 dark still matches. Its 1440 light still came out blank because `stills.mjs` waits a fixed 2.5 s. [proof-audit-2.md](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/02ef3362387c8546/proof-audit-2.md).
- Tests at head: `threadExecution.test.ts` 11/11, `runtimeLayer.test.ts` 88/88. The `apps/server` typecheck, which includes the new scripts, passes locally. Lint, fmt and knip are clean.
- CI at 824b0ba8ec: 28 passing, 2 failing. `Build and Typecheck` fails, and `Check` is only the aggregator of every job. The failure does not reproduce in the server typecheck; the job also runs `build:desktop` and the repo-wide typecheck, which were not re-run locally. Not yet triaged.
- Unverified, web: W3 (the wake while running) was dropped because a running wake can only be written offline; W1/W2 and the server tests cover the end state. The W5 recording starts after reset, so the outcome rests on its DB rows. At 390, the expanded composer stack and sidebar sheet were not captured.
- Unverified, mobile: N/A by path. Its recovery card reads the same shared helper, so it should follow web, but it was not run.
- Not covered: real Claude quota exhaustion, Windows/macOS desktop.
- Model and harness: Claude Opus 5.5 (evidence lead, proof auditors, capturer) through Claude Code in T3 Code.

</details>

Fixes #16815
GitCafe: https://git.cafe/pingdotgg/t3code/issues/16815

