Skip to content

Phase 3.7: fix SSH/remote failures found by adversarial real-host testing - #26847

Merged
OrcaWin merged 13 commits into
mainfrom
OrcaWin/phase37
Oct 9, 2026
Merged

OrcaWin merged 13 commits into
mainfrom
OrcaWin/phase37

Conversation

@OrcaWin

@OrcaWin OrcaWin commented Oct 9, 2026 •

Copy link
Copy Markdown
Collaborator
Files Added Deleted Net
Test 89 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​6049 $\color{#cf222e}{\Huge{\mathbf{−}}}$​224 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​5825
Prod 147 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​2822 $\color{#cf222e}{\Huge{\mathbf{−}}}$​552 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​2270

ELI5

We attacked Orca's SSH and remote-host support on purpose with a bad network, unusual hosts, crashes mid-update and several desktops sharing one host, and wrote down everything that broke. This PR fixes the breakages that mattered most. After it:

  • keys typed into a remote terminal arrive exactly once, in order, even across a long silent outage;
  • a remote terminal keeps its size after reconnecting;
  • interrupted updates heal themselves;
  • one desktop can no longer strand another;
  • a deleted host can be removed;
  • noisy shell startup files, huge folders, slow disks and huge change lists no longer break a connection;
  • scrollback is never silently spliced.

Each fix landed and was reviewed separately on OrcaWin/phase37. Everything was then re-tested together on a packaged build against a real SSH host.

What Changed

Reliable remote input across an outage (P1-1, P1-2, P1-3): #26630, #26832

  • Before:
    • Orca takes about 20 s to notice that a link has died silently. Keys typed in that window were lost.
    • A half-sent batch put part of a command into the shell, glued to the next one.
    • Keys held during "Reconnecting…" were deleted after about 3 minutes, although the terminal then came back by itself.
    • Measured: a 65 s black hole lost 42 of 200 typed lines; a 200 s black hole lost 103 of 120.
  • After: 200/200 and 120/120 delivered, with no duplicates, no reordering and no split lines (re-tested on the integrated build).
  • Mechanism (standard go-back-N):
    • Every keystroke frame gets a sequence number. The host writes frames strictly in order, acknowledges the highest one it has applied, and skips duplicates.
    • The pane keeps everything not yet acknowledged and resends from the acknowledged point whenever:
      • the host asks;
      • the stream is replaced;
      • acknowledgements stop arriving.
    • Hosts and panes negotiate this (inputAck); an older peer gets the old unsequenced input.
    • A write whose delivery is unknown is reported as failed and never resent, so a command can't run twice.
    • fix(remote-terminal): keep input typed past the auto-recovery window for the same terminal, within a bounded grace (P1-3) #26832 restores holding input for the same terminal after the ~3-minute auto-recovery window ends. The pane still reattaches by itself, so the held input waits for it, but only for a 5-minute grace. After that, stale keys are dropped instead of running at some arbitrary later reconnect.

Fixes: Phase 3.7 findings P1-1, P1-2, P1-3 (no GitHub issue).

A reconnected pane keeps its size (P1-4): #26625

  • Before: after an outage longer than about 20 s, a visible pane came back at 120×40. The bottom third was blank, and the host program was told the screen had shrunk (stty size printed 40 120).
  • After: the pane comes back at its real size (159×61 in the test), and the host PTY stays 61 159.
  • Mechanism: the host's terminal width floor now passes size ownership to the same pane's new stream when the old stream is reaped. If the reap happened before the pane came back, ownership resumes when that pane registers again. Previously the host fell back to its spawn size.

Fixes: Phase 3.7 finding P1-4 (no GitHub issue).

Interrupted updates finish themselves; renderers refresh after a re-pair (P1-A, P1-C): #26635

  • Before:
    • Quitting the app during an update left the update's lock and journal on the host. After a reboot no server ran, and recovery needed --accept-changed-state --yes, which rolled profile state back.
    • After a rollback, the host's workspace stayed "unavailable" until you clicked elsewhere, and recovery then duplicated every terminal.
  • After:
    • The next connect runs crash recovery. It only takes a lock that is provably abandoned.
    • Recovery now finishes an update whose new server passes the update's own readiness check, instead of undoing it. It keeps the "never downgrade a newer host" guard.
    • A main-side re-pair now refreshes the renderers' pairing, so they don't keep retrying with a stale one.

Fixes: Phase 3.7 findings P1-A, P1-C (no GitHub issue).

One desktop's Stop no longer strands another; a deleted host can be removed (P1-D, P1-E): #26628, #26766

  • Before:
    • When desktop B stopped the shared managed server, desktop A stayed "connected" to nothing, and reconnecting didn't help.
    • A host that no longer exists could never be removed: Stop needs the host, and every other remove path said to use Stop.
  • After:
    • Desktop A's next connect sees "no activated server" and runs the normal update path, which redeploys and re-pairs it.
    • A new Forget action (managedServer.forget, permission host-admin) unlinks a server whose host is gone, and the SSH host can then be removed.
  • fix(ssh): don't strand a managed server stopped without a recorded Orca version #26766: Stop now records the stopped build's app version, and a host is refused only when its record names a newer Orca. A record with no version (left by an older build's Stop) is admitted again. Before this, every such host fell back to the relay forever. A forced fresh deploy runs anyway.

Fixes: Phase 3.7 findings P1-D, P1-E (no GitHub issue).

Shell startup output on stdout no longer breaks connect (HH-1): #26617

  • Before: any startup file that printed to stdout made connect fail in about a second with Remote home is not a valid path: <banner>. This covered fish config.fish, tcsh .tcshrc, zsh .zshenv, a bash .bashrc echo, and even a bare terminal-title escape.
  • After: all of those connect to a managed server.
  • Mechanism:
    • Every captured POSIX probe is wrapped in a nonce-delimited stdout fence, and only the text between the markers is used.
    • When startup output also corrupts the external SFTP server, the uploads stream over exec-channel stdin instead.

Refs #8324.

An oversized reply fails one request, not the whole connection (HH-2, HH-5): #26624

  • Before: expanding a folder with 80,000 entries showed it empty, and the host closed its whole control connection (1013 Outbound reply buffer overflow). The same happened for an "Unlimited" agent-session list.
  • After: the folder shows "This folder has 80,000 entries, too many to list over a remote connection." The connection and terminals keep working.
  • Mechanism: the WebSocket RPC dispatcher replaces an over-cap reply with a reply_too_large error for that request id. It uses the same admission check the encrypted channel uses, so the channel's size close no longer fires for RPC replies. files.readDir also checks remote callers against the request's content budget.

Fixes: Phase 3.7 findings HH-2, HH-5 (no GitHub issue).

A slow host can finish installing Orca's Node runtime (HH-3): #26631

  • Before: on a home directory writing about 1 MB/s, the runtime extraction hit a 30 s exec timeout. The relay then waited on a lock nobody would release, and connect failed after about 17 minutes, or ended in plain SSH.
  • After:
    • POSIX hosts get the same 300 s promote bound Windows already had.
    • If the client stops waiting, the host shell releases the store lock itself when the promote exits, so the relay fallback can continue.
    • Re-test: connected on the relay route in 490 s.

Fixes: Phase 3.7 finding HH-3 (no GitHub issue).

Stage All stages everything when the list was capped (HH-4): #26618

  • Before: with 20,000 changes, the panel lists 1,000. Stage All staged only those 1,000 and then hid the other 19,000.
  • After: the host ends with staged=20001 unstaged=0.
  • Mechanism: git.bulkStage takes a scope (all / tracked). With a scope, the host stages the whole worktree (git add --all -- ., or --update) instead of a listed subset. Unresolved conflicts are left out by name, and folder workspaces stay inside their folder. This works locally, over IPC, on the SSH relay and over runtime RPC, and in the mobile app.

Fixes: Phase 3.7 finding HH-4 (no GitHub issue).

No stale scrollback spliced above a recovered screen (P2-5, P2-7): #26647

  • Before: at 150 ms RTT, a 300,000-line burst left 5,000 old lines directly above the final screen, with about 260,000 lines silently missing in between.
  • After: the pane holds a continuous tail (298942–300000 in the re-test), and the stale block is cleared.
  • Mechanism:
    • The host's ACK-overflow recovery now sends up to 1,000 history rows once the link has drained.
    • A snapshot without history now replaces the pane's old history instead of keeping it underneath.

Fixes: Phase 3.7 findings P2-5, P2-7 (no GitHub issue).

Sync with main: #26828, plus a second clean merge

Main was merged into the branch before this re-test. The conflicts were the managed-server status line, the WSL login-shell imports and a moved banner test; #26828 lists how each was resolved.

A second, conflict-free merge of main (b3e5597878) picks up main's #26838, a fix to the ssh-orcad-auto-convert catalog-retry e2e test that failed on this PR's first CI run. The only Phase 3.7-adjacent change it brings is main's #26637: a live orcad PID now also has to answer a probe. It reports a different detail from #26628's "not activated" check, so the two don't interact.

Why

These are the P0/P1 failures found by adversarial real-host testing of Phase 3.5. Each one is fixed at its root cause rather than patched in the UI:

  • input sequencing, not a bigger buffer;
  • host-side size ownership, not a client refit;
  • recovery that completes, not a manual command;
  • a per-request reply cap, not a bigger socket buffer.

The alternatives each fix PR considered are in that PR.

Linked Issue

Refs #8324. The other findings come from the Phase 3.7 adversarial test reports and have no GitHub issues.

Visual Proof

N/A for most fixes: they are transport, host and recovery behavior. The UI-visible changes are:

  • the "too many entries" folder row (HH-2);
  • Stage All on a capped list (HH-4);
  • the pane size after an outage (P1-4).

Screenshots of those are in the individual PRs. The integrated re-test evidence is listed below.

Testing

Integrated real-host re-test

Setup:

  • A packaged arm64 build of the phase37 tip with the orcad template.
  • One Docker SSH host (debian bookworm arm64), with hostile users from the attack fixture and 80,000 files in one folder.
  • ORCA_BACKGROUND_LAUNCH=1, isolated profiles and HOME, tokens unset. Driven only through CDP and the bundled CLI.
  • Run sequentially.
Repro Result Evidence
P1-1/2: 65 s black hole while typing (200 tokens / 0.5 s) PASS 200/200, 0 lost / 0 dup / 0 reordered / 0 split
P1-3: 200 s black hole (120 tokens / 2 s) FAIL → fixed → PASS First run on the tip: 18/120 (the last #26630 round discarded held input at the window's end). After #26832: 120/120, 0/0/0/0
P1-4: pane size after a 45 s outage PASS 159×61 before and after; host stty 61 159; viewport at the bottom
HH-1: noisy rc on connect (bash echo + title, fish, tcsh, zsh with a 200-line rc) PASS All managed in 3.5–3.9 s; terminal works
HH-2: 80,000-entry folder PASS Error row with the count; no 1013; terminal still works
HH-3: slow fs (FUSE, 0.9 MB/s) PASS (#26631 scope) Runtime installed under the 300 s bound; connected on the relay in 490 s. The orcad readiness deadline on this storage is a known residual
P1-D: multi-desktop Stop (2 profiles) PASS B: refused with a live terminal, then unlinked. A: reconnect redeployed in 1 s; new terminal works
P1-E: remove a deleted host PASS Stop gave ECONNREFUSED; remove pointed to Forget; Forget gave forgotten (verdict unverifiable); host removed
P2-5: scrollback seam (150 ms RTT, 300k lines) PASS 298942–300000, contiguous, no stale block
Phase 3 smoke: fresh host goes managed PASS Managed in 6.1 s
Phase 3 smoke: Move PASS Relay host with a live relay terminal: offerMove, then moved in 3 s; new terminal on orcad
Phase 3 smoke: update PASS Deferred with a live terminal; forced update applied in 3 s; terminal kept running; stale "deferred" note cleared

Automated

Each fix PR adds regression tests that fail on its base. After the main sync:

  • pnpm tc, oxlint, check:code-quality:changed, audit:anti-slop, the max-lines / reliability / ts-nocheck / runtime-Electron ratchets, the RPC params catalog and the localization verifiers pass;
  • 99 affected test files (1,069 tests) pass, plus the mobile capped Stage All test.

Platforms: macOS desktop against Linux SSH hosts (arm64 glibc). Windows-host code paths are unchanged, except that #26631 shares the promote bound with POSIX.

  • I manually tested these changes locally
  • Automated tests added/updated, or explained why not below

Review

Every fix went through independent review rounds (an Opus workflow plus a separate reviewer, P0/P1 only) before landing on OrcaWin/phase37:

What the rounds caught and fixed, among others:

The integrated re-test then caught one regression introduced by the last #26630 round, fixed in #26832.

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

Accepted limits

  • Runtime restart during an outage: if the host's runtime restarts during an outage (an orcad restart or a forced update), keys typed before Orca detected the outage are dropped, not replayed. The new runtime has no record of what the old one wrote. Keys typed after detection are held and delivered.
  • At most once for delivery-unknown writes: a write whose delivery is unknown (the handoff to the PTY was lost) is reported as failed and never resent, so it is delivered at most once.
  • Long outages: more than 1 MiB of unconfirmed input in one outage replays nothing past the gap. Keys typed more than 5 minutes after a pane gives up reconnecting are refused, and older held keys are dropped then.
  • Downgrade residual (fix(ssh): don't strand a managed server stopped without a recorded Orca version #26766): a newer build activates the server, then a desktop older than fix(ssh): redeploy a managed server another desktop stopped; forget one whose host is gone (P1-D, P1-E) #26628 stops it without recording a version, then a desktop older than that newer build reconnects. That last desktop can start its own build over state the newer build may have migrated. This is the behavior before fix(ssh): redeploy a managed server another desktop stopped; forget one whose host is gone (P1-D, P1-E) #26628 and on main today, and once the version is missing nothing can tell this case apart from the common one.
  • Slow storage: on ~1 MB/s storage the orcad candidate can miss its startup readiness deadline, so the host serves over the relay route instead of managed.
  • Scrollback after an outage: after an outage longer than ~20 s, a visible pane keeps 1,000 rows of history plus the screen until the tab is revealed again (which restores 5,000).

Compatibility

  • New wire behavior is capability-negotiated (inputAck) or an optional field (git.bulkStage scope, the reply_too_large error), so mixed client and host versions keep working.
  • The one new RPC method, managedServer.forget, declares permission: 'host-admin'.
  • SSH: all changes are on the SSH/remote path and keep the live / unverifiable / exited verdict vocabulary.
  • Folder workspaces: Stage All keeps a nested folder inside its folder.
  • Mobile: the capped Stage All fix covers the mobile app too.
  • Cross-platform: no platform-specific code beyond the shared promote bound.

Checklist

  • This PR is small and focused (a phase branch of separately reviewed fixes)
  • 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)

OrcaWin and others added 12 commits October 8, 2026 20:46
…age (P1-4) (#26625)

* fix(remote-terminal): keep a reconnected pane's grid when its stale stream is reaped (P1-4)

The host's width floor only handed ownership to viewers that had claimed
since subscribing. A reconnecting desktop pane re-subscribes without
claiming, so when the heartbeat later reaped the dead socket that owned the
floor, the host reclaimed the PTY to its spawn size (120x40) and held the
pane there. The owner's same-client replacement stream now inherits the
floor, and a pane whose stream was reaped before it reconnected resumes
ownership on re-register unless the host or another viewer claimed since.

* fix(remote-terminal): host typing during an outage revokes the reaped owner's resume

Host input reaches the PTY without a renderer viewport claim once the host has
reclaimed its grid, so the orphaned-owner entitlement survived and a passive
reconnect resized the host pane back to the remote grid. The host write path
now clears that entitlement on driving input (query replies, focus reports and
launch writes excluded).

* test(pty): stub the runtime's host-input note in write-path handler tests

---------

Co-authored-by: m4air <m4air@Mac.localdomain>
…sing the host connection (#26624)

Expanding a folder with ~80k entries on a managed SSH host produced a
files.readDir reply over the 4 MiB encrypted-reply cap. The channel treated
that as a transport fault and closed the whole control socket (1013), and the
explorer rendered the failed folder as empty.

- The WebSocket RPC transport now answers an over-cap reply with a
  reply_too_large error for that request id, so the channel never sees it.
- files.readDir checks remote listings against the request's content budget
  and names the entry count.
- The explorer shows a subfolder's read error on its expanded row.

Finding: HH-2 (Phase 3.7 hostile-host).

Co-authored-by: m4air <m4air@Mac.localdomain>
…HH-3) (#26631)

* fix(ssh): let a slow host finish installing the pinned Node runtime

The POSIX promote (extract, hash, self-test and publish the ~110 MiB node
binary) ran under the 30 s default exec timeout. On slow home storage it
timed out, the remote work kept running, and the runtime store lock it held
stayed until the 20-minute stale rule, so relay rung A queued behind it past
its own deploy bound.

Give both platforms the 300 s promote bound and have the host release the
store lock itself when the promote exits, with owner-checked releases so a
late release never removes a lock someone else took. (HH-3)

* fix(ssh): let the host release the store lock after a slow under-lock re-hash

The under-lock re-check of a sibling-published runtime hashed the ~110 MiB
executable as its own command under the 30 s exec default and outside the
host-side lock release. On slow storage it timed out, the client correctly
kept the lock, and nothing on the host freed it until the 20-minute stale
rule, so later installers ran out their deploy bound.

Run the re-hash and the conditional promote as one host command wrapped in
the host release, and give every full-runtime hash, including the initial
probe, the slow-storage bound. (HH-3)

* test(ssh): count the combined under-lock re-probe and promote as one host command

---------

Co-authored-by: m4air <m4air@Mac.localdomain>
… (HH-1) (#26617)

* fix(ssh): fence captured exec stdout so shell rc output cannot break connect

sshd runs the user's login shell before Orca's /bin/sh wrapper, so any
startup-file stdout (fish/tcsh/zsh/bash echo, or a bare terminal-title escape)
was glued onto every captured answer. The home probe read "banner\n/home/u"
and connect failed with "Remote home is not a valid path".

execCommand now wraps wrapped POSIX commands in a per-call nonce fence and
returns only the payload between the markers; the SSH folder browser gets the
same fence. The fence is the existing WSL captured-login-shell fence, moved to
src/shared/posix-stdout-fence.ts so both transports share it.

Finding: HH-1 (phase 3.7 hostile-host). Refs #8324.

* fix(ssh): fence the login-shell Node probe and fall back from a noisy SFTP handshake

The login-shell 'command -v node' probe ran unwrapped, so output from sshd's
shell and the nested login profile became the Node path. It now prints a
one-line fence every login shell parses, inside that shell.

An external sftp-server is started through the user's shell, so startup output
corrupts the SFTP handshake. ssh2 reports that parse failure only before the
session is ready, so SshConnection.sftp tags it, and relay install transfers
treat it as SFTP unavailable and stream over exec stdin.

Finding: HH-1 follow-up (phase 3.7 hostile-host). Refs #8324.

* fix(ssh): keep the stdout fence marker out of the command text

The runtime-store inventory lists processes with ps, which prints its own
script, so a literal marker in the script was read as the fence and the
store GC skipped every pass. printf now joins the marker from two halves.

---------

Co-authored-by: m4air <m4air@Mac.localdomain>
…ers after a main-side re-pair (P1-A, P1-C) (#26635)

* fix(orcad): finish an update or rollback its client abandoned, and tell renderers about main-side re-pairs

P1-A: a wake that finds the activation fence now runs crash recovery, which takes
only a stale, previous-boot or exited-own fence. Recovery of an activation
interrupted after the candidate launched resumes it: the candidate commits if it
passes the readiness gate, else the activation is undone as before.

P1-C: invalidating a still-linked environment's transport restarts its status
owner, and a renderer that sees status under a newer pairing revision re-reads
the catalog before applying it.

* fix(orcad): a resumed update keeps the app version that started it

Recovery that commits an abandoned candidate stamped no activeAppVersion, so an
older desktop read the host as pre-tracking and downgraded it over state the
newer build had migrated. The activate journal now records the initiating app
version; recovery stamps it, and a journal too old to carry it is undone instead
of committed.

* test(runtime-env): answer nothing for the status restart a re-pair now triggers

* fix(orcad): recovery leaves running an update server a too-old journal cannot commit

A journal without candidateAppVersion was sent to undo, which stopped a
healthy candidate that had already migrated state and then refused to
restart the incumbent, leaving the host serving nothing. Refuse instead,
keeping the fence and the candidate, until the operator accepts Restore.

---------

Co-authored-by: m4air <m4air@Mac.localdomain>
…e new screen (P2-5, P2-7) (#26647)

* fix(terminal): a remote recovery never splices stale history above the new screen

A pushed remote snapshot (overflow recovery, seq-gap resync, first attach) is
sent because the client's stream broke, so the pane's own history ends before
that image. Since #26319 a screen-only image kept that history, splicing a
stale block directly above the final screen with a silent gap of up to ~270k
lines (P2-5), and a first attach to an older host left silent holes (P2-7).

- Host: the ACK-overflow recovery serializes up to 1000 history rows.
- Client: the seq-gap resync asks for the same 1000 rows.
- Client: a pushed snapshot always replaces the pane's scrollback; the
  keep-local-scrollback path is removed. A screen-only image from an older host
  now shows only its screen, which is lossy but never misleading.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix(terminal): a pushed image replaces the normal buffer over a live alt screen too

Over a pane already on the alternate screen, an image that also ended on alt
repainted only its alt frame and kept the pane's normal buffer, inferring the two
had frozen together. Inside the gap the TUI can exit, the shell print and a second
TUI start, so once that TUI exited the pane showed its stale history where the host
had the missed output. A pushed image now always rebuilds the normal buffer and
history from the host before entering alt itself.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix(terminal): bound recovery history by link slack and keep pre-TUI scrollback under screen-only images

A recovery that carries history now goes out only when the stream's ACK
window has drained; under a sustained flood recoveries stay screen-only and
the owed history follows once the client catches up, so snapshot bytes no
longer outgrow the client's intake.

A pushed image without history (scrollbackRows absent or 0) over a live alt
screen repaints only the alt frame again, so a reconnect to an older host
no longer wipes the pane's pre-TUI scrollback.

---------

Co-authored-by: m4air <m4air@Mac.localdomain>
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…it the 1,000 cap (HH-4) (#26618)

* fix(source-control): Stage All stages every change when the listing hit the 1,000 cap

The status listing stops at 1,000 rows, but Stage All built its path list
from those rows, so it staged 1,000 of 20,000 changes and the next commit
was silently partial. When the listing is capped, Stage All (and the
Changes header Stage all) now asks the host to stage the whole worktree
via an optional scope on git.bulkStage; unresolved conflicts stay
unstaged. Older hosts ignore the field and stage the listed paths.

Fixes HH-4.

* fix(source-control): whole-worktree Stage All needs a host receipt, skips shared links, and reaches mobile

- Scoped stage sends no paths and requires a stagedScope receipt, so an older
  runtime or relay stages nothing and the client reports it instead of a
  silent partial stage.
- Scoped 'all' skips Orca's shared symlinks (configured and really a link),
  matching the status listing.
- The capped-listing banner offers Stage All, reachable when the visible
  prefix is entirely staged.
- Mobile reads didHitLimit and uses the same scoped stage and receipt.

* test(source-control): type the Stage All test stubs so lint and the mobile test typecheck pass

* fix(source-control): match Shared Paths saved as ./name or name/ against git's spelling

The Shared Paths setting keeps entries as typed, but git prints worktree paths
without a leading ./ or trailing /. Stage All's conflict path compared the raw
spelling to git's untracked list and staged the shared node_modules link, and
an exclude pathspec ending in / made git refuse the add after tracked changes
were already staged. Status listing had the same mismatch. getSafeRelativePath
now returns git's spelling, so every caller gets it, including saved settings.

---------

Co-authored-by: m4air <m4air@Mac.localdomain>
…ne whose host is gone (P1-D, P1-E) (#26628)

* fix(ssh): redeploy a server another desktop stopped; let a dead managed host be forgotten

P1-D: a connect whose linked server the host reports as not activated (another desktop
stopped it) now goes on to the update check, which redeploys it in place.
P1-E: add Forget (managedServer.forget, orca environment forget, Settings row) that runs
Stop's local unlink without contacting the host; the verdict stays unverifiable.

* test(ssh): type the forget test's hoisted store without an assertion

* fix(ssh): never downgrade a stopped host; redeploy it mid-session and retry after a failure

A stop now keeps the stopped build's Orca version, so an older desktop's
reconnect skips as host-newer instead of starting an older server on a
store a newer build migrated. A stop recorded without that version is not
guessed at. A stopped host retries the redeploy on each connect instead of
the once-per-version failure suppression, and a runtime call that finds
the server stopped mid-session redeploys it before the call.

* fix(ssh): rebuild the managed forward after redeploying a stopped host

A redeploy can bind a different port than the reused forward targets.

* fix(ssh): keep stop journals readable by older desktops and recheck version policy under the fence

- The stop journal's recordAfter is the core deactivated record older peers compute; the
  committed record keeps activeAppVersion (which released planners already honour) and
  rolledBackFrom, rebuilt from recordBefore on recovery.
- Auto-update re-applies its version policy to the record read under the host fence, and a
  census counted against another incumbent is treated as unverifiable.

* fix(ssh): file a dispatched stop where older desktops cannot commit it, so their recovery never drops the version guard

* fix(ssh): refuse a fresh deploy of an older orcad onto a host a newer desktop stopped

The unlinked deploy path never consulted the stopped record's activeAppVersion, so a desktop
without a link to the host could activate its older build over state a newer build migrated.
Both the auto-update planner and the fresh deploy now apply one downgrade rule under the fence.

---------

Co-authored-by: m4air <m4air@Mac.localdomain>
…rced deploy run over a newer one (#26766)

A stopped activation record without activeAppVersion is what every Stop on
main writes, so refusing it stranded those hosts behind an unforceable
policy refusal. Only a provably newer host is now refused; a forced fresh
deploy runs this version anyway, and the refusal names both versions.

Co-authored-by: m4air <m4air@m4airs-MacBook-Air.local>
… silent outage (P1-1, P1-2, P1-3) (#26630)

* fix(remote-terminal): no keystroke lost, duplicated or split across a silent outage

Remote pane input was fire-and-forget: keys handed to a socket that had
silently died were gone, keys still in the 8 ms debounce at detection were
cleared, and held keys were discarded when the auto-recovery window ran out.

- New negotiated `inputAck` capability: the client names an input session
  and sequences Input frames (existing frame header `seq`); the host records
  the last applied sequence per (PTY, session) across connections, writes
  each sequence once and acks it with new opcode InputAck (18), sent only
  to clients that advertised the capability.
- The pane keeps sent-but-unacked input and replays it to the same
  terminal on reattach; the host dedupes. Old hosts keep current behavior.
- Debounced and claim-queued input at detection is held, not cleared.
- Held input survives the 'disconnected' phase; the hold coalesces keys so
  minutes of typing do not hit the per-entry cap.
- Banner copy no longer claims a one-minute retry window.

Fixes P1-1, P1-2, P1-3 (phase 3.7 network findings).

* fix(remote-terminal): never replay into a restarted runtime; keep Ctrl+C and validating paste in order

- Host publishes inputLedgerId with inputAck; the client drops unacknowledged input sent to a
  different ledger (runtime restart with the same PTY) instead of running it twice.
- A replayed duplicate is acked only after the original write settles.
- Accepted input (Ctrl+C, Escape, paste) rides the sequenced stream and resolves on the host ack.
- Keys typed while held queue behind a paste still in size validation.

* fix(remote-terminal): parse subscribed capabilities into a typed shape instead of Reflect.get

* fix(remote-terminal): ack only applied input; withdraw writes their caller gave up on

- Host ledger records each write's outcome: a refused write is rolled back and
  never acked, so the client's replay writes it; nothing queued behind it runs or
  is acked. A write that threw is acked as delivery-unknown, so the client
  reports it failed and never replays it.
- Paste and file-drop timeouts abort their accepted write: a copy still held for
  an outage is withdrawn, and a journaled copy is left out of the replay.
- A batch of sequenced input keeps one endpoint and ledger, so a send that
  retires the stream mid-batch no longer discards earlier unacked input.

* fix(remote-terminal): host writes sequenced input strictly in order and asks the live stream to resend after a refusal

- The ledger admits only the next sequence; a refusal or gap answers with an InputAck resend request instead of WriteUnavailable, so the pane keeps its journal and resends on the same stream (no remount, no carrier bookkeeping).
- Sequenced input uses the provider's settled write; an unverifiable handoff is delivery-unknown, not refused.
- Replays fill given-up slots with empty input so the host can continue; the client remounts only after every resend backoff is refused.
- Typing batched ahead of a paste goes out as its own sequences, so cancelling the paste keeps it.

* refactor(remote-terminal): deliver sequenced input with standard go-back-N

Replace the hand-built "block on refusal" bookkeeping with go-back-N:

- Host: per (PTY, input session) the ledger keeps only appliedSeq, the
  highest contiguous confirmed write. Frames run one at a time in arrival
  order: a duplicate is re-acked, the next sequence is written and acked
  on confirmed success, and a refusal or gap is answered with a resend
  request carrying appliedSeq. No blocked state; every stream is equal.
  A settlement that proves no byte left is a refusal; one that may have
  written advances as delivery-unknown and is never retried.
- Client: one journal keeps unacked input in order and resends from
  appliedSeq + 1 on a resend request (with backoff), on a new stream, or
  when acks stall. Refusals never remount the pane. Withdrawn pastes,
  stale query replies and over-budget bytes resend as empty slots.
- The recovery hold pauses (keeps the rest) when a release is cut short
  by another outage, and keeps delivering typing after an accepted write
  a sequenced stream withdrew or could not confirm.
- Add a seeded model-based test that drives the real pane transport,
  multiplexer and journal against the real host dispatcher and ledger
  through 2,000 random fault schedules.

* test(remote-terminal): cover the refusal remount on a stream without sequenced input

A sequenced stream now resends refused input on the same stream instead of remounting, so the end-to-end remount check negotiates no inputAck, as an older peer does.

* fix(remote-terminal): escalate persistent input refusals, drop input once recovery gives up, and never report a lost unknown ack as applied

- The pane falls back to its remount path when the host keeps refusing input past the resend backoff ladder.
- Held and journaled input is discarded when auto-recovery gives up, and input typed under the disconnected banner is not held.
- The host ledger answers a duplicate for its own sequence and remembers delivery-unknown outcomes; the client settles a write as applied only from its own ack.
- The input model now requires a write reported applied to have reached the PTY whole, and covers a refusal that never clears.

Co-Authored-By: Claude <noreply@anthropic.com>

---------

Co-authored-by: m4air <m4air@Mac.localdomain>
Co-authored-by: m4air <m4air@m4airs-MacBook-Air.local>
Co-authored-by: Claude <noreply@anthropic.com>
# Conflicts:
#	src/main/ipc/runtime-environment-managed-tunnel.ts
#	src/renderer/src/components/right-sidebar/source-control-too-many-changes-banner.test.tsx
#	src/shared/wsl-login-shell-command.ts
…for the same terminal, within a bounded grace (P1-3) (#26832)

The last round of #26630 discarded held and journaled input as soon as auto-recovery gave up,
and refused input under the disconnected banner. On a real host a 200 s outage lost 102 of
120 keystrokes, although the pane reattached the same terminal on its own seconds later.

Input now stays held for the same handle and incarnation while the pane is disconnected, for
up to 5 minutes after it gave up. Past that grace it is discarded, and typing into the dead
pane is refused, so stale keys never run at an arbitrary later reconnect. The grace is a
timestamp checked when input moves, so a latched pane stays free of timers.

Co-authored-by: m4air <m4air@Mac.localdomain>
Co-authored-by: Claude <noreply@anthropic.com>
@OrcaWin
OrcaWin requested a review from brennanb2025 as a code owner October 9, 2026 10:51
@coderabbitai

coderabbitai Bot commented Oct 9, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

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: b34c99d7-598f-4f33-9dc2-6471e67b693d

📥 Commits

Reviewing files that changed from the base of the PR and between fc4707b and b3e5597.


⛔ Files ignored due to path filters (1)
  • src/shared/rpc-contract/rpc-params-catalog.generated.ts is excluded by !**/*.generated.*

📒 Files selected for processing (1)
  • src/main/ssh/orcad-managed-serving.ts

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



📝 Walkthrough

Walkthrough

The changes add scoped Git staging for complete and capped status listings. Remote terminal input gains sequence acknowledgements, replay, cancellation, and recovery handling. Snapshot recovery distinguishes screen-only images from images carrying history. Managed-server changes add local forgetting and update activation, wake, and decommission recovery. SSH command output is fenced against startup noise, and runtime RPC handling adds response-size limits and catalog-aware status updates.


Priority: ⬆️ High

Merge Risk: 🔵 Low · up to b3e55

In narrow cases, terminal ownership may not resume after a refused write, or staging may finish after the client reports a timeout. These risks are bounded but remain open; R1’s stale resize ownership concern is resolved.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage Warning Docstring coverage is 34.15% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 41 functions across 51 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 summarizes the main change: fixing SSH and remote-host failures identified through adversarial real-host testing.
Description check Passed The description is comprehensive and follows the template. It explains the user impact, mechanisms, rationale, linked issue, visual proof, testing, review history, limitations, compatibility, and chec…


  • Fix all pre-merge checks with AI
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Commit to this branch
  • Create a new PR


  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

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

🧹 Nitpick comments (1)
src/main/ssh/remote-node-runtime-store-lock.ts (1)

44-51: 🩺 Stability & Availability | 🔵 Trivial | 💤 Low value

The owner check and the rm -rf do not run as one atomic step.

The command reads .holder, then deletes the lock by path. A second holder can steal the lock and write its own token after the check passes. In that window, this release removes the new holder's lock. The window is small. Stealing also needs the 20-minute stale rule, so the trigger is unlikely. To close the window, rename the lock directory to a unique name before you check the token, then delete the renamed copy.

Source: Learnings


ℹ️ Review info
⚙️ Run configuration
  • Configuration used: Repository UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 1bc1c238-a2c6-41c0-ab0d-ec043231059d
📥 Commits

Reviewing files that changed from the base of the PR and between 049bf80 and fc4707b.

⛔ Files ignored due to path filters (1)
  • src/shared/rpc-contract/rpc-params-catalog.generated.ts is excluded by !**/*.generated.*
📒 Files selected for processing (235)
  • config/reliability-gates.jsonc
  • mobile/src/source-control/MobileSourceControlContent.tsx
  • mobile/src/source-control/mobile-source-control-capped-stage-all.test.tsx
  • mobile/src/source-control/use-mobile-source-control-runners.ts
  • mobile/src/source-control/use-mobile-stage-all-runner.ts
  • src/cli/handler-group-manifest.ts
  • src/cli/handlers/managed-server.test.ts
  • src/cli/handlers/managed-server.ts
  • src/cli/specs/managed-server.ts
  • src/main/codex/codex-trust-grant-host.test.ts
  • src/main/git/runner-wsl-direct-read.test.ts
  • src/main/git/runner-wsl-login-shell-capture.test.ts
  • src/main/git/runner-wsl-network-ssh-policy.test.ts
  • src/main/git/source-control/stage-worktree-changes.test.ts
  • src/main/git/source-control/staging.ts
  • src/main/git/source-control/wsl-tracked-pathspec-banner.test.ts
  • src/main/git/status-shared-symlinks.test.ts
  • src/main/git/status.ts
  • src/main/git/worktree-symlink-detection.test.ts
  • src/main/git/worktree-symlink-detection.ts
  • src/main/ipc/filesystem-git-status-staging.test.ts
  • src/main/ipc/filesystem-test-harness.ts
  • src/main/ipc/filesystem/filesystem-git-index-handlers.ts
  • src/main/ipc/managed-orcad-actions.ts
  • src/main/ipc/orcad-runtime-maintenance-handlers.ts
  • src/main/ipc/pty-runtime-ssh-binding-persistence.test.ts
  • src/main/ipc/pty-serializer-settlement-mapping.test.ts
  • src/main/ipc/pty/ipc/write-input-user-input.test.ts
  • src/main/ipc/pty/ipc/write-input.ts
  • src/main/ipc/runtime-environment-handler-channels.ts
  • src/main/ipc/runtime-environment-managed-tunnel.test.ts
  • src/main/ipc/runtime-environment-managed-tunnel.ts
  • src/main/ipc/runtime-environments-pairing.test.ts
  • src/main/ipc/runtime-environments-subscription-lifecycle.test.ts
  • src/main/ipc/runtime-environments-subscription-teardown.test.ts
  • src/main/ipc/runtime-environments.ts
  • src/main/ipc/ssh-browse.test.ts
  • src/main/ipc/ssh-browse.ts
  • src/main/ipc/ssh-target-crud-handlers.ts
  • src/main/providers/git-provider-contract.ts
  • src/main/providers/ssh-git-provider-staging.test.ts
  • src/main/providers/ssh-git-working-tree-provider.ts
  • src/main/runtime/managed-server-actions-registry.ts
  • src/main/runtime/orca-runtime-mark-pty-liveness-unverifiable.ts
  • src/main/runtime/remote-desktop-driver.test.ts
  • src/main/runtime/remote-desktop-terminal-floor-reconnect.test.ts
  • src/main/runtime/remote-desktop-terminal-floor.ts
  • src/main/runtime/rpc/methods/files-read-dir-remote-budget.test.ts
  • src/main/runtime/rpc/methods/files-remote-content-budget.ts
  • src/main/runtime/rpc/methods/files.ts
  • src/main/runtime/rpc/methods/git-params.ts
  • src/main/runtime/rpc/methods/git.ts
  • src/main/runtime/rpc/methods/managed-server.test.ts
  • src/main/runtime/rpc/methods/managed-server.ts
  • src/main/runtime/rpc/methods/terminal/stream-schemas.ts
  • src/main/runtime/rpc/methods/terminal/terminal-input-delivery.ts
  • src/main/runtime/rpc/methods/terminal/terminal-input-sequence-ledger.test.ts
  • src/main/runtime/rpc/methods/terminal/terminal-input-sequence-ledger.ts
  • src/main/runtime/rpc/methods/terminal/terminal-multiplex-cleanup.ts
  • src/main/runtime/rpc/methods/terminal/terminal-multiplex-connection.ts
  • src/main/runtime/rpc/methods/terminal/terminal-multiplex-flow-control.ts
  • src/main/runtime/rpc/methods/terminal/terminal-multiplex-frame-delivery.ts
  • src/main/runtime/rpc/methods/terminal/terminal-multiplex-initial-snapshot.ts
  • src/main/runtime/rpc/methods/terminal/terminal-multiplex-input-frame.ts
  • src/main/runtime/rpc/methods/terminal/terminal-multiplex-slot-frames.ts
  • src/main/runtime/rpc/methods/terminal/terminal-multiplex-stream-initialization.ts
  • src/main/runtime/rpc/methods/terminal/terminal-stream-types.ts
  • src/main/runtime/rpc/terminal-multiplex-ack-overflow-recovery-history.test.ts
  • src/main/runtime/rpc/terminal-multiplex-ack-overflow-recovery.test.ts
  • src/main/runtime/rpc/terminal-multiplex-input-sequence-replay.test.ts
  • src/main/runtime/runtime-git-stage-worktree-scope.test.ts
  • src/main/runtime/runtime-git-staging-commands.ts
  • src/main/runtime/runtime-rpc-mobile-method-allowlist.test.ts
  • src/main/runtime/runtime-rpc-websocket-reply-size-limit.test.ts
  • src/main/runtime/runtime-rpc/runtime-rpc-reply-size-limit.ts
  • src/main/runtime/runtime-rpc/runtime-rpc-websocket-dispatch.ts
  • src/main/runtime/terminal-run-facts.ts
  • src/main/ssh/managed-server-fence-recheck.ts
  • src/main/ssh/managed-server-serving-gate.ts
  • src/main/ssh/orcad-activation-crash-recovery.test.ts
  • src/main/ssh/orcad-activation-record.test.ts
  • src/main/ssh/orcad-activation-record.ts
  • src/main/ssh/orcad-activation-recovery.ts
  • src/main/ssh/orcad-activation-transaction-schema.ts
  • src/main/ssh/orcad-activation-transaction-transitions.ts
  • src/main/ssh/orcad-activation-transaction.ts
  • src/main/ssh/orcad-decommission-released-peer-recovery.test.ts
  • src/main/ssh/orcad-decommission-transaction.test.ts
  • src/main/ssh/orcad-decommission-transaction.ts
  • src/main/ssh/orcad-host-version-admission.ts
  • src/main/ssh/orcad-installed-activation.ts
  • src/main/ssh/orcad-interrupted-candidate-resume.ts
  • src/main/ssh/orcad-managed-auto-update.test.ts
  • src/main/ssh/orcad-managed-auto-update.ts
  • src/main/ssh/orcad-managed-serving.ts
  • src/main/ssh/orcad-managed-tunnel-manager.ts
  • src/main/ssh/orcad-managed-tunnel.test.ts
  • src/main/ssh/orcad-managed-tunnel.ts
  • src/main/ssh/orcad-managed-update-on-restore.test.ts
  • src/main/ssh/orcad-managed-update-on-restore.ts
  • src/main/ssh/orcad-managed-wake.test.ts
  • src/main/ssh/orcad-managed-wake.ts
  • src/main/ssh/orcad-remote-deploy.test.ts
  • src/main/ssh/orcad-remote-deploy.ts
  • src/main/ssh/orcad-remote-node-runtime-promotion-lock.test.ts
  • src/main/ssh/orcad-remote-node-runtime-slow-hash.test.ts
  • src/main/ssh/orcad-remote-node-runtime-windows.test.ts
  • src/main/ssh/orcad-remote-node-runtime-windows.ts
  • src/main/ssh/orcad-remote-node-runtime.ts
  • src/main/ssh/orcad-remote-stop.ts
  • src/main/ssh/orcad-runtime-decommission.ts
  • src/main/ssh/orcad-runtime-deployment.test.ts
  • src/main/ssh/orcad-runtime-deployment.ts
  • src/main/ssh/orcad-runtime-forget.test.ts
  • src/main/ssh/orcad-runtime-lifecycle.ts
  • src/main/ssh/orcad-runtime-maintenance.ts
  • src/main/ssh/orcad-state-mutation-timeout.test.ts
  • src/main/ssh/orcad-unreachable-setup-release.ts
  • src/main/ssh/remote-node-runtime-store-lock-host-release.test.ts
  • src/main/ssh/remote-node-runtime-store-lock.ts
  • src/main/ssh/sftp-handshake-corruption.ts
  • src/main/ssh/ssh-connection.ts
  • src/main/ssh/ssh-exec-rc-stdout-noise.test.ts
  • src/main/ssh/ssh-host-server-on-connect-stopped-elsewhere.test.ts
  • src/main/ssh/ssh-host-server-on-connect.ts
  • src/main/ssh/ssh-login-shell-command.ts
  • src/main/ssh/ssh-relay-deploy-helpers.test.ts
  • src/main/ssh/ssh-relay-exec-command.ts
  • src/main/ssh/ssh-relay-install-noisy-sftp-wire.test.ts
  • src/main/ssh/ssh-relay-install-transfers-exec-fallback.test.ts
  • src/main/ssh/ssh-relay-install-transfers.ts
  • src/main/ssh/ssh-relay-opencode-runtime.test.ts
  • src/main/ssh/ssh-remote-node-resolution.test.ts
  • src/main/ssh/ssh-remote-node-resolution.ts
  • src/main/wsl/wsl-guest-environment.test.ts
  • src/main/wsl/wsl-runner.test.ts
  • src/preload/api/git-bridge.ts
  • src/preload/api/git-operation-api.ts
  • src/preload/api/managed-orcad-api.ts
  • src/relay/git-handler-stage-worktree-scope.test.ts
  • src/relay/git-handler-worktree-change-operations.ts
  • src/renderer/src/components/right-sidebar/FileExplorerRow.load-error.test.tsx
  • src/renderer/src/components/right-sidebar/FileExplorerRow.tsx
  • src/renderer/src/components/right-sidebar/FileExplorerVirtualRows.tsx
  • src/renderer/src/components/right-sidebar/source-control-too-many-changes-banner.test.tsx
  • src/renderer/src/components/right-sidebar/source-control/commit/use-bulk-actions-capped-stage.test.tsx
  • src/renderer/src/components/right-sidebar/source-control/commit/use-bulk-actions.ts
  • src/renderer/src/components/right-sidebar/source-control/listing/content-status.tsx
  • src/renderer/src/components/right-sidebar/source-control/listing/section-action-buttons.test.tsx
  • src/renderer/src/components/right-sidebar/source-control/listing/too-many-changes-banner.tsx
  • src/renderer/src/components/right-sidebar/source-control/listing/uncommitted-sections.tsx
  • src/renderer/src/components/right-sidebar/source-control/listing/use-file-listing.ts
  • src/renderer/src/components/right-sidebar/source-control/panel/panel-content.tsx
  • src/renderer/src/components/right-sidebar/source-control/panel/use-panel-foundation.ts
  • src/renderer/src/components/settings/ManagedServerRow.test.tsx
  • src/renderer/src/components/settings/ManagedServerRow.tsx
  • src/renderer/src/components/settings/SshHostDeltaMoveDialog.test.tsx
  • src/renderer/src/components/terminal-pane/pty-connection/deferred-cold-restore-and-snapshot.ts
  • src/renderer/src/components/terminal-pane/pty-connection/replay-data-drain.ts
  • src/renderer/src/components/terminal-pane/pty-output-processor.ts
  • src/renderer/src/components/terminal-pane/pty-preconnect-input-buffer.ts
  • src/renderer/src/components/terminal-pane/pty-transport-types.ts
  • src/renderer/src/components/terminal-pane/remote-reattach-input-buffer.ts
  • src/renderer/src/components/terminal-pane/remote-runtime-disconnected-input-grace.ts
  • src/renderer/src/components/terminal-pane/remote-runtime-input-journal.test.ts
  • src/renderer/src/components/terminal-pane/remote-runtime-input-journal.ts
  • src/renderer/src/components/terminal-pane/remote-runtime-pty-snapshot-source-grid.test.ts
  • src/renderer/src/components/terminal-pane/remote-runtime-pty-transport-attach-subscription.test.ts
  • src/renderer/src/components/terminal-pane/remote-runtime-pty-transport-host-session-launch.test.ts
  • src/renderer/src/components/terminal-pane/remote-runtime-pty-transport-input-fallback.test.ts
  • src/renderer/src/components/terminal-pane/remote-runtime-pty-transport-input-replay.test.ts
  • src/renderer/src/components/terminal-pane/remote-runtime-pty-transport-snapshot-replay.test.ts
  • src/renderer/src/components/terminal-pane/remote-runtime-pty-transport-stream-fixtures.ts
  • src/renderer/src/components/terminal-pane/remote-runtime-pty-transport-stream-reconnect.test.ts
  • src/renderer/src/components/terminal-pane/remote-runtime-pty-transport.ts
  • src/renderer/src/components/terminal-pane/remote-runtime-recovery-input-hold.test.ts
  • src/renderer/src/components/terminal-pane/remote-runtime-recovery-input-hold.ts
  • src/renderer/src/components/terminal-pane/remote-snapshot-alt-screen-host-chain.test.ts
  • src/renderer/src/components/terminal-pane/remote-snapshot-history-seam.test.ts
  • src/renderer/src/components/terminal-pane/remote-snapshot-scrollback-preservation.test.ts
  • src/renderer/src/components/terminal-pane/terminal-drop-handler.test.ts
  • src/renderer/src/components/terminal-pane/terminal-drop-internal-handler.test.ts
  • src/renderer/src/components/terminal-pane/terminal-drop-path-writer.test.ts
  • src/renderer/src/components/terminal-pane/terminal-drop-path-writer.ts
  • src/renderer/src/components/terminal-pane/terminal-pane-menu-paste.ts
  • src/renderer/src/components/terminal-pane/terminal-pane-paste-execution.ts
  • src/renderer/src/components/terminal-pane/terminal-paste-coordinator.test.ts
  • src/renderer/src/components/terminal-pane/terminal-paste-executor.ts
  • src/renderer/src/components/terminal-pane/terminal-paste-operation-timeout.ts
  • src/renderer/src/components/terminal-pane/terminal-programmatic-text-paste.ts
  • src/renderer/src/components/terminal-pane/terminal-pty-paste-writer.test.ts
  • src/renderer/src/components/terminal-pane/terminal-pty-paste-writer.ts
  • src/renderer/src/components/terminal-pane/terminal-snapshot-replay-paint.ts
  • src/renderer/src/components/terminal-pane/terminal-startup-command-paste.ts
  • src/renderer/src/components/terminal-pane/use-terminal-pane-mobile-actions.ts
  • src/renderer/src/i18n/en-runtime-required.json
  • src/renderer/src/i18n/locales/en.json
  • src/renderer/src/i18n/locales/es.json
  • src/renderer/src/i18n/locales/fr.json
  • src/renderer/src/i18n/locales/ja.json
  • src/renderer/src/i18n/locales/ko.json
  • src/renderer/src/i18n/locales/zh.json
  • src/renderer/src/runtime/remote-runtime-terminal-binary-controller.ts
  • src/renderer/src/runtime/remote-runtime-terminal-binary-snapshots.ts
  • src/renderer/src/runtime/remote-runtime-terminal-flow-controller.ts
  • src/renderer/src/runtime/remote-runtime-terminal-frame-drop-resync.test.ts
  • src/renderer/src/runtime/remote-runtime-terminal-multiplexer-base.ts
  • src/renderer/src/runtime/remote-runtime-terminal-multiplexer-implementation.ts
  • src/renderer/src/runtime/remote-runtime-terminal-multiplexer-types.ts
  • src/renderer/src/runtime/remote-runtime-terminal-response-controller.ts
  • src/renderer/src/runtime/remote-runtime-terminal-snapshot-controller.ts
  • src/renderer/src/runtime/remote-runtime-terminal-snapshot-state.ts
  • src/renderer/src/runtime/runtime-environment-pairing-refresh.ts
  • src/renderer/src/runtime/runtime-git-client.ts
  • src/renderer/src/runtime/runtime-git-working-tree-client.ts
  • src/renderer/src/runtime/runtime-git-worktree-scope-client.test.ts
  • src/renderer/src/runtime/runtime-terminal-stream.test.ts
  • src/renderer/src/store/slices/runtime-status-snapshot.test.ts
  • src/renderer/src/store/slices/runtime-status-snapshot.ts
  • src/renderer/src/store/slices/runtime-status.ts
  • src/renderer/src/web/preload-api/web-git-api.ts
  • src/renderer/src/web/preload-api/web-git-stage-scope.test.ts
  • src/shared/git-stage-worktree-scope.ts
  • src/shared/orcad-managed-runtime.ts
  • src/shared/posix-stdout-fence.ts
  • src/shared/rpc-contract/git-params.ts
  • src/shared/runtime-environment-store.ts
  • src/shared/terminal-mode-reset-profiles.ts
  • src/shared/terminal-multiplex-flow-control.ts
  • src/shared/terminal-stream-protocol.ts
  • src/shared/wsl-login-shell-command.ts
  • tests/e2e/cross-version-wire/host-terminal-runtime-stub.ts
  • tests/e2e/paired-runtime-rejected-input-remount.unit.test.ts
  • tests/e2e/remote-terminal-input-delivery-model.unit.test.ts
  • tests/e2e/remote-terminal-input-model-link.ts
💤 Files with no reviewable changes (4)
  • src/main/ssh/orcad-remote-node-runtime-windows.ts
  • src/renderer/src/components/terminal-pane/remote-snapshot-scrollback-preservation.test.ts
  • src/renderer/src/runtime/remote-runtime-terminal-snapshot-state.ts
  • src/renderer/src/i18n/en-runtime-required.json

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

lastInputAtByPty.set(args.id, performance.now())
interactiveOutputCharsByPty.set(args.id, 0)
runtime?.terminalRunFacts?.recordInput(args.id, args.inputKind, args.data)
runtime?.noteRemoteDesktopHostInput(args.id, args.inputKind, args.data)

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

Clear the pending owner resume only after an accepted host write.

If a provider refuses a write or the size check rejects input, this call still clears the pending resume before writeAndObserveInput returns false. A later passive reconnect then fails to restore the remote pane's grid even though the host input was not accepted. Move this notification to the accepted-write path.

Comment on lines +206 to +208
if (this.owners.get(ptyId) === subscriptionKey) {
removedOwnerClientId = viewers.get(subscriptionKey)?.clientId ?? null
}

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

Preserve the removed owner's client ID when a key repeats.

If subscriptionKeys contains the owner key twice, the first pass deletes its viewer. The second pass replaces removedOwnerClientId with null because owners still holds that key. The method then leaves a deleted subscription as owner, so remote resize suppression can remain active after the viewer is gone. Capture the client ID only while the viewer still exists.

Comment on lines +67 to +73
const reply = await callRuntimeRpc<unknown>(
target,
'git.bulkStage',
{ worktree: toRuntimeWorktreeSelector(context.worktreeId), filePaths: [], scope },
{ timeoutMs: 15_000 }
)
requireGitStageWorktreeScopeReceipt(reply, scope)

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 | 🟡 Minor | ⚡ Quick win

🔎 Supported by static analysis

🏁 Script executed:

set -o pipefail
printf '%s\n' '--- changed file and related symbols ---'
sed -n '1,180p' src/renderer/src/runtime/runtime-git-working-tree-client.ts
printf '%s\n' '--- references ---'
rg -n -F --glob '!node_modules' --glob '!dist' -- 'stageRuntimeGitWorktreeScope|git.bulkStage|callRuntimeResult|forkSync|requireGitStageWorktreeScopeReceipt' src packages apps 2>/dev/null || true
printf '%s\n' '--- diff from required base ---'
git diff --stat e43997c015c265840b99e291348da933e1ee3344 fc4707bf8e689ea2640983cf78dfe25dc2a97d3b -- src/renderer/src/runtime/runtime-git-working-tree-client.ts
git diff --unified=30 e43997c015c265840b99e291348da933e1ee3344 fc4707bf8e689ea2640983cf78dfe25dc2a97d3b -- src/renderer/src/runtime/runtime-git-working-tree-client.ts
printf '%s\n' '--- timeout implementation candidates ---'
rg -n -F --glob '!node_modules' --glob '!dist' -- 'timeoutMs' src packages apps 2>/dev/null | head -200 || true
printf '%s\n' '--- git.bulkStage definitions ---'
rg -n -F --glob '!node_modules' --glob '!dist' -- 'bulkStage' . 2>/dev/null | head -200 || true

Repository: stablyai/orca

Length of output: 41422


🏁 Script executed:

set -o pipefail
sed -n '1,180p' src/renderer/src/runtime/runtime-git-working-tree-client.ts
rg -n -F --glob '!node_modules' --glob '!dist' -- 'stageRuntimeGitWorktreeScope|git.bulkStage|callRuntimeResult|forkSync|requireGitStageWorktreeScopeReceipt' . 2>/dev/null | head -300 || true
git diff --unified=30 e43997c015c265840b99e291348da933e1ee3344 fc4707bf8e689ea2640983cf78dfe25dc2a97d3b -- src/renderer/src/runtime/runtime-git-working-tree-client.ts

Repository: stablyai/orca

Length of output: 9217


🏁 Script executed:

printf '%s\n' '--- client ---'
nl -ba src/renderer/src/runtime/runtime-git-working-tree-client.ts | sed -n '1,140p'
printf '%s\n' '--- exact references ---'
rg -n -F --glob '!node_modules' --glob '!dist' -- 'stageRuntimeGitWorktreeScope' . || true
rg -n -F --glob '!node_modules' --glob '!dist' -- 'git.bulkStage' . || true
rg -n -F --glob '!node_modules' --glob '!dist' -- 'callRuntimeResult' . | head -120 || true
printf '%s\n' '--- base-to-head diff ---'
git diff --unified=20 e43997c015c265840b99e291348da933e1ee3344 fc4707bf8e689ea2640983cf78dfe25dc2a97d3b -- src/renderer/src/runtime/runtime-git-working-tree-client.ts

Repository: stablyai/orca

Length of output: 41296


🏁 Script executed:

set -o pipefail
printf '%s\n' '--- host runtime git method ---'
nl -ba src/main/runtime/rpc/methods/git.ts | sed -n '170,245p'
printf '%s\n' '--- runtime staging implementation ---'
rg -n -F --glob '*.ts' -- 'bulkStageRuntimeGitPaths|stageWorktreeChanges|bulkStageFiles' src/main/runtime src/main/git src/main/providers src/relay | head -120
printf '%s\n' '--- whole-worktree staging implementation/tests ---'
nl -ba src/main/git/source-control/staging.ts | sed -n '1,260p'
nl -ba src/main/runtime/runtime-git-stage-worktree-scope.test.ts | sed -n '1,150p'
printf '%s\n' '--- web staging and caller ---'
nl -ba src/renderer/src/web/preload-api/web-git-api.ts | sed -n '225,265p'
nl -ba src/renderer/src/web/preload-api/web-runtime-calls.ts | sed -n '1,180p'
printf '%s\n' '--- renderer RPC client and related budgets ---'
nl -ba src/renderer/src/runtime/runtime-rpc-client.ts | sed -n '1,220p'
rg -n -F --glob '*.ts' -- 'git.forkSync' src | head -40
rg -n -F --glob '*.ts' -- 'forkSync' src/main src/renderer/src | head -80
printf '%s\n' '--- scoped-stage caller and cap ---'
nl -ba src/renderer/src/components/right-sidebar/source-control/commit/use-bulk-actions.ts | sed -n '140,195p'
rg -n -F --glob '*.ts' --glob '*.tsx' -- 'status cap|statusCap|MAX.*STATUS|capped' src/renderer/src src/main | head -120

Repository: stablyai/orca

Length of output: 39231


🏁 Script executed:

set -o pipefail
printf '%s\n' '--- runtime staging command binding ---'
nl -ba src/main/runtime/runtime-git-staging-commands.ts | sed -n '1,260p'
printf '%s\n' '--- shared scoped staging contract ---'
nl -ba src/shared/git-stage-worktree-scope.ts | sed -n '1,260p'
printf '%s\n' '--- git runner and timeout options ---'
rg -n -F --glob '*.ts' -- 'function gitExecFileAsync|const gitExecFileAsync|export async function gitExecFileAsync|gitCommandTimeoutMs|timeout:' src/main/git src/main | head -160
nl -ba src/main/git/runner.ts | sed -n '1,260p'
printf '%s\n' '--- runtime transport timeout path ---'
nl -ba src/renderer/src/runtime/runtime-rpc-environment-call.ts | sed -n '1,240p'
nl -ba src/cli/runtime/client.ts | sed -n '70,210p'
nl -ba src/cli/runtime/transport.ts | sed -n '1,100p'
printf '%s\n' '--- forkSync timeout ---'
nl -ba src/renderer/src/runtime/runtime-git-sync-client.ts | sed -n '80,125p'
nl -ba src/renderer/src/web/preload-api/web-git-api.ts | sed -n '140,175p'
printf '%s\n' '--- web/runtime timeout defaults ---'
rg -n -F --glob '*.ts' -- 'DEFAULT_TIMEOUT_MS|DEFAULT_REMOTE_RUNTIME_TIMEOUT_MS|resolveMethodTimeoutMs|methodTimeout' src/renderer/src src/cli src/main | head -180

Repository: stablyai/orca

Length of output: 28633


🏁 Script executed:

set -o pipefail
printf '%s\n' '--- Git command runner implementation ---'
rg --files src/main/git/command-runner src/main | rg 'git-exec-file|git-command-timeout|git-spawn|runtime-environment|runtime.*call|request'
rg -n -F --glob '*.ts' -- 'gitExecFileAsync' src/main/git/command-runner | head -40
sed -n '1,260p' src/main/git/command-runner/git-exec-file.ts
sed -n '1,220p' src/main/git/command-runner/git-command-timeout.ts
printf '%s\n' '--- Git options and admission timeout ---'
rg -n -F --glob '*.ts' -- 'admissionTier|timeoutMs|timeout:' src/main/git/command-runner src/main/git/git-runtime-options.ts src/main/git | head -180
nl -ba src/main/git/git-runtime-options.ts | sed -n '1,220p'
printf '%s\n' '--- runtime client defaults/construction ---'
rg -n -F --glob '*.ts' -- 'requestTimeoutMs|new RuntimeClient|RuntimeClient(' src/cli src/main src/renderer/src | head -160
printf '%s\n' '--- runtime server request/disconnect handling ---'
rg -n -F --glob '*.ts' -- 'socket.on|close|dispatch|dispatchRequest|readline|RuntimeRpc' src/cli/runtime src/main/runtime | head -220

Repository: stablyai/orca

Length of output: 30622


🏁 Script executed:

set -o pipefail
printf '%s\n' '--- shared Git timeout policy ---'
nl -ba src/shared/git-command-timeout.ts | sed -n '1,260p'
printf '%s\n' '--- timeout policy tests and callers ---'
nl -ba src/main/git/command-runner/git-command-timeout-behavior.test.ts | sed -n '1,180p'
rg -n -F --glob '*.ts' -- 'gitCommandTimeoutMs(' src | head -80

Repository: stablyai/orca

Length of output: 7859


Give whole-worktree staging a longer timeout.

A capped listing requires the host to scan the worktree. Scoped staging can run several Git commands. The renderer waits only 15 seconds, while Git read commands allow 120 seconds and git add has no default command timeout. The client can therefore report a timeout while the host continues staging.

Use the existing 60-second Git budget in both remote callers.

Suggested fix
-    { timeoutMs: 15_000 }
+    { timeoutMs: 60_000 }
-      })
+      }, 60_000)

@OrcaWin

OrcaWin commented Oct 9, 2026

Copy link
Copy Markdown
Collaborator Author

CI notes:

  • First run, e2e / ssh host auto-converts to managed orcad (Docker): ssh-orcad-auto-convert.spec.ts:476 ("reconnect restores the managed host name after its conversion catalog fails") found 0 host headings. That is main's own test bug. Main fixed it in test(e2e): keep two host sections in the catalog-retry reconnect test #26838 (049bf80) after this PR's first merge ref was computed. Main was merged into phase37 again (b3e5597878, conflict-free), and this e2e job passed on the new head.
  • Second run, test / tests node 24 5/5: all 2,383 files and 23,444 tests passed. The job failed only on one unhandled ReferenceError: window is not defined from React's scheduler after teardown, attributed to src/renderer/src/components/new-workspace/use-smart-workspace-name-field-foundation.source.test.tsx. Phase 3.7 changes no file under new-workspace/. Rerun once.

@OrcaWin
OrcaWin merged commit 6aef5c5 into main Oct 9, 2026
99 of 101 checks passed
Meapri added a commit to Meapri/orca that referenced this pull request Oct 9, 2026
Re-stacks every topic onto upstream 40716d7 (after stablyai#26483 Phase 3.5 and
stablyai#26847 Phase 3.7) through the server sync; tree is exactly
integrate/fork-2026-10-09-3. First parent is the previous main-update commit,
which descends from main, so main still moves by fast-forward.

Fork-Topic: sync-automation
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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