The four headline bugs from the review series are already merged (#41, #42, #45) along with the reviewer's double-click finding (#49). These are the three defects that were flagged and deliberately left open, now closed.
The SPA destroyed a pane on any dropped frame
There was no reconnect at all — createAttachConnection built one socket, and ws.onclose went straight to onClosed → closePane. The TUI reconnects forever on the same endpoint; the browser gave up on the first blip.
Compounding it, session death reached the UI on two uncoordinated channels: the row from the events socket, the pane from ws.onclose, with nothing reconciling them. Either could happen without the other.
The catalog is now the authority for both. shouldRetryClose decides from session status:
situation
retry?
why
Running, code 1006
yes
the link dropped, the session is alive
Dead
no
PtyBridge::seal() closed the stream; nothing more is coming
absent from catalog
no
it is gone
code ≥ 4000
no
a relayed daemon rejection — retrying loops on it
code 1000
no
deliberate close
Backoff is 500ms doubling to 10s, reset on open, cancelled on dispose. The shell also closes any pane whose session has left the catalog, which is the reconciliation that was missing.
The policy lives in reconnect.ts rather than inside attach.ts, because attach.ts needs a DOM and an xterm instance — this way the decision table above is what the tests actually exercise, and main.ts calls the same function.
kill_session had a TOCTOU that produced the wrong status
It checked has() before kill(). kill already reports a miss as SessionError::NotFound → 404, so the pre-check added nothing except a window for the liveness watcher to reap the session between the two awaits — turning a 404 into a 502. Removed.
I left the equivalent pre-check on attach_ws: there it is load-bearing, since the 404 must be decided before the upgrade, and attach::serve already compensates for the race with a clean close.
with_spawn_history failed silently
It logged and carried on when Arc::get_mut returned None, producing a daemon whose recent-dirs picker is permanently empty with nothing to show for it. It panics now — a construction-order mistake, at startup, in code that runs exactly once.
Verification
cargo clippy --all-targets -- -D warnings 0
cargo test --all 322 passed, 0 failed
pnpm -C web test 43 passed (was 36)
pnpm -C web typecheck clean
pnpm -C web build ok
The four headline bugs from the review series are already merged (#41, #42, #45) along with the reviewer's double-click finding (#49). These are the three defects that were **flagged and deliberately left open**, now closed.
### The SPA destroyed a pane on any dropped frame
There was no reconnect at all — `createAttachConnection` built one socket, and `ws.onclose` went straight to `onClosed` → `closePane`. The TUI reconnects forever on the same endpoint; the browser gave up on the first blip.
Compounding it, **session death reached the UI on two uncoordinated channels**: the row from the events socket, the pane from `ws.onclose`, with nothing reconciling them. Either could happen without the other.
The catalog is now the authority for both. `shouldRetryClose` decides from session status:
| situation | retry? | why |
|---|---|---|
| `Running`, code 1006 | yes | the link dropped, the session is alive |
| `Dead` | no | `PtyBridge::seal()` closed the stream; nothing more is coming |
| absent from catalog | no | it is gone |
| code ≥ 4000 | no | a relayed daemon rejection — retrying loops on it |
| code 1000 | no | deliberate close |
Backoff is 500ms doubling to 10s, reset on open, cancelled on dispose. The shell also closes any pane whose session has left the catalog, which is the reconciliation that was missing.
The policy lives in `reconnect.ts` rather than inside `attach.ts`, because `attach.ts` needs a DOM and an xterm instance — this way the decision table above is what the tests actually exercise, and `main.ts` calls the same function.
### `kill_session` had a TOCTOU that produced the wrong status
It checked `has()` before `kill()`. `kill` already reports a miss as `SessionError::NotFound` → 404, so the pre-check added nothing except a window for the liveness watcher to reap the session between the two awaits — turning a 404 into a 502. Removed.
I left the equivalent pre-check on `attach_ws`: there it is load-bearing, since the 404 must be decided *before* the upgrade, and `attach::serve` already compensates for the race with a clean close.
### `with_spawn_history` failed silently
It logged and carried on when `Arc::get_mut` returned `None`, producing a daemon whose recent-dirs picker is permanently empty with nothing to show for it. It panics now — a construction-order mistake, at startup, in code that runs exactly once.
<details>
<summary>Verification</summary>
```
cargo clippy --all-targets -- -D warnings 0
cargo test --all 322 passed, 0 failed
pnpm -C web test 43 passed (was 36)
pnpm -C web typecheck clean
pnpm -C web build ok
```
</details>
SPA attach had no reconnect at all. Any dropped frame closed the socket,
`onClosed` fired, and the pane was destroyed permanently — the TUI
reconnects forever on the same endpoint. Session death also reached the UI
on two uncoordinated channels: the row from the events socket, the pane
from `ws.onclose`, with nothing reconciling them, so either could happen
without the other.
The catalog is now the authority. `shouldRetryClose` decides from session
status: Running means the link dropped, so retry with 500ms-to-10s
backoff; Dead means the PTY stream was sealed and nothing more is coming;
absent means it is gone. Application closes (4000+, a relayed daemon
rejection) and clean closes are never retried. The shell also closes any
pane whose session has left the catalog. The policy lives in reconnect.ts
so it is tested without a DOM.
kill_session checked `has()` before `kill()`, which only opened a window
for the liveness watcher to reap in between — turning a 404 into a 502.
`kill` reports the miss itself.
with_spawn_history logged and carried on when `Arc::get_mut` failed,
producing a daemon whose recent-dirs picker is permanently empty with
nothing to show for it. It panics now: that is a construction-order
mistake, at startup, in code that runs once.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
The four headline bugs from the review series are already merged (#41, #42, #45) along with the reviewer's double-click finding (#49). These are the three defects that were flagged and deliberately left open, now closed.
The SPA destroyed a pane on any dropped frame
There was no reconnect at all —
createAttachConnectionbuilt one socket, andws.onclosewent straight toonClosed→closePane. The TUI reconnects forever on the same endpoint; the browser gave up on the first blip.Compounding it, session death reached the UI on two uncoordinated channels: the row from the events socket, the pane from
ws.onclose, with nothing reconciling them. Either could happen without the other.The catalog is now the authority for both.
shouldRetryClosedecides from session status:Running, code 1006DeadPtyBridge::seal()closed the stream; nothing more is comingBackoff is 500ms doubling to 10s, reset on open, cancelled on dispose. The shell also closes any pane whose session has left the catalog, which is the reconciliation that was missing.
The policy lives in
reconnect.tsrather than insideattach.ts, becauseattach.tsneeds a DOM and an xterm instance — this way the decision table above is what the tests actually exercise, andmain.tscalls the same function.kill_sessionhad a TOCTOU that produced the wrong statusIt checked
has()beforekill().killalready reports a miss asSessionError::NotFound→ 404, so the pre-check added nothing except a window for the liveness watcher to reap the session between the two awaits — turning a 404 into a 502. Removed.I left the equivalent pre-check on
attach_ws: there it is load-bearing, since the 404 must be decided before the upgrade, andattach::servealready compensates for the race with a clean close.with_spawn_historyfailed silentlyIt logged and carried on when
Arc::get_mutreturnedNone, producing a daemon whose recent-dirs picker is permanently empty with nothing to show for it. It panics now — a construction-order mistake, at startup, in code that runs exactly once.Verification