Commit Graph
2923 Commits
Author SHA1 Message Date
Jinwoo Hong 3a011e48d1 fix(native-chat): load long chats on the phone by keeping every update within its limit (#28203)
* 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
2026-10-13 13:39:34 -04:00
Jinwoo Hong 8471b3a5c2 fix(layout): restore the saved active group as a window does (#28174) 2026-10-13 13:04:55 -04:00
Neil 962c338b67 feat(wsl): Windows path actions for WSL hosts through one owner-routed capability (WSL-via-orcad C8) (#27955)
* 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.
2026-10-13 10:00:55 -07:00
Neil 9f2abd5e70 feat(wsl): Add WSL distro host UI and status (WSL-via-orcad C7b, experimental) (#27934)
* 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.
2026-10-13 09:37:50 -07:00
Neil 7ed23199a5 refactor(orcad-migration): migration source abstraction, capability-gated source.kind, path-rewrite stage (WSL-via-orcad D0) (#27790)
* 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.
2026-10-13 09:23:59 -07:00
Neil a65fdc887f feat(wsl): WSL distro hosts, main-process wiring and persistence (WSL-via-orcad C7a, experimental) (#27933)
* 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.
2026-10-13 08:18:08 -07:00
Neil 5e9489c0c0 feat(wsl): account env for agent tabs on a WSL orcad host (WSL-via-orcad C9b) (#27940)
* 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
2026-10-13 07:42:38 -07:00
Neil 41e6ea530a feat(wsl): accounts shim for agents on a WSL orcad host (WSL-via-orcad C9) (#27874)
* 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
2026-10-13 07:23:53 -07:00
Neil 2bb02f062d [CI] Reuse Windows resolver imports and retire unused helper tests (#28296)
* [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
2026-10-13 06:07:28 -07:00
OrcaWinandm4air 5e09488911 fix(terminal-daemon): status says live, exited or unverifiable, and shows the process ceiling (#28239)
* 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>
2026-10-13 05:21:49 -07:00
OrcaWinandm4air b724e3a24d fix(linux): a supervised AppImage serve follows a replaced AppImage (#28244)
* 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>
2026-10-13 05:12:18 -07:00
Neil e09d732d17 perf(wsl): pooled wsl.exe command loops and orcad compile cache on the WSL wake path (C4 follow-up) (#27923)
* 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.
2026-10-13 04:27:48 -07:00
Neil 9e52645e6c Show Codex model capacity failures as red agent status (#28231)
* Show Codex model capacity failures in agent status

* Reduce Codex capacity detection work on terminal output

* Preserve Codex capacity notices across partial terminal frames
2026-10-13 01:58:00 -07:00
OrcaWinandOrca Worker 4559c250b3 fix(windows): restrict secure files in-process instead of spawning icacls (#28135)
* 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 #26027
Fixes #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>
2026-10-13 01:56:55 -07:00
Neil 7246a0e269 Fix delayed agent launches such as sleep 30 && claude (#28205)
* 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.
2026-10-13 01:55:18 -07:00
Jinwoo Hong aceebea0a4 fix(terminal): name a new tab's first pane so its saved layout is never empty (#28168)
* 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.
2026-10-13 04:48:20 -04:00
OrcaWinandm4air 5929350944 fix(linux): direct AppImage serve honours the chosen profile and --supervise (#28227)
* 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>
2026-10-13 01:45:54 -07:00
Neil 8cd5bb2a74 feat(wsl): deploy and launch orcad inside a WSL distro (WSL-via-orcad C4) (#27819)
* 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.
2026-10-13 01:29:19 -07:00
OrcaWinandm4air 49a1129aa6 build(linux): carry the pinned Node inside the AppImage so serve's slot works offline (#28204)
* build(linux): carry the pinned Node inside the AppImage so orca serve's slot works offline

Linux packaging places the pinned glibc Node at resources/node-runtime/bin/node before the
glibc-floor gate, proves it serves one PTY with the packaged daemon entry, and deb/rpm exclude
it through fpm. orca serve's orcad slot copies that Node (hash-checked) instead of downloading.

* test(serve): type the bundled Node fixture

---------

Co-authored-by: m4air <m4air@Mac.localdomain>
2026-10-13 00:39:39 -07:00
Brennan Benson e2751d4318 After a restart, an agent tab asks the terminal host what is running, never "Couldn't confirm the agent started" (#28043)
* fix(agent-launch): after a restart a launch pane is an ordinary pane, never "couldn't confirm"

A launch pane's spawn read the launch record (and the tab's saved outcome) when no launch was
running for it, so a row left pending by a quit showed "Couldn't confirm the agent started." The
record never observes the process; the terminal host does. With no launch running in this process
the pane now spawns exactly like any tab: a live agent reattaches, an exited or absent one gives a
fresh shell, and a gone daemon shows the normal unavailable state.

- the running launch tells its pane how it ended in memory (finish carries the verdict); only a
  definite failure says "couldn't start", an unknown outcome leaves an ordinary pane
- deleted: agentLaunchPaneVerdictFromRecord, listAgentSessionOperationRowsOwningPane, the
  `unconfirmed` verdict and its notice string, the tab's persisted launch pane, ownedPane writes
- rows written by an older build that carry ownedPane are ignored; they expire with the row

* test(agent-launch): the restart pane test joins the attach suite it shares mocks with

* fix(agent-launch): a launch pane whose agent couldn't start keeps its notice for the session and never spawns a shell under it; a reload saves only a pending launch

* test(agent-launch): drop assertions on code that is gone, and the taking-back case the IPC suite already pins

* fix(agent-launch): narrow the finished-pane check, and test the saved session's launch pane with a typed tab

* fix(agent-launch): a launch whose outcome is unknown still says "Couldn't confirm the agent started" for the session, and attaches its agent if it turns up

* fix(agent-launch): main keeps a failed or unknown launch's answer for its pane until it quits, so the pane never spawns a shell under its notice; the window's spawn gate goes

* fix(agent-launch): a failure kept for a pane goes when the pane is closed during its launch, moved to another tab, or its workspace is removed; comments say what an unknown outcome and a reload now do
2026-10-13 00:31:12 -07:00
OrcaWinandm4air 8e4e7be869 feat(daemon): pace automatic terminal restores on the desktop too (#28202)
* feat(daemon): pace automatic terminal restores on the desktop too

The desktop daemon now enforces the restore budget that orca serve and orcad
already enforce: the visible pane first, at most 3 automatic restores at once
(1/s), at most 8 live per machine, and the rest stay sleeping until opened.
ORCA_TERMINAL_SPAWN_ADMISSION=observe rolls newly launched daemons back to
count-only.

* fix(daemon): never put a user's own resume back to sleep; keep every refused old-client restore

A user-resume past the per-client allowance is still paced as automatic work
but is no longer deferred for a pause, the live cap or its deadline, so a visible
pane is never put back to sleep in front of the user. The legacy restore record
holds as many refusals as the host could run.

---------

Co-authored-by: m4air <m4air@Mac.localdomain>
2026-10-13 00:18:08 -07:00
Neilandm4air 687190e9f1 fix(ssh): let existing profiles migrate without losing client history (#28141)
* fix(ssh): migrate released profiles with retained client state

* fix(ssh): reuse the shared adoption rule when the local and host copies overlap

The host copy replaces the older local one only where it holds tabs, the
catalog places the workspace on that host, no other host claims it, no
worker fence or close record disagrees, and local drafts are kept. Elsewhere
both copies export and must agree, as on main.

---------

Co-authored-by: m4air <m4air@Mac.localdomain>
2026-10-13 00:04:25 -07:00
Brennan Benson f0ccfcc0d3 fix(native-chat): seed the chat permission default from Claude's and Codex's own arguments (#28100)
* fix(native-chat): seed the chat permission default from the Yolo/Manual toggle as it shows

The one-time copy into Settings -> Chat -> Permissions gave Full access only
when every agent's arguments were exactly the bypass flag, so a profile the
toggle shows as Yolo (agents set differently, or an extra flag) got Ask.
Follow the toggle: Manual seeds Ask, anything it shows as Yolo seeds Full access.

* fix(native-chat): seed the chat permission default from Claude's and Codex's own arguments

The Yolo/Manual toggle has no saved value: it summarises every agent's arguments
and shows Yolo for any mix, including after Manual when another agent keeps custom
arguments. Seeding from it would grant Full access to people who chose Manual.
Read what Claude and Codex chats checked before they had modes instead: Full
access when both launch with their bypass flag, otherwise Ask.
2026-10-12 23:47:37 -07:00
Brennan Benson 8be17ddf3a feat(chat): host writes one saved row per subagent (#27947)
* feat(chat): read one saved row per subagent

Adds the singular `subagent` journal block (one row per child, revised in
place, with a plain-text twin for older clients) and teaches every reader
to draw it: desktop and phone fold a turn's sibling rows into the existing
"Ran N subagents" row, the client and off-page rosters name children from
their own rows, the reopen pass settles a row a dead host left working,
and the phone and worker-transcript wires bound it. No producer writes the
block yet; that switch lands in the follow-up.

* feat(chat): host writes one saved row per subagent

Claude, Codex and Grok now only report child-work evidence. The host writes each
subagent's row from its child record, so the chat row, strip and sidebar read one
lifecycle. Deletes the shared subagent tracker, the per-agent rosters and their
caps. An accepted Stop settles only the run it was sent to, provisionally, until
that run reports its own ending. Work the host store has no room to record keeps
the running-tasks strip and its Stop instead of reading as idle.

* fix(chat): open subagent background flag, separate nested group ids, page window skips subagent rows

- background is an open boolean so a later build's value never fails a chat load
- a helper's own helpers fold under a groupId of their own, so the phone opens them apart
- each subagent's own row rides along with the page window instead of counting as conversation

* fix(chat): subagent row writer reads the live turn once, stop acks survive a failed batch

- the row writer reads the journal's live turn at most once per change and only for a
  subagent with no saved, queued or in-flight row; a revision keeps its first scope
- a background stop names each accepted task as its answer lands, so a stop-all batch that
  times out or fails part way still ends the runs it was answered for (Claude and Grok)
- the strip's untargeted Stop rule lives in the shared view (offersStopAll); both strips
  inline it again; incomplete tracking no longer holds commands or rewind
- clients admit a frame whose only change is incomplete tracking
- drop the Claude stop's dead publish; the real-CLI stop test asserts the run's own ending
- list the writer test with the real SQLite tests

* fix(chat): a late Grok report never revives an ended helper; Codex keeps announced helpers

- Grok: once a helper's run ended, a later progress or repeated spawn sends no live evidence,
  so its record and saved row stay ended after the next send retires the record; the test rig
  now retires finished records on each turn open, as the host does
- Codex: past the bound, only a child that ran and ended is evicted, so an announced helper
  that has not started keeps its record, row, Stop and parent
- cut: Codex parentTurnId (written, never read), the Claude identities' turn-end forwarding
  (call the corrections directly), the duplicate Grok outcome map
- update stale comments that described the deleted producers

* test(chat): Claude frames to saved row, kept Codex path and usage rules, agent ingestion exemption

- a captured background Claude helper keeps its saved row working past its spawn result and
  the parent turn's end, through the real adapter, host and child records, until its own end
- restore focused tests for the Codex root path, label trim and placeholder, usage before
  announcement and parent usage
- the per-session live limit test uses tasks: subagents are exempt from it

* fix(chat): incomplete tracking follows the refused children, and the strip's Stop reaches them

- the host remembers which children the store refused for room, by handle; each leaves when a
  later report of it is admitted or it reports its own end, so incomplete clears on a truthful
  signal; the provider session's end still clears all, and past 512 a flag holds until then
- a parent row the store refused counts every live child it reports as refused
- with work no record lists, an untargeted strip Stop while no turn runs ends the agent through
  the idle sweep's stop; under a running turn it leaves the agent alone
- the background Stop moves into its own module

* fix(chat): the incomplete Stop is offered for every agent and decides inside its step

- the strip offers the untargeted Stop for work no row lists whenever no turn runs, whatever
  untargeted stop the provider has (ACP agents, Codex before it proves terminal stops), since the
  host reaches it by ending the agent; never under a running turn
- the Stop reads whether work is unlisted and whether a turn runs inside its own step, after the
  listed stops land, so a wake-up turn opened meanwhile is never ended
- Codex executions past the bound evict the oldest ended child, else the oldest never started
- test the writer's unnamed-subagent label; qualify the writer's retry comment

* refactor(chat): the subagent row writer joins the views with their records

Drops the shared host-only row projection and its hook-server read: the writer's wiring reads
the views every surface reads and the records they come from, and takes residency and the
ending's basis from the record.

* refactor(chat): Grok subagent evidence reads its ending through the one outcome map

* refactor(chat): a subagent's group entry as one object

* fix(chat): a background Stop with nothing listed asks the agent for no task stops

So an agent with no stop route (an ACP agent the probe found none for) never refuses before the
Stop ends the work no row lists. Tests: the unlisted case is untargeted and reads unlisted work
inside the step; the repeat-press rig lists the task its Stops reach; reflow the writer header.

* test(chat): a nested helper's saved parent; describe how incomplete tracking clears

* fix(chat): a closed session claims no Stop; a helper's row follows its own running work

- a session close Orca proves is loss of contact, not a Stop the CLI accepted: Claude no longer
  ends what still ran as stopped, so the host settles it unknown and its row reads unverifiable,
  as before this stack; stop-acknowledged comes only from a Stop the CLI accepted
- a helper that finished while work it started still runs is written working, by the strip's
  own rule; once that work ends, or the session ends, it reads its own ending again
- the CLI starting a completed helper again under the same spawn call is the helper waking when
  its work ends: the decoder reports it as a restart, so its one row revises in place and ends
  with its real duration; after any other ending such a start is still read as late
- tests from a captured 2.1.280 frame order; item 4 Stops pin their stop-acknowledged basis

* refactor(chat): drop the unused Stop basis on a child's own ending

Only the acknowledged-Stop edge writes that basis now; reflow the decoder header.

* fix(chat): an older history page keeps a turn's subagent ordinals and row

A page now reaches back to the first sibling of each subagent group it holds part of, at most one more page of conversation, so loading the older page no longer renumbers same-named helpers or moves (and closes) their folded row.
2026-10-12 22:53:42 -07:00
NeilandClaude bcad291f35 fix(orcad): identify instances by kernel start ticks, not drifting wall-clock start time (#28125)
* fix(orcad): identify instances by kernel start ticks, not drifting wall-clock start time

On Linux the start time is derived from /proc/stat btime, which drifts (WSL2, NTP),
so a live orcad was read as a reused PID and its managed stop reported exited
without ever writing the request. The lock and stop request now carry optional
linuxStartTicks + bootId; readers prefer them, and a wall-clock mismatch on Linux
for records without them is unverifiable, never proof of exit.

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

* fix(orcad): match stop receipts without the incarnation an older client drops

A request recovered by an older client loses linuxStartTicks/bootId, which made
the newer host reject its own receipts and decision records as mismatched. Also
exercise the incarnation reader against a real Linux /proc.

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-10-12 18:21:06 -07:00
1104402f5e fix(browser): annotation card and outline follow the picked element (#27875)
* fix(browser): annotation card and outline follow the selected element

The grab overlay keeps re-measuring the picked element after the click
(scroll in any box, resize, element size changes), and the isolated
viewport bridge reports that live box so the pending card follows it,
including page zoom. The card's side pick uses its measured height.

* fix(browser): bind annotation card to selected element

* fix(browser): keep document annotation card on selection

* fix(ci): point the main sync-spawn ratchet at the shared ratchet library (#28054)

#27756's script imported baseline helpers from check-focus-read-ratchet.mjs
after #27788 had moved them into ratchet-baseline.mjs, so it crashed on start
and failed every PR that merges main. Use the library's helpers, and record the
two daemon-process launches from #27769 that the broken check never saw.

Co-authored-by: Orca Worker <orca-worker@localhost>

* fix(browser): follow selected element outer-box size

* refactor(chat): give the start-refusal resumable table its own module (#28085)

#27946 and #26684 together put agent-session-failure-words.ts at 304 lines,
over max-lines, which fails lint and the pre-commit hook on every merge of
main. The refusal-code table is standalone with one reader, so it moves out.

Co-authored-by: Orca Worker <orca-worker@localhost>

---------

Co-authored-by: OrcaWin <alpha-eng@stably.ai>
Co-authored-by: Orca Worker <orca-worker@localhost>
2026-10-12 16:22:05 -07:00
Jinwoo HongandClaude a1f8e6482b fix(layout): save terminal rows in the tab groups' order (#28066)
* fix(layout): save terminal rows in the tab groups' order

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

* test(e2e): read the paired retention heap after two collections

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

* test(e2e): fail the paired retention check when the heap cannot be read

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

* fix(layout): name the headless flow behind the row-id group fallback

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

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-10-12 18:55:25 -04:00
Brennan Benson 2233fd3669 feat(native-chat): each Claude/Codex chat gets its own permission mode (#26116)
Each native Claude and Codex chat now has its own permission mode (Ask for approval, Accept edits for Claude, Approve for me, Full access), shown and changed from a button right after "+" in the composer. A change takes effect before the next message: the agent restarts quietly and resumes the same conversation, never interrupting a reply or background work. New chats start from Settings → Chat → Permissions, which is seeded once from Agent Permissions. The mode is stored as an ordinary chat option, and the restart reuses the existing "replace the agent before the next message" step.
2026-10-12 15:36:45 -07:00
Brennan Benson 6440d3c520 fix(mobile): keep a not-sent message in place on the phone, using the desktop's logic (#27936)
* fix(native-chat): list not-sent rows at their journal places

The shared projection appended rows the host recorded but never delivered after every other row,
leaving the reader to sort them. A reader that draws the list as given showed an old not-sent
message under newer turns. Merge them in at their journal places, after the stop rows are built,
so the list is already in order. The desktop sorts by the same order, so what it draws is unchanged.

* refactor(native-chat): move the recorded-rejection line into shared words

The desktop's words for a message the host recorded and then rejected now live beside the other
rejection words, returning notice parts each client renders its own way, so the phone can say the
same thing. The desktop calls it with the same inputs; its words are unchanged.

* refactor(native-chat): one projection behaviour for rejected rows

Every reader now draws a message the host recorded and then rejected where it was sent, marked
not sent, so the rejectedInPlace option goes. The phone was its last off caller apart from the
host's conversation outline, which now leaves not-sent rows out itself; a test proves the outline
is the same as if the rejected message had never been sent.

* fix(mobile): show a not-sent message in place with the reason under it

The phone now draws a message the host recorded and then rejected where it was sent, as the desktop
does, with one small line under the bubble in the desktop's words. The line is built from the
journal the view already receives; when a start-failure or sign-in row already gives the reason,
it says only that the message was not sent. The load-earlier header moves to its own file to keep
the view under its line limit.

* fix(mobile): a send the host recorded is never handed back

When the send's own reply said the host recorded the message and then rejected it, the phone put
the text back in the composer with a banner, though the host and the desktop keep it in the chat.
A recorded send now settles like any other: its row draws it in place as not sent (or its kept
card holds it), and the composer stays empty. The banner's failure-fact plumbing, fed only by that
branch, goes with it; the transcript states the sign-in guidance once.

* fix(mobile): keep each structured send on the shared optimistic list

Once a not-sent row stays in the chat, matching a pending bubble by its text breaks: resending the
same words after a not-sent message never reached the count the bubble waited for, so a stray copy
stayed, and an acknowledgement lost on that resend showed a false "Delivery unconfirmed".

A structured send is now one record, kept by the structured session under the id it is sent with.
Three parts match the desktop's sender: the id, the drawing (the shared projection draws it at the
tail until the host's row under that id lands, in the same pass) and the settle rule (the host's own
record of it, a submission, a card, or a card's hand-off, drops it by the one rule the desktop now
shares). The rest stays the phone's own: the bubble shows once the host accepts the send, not from
the tap; a send whose answer was lost is held undrawn and says "Delivery unconfirmed" only if its
deadline passes first, instead of going back to the composer; records are kept whether or not a
chat view is open; and one send in flight is the composer's rule, not the sender's. Its photo is
kept under the row id it shares with the host's row, so it shows on the bubble, the landed row and
a not-sent row. The drafts' text matching, with its older queued-card matching, now serves only
terminal-backed chats, whose transcripts carry no ids.

* fix(native-chat): merge not-sent rows by the transcript order alone

The merge kept a rule for rows with no journal place that no caller reaches and that the desktop's
sort does not have; drop it, and say what merging after the stop rows actually keeps. A test pins
that the list the phone draws as given is already in the desktop's sorted order, with a moved stop
row, a retry run, a cut-turn notice, a held card and two not-sent rows.

* fix(mobile): draw the not-sent line as the desktop does

Red and capped to the bubble's width, like the desktop's line and the phone's own returned-card
caption, and only when there is a notice: the row had a second, unreachable source of words. A
send a Stop took back is no failure, so it no longer rebuilds the notices on every streaming frame.
2026-10-12 15:16:23 -07:00
Brennan Benson e83e40e258 Session History shows OpenCode, Grok, OMP and Pi chats as chats and refuses a second terminal writer (#27183)
* refactor(chat): answer transcript adoption and Session History from the agent registration

Claude and Codex's transcript importers and Session History ownership (row id,
resume-command grammar) move onto their runtime registrations as required-nullable
fields; every other agent writes null. agentSession.agents publishes the derived
transcriptAdoption/sessionHistory flags, and a client reads a host that omits them
(or lists nothing) as the old build's answer. Strict parity: only Claude and Codex
answer yes anywhere.

* refactor(chat): one Session History ownership index, binaries from row agents, fork offer from fork argv

Review round 1: the duplicate-adoption check and Session History ownership read one
index keyed by the registration's row id (an importer now requires sessionHistory);
resume-command binaries derive from rowAgents through the TUI agent config; the
Resume in New CLI offer asks getAgentForkArgv instead of naming Claude and Codex.
Strict parity: only Claude and Codex answer yes.

* feat(chat): Session History owns OpenCode, Grok, OMP and Pi native chats

An OpenCode, Grok, OMP or Pi native chat's conversation listed in Session
History as a plain terminal row, and resuming it ran the agent's CLI as a
second writer to the conversation the chat was still using. Each agent's
registration now fills `sessionHistory`, so those rows show as the chat
(its name, opening the chat) and Orca refuses a terminal resume of an owned
conversation, as it already did for Claude and Codex.

- OpenCode lists under `opencode` and `opencode2` (2.x) rows by its ses_ id;
  `-s/--session`, `-c/--continue` resume, `--fork` does not.
- Grok: `-r/--resume/--load` and `-c`; a non-UUID value names a title, so it
  is read as target-less; `--fork-session` does not resume.
- OMP: `-r/--resume/--session` take an id, any prefix of it, or the session
  file's path; bare `--resume` and `-c` are target-less.
- Pi: rows are listed by the session file name's id while the chat holds the
  file's path; `--session <id|prefix|path>`, `-c`, `-r` (picker).
- An optional `resumeTargetMatches` on the registration covers prefix and path
  targets; it defaults to exact equality.
- Each agent now reads every token naming its own binary, so another agent's
  name earlier in a command (`claude --model pi`, `cd ~/src/codex && claude
  --resume X`) no longer hides a resume. For Claude and Codex this only ever
  refuses more than before.
- A renamed OpenCode chat tab names its `opencode2` row too.

* fix(chat): show owned rows only to clients that open the chat, without a resume command

- The list and search projection now asks, per owning agent, whether the
  requesting client opens that agent's chat. Current mobile renders only Claude
  and Codex chats, so it no longer receives OpenCode/Grok/OMP/Pi rows owned by a
  chat (a tap there opened an empty terminal and showed agent_session_conflict).
  Claude and Codex keep their rule: any client that reads structured sessions.
- An owned search hit opens its chat, so it no longer carries a terminal resume
  command (all agents; `orca` CLI search stopped printing one for a chat).
- OpenCode/Grok/OMP/Pi flags match case-sensitively, as their CLIs read them,
  so `rg foo src/pi -C 3` is not `pi -c`. Claude and Codex still fold case.
- A test feeds every resume command Orca builds for an owned row of each agent
  (Session History, live-tab argv; posix, PowerShell, cmd) into the guard.

* fix(chat): keep the resume guard linear and skip chat-tab lookups with no move

- Each agent binary mention in a command now reads its arguments only up to
  that agent's next mention, which reads the rest, so every token is parsed
  once per agent. A 200 KB text naming agents throughout took 9.4 s on the
  host's main process; it now takes about 90 ms. Every refusal before still
  holds: a later mention still reads its own resume.
- Right-clicking a native chat tab looks up its Session History row only when
  the agent's CLI can fork a conversation, the one move a chat tab offers, so
  OpenCode, Grok, OMP and Pi chat tabs run no list or rescan for nothing.
  Claude and Codex are unchanged.

* fix(chat): read each shell command once in the resume guard

The guard tokenizes the text once and splits it into shell commands at
&&, ||, ;, |, & and newlines (a newline after a line-continuation mark stays
inside its command). In each command, each agent reads its arguments from its
first binary token to the command's end; a later token naming it is an
argument. A `claude` path inside a Claude command no longer ends that command,
so `--session-id <owned>` still cancels a fork after it, and a binary-named
path between a resume and its fork flag keeps the fork a fork. Binary names
are looked up once per token, which keeps a 16 MB send to about 2 s.

Against main's parser, a Claude/Codex command is now allowed only when main
refused it for a flag in a later shell command (`claude ; echo --resume X`).

* fix(chat): end a resume-guard command at a bare carriage return

A PTY's Enter, and many orca terminal send texts, end lines with \r. The guard
now treats \r and \r\n as line breaks like \n, including after a line
continuation mark, so a later line's resume is read as its own command.

* refactor(chat): drop the transcript-adoption and Session History wire flags

agentSession.agents no longer publishes transcriptAdoption or sessionHistory, so
there is no new field for older and newer peers to reconcile. Their only
consumers were renderer gates that could answer from what they already have:
"Resume in New Native Chat" eligibility goes back to main's rule (adopting a CLI
conversation is Claude/Codex-only, and the host still refuses any other agent from
its registration's transcriptImport). Removes the AgentSessionDeclaredCapabilities
split, the older-host decoder shim and the generic host-flag reader.

* refactor(chat): keep main's Claude/Codex rule for the CLI fork offer

"Resume in New CLI" and the chat-tab menu read the agent name again instead of
building a synthetic provider session to ask the fork builder, and the chat-tab
lookup loses its host Session History flag gate. Only Claude and Codex CLIs fork,
so only their chat tabs look up a row, exactly as on main: no new list requests.

* refactor(chat): one registration-aware ownership index, no registration union

- listStructuredProviderSessionOwnership reads each record's registration for its
  Session History row id; the callback parameter and its one-function wrapper
  module are gone (no import cycle: checked value and type imports).
- StructuredAgentCliConversations is a plain type: transcriptImport is nullable
  and sessionHistory is required, since every registered agent now owns its rows.
  The "importer needs history" union and its null checks go.
- A test pins that no two registrations claim the same Session History row agent,
  since the owner lookup takes the first match.

* fix(chat): read resume commands into words as a shell does

Separators glued to a word (`opencode -s id; echo`, `cd /x&&claude -r id`) now end the
command, and quotes are removed inside a word, so `--session="id"` reads as the id. Applies
to every agent, Claude and Codex included. The acp-and-pi allowed() helper now asserts.

* fix(chat): count an agent's name only where a shell would run it

terminal.send checks agent prompts too, and agentPrompt does not keep the text from a shell (it
falls back to a plain PTY write), so prompt text like "Fix the Pi --resume flag" was refused.
The resume check now reads each command's program: its first word past VAR=x prefixes, a path,
and env/exec/command/sudo/npx/bunx/pnpm dlx. Every command Orca's builders produce, Claude and
Codex included and with a user's command override, is still refused.

* refactor(chat): compare resume ids in any case; Claude uses the shared option reader

The default target match now ignores case (ids are UUIDs or ses_ ids), so Grok's override,
which only repeated it, is gone. Claude folds its flags and reads them with
resumeInvocationFromOptions instead of repeating the `=` handling.

* perf(chat): read terminal text in one linear pass

A repeated regex group overflowed the stack on a 16 MB word. A sticky-regex scanner reads words,
quoted runs and separators in one pass; an unclosed quote is searched for once per quote kind.

* refactor(chat): read a command's resume only when its program is an agent

* fix(chat): read an agent run under nohup, time or wsl as the command's program

* chore: restore pnpm-lock.yaml

* fix(chat): keep the CLI's own title on OpenCode, Grok, OMP and Pi chat rows

Session History shows these agents' chat conversations under the title their CLI saved, as it does
for their terminal conversations; the chat tab's name and renames stay on the tab. Claude and Codex
rows keep the chat's name, as on main.

* feat(chat): Resume in New CLI for Grok and Pi chats

A Grok or Pi chat's Session History row and tab menu now offer "Resume in New CLI", like Claude and
Codex: the CLI opens a copy of the conversation (grok --resume <id> --fork-session, pi --fork <file>)
and the chat keeps its own. OpenCode's interactive CLI and OMP have no way to open a copy, so their
chats keep only Resume.

* fix(chat): offer Resume in New CLI only when the copy command can be built

A Pi chat found by search on a remote Orca server has no session file path, so its copy command
can't be built; the row no longer offers the button there.

* fix(chat): refuse pi --session-id of a conversation a chat owns

`pi --session-id <id>` reopens that session when it exists, so it is a second writer like --session.
2026-10-12 15:14:01 -07:00
OrcaWinandOrca Worker 9c1d5d2fb7 perf(pty): check WSL working directories off the main thread (#27705)
* fix(wsl): start host-side wsl.exe probes through process-host so they never open a console

wsl --list, wsl --status, the WSL home and directory probes, and WSL UNC
deletes were started with node:child_process directly. The synchronous ones
omitted windowsHide, so each spawn from the GUI-subsystem main process
flashed a conhost window that could take keyboard focus. Route them through
@orca/process-host, which pins windowsHide, and keep the execFile-shaped exit
errors the availability cache classifies.

Refs #27390

* build(tc): list wsl-exe-process.ts in the web typecheck project

wsl-availability.ts is in the explicit web file list and now imports it.

* refactor(wsl): drop the unused sync availability probe; classify ProcessResult directly

No production code calls the sync isWslAvailable (the IPC and RPC paths
already assert they never do), so delete it and its sync guest-probe twin
instead of porting them. The async probe now reads the --status
ProcessResult itself: an exit code is definitive, a timeout or signal kill
is retryable, ENOENT on spawn is definitive. The WSL2-missing-kernel exit
codes live in one named set. Availability tests now drive the async probe.

* refactor(wsl): shrink wsl-exe-process to a spec builder and stdout reader

With the availability cache classifying ProcessResult itself, nothing reads
the execFile-shaped code/status error any more. wslExeFailure returns a
plain reason string; wslExeStdout throws it; the UNC delete folds it into
its own error. The home probe builds its argv with buildWslExecArgs.

* perf(pty): probe WSL working directories off the main thread

The in-process terminal launch path validated a \wsl.localhost cwd with a
synchronous wsl.exe probe, blocking the main process ~70 ms per launch
(300-530 ms when the distro is stopped, up to the 5 s timeout). The launch
now awaits the existing async probe and the synchronous one is removed.

Refs #20497

* fix(pty): let a canceled spawn abandon its pending WSL cwd probe

The local spawn awaited the WSL directory probe without its cancellation
signal, so a pane closed mid-probe waited up to the 5s wsl.exe timeout
before noticing. Pass the signal through validateWorkingDirectoryAsync and
report an abandoned probe as the spawn's own cancellation, as the daemon
spawn preflight does.

* test(wsl): mock process-host in the empty running-list case from main

* docs(wsl): the availability probe no longer blocks; drop the stale word

* build(ratchet): prune the wsl.exe sync launches this branch removed

* build(ratchet): prune the sync WSL cwd probe this branch moved off the main thread

---------

Co-authored-by: Orca Worker <orca-worker@localhost>
2026-10-12 14:53:09 -07:00
OrcaWinandOrca Worker 50cc50d33e refactor(chat): give the start-refusal resumable table its own module (#28085)
#27946 and #26684 together put agent-session-failure-words.ts at 304 lines,
over max-lines, which fails lint and the pre-commit hook on every merge of
main. The refusal-code table is standalone with one reader, so it moves out.

Co-authored-by: Orca Worker <orca-worker@localhost>
2026-10-12 14:43:03 -07:00
Brennan Benson c0e2b73395 feat(native-chat): require Codex 0.136.0 for chats (#26684)
* Require Codex 0.136.0 for native chats

* Add host-owned Codex install and update actions

* Bind Codex checks and maintenance to the selected installation

* Resolve Windows updater cleanup through system helpers

* Use one child environment for Codex checks and launch

* Expire Codex readiness and reconcile current host jobs

* Resolve Codex checks against the child command search path

* Restrict global npm repair to the selected Codex install layout

* Pin the host updater directory through Windows supervision

* Synchronize Codex maintenance fixtures on readiness

* Unify Codex notices and recover version-refused chats

* Route Codex recovery to the operation that remains

* Preserve Codex setup execution errors in the job log

* Preserve UTF-8 in Codex setup logs

* Remove unused Codex host installation detector

* Require host administration for Codex maintenance

* Show a missing Codex explanation once

* test(native-chat): give scripted Codex runtimes their installation fact; keep the registry out of the relay

Runtime tests that drive a scripted Codex app-server still ran the new
version check against whatever codex was on the machine's PATH: refused
on CI (no codex), and on a dev machine it ran the real codex --version.
A scripted transport now must state the Codex it stands for
(readCodexInstallation), asserted at install, and the tests get it from
one scriptedCodexTransport fixture.

The relay's Codex maintenance path reached @orca/windows-registry through
resolvePathEnvKey's re-export in windows-environment-path; import it from
its registry-free module instead.

Also update the catalog-probe test for the shared child environment.

* refactor(native-chat): drop the Update Codex action; keep Install for a missing Codex

A too-old Codex now gets only the plain message ("Codex X is too old for
chats. Update to 0.136.0 or newer.") with no button and no job. The
existing rechecks (window focus, the <=30 s expiry of a saved result)
retry the refused chat once the user has updated Codex themselves.

A missing Codex keeps the Install Codex button: `npm install -g
@openai/codex` with the npm on the chat's PATH, as a host-run job with
its log, then a recheck and recovery. A configured Command that is
missing gets the message only, since a global npm install would not
land at that path.

Removed: the `codex update` command and its 0.126 floor, manual
instructions, npm layout and launcher identity detection, the action
descriptor (install|update) and its titles, the legacy capability, and
the SSH relay maintenance path (structured chat never runs over SSH or
WSL), so the relay preflight handler is back to main's.

* refactor(native-chat): keep Install Codex in Settings only

When Codex isn't installed, main already hides Codex from the New tab menu,
so the in-chat Install button was almost never reachable. Settings → Agents
already shows "Codex isn't installed." with Install Codex.

- Chats and the new-workspace composer now only state "Codex isn't
  installed." (no button, no install job from the chat).
- The Install action, its busy spinner and the host-busy subscription move
  into the Settings Codex row, the only place that starts the job.
- The store no longer re-checks every other workspace context when a job
  finishes. A refused chat recovers through the same re-check a too-old
  Codex uses (window focus, or the <=30 s expiry of a saved result).
- The chat recovery key no longer includes the install job id.
- Tests: Settings drives the install; a new test proves a chat refused for
  a missing Codex recovers through focus after a Settings install, not
  through the job; the expiry recovery test covers missing and too old.

* refactor(codex): check chat compatibility when acquiring the child

* Restore workspace submit guard and simplify Codex maintenance

* Keep only the Codex chat minimum version
2026-10-12 14:30:27 -07:00
Brennan BensonandClaude 6af34a4d99 fix(chat): name the agent when it refuses a message (#27946)
* refactor: move provider-retry pieces into a sibling words file

Pure move of the six provider-retry translation pieces out of
agent-session-failure-words-text.ts, which sits at the 300-line limit.
No key or behaviour change.

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

* fix(chat): name the agent when it refuses a message

"The provider did not accept this message" becomes "{{agent}} didn't
accept this message" ("The agent" when the name is unknown), in English
and every locale. The providerRejected failure kind is unchanged.

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

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-10-12 14:29:38 -07:00
Brennan Benson fe1f0223af Native chat: read one saved row per subagent (part 1 of 2) (#27914)
* feat(chat): read one saved row per subagent

Adds the singular `subagent` journal block (one row per child, revised in
place, with a plain-text twin for older clients) and teaches every reader
to draw it: desktop and phone fold a turn's sibling rows into the existing
"Ran N subagents" row, the client and off-page rosters name children from
their own rows, the reopen pass settles a row a dead host left working,
and the phone and worker-transcript wires bound it. No producer writes the
block yet; that switch lands in the follow-up.

* fix(chat): open subagent background flag, separate nested group ids, page window skips subagent rows

- background is an open boolean so a later build's value never fails a chat load
- a helper's own helpers fold under a groupId of their own, so the phone opens them apart
- each subagent's own row rides along with the page window instead of counting as conversation

* refactor(chat): a subagent's group entry as one object
2026-10-12 14:21:25 -07:00
fde0fe35a7 fix(daemon): stop giving new terminals to a daemon whose AppImage files are gone (#28011)
* fix(daemon): stop giving new terminals to a daemon whose AppImage files are gone

On Linux AppImage, a terminal daemon can outlive the mount it runs from and
keep answering health checks until it SIGBUSes. Orca now reads the daemon's
/proc exe link and mount table at adoption: an idle severed daemon is
replaced; a busy one is kept for its sessions but quarantined, so fresh
terminals use the local provider and a healthy RPC never re-admits them.
Status and a verified restart are available over RPC and as
`orca terminal-daemon status|restart`.

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

* fix(daemon): lift the severed quarantine once a new daemon owns the endpoint

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

* fix(daemon): a severed-daemon restart counts fallback terminals and never stops a replacement

The restart re-checks the authenticated endpoint owner before any teardown
and again at shutdown, and never falls back to a pid kill when targeted.
Status counts the local fallback terminals the restart also ends.

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

* chore(daemon): merge duplicate import

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

* fix(runtime): classify the terminalDaemon RPC namespace as host-owned

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

---------

Co-authored-by: m4air <m4air@Mac.localdomain>
Co-authored-by: Claude <noreply@anthropic.com>
2026-10-12 08:50:25 -07:00
OrcaWinandm4air dedea9c76c fix(updater): clear a cancelled macOS install request before staging the next update (#28010)
* fix(updater): clear a cancelled macOS install request before staging the next update

* fix(updater): match only Orca's ShipIt and recheck owners right before clearing

---------

Co-authored-by: m4air <m4air@Mac.localdomain>
2026-10-12 08:28:58 -07:00
OrcaWinandm4air 7c13365f03 refactor(build): relay survivors named for what they are (#27962)
Co-authored-by: m4air <m4air@Mac.localdomain>
2026-10-12 04:09:18 -07:00
OrcaWinandm4air b368ebc7a6 refactor(ssh): delete relay-only GC, takeover and install remnants (#27770)
Co-authored-by: m4air <m4air@Mac.localdomain>
2026-10-12 03:23:17 -07:00
Neil cfbf3610d5 fix(linux): keep Wayland clipboard text current (#27772)
Publish Linux Wayland text copies with an independent clipboard owner, then clear Chromium's stale source only after publication succeeds. Use data control where available and GTK's clipboard bridge on GNOME. Keep macOS, Windows and X11 on their existing Electron writers.

Temporary workaround for https://github.com/electron/electron/issues/54645, with removal checks documented in the native README.

Verified full build, macOS arm64 packaging, Linux x64/arm64 library floors, clipboard/platform tests, real Wayland copy/paste and CI packaging.

Fixes #26917
Fixes #23229
2026-10-12 02:31:03 -07:00
OrcaWinandm4air 822bd9f972 fix(ssh): keep 1.4.224's running terminals working after the upgrade (legacy relay bridge) (#27787)
* wip: legacy relay bridge client

* Revert "fix(ssh): end an older Orca's relay terminals from the connect refusal (STA-10166) (#27748)"

This reverts commit d4a55f583e.

* feat(ssh): bridge terminals an older relay still runs into their old tabs

* test(ssh): cover the legacy relay bridge client, target bridge, transport and gate

* test(ssh): type the bridge test fakes without assertions

* test(e2e): a 1.4.224 relay's live terminal survives the upgrade on a Docker SSH host

* ci(e2e): route legacy relay bridge changes to its Docker spec

* fix(ssh): bridge delta moves too, and never read a non-exhaustive relay listing as exit

* fix(ssh): keep retrying a bridge for panes that mount before connect; end panes proven gone

* fix(ssh): bridge 1.4.224's detached leases through export preflight; publish every leased pane before the first look

* test(e2e): build the released relay out of process so Playwright never loads ESM build tooling

* fix(renderer): drop the recovery hook main removed from PtyTransport

* test(e2e): show the converted worktree before reading the bridged tab

* test(e2e): reconnect and wait for the converted host before opening the bridged tab

* fix(renderer): match bridged leases to tabs mirrored from the managed server

* test(e2e): find the mirrored relay-era tab under any worktree key

* test(e2e): open the converted worktree before looking for its mirrored tab

* test(e2e): a retired lease is terminated or pruned

* fix(ssh): back off attaches a listed terminal refuses; one pane per bridged shell

---------

Co-authored-by: m4air <m4air@Mac.localdomain>
2026-10-12 02:24:12 -07:00
Brennan Benson 627716fdc1 perf(ports): stop leaving stuck lsof processes behind on a dead network mount (#27729)
* perf(ports): run lsof without blocking on dead network mounts

Without -b, lsof stat()s every mounted file system before answering. A
hard-mounted network share that stops responding blocks that call in the
kernel, so each port scan left an unkillable lsof behind. Pass -b -w to
the lsof queries that name no file (port list, process cwd, executable
path); their output is unchanged. The -U and +D queries keep stat
because -b makes lsof reject named-file arguments.

Linear: STA-9017, STA-9716

* perf(ports): name the stuck lsof helper in the comment; plain readiness test name

* test: query a known child executable in lsof readiness smoke test
2026-10-12 00:55:43 -07:00
Brennan Benson e3ef95dc04 feat(cli): launch agents in existing workspaces (#27794) 2026-10-12 00:38:00 -07:00
OrcaWinandm4air 8a1c63b381 refactor(shared): dedupe the local-default connection-to-host conversion and the trim helpers (#27797)
Co-authored-by: m4air <m4air@Mac.localdomain>
2026-10-11 23:40:18 -07:00
Brennan Benson d3182d4bf1 Native chat: every agent launches through one composed launch path (#27204)
* test(chat): snapshot every native-chat agent's spawned child as a parity oracle

Captures the exact child each native-chat agent spawns (Claude, Codex, Grok,
OpenCode, OMP, Pi), worktree and floating, fresh and resumed, plus a registered
worker with visuals off, at the real spawn boundary (the process host's spawn)
and through production's own host dependency wiring. The inherited environment
holds every pane, hook, caller, terminal-handle and visuals key a parent Orca
pane could hand down, so the snapshot records today's per-agent differences as
they are. Each case also records the launch's permission posture, which for
Codex (thread policy) and the ACP agents (full access) acts only after spawn.

Each agent also runs fresh and resumed with every launch setting moved off its
default: a Command setting naming another binary, the permission posture
flipped, an environment overlay over inherited and shell keys, and only named
login-shell variables inherited (the fake login shell carries its own PATH and
a variable no case names).

A second test fails when a registered agent has no snapshot, since the oracle
lists its agents by hand.

Test only: the launch-path refactor that follows must leave both tests and the
snapshot file unchanged.

* refactor(chat): one composed launch for every native-chat agent

Every native-chat agent (Claude, Codex, Grok, OpenCode, OMP, Pi) now builds its
child through one composer instead of six hand-written resolvers. Each agent
contributes only its own part (command, args, agent env, keys it must strip);
the shared composer admits the session record, reads the launch directory,
base environment, launch overrides and visuals once each (memoized, in the
agent's original order), sets cwd, and seals the child.

The seal (sealStructuredSessionChild) is the one place that names a child:
session id, structured marker, worker terminal handle, the Orca CLI env, the
spawn token under one shared constant, and the per-launch list of inherited
keys to delete, minus any key the seal itself sets. Claude's delete list now
reaches its SDK spawn after buildClaudeChildProcessEnv's merge.

Behaviour is unchanged: the parity snapshot from the previous commit passes
byte for byte, including each agent's existing differences (Claude and Codex
keep inherited pane and hook env, Pi has no visuals and always full access,
the ACP history read stays unsealed). Admission refusals keep their reasons;
only log-only wording moved to the shared check.

Drops launch dependencies no production caller wires (resolveLaunchEnv,
resolveCodexOverrides) and folds the per-agent env overlay hooks into the
existing resolveAgentLaunchEnv.

* refactor(chat): only the seal names a native-chat child

Follow-ups from review of the composed launch:

- The spawn token is required on every resolveLaunch. The one read that
  starts no child, ACP's restart-recovery history read, now has its own
  path (composeStructuredLaunchRead, deps.readLaunch) that returns the
  agent's own env unsealed, as before. Agent parts receive the session
  identity only, never the token.
- structuredSessionChildIdentityEnv is gone; the seal is the only way to
  build a child's identity env, and tests go through it.
- A launch's environment is the runtime's resolveAgentEnvironment, so the
  base-plus-overlay rule exists once.
- Claude's connection test now covers a non-empty envToDelete applied
  after the inherited env is merged.

No change to any spawned child: the parity snapshot is unchanged.

* ci: run the login-shell contract job for the renamed child-env seal

The seal moved from structured-session-child-identity-env to
structured-session-child-env, but the PR job classifier still listed the old
path, so a change to only the seal would skip the live login-shell contract
job. The prefix now covers the seal, its test and its test support (which
the login-shell harness imports).
2026-10-11 23:27:04 -07:00
Brennan Benson 5035baac50 Edit agent launch settings on the workspace host (#26479)
* Add host-owned agent launch settings mutations

* Edit agent launch defaults on the selected execution host

* Await durable writes for host agent launch settings

* Fence agent detection by execution-host pairing

* Acknowledge host settings before hook reconciliation

* Preserve Reset after pending agent launch saves

* Simplify agent detection re-pair handling to the clear and the transport fence

A re-pair already clears every detected-agent list, flag and pending probe for
the environment, so the per-record pairing owner keys, the owner-keyed pending
probes, the settle-time pairing checks and the hook's ownership gating were
never reached. Detection now captures the pairing before the capability wait
and fences its RPCs with it; the refresh fallback still refuses after a
re-pair. Pins the fence across a capability wait, the needs-update clear, and
the agent-launch RPC permission levels.

* Open launch settings on the Active Server without the focus-routing helper

Main's new owner-routing ratchet counts getActiveRuntimeTarget calls, and this
pane's no-workspace default added one. It now reads the Active Server field the
way the Agents pane did before host selection, so the default is unchanged.

* Re-probe mounted launch surfaces after a re-pair; rely on the transport fence for the refresh fallback

A re-pair clears an environment's detected-agent lists, but a mounted tab bar,
quick launch or composer that was still waiting for its first list (or whose
first probe had failed) never asked again, leaving "Can't reach this
workspace's host" until another surface mounted. The hook now re-runs its
effect when the target environment's pairing revision changes.

The refresh fallback no longer re-checks the pairing in the renderer: it
already carries the pairing captured before the wait, and main refuses any
mismatch. Pins the Settings host form's remount key on re-pair.
2026-10-11 23:26:30 -07:00
OrcaWinandm4air d4a55f583e fix(ssh): end an older Orca's relay terminals from the connect refusal (STA-10166) (#27748)
* fix(ssh): end an older Orca's relay terminals from the refusal, and name the command

* test: type confirmation mocks

* fix(ssh): match old relay daemons by argv position; owner-check the Windows command

* fix(ssh): select Windows relay daemons by argv position in the manual command

* fix(ssh): keep the relay daemon marker local so census mocks stay intact

---------

Co-authored-by: m4air <m4air@Mac.localdomain>
2026-10-11 21:53:57 -07:00
Brennan Benson 30660086f2 test(launch): pin what the "+" launchers produce on main today (#27691)
* test(launch): pin what the "+" launchers and the AI buttons' window route produce on main

Row 1 ("+": menu, shortcut, dashboard) joins the window-tab table on both halves;
saved-arg tokenize failures before routing, the restart of a never-mounted "+" tab
and the AI-button host gate are pinned at the window planner.

* test(launch): cut "+" pins main already holds and check outcomes, not internals

Drop the AI-button and restart tests and the duplicate \\wsl$ folder and macOS
unclosed-quote rows; the Q5 pair now checks that no tab or startup was made.
2026-10-11 21:42:40 -07:00
OrcaWinandm4air 6c3ce444bc feat(updater): auto-park registered serve supervisors during a desktop install (#27722)
* feat(updater): auto-park registered serve supervisors during a desktop install

* test(serve): drop a type assertion in the bundle park test

* fix(updater): stop any server started during an install hold and re-list blockers right before commit

---------

Co-authored-by: m4air <m4air@Mac.localdomain>
2026-10-11 21:11:53 -07:00
OrcaWinandm4air 78144e91cc feat(serve): supervised mode for orca serve --supervise (#27716)
* feat(serve): supervised mode for orca serve --supervise

Restart the server with jittered backoff instead of exiting when it dies,
with a crash-loop brake whose exhaustion is durable per profile and build,
an install hold while the executable is missing or ShipIt replaces the
bundle, and standby while another Orca serves the profile. Only an
explicit --supervise turns it on; Windows refuses it for now.

* fix(serve): let a supervised orcad serve wait for a live peer instead of counting it

* fix(serve): gate update replacements on the install hold, honor stop during the bundle wait, keep a give-up while the executable is missing

* fix(serve): hand a removed executable back to the service manager and key the brake on the app archive

---------

Co-authored-by: m4air <m4air@Mac.localdomain>
2026-10-11 20:16:33 -07:00