* refactor(runtime-rpc-terminal-io): clean up Phase 3-3.8 code
- One admission prologue and one unary invocation shared by both RPC dispatchers
- Delete rpc schema re-export shims; import from the shared params catalog
- Delete the unregistered duplicate markdown tab method module
- Collapse multiplex installer stage types; add isMultiplexStreamAttached
- One source-identity guard in the ACK recovery snapshot
- Single stream teardown in the renderer multiplexer
- De-duplicate RPC method declarations; derive dispatch options from RpcContext
- Share viewport-frame decoding and one head-trim loop for pending queues
- Single sendTerminal call in sendTerminalStreamInput
* test: point cross-version and scope checks at the moved browser tab-create schema; run transport parity test under Node
---------
Co-authored-by: m4air <m4air@Mac.localdomain>
Co-authored-by: m4air <m4air@m4airs-MacBook-Air.local>
* refactor(runtime-core): clean up Phase 3-3.8 code
- worktree.ps: summary lookup is a closure, not a 4-arg callback
- relay: shared RelayStreamAckWindow for fs and git response streams
- relay: terminal-frame slots live on RelayStreamRegistry
- one isBinaryBuffer/BINARY_PROBE_BYTES in shared/binary-buffer
- one automation owner fence on the controller, no casts
- drop unchecked Store cast in remote repo registration
- speech download reuses the shared abort-aware delay, no casts
- RelayRuntimeServices: drop leftover handler list and param casts
- use shared errorMessage() in audited files
Co-Authored-By: Claude <noreply@anthropic.com>
* test(cross-version-wire): pass worktree ps summary lookup in both builds' shapes
---------
Co-authored-by: m4air <m4air@Mac.localdomain>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: m4air <m4air@m4airs-MacBook-Air.local>
Make orcad --help print usage and exit before native preflight or server startup, preserving existing flag parsing and managed-runtime startup. Verified against the built server and launcher.
Adapted from #26621 by @espetro.
Co-authored-by: Joaquin Terrasa <josocjoq+dev@pm.me>
Let the configured launch command own the server mode when resuming OpenCode 2 sessions. Resume arguments select only the session, while Session History uses the existing default launch command so default standalone behavior is preserved.
Fixes#25003. Retains the contribution from #25004.
Co-authored-by: Justas Brazauskas <brazauskasjustas@gmail.com>
A workspace file dragged into a terminal now pastes only when its source
host (and paired-server namespace) matches the receiving terminal's
transport; otherwise the drop is refused with a toast.
Co-authored-by: m4air <m4air@m4airs-MacBook-Air.local>
Remove the dead connectViaSystemSsh startup path, collapse SshConnection's
repeated transport teardown and duplicated passphrase rung, share one
execHostCommand across relay install modules, share the orcad preflight
short-circuit, single-source telemetry pick/libc, drop dead exports and the
TCP_NODELAY socket probe, and trim sftp-upload and reconnect-classifier
duplication. No behavior change.
Co-authored-by: m4air <m4air@Mac.localdomain>
* Keep terminal workspace root links on their execution host
* Resolve terminal root-link source host from the owner, not a missing connection id
A direct SSH pane whose worktree id also exists on the desktop has no
connectionId (ambiguous owner), which was read as local and offered the
desktop workspace. Resolve the source via the owner-route resolver and
skip the root shortcut when the source host is unknown.
* Resolve terminal file links against the source owner's host
A same-id SSH workspace left connectionId unset, so a rejected root link,
the default-app row and Shift-click fell through to this machine's files.
The terminal file context now names the resolved direct SSH host and
refuses file access when no owner can be determined.
* Keep an ambiguous terminal link owner unresolved instead of local
The connection lookup ignores detected worktree rows, so a registered local
row could override an owner the full resolver found ambiguous (registered
local, detected SSH) and send link stat/open to this machine. Only a missing
route may now defer to the connection lookup.
* Re-resolve a held chat link's owner before rechecking it
Native chat rechecks held link targets when a turn ends. A target resolved
while its owner was local kept that context after a same-id worktree was
detected on an SSH host, so the recheck still asked this machine. Each
existence ask now re-resolves the workspace owner and refuses when it is
unresolved or names a different host.
---------
Co-authored-by: m4air <m4air@Mac.localdomain>
* refactor(orcad-daemon-startup-cli): clean up Phase 3-3.8 code
- One helper (getAllDaemonAdapters / listEveryDaemonGeneration) for enumerating
daemon generations, replacing five hand-rolled copies.
- Shared serve pairing/endpoint readiness for Electron serve and orcad.
- One option list builds both --serve-* and orcad serve argv.
- holdDaemonAdoptionLease takes an options object; reuse childProcessHasExited.
- Drop orcad-entry's dead re-exports and repeated version fallback.
- Reuse the shared withTimeout in daemon retirement.
- Share the install-wait step in the serve update supervisor.
- Trim stale or misplaced comments.
Behavior unchanged; production code is net -132 lines.
* test(serve): keep real serveOptionArgs in launch mock; pin Electron serve argv
---------
Co-authored-by: m4air <m4air@Mac.localdomain>
Co-authored-by: m4air <m4air@m4airs-MacBook-Air.local>
* Let in-page links switch sections inside a chat visual
* Make in-page links work in chat visuals on mobile and in WebKit
The first fix (an about:srcdoc base in the visual) only reached desktop
Chromium. On mobile, the page hosting the visual carried a policy with
base-uri 'none', and a srcdoc visual inherits its host's policy before
its own, so the base was refused and an in-page link still took the
visual off its page and showed it as failed. The mobile web shell sends
the same rule as a header. Separately, WebKit (iOS, and the web client
in Safari) fires the frame's load event for an in-page link inside a
sandboxed frame, and both hosts treated any second load as the visual
navigating away and retired it.
- The mobile host page and the mobile web shell header now allow only an
about: base, so the shell's own base applies; the visual's own policy
still refuses any base it declares.
- Instead of retiring a visual on its second load, the host now asks the
visual's shell after every load. A later load must be answered by the
same run of the shell within the plugin panel watchdog's 5 s deadline;
no answer, or an answer from a new run (a reload), still retires it.
- Tests: unit tests for the policy, the answer reader and the frame's
keep/retire rules, and a Chromium + WebKit test of both mobile surfaces
that runs in the mobile web app CI job.
* Keep any answered later load, and say plainly what the check is for
The previous commit tagged each run of a visual's wrapper with a random
id so a reload would still retire it. The visual shares a window with
its wrapper, so it could read or set that id itself, and a reload only
reruns the same Orca-written document, which is not an escape. Drop it:
after any load past the first, the host asks once, and any answer on
this visual's channel within the wait keeps it; silence retires it.
- Comments now say the check is a tripwire for a frame gone blank or to
another page; the frame guard (desktop), the host page's policy plus
the app's load filter (mobile) and the sandbox are the boundaries.
- Visuals get their own 5 s wait instead of borrowing the plugin panel
watchdog's, because a 1 s wait retired a busy visual in WebKit.
- Tests: a reload is now kept; going blank, or dropping the base and
following a link, still retires. The CI routing for the shared wrapper
is now pinned in the path-classification test.
* Load the visual links check's mobile modules only where they can build
The unit shards run every config/scripts test with only the root
dependencies installed. This check imported a mobile/ module at load,
and transforming it reads mobile/tsconfig.json, which extends a package
those shards do not have. The transform error could not be sent back to
the test worker under Bun, so the whole shard stopped with
"DataCloneError: The object can not be cloned".
Import the visual shell and the mobile host page inside the dependency
gate, as the session-dictation check already does, so the shards skip
the file without loading them. The mobile web app job, which installs
mobile dependencies, still runs it in full.
- Pinned-relay planning throws its PinnedRelayFallbackError instead of returning a host-node plan
- One shared execHostCommand replaces eight private copies
- Install lock shares its post-acquire GC recheck and unconfirmed-termination catch
- Relay session tracks unsupported optional methods in one set
- Previous-relay census carries one status instead of two booleans
- Remote-node resolution drops the signal-only option shims
Co-authored-by: m4air <m4air@Mac.localdomain>
Co-authored-by: Claude <noreply@anthropic.com>
* fix(remote-terminal): keep the disconnected-input grace honest across retries
A web pane's Reconnect cancelled recovery to idle before replaying its attach, which
reset the 5-minute grace, so each failed Reconnect extended how long held keys could
wait. And a retry started inside the grace discarded every held key once the grace
ended, including keys typed during that retry.
Held input is now split into cohorts at each retry from the latch: the sealed cohort
keeps the grace of the latch it was held through, keys typed during the retry are only
charged to a later latch, and the web Reconnect cancel no longer resets the grace.
* fix(remote-terminal): a failed in-flight write from an expired cohort keeps newer input
Dropping an expired cohort discards the journal, which fails a held accepted write
still in flight; the suspended flush then treated that as a broken link and cleared
the keys typed during the retry. The dropped write is now retired, so the flush pauses
or continues with the newer cohort instead.
* fix(remote-terminal): keep held input whole across a retry begun inside the grace
Dropping an expired older part of the hold while delivering keys typed after it is
unsafe: held input is one ordered stream, and the dropped part may be the Ctrl+C the
newer keys rely on. Replace the cohort split with an order-safe bound: a retry begun
inside the grace carries the whole hold until it ends, and the next latch measures
from the original start, so the overrun is at most one recovery window.
* fix(remote-terminal): replay a web pane Reconnect inside a recovery epoch
The web Reconnect path cancelled recovery to idle before replaying its attach, which
cleared the deadline: an attach whose snapshot stalled could release held input at any
later time. It now begins a fresh recovery epoch, as auto-recovery replays do, so the
deadline bounds the attempt and fences a late snapshot, and the cancel no longer counts
as a recovery that resets the grace.
* docs(remote-terminal): grace reset wording matches its reset points
---------
Co-authored-by: m4air <m4air@Mac.localdomain>
The "+N" overflow chip in the status bar changed width whenever its count changed, so the density probe that decides how many usage chips fit kept re-measuring. Reserve the width of the provider-count label with an invisible sizer span, so the chip keeps the same size across counts.
- Pass the total provider count into UsageOverflowChip and render it in an invisible, grid-stacked sizer span.
- Overlay the visible "+N" label on the same grid cell so the chip width tracks the total, not the hidden subset.
- Add an e2e test that loads ten usage providers, shrinks the viewport to collapse them, and checks the chip width stays stable and no "Maximum update depth" or React #185 errors occur.
* Invalidate shared file watch targets when the selected host changes
* Watch the selected host's own workspace root when hosts share a workspace id
* Watch each open editor on its own execution host instead of the selected host
---------
Co-authored-by: m4air <m4air@Mac.localdomain>
The receipt transaction test's fake writer took a single row, but the journal commit hook now receives an array of rows. The mismatch broke typecheck. The mock now accepts the rows array and spreads them into the captured list. The test also asserts the committed row's contents, so a wrong row would fail the test instead of passing on length alone.
* Show whole-tab hover cards with icons and sliding handoffs
* Remove the gap below tab hover cards
* Keep tab hover handoffs reliable during rapid reversals
* Place hover-card icons and program names below the title
* Fix nested keyboard hints and complete hover-card translations
* Isolate keyboard hover checks from terminal activation focus
* Sample rapid tab slides within the renderer
* Control hover frame recording around actual pointer input
* Keep tab hover cards warm through StrictMode ref reattachment
* Keep hidden renderer animation frames live during hover checks
* Sample hidden tab hover cards with an unthrottled timer
* Narrow hover animation effects and avoid shadowing window
* Keep hidden renderers painting while checking hover card frames
* fix(remote-terminal): keep a reconnected pane's size after an SSH outage (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>
* fix(remote): an oversized RPC reply fails that request instead of closing 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>
* fix(ssh): let a slow host finish installing the pinned Node runtime (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>
* fix(ssh): shell startup output on stdout no longer breaks SSH connect (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>
* fix(orcad): finish an abandoned update on connect, and refresh renderers 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>
* fix(terminal): a remote recovery never splices stale history above the 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>
* fix(source-control): Stage All stages every change when the listing hit 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>
* fix(ssh): redeploy a managed server another desktop stopped; forget one 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>
* fix(ssh): redeploy a managed server a pre-version Stop left; let a forced 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>
* fix(remote-terminal): no keystroke lost, duplicated or split across a 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>
* fix(remote-terminal): keep input typed past the auto-recovery window 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>
---------
Co-authored-by: m4air <m4air@Mac.localdomain>
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-authored-by: m4air <m4air@m4airs-MacBook-Air.local>
* Restore remote explorer updates through the recovered app watcher
* Keep the remote Explorer watched when it is shown as a fallback tab
---------
Co-authored-by: m4air <m4air@Mac.localdomain>
* Require a server reply before treating a live managed process as serving
* Log the unanswered live managed server like the other unverifiable verdicts
---------
Co-authored-by: m4air <m4air@Mac.localdomain>
Co-authored-by: OrcaWin <alpha-eng@stably.ai>
* Preserve desktop browser page placement when SSH workspaces convert
* Place the browser-service-status page on the managed host explicitly
Converted SSH workspaces now keep retained desktop pages local, so the
spec opens its remote-placed page on the managed environment instead of
relying on the retained page moving there.
* test(e2e): refocus the managed browser page before retrying its Reconnect
The managed host can seed a terminal for the converted workspace and the
client mirrors it with focus, hiding the browser notice mid-test.
---------
Co-authored-by: m4air <m4air@Mac.localdomain>
Co-authored-by: OrcaWin <alpha-eng@stably.ai>
* feat(acp): compact Grok, OpenCode and OMP chats
* test(acp): expect Grok compaction capability
* fix(chat): keep skipped compaction outcomes visible across history pages
* fix(acp): a signed-out /compact reports not signed in, as a send does
A /compact the agent refused for sign-in now records the same notSignedIn
fact as an ordinary send, so the chat shows sign-in guidance and the next
send starts a new agent that reads the new login.
* refactor(acp): keep the timeline translator under its line limit after main's growth
Main's OMP per-channel message identity grew acp-timeline-translator.ts past
max-lines with this PR's compaction handling. The running compaction's frame
folding moves beside its ending in acp-compaction-turn.ts, and the
compacting-prompt lookup into AcpPromptTurns; behaviour is unchanged.
Single and bulk discard quiesced editor autosaves for the globally focused
runtime, so a managed repo's in-flight save could land after git restored the
file and bring the discarded edits back. Use the repo-owner-routed settings
the discard itself already uses.
This reverts commit 46460d49c7 (#26171).
Pause the Node 24 requirement so existing SSH and WSL users are not
surprised by a host-runtime upgrade requirement in the next release.
This is a precautionary rollout hold requested to avoid near-term user
disruption, not a cancellation of the Node 24 transition.
Restore the prior Node 18 host minimum and companion build targets.
The previously shipped bundled Node 24 runtime and compatibility launcher
remain in place. Preserve the original change in a separate draft PR for
reintroduction after compatibility and rollout validation.
* feat(agent-launch): accept a create-folder-workspace target
agent.launch can now create a folder workspace and start its agent there,
the same operation it already runs for a new git worktree. The host
creates the folder workspace (metadata only, no startup terminal), resolves
it the way an existing target is resolved, then settles the mode and builds
the surface as for any existing folder workspace.
Hosts advertise agent.launch.create-folder-workspace.v1 so a client offers
the target only to a host that accepts it.
* fix(agent-launch): authorize, classify and navigate folder creates like their create method
A create-folder-workspace launch could do three things its two-call
equivalent never did:
- A phone could create folder workspaces. The mobile allowlist admits
agent.launch but not folderWorkspace.create, and is checked by method
name only. A create target now needs the authorization of the create
method it stands for, read from the same allowlist, and a phone is
refused with agent_launch_target_forbidden before admission, so no
ledger row and no create. create-worktree maps to worktree.create,
which phones already may call, so nothing changes for it.
- A create refused before anything was stored (group gone, folder
missing, SSH host unreachable) replayed as
agent_session_operation_unknown, and a retry under the same operation
id was refused until the row expired. runtime.createFolderWorkspace now
raises those refusals as FolderWorkspaceCreateRefusedError, with the
same message text, and the launch records it as failed for any target.
A failure after the create still replays unknown.
- A paired caller's folder create activated the new chat on the host
window and did not select the caller's own tab. Only create-worktree,
whose host activation runs setup, keeps the host navigation now; a
folder create navigates like an existing workspace.
Tests cover the phone refusal, replay of a success, a refused create
settling failed and replaying that failure, an after-create failure
staying unknown, an SSH connection reaching the agent's view mode, paired
navigation, and an older host refusing the target at parse.
* fix(agent-launch): replay a refused folder create with its whole path
A folder create refused before anything was stored is recorded as the
launch's failure, and a retry under the same operation id answers from
that record. The record went through the 128-character cap every launch
failure code gets, and folder refusals carry the folder's absolute path,
so a retry could show the user a path cut short: a different folder.
The refusal is now recorded whole when it fits a path as long as Linux's
PATH_MAX after its code. Past that, which no folder a stat accepts
reaches, only the code before the path is kept, so the replay degrades to
the bare code instead of a partial path.
Also: the controller test asserts the exact refusal message, the
create-folder-workspace capability comment says a phone needs its own
gate, and the forbidden-target code says why it is not `forbidden`.
* test(folder-workspace): type the controller test's store stub without an assertion
The changed-code quality gate refuses the `as never` cast on the stub
store. It is now a RuntimeStore whose unreached members throw.
* test(agent-launch): run the folder-create launch tests in the Node runtime project
The operationId replay tests open the real SQLite record store, so the
file belongs with the other ledger-backed agent.launch tests.
* test(agent-launch): pin that a paired folder create's terminal stays off the host window
The folder-create test now asserts surfaceOwner: false on the terminal a
paired caller's launch creates, rather than matching any options. The
caller-selection header now says only an in-process worktree create keeps
worktree.create's navigation.
* test(agent-launch): drop the per-connection view-mode expectation main removed
* feat(native-chat): one saved model catalog for every structured agent
Every agent registration now declares how its models are listed without a
session (a probe, or explicitly none). Probes share one supervised runner
(15 s budget, bounded output, teardown on every path). Grok lists from
initialize (or x.ai/models/list), OpenCode from models --verbose, Pi from
--list-models; Claude moves onto the shared runner. ACP option reads no
longer copy the running model's effort menu or selection onto every model.
Desktop and phone pickers read the same catalog for every agent, with a
non-pickable provider-default model pill before any list exists.
* fix(native-chat): host names a listing's configured model; live listings saved for every agent
- The catalog answer carries an optional listingNamesConfiguredModel field, set
per registration (Codex, Grok) and per workspace; pickers name the listed
default from it instead of a Codex-only client flag (older hosts fall back).
- Grok, OpenCode and Pi live option reads save their model list through one
host step, keeping the account's known default and per-model efforts.
- Phone reads the host catalog like the desktop and seeds every agent.
- Saved picks accept any known agent id (OpenCode, Pi).
- ACP agents with no session-free listing register as unavailable; Grok's live
effort menu falls back to the session's when the advertised one is empty.
* fix(native-chat): discovery and live listings kept apart; every agent's live listing saved by the host; phone-created chats name their model
* fix(native-chat): phone new-chat mark clears on the chat's first live answer; Claude's startup listing reaches the host catalog
* fix(native-chat): live-listing type lives beside the catalog entry so wire types don't pull the store's persistence into the phone
* fix(native-chat): the model picker never waits on a listing; a first listing lands in place
* feat(native-chat): the host lists every agent's catalog ahead of use and traces each listing
* feat(native-chat): a new Claude chat names the model and effort its config resolves to
* test(native-chat): catalog service fakes gain prewarm
* test(native-chat): prewarm span recorder without a type assertion
* fix(native-chat): project agent config overrides the account default only when it sets a model or effort
A committed .claude/settings.json for permissions or hooks, or the settings.local.json Claude writes on always-allow, no longer hides the configured Claude model in that project. Unreadable config still counts; Codex config.toml follows the same rule.
* test(native-chat): the Codex picker read and the launch it starts resolve one account key, through the real resolvers
Runs resolveHostCodexHomePathForLaunchReadOnly and prepareCodexRuntimeHomeForLaunch as the runtime service wires them, for the system account on the real home, on the shared runtime home, and a managed account. Also scopes the prewarm suite to agents in use.
* fix(native-chat): Claude's configured model and effort are saved together behind the project check; prewarm lists only agents in use and stops with its host
A live Claude chat no longer stamps its effort onto the account catalog; the effort travels with the configured default, which a no-pick chat whose config resolves to no listed model now forgets. Prewarm lists agents with a saved catalog or a chat, and host teardown (quit included) starts no queued listing and stops running ones through an abort signal the listing runners honor.
* test(native-chat): new catalog tests use typed hoisted state and type-only namespace imports
* test(native-chat): a prewarm probe sets and clears the sign-in verdict like a read's, and re-checks an aged one beside a fresh catalog
* fix(native-chat): an aged sign-in verdict re-probes only from probe paths, not a Codex chat's picker
The main merge put the held-verdict check into store.shouldRefresh, which also gates
the live Codex picker. A chat's own listing never clears the verdict, so a signed-out
Codex whose chat works re-listed model/list on every picker read. shouldRefresh is
back to main's rule; the catalog read's probe and prewarm ask probeDue instead.
* fix(native-chat): Claude project settings with an effort cap, a main-thread agent or an Anthropic cloud provider count as an override
maxEffortLevel clamps every effort including the model default, and agent applies that
agent's model (both from the bundled SDK settings type). CLAUDE_CODE_USE_ANTHROPIC_AWS
and CLAUDE_CODE_USE_ANTHROPIC_GOOGLE_CLOUD sit in the SDK's provider-switch list beside
Bedrock and Vertex. A project that sets only these no longer shows, or saves as the
account default, the account's model or effort.
* refactor(native-chat): the catalog store's picker waiters live in their own module
No behavior change: pendingListing and the waiter notifications move as they were,
which keeps the store under its line limit for the fixes that follow.
* fix(native-chat): changing an agent's command or environment re-lists its catalogs and re-checks its notice
The settings change called prewarm, but prewarm skipped any catalog still fresh (the
catalog key is agent + account, not the command), and a held "not installed" verdict
kept its 30-second window. The change now marks that agent's catalogs due and ages its
held verdicts, so the prewarm it triggers re-lists them and re-checks the notice. Only
agents whose command or env actually changed are touched.
* test(native-chat): the sign-in notice tests check the model picker is really usable
They checked only that a model entry existed, which a disabled or empty pill also
passes. They now check it is settable, carries no disabled reason, and offers the
catalog's or the seed's model.
* fix(native-chat): a probe the host stops keeps the held notice reason, and a stopped prewarm reads nothing
A probe aborted because the host is going away says nothing about the account, so it
no longer replaces a held sign-in or not-installed reason with a plain failure. A
prewarm after stop returns before reading chat records.
* test(codex): the catalog account-key test uses the runtime's real Codex launch-home resolvers
The resolvers move to codex-launch-preparation as one export the runtime service
spreads, so the test runs exactly what the runtime is handed instead of a copy.
* feat(native-chat): keep each host's saved model lists in the renderer
A chat picker could only learn the host's saved model list by reading it
after it mounted, so its first frame was a guess. The renderer now keeps
the host's answers per host and agent: this machine's lists are read once
its runtime answers, for the agents its chats were used with; a paired
host's again when this client regains contact; and an account or launch
setting change drops this machine's lists. Every entry is a host answer.
* fix(native-chat): render the model pills once, from the host's list
A new chat's composer painted its first frame from the built-in model list:
a saved pick that list lacks (Claude's `opus[1m]`) showed as a raw id with
no effort pill, then jumped to "Opus (1M context) · High" when the host's
list landed. Every new chat painted a guess and then corrected it.
Now the first frame reads the host's saved list the renderer already holds,
so it is the final frame. Without one, the quiet "Model" placeholder stands
until the host answers; a host with nothing saved makes the built-in list
usable, naming nothing, until its listing lands. A saved pick is named only
from a host or session list that names it. The phone follows the same rule
and keeps the host's list across a new fence.
* feat(native-chat): answer a saved-only model catalog read
The app could preload a host's saved model lists only for agents with a
saved pick, because the catalog read starts a listing (spawns the agent's
CLI) when nothing is saved. Grok, OpenCode and OMP chats with no saved pick
were never preloaded, so their pickers showed "Model" before the host's list.
`agentSession.modelCatalog` now takes `savedOnly`: the host answers from
what it saved (the entry, configured default and held reason) and starts,
joins or re-checks no listing. Its params are strict, so the host
advertises `agent-session.model-catalog.saved-only.v1` and clients send it
only to hosts that do.
An answer also says when its named default holds in every workspace (Grok
reads no project config for its model), so a new chat in any workspace can
use it on its first frame.
OMP stays without a session-free listing: `omp models --json` lists every
model without the `enabledModels` filter its chats apply.
* fix(native-chat): preload every registered agent's saved model list
Grok, OpenCode and OMP chats painted the "Model" placeholder, then the
host's pills, because the app preloaded only agents with a saved pick.
Now the app reads every agent a host registered through the saved-only
read: this machine once its runtime and agents are known, a paired host at
connect and on each reconnect, and this machine again after an account or
agent-setting change. A host that does not advertise the saved-only read
keeps the earlier rule (saved picks only; a paired host's answered agents).
An agent the host saved nothing for stays unknown. An answer sent before
the host's lists were dropped is not recorded.
* test(native-chat): drop a type assertion from the snapshot sync test
* fix(native-chat): learn an ACP agent's default from a chat started with no pick
Grok's session-free listing named the model `initialize` computed (grok-4.7), but chats ran
another (grok-4.6), so a new chat painted 4.7 and then jumped. The listing no longer names a
default; a new ACP session with no model pick reports what it runs, and the host saves that as
the account's configured default behind the workspace check. A catalog read waits for a
recording still in flight, so the chat's own follow-up read names it.
* feat(native-chat): check OpenCode and OMP project config before naming their default
OpenCode reads opencode.json(c) and .opencode/ in every ancestor of the workspace; OMP merges its
.omp settings and other agents' project settings. A workspace whose config sets a model, effort
or provider key (or can't be parsed) names no default; any other lets a no-pick chat's model
become the account's default, so the next new chat names it on its first frame.
* fix(native-chat): re-read the host's list once a no-pick chat reports its model
The renderer kept the answer it read before the chat started, so the next new chat's first frame
still named no model. A new chat that named no model reads the host's list again once it reports
what it runs; a default no workspace can replace also serves chats in other worktrees.
* fix(acp): start chats for agents that report no model
* fix(acp): omit unknown models from resting session reads
* test(acp): expect absent models for resting Claude chats
* refactor(native-chat): move known agent discovery out of session host
* Start worktree.create's agent through the launch executor
The host's worktree.create handler now starts a requested agent (startupAgent,
or a linked startupDraft) through executeAgentLaunch instead of calling the
create directly. Nothing on the wire changes: the request, the reply and
agentTerminalHandle are the same, the agent is still the workspace's startup
terminal whatever the chat default says, and the create still delivers the
text itself (folded into the command at any length, a follow-up for agents
that read it after start, and the draft pasted unsent).
Adds three host-internal executor inputs: terminalOnly, the legacy-host prompt
policy, and a shared entry (createWorktreeWithStartupAgent) that host
producers can call in place of createManagedWorktree.
* Keep a linked-draft create recording only the agent it actually started
Problem: after worktree.create moved onto the launch executor, a create from
a linked issue (phone "create from issue", or a raw worktree.create with
startupDraft) could differ from before in two narrow cases. When the draft's
agent came from the settings default or from detection and its launch command
could not be built (for example unbalanced quotes in the saved agent
arguments), the workspace was recorded as created with that agent even though
it never started, and a background create opened no terminal at all instead
of its plain shell. When no agent was found, detection ran twice (an extra SSH
round trip on remote repos).
User-facing change: none intended; both cases now behave exactly as before
the executor move.
Mechanism:
- The entry hands the draft's chosen agent to the create as a main-internal
startupDraftAgent, which steers only the draft; createdWithAgent stays what
the caller sent. A draft that resolves no agent is created without the
draft, so the create does not detect again.
- AgentLaunchExecution is a discriminated union: every normal launch must
bring its surface factory, and a legacy-host launch must be terminal-only,
bring a workspace factory, and cannot bring surfaces. The runtime guards
the types replace are gone; the target kind is checked before any create.
- Legacy-host receipts follow the delivery module's rule: a blank prompt is
no prompt, a submit folded into the launch command is handed-to-terminal,
a post-start send the create does not await is unconfirmed (host-internal),
a draft is not-delivered.
- The no-agent error lives with the legacy-host policy and is caught only by
the shared entry.
- Tests: a real-runtime parity test (stored agent, terminal spawns,
detection count vs the create alone, including the unbuildable plan),
exact-argument checks over every non-agent startup field, compile-time
checks for the union, and direct tests of the draft agent resolver.
* Say which creates still bypass the shared agent entry
Two comments stated that headless automations and federation already go
through createWorktreeWithStartupAgent; both still call
createManagedWorktree directly. Reword them as the plan. Comment-only.
* Start orchestration workers and remote workers through the launch executor
A fresh worker (new worktree or existing, terminal or structured) and a
federated worker on its receiving host now start their agent through
executeAgentLaunch with the worker's own workspace and surface factories.
The worker hands in the mode it decided before its dispatch record existed
(decidedMode), so the host is asked once and the stored receipt is the one
that ran. Effects, setup receipt, stage rows and their order are unchanged;
a refused structured worker still fails rather than becoming a terminal.
* Tighten the worker launch contract and discard a worker session a later launch step strands
- decidedMode is now exclusive with terminalOnly and callerRendersStructured in the type, labelled temporary until one planner settles every launch.
- connectionId's doc says what is true: the executor carries it and nothing reads it yet.
- A worker launch that throws after its structured session exists discards that session itself, since the caller only tears down a returned placement.
- A federated create refuses a launch with no startup agent and narrows repo/name before recording its first stage.
- Tests: exact federated existing-path stages, a created-worktree host refusal that starts a worker terminal, and the stranded-session discard.
* Say exactly which inputs a decided launch mode refuses, and pin the refusal at compile time
* fix(remote): retry failed SSH conversion catalog loads
* fix(remote): keep the loaded catalog across ordinary disconnects
* fix(remote): replay a reconnect that lands while a failed catalog read is pending
---------
Co-authored-by: m4air <m4air@Mac.localdomain>
* feat(grok): show Grok's subagents as subagent rows
* fix(grok): retain live subagent roster ownership
* fix(grok): retain child outcomes during shutdown
* fix(grok): show live subagents in running task surfaces
* fix(grok): settle owned child session completion notices
* refactor(grok): fold child-session guards under the translator line limit
The host header lays out as a 32px card in 4px top padding (36px), but
estimateRenderRowSize sized the row at 32px. The group tier's 36px pin
offset then fired 4px early, so a project header could pin while its
in-flow copy was still on screen — two headers at one coordinate.
Size the host row 36px so it matches the pinned slot, and resolve each
sticky handoff from the row's real start offset instead of only the
lagging range start, so a header pins exactly when it reaches the slot.
Run Muse Windows hooks through the existing direct launcher on safe profile paths and refresh managed installations. Keep the PowerShell compatibility fallback for paths requiring special quoting. Verified on Windows; CI passed.
Adapted the direct-launcher change from community PR #26734.
Co-authored-by: Naveen Chatlapalli <naveen.aifanatic@gmail.com>
Enable the independent hang watchdog on Windows and preserve the previous run's marker until the startup trace sink is ready. Retain local consent, heartbeat timing, worker cleanup and bounded marker validation. Operation tracking is deferred.
Adapt the startup-order regression coverage from community PR #26468.
Co-authored-by: fanzzzd <fanzzzd@outlook.com>