Terminal Session Correctness

Design & test report · branch nwparker/sta-3077-reattach-pane-cardinality · PRs #13110, #13111

+214
Production lines
+60,903
Rejected approach
12
Oracles that bite
2 / 13
Journeys proven
0 / 8
Gates proven
2
Claims retracted

The one-sentence version

Every bug here is the same bug — identity compared with the wrong key, or not compared at all. A previous attempt added a second identity system beside the first at a cost of 60,903 lines and fixed none of the three root causes. This fixes all three in 214.

1 · What sits behind one terminal

Five separate things line up behind the rectangle you see.

Anatomy of a terminal⤢ zoom

The real computer

Orca's bookkeeping

What you see

bound to

points at

claims

runs on

Pane
a rectangle in a tab

Binding
this pane uses that shell

Lease
we own a remote shell

PTY
the shell process

Host
your Mac, or an SSH box

The real computer

Orca's bookkeeping

What you see

bound to

points at

claims

runs on

Pane
a rectangle in a tab

Binding
this pane uses that shell

Lease
we own a remote shell

PTY
the shell process

Host
your Mac, or an SSH box

The PTY is the real shell doing the work. The binding is the note connecting it to your pane. On a remote machine a lease is a second note saying “we still own that shell.” Every failure below is one of those notes pointing at the wrong thing.

2 · The reported failure

Reconnect growth — a real customer report⤢ zoom

2 terminals

network hiccup

reconnect

19 terminals

hiccup

20 terminals
most are ghosts

2 terminals

network hiccup

reconnect

19 terminals

hiccup

20 terminals
most are ghosts

The remote machine also fills with unused shells until it refuses to start more. A second, worse variant: an AI coding agent resumed twice — two claude processes writing to one conversation file. One report reached five.

3 · Three root causes

Cause 1 — the lease forgot its pane

A lease identified a shell but never checked which pane it belonged to. Every reconnect added a note; nothing removed one.

Lease accumulation⤢ zoom

reconnect

reconnect

Pane A

lease → shell-1

lease → shell-2

lease → shell-3

nothing retires
the old notes

next reconnect restores
all three

reconnect

reconnect

Pane A

lease → shell-1

lease → shell-2

lease → shell-3

nothing retires
the old notes

next reconnect restores
all three

FIXED A lease is identified by its pane. One pane, one live lease. Superseded leases are marked expired, not killed — the remote shell keeps running, because losing a note is not proof the shell died.

Cause 2 — reconnecting could create panes

Reattach decision⤢ zoom

yes

no · before

no · after

reconnect finds a lease

does its pane
still exist?

reattach — correct

invent the pane
👻 ghost appears

refuse
shell left running, unbound

yes

no · before

no · after

reconnect finds a lease

does its pane
still exist?

reattach — correct

invent the pane
👻 ghost appears

refuse
shell left running, unbound

FIXED Reattach binds only. Opening a new terminal still creates, exactly as before.

Cause 3 — a living shell reported as dead

Real meaningReported asConsequence
This shell is goneSESSION_EXPIREDcorrect
Shell is fine — output pipe needs re-establishingSESSION_EXPIREDrespawn → agent resumed twice

FIXED The second case has its own message, and respawn now requires positive proof. Anything unrecognised is “unknown”, which never justifies a replacement.

4 · The rule underneath all of it

Unknown is not dead⤢ zoom

shell exited

can't reach it

timed out

absent from a list

3 failed retries

evidence

what does it prove?

DEAD
cleanup allowed

UNKNOWN

nothing destructive
wait · retry · ask

shell exited

can't reach it

timed out

absent from a list

3 failed retries

evidence

what does it prove?

DEAD
cleanup allowed

UNKNOWN

nothing destructive
wait · retry · ask

The liveness check also became three-valued. It previously returned yes/no, so a provider whose socket was down could only answer “no” — meaning dead — when the honest answer was “I can’t see”.

5 · What changed

#ChangePrevents
1Reattach binds, never createsGhost panes
2Leases identified by pane2 → 19 → 20 growth
3Heal already-corrupted lease dataExisting installs stuck at 20
4“Needs re-establishing” ≠ “expired”Duplicate agent resume
5Liveness can answer “unknown”Live shells declared dead
6One PTY’s failure can’t drop the shared channelOne bad pane killing every session on a host
Change 6 — blast radius⤢ zoom

after

pane 3 can't re-prove
its output stream

park pane 3 only

its shell keeps running
siblings untouched

before

pane 3 can't re-prove
its output stream

give up after N tries

drop the whole
host connection

every pane, file transfer
and git command dies

after

pane 3 can't re-prove
its output stream

park pane 3 only

its shell keeps running
siblings untouched

before

pane 3 can't re-prove
its output stream

give up after N tries

drop the whole
host connection

every pane, file transfer
and git command dies

No wire change

The keystroke guard adds nothing to the payload. Orca already keeps two lookup tables in lock-step, so when they disagree that is proof the id is stale — a purely server-side check.

6 · How we decide a test is worth trusting

This happened to us twice

An end-to-end test was reported as proving the reconnect fix. It passed with the fix and with it removed — retracted. Separately, a guard shipped with no production caller at all; every test stayed green because they called the capability directly.

The four-step proof⤢ zoom

stays green

1 · run it
green

2 · delete a guard
in production code

3 · run it
MUST go red

4 · restore
green again

worthless
say so — don't ship
it as proof

stays green

1 · run it
green

2 · delete a guard
in production code

3 · run it
MUST go red

4 · restore
green again

worthless
say so — don't ship
it as proof

Delete a real guard, not the test. Prefer one clause at a time — a mutation reddening exactly one claim proves the claims independently. Watch it fail yourself: six of twelve oracles were personally re-verified, and two of those re-checks overturned the original claim.

7 · Why “same id” is not good enough

The most valuable lesson here. Break the quit path so it destroys shells instead of detaching, then restart:

tab id    same ✓
pane id   same ✓
pty id    same ✓
OS pid    13756 → 8852   ✗
start     02:01:29.337 → 02:01:34.728   ✗

Every id-only restart test stays green here.
Your shell is gone.
Proving the process really survived⤢ zoom
KernelShellOrcaTestKernelShellOrcaTestrestart the apptype "echo MARKER=$$"through the real write pathMARKER=13756start time of 13756?02:01:29.337ask againMARKER=13756start time of 13756?02:01:29.337 — same process ✓
KernelShellOrcaTestKernelShellOrcaTestrestart the apptype "echo MARKER=$$"through the real write pathMARKER=13756start time of 13756?02:01:29.337ask againMARKER=13756start time of 13756?02:01:29.337 — same process ✓

8 · Journey status

#JourneyStateWhy
1Local restart — macOS · Linux · WindowsPROVENClause-selective on all three platforms
2Daemon & physical WSLPROVENClause-selective on macOS, Linux, WSL2
4Concurrent multi-hostBY CONSTRUCTIONA mux is per-host — dispose cannot cross hosts, so there is nothing to break
3Lazy discoveryNO SELECTIVE MUTATIONRemoving the real guard breaks setup before the clause is reached
6Docker MaxSessions=1PARTIAL2 clauses redden; a third survived four guard removals
12Mixed versionsWITHDRAWNThe token never crosses a version boundary in production
13Performance & scaleIN PROGRESS1 of 10 dimensions measured; rest running
5, 10, 11Device proof · identity reset · namespace admissionNO SUBJECTThe code does not exist. No oracle is writable at any effort.
Why a property may resist proof⤢ zoom

yes

no — true by
construction

no code exists

property to prove

is a shipped guard
protecting it?

remove it → red
provable

nothing to remove
not provable by mutation

nothing to test
not writable

yes

no — true by
construction

no code exists

property to prove

is a shipped guard
protecting it?

remove it → red
provable

nothing to remove
not provable by mutation

nothing to test
not writable

Read this carefully

“True by construction” is a good outcome — the property holds because of how the code is shaped, not because a guard watches it. It simply cannot be demonstrated by breaking something. Reporting it as proven would be false.

9 · Traps that can fake a result

TrapEffectMitigation
Shared temp directory — the harness writes a pointer to a machine-wide pathConcurrent runs can fake a red and hide a real oneIsolated TMPDIR per run
Stale build — e2e runs a built appYou tested the old codeCheck build timestamp against source edit
Mutation silently didn’t applyTest stays green, looks like “no teeth” — wrong conclusionVerify the mutation landed before believing the result
Serial mode reports “did not run”A skip mistaken for a passRe-run the others alone under the same mutation

Two of our own tests were broken in ways only real hardware revealed: one failed on every Linux run (a single unpolled read where its sibling polls for 15s), and one could not execute at all on Windows (echo $$ and ps don’t exist there).

10 · Open decisions

IDDecisionBlocks
D2Re-scope G4? It demands an architecture where 0 of 394 files exist, measured at +62k LOC — the approach that already failed at 0/13.G4, G1, G3
D3Strike or defer J5 / J10 / J11? They have no production subject.3 journeys
D4Architectural gates need a “reviewed diff + file list” receipt; the doc’s red-SHA template makes them structurally unpromotable.G0, G1, G4
D5The input quarantine is load-bearing, not superseded. Deleting it re-opens a verified rm -rf hazard. Amend the clause, or accept something weaker on remote hosts?G6

The hazard, verified

Disable the quarantine and the oracle reproduces it exactly: echo hi; rm -rf x arrives as cho hi; rm -rf x. The shell rejects cho and then runs rm -rf x. The replacement was built and costed at roughly +140 lines to delete 88 — and it does not work on remote hosts at all, because old hosts never publish the incarnation it needs.

11 · Corrections made along the way

Included because a report without them is not trustworthy.

ClaimOutcome
“The reconnect e2e proves the fix”Retracted — passed with the fix removed, twice, isolated
“Cross-version tests confirm old clients are safe”Withdrawn — they exercise a route production never takes
“mayCreate fixes reattach”Was inert — no production caller for several commits
“A branded binding type is the fix”Rejected — three claims false; would have added a second identity system
“Fixtures compile as production”Corrected — none ship; the sweep was stopped
“bash 5.3.9 causes the WSL failure”Corrected — macOS runs the same version and passes
Generated for review alongside design-overview.md, test-overview.md and goalposts.md. Click any diagram to zoom · scroll to scale · drag to pan.
×
scroll to zoom · drag to pan · Esc to close