`orca terminal send --text … --enter` now uses the bracketed paste + Enter agent-prompt path for every recognized agent (not just Claude/Codex), so Pi's #/@ completion and Grok's y/n trust question no longer swallow or answer typed prompts. Agents whose process names collide with ordinary tools (orca, acli, cn, vibe) still need title/hook evidence. A send into a permission/trust menu is refused with a hint to send --text and --enter separately.
Refs #20695
Refs #18399
* feat(managed-server): offer Update and Update anyway on a managed SSH server's row
A managed SSH server only updated on connect, and every connect deferred while
a terminal ran there. Its row now offers Update when this desktop bundles a
newer build than the Orca that runs the host. When running terminals defer it,
Update anyway restarts the server past them; the terminal daemon carries them
across the restart.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* fix(managed-server): Update anyway passes only counted live terminals and keeps Update's version admission
Update anyway now sends the incumbent build it confirmed over; under the host
fence it is refused if another desktop activated a build since, keeps the
newer-host downgrade refusal, and never passes an uncounted terminal census.
The offer also covers hosts activated before the Orca version was recorded,
for the build this desktop already deferred there.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* fix(managed-server): Update anyway also confirms a menu Update the row does not advertise
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* fix(managed-server): the row shows one confirmation at a time
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* ci(e2e): route managed-server update sources to the Update anyway Docker spec
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* fix(agent-teams): run the tmux shim through the headless host's CLI, never the GUI binary
On an orca serve host the runtime has no Electron resources directory, so the
shim fell back to PATH and picked the AppImage's Electron binary, which booted
Chromium and died on the SUID sandbox. Prefer the host's profile-scoped CLI
launcher, and route agent-teams-tmux through the packaged CLI redirect so a
direct Electron invocation never initializes the GUI.
Fixes#33354
* fix(agent-teams): prefer a fresh CLI over a shim bin inherited from an older leader pane
* feat(cli): add orca ports to list a workspace's detected ports
Scripts and agents had no way to read the ports Orca already detects for
a workspace, so finding a dev server on a random port meant redoing the
lsof scan by hand. `orca ports` reuses the host-scoped
workspacePorts.scanHost RPC, keeps the rows owned by the current (or
--worktree) workspace, and prints port, pid, process, connect host,
protocol, how the owner was matched, and the advertised URL. --all shows
every listener on that host. An older runtime without scanHost gets an
"update Orca" error instead of a raw method_not_found.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(cli): resolve folder workspaces in orca ports and match rows by path
Run inside a native folder, `orca ports` failed with selector_not_found
(or picked an enclosing git worktree) because it only searched the
worktree list. It now resolves the cwd like `agent launch`, native
folders first, and shows that host's listeners. Rows are matched on the
owner's path instead of its id, so every instance of a repo-backed
folder workspace keeps its ports even though attribution names only the
first instance as owner. `--all` adds the worktree id when two owners
share a display name, and the skill guide notes that only workspace
rows carry `owner` and `advertisedUrl`.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(cli): refuse orca ports for folder workspaces without --all; document the command
Co-authored-by: Manuel Meurer <manuel@meurer.io>
* fix(cli): recognise native folder path selectors in orca ports; note SSH scan limits
Co-authored-by: Manuel Meurer <manuel@meurer.io>
* fix(cli): refuse a folder path shared by several folder workspaces in orca ports
Co-authored-by: Manuel Meurer <manuel@meurer.io>
* fix(cli): refuse a path a worktree and a folder workspace share in orca ports
Co-authored-by: Manuel Meurer <manuel@meurer.io>
---------
Co-authored-by: Manuel Meurer <manuel@meurer.io>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* feat(connectors): cancel a conversation's queued work and stop its agent, behind experimentalConnectors (B4)
* test(connectors): drop a type assertion from the interrupt test
* fix(connectors): interrupt only a running main-agent turn, never background-only work
* chore(connectors): shorten the interrupt comment
* fix(connectors): fence cancel and prompt keys at transport handoff
* fix(connectors): clear the prompt Claude restores after our cancel, and end a turn our cancel rejected
* fix(connectors): require stronger evidence for cancel cleanup and settlement
* fix(connectors): cancel waits for a published agent's link, keeps cancel headroom in a full backlog, and pastes nothing after an unsettled clear
* fix(connectors): bound cancellation settlement and preserve unresolved lanes
* refactor(connectors): drop the leftover-prompt clear after cancel; keep the rejected-turn end
* fix(orcad): add --reclaim-stale-lock for a lock no start may judge
On a Linux host with no machine id whose ~/.orca is on a network or FUSE
home, a lock left by the host's own previous boot names a PID that is
evidence only on the machine that issued it, so every start after a
reboot refused it, and the only way out was deleting orcad.lock by hand.
Starts still refuse it: losing contact is not proof of exit, and a home
other machines may share must never be taken over automatically. Now:
- `orca serve --reclaim-stale-lock` (and `orcad --reclaim-stale-lock`)
checks that the lock was written on this host, in an earlier boot, at
least 2 minutes ago, and that nothing answers at its holder's
endpoints. It prints the pid, host, boot and age and warns about other
machines sharing the home, and clears the lock only with --yes,
through the normal race-safe reclaim of that exact record.
- Lock records now name their host, as evidence for that command.
- The refusal has its own code, orcad_instance_lock_holder_unverifiable,
names the command, and exits 3, so `orca serve --supervise` waits on it
instead of giving up, and the desktop refuses on it like a held lock.
- orcad advertises orcad.stale-lock-reclaim.v1 so a client can offer
the command on hosts that have it.
Fixes#32177
Co-authored-by: Jesse Sanford <108698+jessesanford@users.noreply.github.com>
* fix(orcad): let --reclaim-stale-lock clear a reclaimer cut short by the reboot, and probe IPv6
A reclaimer from the same earlier boot on this host that the reboot cut
short left a recovery owner the command could never judge, so --yes
always failed. The confirmation now covers such an owner too, and only
one from this host's earlier boot. An IPv6 WebSocket endpoint is probed
by its bare address.
Co-authored-by: Jesse Sanford <108698+jessesanford@users.noreply.github.com>
* fix(cli): ship the stale-lock reclaim module with packaged builds, and refuse an unreachable lock
The CLI loads main modules from out/main after the main build replaces
it, so the reclaim module becomes a stable plain-Node entry and is
unpacked from the asar. A lock that cannot be stat'ed for any reason other
than absence (a permission error, say) is now a refusal, not "nothing to
reclaim".
Co-authored-by: Jesse Sanford <108698+jessesanford@users.noreply.github.com>
* fix(orcad): make --reclaim-stale-lock name the real data root and report a status
- The refusal prints a runnable command for the data root it refused:
an orcad names its own runtime and entry plus `--data-root <dir>`,
since managed hosts have neither `orca` nor `orcad` on PATH and their
data root is not the default. The hint is Linux-only.
- Both entry points take `--data-root` and `--json` and report one of
none, eligible, refused or reclaimed, each with its own exit code
(4, 2, 1, 0), so a missing lock never reads as a recovery.
- The evidence says nothing answers on this machine, and that the host
name and boot checks cannot rule out another machine with the same
name; --yes stays the gate.
- wss endpoints default to port 443, a same-boot branch that could never
run is gone, and the supervisor's standby line points at the command.
Co-authored-by: Jesse Sanford <108698+jessesanford@users.noreply.github.com>
* fix(orcad): point the eligible message at the same command plus --yes
Co-authored-by: Jesse Sanford <108698+jessesanford@users.noreply.github.com>
* fix(orcad): refuse an empty --data-root, and name the right program in reclaim hints
- `--data-root` with no usable value (a trailing flag, an empty or blank
string) prints usage and exits 1 on both entry points, never falling
back to the default profile.
- A refusal raised while the reclaim command itself runs names
`orca serve --reclaim-stale-lock`, not the binary the CLI runs under;
an orcad still names its own runtime and entry.
- The supervisor's standby hint includes this profile's --data-root.
- The unreadable-lock test is skipped when running as root.
Co-authored-by: Jesse Sanford <108698+jessesanford@users.noreply.github.com>
* test(cli): reuse the existing client stub for the --data-root refusal test
Co-authored-by: Jesse Sanford <108698+jessesanford@users.noreply.github.com>
---------
Co-authored-by: Jesse Sanford <108698+jessesanford@users.noreply.github.com>
* fix(terminal): a timed-out CLI split names its pending pane and can be retried without splitting again
Fixes#32169
* fix(terminal): a blank retry id or a closed pane never re-runs a split's command
* refactor(terminal): a split retry only waits for its pane and never asks the window again
* fix(terminal): a split missing from the graph is reported pending, never closed
* fix(cli): reject orchestration ask waits above the documented maximum
* fix(cli): keep ask timeout help ASCII and trim redundant cases
Co-Authored-By: Claude <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
* fix(cli): negotiate agent launches with paired runtimes
* refactor(cli): share paired client capability list across request paths
Co-Authored-By: Claude <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
* Add Orca's own bundled-skill planner and installer (not wired in yet)
One planner decides what installing Orca's bundled skills does to each
target folder; execute re-plans under per-path locks and runs only a plan
whose digest still matches; a shared summary derives badge, wording and
exit code. A copy counts as Orca's untouched one only when its SKILL.md
bytes match a version some build shipped (resources/skills/released-digests.json,
generated from git history and checked add-only).
Nothing calls this yet; the npx path is unchanged.
* Report left-alone folders as warnings and keep backups in the home
A leave is no longer a failure: the outcome line says what happened plus
"N left alone: <reason>", and the exit code is 1 only for a failed or
changed op. Backups for every scope go to <home>/.agents/.orca-skill-backups
on the executing host, so a project Replace never writes into the repo;
moving there falls back to copy, verify, then remove across filesystems.
* Update Orca's skills with Orca's own installer instead of npx skills
The app's Update skills dialog, badge, nudge and status pills now read the
bundled-skill plan and summary, and Update executes the reviewed plan's
digest. `orca skills install|update` runs the same planner in-process with
--dry-run, --force, --json and per-summary exit codes. Removes both npx
spawn sites, the freshness inventory modules and the npx round-trip CI.
* Update only where the skill already is; one provider-root rule for app and CLI
installedOnly now drops every location that holds nothing (keeping the shared slot
only when an existing agent link points at it), so Update never adds a skill for
another agent. The app and CLI resolve the Claude skills folder through the same
rule in localBundledSkillInput; a test asserts both plan identically.
* Compact Update skills dialog: one row per skill, locations named by agent
summarizeBundledSkillPlan now returns each skill's locations, labelled by
agent (or "Shared folder") with a ~-collapsed path; the dialog shows one row
per skill that needs something, collapses up-to-date skills into one line, and
lists every location behind "Show all locations". The result reads "Updated N
skills · M left alone". Under installedOnly a Replace with no shared copy
replaces in place instead of adding one.
* fix(skills): keep location paths readable by putting the state under the path
* fix(skills): hold the last plan in state, not a ref mutated during render
Split the update dialog's rows, headline and footer into components, move the Update label
next to the other plan copy, and use z.strictObject for the request schemas.
* Escape non-ASCII characters in bundled-skill regex and wording
The orcad bundle rejects a literal non-ASCII character in a regex.
* fix(cli): escape non-ASCII apostrophes in the skills install help text
* test(orcad): check the bundled CLI plans a skill install without npx
Drop the deleted roundtrip workflow's group from the closed-PR cancellation fixture.
* Make bundled-skill test fixtures link folders the way the product does on Windows
Directory links in fixtures are junctions on Windows, which need no
Developer Mode; the one file-link fixture is skipped there.
* Resolve legacy skill agent keys canonically
* Reject stale skill replacements when auxiliary content changes
* Keep ordinary bundled skill classification shallow
* Leave unreadable skill replacements alone without aborting plans
* Pass force-all through shallow planning and describe unreadable copies
* Clarify skill update statuses and folder details
* Keep selected skill replacements in place with one status
* Report completed skill replacements with their backup confirmation
* Clarify available skill updates and skipped copies
* Keep skill update results in preview order
* Show each skill skip reason once
* Count skipped skills instead of skipped folders
* Make installer input and link tests work on Windows
* Bound bundled skill classification reads and parse names linearly
* Keep workspace skill roots inside the resolved workspace
* Compare workspace skill roots with native path semantics
* Restrict bundled skill backups to their owner on POSIX
* Keep recovered skills needing attention until they are updated
* Count completed skill updates in the status tooltip
* Sync the runtime localization catalog
* feat(connectors): reopen a closed or exited agent when its conversation gets a message (B2c)
Behind experimentalConnectors. When a connector message reaches a conversation whose agent tab was
closed or whose agent exited, the seam resumes the agent's own session in the same workspace
through resumeAgentSessionInWorkspace, waits for its composer, delivers the message and answers
accepted with reopened. The session id is copied onto the conversation link as hooks report it.
Refusals: agent_asleep_in_window, no_session_to_reopen, reopen_refused; a session another pane
may still hold (an SSH host out of contact included) stays agent_not_reachable.
Resume claims now end as soon as the hook store reports the resumed session, rather than at the
next claim, since an agent that exits to its shell drops its hook row and kept the claim held.
* fix(session-resume): end a resume claim when the hook store reports it, not at the next claim
A resume claim (#31718) ended only when a later claim found the hook store naming the session in
the claimed pane. An agent that quits to its shell drops its hook row, so that later check found
nothing and the claim stayed held for as long as the shell lived: every other resume of the
session was refused as held_elsewhere. Claims now watch the hook store's provider-session changes
while any is held and end each hold as soon as it is reported.
* fix(connectors): pin reopened process before readiness and capture existing session
* fix(connectors): watch persisted links' sessions from host startup and after re-enable
A fresh seam (connectors turned back on, or a runtime restart) watched only the links its own
calls touched, so a session that changed before the next connector call was missed and a reopen
resumed the old conversation. A new seam now snapshots every persisted link's hook-reported
session and watches it, and the runtime builds the seam when its hook store attaches at startup
rather than on the first connector call.
* fix(session-resume): end a hold on report only when it covers one bound spawn
The report names a pane, not a spawn, so with a second overlapping open of the same pane still
unsettled or bound to its own PTY, ending the hold early could let another pane resume while
that spawn still writes. Those holds keep the existing PTY-exit and next-claim ends.
* fix(session-resume): settle a lost-reply resume attempt when a retry of the same request binds
A targeted resume whose spawn reply was lost keeps its share held, since its process may run.
A retry of the same request reconciles that same create, so when the retry binds its PTY the
earlier attempt's share is accounted for by it: the hold now ends when that PTY exits instead of
never. A different request's spawn still accounts only for itself, and a lost reply is never
read as an exit.
* fix: preserve overlapping resume holds during lazy report checks
* test: verify disabled connector startup and restored capture
* feat(connectors): connector seam lifecycle, conversation links and start behind experimentalConnectors (B2b-1)
* feat(connectors): connector follow-ups, held while the agent asks, behind experimentalConnectors (B2b-2)
* feat(agent-write): opt-in launch cancel, approval wait and binding fence on the shared writer
executeAgentLaunch takes an optional signal; sendTerminalAgentPrompt (and
sendAgentTurn) take optional onPermission: 'wait' and expectedBinding. No
existing caller sets them, so every current path is unchanged.
* fix(agent-write): keep the default writer path main's; fence cancel and real submit boundary
Only opted-in writes take the submit layer, so default timing and the leaf
re-read are unchanged. Cancellation is rechecked before a structured-refusal
terminal fallback, and an opted-in launch learns Enter from an onSubmitWritten
callback at the write itself, OMP's combined frame included.
* fix(connectors): pass the seam's signal to the launch, read links from each host's rows, and keep delivery ids on orchestration's durable receipts (B2b-1)
* fix(agent-launch): a cancel after a failed startup spawn recovers the created workspace
* fix(connectors): keep an unknown receipt once the launch published, and restore local-only metadata ownership (B2b-1)
* fix(connectors): settle a lane when its process is lost; removal stays host-qualified; folder delete announces removal
* fix(plugins): keep folder removals out of public worktree events
* fix(connectors): a launch admits its process for the lane, so queued sends never adopt a successor
* fix(connectors): classify connector RPCs as host-owned; CLI start output claims a start only when definite
* fix(orchestration): let a worker start helpers on the Run it works for
A dispatched worker could only start helper workers by taking a coordinator
seat on a new Run, and taking a seat moved its inbox. worker-start now also
accepts a worker on an active Dispatch: its helpers run in the same Run, report
to it (worker_done, heartbeat, escalation, status, ask), and it answers their
questions. The worker's own mailbox never moves. When it settles, whatever its
helpers sent that it never handled goes to the Run's coordinator.
Authority comes from the worker's own Dispatch, re-checked inside the
reservation transaction. Helpers are fresh agents on this host; a worker may
retry only its own helper's latest attempt. Remote workers keep the old
own-Run instructions. No saved field or wire change.
* fix(orchestration): read helper rows without type assertions
* fix(orchestration): hand a settled helper creator's mail to the Run as live delivery
When a worker that started helpers settled, its helpers' unhandled mail was
re-addressed to the Run by settlement SQL alone. Nothing woke the Run's
coordinator, delivered rows kept their old delivery stamp so pointers and the
restart scan skipped them, payload-free status reports were left behind
because selection required payload.dispatchId, and a question asked while the
creator was being stopped stayed with the stopped creator.
- Select handed-back mail by its recipient mailbox and Run (unread reports
plus pending questions, even ones already read), reset delivery and pointer
state on the moved rows, and fence the old mailbox's Deliveries.
- After the settling transaction ends, the runtime re-reads exactly the moved
rows and wakes the Run with their real types; a rollback wakes no one and a
failure falls back to the existing repoint retry. Restart recovery is the
existing unread-undelivered scan.
- Hand mail back again at the final stop settlement, which also closes the
stopped Dispatch's own questions asked during the stop.
- A worker_done receipt replay re-offers the handed-back targets.
- Scope markAsDelivered to the mailbox it pointed, so a late pointer stamp
from the old owner cannot hide mail now owned by the Run.
* test(orchestration): drop an unused inbox type
* fix(orchestration): read only the Run when handing a settled worker's mail back
Every settlement runs the hand-back, so a full dispatch row read there added a statement per settlement. Also expect the mailbox-scoped delivered stamp in two delivery tests from main.
* fix(orchestration): re-offer handed-back Run mail on every worker_done replay
A helper creator's worker_done hands its helpers' unread reports and pending
questions to the Run. The send stored the Run's extra wake targets in the
mutation receipt, but the executor rewrites that receipt on completion and on
each replay, so an identical retry only woke the original destination.
Derive the targets at replay time instead: for the settled Dispatch (when it
has same-Run helpers), notify run:<id> for each type of unread report or
pending question there. Notification only; nothing is re-settled or inserted.
The receipt augmentation is removed.
* style(orchestration): format with the formatter version main now pins
* feat(connectors): connector seam lifecycle, conversation links and start behind experimentalConnectors (B2b-1)
* feat(agent-write): opt-in launch cancel, approval wait and binding fence on the shared writer
executeAgentLaunch takes an optional signal; sendTerminalAgentPrompt (and
sendAgentTurn) take optional onPermission: 'wait' and expectedBinding. No
existing caller sets them, so every current path is unchanged.
* fix(agent-write): keep the default writer path main's; fence cancel and real submit boundary
Only opted-in writes take the submit layer, so default timing and the leaf
re-read are unchanged. Cancellation is rechecked before a structured-refusal
terminal fallback, and an opted-in launch learns Enter from an onSubmitWritten
callback at the write itself, OMP's combined frame included.
* fix(connectors): pass the seam's signal to the launch, read links from each host's rows, and keep delivery ids on orchestration's durable receipts (B2b-1)
* fix(agent-launch): a cancel after a failed startup spawn recovers the created workspace
* fix(connectors): keep an unknown receipt once the launch published, and restore local-only metadata ownership (B2b-1)
* fix(connectors): classify connector RPCs as host-owned; CLI start output claims a start only when definite
* fix(orcad): ship computer use with the server and say when it is unavailable
* test(computer): drop the type assertion in the sidecar app-environment mock
* fix(cli): carry the server reprovision hint into --json computer failures
* fix(profile-state): repair only the damaged sections instead of rolling back the whole profile
One profile-state row whose checksum no longer matches made Orca refuse to open the
whole profile, and the only recovery swapped in a whole backup, losing every newer
change in every section.
Startup now checks each section on its own. When only some sections are damaged, the
dialog offers "Repair and Restart": Orca copies the damaged database aside, then replaces
just those sections with the newest backup copy that passes its checks, keeping every
intact section's current value, in one checked write. Headless starts name the damaged
sections and `orca profile state rollback --repair`. Whole-file damage keeps the existing
backup rollback.
Also turns on checkpoint_fullfsync for the profile database: on macOS plain fsync leaves
the drive cache unflushed, which can leave a long row's pages from two versions after a
power loss or forced shutdown.
Tracked in STA-10893.
* test(profile-state): pin the repair dialog's text and buttons
* fix(profile-state): list the repair module for the CLI typecheck; keep section labels out of the layout-writer count
* fix: validate section repair admission before decoding payloads
* fix(profile-state): share backup handles during section repair
* fix(orcad): voice dictation on headless orca serve hosts
Mobile dictation against an orca serve host answered speech_unavailable
since serve moved to orcad: orcad never installed the speech service
factories, and the model manager and download imported Electron.
The model download now takes a host request opener (Chromium net on the
desktop, Node https on orcad, with redirects mapped to the same event),
the STT worker ships in the orcad slot, and orca serve hands orcad the
installed app's on-device recognizer. A host without a recognizer (an
SSH or WSL managed slot) lists only cloud models and sends a phone that
picked an on-device model to the setup sheet. Mobile names the fix when
an older server still has no speech at all.
The parity test now requires both roots to install speech.
Reported-by: delorean2020 (@delorean2020)
* fix(orcad): retry Node download drops, stage the recognizer on Windows, review fixes
- Node https errors (ECONNRESET, ETIMEDOUT, ...) are retryable, so an
interrupted orcad model download resumes (Codex P2).
- On Windows orca serve hands orcad a per-version copy of the recognizer
in the profile, so a loaded addon never locks the app's files against
an update (Opus).
- Drop a recognizer candidate path that could never match; one SAFETY
helper per test file instead of repeated casts; comments.
* fix(orcad): cloud speech is ready only with a key this host can decrypt; prune superseded recognizer copies
Codex round 2: a desktop-sealed OpenAI key exists on disk but orcad's
secret store cannot open it, so the catalog reported cloud models ready
and dictation failed at the end. orcad's model manager now proves the
key decrypts. The desktop keeps its existence check (no keychain prompt).
* fix(orcad): never prune staged recognizers before the profile lock; copy linked packages as real files
Codex round 3. ref-oss check: versioned copies are cleaned only by the
process that owns them, so the pre-lock prune is dropped rather than
patched.
* fix(orcad): remove a partial recognizer copy when staging fails (Codex round 4)
* fix(speech): never hand out another store's ciphertext as the OpenAI key (Codex round 5)
* refactor(orcad): no recognizer copy on Windows; Windows orcad keeps cloud dictation only
Design check after review round 5: every remaining finding came from the
Windows copy that dodges addon file locks. Dropping it shrinks the
surface to a path lookup; Linux and macOS serve keep on-device models.
* fix(speech): name the fix when an old server refuses a mic tap; always set the recognizer env; retry ENOTFOUND (final Opus review)
* fix(managed-server): probe install completeness relative to the slot dir
CI: shipping stt-worker.js added one more absolute slot path to the
install-complete probe, pushing it further past the exec-size budget.
Testing each artifact relative to the slot (inside a subshell cd) cuts
the probe from ~12.2 KB to ~3.6 KB, so it drops off the over-budget
list instead of raising a ceiling.
* fix(mobile): pin host.platform's read to a platform type, independent of which zod copy resolves
With mobile's own dependencies installed (which CI does for any PR that
touches mobile/), the e2e program resolves two zod copies, and the
salvaged platform arm from src/shared inferred as {}. The schema now
narrows through an explicit platform guard.
* fix(mobile): keep the host.platform read line intact for its RPC mutant; narrow in a second step
Mobile verify: the recorded-RPC mutant anchors on the read line, which
the previous guard rewrote, so the mutant could not apply.
* refactor(cli): app-executable resolution in its own module; drop the platform guard now zod is aligned
launch.ts went one line past max-lines with main; resolveAppRoot,
resolveForegroundOrcaExecutable, getExecutableAppArgs and
stripElectronRunAsNode move to orca-app-executable.ts (re-exported, so
callers are unchanged). With mobile and root on one zod (#31260), the
host.platform schema no longer needs the extra narrowing step.
* refactor(cli): import app-executable helpers from their module; no re-exports from launch.ts
* refactor(serve): move the update handoff file reads and writes into their own module
Move-only: the supervisor module is at its line budget, and the handoff file
IO is a separate concern from the child supervision loop.
Claude-Session: serve-supervisor-reexec
* fix(serve): restart the macOS serve supervisor from the updated app
After a remote update on macOS, the node-mode supervisor kept running the
old Orca executable, which the installer moved aside and deleted. Under a
LaunchAgent the system charges every descendant's privacy permissions
(Accessibility, Screen Recording, files) to that supervisor, so once its
path no longer resolved, screen capture and computer use failed for the
whole served tree while the permission check still reported them granted.
Once the new bundle is verified on disk and no stop is pending, the
supervisor now re-runs itself in place (same pid) from the installed
executable instead of spawning the replacement itself. The handoff stays
install-requested, so startup's existing resume path spawns, verifies and
completes it. A --supervise supervisor that runs from its copy outside the
app restarts through the app's own CLI entry, so it copies the new release
aside again rather than mixing old scripts with the new executable. If the
re-run fails it logs and starts the replacement as before. Only applies
when serving our own executable on macOS; Linux, Windows, orcad and
overridden executables are unchanged.
Claude-Session: serve-supervisor-reexec
* fix(serve): restart a supervisor whose executable the install replaced
Three review fixes on top of the update restart:
- Decide the restart by file identity instead of which path observed the
install. The supervisor records the device and inode of its executable
at start; after any install wait (including resuming an interrupted
update at startup) it restarts only if the installed executable is a
different file. A supervisor that started on the old app before the swap
now restarts too, and the process after a restart never restarts again.
- A relocated --supervise supervisor releases its marker on the copy it is
leaving, so the next relocation can delete that copy. Before, every
update kept one more full copy of the app's scripts for the life of the
service.
- A failed process.execve aborts the process natively rather than
throwing, so the fallback never ran. The restart now checks first that
the executable can run and the script entry exists, and otherwise logs
and starts the replacement directly as before.
Claude-Session: serve-supervisor-reexec
* fix(serve): restart a parked serve service whose app a desktop install replaced
When the desktop app's Update & Restart parks an `orca serve --supervise`
service for another profile, it writes no update handoff for that profile.
After the install the service's outer loop just starts its next server, so
neither update restart point ran and the supervisor kept the deleted
executable.
Every server start now runs the same identity check right after the
park/stop gate and before spawning, so a supervisor whose executable was
replaced restarts in place first. The check is a no-op when the executable
is unchanged, so first starts and the process after a restart do not loop.
The test drives the real park and commit functions, so the CLI typecheck
project now includes that updater module (it imports only shared code).
Claude-Session: serve-supervisor-reexec
* refactor(serve): check for a replaced app in the one place every server start passes
The restart check ran at three call sites and was threaded through the update
loop as a hook, and each review round found another start path it missed. Every
path (first start, resumed update, after an install, after a park or crash)
spawns its server through the function spawnElectronServe builds, after its stop
and park gates, so the check now lives in that spawn and nothing else carries it.
The restart moves to its own module, the hook and the handoff-file move it
forced are gone, and the tests drive serveOrcaApp instead of the hook.
Claude-Session: serve-supervisor-reexec
* perf(serve): copy the supervisor's code aside with one cp clone on macOS
orca serve --supervise copies the app's unpacked code and node_modules out
of the app before re-running itself from the copy. Node's fs.cp clones those
~6,000 files one at a time, which took 8-15 s in the foreground and around
35 s under the service manager's throttled I/O. One /bin/cp -cR per tree
does the same copy in about 1.5 s.
The clone-then-plain-copy fallback the terminal host copy already used now
lives in src/shared/macos-tree-clone.ts, and both copies call it. Linux keeps
fs.cp with reflink cloning, and Windows does not support --supervise.
Claude-Session: serve-snapshot-clone
* fix(serve): let the macOS tree copy run without a time limit and throw on failure
The shared cp helper ran under runProcess's 30 s default time limit, which the old fs.cp copy never had. At background I/O priority a large tree can outlast it, so the supervisor would quietly give up and run from the app itself. The helper now runs cp with no limit; the terminal helper copy stays bounded by its deadline signal.
The helper moves to src/main beside the asar-safe rm and uses it directly, so it no longer takes an rm option. It throws instead of returning a boolean, so copyBundle is gone. The relocation reads process.platform instead of a platform dependency, and its copy test now runs on every platform but Windows.
Claude-Session: serve-snapshot-clone
* fix(build): keep the macOS tree copy module where the packaged CLI loads it
The serve supervisor's relocation code in the CLI now imports the macOS
tree copy module from the main-process sources. The CLI is compiled file by
file, but the later main-process build replaces out/main and keeps only the
files it is told to build as entries, so the packaged CLI failed at load
with "Cannot find module '../../main/macos-tree-clone'" on every platform,
and the CLI runtime verifier rejected the build.
Register the module as a CLI main entry, like the other main modules the
CLI imports, and unpack it from the app archive. Its asar-safe removal
dependency is bundled into a shared main chunk, which is already unpacked.
The build output contract test now covers both.
Claude-Session: serve-snapshot-clone
* fix(build): unpack the asar-safe rm the CLI's tree clone imports
The compiled CLI layout keeps out/main/macos-tree-clone.js requiring
./asar-transparent-fs directly, and that file was still packed, so the
packaged CLI failed to load (caught by packaged-cli-assets in CI). The
bundled build only passed because the main build inlines it into an
unpacked chunk.
Claude-Session: serve-snapshot-clone
* refactor(protocol): each area owns its published runtime capability list
RUNTIME_CAPABILITIES now spreads one array per area instead of a flat
hand-appended list, and runtime-capability-constants.ts is split into
the areas that own each constant. The published set is unchanged and
pinned by an exact sorted test.
* test: preserve capability baseline while allowing additions
* feat(agent.launch): a launch can name chat or terminal, and the CLI gets --surface chat|terminal
agent.launch gains an optional `surface` field, read by hosts advertising
agent.launch.surface.v1. `terminal` always runs the agent's CLI; `chat` runs a
chat even when the user's default is a terminal, and one the host can prove it
cannot open is refused with a typed no-effects error before anything is
created, so a retry answers with the same refusal. The pick is part of the
replay fingerprint. `orca agent launch` and `orca worktree create --agent`
accept --surface chat|terminal and refuse it on a host that would drop it,
including the legacy worktree.create fallback.
* test(agent.launch): pin the surface pick beside the other pre-start failures
* fix(agent.launch): settle requested surfaces from resolved workspace facts
* fix(agent.launch): describe fallback boundaries and correct test types
* feat(speech): set the OpenAI dictation key on a headless host (orca speech openai-key)
Cloud dictation needs an OpenAI key on the host that transcribes, and
desktop Settings was the only way to save one. A headless orca serve or
managed server had none. speech.openAiKey.{status,set,clear} save it
through the host's existing key store and secret sealing; the CLI reads
the key from stdin so it never lands in shell history.
* fix(cli): name the update when an older server lacks the speech key methods; one key length cap; PowerShell example (Opus review)