Skip to content

fix(ssh): stop reporting a confirmed kill when the SSH provider is gone - #14977

Merged
brennanb2025 merged 21 commits into
mainfrom
brennanb2025/ssh-stop-verdict-2
Aug 17, 2026
Merged

brennanb2025 merged 21 commits into
mainfrom
brennanb2025/ssh-stop-verdict-2

Conversation

@brennanb2025

@brennanb2025 brennanb2025 commented Aug 16, 2026 •

Copy link
Copy Markdown
Contributor
Files Added Deleted Net
Test 25 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​1368 $\color{#cf222e}{\Huge{\mathbf{−}}}$​58 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​1310
Prod 21 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​610 $\color{#cf222e}{\Huge{\mathbf{−}}}$​103 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​507

The invariant

Orca's SSH execution boundary has two rules. This PR enforces the second one:

No asserting what you cannot observe. "I lost contact with the host" is not evidence of "the process died." exited requires positive evidence of absence from the owning host.

The vocabulary is the one the worktree teardown gate already spoke — live / unverifiable / exited — promoted out of unstopped-pty-verification.ts into src/shared/pty-liveness-verdict.ts so every surface uses the same three words. No synonyms were introduced.

The defect

src/main/ipc/pty.ts — both kill and stopAndWait returned true when the SSH provider lookup threw. The in-code justification ("an absent SSH provider means there is no live target left to await") is false: a detached relay PTY is designed to outlive the provider that addressed it — it ignores SIGHUP (src/relay/relay.ts:1361-1364) and the shipped grace default is 0 (unlimited).

Every caller downstream reported that fabricated success:

Call site Before After
pty.ts kill / stopAndWait return true return false + record unverifiable with the reason; the local lease is still tombstoned exactly as before
orca-runtime.ts closeTerminal { ptyKilled: true } { ptyKilled: false, ptyStopVerdict: 'unverifiable', ptyStopReason } via a single describeTerminalClose receipt builder used by all four return sites
cli/terminal-format.ts "Closed terminal X. PTY killed." "Closed terminal X. The PTY was not confirmed stopped: its SSH provider is no longer registered."
orchestration-worker-stop.ts processAction: 'closed_agent_terminal' + settleWorkerStop markWorkerStopUnknown with "could not be confirmed stopped"; the dispatch is not settled as stopped
worktree-teardown.ts stop counted as success → PTY never reached failedPtyIds → gate skipped → removal proceeded past a live remote agent the failed stop reaches the gate, and a recorded unverifiable verdict short-circuits the re-list so removal is refused with the --force hint

Second-order bypass, also fixed

Even with the honest false, the gate would still have answered exited: verifyUnstoppedPtys re-lists the client's own provider, which is silent about a host it cannot reach. An empty list read as "they exited." unverifiableStopVerdict now consults the recorded verdict first — silence from a provider that does not own the PTY cannot overturn lost contact. Force Delete remains the documented escape hatch.

Related defect (false death)

orchestration-worker-observation.ts classified terminal.connected === false as 'exited'. But the aggregate inventory only iterates registered providers (pty.ts:5750-5753), so one dropped relay clears connected for every remote PTY at once — and worker-stop then asserted "The recorded worker process is exited; no terminal was closed."

The sweep now distinguishes the two cases by what the provider actually answered:

  • hasPty === false → observed absence → exited (today's behavior, unchanged)
  • hasPty === null → nobody could be asked → unverifiable
  • hasPty absent entirely → no new claim either way

The observation gains an 'unverifiable' status. Its three consumers degrade safely: worker-control show passes it through for display, worker-control read maps anything non-exited to 'running', and release-completion only branches on identity_changed.

Explicitly NOT changed

  • The connected field's meaning or shape. Other clients read it; that is a wire-visible change owned by a separate PR. The verdict is carried internally and surfaced only through new optional fields.
  • src/main/providers/ssh-pty-provider.ts restoreRequired handling and the terminal-list schema — owned by other workers.

Wire compatibility

Per docs/reference/remote-wire-compatibility.md:

  • RuntimeTerminalClose.ptyStopVerdict / .ptyStopReason are new optional fields (Rule 1 — safe). Absent means today's answer, so a client reading only ptyKilled is unaffected. 'exited' deliberately never appears in ptyStopVerdict — that is what ptyKilled reports.
  • No new stream opcode (Rule 2 N/A).
  • Rule 3 (host publishes something new to old clients): the only such change is the new 'unverifiable' value in observation.status and the reworded lastError strings. No client switches on observation.status — it is rendered, not branched on — and lastError is already free text.

Red-first evidence

Every test below was written and run before the fix. Exact failure output:

src/main/ipc/pty-ssh-stop-verdict.test.ts — 2 failed | 1 passed

AssertionError: expected true to be false // Object.is equality
 ❯ src/main/ipc/pty-ssh-stop-verdict.test.ts:87   (controller.kill('ssh-detached-pty'))
 ❯ src/main/ipc/pty-ssh-stop-verdict.test.ts:101  (await controller.stopAndWait('ssh-detached-stop'))

src/cli/terminal-format.test.ts — 2 failed | 2 passed

Expected: "Closed terminal term_remote. The PTY was not confirmed stopped: its SSH provider is no longer registered."
Received: "Closed terminal term_remote."

expected 'Closed terminal term_live.' to be 'Closed terminal term_live. The PTY is still running.'

src/main/runtime/rpc/methods/orchestration-worker-stop-liveness-verdict.test.ts — 1 failed | 1 passed

Error: The property "getTerminalLivenessVerdict" is not defined on the object.

src/main/runtime/worktree-teardown-unstopped-pty.test.ts — 1 failed | 14 passed

AssertionError: promise resolved "{ runtimeStopped: +0, …(2) }" instead of rejecting

(removal completed successfully past a PTY nothing had confirmed stopped)

src/main/runtime/pty-inventory-liveness-verdict.test.ts — red obtained by reverting the sweep-classification hunk:

AssertionError: expected null to deeply equal { status: 'unverifiable', …(1) }

Mutation checks

Each key assertion was re-run against a deliberately broken implementation to prove it binds:

Mutation Result
pty.ts kill catch back to return true expected true to be false — 1 failed
worktree-teardown.ts drop the unverifiableStopVerdict short-circuit promise resolved "{ runtimeStopped: +0, …(2) }" instead of rejecting — 1 failed
orchestration-worker-observation.ts drop the unverifiable branch - "status": "unverifiable" / + "status": "exited" — 1 failed
terminal-format.ts drop the unverifiable wording expected 'Closed terminal term_remote.' to be 'Closed terminal term_remote. The PTY …' — 1 failed
orca-runtime.ts describeTerminalClose never reads the verdict expected { …(3) } to match object { ptyKilled: false, …(2) } — 1 failed

The lint gate was verified live the same way (injecting const unusedProbe: any = 1 produced two oxlint errors), since oxlint is silent when clean.

Non-regression

Every suite carries a passing counterpart proving the confirmed path is untouched:

  • a real provider-performed stopAndWait still resolves true, still calls shutdown(..., { immediate: true }), and never records a verdict;
  • ptyKilled: true still prints "Closed terminal X. PTY killed.", and a confirmed-kill receipt carries no verdict fields at all;
  • a locally observed exit still reports 'exited' with the exact message "The recorded worker process is exited; no terminal was closed.";
  • hasPty === false still records no doubt.

Two incumbent tests in pty-daemon-controller-teardown.test.ts asserted kill(...) === true for exactly the branch this PR corrects. Their real subject — that the SSH lease is tombstoned and the runtime is told — is unchanged and still asserted; only the fabricated return value flipped.

Verification

  • src/main/ipc/pty* — 541 passed, plus 10 pre-existing failures in MiMo/Pi/OMP env-overlay, login-shell and serializer suites. Proven pre-existing by reverting src/main/ipc/pty.ts to the base commit and re-running: they fail identically (expected "vi.fn()" to be called 1 times, but got 0 times). Untouched by this PR.
  • src/main/runtime/orca-runtime* — 1482 passed | 1 skipped
  • src/main/runtime/rpc/methods/orchestration* — 333 passed
  • src/main/runtime/worktree*, src/main/ipc/worktrees* — 347 passed
  • src/cli/ — 791 passed
  • tsc --noEmit -p config/tsconfig.node.json and -p config/tsconfig.cli.json — clean. (tsconfig.web.json reports 16 pre-existing TS6307 project-membership errors, none in files this PR touches.)
  • oxlint + oxfmt clean on all 16 changed files. worktree-teardown.ts crossed the 300-line cap during this work; rather than disable max-lines, unverifiableStopVerdict was moved to unstopped-pty-verification.ts, where the other verdict decisions already live.

Note

The verdict map is bounded at 256 entries with oldest-first eviction, and entries are cleared on any positive evidence — a reconnect, a provider reporting absence, or a real wait status from the owning host.


Follow-up commit: the federation host had the same two defects

Reviewing the call sites turned up a second, parallel copy of this logic on the federation host (orchestration-federation-control.ts), serving remote coordinators. It had both defects:

  • inspectRemoteAttachment read a dropped relay's connected: false as exited — the same false death, relayed home.
  • federationStop called settleRemoteAttachmentStop unconditionally, so a close it never confirmed was reported to the coordinator as state: 'stopped' — the same false success, one hop further away.

Both now follow the local rule. federationStop routes an unconfirmed close to markRemoteAttachmentStopUnknown and still returns processAction: 'closed_agent_terminal' (the close was attempted) with the honest lastError.

Two guards had to move so honesty does not become a new refusal

Adding an unverifiable status to a union that other code gates on is exactly where a fix like this turns into a regression. Two sites gated on status !== 'running':

  1. federationRead would have refused to read output from a connected terminal the moment a stop lost contact with it. It now gates on status === 'exited', which is provably equivalent for every pre-existing status — unattached/missing already imply !observation.terminal, and identity_changed already implies !observation.exact — while leaving an unverifiable terminal readable.
  2. workerStop would have skipped the close attempt entirely and gone straight to an unknown receipt, leaving the terminal open. It now proceeds to the close for unverifiable as well: losing contact is a reason to report the outcome honestly, never a reason to stop trying to stop the worker. The close receipt's verdict then decides the outcome.

Both are covered and mutation-verified below. The unverifiable reason is now published alongside the status in both show observations (new optional field) so the status is actionable rather than a mystery.

Red-first / mutation evidence for the follow-up

src/main/runtime/rpc/methods/orchestration-federation-liveness-verdict.test.ts is new (5 tests). Each production branch was verified by breaking it:

Mutation Result
federationStop settles an unconfirmed close expected 'stopped' not to be 'stopped' — 1 failed
federationShow ignores the verdict expected { …(5) } to match object { observation: { …(3) } } — 1 failed
federationRead gate back to !== 'running' expected OrchestrationError: Remote Dispatch ctx_f… to not match object { code: 'worker_identity_changed' } — 1 failed
workerStop refuses to try when unverifiable expected "closeTerminal" to be called with arguments: [ 'term_worker' ] — 1 failed

Non-regression in the same file: a confirmed close still settles as stopped with processAction: 'closed_agent_terminal', and a locally observed exit still reports exited.

The new federation assertions live in their own file rather than in orchestration-federation.test.ts, because adding them there pushed that file to 808 lines against an 800-line max-lines cap. No lint rule was disabled to make room — the same reason unverifiableStopVerdict moved to unstopped-pty-verification.ts in the first commit.

orchestration suites: 338 passed (27 files). tsc --noEmit clean on both the node and CLI projects. CI was green on the first commit; this second one is pushed on top.

Live SSH QA — 2026-08-16

Validated HEAD 3127133768fcb0f910b317aacafb9ff8672732a4 against an isolated Ubuntu 20.04 SSH target. After disconnecting the owning provider, the real CLI close receipt reported ptyKilled: false, ptyStopVerdict: unverifiable, and reason its SSH provider is no longer registered; an independent SSH probe confirmed the same remote PID remained live after the attempt.

Live SSH close receipt

A detached relay PTY is designed to outlive the provider that addressed it
(it ignores SIGHUP and ships with an unlimited grace), so "the SSH provider
is no longer registered" is lost contact, never evidence the remote process
stopped. Both stop primitives in the PTY controller returned `true` from
that branch, and every caller downstream reported the fabricated success:
the CLI printed "PTY killed.", worker-stop settled the dispatch as stopped,
and — because the stop "succeeded" — the unstopped-PTY gate never ran, so
worktree removal walked straight past a live remote agent.

`kill`/`stopAndWait` now still tombstone the local lease but report an
unconfirmed stop and record why, using the three-verdict vocabulary the
worktree teardown gate already spoke (`live` / `unverifiable` / `exited`),
promoted out of that module into `src/shared/pty-liveness-verdict.ts`.
The close receipt, the CLI wording, worker-stop and the removal gate all
read that verdict instead of inferring an exit from silence.

The same rule fixes the mirror-image defect: the aggregate inventory only
enumerates registered providers, so a dropped relay clears `connected` for
every remote PTY at once. The sweep now separates the provider answering
"absent" (an exit) from no provider being able to answer (lost contact), so
worker-stop stops claiming `exited` from a disconnect.

The `connected` wire field is unchanged in meaning and shape.
@coderabbitai

coderabbitai Bot commented Aug 16, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

@brennanb2025, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 12 minutes

Limit details: You’ve used all 10 included reviews currently available under your plan.

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: ca953006-be58-44c5-a31f-1f8bfc90a8e6

📥 Commits

Reviewing files that changed from the base of the PR and between 86e111a and fab3050.

📒 Files selected for processing (1)
  • src/main/runtime/orchestration/db/worker-terminal/worker-terminal-release.ts

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 0e5b0454-a8d6-47b8-a171-766a810b076c

📥 Commits

Reviewing files that changed from the base of the PR and between 08bf209 and 86e111a.

📒 Files selected for processing (46)
  • src/cli/terminal-format.test.ts
  • src/cli/terminal-format.ts
  • src/main/crash-reporting/process-gone-killed-one-ordering.test.ts
  • src/main/crash-reporting/process-gone-recorder.test.ts
  • src/main/ipc/pty-daemon-controller-teardown.test.ts
  • src/main/ipc/pty-daemon-ssh-lease-lifecycle.test.ts
  • src/main/ipc/pty-runtime-kill-and-exit.test.ts
  • src/main/ipc/pty-ssh-stop-verdict.test.ts
  • src/main/ipc/pty.ts
  • src/main/runtime/claude-agent-teams-service.test.ts
  • src/main/runtime/claude-agent-teams-tmux-dispatcher.ts
  • src/main/runtime/claude-agent-teams-types.ts
  • src/main/runtime/orca-runtime-process-incarnation-liveness.test.ts
  • src/main/runtime/orca-runtime-terminal-close-continuity.test.ts
  • src/main/runtime/orca-runtime-terminal-split-authority.test.ts
  • src/main/runtime/orca-runtime.ts
  • src/main/runtime/orchestration/db.ts
  • src/main/runtime/orchestration/worker-terminal-process-liveness.ts
  • src/main/runtime/pty-inventory-liveness-verdict.test.ts
  • src/main/runtime/rpc/methods/orchestration-federation-control.ts
  • src/main/runtime/rpc/methods/orchestration-federation-liveness-verdict.test.ts
  • src/main/runtime/rpc/methods/orchestration-federation-output.test.ts
  • src/main/runtime/rpc/methods/orchestration-federation-setup.test.ts
  • src/main/runtime/rpc/methods/orchestration-federation.test.ts
  • src/main/runtime/rpc/methods/orchestration-worker-control.ts
  • src/main/runtime/rpc/methods/orchestration-worker-observation.ts
  • src/main/runtime/rpc/methods/orchestration-worker-output.test.ts
  • src/main/runtime/rpc/methods/orchestration-worker-output.ts
  • src/main/runtime/rpc/methods/orchestration-worker-release-completion.ts
  • src/main/runtime/rpc/methods/orchestration-worker-release-liveness-verdict.test.ts
  • src/main/runtime/rpc/methods/orchestration-worker-release-recovery.test.ts
  • src/main/runtime/rpc/methods/orchestration-worker-release.test.ts
  • src/main/runtime/rpc/methods/orchestration-worker-release.ts
  • src/main/runtime/rpc/methods/orchestration-worker-stop-capability.test.ts
  • src/main/runtime/rpc/methods/orchestration-worker-stop-liveness-verdict.test.ts
  • src/main/runtime/rpc/methods/orchestration-worker-stop.ts
  • src/main/runtime/rpc/methods/orchestration-workers-recovery.test.ts
  • src/main/runtime/unstopped-pty-verification.ts
  • src/main/runtime/worktree-teardown-unstopped-pty.test.ts
  • src/main/runtime/worktree-teardown.test.ts
  • src/main/runtime/worktree-teardown.ts
  • src/main/text-generation/commit-message-text-generation.ts
  • src/shared/orchestration-worker-output.ts
  • src/shared/protocol-version.ts
  • src/shared/pty-liveness-verdict.ts
  • src/shared/runtime-types.ts
🚧 Files skipped from review as they are similar to previous changes (42)
  • src/main/ipc/pty-daemon-ssh-lease-lifecycle.test.ts
  • src/main/runtime/orchestration/db.ts
  • src/main/runtime/rpc/methods/orchestration-worker-output.test.ts
  • src/main/runtime/rpc/methods/orchestration-federation-output.test.ts
  • src/main/runtime/orca-runtime-terminal-split-authority.test.ts
  • src/main/runtime/rpc/methods/orchestration-federation-setup.test.ts
  • src/main/runtime/orca-runtime-process-incarnation-liveness.test.ts
  • src/shared/orchestration-worker-output.ts
  • src/main/runtime/rpc/methods/orchestration-worker-release-recovery.test.ts
  • src/main/runtime/orchestration/worker-terminal-process-liveness.ts
  • src/cli/terminal-format.test.ts
  • src/shared/protocol-version.ts
  • src/main/runtime/rpc/methods/orchestration-worker-release-completion.ts
  • src/main/runtime/claude-agent-teams-service.test.ts
  • src/main/runtime/claude-agent-teams-types.ts
  • src/main/runtime/rpc/methods/orchestration-worker-stop-liveness-verdict.test.ts
  • src/main/runtime/rpc/methods/orchestration-workers-recovery.test.ts
  • src/main/runtime/rpc/methods/orchestration-federation-control.ts
  • src/shared/runtime-types.ts
  • src/main/runtime/rpc/methods/orchestration-worker-stop.ts
  • src/main/runtime/rpc/methods/orchestration-worker-output.ts
  • src/main/runtime/rpc/methods/orchestration-worker-release.ts
  • src/shared/pty-liveness-verdict.ts
  • src/main/runtime/rpc/methods/orchestration-worker-release-liveness-verdict.test.ts
  • src/main/runtime/rpc/methods/orchestration-worker-stop-capability.test.ts
  • src/main/ipc/pty-daemon-controller-teardown.test.ts
  • src/main/runtime/rpc/methods/orchestration-worker-control.ts
  • src/main/ipc/pty-ssh-stop-verdict.test.ts
  • src/main/runtime/pty-inventory-liveness-verdict.test.ts
  • src/main/runtime/orca-runtime-terminal-close-continuity.test.ts
  • src/cli/terminal-format.ts
  • src/main/runtime/rpc/methods/orchestration-worker-observation.ts
  • src/main/runtime/worktree-teardown-unstopped-pty.test.ts
  • src/main/runtime/rpc/methods/orchestration-federation.test.ts
  • src/main/runtime/rpc/methods/orchestration-federation-liveness-verdict.test.ts
  • src/main/runtime/rpc/methods/orchestration-worker-release.test.ts
  • src/main/runtime/claude-agent-teams-tmux-dispatcher.ts
  • src/main/runtime/unstopped-pty-verification.ts
  • src/main/runtime/worktree-teardown.test.ts
  • src/main/runtime/worktree-teardown.ts
  • src/main/ipc/pty.ts
  • src/main/runtime/orca-runtime.ts

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


📝 Walkthrough

Walkthrough

The change adds shared PTY liveness verdicts for exited, live, and unverifiable states. SSH PTY shutdown records unverifiable liveness when its provider is unavailable or termination cannot be confirmed. Runtime inventory reconciliation and terminal-close receipts preserve verdicts and reasons. Worker and federation orchestration expose liveness details and unknown stop results. Worktree teardown blocks removal for unverifiable stops. Agent pane operations and CLI output report the resulting PTY state.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 25.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
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.
Title check ✅ Passed The title clearly identifies the SSH provider loss defect and the corrected kill reporting behavior.
Description check ✅ Passed The description thoroughly covers the defect, implementation, testing, compatibility, regression evidence, and live SSH QA, but omits an explicit linked issue and 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.

The federation host runs its own copy of the worker observation and stop
logic, with the same two defects: `inspectRemoteAttachment` read a dropped
relay's `connected: false` as `exited`, and `federationStop` settled the
dispatch as stopped from a close it never confirmed — relaying a fabricated
success all the way home to the coordinator.

Two guards also had to move so the honest verdict does not become a new
refusal. `federationRead` gated on `status !== 'running'`, which would have
rejected a connected terminal the moment a stop lost contact with it; it now
gates on `status === 'exited'`, which is equivalent for every pre-existing
status given the two guards beside it. Local `workerStop` likewise still
attempts the close when the verdict is `unverifiable` — losing contact is a
reason to report the outcome honestly, never a reason to stop trying.

The show observations now carry the reason alongside the status, so a bare
`unverifiable` is actionable. Both are new optional fields.
@brennanb2025
brennanb2025 force-pushed the brennanb2025/ssh-stop-verdict-2 branch from c7aa737 to f48ccaf Compare August 17, 2026 00:09

@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: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/main/runtime/rpc/methods/orchestration-worker-stop.ts (1)

62-69: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Unknown worker-stop receipts discard close diagnostics.

  • src/main/runtime/rpc/methods/orchestration-worker-stop.ts#L62-L69: pass remote.close through unknownReceipt.
  • src/main/runtime/rpc/methods/orchestration-worker-stop.ts#L122-L129: pass close through unknownReceipt.
  • src/main/runtime/rpc/methods/orchestration-worker-stop-liveness-verdict.test.ts#L88-L99: assert the returned close verdict and reason.
🧹 Nitpick comments (2)
src/main/runtime/orca-runtime.ts (1)

17538-17577: 🗄️ Data Integrity & Integration | 🔵 Trivial | 💤 Low value

Remove or support the unreachable live close verdict branch.

ptyLivenessVerdictByPtyId records only unverifiable verdicts and clears entries. The separate live verdict in unstopped-pty-verification.ts does not populate this map. Therefore describeTerminalClose cannot return ptyStopVerdict: 'live'. Remove the branch, or add a runtime writer and a terminal-close test if live close receipts are required.

src/shared/pty-liveness-verdict.ts (1)

1-8: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Keep liveness rationale comments concise.

Use one brief sentence explaining that provider silence does not prove remote PTY exit, and remove repeated implementation context from the module and related test comments.

Source: Coding guidelines


ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: e1c9333c-e943-493e-bd31-71034983b3b7

📥 Commits

Reviewing files that changed from the base of the PR and between d670355 and c7aa737443d7dd957310aeae4203c43d81494c22.

📒 Files selected for processing (19)
  • src/cli/terminal-format.test.ts
  • src/cli/terminal-format.ts
  • src/main/ipc/pty-daemon-controller-teardown.test.ts
  • src/main/ipc/pty-ssh-stop-verdict.test.ts
  • src/main/ipc/pty.ts
  • src/main/runtime/orca-runtime-terminal-close-continuity.test.ts
  • src/main/runtime/orca-runtime.ts
  • src/main/runtime/pty-inventory-liveness-verdict.test.ts
  • src/main/runtime/rpc/methods/orchestration-federation-control.ts
  • src/main/runtime/rpc/methods/orchestration-federation-liveness-verdict.test.ts
  • src/main/runtime/rpc/methods/orchestration-worker-control.ts
  • src/main/runtime/rpc/methods/orchestration-worker-observation.ts
  • src/main/runtime/rpc/methods/orchestration-worker-stop-liveness-verdict.test.ts
  • src/main/runtime/rpc/methods/orchestration-worker-stop.ts
  • src/main/runtime/unstopped-pty-verification.ts
  • src/main/runtime/worktree-teardown-unstopped-pty.test.ts
  • src/main/runtime/worktree-teardown.ts
  • src/shared/pty-liveness-verdict.ts
  • src/shared/runtime-types.ts

Included review availability: Your plan includes up to 10 reviews per rolling hour; 1 remains after this review.

Comment thread src/main/ipc/pty.ts Resolved
Comment on lines +104 to +109
const outcome = await call('orchestration.federationRead', {
dispatchId: DISPATCH_ID
}).catch((error: unknown) => error)

expect(outcome).not.toMatchObject({ code: 'worker_identity_changed' })
})

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.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Require a successful terminal read.

This test catches every error. An unrelated readTerminal failure satisfies the assertion.

Mock a successful runtime.readTerminal result. Then await a resolved federation-read response and assert that readTerminal received HANDLE. This verifies that output remains available.

Comment on lines +88 to +99
const stopped = (await call('orchestration.workerStop', { dispatch: dispatch.id })) as {
state: string
processAction: string
lastError: string
}
// Losing contact is a reason to report honestly, never to stop trying.
expect(closeTerminal).toHaveBeenCalledWith('term_worker')
expect(stopped.processAction).toBe('closed_agent_terminal')
expect(stopped.state).toBe('stop_unknown')
expect(stopped.lastError).toContain('could not be confirmed stopped')
expect(stopped.lastError).not.toContain('exited')
})

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.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Assert the returned close verdict.

This test does not inspect close.ptyStopVerdict or close.ptyStopReason. It passes when orchestration.workerStop drops the structured diagnostic.

After the receipt contract is fixed, assert both fields on stopped.close.

Comment thread src/shared/runtime-types.ts Resolved
@brennanb2025

Copy link
Copy Markdown
Contributor Author

CI green on f48ccaf: 51 checks, 0 non-passing (mergeStateStatus CLEAN). Not merged, per the task.

@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

🧹 Nitpick comments (1)
src/shared/pty-liveness-verdict.ts (1)

24-33: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Use the shared stop-receipt formatter.

describeUnconfirmedAgentStop duplicates implementations in the agent-pane, worker-stop, federation-control, and worker-release modules. Import this helper there and remove the copies.


ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 0968b788-4c5e-45d5-a9b5-42592bcbb082

📥 Commits

Reviewing files that changed from the base of the PR and between c7aa737443d7dd957310aeae4203c43d81494c22 and 03a785a.

📒 Files selected for processing (40)
  • src/cli/terminal-format.test.ts
  • src/cli/terminal-format.ts
  • src/main/ipc/pty-daemon-controller-teardown.test.ts
  • src/main/ipc/pty-ssh-stop-verdict.test.ts
  • src/main/ipc/pty.ts
  • src/main/runtime/claude-agent-teams-service.test.ts
  • src/main/runtime/claude-agent-teams-tmux-dispatcher.ts
  • src/main/runtime/claude-agent-teams-types.ts
  • src/main/runtime/orca-runtime-process-incarnation-liveness.test.ts
  • src/main/runtime/orca-runtime-terminal-close-continuity.test.ts
  • src/main/runtime/orca-runtime-terminal-split-authority.test.ts
  • src/main/runtime/orca-runtime.ts
  • src/main/runtime/orchestration/db.ts
  • src/main/runtime/orchestration/worker-terminal-process-liveness.ts
  • src/main/runtime/pty-inventory-liveness-verdict.test.ts
  • src/main/runtime/rpc/methods/orchestration-federation-control.ts
  • src/main/runtime/rpc/methods/orchestration-federation-liveness-verdict.test.ts
  • src/main/runtime/rpc/methods/orchestration-federation-output.test.ts
  • src/main/runtime/rpc/methods/orchestration-federation-setup.test.ts
  • src/main/runtime/rpc/methods/orchestration-federation.test.ts
  • src/main/runtime/rpc/methods/orchestration-worker-control.ts
  • src/main/runtime/rpc/methods/orchestration-worker-observation.ts
  • src/main/runtime/rpc/methods/orchestration-worker-output.test.ts
  • src/main/runtime/rpc/methods/orchestration-worker-output.ts
  • src/main/runtime/rpc/methods/orchestration-worker-release-completion.ts
  • src/main/runtime/rpc/methods/orchestration-worker-release-liveness-verdict.test.ts
  • src/main/runtime/rpc/methods/orchestration-worker-release-recovery.test.ts
  • src/main/runtime/rpc/methods/orchestration-worker-release.test.ts
  • src/main/runtime/rpc/methods/orchestration-worker-release.ts
  • src/main/runtime/rpc/methods/orchestration-worker-stop-capability.test.ts
  • src/main/runtime/rpc/methods/orchestration-worker-stop-liveness-verdict.test.ts
  • src/main/runtime/rpc/methods/orchestration-worker-stop.ts
  • src/main/runtime/rpc/methods/orchestration-workers-recovery.test.ts
  • src/main/runtime/unstopped-pty-verification.ts
  • src/main/runtime/worktree-teardown-unstopped-pty.test.ts
  • src/main/runtime/worktree-teardown.test.ts
  • src/main/runtime/worktree-teardown.ts
  • src/shared/orchestration-worker-output.ts
  • src/shared/protocol-version.ts
  • src/shared/pty-liveness-verdict.ts
🚧 Files skipped from review as they are similar to previous changes (4)
  • src/cli/terminal-format.ts
  • src/cli/terminal-format.test.ts
  • src/main/runtime/rpc/methods/orchestration-worker-observation.ts
  • src/main/runtime/rpc/methods/orchestration-federation-control.ts

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

Comment on lines +28537 to 28564
let stopped = false
if (this.ptyController?.stopAndWait) {
try {
stopped = await this.ptyController.stopAndWait(ptyId, { deadlineMs })
} catch (error) {
this.markPtyLivenessUnverifiable(
ptyId,
error instanceof Error ? error.message : String(error)
)
}
if (!stopped) {
const verdict = this.getPtyLivenessVerdict(ptyId)
const providerAlreadyRetiredPty =
verdict?.status === 'unverifiable' &&
verdict.reason === SSH_PROVIDER_UNREGISTERED_REASON
if (!providerAlreadyRetiredPty) {
this.ptyController.kill(ptyId)
if (!verdict || verdict.status === 'live') {
this.markPtyLivenessUnverifiable(
ptyId,
'a follow-up stop was issued but its outcome could not be verified'
)
}
}
}
} else {
stopped = this.ptyController?.kill(ptyId) ?? false
}

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

Guard this.ptyController against becoming null after the stopAndWait await.

At line 28553, this.ptyController.kill(ptyId) runs without a null check or try/catch, after await this.ptyController.stopAndWait(...) at line 28540. setPtyController accepts null and can run while this await is pending (for example during a renderer detach). If this.ptyController becomes null in that window, this.ptyController.kill(ptyId) throws a TypeError.

No caller of stopExplicitlyClosedTabPtys (for example closeTerminal) wraps the call in a try/catch, so the exception would propagate out of the terminal-close RPC path instead of leaving the terminal closed with an unverified-stop receipt.

The rejected-pty cleanup path in splitPtyBackedTerminal (around lines 29066-29071) already guards the equivalent this.ptyController.kill(...) call with a try/catch for exactly this reason. Apply the same guard here, or capture this.ptyController once at the top of the loop iteration and use optional chaining for both calls.

🛡️ Proposed fix
           if (!providerAlreadyRetiredPty) {
-            this.ptyController.kill(ptyId)
+            try {
+              this.ptyController?.kill(ptyId)
+            } catch {
+              // Best-effort fallback kill; the pty already carries an unverified-stop verdict below.
+            }
             if (!verdict || verdict.status === 'live') {
📝 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
let stopped = false
if (this.ptyController?.stopAndWait) {
try {
stopped = await this.ptyController.stopAndWait(ptyId, { deadlineMs })
} catch (error) {
this.markPtyLivenessUnverifiable(
ptyId,
error instanceof Error ? error.message : String(error)
)
}
if (!stopped) {
const verdict = this.getPtyLivenessVerdict(ptyId)
const providerAlreadyRetiredPty =
verdict?.status === 'unverifiable' &&
verdict.reason === SSH_PROVIDER_UNREGISTERED_REASON
if (!providerAlreadyRetiredPty) {
this.ptyController.kill(ptyId)
if (!verdict || verdict.status === 'live') {
this.markPtyLivenessUnverifiable(
ptyId,
'a follow-up stop was issued but its outcome could not be verified'
)
}
}
}
} else {
stopped = this.ptyController?.kill(ptyId) ?? false
}
let stopped = false
if (this.ptyController?.stopAndWait) {
try {
stopped = await this.ptyController.stopAndWait(ptyId, { deadlineMs })
} catch (error) {
this.markPtyLivenessUnverifiable(
ptyId,
error instanceof Error ? error.message : String(error)
)
}
if (!stopped) {
const verdict = this.getPtyLivenessVerdict(ptyId)
const providerAlreadyRetiredPty =
verdict?.status === 'unverifiable' &&
verdict.reason === SSH_PROVIDER_UNREGISTERED_REASON
if (!providerAlreadyRetiredPty) {
try {
this.ptyController?.kill(ptyId)
} catch {
// Best-effort fallback kill; the pty already carries an unverified-stop verdict below.
}
if (!verdict || verdict.status === 'live') {
this.markPtyLivenessUnverifiable(
ptyId,
'a follow-up stop was issued but its outcome could not be verified'
)
}
}
}
} else {
stopped = this.ptyController?.kill(ptyId) ?? false
}

@brennanb2025

Copy link
Copy Markdown
Contributor Author

Live SSH QA evidence (previously captured)

This evidence was captured at 3127133768fcb0f910b317aacafb9ff8672732a4 against a real Ubuntu 20.04 ARM64 Docker SSH target. It does not establish Electron/SSH behavior at the current or final PR head; every production change after 3127133 remains uncovered by this QA, and the later closeout commit is test-only.

After window.api.ssh.disconnect unregistered the provider, the real compiled CLI close returned ptyKilled=false, ptyStopVerdict=unverifiable, and reason its SSH provider is no longer registered. Independent SSH kill -0 and ps checks proved the remote PID remained alive as sleep 86400; the relay log showed clients=0, ptys=1, and grace timeout 0. The renderer console had zero errors, with only pre-existing development warnings.

Live SSH close receipt

Evidence limits: SSH auth.log was unavailable; only ARM64 was exercised (not x64). Electron UI, local/native SSH, WSL SSH, Windows SSH, folder-workspace SSH, live federation, live worker-release, and live agent-team respawn QA did not run. Mobile QA is not applicable to this desktop/SSH change.

@coderabbitai

coderabbitai Bot commented Aug 17, 2026

Copy link
Copy Markdown
Contributor

Note

GitHub couldn't provide a complete incremental comparison for this pull request, so CodeRabbit is performing a full review instead. This review may take a little longer.

…7-closeout

# Conflicts:
#	src/main/runtime/orchestration/db.ts
@brennanb2025

Copy link
Copy Markdown
Contributor Author

Final review verdict: release-ready

The review enforced one invariant throughout: losing contact with a process-owning host is not positive evidence that the process exited. The implementation now preserves live / unverifiable / exited across five surfaces:

  1. SSH teardown — an unavailable provider no longer fabricates a successful kill; close receipts and worktree teardown preserve unverifiable.
  2. Inventory failure — failed providers are omitted from the observed host scope, their PTYs remain unverifiable, and inventories from responding providers stay visible.
  3. Worker release — an unknown stop is not retried as though it were a fresh release, while the canonical confirmed state remains exited.
  4. Federation reads — an unverifiable remote worker remains readable; only a positively observed exit blocks the read.
  5. Agent-team respawn — unverifiable prior panes are not treated as exited and silently respawned.

Failure diagnosis and evidence

The failures at 03a785aadd3c0a2edf70de80efe37e2a6f361ff4 were genuine stale assertions, not load/order flake. Each failing file reproduced the same failure in isolation: the SSH lease test expected fabricated -1 instead of provider-confirmed 0, the runtime kill/exit test expected aggregate rejection instead of preserving responding inventories, and worker-release mocks used legacy dead instead of canonical exited. After correcting those assertions, the three-file group passed 59/59; the later main-sync/host-scope work passed focused suites locally as well.

The PR body records the red-first evidence before the implementation: failures in pty-ssh-stop-verdict, terminal formatting, worker-stop liveness, worktree teardown, and inventory liveness. It also records deliberate mutation checks that reintroduced fabricated kill success, removed the teardown short-circuit, collapsed unverifiable to exited, removed receipt/CLI wording, unconditionally settled federation stops, restored the federation-read refusal, and skipped unverifiable worker-stop attempts; each mutation made its binding assertion fail.

Final-head CI

Final head: fab305030ce71aa3cafc9ef8184aa3bf44b7bb6f.

Observed at that exact head: 47 passing checks, 4 intentional skips, 0 non-passing; mergeable/CLEAN. PR Checks run 32000317068 completed with 43 successes and 1 skip, including green Node 24/26 shards 3, 9, and 13 and a green verify gate. The skipped checks were PR e2e plus Computer-use e2e mac, linux, and windows; they were not counted as executed QA.

QA boundary

The visible live SSH evidence was captured only at 3127133768fcb0f910b317aacafb9ff8672732a4, against a real Ubuntu 20.04 ARM64 Docker SSH target. It showed ptyKilled=false, ptyStopVerdict=unverifiable, the remote PID still alive via independent kill -0 and ps, and relay state clients=0, ptys=1, grace 0.

The following QA legs did not run: Electron UI, local/native SSH, WSL SSH, Windows SSH, folder-workspace SSH, live federation, live worker-release, and live agent-team respawn. SSH auth.log was unavailable; only ARM64 was exercised, so x64 was not tested. Mobile QA is inapplicable. Every production change after 3127133 remains uncovered by live Electron/SSH QA.

Verdict: release-ready on the observed evidence above. Not merged.

@brennanb2025
brennanb2025 merged commit 7afce2e into main Aug 17, 2026
51 checks passed
brennanb2025 added a commit that referenced this pull request Aug 17, 2026
The Known gaps section was accurate when written and becomes actively
misleading as its fixes land: it told a reader to go fix restoreRequired,
the missing unverifiable verdict, and the absent terminal-list host field,
three things now addressed by #14974, #14977 and #14973.

Mark the section as dated, require verification against current code before
acting on any entry, name the PR per entry, and move landed items out. Also
correct the two body claims that the landed fixes invalidated. The rules
above are durable; only this section rots.
brennanb2025 added a commit that referenced this pull request Aug 20, 2026
…bulary (#14971)

* docs(ssh): state the SSH execution boundary and pin the liveness vocabulary

Nothing under docs/ described how work splits between the client and an SSH
host, so agents and humans inferred it from error strings and got it wrong:
loss of contact was repeatedly reported as process death, which orphaned live
remote agents and cold-started duplicates over the same worktree.

Pins the vocabulary to the incumbent live/unverifiable/exited verdict from
unstopped-pty-verification so no synonym is introduced, records the one real
discriminator (all of a host's terminals drop together on link loss; one alone
means process exit), and lists the outstanding gaps with citations.

Tracked via the docs allow-list and linked from AGENTS.md, per the convention
in .gitignore.

* docs(ssh): cite the live restoreRequired site after it moved

The throw now lives in reattachSshPtySessionForSpawn; ssh-pty-provider.ts no
longer contains it. Caught by the worker fixing it, against a newer main than
the audit ran on.

* docs(ssh): require host evidence for liveness verdicts

* docs(ssh): keep boundary references stable

* docs(ssh): fence liveness evidence to its host identity

* docs(ssh): state replay and environment boundaries precisely

* docs(ssh): correct replay and platform boundary claims

* docs(ssh): describe headless runtime continuity accurately

* docs(ssh): distinguish authority from client metadata

* docs(ssh): describe pending fixes accurately

* docs(ssh): date the gap list and name the PR that closes each entry

The Known gaps section was accurate when written and becomes actively
misleading as its fixes land: it told a reader to go fix restoreRequired,
the missing unverifiable verdict, and the absent terminal-list host field,
three things now addressed by #14974, #14977 and #14973.

Mark the section as dated, require verification against current code before
acting on any entry, name the PR per entry, and move landed items out. Also
correct the two body claims that the landed fixes invalidated. The rules
above are durable; only this section rots.

* docs(ssh): make the boundary doc a durable ruleset, not an incident record

The Known gaps section was 18 of 93 lines enumerating specific defects from
one investigation, several already fixed by sibling PRs in the same batch. A
reference doc that needs a 'this section rots' warning is telling you the
section belongs somewhere else; those entries belong in issues.

Replace the six-row table of currently-lying signals with the method that
outlands any particular bug: ask whether the owning host produced the signal,
whether every PTY on the target went quiet together, whether the termination
event matches the current incarnation and generation, and whether a returned
status is actually a claim. Same for artifacts - state what ls-remote and a PR
head each do and do not prove, rather than listing which command is currently
wrong.

Nothing here goes stale when the open fixes land.
paidaxingyo666 pushed a commit to paidaxingyo666/Manta that referenced this pull request Aug 21, 2026
…ne (stablyai#14977)

* fix(ssh): stop reporting a confirmed kill when the SSH provider is gone

A detached relay PTY is designed to outlive the provider that addressed it
(it ignores SIGHUP and ships with an unlimited grace), so "the SSH provider
is no longer registered" is lost contact, never evidence the remote process
stopped. Both stop primitives in the PTY controller returned `true` from
that branch, and every caller downstream reported the fabricated success:
the CLI printed "PTY killed.", worker-stop settled the dispatch as stopped,
and — because the stop "succeeded" — the unstopped-PTY gate never ran, so
worktree removal walked straight past a live remote agent.

`kill`/`stopAndWait` now still tombstone the local lease but report an
unconfirmed stop and record why, using the three-verdict vocabulary the
worktree teardown gate already spoke (`live` / `unverifiable` / `exited`),
promoted out of that module into `src/shared/pty-liveness-verdict.ts`.
The close receipt, the CLI wording, worker-stop and the removal gate all
read that verdict instead of inferring an exit from silence.

The same rule fixes the mirror-image defect: the aggregate inventory only
enumerates registered providers, so a dropped relay clears `connected` for
every remote PTY at once. The sweep now separates the provider answering
"absent" (an exit) from no provider being able to answer (lost contact), so
worker-stop stops claiming `exited` from a disconnect.

The `connected` wire field is unchanged in meaning and shape.

* fix(orchestration): apply the same honesty to the federation stop path

The federation host runs its own copy of the worker observation and stop
logic, with the same two defects: `inspectRemoteAttachment` read a dropped
relay's `connected: false` as `exited`, and `federationStop` settled the
dispatch as stopped from a close it never confirmed — relaying a fabricated
success all the way home to the coordinator.

Two guards also had to move so the honest verdict does not become a new
refusal. `federationRead` gated on `status !== 'running'`, which would have
rejected a connected terminal the moment a stop lost contact with it; it now
gates on `status === 'exited'`, which is equivalent for every pre-existing
status given the two guards beside it. Local `workerStop` likewise still
attempts the close when the verdict is `unverifiable` — losing contact is a
reason to report the outcome honestly, never a reason to stop trying.

The show observations now carry the reason alongside the status, so a bare
`unverifiable` is actionable. Both are new optional fields.

* fix(ssh): preserve unconfirmed stop verdicts across consumers

* fix(ssh): use canonical live verdict wording

* fix(ssh): refuse wrong-host teardown verification

* test(orchestration): confirm worker release teardown

* fix(orchestration): negotiate honest worker stop receipts

* fix(agent-teams): fence uncertain teammate respawns

* fix(ssh): avoid duplicate missing-provider teardown

* fix(orchestration): preserve archives across release retries

* fix(ssh): preserve verdicts across synthetic kill exits

* fix(ssh): preserve liveness evidence across teardown

* fix(agent-teams): replace panes only after confirmed stop

* fix(ssh): distinguish host exits from relay loss

* fix(ssh): narrow concurrent inventory verdicts

* fix(orchestration): serve archives after uncertain release

* fix(orchestration): expose unverifiable read liveness

* test(ssh): align liveness assertions with verdicts

* fix(ssh): preserve host scope across inventory failures
paidaxingyo666 pushed a commit to paidaxingyo666/Manta that referenced this pull request Aug 21, 2026
…bulary (stablyai#14971)

* docs(ssh): state the SSH execution boundary and pin the liveness vocabulary

Nothing under docs/ described how work splits between the client and an SSH
host, so agents and humans inferred it from error strings and got it wrong:
loss of contact was repeatedly reported as process death, which orphaned live
remote agents and cold-started duplicates over the same worktree.

Pins the vocabulary to the incumbent live/unverifiable/exited verdict from
unstopped-pty-verification so no synonym is introduced, records the one real
discriminator (all of a host's terminals drop together on link loss; one alone
means process exit), and lists the outstanding gaps with citations.

Tracked via the docs allow-list and linked from AGENTS.md, per the convention
in .gitignore.

* docs(ssh): cite the live restoreRequired site after it moved

The throw now lives in reattachSshPtySessionForSpawn; ssh-pty-provider.ts no
longer contains it. Caught by the worker fixing it, against a newer main than
the audit ran on.

* docs(ssh): require host evidence for liveness verdicts

* docs(ssh): keep boundary references stable

* docs(ssh): fence liveness evidence to its host identity

* docs(ssh): state replay and environment boundaries precisely

* docs(ssh): correct replay and platform boundary claims

* docs(ssh): describe headless runtime continuity accurately

* docs(ssh): distinguish authority from client metadata

* docs(ssh): describe pending fixes accurately

* docs(ssh): date the gap list and name the PR that closes each entry

The Known gaps section was accurate when written and becomes actively
misleading as its fixes land: it told a reader to go fix restoreRequired,
the missing unverifiable verdict, and the absent terminal-list host field,
three things now addressed by stablyai#14974, stablyai#14977 and stablyai#14973.

Mark the section as dated, require verification against current code before
acting on any entry, name the PR per entry, and move landed items out. Also
correct the two body claims that the landed fixes invalidated. The rules
above are durable; only this section rots.

* docs(ssh): make the boundary doc a durable ruleset, not an incident record

The Known gaps section was 18 of 93 lines enumerating specific defects from
one investigation, several already fixed by sibling PRs in the same batch. A
reference doc that needs a 'this section rots' warning is telling you the
section belongs somewhere else; those entries belong in issues.

Replace the six-row table of currently-lying signals with the method that
outlands any particular bug: ask whether the owning host produced the signal,
whether every PTY on the target went quiet together, whether the termination
event matches the current incarnation and generation, and whether a returned
status is actually a claim. Same for artifacts - state what ls-remote and a PR
head each do and do not prove, rather than listing which command is currently
wrong.

Nothing here goes stale when the open fixes land.
dallascrilley pushed a commit to dallascrilley/orca that referenced this pull request Aug 27, 2026
…bulary (stablyai#14971)

* docs(ssh): state the SSH execution boundary and pin the liveness vocabulary

Nothing under docs/ described how work splits between the client and an SSH
host, so agents and humans inferred it from error strings and got it wrong:
loss of contact was repeatedly reported as process death, which orphaned live
remote agents and cold-started duplicates over the same worktree.

Pins the vocabulary to the incumbent live/unverifiable/exited verdict from
unstopped-pty-verification so no synonym is introduced, records the one real
discriminator (all of a host's terminals drop together on link loss; one alone
means process exit), and lists the outstanding gaps with citations.

Tracked via the docs allow-list and linked from AGENTS.md, per the convention
in .gitignore.

* docs(ssh): cite the live restoreRequired site after it moved

The throw now lives in reattachSshPtySessionForSpawn; ssh-pty-provider.ts no
longer contains it. Caught by the worker fixing it, against a newer main than
the audit ran on.

* docs(ssh): require host evidence for liveness verdicts

* docs(ssh): keep boundary references stable

* docs(ssh): fence liveness evidence to its host identity

* docs(ssh): state replay and environment boundaries precisely

* docs(ssh): correct replay and platform boundary claims

* docs(ssh): describe headless runtime continuity accurately

* docs(ssh): distinguish authority from client metadata

* docs(ssh): describe pending fixes accurately

* docs(ssh): date the gap list and name the PR that closes each entry

The Known gaps section was accurate when written and becomes actively
misleading as its fixes land: it told a reader to go fix restoreRequired,
the missing unverifiable verdict, and the absent terminal-list host field,
three things now addressed by stablyai#14974, stablyai#14977 and stablyai#14973.

Mark the section as dated, require verification against current code before
acting on any entry, name the PR per entry, and move landed items out. Also
correct the two body claims that the landed fixes invalidated. The rules
above are durable; only this section rots.

* docs(ssh): make the boundary doc a durable ruleset, not an incident record

The Known gaps section was 18 of 93 lines enumerating specific defects from
one investigation, several already fixed by sibling PRs in the same batch. A
reference doc that needs a 'this section rots' warning is telling you the
section belongs somewhere else; those entries belong in issues.

Replace the six-row table of currently-lying signals with the method that
outlands any particular bug: ask whether the owning host produced the signal,
whether every PTY on the target went quiet together, whether the termination
event matches the current incarnation and generation, and whether a returned
status is actually a claim. Same for artifacts - state what ls-remote and a PR
head each do and do not prove, rather than listing which command is currently
wrong.

Nothing here goes stale when the open fixes land.
enrick-dev pushed a commit to enrick-dev/orca that referenced this pull request Oct 15, 2026
…ne (stablyai#14977)

* fix(ssh): stop reporting a confirmed kill when the SSH provider is gone

A detached relay PTY is designed to outlive the provider that addressed it
(it ignores SIGHUP and ships with an unlimited grace), so "the SSH provider
is no longer registered" is lost contact, never evidence the remote process
stopped. Both stop primitives in the PTY controller returned `true` from
that branch, and every caller downstream reported the fabricated success:
the CLI printed "PTY killed.", worker-stop settled the dispatch as stopped,
and — because the stop "succeeded" — the unstopped-PTY gate never ran, so
worktree removal walked straight past a live remote agent.

`kill`/`stopAndWait` now still tombstone the local lease but report an
unconfirmed stop and record why, using the three-verdict vocabulary the
worktree teardown gate already spoke (`live` / `unverifiable` / `exited`),
promoted out of that module into `src/shared/pty-liveness-verdict.ts`.
The close receipt, the CLI wording, worker-stop and the removal gate all
read that verdict instead of inferring an exit from silence.

The same rule fixes the mirror-image defect: the aggregate inventory only
enumerates registered providers, so a dropped relay clears `connected` for
every remote PTY at once. The sweep now separates the provider answering
"absent" (an exit) from no provider being able to answer (lost contact), so
worker-stop stops claiming `exited` from a disconnect.

The `connected` wire field is unchanged in meaning and shape.

* fix(orchestration): apply the same honesty to the federation stop path

The federation host runs its own copy of the worker observation and stop
logic, with the same two defects: `inspectRemoteAttachment` read a dropped
relay's `connected: false` as `exited`, and `federationStop` settled the
dispatch as stopped from a close it never confirmed — relaying a fabricated
success all the way home to the coordinator.

Two guards also had to move so the honest verdict does not become a new
refusal. `federationRead` gated on `status !== 'running'`, which would have
rejected a connected terminal the moment a stop lost contact with it; it now
gates on `status === 'exited'`, which is equivalent for every pre-existing
status given the two guards beside it. Local `workerStop` likewise still
attempts the close when the verdict is `unverifiable` — losing contact is a
reason to report the outcome honestly, never a reason to stop trying.

The show observations now carry the reason alongside the status, so a bare
`unverifiable` is actionable. Both are new optional fields.

* fix(ssh): preserve unconfirmed stop verdicts across consumers

* fix(ssh): use canonical live verdict wording

* fix(ssh): refuse wrong-host teardown verification

* test(orchestration): confirm worker release teardown

* fix(orchestration): negotiate honest worker stop receipts

* fix(agent-teams): fence uncertain teammate respawns

* fix(ssh): avoid duplicate missing-provider teardown

* fix(orchestration): preserve archives across release retries

* fix(ssh): preserve verdicts across synthetic kill exits

* fix(ssh): preserve liveness evidence across teardown

* fix(agent-teams): replace panes only after confirmed stop

* fix(ssh): distinguish host exits from relay loss

* fix(ssh): narrow concurrent inventory verdicts

* fix(orchestration): serve archives after uncertain release

* fix(orchestration): expose unverifiable read liveness

* test(ssh): align liveness assertions with verdicts

* fix(ssh): preserve host scope across inventory failures
enrick-dev pushed a commit to enrick-dev/orca that referenced this pull request Oct 15, 2026
…bulary (stablyai#14971)

* docs(ssh): state the SSH execution boundary and pin the liveness vocabulary

Nothing under docs/ described how work splits between the client and an SSH
host, so agents and humans inferred it from error strings and got it wrong:
loss of contact was repeatedly reported as process death, which orphaned live
remote agents and cold-started duplicates over the same worktree.

Pins the vocabulary to the incumbent live/unverifiable/exited verdict from
unstopped-pty-verification so no synonym is introduced, records the one real
discriminator (all of a host's terminals drop together on link loss; one alone
means process exit), and lists the outstanding gaps with citations.

Tracked via the docs allow-list and linked from AGENTS.md, per the convention
in .gitignore.

* docs(ssh): cite the live restoreRequired site after it moved

The throw now lives in reattachSshPtySessionForSpawn; ssh-pty-provider.ts no
longer contains it. Caught by the worker fixing it, against a newer main than
the audit ran on.

* docs(ssh): require host evidence for liveness verdicts

* docs(ssh): keep boundary references stable

* docs(ssh): fence liveness evidence to its host identity

* docs(ssh): state replay and environment boundaries precisely

* docs(ssh): correct replay and platform boundary claims

* docs(ssh): describe headless runtime continuity accurately

* docs(ssh): distinguish authority from client metadata

* docs(ssh): describe pending fixes accurately

* docs(ssh): date the gap list and name the PR that closes each entry

The Known gaps section was accurate when written and becomes actively
misleading as its fixes land: it told a reader to go fix restoreRequired,
the missing unverifiable verdict, and the absent terminal-list host field,
three things now addressed by stablyai#14974, stablyai#14977 and stablyai#14973.

Mark the section as dated, require verification against current code before
acting on any entry, name the PR per entry, and move landed items out. Also
correct the two body claims that the landed fixes invalidated. The rules
above are durable; only this section rots.

* docs(ssh): make the boundary doc a durable ruleset, not an incident record

The Known gaps section was 18 of 93 lines enumerating specific defects from
one investigation, several already fixed by sibling PRs in the same batch. A
reference doc that needs a 'this section rots' warning is telling you the
section belongs somewhere else; those entries belong in issues.

Replace the six-row table of currently-lying signals with the method that
outlands any particular bug: ask whether the owning host produced the signal,
whether every PTY on the target went quiet together, whether the termination
event matches the current incarnation and generation, and whether a returned
status is actually a claim. Same for artifacts - state what ls-remote and a PR
head each do and do not prove, rather than listing which command is currently
wrong.

Nothing here goes stale when the open fixes land.
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