Commit Graph
2914 Commits
Author SHA1 Message Date
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
Neil 1a1a2a165e perf(zod): use Zod 4.6 validation and compile ACP updates (#27735)
* perf(zod): compile ACP updates and use Zod 4.6 boolean validation

* test(zod): expand regression coverage and move benchmark to PR body
2026-10-11 19:55:53 -07:00
OrcaWinandm4air 3a52598cdb refactor(ssh): one pinned-runtime host target (#27709)
The relay runtime ladder (rungs A/B/C/legacy/D, its self-test, refusal
cache, per-host decision store and its telemetry) lost its only caller
when the relay was deleted. Two pieces were still live: picking the Node
runtime a host can run by glibc (managed orcad deploy, WSL guest
runtime) and the runtime-store pins orcad version GC keeps. Those move
to pinned-runtime-host-target.ts; pick/durationBucket move into
ssh-host-server-telemetry.ts, their only remaining user.

Co-authored-by: m4air <m4air@Mac.localdomain>
2026-10-11 19:48:49 -07:00
Neil be4cea1584 perf(wsl): search Windows-drive projects with host ripgrep (#27690)
* perf(wsl): run ripgrep on the Windows host for drive-path projects

A project stored on a Windows drive (C:\...) with the WSL runtime selected
ran quick open, file-path search, content search and Markdown listing
through wsl.exe and the Linux rg reading /mnt/c over drvfs. rg only reads
files, so it has no git index whose stat cache two writers could thrash;
the host binary reads NTFS directly.

Measured on a 21.6k-file clone on C: (Windows 11, Ubuntu-24.04):
  content search  WSL rg 3.5 s  -> host rg.exe 0.19 s
  file listing    WSL rg 0.14 s + ~65 ms wsl.exe spawn -> host 0.03 s

Git routing is intentionally unchanged: alternating Windows git and WSL git
on the same index forced a full ~250 s rehash on the next WSL git status.

WSL UNC roots and non-drive roots keep their existing routing.

* fix(search): keep host rg results whole when files only WSL can open exist

Windows rg cannot open a 'nul' file, Win32-trimmed names, or WSL-created
symlinks; treat those per-file errors as skipped rather than failing or
truncating the search, and keep symlink-following scans in WSL.
2026-10-11 19:35:46 -07:00
Jinwoo Hong d9a3c84b00 fix(terminal): don't duplicate terminals a newer Orca still runs after a downgrade (#27577)
* refactor(daemon): share the cold-restore payload builder

Lets a reader outside the adapter build the same saved-screen payload from
history without spawning.

* feat(daemon): hold restored terminals a newer build's daemon may still run (STA-9257)

After a downgrade, an older build cannot adopt a newer-protocol daemon, so it
cold-restored that daemon's terminals under the same ids and resumed their
agents beside the originals. Startup now classifies newer-protocol pid records
in this profile's runtime dir as live, unverifiable or exited, and pty:spawn
refuses a restore no adopted daemon owns while one is live or unverifiable.
Reopen stops the newer daemon by verified signal only and reports success only
once its pid is confirmed gone.

* feat(terminal): show held restored panes read-only with a Reopen banner (STA-9257)

A pane refused for a newer daemon shows its saved screen read-only under a
pane-top warning. Reopen terminal stops the newer daemon and releases every held
pane through the existing reattach path, then toasts what actually reopened and
resumed; an unconfirmed stop reopens nothing and offers Try again.

* refactor(daemon): hold newer-daemon restores at the router, one classifier (STA-9257)

The hold moves from the pty:spawn IPC handler into DaemonPtyRouter.spawn so
every spawn source (IPC, runtime controller, mobile refresh, CLI) refuses a
cold restore of an id no adopted daemon owns while a newer daemon is held.
Discovery and stop share one classifier; an unidentifiable owner settles by
whether its endpoint accepts a connection, so a dead leftover never holds a tab
forever. Main announces a confirmed stop to the renderer.

* fix(terminal): per-pane failed state, one release path, no repeated screen (STA-9257)

A pane held after a failed Reopen shows the held state. Held panes also release
on main's stop announcement, with one toast only from the clicking window. A
pane whose saved buffer the layout restore already painted skips the history
screen and is not cleared on release.

* refactor(terminal): drop the release event, await the router hold, one phase per pane (STA-9257)

The release announcement only reached the window that clicked, which already
releases on its reply, so it is removed. The router hold check is always
awaited; three router tests released the adapter spawn before the router had
reached it and now wait for it instead. Each held pane carries its own phase.

* refactor(terminal): trim the newer-daemon hold, keep paired panes out of it (STA-9257)

A paired host's refusal is now an ordinary error: holding it locally looped,
since this window cannot stop that host's daemon. The history-screen read is
gone (the layout restore already paints saved output), discovery folds into the
hold module, the hold store uses zustand, the reattach returns its outcome so a
fresh-session fallback counts as reopened, and the toast uses plural keys.

* feat(terminal): show a held local pane's saved screen read-only (STA-9257)

Local daemon-backed panes keep no layout buffer, so a held pane was blank. Main
reads the session's saved screen from history without spawning and the pane
paints it read-only, unless the layout restore already painted the pane; it is
cleared before the normal reconnect.

* fix(daemon): re-probe a timed-out newer-daemon endpoint before holding (STA-9257)

* feat(terminal): count held panes in the reopen label, add Dismiss (STA-9257)
2026-10-11 22:21:20 -04:00
Neil d8ebe75efe fix(wsl): restore Windows interop after another distro wipes it (#27682)
* fix(wsl): detect and restore a wiped WSLInterop entry in running distros

On WSL < 3.0.1 a graceful stop of any systemd distro deletes every
binfmt_misc entry VM-wide, so Windows .exe launches (code ., explorer.exe,
the orca WSL CLI bridge) fail with Exec format error in every other distro.

Check, never boot: 5 s after a WSL terminal env build (gated on the distro
being in the confirmed running list, at most once a minute per distro), and
whenever a confirmed running-distro probe shows another distro stopped.
Repair is automatic and idempotent as root, skipped when the distro's
wsl.conf sets [interop] enabled=false, single-flight per distro, with a
10 minute backoff after a failed repair.

* fix(wsl): repair interop with a single register write matching WSL's flags

Busybox printf writes a trailing newline separately; the kernel registers the
entry, then rejects the lone newline, so the repair reported failure after
succeeding. Write without the newline and let the re-check decide. Use WSL's
own P flags rather than PF, and treat an entry restored between probe and
repair as intact. Add a spawn-budget test.
2026-10-11 19:16:47 -07:00
Neil a3e97184f4 fix(runtime): keep remote servers usable when one saved record can't be read (#27685)
* fix(runtime-environments): tolerate unreadable server records without dropping them

One corrupt or newer-format record in orca-environments.json or the
environment sidecar made every server unlistable. Each file now parses
per record: readable records load, unreadable ones are rewritten
verbatim on every write. A server whose sidecar entry cannot be read is
hidden and refused for edits, so it never connects or is rewritten
without its SSH access or deployment link.

* test(runtime-environments): fuzz interleaved writers against unreadable records
2026-10-11 19:15:43 -07:00
OrcaWinandOrca Worker 83056e5edb fix(skills): judge official skill updates by ownership, not snapshot matching (#27603)
* fix(skills): judge official skill updates by ownership, not snapshot matching

Orca decided whether an installed official skill was pristine by matching its
bytes against a shipped list of past snapshots, falling back to the skills CLI's
lock hash. Copies installed between releases, CRLF copies, and installs whose lock
recorded a sha256 hash all read "doesn't match the official version" and could not
be updated (#26841), and older content could be labelled newer and never offered.

Now an official skill is Orca's when the skills.sh global lock says it came from
stablyai/orca, and it is current only when its listed files equal the bundled
manifest (line endings and sidecars ignored); anything else is outdated. Install
and update are one pinned `skills add .../tree/v<appVersion>` command, used by the
in-app runner, `orca skills update`, and the Settings rails. The update dialog
preselects Orca-owned rows and lists same-named skills from elsewhere unchecked.

Removes the snapshot registry, release mapping, git-tree identity, lock trust and
convergence logic, and the release-cut provenance step.

* ci(skills): run the pinned-add round trip on a skills CLI that can fetch a commit

* fix(skills): let orca skills update target the shared folder on a bare host

Update became the same pinned add as install, so it inherited install's
refusal when no agent is detected. That broke the bare-host update the old
command guaranteed, and the built-CLI check (update --dry-run, no --agent) on
CI runners. The in-app runner already fell back to universal; both now share
one detectSkillsCliUpdateAgentKeys rule.

Also brings the CLI skills docs off `npx skills update`.

* refactor(skills): derive the update selection from the grouped rows

The selection hook took the live and the retained inventory, re-derived Orca
ownership from installations beside the grouping's own orcaOwned, and kept a
separate display count. Groups already carry both eligibility and ownership,
so the hook reads them once; the button's existing live-scan gate is the only
authorization check. The unselected line is inlined into the headline.

* refactor(skills): drop helpers the ownership design made redundant

- isOwnerManagedSkillScope only negated into the participation check once
  plugin caches stopped participating; fold it in.
- Eligibility re-checked resolvedPath/physicalIdentity, which observation
  already turns into 'inaccessible'.
- The manifest verifier probed access() before a readFile that throws anyway.
- orchestration-install-command.ts only forwarded one constant from the lib
  module its three importers already import.

* i18n(skills): stop naming npx skills update in four skipped-copy strings

The es/ja/ko/zh cantUpdateReason and tipDuplicate strings still named
`npx skills update`, which Orca no longer runs for its skills. Say 'the update
command' instead.

* refactor(skills): inline the single-use pinned source URL helper

orcaSkillsSourceUrl was exported but only called by the builder beside it.

* fix(skills): let an update choice lapse when the install source changes

The selection froze every checked name, Orca's preselection included, the
moment the user ticked anything. A re-check that then attributed a name to
another source kept it checked, replacing it without consent. Store only the
user's explicit choices, keyed by name and lock source, and default to
ownership otherwise.

* fix(skills): label the empty update action 'Update skills', not 'Update 0 skills'

The button stays whenever a row is updatable, so unticking every row (or a
dialog with only non-Orca copies outdated) doesn't reflow the footer. At zero
selected it stays disabled and reads 'Update skills' instead of a zero count.
The label lives beside the dialog's other count copy.

* fix(skills): let keyboard focus open the Not from Orca tooltip

The source of a non-Orca copy is only in the tooltip, and its trigger was a
plain span. Make it focusable like Orca's other tooltip-on-text triggers
(tabIndex 0, cursor-help, the row primitive's focus ring), as in the agreed
mock.

* test(skills): hold the round-trip add equal to the shared install builder

The CI round trip hardcoded its skills add argv, so it could drift from what
Orca runs. A .mjs script can't import the TS builder (extensionless imports),
so the argv moves into an exported roundTripAddArgs and a unit test asserts it
is the builder's unattended add with the tag swapped for the PR commit. The
script runs under the same main guard as the manifest generator.

* i18n(skills): translate the skill row's location count and duplicate reason

es, ko and zh still carried English for SkillUpdateRow.oneLocation and
manyLocations, and es and ko for SkillFreshnessRow.skippedReasonDuplicate.

* fix(skills): pin official skill installs to the build commit and skills CLI

`skills add .../tree/v<version>` only works for stable and RC builds: hourly,
daily and adhoc builds publish elsewhere, so stablyai/orca has no tag for their
version and both install and update failed there. Every build is made from a
commit that exists in stablyai/orca, so the build now stamps `git rev-parse HEAD`
and the one shared builder pins to `tree/<commit>`, which equals the bundled
skills by construction. Main, renderer, web and vitest get it as
ORCA_BUILD_COMMIT (replacing ORCA_APP_VERSION); the tsc-built CLI reads it from
the compiled out/package.json boundary. A missing or malformed commit fails the
build or the command with a clear error instead of building a bad URL.

The builder also pins the skills CLI (`npx skills@1.7.2`): 1.5.x cannot fetch a
commit, and a bare `npx skills` can reuse a stale cache or resolve to another
binary (#18687). The CI round-trip script reuses the same constant.

* ci(skills): run the round trip on the shared pinned skills CLI version

* docs(skills): document the commit-pinned skills add

* refactor(skills): keep the pinned skills CLI version in the install builder

skills-cli-version.ts existed only so the plain-Node round-trip script could
strip-load a .ts file, which needed --disable-warning in CI. The version now
lives beside the builder that uses it; the script keeps a literal, and the
existing round-trip test already asserts its argv, skills@<version> included,
equals the builder's.

* refactor(skills): validate the build commit once, where it is resolved

resolveBuildSourceCommit already refuses anything but a full hash, and both the
define and the CLI's stamped package.json come from it. The builder's second
check could only fire as a module-load throw in the renderer's command
constants. Also drops the resolver's unused cwd parameter.

* test: stamp a fixed build commit instead of running git in the vitest config

Tests only interpolate ORCA_BUILD_COMMIT, so they need not read HEAD (or fail
outside a git checkout) on every run.

* refactor(settings): match only skills add in the Windows npx preflight

Every official skill command is now the builder's skills@<version> add; no
rail emits skills update any more.

* fix(skills): fail an update that leaves a broken skill link behind

Eligibility already skips names with a broken link before the run, so one
afterwards is the run's own damage; report it instead of claiming success.

* ci(release): refuse to tag when the skill manifest is stale

The removed --release step implicitly checked the committed manifest against
skills/. Keep that guarantee with a verify-only run; it writes nothing.

---------

Co-authored-by: Orca Worker <orca-worker@localhost>
2026-10-11 18:30:03 -07:00
Brennan Benson 4526d8bab4 native chat: queued-message actions and question answers accept through their own receipt (#26988)
* feat(native-chat): add a chat-owned command receipt store

Adds the agent_session_command_receipts table and an insert-if-absent
receipt write that runs inside a command's own journal transaction. A
receipt records the command's identity, canonical hash, status and a
pointer to the row holding its result; it dies with its chat record.
Nothing uses it yet: the existing operation ledger stays authoritative.

* fix(native-chat): key command receipts like today's ledger ids

Rows are keyed by caller and operation id; a send looks across callers and
other methods look up only their caller's row, matching the existing ledger.
Bad rows and newer table shapes read as unreadable instead of throwing.

* fix(native-chat): order receipt lookups by insertion and key by operation id

Global lookups now take rows in insertion order, so a read and an insert
agree on which receipt answers an id regardless of the host clock. The
primary key leads with the operation id, so its own index serves both
lookups.

* fix(native-chat): accept sends through their own receipt

Send (direct, queued, steer) and /compact no longer commit a separate
ledger reservation before the message is saved. Their command receipt is
inserted-if-absent in the same transaction as the submission or queued
draft; a repeated id replays from it, a changed request conflicts.
Operation ids are checked for syntax only, so age, clock skew, pruning
and the reservation's own storage can no longer refuse a send.

The ledger success row they already co-committed stays as a temporary
insert-if-absent write so families not yet switched keep seeing these
ids; it is never read on this path.

* fix(native-chat): answer a send's receipt one way, share /compact's id space with Send, wake delivery once

- After its commit, a send whose publication fails answers through the same receipt replay a retry takes, so one id gets one answer; the in-memory copy and journal reload are gone.
- /compact ids are global like Send's: a retry under a new connection replays instead of needing a second acknowledgment write.
- The receipt route calls its runner directly; the runner wakes delivery when a new message row commits.
- The compatibility ledger row expires from host time; each temporary site is labelled.
- Receipt read errors and post-commit failures are logged.

* test(native-chat): expect receipt answers where tests assumed the send ledger

Restart-continuation tests now model the case Send cannot vouch for (saved but
not loaded into memory) and keep it unconfirmed; a later throw after the message
is in the chat answers continued, as a resend would. Drop /compact's unreachable
legacy replay branch and fix stale comments.

* test: run the queued tests that open the journal database in the Node runtime project

* fix(native-chat): queued-message actions and prompt answers accept through their own receipt

Send Now, Delete, Resume and prompt answers move onto the command-receipt route
Send uses: the receipt is inserted with the command's own write. A no-op answers
only once its receipt commits (otherwise outcome unknown), and a prompt answer's
replay reads the exact revision it was accepted at.

* fix(native-chat): replay a queued action from what its receipt recorded

Send Now's acknowledgement points at the submission it handed off; a question
answer's receipt keeps the accepted resolution; a no-op that cannot save its
receipt answers outcome unknown and holds nothing in memory; classified refusals
pass through.

* docs(native-chat): receipt comments say what a replay answers from

* fix(native-chat): refuse a prompt answer on a newer Orca's journal with the update words

A prompt answer now runs through the same newer-Orca refusal the queued-message actions use, so a read-only journal answers agent_session_journal_unreadable instead of throwing journal_read_only.
2026-10-11 18:25:26 -07:00
OrcaWinandm4air c5224f70d8 refactor(providers): delete the SSH git and filesystem providers and relay-only wiring (#27644)
* refactor(providers): delete the SSH git and filesystem providers and relay-only wiring

* refactor(ssh): drop relay-protocol exports only the deleted stream readers used

---------

Co-authored-by: m4air <m4air@Mac.localdomain>
2026-10-11 17:48:43 -07:00