fix(terminal): stop stale multiplex stream handles from swallowing input by AmethystLiang · Pull Request #16325 · stablyai/orca · GitHub
Skip to content

fix(terminal): stop stale multiplex stream handles from swallowing input - #16325

Merged
AmethystLiang merged 3 commits into
mainfrom
sta-5097-5098-cold-park
Aug 25, 2026
Merged

AmethystLiang merged 3 commits into
mainfrom
sta-5097-5098-cold-park

Conversation

@AmethystLiang

@AmethystLiang AmethystLiang commented Aug 25, 2026 •

Copy link
Copy Markdown
Contributor
Files Added Deleted Net
Test 1 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​87 0 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​87
Prod 2 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​28 $\color{#cf222e}{\Huge{\mathbf{−}}}$​14 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​14

ELI5

You park a remote terminal tab, come back to it, and it looks fine — the old output is there, it says it's connected — but typing does nothing and it never paints again. The client was writing keystrokes to a stream the host had already thrown away, and nobody noticed because the write kept reporting success.

What Changed

src/renderer/src/runtime/remote-runtime-terminal-multiplexer.ts

sendFrame gates only on socket readiness (matchesCurrentEnvironmentRevision() && ready && subscription) and never checks that the stream id is still registered. A handle whose record had been dropped — close() on park, or handleClose() clearing the stream table on reconnect — kept returning true from sendInput, claimViewport, and resize, while the host discarded those frames for an unknown stream id.

Guard those three senders on stream membership via a new isRegisteredStream. This is the same check close() and setOutputPaused already perform; these three were simply missing it.

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

Drain input queued behind a viewport claim on every stream install, not only a still-pending claim. Previously a claim that was pending with no known viewport left its queued bytes orphaned. Extracted as flushPendingClaimInput, called from both the live-claim path and the install path.

Also drops a previously merged test whose fake host answered Subscribe with an immediate snapshot — it was green by construction and could not fail for the reason it was written.

Why

A revealed cold-parked remote terminal lost PTY input and live painting. The diagnostics from the failing CI run line up field for field with the stale-handle theory:

  • restoredBuffer: true — the snapshot arrived from the earlier, still-valid subscription
  • hostReceivedInput: false, paintedLive: false — frames discarded host-side for an unknown stream id
  • recoveryState: "connected" — the client had no signal anything was wrong

It is a race: whether the stale handle is still the transport's current stream when the socket regains readiness. That is why it reproduces intermittently on CI and never on macOS.

The failure was silent rather than degraded because the transport already has a recovery path — when a stream refuses input it re-sends over the terminal.send RPC. The stale stream's false success is precisely what kept that fallback from running. Making the senders honest restores it.

Membership-gating is the minimal fix and the one consistent with the surrounding code, rather than adding new reconnect bookkeeping.

Linked Issue

Tracked in the internal issue tracker; no public GitHub issue.

Visual Proof

N/A — no UI change. The behavioral change is covered by the E2E specs below, which assert on pane content and host-side input rather than appearance.

Testing

Platform tested: macOS. CI covers Linux.

  • src/renderer/src/runtime/remote-runtime-terminal-stale-stream-frames.test.ts (new) drives the real multiplexer — not a fake host — and asserts a closed stream refuses input/claim/resize while a sibling stream keeps the socket live. Verified failing without the fix (expected true to be false), passing with it.
  • Full unit suite: 498 files / 4584 tests pass.
  • tsc --noEmit -p config/tsconfig.tc.web.json clean; oxlint clean on changed files.
  • E2E, 2x each, both pass: tests/e2e/paired-remote-terminal-parked-reveal-interactivity.spec.ts and tests/e2e/host-parked-pane-remote-viewer.spec.ts. The cold-parked scenario now reports hostReceivedInput: true, paintedLive: true.

Reviewer-relevant limits, stated plainly:

  • The local E2E pass is a non-regression check, not proof. That spec has never failed on macOS across repeated runs; only CI can confirm the fix.

  • The transport-side drain has no regression test of its own. I could not construct a deterministic repro for it — it is hardening on a reachable leak, not the proven root cause.

  • I wrote and then deleted a second unit case for the reconnect-orphan path: it passed without the fix, because handleClose releases the multiplexer instance so that handle's socket is already dead. It could not fail for the right reason, so it is not included.

  • I manually tested these changes locally

  • Automated tests added/updated, or explained why not below

AI Disclosure

Review

Agent skill upstream boundary

  • Not applicable, or this change follows docs/reference/agent-skill-sharing-upstream-boundary.md and copies or mechanically translates no upstream skill-installer source, tests, fixtures, registry entries, path tables, comments, or documentation.

Notes

Ensure no issues in: Security, Cross-platoform support (Linux, Windows, Mac), Remote SSH, Mobile, general backwards compatibility, performance

  • Remote wire compatibility: no protocol change. No new opcode, no changed payload, no change to what the host publishes. The client now stops emitting frames the host already discarded, so old hosts and old clients are unaffected in either pairing direction.
  • Remote SSH: the change is client-side within the multiplex transport and is agnostic to the execution host; it does not alter liveness reporting or the execution boundary.
  • Cross-platform: no platform-dependent code paths touched; no path, shortcut, or process handling involved.
  • Backwards compatibility: the guard can only turn a previously-true return into false, and only for a stream that was already unusable. Callers already handle false — that is the existing fallback path.
  • Performance: one Map.get identity comparison per frame send.
  • Mobile / Security: not touched.

Checklist

  • This PR is small and focused
  • I explained what changed and why (including ELI5)
  • 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)

Ran locally: full vitest suite, typecheck:web, and oxlint on changed files — all clean. Did not run the full pnpm lint / pnpm typecheck / pnpm build matrix; leaving that to CI.

Investigating STA-5098. Parking a mirrored remote tab closes its stream
while a sibling tab keeps the multiplexer alive, so the reveal
re-subscribes on an instance that already retired a stream.

This came back green, which exonerates the multiplexer as the cause of
STA-5098 — the wedge is above it. Kept as a contract guard; the header
says explicitly that it is not coverage for that ticket.
A stream handle whose record was dropped (park close, or a reconnect that
clears the stream table) kept reporting success: sendFrame gates only on
socket readiness, never on stream membership. The host drops those frames
for an unknown stream id, so a revealed cold-parked remote pane looked
connected while the PTY never saw a byte and never painted (STA-5098).

Reporting success also defeated the transport's own recovery — it re-sends
input over terminal.send when the stream refuses it, which never ran.

Guard the three public senders on stream membership, the check close() and
setOutputPaused already use, and drain input queued behind a viewport claim
on every stream install rather than only a still-pending claim.

Withdraws the parked-reveal re-subscribe test: its fake host answered
Subscribe with an immediate snapshot, so it could not fail.
@coderabbitai

coderabbitai Bot commented Aug 25, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 6ef3d788-8b51-47be-977d-6670869667e8

📥 Commits

Reviewing files that changed from the base of the PR and between 0b78479 and a833e99.

📒 Files selected for processing (3)
  • src/renderer/src/components/terminal-pane/remote-runtime-pty-transport.ts
  • src/renderer/src/runtime/remote-runtime-terminal-multiplexer.ts
  • src/renderer/src/runtime/remote-runtime-terminal-stale-stream-frames.test.ts

📝 Walkthrough

Walkthrough

The transport now uses flushPendingClaimInput to deliver queued input after successful viewport claims and stream installation. Terminal handle operations now verify that their stream remains registered before sending input, claim, or resize frames. New tests verify that operations on a closed stream return false and emit no stale frames while another stream keeps the socket active.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 16.67% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 6 functions across 3 files. 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 primary fix: stale terminal stream handles no longer report successful input sends.
Description check ✅ Passed The description is detailed and covers the change, rationale, testing, visual proof, compatibility considerations, and checklist. It does not provide the required linked issue reference; it states tha…
Full details: Description check

Explanation

The description is detailed and covers the change, rationale, testing, visual proof, compatibility considerations, and checklist. It does not provide the required linked issue reference; it states that the issue is internal instead.

✨ Finishing Touches
📝 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.

@AmethystLiang AmethystLiang changed the title Sta 5097 5098 cold park fix(terminal): stop stale multiplex stream handles from swallowing input Aug 25, 2026
@AmethystLiang
AmethystLiang merged commit cc48013 into main Aug 25, 2026
45 checks passed
paidaxingyo666 pushed a commit to paidaxingyo666/Manta that referenced this pull request Aug 25, 2026
…put (stablyai#16325)

* test(terminal): pin park/reveal re-subscribe on a shared multiplexer

Investigating STA-5098. Parking a mirrored remote tab closes its stream
while a sibling tab keeps the multiplexer alive, so the reveal
re-subscribes on an instance that already retired a stream.

This came back green, which exonerates the multiplexer as the cause of
STA-5098 — the wedge is above it. Kept as a contract guard; the header
says explicitly that it is not coverage for that ticket.

* fix(terminal): stop stale multiplex stream handles from swallowing input

A stream handle whose record was dropped (park close, or a reconnect that
clears the stream table) kept reporting success: sendFrame gates only on
socket readiness, never on stream membership. The host drops those frames
for an unknown stream id, so a revealed cold-parked remote pane looked
connected while the PTY never saw a byte and never painted (STA-5098).

Reporting success also defeated the transport's own recovery — it re-sends
input over terminal.send when the stream refuses it, which never ran.

Guard the three public senders on stream membership, the check close() and
setOutputPaused already use, and drain input queued behind a viewport claim
on every stream install rather than only a still-pending claim.

Withdraws the parked-reveal re-subscribe test: its fake host answered
Subscribe with an immediate snapshot, so it could not fail.

* chore(terminal): tighten the stale-stream comments

(cherry picked from commit cc48013)
dallascrilley pushed a commit to dallascrilley/orca that referenced this pull request Aug 27, 2026
…put (stablyai#16325)

* test(terminal): pin park/reveal re-subscribe on a shared multiplexer

Investigating STA-5098. Parking a mirrored remote tab closes its stream
while a sibling tab keeps the multiplexer alive, so the reveal
re-subscribes on an instance that already retired a stream.

This came back green, which exonerates the multiplexer as the cause of
STA-5098 — the wedge is above it. Kept as a contract guard; the header
says explicitly that it is not coverage for that ticket.

* fix(terminal): stop stale multiplex stream handles from swallowing input

A stream handle whose record was dropped (park close, or a reconnect that
clears the stream table) kept reporting success: sendFrame gates only on
socket readiness, never on stream membership. The host drops those frames
for an unknown stream id, so a revealed cold-parked remote pane looked
connected while the PTY never saw a byte and never painted (STA-5098).

Reporting success also defeated the transport's own recovery — it re-sends
input over terminal.send when the stream refuses it, which never ran.

Guard the three public senders on stream membership, the check close() and
setOutputPaused already use, and drain input queued behind a viewport claim
on every stream install rather than only a still-pending claim.

Withdraws the parked-reveal re-subscribe test: its fake host answered
Subscribe with an immediate snapshot, so it could not fail.

* chore(terminal): tighten the stale-stream comments
enrick-dev pushed a commit to enrick-dev/orca that referenced this pull request Oct 15, 2026
…put (stablyai#16325)

* test(terminal): pin park/reveal re-subscribe on a shared multiplexer

Investigating STA-5098. Parking a mirrored remote tab closes its stream
while a sibling tab keeps the multiplexer alive, so the reveal
re-subscribes on an instance that already retired a stream.

This came back green, which exonerates the multiplexer as the cause of
STA-5098 — the wedge is above it. Kept as a contract guard; the header
says explicitly that it is not coverage for that ticket.

* fix(terminal): stop stale multiplex stream handles from swallowing input

A stream handle whose record was dropped (park close, or a reconnect that
clears the stream table) kept reporting success: sendFrame gates only on
socket readiness, never on stream membership. The host drops those frames
for an unknown stream id, so a revealed cold-parked remote pane looked
connected while the PTY never saw a byte and never painted (STA-5098).

Reporting success also defeated the transport's own recovery — it re-sends
input over terminal.send when the stream refuses it, which never ran.

Guard the three public senders on stream membership, the check close() and
setOutputPaused already use, and drain input queued behind a viewport claim
on every stream install rather than only a still-pending claim.

Withdraws the parked-reveal re-subscribe test: its fake host answered
Subscribe with an immediate snapshot, so it could not fail.

* chore(terminal): tighten the stale-stream comments
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