Skip to content

fix(ssh): stop pane adoption certifying a death from the relay's not-found union - #18531

Merged
nwparker merged 2 commits into
mainfrom
fix/ssh-pane-attach-absence-certificate
Sep 3, 2026
Merged

nwparker merged 2 commits into
mainfrom
fix/ssh-pane-attach-absence-certificate

Conversation

@nwparker

@nwparker nwparker commented Sep 3, 2026 •

Copy link
Copy Markdown
Contributor
Files Added Deleted Net
Test 7 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​253 $\color{#cf222e}{\Huge{\mathbf{−}}}$​20 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​233
Prod 7 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​109 $\color{#cf222e}{\Huge{\mathbf{−}}}$​11 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​98

The defect

attachStablePaneOwner (src/main/ipc/pty/pane/stable-owner.ts) was the last reader that synthesised a runtime exit out of a reattach refusal, and it published exit code 0:

runtime?.onPtyExit(owner.ptyId, 0, owner.incarnationId)

orca-runtime-on-pty-exit.ts reads exitCode >= 0 as rememberPtyLivenessVerdict(ptyId, { status: 'exited' }) — a death certificate whose own doc comment says "Its only writer is a host-delivered exit frame; nothing weaker may reach it."

The refusal it acts on is a union. src/relay/pty-handler.ts attach throws PTY "<id>" not found for two unrelated situations:

  1. a pid it probed with isProcessAlive and found gone (reapPtyProvenExited) — exited is correct;
  2. an id its session map simply never had, with no liveness check on that path at all — which, because ids are pty2:<ptyIdMintEpoch>:<n>, is every previously minted id after a relay restart.

So: relay restarts → user reconnects → pane adoption runs attachOnly for a lease whose shell is still alive under the old daemon's orphaned process tree → exited is recorded → retirePersistedStablePaneOwner clears the binding → fresh spawn → duplicate agent on one transcript.

The codebase already knew this. handlePtyReattachFailure (ssh-relay-session.ts) carries a paragraph explaining exactly this union and deliberately does not call onPtyExit, publishing -1 instead. stable-owner.ts was the one remaining path that did — with 0.

Blame is 3bc13f7b8cb (Aug 24); pre-existing, not from today's sweep. The existing test (ssh-expired-lease-pane-readoption.test.ts) passed runtime: undefined, so onPtyExit was never observable.

The fix

The two relay situations were genuinely indistinguishable at the client — the same string produced both, and SshPtyAbsentFromRelayError was minted for the whole union. Following the restoreRequired precedent from #17951, the narrow case now gets its own token:

  • Relay (pty-handler.ts): the reapPtyProvenExited refusal carries PTY_ATTACH_PROVEN_EXITED_MARKER. Additive on purpose — an unmarked answer, including an older relay's, stays ambiguous, so a missing marker is never evidence.
  • Client (ssh-pty-errors.ts, ssh-pty-session-reattach.ts): that half is minted as SshPtyProvenExitedOnRelayError, a subclass of SshPtyAbsentFromRelayError, so every existing isSshPtyAbsentFromRelayError consumer (spawn-execute.ts ×2 lease bookkeeping) is unchanged.
  • stable-owner.ts: publishes UNVERIFIED_PROCESS_EXIT_CODE (-1), the sentinel its sibling publishes, and passes hostExitConfirmed: true only for evidence that observed the process — the marked relay refusal, or SessionNotFoundError from the registry that owns the PTY. The ambiguous half now records unverifiable, not exited.
  • Discriminate by type, not text: the gone-branch keys on isHostReportedPtyAbsenceError (classes) instead of isPtyAlreadyGoneError, whose bare /PTY ".+" not found/ match let an untyped string abandon a binding. This is the discriminator pty-connect-limits.ts already documented on the renderer side.

isPtyAlreadyGoneError is untouched for the kill/teardown callers, where leniency is correct.

What is deliberately not changed

The ambiguous case still retires the client's pane binding and falls through to a fresh spawn. That is client routing bookkeeping, not a claim about the process: after a relay change the old id is genuinely unroutable, and docs/reference/ssh-execution-boundary.md already documents that fallback. Refusing to retire would also strand the pane — resolveStablePaneOwner throws terminal_pane_owner_conflict once the runtime's pane→pty and persistence disagree. The destructive artifact was the certificate, and that is what is removed: the orphan now stays unverifiable, so the sweep, stop verdicts and getPtyLivenessVerdict keep failing closed over it.

Tests

  • src/main/ipc/pty/pane/stable-pane-absence-death-certificate.test.ts (new) — a real OrcaRuntimeService, so onPtyExit and the verdict register are observable. Covers the relay-restart case (unverifiable, no certificate), the pid-probed case (exited, binding retired — a genuinely dead PTY must still retire), the owning-registry case, and an untyped not found string (no verdict, persistence untouched).
  • src/relay/pty-handler-attach-replay.test.ts — the same spec now asserts the probed refusal carries the marker and the "id the map never had" refusal does not.
  • src/main/providers/ssh-pty-reattach-absence-discrimination.test.ts — the reattach path maps only the marked refusal to the proven class.
  • ssh-expired-lease-pane-readoption.test.ts now throws the typed error the real SSH provider raises rather than raw wire text.

Failing before the fix (verified by reverting each half in place):

× records no death certificate when the relay merely does not know the id
    expected { status: 'exited' } to deeply equal { status: 'unverifiable', … }
× refuses to abandon the binding on an untyped "not found" string
× reports not-found and reaps a reattach whose backing shell is dead   (relay marker)

Verification

  • pnpm tc ✅
  • pnpm run check:code-quality:changed ✅ (0 new findings across 10 files; max-lines proved live with a 320-line positive control)
  • oxfmt --check ✅
  • pnpm test src — see comment below

@coderabbitai

coderabbitai Bot commented Sep 3, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Team

Run ID: 82dc1ef5-cc4c-40ae-a502-44d8c6727fcc

📥 Commits

Reviewing files that changed from the base of the PR and between a23adea2fe8ff1982247bb0ce156422a39295e33 and 2b8b2ba.

📒 Files selected for processing (3)
  • src/main/ipc/pty-dead-owner-respawn.test.ts
  • src/main/ipc/pty-pane-reservation-settlement.test.ts
  • src/main/ipc/pty-persisted-incarnation-repair.test.ts

Included review availability: Your plan provides up to 10 included reviews per hour; 0 remain after this review.


📝 Walkthrough

Walkthrough

The relay now marks attach refusals only when it proves that the PTY process exited. SSH reattach code maps marked refusals to SshPtyProvenExitedOnRelayError and other typed absence responses to SshPtyAbsentFromRelayError. Liveness handling records confirmed exits only from observed evidence; ambiguous absence produces an unverified result. Tests and documentation cover these distinctions.

Merge Risk: 🟡 Moderate · up to 6fbd0

SSH PTY exit classification is covered by updated fixtures, but the relay deployment bundle still needs confirmation that its shared-module import resolves correctly before merge.

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Description check ⚠️ Warning The description provides a detailed defect explanation, fix rationale, scope boundaries, tests, and verification results. However, it omits required template content, including the Linked Issue, Visua… Add the required linked issue after Fixes #, provide visual proof or write N/A with a brief reason, and complete the Testing, AI Disclosure, Review, Agent skill upstream boundary, Notes, and Checklist sections as applicable.
Docstring Coverage ⚠️ Warning Docstring coverage is 15.38% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 13 functions across 13 files. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely identifies the primary fix: preventing an ambiguous SSH relay not-found response from certifying PTY death.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Full details: Description check

Explanation

The description provides a detailed defect explanation, fix rationale, scope boundaries, tests, and verification results. However, it omits required template content, including the Linked Issue, Visual Proof or an explicit N/A statement, and several checklist sections.

✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1


ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Team

Run ID: b58dab9d-f3fd-4147-b7a7-ed924efcc879

📥 Commits

Reviewing files that changed from the base of the PR and between c79e1c0 and a23adea2fe8ff1982247bb0ce156422a39295e33.

📒 Files selected for processing (11)
  • docs/reference/ssh-execution-boundary.md
  • src/main/ipc/pty/pane/stable-owner.ts
  • src/main/ipc/pty/pane/stable-pane-absence-death-certificate.test.ts
  • src/main/ipc/pty/provider/liveness.ts
  • src/main/providers/ssh-pty-errors.ts
  • src/main/providers/ssh-pty-reattach-absence-discrimination.test.ts
  • src/main/providers/ssh-pty-session-reattach.ts
  • src/main/ssh-expired-lease-pane-readoption.test.ts
  • src/relay/pty-handler-attach-replay.test.ts
  • src/relay/pty-handler.ts
  • src/shared/pty-attach-absence-evidence.ts

Included review availability: Your plan provides up to 10 included reviews per hour; 5 remain after this review.

Comment thread src/relay/pty-handler.ts
@@ -35,6 +35,7 @@ import {
type RelaySpawnCwdResolution
} from './pty-spawn-cwd'
import { PhysicalExitTracker } from '../shared/physical-exit-tracker'
import { PTY_ATTACH_PROVEN_EXITED_MARKER } from '../shared/pty-attach-absence-evidence'

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Keep the relay marker local.

src/relay is deployed as a separate bundle. This import can make the remote relay bundle resolve an app-side shared module. Define PTY_ATTACH_PROVEN_EXITED_MARKER locally in src/relay/pty-handler.ts. Keep the test assertion against the shared value so it detects literal drift.

Proposed fix
-import { PTY_ATTACH_PROVEN_EXITED_MARKER } from '../shared/pty-attach-absence-evidence'
+const PTY_ATTACH_PROVEN_EXITED_MARKER = 'process exited'

Based on learnings: src/relay is bundled separately for remote deploy, so it must not import app-side modules, including shared constants.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
import { PTY_ATTACH_PROVEN_EXITED_MARKER } from '../shared/pty-attach-absence-evidence'
const PTY_ATTACH_PROVEN_EXITED_MARKER = 'process exited'

Source: Learnings

@nwparker

nwparker commented Sep 3, 2026

Copy link
Copy Markdown
Contributor Author

Local verification (in progress → final)

  • pnpm tc ✅
  • pnpm run check:code-quality:changed ✅ — 0 new findings; max-lines proven live first with a 320-line positive control (npx oxlint on a single file can otherwise exit 0 without running it).
  • oxfmt --check ✅ on every changed file.
  • pnpm test src — first full run surfaced 6 failures, all in fixtures that rejected with untyped new Error('Session not found: …') / new Error('PTY "…" not found'). No provider produces either untyped (local-pty-spawn and decodeDaemonResponseError mint SessionNotFoundError; the SSH reattach path types the relay's wire text), so those fixtures were misrepresenting their providers — fixed in 343da172cb2, and the three files pass. Re-running the whole tree on the final commit; result to follow.

Five other failures in that run are unrelated and pre-existing/load-sensitive, verified individually:

test verdict
remote-runtime-shared-control-connection — keeps sent request bytes admitted… fails on origin/main too
AgentMapWorkspaceContextMenu.test.tsx unresolved @/… import on origin/main too
ssh-remote-commands — bounds real POSIX GC output… passes alone (19.7s) and in its own file (22s); only fails when batched — a wall-clock flake
terminal-output-frame-chunks-equivalence — 800-payload fuzz (52s) passes when run alone
browser-cookie-import-partition-reland.electron (241s) browser cookie partitioning; no path to this change

Docker SSH lane

Not run. Worth stating precisely why rather than implying coverage: no spec in that lane restarts a relay. resetDockerSshRelayTarget exists in tests/e2e/helpers/docker-ssh-relay-connection.ts and has zero callers; ssh-restart-tab-accumulation restarts the app, not the relay, and the relay survives that — so the lane exercises the reattach-succeeds path, not the not-found union this PR is about. The specific scenario is covered by the new unit test against a real OrcaRuntimeService, and the relay half by pty-handler-attach-replay, which drives the real PtyHandler through both refusals (isProcessAlive stubbed false, then the reaped id). A lane spec that calls resetRelay and asserts the pane's liveness verdict afterwards would be a genuine addition — flagging it as a follow-up rather than folding an e2e into this fix.

Related seam left alone

handlePtyReattachFailure now could certify the marked half, since the marker reaches it too. It still does not. That is the leak direction and safe; tightening it is a separate change with its own reasoning.

@nwparker
nwparker force-pushed the fix/ssh-pane-attach-absence-certificate branch from 343da17 to 2b8b2ba Compare September 3, 2026 22:36

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ No new issues found.

Reviewed changes

  • Marker token for the probed-dead relay refusal — src/relay/pty-handler.ts appends PTY_ATTACH_PROVEN_EXITED_MARKER to the reapPtyProvenExited not-found answer; src/shared/pty-attach-absence-evidence.ts defines the marker and isProvenExitedPtyAttachRefusal.
  • Typed subclass at the reattach boundary — ssh-pty-session-reattach.ts mints SshPtyProvenExitedOnRelayError (subclass of SshPtyAbsentFromRelayError) for the marked half, leaving every isSshPtyAbsentFromRelayError consumer untouched.
  • No more death certificate from the union — stable-owner.ts publishes -1 and passes hostExitConfirmed: true only for observed evidence, keying the retire gate on isHostReportedPtyAbsenceError (type) instead of the text-matching isPtyAlreadyGoneError.
  • Test-deed alignment — the follow-up commit throws what the real providers throw (SessionNotFoundError, SshPtyProvenExitedOnRelayError) and asserts onPtyExit(-1, …, { hostExitConfirmed: true }).

The fix is well-reasoned and wire-safe in both directions: a new relay's marker is additive (an older client keeps its old, already-buggy behavior), and a missing marker — including an older relay's — is never read as evidence, so the certifying branch degrades to unverifiable. I traced the three seams independently (relay refusal → onPtyExit/verdict register → retirement) and ran the affected suites: pty-dead-owner-respawn, pty-persisted-incarnation-repair, pty-pane-reservation-settlement, and stable-pane-absence-death-certificate all pass (17/17), as does the PR's own SSH/relay suite (32/32).

Pullfrog  | View workflow run | Using DeepSeek Pro (free via Pullfrog for OSS) | 𝕏

…found union

`attachStablePaneOwner` was the last reader that synthesised a runtime exit
from a reattach refusal, and it published code `0` — which
`orca-runtime-on-pty-exit` records as `rememberPtyLivenessVerdict(exited)`, a
death certificate whose only legitimate writer is a host-delivered exit frame.

The refusal it acted on is a union. `pty.attach` answers `PTY "<id>" not found`
both for a pid the relay probed with `isProcessAlive` and for an id its session
map simply never had — which, because ids carry a per-start mint epoch, is every
id minted before a relay restart, checked against nothing. So a relay restart
plus a reconnect certified a shell that was still running under the old daemon's
orphaned process tree, retired the pane binding, and cold-started a second agent
onto the same transcript. The sibling `handlePtyReattachFailure` has always
refused to certify from that union; this path did not.

- The relay marks the one refusal it backed with a liveness check
  (`PTY_ATTACH_PROVEN_EXITED_MARKER`). The marker is additive, so an unmarked
  answer — including an older relay's — stays ambiguous, which is the safe
  direction.
- The client mints that half as `SshPtyProvenExitedOnRelayError`, a subclass so
  every existing `isSshPtyAbsentFromRelayError` consumer is unchanged.
- Pane adoption publishes `UNVERIFIED_PROCESS_EXIT_CODE` (-1), the sentinel its
  sibling publishes, and passes `hostExitConfirmed` only for evidence that
  observed the process: the marked relay refusal, or `SessionNotFoundError` from
  the registry that owns the PTY. The ambiguous half now records `unverifiable`
  instead of `exited`.
- The gone-branch keys on the error type rather than the bare `PTY ".+" not
  found` text, so an untyped string can no longer authorise abandoning a
  binding — the discriminator `pty-connect-limits.ts` already documented.

Refs docs/reference/ssh-execution-boundary.md
…throw

These four fixtures rejected with bare `new Error('Session not found: ...')` and
`new Error('PTY "..." not found')`. No provider produces either untyped:
`local-pty-spawn` and `decodeDaemonResponseError` both mint
`SessionNotFoundError`, and the SSH reattach path types the relay's wire text
before any pane sees it. Fixtures that skip the type were the reason a
message-shaped gate looked adequate.

The exit-code expectations move with it: the pane path now publishes the -1
stop sentinel plus `hostExitConfirmed`, so a certificate follows the evidence
rather than a synthesized zero.
@nwparker
nwparker force-pushed the fix/ssh-pane-attach-absence-certificate branch from 2b8b2ba to 6fbd093 Compare September 3, 2026 23:05
@nwparker

nwparker commented Sep 3, 2026

Copy link
Copy Markdown
Contributor Author

CI: all 8 test shards pass

static analysis fails on verify:localization-runtime-catalog — four components.onboarding.integrations.capabilities.* keys missing from en-runtime-required.json. Not from this PR, which touches no i18n file. Reproduced on a clean origin/main checkout:

$ git checkout --detach origin/main   # 7d27c841b4d
$ pnpm run verify:localization-runtime-catalog
src/renderer/src/i18n/en-runtime-required.json no longer covers en.json.
  components.onboarding.integrations.capabilities.browseIssues
  components.onboarding.integrations.capabilities.managePullRequests
  components.onboarding.integrations.capabilities.reviewStatus
  components.onboarding.integrations.capabilities.startWorkspaceFromIssue

Introduced by c79e1c097b7 (fix(i18n): localize remaining onboarding UI), which is an ancestor of origin/main. verify fails only because it needs static analysis. Left alone rather than folded in — it belongs to whoever owns that catalog, and running sync:localization-runtime-catalog here would hide a main-wide break behind an SSH fix.

Rebased onto 7d27c841b4d; git diff origin/main...HEAD is exactly the 14 intended files.

Local pnpm test src

Not a usable signal on this machine — it is shared with dozens of concurrent agents, and the run produced timeouts at 37s–1346s per test in renderer suites this PR cannot reach (opens the native picker requested by a structured slash command, prefers collision-aware upward placement for model and option menus, and similar). The CI shards, which run the same suite on dedicated runners, are green. Every directly affected file was also run in isolation: 9 files / 58 tests green.

@nwparker

nwparker commented Sep 3, 2026

Copy link
Copy Markdown
Contributor Author

Local pnpm test src — full-tree result

69,463 passed / 15 failed / 271 skipped (69,749). None of the 15 are caused by this change; here is every one, accounted for rather than waved off.

One was my own testing mistake, not a defect. ssh-expired-lease-pane-readoption › falls through to a fresh spawn once the host answers that the PTY is absent failed at 28ms. Mid-run I ran git checkout --detach origin/main (to reproduce the localization break on a clean main) and back. That reverted the committed fixture on disk while vitest was still importing files, so the run compared origin/main's bare-string fixture against this branch's type gate. Re-run on the final commit in isolation:

$ npx vitest run src/main/ssh-expired-lease-pane-readoption.test.ts
 Test Files  1 passed (1)
      Tests  4 passed (4)

Twelve are wall-clock timeouts on a box shared with dozens of concurrent agents — note the durations, which are 100–1000× their normal cost and all in suites this PR cannot reach: agent-picker/model-menu rendering (37s, 54s, 72s, 82s, 85s, 96s, 97s, 322s), cleans path metadata when interrupted before destination recovery is linked (1,346s), GitHub rate-limit budget (58s), SOCKS dns-prefetch partition (30s), durable-account-home tailing (1.4s). Spot-checked: ssh-remote-commands › bounds real POSIX GC output… passes alone in 19.7s and in its own file in 22s, and fails only when batched.

Two are pre-existing on origin/main, verified by running them from a detached origin/main checkout: remote-runtime-shared-control-connection › keeps sent request bytes admitted…, and AgentMapWorkspaceContextMenu.test.tsx (unresolved @/… import).

The authoritative signal is CI, which runs the same suite sharded on dedicated runners: all 8 test / tests node 24 N/8 shards passed on the identical source. The current run also picked up e2e / ssh docker watcher isolation, which is the SSH Docker lane.

@nwparker
nwparker merged commit a5d6114 into main Sep 3, 2026
30 of 31 checks passed
@nwparker

nwparker commented Sep 3, 2026

Copy link
Copy Markdown
Contributor Author

Final CI: 21 pass / 2 fail — both pre-existing on origin/main

lane result
test / tests node 24 1..8 (all 8 shards) pass
e2e / ssh docker watcher isolation pass (19m46s)
e2e / changed e2e specs pass (9m28s)
typecheck, managed hooks on Node 18, root directory guard, test vs non-test LoC, packaging (incl. Windows) pass
static analysis fail — verify:localization-runtime-catalog
verify fail — only because it needs: static analysis

The two failures are the onboarding-i18n catalog drift from c79e1c097b7, reproduced verbatim on a clean detached origin/main checkout. Nothing in this PR touches an i18n file.

The SSH Docker lane passing is a regression check on the reattach-succeeds path, not proof of the fix — as noted above, no spec in that lane restarts a relay, so it cannot reach the not-found union. That remains a recorded coverage gap rather than something this PR quietly claims.

enrick-dev pushed a commit to enrick-dev/orca that referenced this pull request Oct 15, 2026
…found union (stablyai#18531)

* fix(ssh): stop pane adoption certifying a death from the relay's not-found union

`attachStablePaneOwner` was the last reader that synthesised a runtime exit
from a reattach refusal, and it published code `0` — which
`orca-runtime-on-pty-exit` records as `rememberPtyLivenessVerdict(exited)`, a
death certificate whose only legitimate writer is a host-delivered exit frame.

The refusal it acted on is a union. `pty.attach` answers `PTY "<id>" not found`
both for a pid the relay probed with `isProcessAlive` and for an id its session
map simply never had — which, because ids carry a per-start mint epoch, is every
id minted before a relay restart, checked against nothing. So a relay restart
plus a reconnect certified a shell that was still running under the old daemon's
orphaned process tree, retired the pane binding, and cold-started a second agent
onto the same transcript. The sibling `handlePtyReattachFailure` has always
refused to certify from that union; this path did not.

- The relay marks the one refusal it backed with a liveness check
  (`PTY_ATTACH_PROVEN_EXITED_MARKER`). The marker is additive, so an unmarked
  answer — including an older relay's — stays ambiguous, which is the safe
  direction.
- The client mints that half as `SshPtyProvenExitedOnRelayError`, a subclass so
  every existing `isSshPtyAbsentFromRelayError` consumer is unchanged.
- Pane adoption publishes `UNVERIFIED_PROCESS_EXIT_CODE` (-1), the sentinel its
  sibling publishes, and passes `hostExitConfirmed` only for evidence that
  observed the process: the marked relay refusal, or `SessionNotFoundError` from
  the registry that owns the PTY. The ambiguous half now records `unverifiable`
  instead of `exited`.
- The gone-branch keys on the error type rather than the bare `PTY ".+" not
  found` text, so an untyped string can no longer authorise abandoning a
  binding — the discriminator `pty-connect-limits.ts` already documented.

Refs docs/reference/ssh-execution-boundary.md

* test(pty): make the pane-adoption fixtures throw what real providers throw

These four fixtures rejected with bare `new Error('Session not found: ...')` and
`new Error('PTY "..." not found')`. No provider produces either untyped:
`local-pty-spawn` and `decodeDaemonResponseError` both mint
`SessionNotFoundError`, and the SSH reattach path types the relay's wire text
before any pane sees it. Fixtures that skip the type were the reason a
message-shaped gate looked adequate.

The exit-code expectations move with it: the pane path now publishes the -1
stop sentinel plus `hostExitConfirmed`, so a certificate follows the evidence
rather than a synthesized zero.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant