* fix(ssh): a disconnect keeps an old relay terminal's pane on the bridge instead of a new shell
* fix(ssh): release held panes when a reconnect finds their leases retired
* test(ssh): let the node typecheck see the renderer's bridged-pane selection
* fix(remote-runtime): wait for the host's terminal list before opening a terminal
A paired client opening a workspace whose terminals it had not heard about
yet read "no answer from the host" as "zero terminals" and asked the host for
one. When the host's list then arrived with terminals, the workspace gained an
extra terminal; when it arrived empty, the first-frame bootstrap created a
second one.
The client now treats an unknown host count as unknown. Activation without
explicit startup work waits, and the active subscription's first real answer
seeds the workspace if it is empty. An "ask me later" frame from a host that
has not published yet no longer records a count of zero, and the host's later
answer (sent as an update) is accepted as the first answer for seeding.
* fix(remote-runtime): count an unknown host as empty unless it answers later
Hosts before #25736 answer an empty workspace with "ask me later" and
never follow it with a real answer, so waiting for that answer left a
blank workspace with no terminal against them. Wait only on hosts that
advertise session-tabs.empty-worktree-answer.v1; older hosts keep
main's behaviour. Workspaces this client just created are known empty
and open their first terminal without waiting.
Co-Authored-By: Claude <noreply@anthropic.com>
* fix(remote-runtime): log a failed delayed terminal open; drop an inert flag
Co-Authored-By: Claude <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
* fix(remote): keep a paired pane when the host has not mounted it yet
A paired client read the host's `no_connected_pty` answer as a clean
exit and closed the pane. The host also sends it when its window has
not mounted the pane within its wait, so it proves nothing about the
terminal. Back off and resubscribe within the existing recovery budget
instead; only an explicit exit verdict closes the pane.
Co-Authored-By: Claude <noreply@anthropic.com>
* test(remote): a pane the host stops listing after no_connected_pty ends as lost
Co-Authored-By: Claude <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
* fix(automations): stop offering Resume workspace for runs that never dispatched (STA-10103)
* fix(automations): hide the run page action when there is nothing to open
* fix(native-chat): fit chat stream pages in one mobile bridge frame
A long native chat failed to open on a phone with "the shell ended this
stream (overflow)". The host sized each history page to ~1.94 MiB (half
the relay cap), but the mobile app ends any stream whose event frame is
over 640 KiB, so the opening snapshot of a long chat never arrived.
Pages now fit that frame; older rows load through Load earlier as before.
The page also words a stream the app ended in plain language instead of
the protocol's reason.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* refactor(native-chat): keep the outline on the relay page share; plainer end words
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* test(mobile): read the bridge frame cap from its shared definition
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* feat(native-chat): bounded frames for the phone's chat stream
A phone page now asks the host (when it advertises
agent-session.bounded-frames.v1) to keep every agentSession.subscribe
frame within the bridge's 640 KiB frame:
- each row is cut to what the phone draws (tool output to its detail
length unless it draws as a diff, tool inputs to the diff view's
length) and then to one row's share of a page, its long strings cut to
a common length; the row is marked `truncated` with its full size
- history frames carry rows and metadata held to a fixed ceiling; the
draft queue and the background-task roster go in frames of their own;
the command list is not sent, since the phone reads it from options
- the queue's admission bound counts whole cards and is sized to one
frame, so a phone always gets every card whole
Other subscribers get the frames they always did.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* refactor(native-chat): bounded frames review round 1
- the phone waits for the host's capabilities before opening the
stream, so its opening frame is never sent unbounded
- rows are bounded once, by the reader that sizes the page; the item
form is an options field and always a function
- one cut rule (`cutLongStrings`) serves the display trims and the fit
- a side list rides an empty history batch rather than a second frame;
commands changes send a bounded subscriber nothing
- queue admission measures the candidate list once; a legacy queue of
more cards than fit lists the ones it sends first
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* refactor(native-chat): bounded frames review round 2
- an over-budget legacy queue cuts its text to one common length and
marks the cards it cut, and no longer names a next card it left out
- one predicate says whether a subscriber is owed the `/` list
- a lighter invariant fixture
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* fix(native-chat): type the shortening guards against named shapes
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* refactor(native-chat): bound every phone's chat stream at the host
The host now bounds frames for every phone (clientKind 'mobile') at the RPC
boundary, so the subscribe param, the host capability and the phone's wait
for the capability probe are gone. Phones of every version already ignore
the stream's slash commands, and a phone reached through the desktop relay
signs in as a phone too.
- A live gap of more than one page reaches a phone as the newest page, so
no publish outruns the bridge's unacked window; older rows load on scroll.
- Queue admission sizes each card as wide as a failed send can make it.
- Rows are bounded only as a page takes them; the unread row marker is gone
and a shortened queue card is marked `truncated: true`.
- A bounded subscriber no longer reads or tracks the slash commands.
- The stream-end words are back to main's: `overflow` also covers a page
that stopped acking, which "too large" would misstate.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* fix(native-chat): count every card in line and never shorten one
Queue admission now counts every card the queue can publish again without
passing admission: waiting and returned cards, and sent cards whose delivery
is not yet accepted, which a refusal or a Stop puts back. The fixed per-card
reserve for a failed send's words is gone.
When a phone's list is over its frame room anyway (an older build's larger
queue, or a card that grew after admission), the phone gets the cards it
sends first, whole, and the rest once those leave. Cards are never shortened,
so no app version can Edit a partial copy over the full draft, and the
`truncated` card flag is gone.
Bounded frames strip only the roster from an event; no event carries the
other side lists.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* refactor(native-chat): bound only phone frames; keep main's queue and page sizes
Queue admission is main's again. The phone's list is bounded where it is
sent: the cards it sends first, whole, so every frame fits for any queue
size and no card is shortened.
History pages keep main's size for every reader but phone subscribe frames.
A reader is now its item form plus its page budget, so desktop pages, the
outline and phone history replies (chunked by the app's bridge) are as on
main, and only bounded frames use the bridge frame's share.
Drops the queue's `nextQueuedMessageId` rewrite, which no phone reads, and a
test that duplicated the prefix check.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* refactor(native-chat): bound phone rows by size alone; final tidy
Rows the journal already caps at a 16 KiB head gain little from a second,
display-shaped cut, and the diff check behind it passed a character count as
a line count. A row is now held to its frame share by shortening alone, and
native-chat-diff is back to main's.
Unexports file-local bounds, fixes a capability-era test comment, and adds
tests for string shortening and a phone's stop-note refresh.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* test(native-chat): assert a phone's stop-note refresh pages
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* test(native-chat): run the bounded-frames suite in the Node runtime project
It opens a real SQLite journal.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* fix(native-chat): bound legacy dispatch reasons on read
Rows written before the write-side bound carry a provider's full error text,
which every page and phone frame attaches with its submission.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* feat(wsl): open WSL host paths from Windows through one owner-routed capability (WSL-via-orcad C8)
* test(wsl): typed shell handler lookup for the WSL owner tests
* fix(wsl): read known WSL hosts through a registered accessor; e2e for the Windows folder picker
* fix(wsl): open the host-path entry filled in after a Windows pick; e2e screenshots
* test(wsl): read the e2e picker's default path through the main process env
* refactor(wsl): read a WSL host's distro from its record instead of a new accessor
* fix(wsl): skip the host browser only for a path Windows' picker or an Explorer drop filled in
A path left filled from an earlier pick on an SSH or paired host opens the host browser again, as it did before.
* Show folder workspaces in the Cmd+J jump palette
Cmd+J only listed git worktrees, so folder workspaces could not be found or opened from it. The palette now includes folder workspaces alongside git worktrees and renames the section from "Worktrees" to "Workspaces" to match.
- Folder rows are searchable by name, comment, and attached GitHub item, and follow the sidebar's host scope.
- A blocked activation (for example, a folder path that no longer exists) keeps the palette open and shows only the path error, instead of closing or showing a second generic error.
- Folder path-status caching and sidebar reveal are keyed by execution host, so folders with the same id on different hosts no longer share cache entries or reveal the wrong group.
* Consolidate runtime target imports in folder-path-status
`getRuntimeTargetHostId` now comes from the same module as `runtimeTargetForOwnerEnvironment`, so both are imported from `runtime/runtime-client-target` instead of splitting them across two modules.
* Wake sleeping folder workspaces from Cmd+J
Picking a sleeping folder workspace from the jump palette did not restart its terminal or restore its Files explorer view. Folder and git worktree activation now share one code path in workspace-activation.ts, so both wake the same way.
- Wake a sleeping folder the same way as a worktree: bump the tab generation and tag the pending spawn when no PTY is live, including when the workspace is already selected.
- Restore the remembered right-sidebar explorer view for folders.
- Skip the GitHub PR refresh for folder workspaces, which have no PR.
- Clear unread only on the folder entry whose execution host matches the activation.
* Reveal folder workspaces on their catalog owner host
Cmd+J activation revealed folder workspaces against the focused runtime
server's host, so a folder owned by a local or SSH project could be
revealed under an unrelated server. The reveal host now comes from the
folder workspace's catalog owner, and an explicit caller host still wins.
Add a regression test covering local and SSH owners while another
runtime environment is focused.
* Keep shared workspace activation in the existing layout writer
* feat(wsl): Add WSL distro host UI, host row status and fake-transport e2e (WSL-via-orcad C7b)
Experimental toggle (Windows only), an Add WSL distro choice beside SSH host and remote server,
the WSL host's own row in host pickers with its liveness state, a Connect action that starts a
stopped distro only on request, a WSL hosts list in Settings, and the renderer report of which
WSL hosts an open project or terminal needs.
* feat(wsl): show WSL hosts as turned off while the experiment is off, and confirm their removal
With Experimental → WSL distros as hosts off, a WSL host keeps its row so its projects never
seem to vanish, says it is turned off, and offers Turn on (opening that setting) instead of
Connect; Settings lists it the same way with Connect disabled. Removing a WSL host now asks
first in the dialog SSH hosts use, with WSL copy. The e2e covers both.
* fix(wsl): skip the WSL host status bridge when the preload lacks it
* fix(wsl): show when this build cannot run WSL hosts instead of failing an add
The UI reads main's template capability once. Without it, the add dialog says this build can't
run WSL hosts and disables Add, existing hosts' rows and Settings say the same with Connect
disabled, and the Experimental toggle notes it. The e2e covers a fake build without the files.
* fix(wsl): re-read the WSL distro list when the add dialog's window returns, and on Refresh
The add dialog read the registered distros once, so one imported while it was open never
appeared. It now re-reads when the window regains focus or becomes visible, and from an explicit
Refresh button, never on a timer. The selection is derived from the current list, so a refresh
cannot leave a stale pick. The e2e imports a distro mid-dialog.
* fix(wsl): list WSL hosts only as WSL hosts, not among the generic remote servers
Settings → Remote Orca Servers, the server-update checks and the status bar's server list now
leave out environments whose deployment record is a WSL host (by kind, never by name). The WSL
hosts section reads them from the store itself.
* fix(wsl): say a WSL host's removal ends its terminals, and report what it left running
The confirmation now says removing ends the host's open terminals as well as Orca's server, and
a removal main could not complete in the distro (a terminal or the daemon kept running) is
reported instead of passing silently.
* fix(wsl): ship the removal copy that says a WSL host's terminals end
The build resolves copy from the English catalog, which still held the old text under the
reused key, so the new inline default never reached the dialog. The copy gets its own key, the
stale entry is dropped from both catalogs, and a test reads the shipped catalog.
* fix(wsl): leave a removed host's workspace, keep its removal warning up, and prove removal in e2e
After removal the active workspace is left when it lived on that host, so nothing keeps calling a
host that no longer exists, and a warning about what still runs in the distro stays until
dismissed. The e2e now checks that removal ends the host's terminals on the server standing in
for orcad, shows no warning, leaves the workspace, and starts nothing for the host afterwards.
* test(wsl): read the shipped removal copy and the fake's connect log without reflection
* fix(wsl): say a WSL host is turned off in its terminal instead of 'Reconnecting'
With the experiment off (or without server files) nothing retries, so a WSL host's terminal now
says so, with Turn on, rather than claiming Orca is reconnecting.
* test(wsl): reproduce project-removal survivors and orphan terminals in e2e
The headless server standing in for orcad now carries a project whose terminal must end when the
project is removed (and leave the host's own catalog), and an orphan terminal whose project the
host already forgot, which removing the host must still end. Both checks fail without the fixes.
* fix(wsl): drop the IPC wrapper from file-explorer and editor load errors; offer Turn on when WSL hosts are off
The file explorer showed "Error invoking remote method 'runtimeEnvironments:call': Error: ..." for a
WSL workspace whose host was turned off. Explorer read errors now go through the shared IPC-error
unwrapping, and when the workspace's WSL host is turned off the empty pane shows the same turned-off
copy and Turn on button as the terminal banner instead of a read error and a Retry that cannot work.
The editor's file-load error view drops the same wrapper at render time (stored text is unchanged
for retry classifiers).
* test(wsl): open the internal gate for the WSL host e2e, and prove a closed gate shows nothing
The fake-transport e2e launches its desktop with ORCA_INTERNAL_WSL_HOSTS=1.
A new case leaves the gate closed with a WSL host already saved and the
Experimental setting on: no WSL host API, list entry, host row, Add WSL
distro action, Experimental toggle or Settings section appears, the fake
never starts a distro, and the saved host stays on disk.
* test(wsl): count only live shells when proving a removed project's terminals ended
On Linux CI the hub keeps a pty-less tab record for the removed worktree after terminal.stop ended its shell, so counting every terminal.list row never reached zero.
* test(orcad-migration): pin the SSH manifest digest before the source abstraction
* feat(orcad-migration): capability-gated source.kind discriminator, SSH wire unchanged
* refactor(orcad-migration): MigrationSource abstraction with SSH relay as the only source; table-driven path-rewrite stage (identity for SSH)
* fix(orcad-migration): name the wire source type
* chore(orcad-migration): exhaustive path-kind switch; no test assertion
* test(ssh): run the relay migration source's SQLite test in the Node runtime project
* perf(orcad-migration): rename record keys in linear time and skip the walk for the identity
SSH exports run the path-rewrite stage on every stage and commit check; re-reading Object.keys per entry made a 10k-key record cost seconds on the main thread.
* feat(wsl): persist WSL host deployments in their own store and type orcad deployment links by kind
* chore(wsl): drop an unused link type export
* feat(wsl): add, persist and connect WSL distro hosts behind an experimental flag (WSL-via-orcad C7a)
Main-process wiring for WSL hosts: list registered WSL 2 distros, add one (C4 deploy and
launch through C6 liveness, then persist the paired environment and its C2 record), keep a
relaunch's new device token, connect a host only for the user or an open project or terminal,
adopt holders at startup, and remove a host (release its holder, drop both records). Refuses a
distro whose orcad this desktop already reaches as another server (R25).
* fix(wsl): read distro registrations through one reader that returns the canonical unbraced GUID
C4's registry reader returned the braced GUID while the C2 record only accepts it unbraced, and
C7 parsed the same output a second time. The one reader now yields lowercase unbraced ids and
each registration's WSL version; the host distro list uses it.
* fix(wsl): refuse to connect a WSL host while the experiment is off
Adding, connecting and background connects all stop while Experimental → WSL distros as hosts
is off; removing still works. The pre-deploy refusals move to their own module.
* fix(wsl): stop a connected WSL host's server when the host is removed
Remove ended the bridge and released the distro's holder but left orcad to exit with the
distro. A connected server is now ended right away, only in a distro already running.
* fix(wsl): import the orcad host errors from their new orcad/ home
* fix(wsl): refuse WSL hosts in a build without the orcad template, and say so up front
A build shipped without the orcad deployment template let the add dialog run and fail after
seconds with a raw internal error. Main now checks the template as SSH hosts do, refuses add
and connect (and skips lazy connects and holder adoption) before any deploy work, and exposes
the capability so the UI can show it.
* test(wsl): let the e2e WSL fake re-read its distro list, to register one mid-run
* fix(wsl): end a removed host's terminals and daemon, and stop WSL hosts when the experiment is off
Remove now ends the host's terminals through orcad's terminal RPC, waits for the terminal census
to prove none remain, closes the bridge, and runs the managed decommission SSH hosts use, which
stops orcad and retires its idle terminal daemon. What it could not end is reported back.
Turning Experimental → WSL distros as hosts off closes every connection and liveness watch,
releases the holders so distros can idle-stop, and keeps liveness from relaunching for visible
work; turning it on resumes only the hosts open work needs.
* fix(wsl): keep WSL hosts out of the servers a phone reaches through this desktop
They are this desktop's own distros, connected only for work open here; a phone listing or
routing to one would wake it behind the user's back.
* fix(wsl): end a removed host's terminals through its own route, and never respawn it
Removal reached orcad with the bridge's raw pairing offer rather than the routed environment and
saved pairing the app already uses for the host; on a real orcad ending terminals failed fast and
left them, the daemon and orcad running. It now calls by environment id through the same
transport as every other call to the host, and the e2e fake runs these real steps against a
headless server.
Removal also stops tracking the host first (open work, connects, runtime calls), so nothing
reconnects it while it stops, and a spawn guard in the connector refuses any holder, deploy or
bridge for a distro whose host is gone (the respawn loop after removal). A WSL host that isn't
connected answers runtime calls with why, instead of failing as an unreachable network server.
* fix(wsl): suggest reconnecting a WSL host, not Tailscale, when it can't be reached
Unreachable-server hints are chosen by the owner host's kind: a WSL host is a distro on this
computer, so it points at its reconnect instead of the Tailscale advice for network servers.
* test(wsl): parse the terminal list removal reads, and log the e2e fake's connects to a file
* fix(wsl): close a removed host's terminals by handle and report stopped only on proof
Removal stopped terminals per workspace, so one orphan whose project was gone threw
selector_not_found and aborted before orcad was stopped. It now closes each terminal by handle,
keeps going past failures and names them, and still asks orcad to stop.
It reports stopped only when the census shows no session left, the daemon retired, and the
orcad the distro records as active is the one this computer was connected to; a re-add that
adopted an orcad the record does not name is reported instead of claimed stopped.
* feat(wsl): close WSL hosts behind an internal-only gate in every build
WSL distros as hosts must not be reachable by users until moving a project
between hosts is lossless and the feature matches today's WSL support.
ORCA_INTERNAL_WSL_HOSTS=1 opens the gate; otherwise main creates no WSL host
service (no holder adoption, wsl.exe spawn or reconnect), refuses add,
connect, remove and every runtime call to a WSL host, keeps persisted WSL
hosts out of the server list, startup status probes and AI vault scans while
leaving them on disk, and the preload exposes no WSL host API.
* fix(wsl): keep SSH and paired servers listed whatever the WSL deployment file holds
An unreadable WSL record no longer hides an SSH-linked server, and with the internal gate closed a corrupt WSL file is warned about instead of failing the whole registry.
* fix(wsl): let an unreadable WSL record hide only a loopback pairing
A WSL host always pairs over a loopback endpoint, so a stale record can no longer remove a remote paired server from the list.
* fix(wsl): ignore unreadable WSL deployment state while the internal gate is closed
A newer or stale record could otherwise hide a re-paired loopback server for users who never open the gate.
* feat(wsl): account env for agent tabs on a WSL orcad host (WSL-via-orcad C9b)
terminal.createAgentSession and an explicit terminal.ensureAgentSession (new
agent tabs, restored and sleeping agents) accept an optional accountEnv, gated
by the new agent-session.account-env.v1 capability because both schemas are
strict. The desktop shim attaches it only for an advertising WSL orcad, reading
its capabilities from status.get; an older host keeps its default account and
the desktop shows a one-time notice per distro. accountEnv stays outside the
create replay fingerprint.
* test(wsl): name the C9b runtime store stub's cast
* test(wsl): expect the WSL account notice listener in the app-lifetime IPC lifecycle
* feat(wsl): accounts shim for agents on a WSL orcad host (WSL-via-orcad C9)
The desktop prepares the selected Claude/Codex account's in-distro home with the
existing WSL account code and sends it as env on terminal.create /
session.tabs.createTerminal, or as agent.launch's new optional accountEnv.
Structured sessions keep their host-derived home and warn. Launch-time CLI
resolution inside a WSL guest skips /mnt/<drive> shims.
* refactor(wsl): track WSL host environments beside their registry
* ci(wsl): real WSL2 lane for orcad deploy, bridge, liveness and holder (WSL-via-orcad C10)
* ci(wsl): upload the template's dot-directories; save the rootfs cache on failure
* ci(wsl): update the runner's WSL to 3.0.1+ before asserting WSLInterop survives
* test(wsl): let a released holder settle before asserting it exited
* ci(wsl): install a pinned, cached WSL 3.0.1 MSI instead of wsl --update
* test(wsl): keep the lane's survival seconds typed in its assertion message
* test(e2e): read the parked-reveal decoy grid only after it settles at the new fit
* test(e2e): name the settle wait and give it the file's reveal timeout
* feat(wsl): liveness, failure classification and recovery for the WSL orcad connection (WSL-via-orcad C6)
* fix(wsl): a user connect supersedes liveness recovery; report a stop once
* chore(wsl): exhaustive terminal verdict switch; typed test state
* fix(linux): unpack the AppImage copy in the background so the first daemon runs from it
A validated AppImage now extracts its generation in the background, at the
lowest CPU and I/O priority and under the registration lock, at a launch that
finds no copy for its file and when an update finishes downloading (into the
namespace and generation key the installed file will have). The copy is staged
without publishing the CLI launcher. A daemon about to be launched while this
launch is unpacking waits up to 8s for it, then runs from the copy instead of a
FUSE mount it would outlive. A full cache, noexec cache, untrusted download or
failure is logged and the launch keeps today's fallback.
* fix(linux): drop idle copies of earlier releases at launch
Staging never prunes, so a desktop that never registers the CLI would keep
every release's unpacked copy. At launch, once this release has its copy, idle
sibling generations in its namespace are pruned under the registration lock;
leases, residency and the installed CLI target still protect anything in use.
* fix(linux): leave serve's own first-start unpack and its progress in place
Serve already unpacks the copy itself with one deadline and progress on its
output (D3-D2), which the packaged shutdown oracle checks. Launch-time
pre-extraction now runs for the desktop only; serve still gets the
update-downloaded staging.
---------
Co-authored-by: m4air <m4air@Mac.localdomain>
* [CI] Retire the weak local title-corpus comparison
* [CI] Retire tests for seven unconsumed helper modules
* [CI] Reset the Windows launch resolver graph once per group
When the daemon gets SIGTERM or SIGINT it now durably marks its restore
marker as stopped before the async teardown starts. A systemd scope stop
signals the daemon and its AppImage mount helper together, so the teardown
can die with SIGBUS before it writes clean-exit; that left the marker at
running and the next start paused automatic restores for a crash that never
happened. A real crash (no signal handler run) still leaves running and still
pauses in the same boot.
Co-authored-by: m4air <m4air@Mac.localdomain>
* perf(wsl): skip re-hashing unchanged orcad artifacts and the guest Node runtime on the wake path
materializeOrcadArtifact now reads each template file once (manifest check and slot identity
in one pass) and remembers the digests in <cacheRoot>/<target>/.verified-sources.json keyed
by each file's device, inode, size, mtime and ctime; any change re-hashes. WSL deploys probe
the guest runtime against a recorded stat signature beside .verified, hashing only on
mismatch. SSH probe commands are unchanged.
* fix(orcad): trust the artifact verification record only on the WSL wake path
SSH deploy and local serve hashed every launch before; a record anyone
with userData access could edit must not let them skip that. Only the
WSL deploy opts in, and untrusted callers neither read nor write it.
* fix(orcad): reject a browser named like a slot file; skip signatures when untrusted
A browser digest keyed like orcad.js replaced the common digest it must also match; callers that keep no record no longer stat every source.
* fix(terminal-daemon): status reports live, exited or unverifiable, and the process ceiling
`orca terminal-daemon status` printed 'Terminal daemon: OK.' whenever no severed daemon was
recorded, including the ~60 s after a kill -9 before the next terminal respawned it. The host
now probes the daemon (health check, then its pid record with Linux start-time identity) and
returns an additive `daemon` report; the CLI prints the verdict and, for a live daemon, the
terminal process ceiling, which no CLI command showed under Electron serve.
Fixes BUG-5 and BUG-7 from the real-host Linux test run.
* fix(terminal-daemon): load the ceiling description from shared so the packaged CLI can resolve it
The packaged CLI cannot load out/main/daemon modules: electron-vite replaces out/main after the
CLI build. The ceiling state and its description move to src/shared, which the CLI already ships.
* fix(terminal-daemon): a pid the signal proved live is never exited because procfs hides it
* fix(terminal-daemon): load the liveness probe lazily so the RPC table stays light
Importing the probe eagerly pulled the daemon provider graph into every module that loads the
runtime RPC method table, which broke the cross-version terminal wire harness.
---------
Co-authored-by: m4air <m4air@Mac.localdomain>
* fix(linux): a supervised AppImage serve restarts from a replaced AppImage
Swapping the AppImage file never reached a supervised serve: it runs the copy it unpacked, and
even a full service restart went back through the stable launcher to that same copy. The
extraction cache now records which AppImage each namespace came from. Before each start the
supervisor compares that file's identity with the build it runs; once a settled new file is
there and runs on this host, it execs the new AppImage with its own arguments (same pid), whose
serve unpacks the new build and re-points the stable launcher.
Fixes BUG-4 from the real-host Linux test run.
* fix(linux): try a replaced AppImage once, and let it past an old build's give-up
- A give-up recorded against the old build no longer stops the supervisor before it checks
for a replaced AppImage.
- The supervisor records the AppImage identity it is about to exec in the profile. If that
build dies before its own crash brake, later starts of the old build keep running it
instead of exec'ing the same broken file until systemd's start limit disables the service.
A supervisor running that build, a further replacement, or --force clears the record.
* test(cli): compare the namespace listing order-independently
---------
Co-authored-by: m4air <m4air@Mac.localdomain>
Since a direct `Orca.AppImage serve --supervise` now runs the CLI supervisor (#28227), the
supervisor's relocation re-exec ran from the AppImage mount. An exec from node mode closes every
inherited descriptor, including the AppImage runtime's keepalive pipe, so the runtime unmounted
the AppImage during the exec and the new image failed with 'libffmpeg.so: cannot open shared
object file' (exit 127) on every start. Relocation exists so an update cannot swap the app under
a running supervisor; an AppImage's mount cannot change during the run, so it stays in place.
Co-authored-by: m4air <m4air@Mac.localdomain>
#28295 and #28297 each removed one of the two duplicate entries, leaving none, so shard 5 fails
again (expected 0, got 12). Re-add exactly one.
Co-authored-by: m4air <m4air@Mac.localdomain>
On a validated AppImage the desktop's daemon launch first runs <generation>/resources/node-runtime/bin/node
on the generation's daemon-entry.js when the extracted copy for the file at $APPIMAGE is complete, carries
the bundled Node and the entry, and is the running release. It then falls back to the AppImage's own mount
and the current mount, logging every fallback. The copy rung records $APPIMAGE/<entry> like the own-mount
rung, so either daemon is adopted. Generation leases recognize the bundled Node. macOS, Windows, deb and
rpm never reach it.
Co-authored-by: m4air <m4air@Mac.localdomain>
* perf(wsl): run stdin-free probes over pooled wsl.exe command loops; cache orcad's compiled bundle
The WSL wake path made ~37 sequential wsl.exe starts at ~180 ms each. Stdin-free
execCommand calls now go through a few long-lived wsl.exe sh loops (one command at a
time per loop, own session, stdin at EOF, base64-framed output). SSH has no such
method, so its commands and options are unchanged.
orcad enables Node's compile cache inside a client-managed slot, and hands it to the
profile preflight child, which compiles the same bundle.
* fix(orcad): share the slot compile cache with the preflight only in a managed slot
An unmanaged orcad started with its own NODE_COMPILE_CACHE kept that
location for its preflight before; never redirect it into the install dir.
#28192 and #28286 both recorded 'codex-model-capacity-fullscreen' with the same count, and the
duplicate object key fails typecheck (TS1117) on main.
Co-authored-by: m4air <m4air@Mac.localdomain>
#28231 added the codex-model-capacity-fullscreen PTY capture without an entry in the replay
test's known-divergence table, so every PR's test shard fails on main (expected 0, got 12).
The serializer is untouched; the divergence is the same restore limitation the table records.
Co-authored-by: m4air <m4air@Mac.localdomain>
* ci: run mobile runtime test groups on separate runners
* ci: use a lightweight runner for the mobile result gate
* ci: distribute Bun tests across both mobile runners
* ci: reuse mobile compiler state without adding runner queues
* test: verify eviction of source variants in one cache bucket
* test: baseline existing serializer limits in the capacity capture
* refactor(runtime): main alone starts setup and default tabs for an activated create
An activated CLI or paired create with a window open handed setup and default
tabs to the host window, and with navigation 'all' to paired clients too, so
both could run setup. Main now provisions them itself. Only when main tried to
start setup and failed does it hand setup to the host window, which retries it
where its output or spawn error is visible.
* fix(runtime): hand the window the whole create when the agent fails to start
Main provisioned setup and default tabs after a failed agent start and sent the
window only the startup. The window found tabs, seeded nothing, and the agent
never launched. A failed agent start now provisions nothing and hands startup,
setup and default tabs to the host window, as before this PR.
The ready toast added in #28238 read the Active Server setting through
getKnownExecutionHostIdForWorktree, which the focus-read ratchet rejects,
so every PR's static-analysis job failed on main.
* Show Codex model capacity failures in agent status
* Reduce Codex capacity detection work on terminal output
* Preserve Codex capacity notices across partial terminal frames
* fix(windows): restrict secure files in-process instead of spawning icacls
Every secure write on Windows spawned icacls.exe five times on the calling
thread (the Electron main thread for most stores): verify, /reset,
/inheritance:r /grant:r, verify, verify. Process creation under endpoint
security costs ~0.6 s each, so each write froze the UI for seconds (#26027).
The /reset + /inheritance:r pair is also what behavioural EDR scores as
ransomware tradecraft (#25583).
Add native/windows-security (@orca/windows-security): a small N-API addon
over advapi32 that reads and writes DACLs in-process. It exposes the token
user SID, a DACL read as SDDL, a DACL write (sync and on the thread pool),
a create-with-DACL file write, and an additive grant. Policy and
verification stay in TypeScript.
- writeSecureFile creates each temp file already carrying the protected
user/SYSTEM/Administrators DACL (CreateFileW + SECURITY_ATTRIBUTES), reads
it back once, and renames it. No ACL is modified on the write path and no
process starts. Before: 5-7 spawns, 94-138 ms per write. After: 0 spawns,
~0.5 ms.
- A secure directory is verified once per process. It is rewritten only
when verification fails, on the thread pool, because the write walks
every descendant (~2 s on a 29k-entry userData).
- The verifier follows OpenSSH's rule: only the user, SYSTEM and
Administrators may be granted anything, and the path must be protected
(or, for a file, inherit from a secure parent). DACLs written by the
icacls-era hardening pass unchanged, so there is no migration.
- If the addon cannot load, the write still happens and reports
unrestricted, logged once. There is no icacls or PowerShell fallback.
- grantDirAcl, the startup userData grant, and the install-dir probe and
package repair use the same addon, so whoami.exe and icacls.exe are no
longer spawned anywhere at runtime.
- orcad's Windows slots ship the addon beside their bundles (built per
arch, PE-machine checked). Its data-root privacy check fails closed, so
without the addon a Windows orcad would not start.
Fixes#26027Fixes#25583
* refactor(secure-file): harden existing Windows files through the sync lane
The read path called restrictWindowsPathSync directly and recorded success
itself; the existing sync lane already does both. One call site fewer.
* refactor(windows-security): one load, one rule, no fallthrough (review #28135)
- The descriptor parser is anchored, so a conditional ACE (its body holds
parentheses) or anything else it cannot read fails closed instead of
verifying the ACEs before it.
- findSecureDaclViolation is one flat rule: protected, allow rules only for
the user, SYSTEM and Administrators (OpenSSH's ACE rule), and full
control for the user. The inherits-from-a-secure-parent exception is gone:
it accepted exactly what loosening the parent reaches.
- The addon and the user SID load once, behind one wrapper that adds the
extended-length prefix to every path and runs async DACL writes to one
path in order, so a startup grant cannot land after a re-lock and restore
the broader DACL.
- With the addon loaded, a failed born-restricted create throws with the
matching Node error code instead of falling back to a plain write.
- A grant leaves a NULL DACL untouched instead of turning 'everyone' into
'only this SID'. WriteFile runs once and fails on a short write.
- The artifact create-intent store writes through writeSecureFile and drops
its own temp writer, directory LRU and synchronous directory re-lock.
- Test seams the global fake makes redundant are gone.
* fix(packaging): fail a Windows build whose workspace addon binary is missing or foreign-arch
The @orca/windows-* workspace links exist on every host, so their presence
proved nothing: a missing or wrong-architecture .node only surfaced at
runtime, as unrestricted secure files or an orcad that refuses to start.
beforePack now reads each addon's PE machine against the target arch.
* refactor(windows-security): prune what the native ACL left dead (review #28135)
- Sync-spawn ratchet: hardenSecurePath and the sync-flag exemption it alone
used are gone, and the advice and baseline header no longer say secure
writers run icacls.
- The install-dir probe reads ACEs through parseSddlDacl, so an ACE it
cannot parse fails the whole reading closed instead of being skipped; AC
is a resolved alias there now.
- The packaging check reads the addon binary through node_modules, the copy
that ships.
- No winCode-to-errno table: nothing reads .code off a failed secure write.
- The load memo is not mutated; secure-path keeps its own log-once flag.
- grantInstallDirPackageAcl is a plain function, and secure-file restricts
a Windows directory and a POSIX path directly instead of through one
two-platform helper.
* fix(windows-security): resolve LA in real-ACL tests; tear a file whose close fails
- The GitHub Windows runner is the built-in Administrator (RID 500), whose
SID reads back as LA. Production already resolves it through
localDomainSidOf; the win32 test helpers parsed without it. One shared
readDaclOf resolves aliases for every real-ACL suite, and the verifier's
unit test covers an LA-aliased directory and LA as a stranger.
- createFileWithDacl treats a failed CloseHandle like a failed write (lost
data on a network volume) and deletes the torn file.
- Drop a duplicate test reset and a typo.
* fix(windows-security): load only from inside the app; read DACLs as structure (review #28135)
- Loader: orcad's staged binary first, then the workspace package only if
its real path is inside the app (process.resourcesPath, or this
checkout). A bare specifier otherwise walks every parent up to
C:\node_modules, which any local user may create, and orcad holds
credentials.
- The addon reads a DACL as { protected, aces: [{ type, flags, mask, sid }] }
via GetAce, with a SID only for plain allow/deny ACEs and null for a NULL
DACL. findSecureDaclViolation and the install-dir probe check that
structure, so conditional, callback and object ACEs fail closed. The SDDL
parser, its alias table, LA/localDomainSid resolution and getDaclSddl are
gone; SDDL is only the write format.
- Install-dir tree pass: files flagless as before, but a directory only when
its own DACL is protected (inheritably). Re-granting inheriting
directories re-propagated each subtree (3.2 s for 1,093 directories). The
root is no longer granted, or counted, twice. The two passes are plain
functions behind a { root, tree } seam.
- One namespacing (path.win32.toNamespacedPath) at the addon boundary, also
the per-path ordering key; the comment states the real sync-grant
invariant.
- restrictWindowsDirectory(dir): Promise<boolean> replaces the callback
lane; the reporter is guarded once, so the 'settle' stage is gone.
- Sync-spawn ratchet: the secure writers no longer spawn or block, so they
leave the gate (109 -> 61 uses).
- Errors name the Win32 call and code only; a failed
GetSecurityDescriptorControl fails the write instead of defaulting to
unprotected.
* test(secure-file): assert the durable POSIX write's outcome, not the old chmod order
writeSecureFile now chmods the temp file to 0o600 before fsyncing it, so
the fsync also covers the mode change and the file is owner-writable when
opened. Assert what matters under umask 0o200: the write succeeds, the
temp file is fsynced read-only, and the published file is exactly 0o600.
* fix(windows-security): namespace paths with the host's toNamespacedPath
path.win32.toNamespacedPath rewrote POSIX temp paths (/tmp/x -> \tmp\x) in the
Linux runs of the Windows-logic unit tests, so the fake addon wrote to the wrong
place. The addon only runs on Windows, where the host function is the win32 one;
elsewhere it is the identity. The long-path assertion runs on Windows only.
* test(windows-security): assert the install-dir long-path prefix on Windows only
The prefix comes from the host's toNamespacedPath, which is the identity off
Windows; the Linux run of this suite expected the win32 escape.
* fix(windows-user-data-acl): grant the root even when its children cannot be listed (review #28135)
A readdir failure on userData aborted the whole grant, so the root grant
that could repair a DACL refusing the listing was never attempted. The
listing failure is now recorded, the root is still granted, and the
marker stays withheld so the next launch retries.
* test(windows-security): wait for relock propagation, accept Entra ID SIDs (review #28135)
- The large-relock test waited only for the parent to turn protected,
which happens before propagation reaches the children, then read a child.
It now waits on the parent and both ends of the child range.
- The token-user test rejected S-1-12-1 (Entra ID) accounts, failing the
suite on Entra-joined dev machines.
---------
Co-authored-by: Orca Worker <orca-worker@localhost>
* fix(terminal): preserve preparation and discovery for delayed agent launches
* refactor(agents): centralize launch intent recognition
* refactor(agents): keep launch intent in one module apart from process recognition
Move recognizeAgentLaunchCommand next to the sleep-prefix scanner so the strict
process-table matcher is untouched, name the module away from the agent-launch
funnel files, share one executable-name normalizer, and colocate the
preparation tests with their modules.
A supervised serve started through the stable launcher runs from one extracted generation with
no $APPIMAGE. Its CLI registration then refused Orca's own ~/.local/bin/orca-ide (a link to
the stable launcher) as a non-Orca command, and pointed the bare `orca` dispatcher at the
generation's launcher, which a later update prunes. Both now resolve to the stable launcher.
Fixes BUG-8 from the real-host Linux test run.
Co-authored-by: m4air <m4air@Mac.localdomain>
* fix(terminal): name a new tab's first pane so its saved layout is never empty
createTab now always mints the first leaf id, so the window never saves a
terminal tab with an empty layout root. With no empty roots coming from the
window, main's save-time swap that kept its own root over an empty window
root (from #1909) has nothing left to guard, and is deleted.
* test(persistence): an empty-root layout from an old session still loads and gets its pane
* fix(persistence): name an old session's empty layout root once at load
A session saved before tabs named their first pane can hold an unmounted tab
with an empty root. If main bound a terminal there, the window's next save of
that tab dropped the binding once the save-time swap was gone. Name the pane at
load instead (the same pane the window would mount), so no save carries an
empty root. The rootless-pane resolver moves to shared so main and the window
pick the same pane.
* test(persistence): name the bind-then-stale-write order the load repair covers
* fix(persistence): name an old empty tab's pane with the stable headless leaf id
The load repair fell back to a random id, so an unsaved headless serve gave a
phone a new tab::leaf id on every restart. Use deriveHeadlessLegacyTerminalLeafId,
which phones already saw for these tabs. Also import the rootless resolver from
shared directly and return the loaded state unchanged when nothing needs repair.
* fix(linux): run a direct AppImage serve through the CLI so it keeps its profile and supervisor
A direct `Orca.AppImage serve` stayed in the Electron process and skipped the CLI's serve
front end, so it ignored ORCA_USER_DATA_PATH and silently dropped --supervise. It now execs
into the bundled CLI (same pid, so service managers and signals still reach it), translating a
leading --user-data-dir into ORCA_USER_DATA_PATH. Also lists terminal-daemon and reference as
CLI commands so a direct binary launch of them reaches the CLI.
Fixes BUG-1, BUG-3 and BUG-10 from the real-host Linux test run.
* fix(linux): drop desktop switches anywhere in a direct serve launch
* fix(linux): keep a direct serve's --no-sandbox for the Electron server the CLI starts
---------
Co-authored-by: m4air <m4air@Mac.localdomain>
* feat(wsl): deploy and launch orcad inside a WSL distro (WSL-via-orcad C4)
* fix(wsl): keep the install-dir fallback on the PATH detection searched, so a /mnt/c shim cannot re-enter
* fix(wsl): call the install-dir resolver exactly as before when no PATH is given
* fix(wsl): import orcad artifacts from their new orcad/ home
* fix(wsl): adopt an orcad serving in the distro that the activation record does not name
* fix(wsl): filter /mnt/<drive> PATH entries only in an orcad launched as a WSL host
SSH-managed orcad inside a distro and Orca under WSLg kept detecting
drive-mounted CLIs before this stack; the setsid launch now marks itself
with ORCA_WSL_ORCAD_HOST=1 and only that process filters.
* fix(wsl): keep the install-dir fallback on the process PATH outside a WSL host orcad
On Windows the preflight env merges the persisted registry Path, which
the fallback never searched before; only the marked WSL host runtime
now passes a filtered PATH.
- A headless serve with no X display told the user to run `orca-ide serve`, which they just
ran. Serve now gets its own message: it needs an X display, Wayland alone is not enough, and
which X virtual framebuffer package to install (now including Arch's xorg-server-xvfb).
- The supervisor's restart line read 'Orca serve Orca serve exited via SIGKILL.; restarting'.
- The supervisor and 'serve service status' gave different advice after giving up; both now
give the same, correct advice.
Fixes BUG-2 and BUG-9 from the real-host Linux test run.
Co-authored-by: m4air <m4air@Mac.localdomain>
* test(daemon): a Node-hosted launcher keeps a busy Electron-hosted daemon and its terminals
Runs the desktop daemon launcher under Electron (ELECTRON_RUN_AS_NODE), opens a terminal on the
daemon it starts, then relaunches the launcher on plain Node at the same and at a newer version.
The busy Electron daemon is adopted as a normal target with its shell intact, and only after it
retires on its own does a Node daemon take the endpoint. Linux only.
* test(daemon): stop every launcher a failed adoption test leaves running
Share the launcher window and session harness through the fixture, track each launcher child,
kill it if its startup line never arrives, bound quit, and close any left open in afterEach.
---------
Co-authored-by: m4air <m4air@Mac.localdomain>