# fix\(web\): SSH environments on an older reused remote server explain the version mismatch instead of reconnecting forever · PR \#23647 · pingdotgg/t3code

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

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

Visibility: public

State: merged

Source repository: pingdotgg/t3code

Source branch: fix/issue-16984

Target branch: main

Head commit: 31019fac0f9d55584cfae7e66c6b13ab938ca451

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

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

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

## Description

---

## Problem

After a desktop update, an SSH environment can stay on "<env> is reconnecting" forever. When the remote host already has a T3 server running that the desktop did not start, the launcher adopts it whatever its version. The desktop's newly installed runner then mints the pairing token into the shared database. An older server cannot decode the newer scopes in that grant, so `POST /oauth/token` returns 500 `access_token_issuance_failed` on every attempt. The client treated that as a transient failure and kept retrying without saying why.

Repro with the real Linux release builds: start `t3 v0.0.46-nightly.20261004.2652 serve --base-dir <home>`. Mint a token against the same home with the `20261007.2787` CLI (`auth pairing create --json`, 13 scopes). Exchanging it returns HTTP 500. A token minted by the running Oct 4 build exchanges fine.

## Change

In `provisionDesktopSshEnvironment`, a bearer-bootstrap failure becomes a blocked connection with an explanation when both of these hold:
- the desktop IPC reports `[ssh_http:500]`;
- the remote descriptor's `serverVersion` differs from this client's version.

Blocked means no backoff loop. The message names both versions and says to restart the remote T3 server on the desktop's version, then retry. A blocked connection retries on the next wake signal, so it reconnects once the server has been restarted, without re-enabling the environment. Every other bootstrap failure behaves as before: matching versions, failures with no status or a non-500 status, and cancellation. A one-line comment on the desktop error notes that the renderer matches that prefix.

## Scope and approval

Fixes https://github.com/pingdotgg/t3code/issues/16984
GitCafe: https://git.cafe/pingdotgg/t3code/issues/16984
Triaged bug via the automated issue flow.

## Verification

- `vp test run apps/web/src/connection/platform.test.ts`: 16 passed.
  - New: `blocks with both versions when a server on another build rejects pairing` fails on `main`. It gets `ConnectionTransientError` with `Could not prepare the SSH environment: [ssh_http:500] …` instead of `ConnectionBlockedError`.
  - New: `keeps retrying pairing failures that are not a version mismatch` covers a matching version with a 500, and differing versions with no HTTP status.
- `vp run check:changed`: format, lint, knip, typecheck (2 packages) and tests all pass.
- Server-side runtime transcript with the real release archives (linux-x64), access token redacted:

```text
$ curl -s 127.0.0.1:38917/.well-known/t3/environment | jq -r .serverVersion   # running server (Oct 4)
0.0.46-nightly.20261004.2652
$ t3-20261007 auth pairing create --base-dir <same home> --json   # what the updated desktop's runner does
13 scopes
$ POST /oauth/token (token-exchange) with that credential
{"_tag":"EnvironmentInternalError","code":"internal_error","reason":"access_token_issuance_failed",...} HTTP 500
$ t3-20261004 auth pairing create ... ; POST /oauth/token   # control: token minted by the running build
{"access_token":"<redacted>","issued_token_type":"urn:ietf:params:oauth:token-type:access_token","token_type":"Bearer",...}
```

Not checked: there is no before/after capture of the desktop connection banner over a real SSH session. Reproducing it end to end needs an SSH account whose `~/.t3` is not this machine's live T3 install, and none was available here. The changed decision is covered by the tests above, and the 500 by the transcript.

🤖 Generated with [Claude Code](https://claude.com/claude-code) (Claude Opus 5.5 in T3 Code; reviews by Claude Opus 5.5 and Claude Sonnet 5.5)


## Takeover

No code changes; evidence and verification added below.

## Evidence

Rendering: Linux Electron desktop (`vp run dev:desktop`, Electron 44.9.3) on Xvfb 1440×900×24, captured with ImageMagick `import`/ffmpeg x11grab. Before = merge-base `f054d1b801`, after = head `31019fac0f`. Both sides run the same seeded `statev2.sqlite` (sha256 `9657a7f8…`). The remote is a real SSH host (a private sshd on 127.0.0.1:2222 with its own HOME) running the released `t3 0.0.46-nightly.20261004.2652` from npm as an already-running server. The desktop's own runner mints the pairing token, as in the issue. Driver scripts: [driver.tgz](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/c126763adb9f895b/driver.tgz) (sha256 `356d5c4b…9618`). Fixture matrix: [matrix.md](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/8b8bb3c6db5015bf/matrix.md).

| Row | State | Captured |
|---|---|---|
| R1a | Add environment over SSH, remote on another build answers 500 | before/after, 1440+390, light+dark |
| R1b | Saved SSH environment, remote swapped to the older build | before recording + stills; after stills (banner) |
| R5 | Remote restarted on the matching build, then Retry now | before recording only |
| R2, R3a | Same version + 500; different version + 502 | not captured |
| R3b | Different version, failure with no HTTP status | after log/steps only (ensure timeouts keep retrying) |

**R1a: Add environment dialog**

| Before | After |
|---|---|
| ![R1a before 1440 light](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/dd64ccabce87f75a/R1a-before-1440-light.png) | ![R1a after 1440 light](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/1ce989e171b92e3b/R1a-after-1440-light.png) |
| ![R1a before 390 dark](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/904c79f85938702f/R1a-before-390-dark.png) | ![R1a after 390 dark](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/c56b272022b58e0c/R1a-after-390-dark.png) |

**R1b: composer banner on a thread in the SSH environment's project.** Before: "fs-29 is reconnecting", retrying forever. After: "fs-29 is offline" with Reconnect.

| Before | After |
|---|---|
| ![R1b banner before 1440 light](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/70ac0d79162e2987/R1b-S3-before-1440-light.png) | ![R1b banner after 1440 light](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/0424fd87a009ff00/R1b-S3-after-1440-light.png) |
| ![R1b banner before 390 dark](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/0f08eeeb4d2bd58f/R1b-S3-before-390-dark.png) | ![R1b banner after 390 dark](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/0a4575c4bd28a19a/R1b-S3-after-390-dark.png) |

Recordings and steps (before):
- R1b: [R1b-before.mp4](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/6dd2424a82b7f5dc/R1b-before.mp4), 59.4 s, [steps](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/5313e7dfbe300fbe/R1b-before.steps.txt). The row goes "retrying in Ns · attempt 2…5" and the banner reads "is reconnecting".
- R5: [R5-before.mp4](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/d592e1becbd1ef1f/R5-before.mp4), 51.6 s, [steps](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/333ef37743103478/R5-before.steps.txt). Connected about 5 s after Retry now.
- Settings row Details, before: ![R1b settings before](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/b9ee4169c8b4c739/R1b-S2-before-1440-light.png)

What else changed between the before and after captures: nothing outside the error text and the banner's reconnecting→offline state. A provider-update toast appears on both sides and was dismissed before each still.


**Round 5 completion — supersedes the round-1 missing cells and “Unverified” list below.** Head [31019fac0f](https://github.com/pingdotgg/t3code/commit/31019fac0f9d55584cfae7e66c6b13ab938ca451); merge-base re-derived as [f054d1b801](https://github.com/pingdotgg/t3code/commit/f054d1b8018721bd997d99cbb918a7b17bf098a4). One installed worktree, fresh synthetic state, Linux Electron/Xvfb. Real old released server for R1a/R1b/R5; deterministic HTTP fixture behind real SSH for controls. No new tests or reviews.
- R1b after, 97.2s: [R1b-after.mp4](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/ca2c4a18c6ed7be9/R1b-after.mp4) [R1b-after.steps.txt](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/0679861373726f63/R1b-after.steps.txt); attempt 2 stays blocked, then offline + Reconnect. Settings row with expanded version explanation: ![R1b-S2-after-1440-light.png](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/8285b41ab8f19bf8/R1b-S2-after-1440-light.png)
- R5 after, 50.9s: [R5-after-final.mp4](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/db11965db8eeb84b/R5-after-final.mp4) [R5-after-final.steps.txt](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/a75e8914400a4d62/R5-after-final.steps.txt); old server stops at 0s, matching server listens at 5s, Retry now at 10s, Connected by 14s; banner clears. Explicit retry, without toggling the environment.
| Control | Before (merge-base) | After (head) |
|---|---|---|
| R2: same version + pairing 500; both retry through attempt 5 | [R2-before.mp4](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/4718078936051d9f/R2-before.mp4) [R2-before.steps.txt](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/620a58bda1ba2e1f/R2-before.steps.txt) ![R2-before-1440-light.png](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/e78144278b94530e/R2-before-1440-light.png) | [R2-after.mp4](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/ac32719972f72248/R2-after.mp4) [R2-after.steps.txt](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/32fbe4bf0e7b8a14/R2-after.steps.txt) ![R2-after-1440-light.png](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/ef95300215bd63b0/R2-after-1440-light.png) |
| R3a: old version + pairing 502; both retry through attempt 5 | [R3a-before.mp4](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/2153a63fa5aeaf58/R3a-before.mp4) [R3a-before.steps.txt](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/3ba10653aee89d51/R3a-before.steps.txt) ![R3a-before-1440-light.png](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/7a96759354c5e4d5/R3a-before-1440-light.png) | [R3a-after.mp4](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/1d174ef32e6469de/R3a-after.mp4) [R3a-after.steps.txt](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/4bea43e0bbfb1da8/R3a-after.steps.txt) ![R3a-after-1440-light.png](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/d028c8e0150afc92/R3a-after-1440-light.png) |
| R3b: old version + pairing socket close/no status; both retry through attempt 5 | [R3b-before.mp4](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/974bb411e74fbe0c/R3b-before.mp4) [R3b-before.steps.txt](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/61f819795a6843e9/R3b-before.steps.txt) ![R3b-before-1440-light.png](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/c3eee483f699ed5b/R3b-before-1440-light.png) | [R3b-after.mp4](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/838ef688441fb5c9/R3b-after.mp4) [R3b-after.steps.txt](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/4641bf3998884998/R3b-after.steps.txt) ![R3b-after-1440-light.png](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/bbadbe3d6472dabd/R3b-after-1440-light.png) |
Independent head R1a recapture: ![R1a-rebuilt-after-1440-light.png](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/521a93fab5830999/R1a-rebuilt-after-1440-light.png); delta from the original after still is 145/1,296,000 pixels at >16/255 (0.0112%), including fs-29→fs-36.
Claim corrections: published round-1 variants are 1440/light and 390/dark (not all four combinations); narrow stills are cropped. The version explanation is in Settings Details, not the row headline or composer banner. “Host version” remains stale at 0.1.1 while the error names the old build. New control/Settings diffs include timing, scroll and fixture differences; the original “nothing else changed” statement is not a claim of zero pixel differences.
Diffs below: zero threshold amplifies every nonzero RGB-channel delta to magenta; stated threshold is max RGB-channel delta >16/255. No alignment, crops or masks. Includes every published still pair and the independent after/after rebuild.
| Pair | Threshold 0, amplified | Threshold >16/255 |
|---|---|---|
| R1a 1440/light | ![R1a-diff-threshold-0-1440-light.png](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/d5fb793a9ccd955c/R1a-diff-threshold-0-1440-light.png) | ![R1a-diff-threshold-16-1440-light.png](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/f1f8ad987d871e86/R1a-diff-threshold-16-1440-light.png) |
| R1a 390/dark | ![R1a-diff-threshold-0-390-dark.png](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/08f352db17f661e9/R1a-diff-threshold-0-390-dark.png) | ![R1a-diff-threshold-16-390-dark.png](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/53500474776b03bd/R1a-diff-threshold-16-390-dark.png) |
| R1b banner 1440/light | ![R1b-S3-diff-threshold-0-1440-light.png](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/5c18222e64f67aea/R1b-S3-diff-threshold-0-1440-light.png) | ![R1b-S3-diff-threshold-16-1440-light.png](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/9692ed4b18c9c7e3/R1b-S3-diff-threshold-16-1440-light.png) |
| R1b banner 390/dark | ![R1b-S3-diff-threshold-0-390-dark.png](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/83a34afe8c8706c9/R1b-S3-diff-threshold-0-390-dark.png) | ![R1b-S3-diff-threshold-16-390-dark.png](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/0994ba8adbf80d89/R1b-S3-diff-threshold-16-390-dark.png) |
| R1b Settings Details | ![R1b-S2-diff-threshold-0-1440-light.png](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/2837f2a92b499b97/R1b-S2-diff-threshold-0-1440-light.png) | ![R1b-S2-diff-threshold-16-1440-light.png](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/23b3ea3075c47fa0/R1b-S2-diff-threshold-16-1440-light.png) |
| R2 | ![R2-diff-threshold-0-1440-light.png](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/f15f6de01bb4fba0/R2-diff-threshold-0-1440-light.png) | ![R2-diff-threshold-16-1440-light.png](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/5d7bb76384466f54/R2-diff-threshold-16-1440-light.png) |
| R3a | ![R3a-diff-threshold-0-1440-light.png](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/69c50a15479a4299/R3a-diff-threshold-0-1440-light.png) | ![R3a-diff-threshold-16-1440-light.png](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/c034ad06797aa044/R3a-diff-threshold-16-1440-light.png) |
| R3b | ![R3b-diff-threshold-0-1440-light.png](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/6dcb1e806a83b8bb/R3b-diff-threshold-0-1440-light.png) | ![R3b-diff-threshold-16-1440-light.png](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/5b5f8c6579498cf5/R3b-diff-threshold-16-1440-light.png) |
| R1a original after ↔ rebuilt after | ![R1a-rebuild-diff-threshold-0.png](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/6686b5d33a611c08/R1a-rebuild-diff-threshold-0.png) | ![R1a-rebuild-diff-threshold-16.png](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/c0d459ab74e3d0f1/R1a-rebuild-diff-threshold-16.png) |
Proof audit pass 2: [audit-pass-2.md](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/c57e69b0816efe9b/audit-pass-2.md) — **VERDICT: SUFFICIENT**. Six captured rows / eight matrix rows; R4 and R6 are explicit exclusions from capture scope. All requested missing rows are resolved. The author’s historical “Not checked” paragraph is superseded; prior test/review results were not rerun.

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

- Reviews (one round, head `31019fac0f`): Claude Sonnet 5.5 → MERGE; Claude Opus 5.5 → MERGE. Neither found a bug or any change to what pairing or auth accepts. Both confirmed that blocked waits for a wake signal or an explicit retry, then re-pairs.
- Real-runtime check at head: the Add flow against the 20261004 build returns `[ssh_http:500]` from `bootstrap-ssh-bearer-session`, and the dialog shows the version message. The merge-base shows the raw `[ssh_http:500]` error.
- In the after R1b run, the explanation appears only after the attempt that reaches the 500. Earlier attempts hit unrelated, status-less ensure failures (remote swap race; SSH timeouts under host load), and those correctly kept retrying ([steps](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/09d0730f91c440f7/R1b-after.steps.txt)).
- Unverified: a clean after recording for R1b and R5; an after Settings-row still showing the blocked explanation; R2 (same version + 500); R3a (502); diff images; proof audit pass 2. Reason: the run went over its time budget while the shared VM was saturated by an unrelated Android build.
- Model: Claude Opus 5.5 in T3 Code (lead, audit); reviews by Claude Opus 5.5 and Claude Sonnet 5.5.


- Round 5 drivers: [drivers-r5.tgz](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/08bf6e5fab4479eb/drivers-r5.tgz) (archive sha256 `2a16268b4c3402efc1009e3eb7c5c62400f7121ecbb6c291750e151c0fd2a575`); metadata-only HTTP transcript, full decode checks, diff metrics, capture hashes, source hashes and PID/cleanup log: [verification-r5.log](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/ba1666364a4cec13/verification-r5.log). Each round-5 driver sha256: `setup.py` = `bc7ee115081f9aacc24a415c051bb475ad4baf60c8270910cb431b204f1e3e8e`; `launch.sh` = `14eb96a71a05023c3e3148957e7e5d7659ec0ff2ac19adec62fcd8f2ff2f45d1`; `record-r1b.sh` = `0959dd3b70c1f578bb7218347f0013cda75fab7abeac283949c755c693e23aa3`; `record-r5.sh` = `956fd734059e29e207bdc261bafcb49231ee9e1b14068abb501941c3882308ee`; `controls.sh` = `32f8b2e62066891eb2313db3c94813112d0f2c01e9feea6e129da7df8fcfa675`; `controls-R2-after-captured.sh` = `e10ec48ece4663db3bea7fcdb789bf897b6dd44900b8567e9d0d1b4f926a5481`; `driver/cdp.mjs` = `ff970c2eb8aa798e91866ef8109feb36dd6fb5cf7adf3802623188d90ad1a31b`; `fake-remote.mjs` = `c3a93750bf3761a5e2359357160a1e4ea4e9062c0181a9b9778642c062ffc679`; `diff.py` = `ccb92aefe257ac91e6391e77052204dcb9947f206df5804e040ceb2d23a3028c`; `pids.py` = `57b6e806f79f26331ec044539b3be40a8dd1cd80b52cca2275227d051c465c02`; `stop-port.py` = `6a788682a91f020362611d307f9c90a0c3d9840de760e7a03140e639e354c111`; `audit-artifacts.py` = `8bf873593d2d2f4c462dd5833bdd9e42ca3d6cf49f3b00c46e224d8e7d4bfc17`; `inspect-videos.py` = `5cff910bab2cfeb33e5f06620589dfe132d8b99d529ceda4b63d20a4536b6687`; `bin/ssh` = `d40a4f8c6a9e6a370263ff57ae316c2a081a54038bdd68920b5f4985b764e10d`; `bin/pkill` = `470555b8831d1ba5cbc878fed047d5e31ee2330e43a643214ef58e2b2d214290`; `ssh/forced.sh` = `27361afee4882d50b8ba4551fb9fb9665ca49f2a684ac8ee49252175b5d4e101`.
- Operational exceptions: initial automatic dev seeding began reading/copying the live database; stopped immediately, aborted copy deleted, then fresh state with `T3CODE_DEV_SEED=0`. An orphan recorder PID was recovered from a process listing before cwd-checked SIGINT, breaching the requested PID-discovery rule. No database or aborted-seed data is uploaded. Missing `xdotool` and `libsecret-1-dev` were installed; interrupted/corrupt capture attempts were discarded. Teardown: task servers, sshd, Xvfb and Electron stopped through retained sessions/recorded port-owner PIDs and cwd checks; created worktree removed. Disk stayed ≥10 GB during capture (start 12 GB rounded). Model/harness: GPT-6.1-sol, Codex in T3 Code.

</details>

