Files
orca/src/main/ssh
Neil a7b1da9e14 fix(ssh): a reattach may bind a pane but must never create one (#16751)
`reattachKnownPtys` treats every non-terminated lease as live and calls
`persistPtyBinding`, which had no way to say "bind only". Two of its branches
then rebuild UI the user is not asking for:

- `pty-binding-persistence.ts:133-143` -- `if (args.incarnationId)`
  unconditionally deletes the pane's close tombstone.
- `:145-160` -- on a tab-not-found it mints one via
  `createMinimalPersistedTerminalTab`. The in-code comment names its only
  intended caller: "pty:spawn can beat the debounced writer." Spawn. Reattach
  took the same branch.

This is the mechanism canceled ticket STA-4268 described: "Leases have no pane
incarnation and upsert only by target/PTY. Every nonterminal lease is reattached;
frozen coordinates are passed to persistPtyBinding, which creates and flushes
missing tabs and layout leaves." It was fixed in #13326, reverted by #14361,
re-applied by #14384, and reverted again by #14395 (opened and merged nine
seconds apart, empty commit body). `grep -rn "mayCreate" src/` returns nothing on
main -- the mechanism is genuinely out of the tree.

Four parts:

1. `mayCreate` (default true). When false, one pre-mutation check mirrors all
   four creating branches and returns `false` without mutating, so a refusal
   leaves nothing half-written.

2. The authority gate -- the part both prior attempts lacked, and probably why
   both were reverted. `mayCreate: false` alone refuses in two situations: "the
   user closed it" AND "the renderer has not published its layout yet". The
   second is routine on disconnect->reconnect and is almost certainly the #14361
   tab-loss mechanism. The fence was not wrong; it was UNCONDITIONED. It is now
   passed only when `hasHostAuthoritativeTerminalMembership()` says the persisted
   membership speaks for this worktree, reusing the function already guarding the
   same question at `orca-runtime.ts:8608`. Losing a tab is worse than keeping a
   duplicate, so an unauthoritative session still gets the creating write.

   Authority is read from `local` because that is the partition the write lands
   in -- it is local's absence being interpreted. But a pane the `ssh:<target>`
   partition still holds is not gone, so it keeps its creating write; refusing
   there would strand a live pane behind a binding reattach can no longer reach.
   (SSH spawns bind into `ssh:<target>` while this reattach binds into `local` --
   GH #12721/#12723, STA-3980. This does not fix that split; it refuses to judge
   from one side of it.)

3. `findTerminalTabIdForLeaf` -- bind resolves the tab from the live layout
   instead of the lease's frozen `tabId`. Only the leaf half of a pane key is
   remint-stable: `detachTerminalPaneToTab` moves a live pane into a new tab, so
   a stored tabId names the tab the pane left. Identity vs location.

4. Pane-keyed supersession -- retires siblings on `(targetId, worktreeId,
   leafId)` to `expired`, guarded by the durable binding, which reads `local`
   then `ssh:<target>` so it is correct whichever partition the binding landed
   in. `upsertSshRemotePtyLease` matched `(targetId, ptyId)` alone
   (`ssh-pty-lease-operations.ts:34-36`), so a new relay pty id on reattach minted
   a SECOND lease instead of updating the first, leaving the predecessor
   non-terminated with nothing to retire it.

On refusal the lease goes `expired`, never `terminated` -- `expired` records that
this shell has no surface to reach it through; `terminated` would assert an exit
nothing here observed, which `docs/reference/ssh-execution-boundary.md` forbids.
The remote process is left running.

Deliberately NOT done:
- A collision guard for `upsertSshRemotePtyLease`. Built, tested, and REMOVED --
  its own test passed with the guard disabled, i.e. vacuous. Telling "same lease"
  from "recycled id on a different shell" needs a relay-start identity, which
  would be a twelfth per-tab identity concept; the codebase already carries
  eleven (784 refs) that SSH-v3 Phase 3 deletes. Left as an in-code NOTE. This
  handles lease DIVERGENCE, not COLLISION.
- A port of #13324. Its own authors deleted its load fold and reverted its
  local-only reader in #13326 ("a headless-owned pane still gets a vote before
  its lease is retired"); porting it ships a state-destroying migration they
  removed.
- `bindPaneShell` from #13325. Its purpose is making `isSupersededPtyId` live,
  and that fence does not exist in main. It would have been a refactor plus a
  silently-ignored `mayCreate` -- TS drops excess props through spreads
  (verified), which is why `mayCreate` is passed as a conditional spread here.

Honesty about scope: this does NOT close the daily-tab-growth report. The
deterministic e2e repro (`ssh-lost-kill-tab-resurrection.spec.ts`, later in this
stack) is byte-identical before and after, and instrumentation shows why -- in
that scenario `restoreReattachedPtyRuntime` is never called at all
(`CREATING TAB` 9 hits, `reattach gate` 0, `BYPASS` 0). The fence is on a path
that bug does not take. It is a real, separately-provable defect; it is not the
headline fix, and must not be claimed as one.

Evidence, A/B on this tree. Disabling the authority gate (`mayCreate = true`) and
the supersession call by hand: **8 of 12 fail**, including "does not resurrect a
tab whose closing pty.kill failed with a transport error" and "holds the live
lease count flat across ten reconnects of one pane"
(`[ 'pty-0', 'pty-1', 'pty-2', …(7) ]` vs `[ 'pty-9' ]`). The 4 that pass both
ways are the over-refusal tripwires, which is the point of having them. Restored:
12 passed; 2,439 passed across `src/main/ssh`, `src/main/persistence` and
`src/main/runtime/workspace-session`.
2026-08-27 19:45:49 -07:00
..