Skip to content

fix(remote-runtime): stop a shared inventory snapshot from retiring a sibling split leaf - #20922

Open
YgorLeal wants to merge 1 commit into
stablyai:mainfrom
YgorLeal:YgorLeal/scoter
Open

YgorLeal wants to merge 1 commit into
stablyai:mainfrom
YgorLeal:YgorLeal/scoter

Conversation

@YgorLeal

@YgorLeal YgorLeal commented Sep 16, 2026 •

Copy link
Copy Markdown

ELI5

When several terminal panes open at once on a remote workspace, each one asks the host "which terminals exist?". If a pane's answer shows its neighbours but not itself, Orca decides that pane was closed and shows Remote terminal was closed. over a terminal that is actually running. One answer like that isn't enough to know: the host may not have published the new pane yet, or the pane may have been handed a neighbour's older answer. This PR has Orca check a second time before declaring the pane dead, unless the host has said outright that it closed it.

What Changed

Rebased onto current main and narrowed. #26483 already moved this wait loop onto listRemoteRuntimeSessionTabsAfterCurrentInFlight, so this PR now carries only the part main still lacks.

In waitForHostSessionHandle (remote-runtime-pty-transport.ts), when a listing does not contain the pane's leaf:

  • The host says it retired the leaf (retiredTerminalSurfaces has this parentTabId/leafId): retire right away. This check runs before the empty-tab guard, so a tab whose last leaf was closed is reported closed immediately. Today it waits out the full 15s window and ends as unknown liveness.
  • Only siblings are listed, and there is no proof: poll once more. Retire only if a second listing also leaves the leaf out. If a listing shows the leaf in between, start counting again.
  • Everything else is unchanged: anything that doesn't meet this bar still ends as unknown liveness and keeps its parked retry.

I also added hasRetirementProofForSurface() to src/shared/terminal-retirement-proof-ledger.ts, next to the other readers. It is keyed by surface rather than by handle, so it uses the same identity dropRetirementProofsForLiveSurfaces already uses.

Before: an orchestrator opens several split panes, and one of them gets a dead Remote terminal was closed. box with no way to recover it, even though its shell is running.
After: that pane attaches normally. A pane the host really did close still closes, after one more poll (or right away when the host sent a proof).

Why

Waiting for in-flight requests (#26483) is not enough on its own. listRemoteRuntimeSessionTabsAfterCurrentInFlight waits for the current in-flight listing and then joins whichever request is in flight at that moment. Another sibling pane can start that request in the gap, so the transport still gets an answer built before its own leaf was published. The new test confirms absence from a listing that began after a sibling listing still in flight reproduces this and fails on main.

The bar is not new. web-session-terminal-orphan-recovery-inventory.ts already requires either an explicit retiredTerminalSurfaces entry or two authoritative inventories that both omit the identity, and docs/reference/ssh-execution-boundary.md forbids reporting unverifiable as exited. This PR applies the same rule to the attach path.

Alternatives I rejected:

  • Make every listing in the loop a guaranteed-new RPC (no joining). That fixes the stale-snapshot half, but not a host that is still publishing the split, and it costs one RPC per pane on the hot path.
  • Treat sibling-only absence as unknown and let the 15s window expire. That never wrongly closes a live pane, but a genuinely closed split would hang for 15s instead of closing after about 450ms.

Linked Issue

Fixes #20917

Visual Proof

N/A: the pane renders the same way for both outcomes. The change is in which evidence can choose the "closed" outcome. The symptom is the error box in #20917, which no longer appears for a live terminal.

Testing

These tests are in remote-runtime-pty-transport-host-absence-evidence.test.ts, next to the existing activation-absence case. Results are against the unmodified code on current main:

  • does not retire a split leaf on one sibling-only listing the host has not caught up with: fails on main
  • confirms absence from a listing that began after a sibling listing still in flight: fails on main
  • retires the last leaf of a tab on the host retirement proof: times out on main
  • retires a split leaf once two listings agree it is gone while a sibling remains replaces the single-listing test in split-leaf-activation.test.ts. On main it fails because only one listing is made.
  • retires a split leaf on the host retirement proof without a second listing: passes on main, and guards that a proof still short-circuits.
pnpm tc                                                         clean
pnpm run check:code-quality:changed                             0 new findings
vitest src/renderer/src/components/terminal-pane/remote-runtime 47 files, 328 tests
vitest src/renderer/src/runtime + terminal-pane/remote-runtime  317 files, 2495 tests
  • I manually tested these changes locally
  • Automated tests added/updated, or explained why not below

"Manually tested" here means the specs run against the real transport. I don't have a paired remote host to reproduce the orchestrator race by hand.

AI Disclosure

Review

#20923 (the two sibling inferences I flagged in the first version of this PR) was fixed by #26483, which is why the scope here shrank.

Agent skill upstream boundary

  • Not applicable, or this change copies or mechanically translates no upstream skill-installer source, tests, fixtures, registry entries, path tables, comments, or documentation.

Notes

  • Security: no new surface. The client becomes less willing to act on input the host hasn't attested.
  • Cross-platform: renderer-only, with no platform-dependent paths.
  • Remote SSH: this is the remote path, and the change is the one ssh-execution-boundary.md prescribes.
  • Mobile: paired clients share the mirrored-pane transport and get the same behaviour change.
  • Backwards compatibility: no wire change. retiredTerminalSurfaces is an existing optional field that is only read here. A host that never sends it falls back to the two-listing rule.
  • Performance: one extra session.tabs.list, only on the absence path and only when the host sent no proof. The success path is unchanged.

Checklist

  • This PR is small and focused
  • I explained what changed and why (ELI5, the user-facing before/after, the mechanism, and why over the alternatives)
  • Before/after screenshots or videos attached for UI changes, or N/A with reason
  • Self-reviewed for correctness, security, and performance
  • Cross-platform, SSH/remote, and path/shortcut impact considered (or N/A)
  • pnpm lint, pnpm typecheck, pnpm test, and pnpm build pass (or CI will cover; local preferred)

@coderabbitai

coderabbitai Bot commented Sep 16, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration
  • Configuration used: Repository UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 813772cf-b549-4b5d-951a-3b8b8fb93cbe
📥 Commits

Reviewing files that changed from the base of the PR and between 55193b4 and 4d8cde2.

📒 Files selected for processing: 3
  • src/renderer/src/components/terminal-pane/remote-runtime-pty-transport-host-absence-evidence.test.ts
  • src/renderer/src/components/terminal-pane/remote-runtime-pty-transport-split-leaf-activation.test.ts
  • src/renderer/src/components/terminal-pane/remote-runtime-pty-transport.ts
💤 Files with no reviewable changes: 1
  • src/renderer/src/components/terminal-pane/remote-runtime-pty-transport-split-leaf-activation.test.ts

Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 9 remain after this review.

📝 Walkthrough

The remote runtime now requires two sibling-only listings before it treats a requested split leaf as absent. A host retirement proof can establish retirement after one listing, including when the leaf is the tab’s only terminal surface. New tests cover repeated absence, delayed publication, retirement proofs, and overlapping listings. The earlier test for immediate retirement after one sibling-only listing was removed.

Priority: ➖ Normal

Severity of issue fixed: Medium

Merge Risk: ⚪ Minimal · up to 4d8cd

No actionable merge-blocking risk remains in the reviewed change. It is ready to merge after normal checks.

Pre-merge checks: Passed 4 · Failed 1

Failed Failed checks: 1 warning

Check name Status Explanation Resolution
Docstring Coverage Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 10 functions across 5 files. Write docstrings for the functions missing them to satisfy the coverage threshold.
Passed Passed checks: 4 passed
Check name Status Explanation
Linked Issues check Passed The changes address [#20917]. waitForHostSessionHandle now accepts immediate retirement only from matching host proof. Without that proof, it requires a second listing after the first observed the l…
Out of Scope Changes check Passed The changes stay within [#20917]. The transport change, hasRetirementProofForSurface helper, and focused test additions or updates all support terminal-retirement evidence. The test-file change pres…
Title check Passed The title clearly and concisely identifies the remote-runtime fix for incorrect sibling split-leaf retirement.
Description check Passed The description includes all required sections, explains the user impact and implementation, links issue #20917, documents testing, and addresses compatibility and performance.
Full details: Linked Issues check

Explanation

The changes address [#20917]. waitForHostSessionHandle now accepts immediate retirement only from matching host proof. Without that proof, it requires a second listing after the first observed the leaf missing. A later live sighting clears the pending absence. The new tests cover stale split-leaf listings, delayed publication, explicit split-leaf proof, explicit last-leaf proof, and the two-listing rule. This prevents a live terminal from receiving Remote terminal was closed. from one stale inventory.

Full details: Out of Scope Changes check

Explanation

The changes stay within [#20917]. The transport change, hasRetirementProofForSurface helper, and focused test additions or updates all support terminal-retirement evidence. The test-file change preserves coverage for the same behavior. No unrelated implementation or behavior is shown.

  • Fix all pre-merge checks with AI

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: Advanced

Run ID: 0f26e39f-b182-4dc4-bb8d-982537867889

📥 Commits

Reviewing files that changed from the base of the PR and between 7ec2986 and a58cd96acf3a9b8b8a6117498cd399112e5c5679.

📒 Files selected for processing (3)
  • src/renderer/src/components/terminal-pane/remote-runtime-pty-transport-split-leaf-activation.test.ts
  • src/renderer/src/components/terminal-pane/remote-runtime-pty-transport.ts
  • src/shared/terminal-retirement-proof-ledger.ts

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

Comment thread src/renderer/src/components/terminal-pane/remote-runtime-pty-transport.ts Outdated

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

Run ID: 180bf7ca-b41b-4c3c-a4f0-6fafec1ad6d7

📥 Commits

Reviewing files that changed from the base of the PR and between 4a5e946 and 445eab2.

📒 Files selected for processing (3)
  • src/renderer/src/components/terminal-pane/remote-runtime-pty-transport-split-leaf-absence-proof.test.ts
  • src/renderer/src/components/terminal-pane/remote-runtime-pty-transport-split-leaf-activation.test.ts
  • src/renderer/src/components/terminal-pane/remote-runtime-pty-transport.ts
💤 Files with no reviewable changes (1)
  • src/renderer/src/components/terminal-pane/remote-runtime-pty-transport-split-leaf-activation.test.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/renderer/src/components/terminal-pane/remote-runtime-pty-transport.ts

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

@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.

ℹ️ Core fix looks correct. One out-of-scope sibling path still shares the defect class, and the new regression test doesn't pin the freshness wiring it relies on.

Reviewed changes

  • Two-observation absence gate in waitForHostSessionHandle — siblingOnlyAbsenceSeen stops a sibling-only listing from retiring the leaf on first sight; the retry runs through listRemoteRuntimeSessionTabsAfterCurrentInFlight, so the confirming listing cannot be the same shared snapshot; a live sighting retracts the pending proof.
  • Host retirement-proof fast path — new hasRetirementProofForSurface in src/shared/terminal-retirement-proof-ledger.ts, keyed on parentTabId + leafId, retires immediately on explicit host removal evidence.
  • Tests — adds a stale-listing regression test (fails without the fix) and a first-listing proof test, and updates the "two listings agree" test to expect two session.tabs.list calls.

ℹ️ The sibling resubscribe path still reads one shared listing as removal

waitForResubscribeHostSessionHandle infers removal from a single listRemoteRuntimeSessionTabsDeduped response and — unlike the function this PR fixes, and unlike the empty-inventory guard #17945 added to it — retires even when the snapshot carries no surface at all for the tab. The call site turns that into retireRemoteTerminalId(-1). It is not a regression from this PR, and its reachability is narrower (the loop leads with a fresh, leaf-targeted activate, and previousHandle implies the leaf was already published), but it is the same "unverifiable read as exited" shape the fix targets.

The PR body cites #17825 as tracking this, but that issue names attachHostSessionMirror (the falsy hostHandle surfacing), and #17945 closed it without touching waitForResubscribeHostSessionHandle. So this may be untracked — a follow-up issue seems more appropriate than expanding this PR.

Technical details
# Sibling resubscribe path shares the removal-inference defect

## Affected sites
- `src/renderer/src/components/terminal-pane/remote-runtime-pty-transport.ts:862-865` — `waitForResubscribeHostSessionHandle` returns `{ handle: null }` when one `listRemoteRuntimeSessionTabsDeduped` response lacks the requested leaf: no sibling check, no `retiredTerminalSurfaces` proof, no repeat observation.
- `remote-runtime-pty-transport.ts:1802-1807` — `{ handle: null }` reaches `retireRemoteTerminalId(-1)`, which cancels recovery, sets `terminalEnded = true`, clears the published-handle listener, and arms no retry.

## Required outcome
- Either bring this path to the same bar as `waitForHostSessionHandle` (host proof or two fresh listings, and keep polling on an empty tab listing), or file it explicitly as a follow-up.

## Open questions for the human
- Is the resubscribe path intentionally more aggressive, given a handle implies the leaf was already published once?

Pullfrog  | Fix all ➔ | Fix 👍s ➔ | View workflow run | Using DeepSeek Flash (free via Pullfrog for OSS) | 𝕏

@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 in this push. The prior inline note about the regression test not pinning the in-flight wiring still stands (the test moved unchanged); the delta itself is sound.

Reviewed changes (delta a58cd96 → 445eab2)

  • Retirement proof now outranks the sibling check — the hasRetirementProofForSurface test was hoisted above sibling detection, so a retired leaf retires on the first listing even when it was the tab's last surface; the two-observation rule still gates the no-proof sibling case. This closes the asymmetry where an only-leaf close would otherwise poll to the timeout.
  • Tests reorganized — the split-leaf absence tests moved to a new remote-runtime-pty-transport-split-leaf-absence-proof.test.ts, with a new "retires the last leaf of a tab on the host retirement proof" case that genuinely fails without the hoist (the empty-tab listing otherwise polls to the timeout, so the new assertion cannot pass). Verified locally: the new file, the activation file, and remote-runtime-session-tabs-inflight.test.ts → 13 passed.

Pullfrog  | Fix it ➔ | View workflow run | Using DeepSeek Flash (free via Pullfrog for OSS) | 𝕏

@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. The delta is test-only and closes the one prior inline note.

Reviewed changes (delta 445eab2 → 55193b4, test-only)

  • Added a spec that pins the freshness wiring — waits out a sibling listing still in flight instead of confirming absence from it keeps a sibling's session.tabs.list pending in the shared in-flight cache across the confirming request, which is the only state where listRemoteRuntimeSessionTabsAfterCurrentInFlight and the plain deduped helper diverge.
  • Resolved the prior review's inline note — the regression spec previously asserted the two-observation outcome but passed even with the deduped helper, so it did not guard the call site. I independently confirmed the new spec fails (connect resolves undefined) with the helper swapped back, and that all 5 tests in the file pass.

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

…ring a split leaf

waitForHostSessionHandle retired a pending split leaf on a single
session.tabs.list that showed its siblings but not the leaf. That listing
can be a split the host is still publishing, or another sibling's stale
in-flight listing joined after the one the transport waited out, so a
live terminal was reported as "Remote terminal was closed." (stablyai#20917).

- A retiredTerminalSurfaces proof for the leaf retires on the first
  listing, checked before the empty-tab republish guard, so a tab whose
  last leaf was closed no longer waits out the 15s window.
- Without a proof, a sibling-only listing must be confirmed by a second
  one; a live sighting in between resets it.

stablyai#26483 already moved this loop to listings that start after the current
in-flight request; this closes the remaining single-observation inference.

This branch has not been deployed

No deployments
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.

[Bug]: New Pane Opens up as a bug

1 participant