* fix(i18n): correct Korean Settings translations
Consolidates the Korean Settings corrections from the community audit
(#27820) into one change. 514 catalog values are corrected: reversed
meanings (session search privacy notice), literal machine glosses
(Custom as 관습, gutter as 배수구, Open as 열려 있는), the terminal
caret named after the Cursor app, and Latin terminal/agent/repo left
inside Korean prose. Every value is pinned in the ko key overrides so a
catalog repair cannot revert it.
Co-authored-by: YEONGUK LEE <contact@abruption.dev>
* fix(i18n): keep the contributor's wording where it was already correct
Co-authored-by: YEONGUK LEE <contact@abruption.dev>
---------
Co-authored-by: YEONGUK LEE <contact@abruption.dev>
* 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.
* refactor(automations): main provisions an automation workspace's setup and default tabs
An automation's create handed setup and default tabs back to the window, which
launched them through its own background-terminal code. Main now provisions
them through the same provisionWorktreeTerminals the CLI create uses, and
returns neither, so setup runs once and the window's copy is deleted.
* fix(worktree): translate the Setup tab title wherever it is created
Main had no copy of the automation path's translated title, and the window and
CLI creates used the English literal. All three now read worktree.setupTabTitle
through the existing main and renderer translators. The create path's runtime is
required, since every production caller passes it.
* fix(worktree): keep the Setup title in English; runtime code cannot load main's translator
main-i18n imports Electron, which orcad and the runtime must not load. The
title stays the English literal the window and CLI creates already use.
* refactor(worktree): drop the runtime check the required runtime made dead
Overlap mobile typechecks with tests and avoid repeated filesystem probes and unused device substitutes in recording replays. Keep state isolated between loaders, give exhaustive snapshot cases an explicit execution deadline, and retire obsolete locale keys that blocked repository validation.
* Remove repeated daemon smoke builds and idle compiler slots
* Test compiler scheduling through its process interface
* Keep preview component below the existing line limit with a type-only split
* perf(claude): run the Windows Claude hook as a native orca-hook.exe
Each Claude hook event on Windows started cmd.exe (plus the user's cmd
AutoRun) and then curl.exe on top of the agent's own shell. A small signed
Rust binary now does the same synchronous POST in one process, under Git
Bash, PowerShell and direct CreateProcess alike.
- native/windows-agent-hook: std-only crate, same contract as the .cmd hook
(prints {}, env guards before stdin, endpoint re-read, 0.5s/1.5s caps,
waits for the listener reply, always exit 0). Its installed file name
selects the agent profile, so the registration stays a bare path.
- Packaged as resources/bin/orca-hook.exe (SignPath signs it with the other
inner PEs); afterPack fails on a missing or wrong-machine binary.
- Claude install copies it verbatim to ~/.orca/agent-hooks/claude-hook.exe
(temp + rename, running copy moved aside) and registers it; it falls back
to the .cmd hook when the bundle is missing, unsigned or cannot be copied.
Only Claude opts in, via windowsNativeHookExecutable.
- The managed-command matcher sweeps .exe entries alongside .cmd/.ps1/.sh.
- The CLI launcher and hook builds share windows-rust-executable-build.mjs,
which now also checks the PE machine.
Refs #20913
* fix(build): publish native Windows binaries atomically so parallel builds cannot collide
The Windows CI lane runs every orca-hook.exe suite in one vitest
invocation, each building through build-windows-agent-hook.mjs. The build
copied straight onto native/windows-agent-hook/.build/orca-hook.exe, and
CopyFile opens its destination unshared, so a second build (or a scanner
still holding the fresh image) failed with EBUSY.
- windows-rust-executable-build.mjs skips the publish when the output
already has the built bytes, otherwise stages a per-process temp file and
renames it over the output, retrying briefly while another handle holds it.
- The test fixture hands each suite a private copy of the build output, so
no suite reads or starts the shared file while another build replaces it.
Refs #20913
* refactor(claude): simplify the native Windows hook to one Claude binary
Review follow-up on the native hook (#20913): the same behaviour with less
machinery.
- The binary is built, shipped and installed as claude-hook.exe, so the
installed name matches its PE OriginalFilename. It is Claude-only, so the
file-name profile table and the per-agent opt-in flag are gone; the
registration is gated by the existing Claude-only usesWindowsEntry check.
- Install and refresh reuse writeManagedScript / refreshManagedScriptIfPresent
(now accepting bytes): temp file, rename, skip when identical. A copy whose
hook is still running cannot be replaced; install then registers the .cmd
hook and refresh keeps the old build until a later start, instead of the
rename-aside and *.retired sweep.
- The install-time Authenticode-presence check is dropped. Release CI now
requires resources\bin\claude-hook.exe to be signed (release-cut and the
signing rehearsal), next to resources\bin\orca.exe.
- getStatus counts either Windows form through findManagedEventHookForms in
hook-settings.ts, replacing managed-event-hook-status.ts.
- Rust: plain Claude constants, one endpoint-file lookup (last assignment
wins, as under `call`), one request write and a deadline-bounded drain.
Guard order and form encoding are unchanged.
- Build: PR CI builds the hook once before the Windows tests, so the suites
only read it; the launcher publish goes back to copyFileSync and the retry
loop and its test are gone. The PE-machine check lives only in afterPack.
Cargo.lock joins both crates' build fingerprints, and the launcher's alias
exports are removed.
Refs #20913
* test(ci): run the managed hook stdin-lifecycle suite in the Windows lane
Its claude-hook.exe case (and the other Windows-only cases) are gated per
test, so the lane-registration guard did not require the file and it ran
only on Linux, where those cases skip. Register it in both Windows lists.
Refs #20913
* feat(agent-hooks): ship the native hook as orca-agent-hook.exe <agent>
The native Windows hook (#20913) becomes one general binary that stays
Claude-only in behaviour.
- Named orca-agent-hook.exe everywhere: build output, PE OriginalFilename
and InternalName, resources\bin, the afterPack check, the installed copy
in ~/.orca/agent-hooks, and the release signed-target lists.
- It takes the agent as its first argument and Claude registers
`<path> claude`. The CLAUDE_JOB_DIR exit and the Grok/Devin drain belong
to that agent; an unknown or missing agent answers {} without reading
stdin. The managed-command matcher owns `orca-agent-hook.exe claude` and
still owns the old claude-hook.exe entry, so installs migrate; install
deletes a leftover claude-hook.exe when nothing runs it.
- A copy still running from the previous build no longer forces the .cmd
fallback. Identical bytes are not rewritten; otherwise the temp file is
renamed over the target, and when Windows refuses because the image is
mapped, the running copy is renamed to orca-agent-hook.exe.old first.
Install deletes a stale .old; only when that copy is also still running
does the install fall back to .cmd. writeManagedScript and
refreshManagedScriptIfPresent take the aside path, sharing one helper.
Refs #20913
* fix(agent-hooks): roll back a failed aside rename and keep an installed hook exe
Design-review follow-up on the native hook (#20913).
- If the running copy was moved to orca-agent-hook.exe.old but the new
file then could not take its name, the copy is renamed back before the
error is rethrown, so settings.json never points at a missing exe. The
rename helper now owns the `.old` name, next to the comment that
explains it; a mocked-fs unit test covers both the sync and async paths.
- A failed write, or a build with no bundled exe, keeps registering a copy
that is already installed (every build knows `claude`), so a dev checkout
and an installed build no longer flip settings.json between forms.
- The claude-hook.exe migration is gone: no release ever shipped that
name, so the legacy sweep, the `.exe` matcher needle and their test are
removed.
Refs #20913
* fix(agent-hooks): derive the native hook's agent with win32 path rules
getWindowsNativeHookAgent took the host's basename of the managed script
path, so on a POSIX host (Linux CI) a Windows path was not split and the
registered command became `orca-agent-hook.exe C:\...\claude`. It now takes
the path and applies path.win32.basename itself. The install destination
and the `.old` aside stay on host paths: they name files the host opens.
Refs #20913
---------
Co-authored-by: Orca Worker <orca-worker@localhost>
* fix(mobile): make message and markdown source copy reachable
* fix(mobile): keep copy failure feedback during streaming
* fix(mobile): complete source copy on paired web pages
* Align mobile source Copy controls with desktop
* Update chat status test for icon-only Copy
* Preserve literal source when copying own messages
* fix(mobile): show a message's copy button only after tapping it
Like desktop's hover-only copy, the chat message copy button stays hidden
until the message is tapped; another tap hides it. The tap target is not a
single accessible element, so screen readers still reach the text inside.
* fix(mobile): a held press on a message selects text without toggling Copy
On iPad the release of a long-press selection, or of a selection-handle drag,
also counted as a tap and toggled the message's copy button. Claiming the long
press keeps only quick taps as toggles; native selection is unaffected.
* test(mobile): load the web clipboard in the copy-button test the way other tests do
vi.importActual with a relative path resolved from the mocker under CI's
runner and failed to find ../platform/clipboard.web; a plain import() in the
mock factory resolves from the test file.
* chore: upgrade terminal dependencies to the matching payload betas
* test: harden xterm payload contract, align mobile pins, fix serialize-at-ref across version bumps
- Cover foreign-payload replacement and the shared blank-cell reset in the
installed image payload contract (both mutants previously survived).
- Bump mobile's xterm, webgl and unicode11 pins to the root's betas.
- Resolve the ref's own serialize version and patch so the differential
fuzz baseline builds across a version bump.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
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.
* fix(claude): register a statusLine Windows PowerShell can run
Claude runs the statusLine under Git Bash when present and otherwise under
PowerShell, which cannot parse the POSIX runtime-home command Orca registered.
On Windows without Git Bash the statusline never posted rate_limits, so the
live Claude usage feed went dark. Register the same shell-neutral .cmd
invocation the lifecycle hooks use; the old form is still recognised as
Orca-owned and replaced, and a user's own statusLine is never touched.
* fix(claude): keep the statusLine free of a per-tick PowerShell launch
A profile path the bare form cannot carry (a space, non-ASCII, quotes) fell
back to the encoded PowerShell launcher, which would start a hidden PowerShell
on every statusline tick and print "{}" if the script went missing. Register
the bare .cmd path only when it is cmd-safe and keep the runtime-home form
otherwise, so Git Bash users on such paths still run the .cmd directly.
Fold the registration tests into claude-status-line-install.test.ts and add a
real Git Bash run for a profile path with a space.
* test(claude): pair the stubbed win32 platform with win32 basename
The Windows statusLine case fed a Windows path to getManagedCommand on a
Linux runner, whose posix basename does not split backslashes. Production
only takes that branch on real Windows, where node:path is path.win32.
---------
Co-authored-by: Orca Worker <orca-worker@localhost>
* fix(daemon): verify daemon PID identity from the native Windows process table
The Windows daemon PID check forked powershell.exe Get-CimInstance through
node:child_process with no windowsHide, flashing a console and costing
~850ms per check. Read the command line and creation time from the shared
windows-process-table reader instead, and route the POSIX ps queries
through @orca/process-host.
Refs #27390
* refactor(daemon): drop the ps migration and trim the PID identity tests
Keep #27703 to the PowerShell removal: restore the macOS/Linux ps paths,
their tests and the process-host baseline entry. Collapse the mocked
Windows identity cases into one table, keep only the real-addon native
case, and move the creation-time fallback beside queryWindowsProcess.
---------
Co-authored-by: Orca Worker <orca-worker@localhost>
* fix(rate-limits): spawn the Codex usage probe off the main thread
On Windows, creating a cold standalone codex.exe can spend seconds in code
integrity checks on the calling thread. The usage probe runs on window focus,
so that froze the UI. The probe's child is now created and piped on a worker
thread and proxied back to the main thread.
Fixes#27362
* refactor(rate-limits): type the worker's children through @orca/process-host
* fix(rate-limits): kill an orphaned Codex probe by pid when its worker dies
Once the worker thread that owns the probe's child is gone, kill() can no
longer reach the child through it. The proxy now falls back to a pid-addressed
kill; the Codex probe uses the app-server tree kill so a node launcher's
codex.exe child goes too, while SIGTERM stays a graceful root signal.
* refactor(build): move the main build's entry map out of electron.vite.config.ts
The new worker entry pushed electron.vite.config.ts past the 300-line limit.
The Rollup input map for the main build now lives in its own module.
* refactor(rate-limits): drop the orphan pid kill and trim the worker child-process stand-in
A worker thread's death closes its children's pipes, so an abandoned Codex
probe already gets stdin EOF, its graceful stop. The pid-addressed orphan kill
was redundant with that and with terminateCodexProbeChild's own gated Windows
tree kill. Its hard kill also bypassed the tree-kill gate's recycled-pid refusal
by turning the handle-addressed fallback into a pid kill.
- Remove orphan-pid-kill, killOrphanedCodexProbe, the injected killOrphan, and
the orphaned flag. kill() now reports false once a child is no longer tracked.
- Process-creation failure is one 'spawn-error' event that the stand-in expands
into error/exit/close, replacing the duplicated triple and the exported exit
code. Stdin errors arrive as child errors, as the probe already treated them.
- Fold both suites into one over a real MessageChannel and real children. Add a
real-worker test showing that terminating or crashing the worker lets the child
exit on stdin EOF.
* test(rate-limits): run the worker child-process suite under Node
The suite asserts Node behavior that Electron main relies on: a failed spawn
closes with a null signal, and a dead worker thread closes its children's
pipes. Bun reports an undefined signal and, on Linux, leaves the pipes open,
so the Bun lane failed on runtime differences, not on the product code.
* test(rate-limits): count a reaped-by-nobody zombie as an exited child
On Linux and macOS a child whose worker thread died gets stdin EOF and exits,
but libuv reaps a child only through the handle on the loop that spawned it.
Once that loop is gone, nothing waits on the pid, so it stays a zombie and
kill(pid, 0) still succeeds. The real-worker tests now treat a zombie as
exited, and still require the child's EOF marker.
---------
Co-authored-by: Orca Worker <orca-worker@localhost>
#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>
* [CI] Remove wall-clock waits and stabilize GC fairness check
* [CI] Include the final GC timer gap in the fairness check
* [CI] Advance test clocks and preserve cleanup checks
* ci: ratchet synchronous child-process launches in main and shared code
The Electron main process freezes every window while a synchronous child
process runs, and on Windows each launch is slow because antivirus scans
it. Nothing stopped new synchronous launches from landing in main-process
code, and the spawn observer behind ORCA_MAIN_THREAD_DIAGNOSTICS only saw
async spawns, so the worst blocks were invisible to it.
Ratchet: check:main-sync-spawn-ratchet parses production src/main and
src/shared files (test-only paths excluded) and fails when a file not in
config/main-sync-spawn-baseline.txt reaches execFileSync, spawnSync or
execSync from child_process, runProcessSync from @orca/process-host, or
one of the synchronous secure-file writers (they run icacls on Windows).
Named, aliased, namespace, default, require() and import() forms count.
A baseline entry whose file no longer launches synchronously fails as
stale, so the list can only shrink. It runs in lint and in the PR
static-analysis job next to the process-host import check.
Observer: runProcessSync now reports to the spawn observer, timing the
whole call (that is how long the thread was stuck) and reporting from a
finally so failing, throwing and timed-out children are counted too.
Runtime behavior is unchanged except that the diagnostics observer now
sees synchronous launches; with no observer registered the sync path
pays one extra branch.
* ci: count sync launches per file and gate the repo's sync wrappers
The sync-spawn ratchet only tracked which files launched synchronously, so a
file already on the list (wsl.ts, win32-utils.ts, git-exec-file.ts) could add
more launches unnoticed, and new callers of the repo's own synchronous
wrappers passed CI.
The baseline now stores a per-file count of uses, in the same format as the
focus-read ratchet: a rise or a new file fails, a lower or zero count fails
until `pnpm check:main-sync-spawn-ratchet --prune` lowers it, and --prune
never raises an entry. Every value reference to a gated function counts
(call, hand-off, alias and its uses, re-export); imports alone count nothing.
Gated wrappers: gitExecFileSync (and the git/runner re-export),
restrictWindowsPathSync, hardenSecurePath when called with sync: true,
grantDirAcl, getWslHome, listWslDistros, wslUncDirectoryExists,
isWslAvailable (and the wsl.ts re-export), isPwshAvailable. Their 18
current caller files join the baseline.
Failure messages now give advice per family: the async process-host API for
child-process launches, the async twin for wrappers, and for the secure
writers a note that no async secure writer exists yet, so the sync one stays
and must never become a plain write (that drops the Windows ACL hardening).
Also refresh two stale blockMs descriptions and drop a no-op line from the
spawn-observer "stays silent" test.
* ci: count hardenSecurePath unless visibly async, and tighten ratchet tests
hardenSecurePath was counted only when `{ sync: true }` was written inline,
so moving the options into a variable or a spread dropped the count and the
check then asked for a prune. It now counts every use except a call with no
options or a spread-free object literal whose `sync` is absent or false.
Also: resolve backslash paths in the importing file as well as the
specifier; still visit heritage and instantiation expressions
(`extends mixin(fn)`, `fn<T>`); state the reference-only counting limit;
and point the secure-writer advice at raising this file's baseline count.
Tests cover the hardenSecurePath shapes, the alias chains that need the
fixpoint loop, and a prune that must not raise a grown row.
* test(ci): pin the main sync-spawn ratchet background step
* fix: count secure hardening calls with spread arguments
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#26917Fixes#23229
* 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>
* 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).
* 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.
* refactor(terminal): one remote input hold
* fix(terminal): keep restored typing scoped to its terminal until the connect settles
* refactor(terminal): name the restored-input expectation type
---------
Co-authored-by: m4air <m4air@Mac.localdomain>
* Share immutable historical source across local cross-version test caches
* Add preview-first cleanup of old historical test caches
* Include historical test cache cleanup in worktree space maintenance
* Report and retain read-only Electron installs during cleanup
* Protect read-only installs under root and isolate cleanup CLI fixtures
* Skip unnecessary compatibility-boundary parsing and benchmark it in CI
* Use pinned preflight Bun for compatibility checks and localization extraction
* Measure two concurrent Bun projects in full preflight and update its scope test
* fix(bench): make the idle-CPU benchmark run and report CPU on Windows
Three independent Windows defects in the idle-CPU benchmark (its report is
what Windows performance work would cite):
- `buildAppIfNeeded` spawned `npx`, which is `npx.cmd` on Windows, so
execFileSync failed with ENOENT before any sampling. Build through
`config/scripts/run-electron-vite-build.mjs` instead — the repo's own
Windows-safe wrapper, which also runs the boot-graph verification.
- `readWindowsProcesses` returned `cpuTimeSeconds: null`, so the sampler's
per-process CPU delta (idle-cpu-process-sampling.mjs:198) fell back to
the hardcoded `percentCpu: 0` and every CPU KPI in the report was zero.
Read `UserModeTime`/`KernelModeTime` from `Win32_Process` (100 ns units)
and keep `null` when a host does not return them.
Measured on Windows, same options before and after the CPU fix:
| class | mean CPU before | mean CPU after |
| --- | ---: | ---: |
| renderer | 0 | 2.20 |
| gpu | 0 | 2.08 |
| utility | 0 | 0.11 |
| agent-or-node | 0 | 0.06 |
| total | 0 | 4.51 (p95 8.40) |
RSS is unchanged (934 MiB -> 937 MiB), so only the CPU reporting changed.
Refs #25235
* fix(bench): reject a missing Win32_Process time instead of reading it as zero
Review follow-up (CodeRabbit). `Number(null)` is `0`, which is also a real
counter value, so an absent `UserModeTime`/`KernelModeTime` passed the
finite check and the sampler would report a kernel-only delta instead of
taking the fallback. Require an actual number from the CIM payload.
Verified on Windows that the real payload still reports CPU once the check
is tight: total mean 4.40%, p95 14.48%, renderer 3.51%.
* test(bench): cover the Windows CIM CPU time mapping
The mapping was only reachable through Get-CimInstance, so no test ran it.
Lift it into cimCpuSeconds and assert the 100ns-to-seconds sum plus the
missing / string / non-finite cases that must stay null.
* fix(bench): read the Windows process table as UTF-8 so JSON.parse survives
PowerShell writes to a redirected stdout in the OEM code page (437 here),
which best-fits characters such as U+2022 and U+2192 in any running
process's command line to raw 0x07 / 0x1A bytes. ConvertTo-Json had left
them unescaped because they are not control characters in UTF-16, so the
sampler's JSON.parse failed with "Bad control character in string literal"
whenever such a process happened to be alive -- hence intermittent.
Set [Console]::OutputEncoding to UTF-8, as the app's own CIM scan does,
and cover it with a Windows-only test that fails without the fix.
Fixes#25236
* fix(bench): pass the local runtime to fetchRepos in the idle-CPU fixture
fetchRepos now requires a runtimeEnvironmentId, so the fixture's bare call
threw "Cannot read properties of undefined (reading 'runtimeEnvironmentId')"
before sampling on every platform. null selects this app's own catalog, as
the e2e helpers already do.
* ci(bench): run the Windows process-table test in the Windows lane
The UTF-8 regression test self-skips off win32, so it must be registered in
both Windows lists or it runs on no machine.
---------
Co-authored-by: Paolo <paovalletta@hotmail.it>
Co-authored-by: Orca Worker <orca-worker@localhost>
* fix(bench): resolve dynamically imported sources as file URLs
`await import(path.join(ROOT, '...ts'))` hands Node a bare Windows path
(`C:\...`), which the ESM loader rejects as an unsupported URL scheme
("Received protocol 'c:'"). The two benchmarks that load production
modules this way crashed on Windows before running a single scenario,
while working on macOS and Linux where an absolute path is a valid
specifier.
Wrap each computed specifier in `pathToFileURL(...).href`, the form the
other benchmark scripts already use for the same purpose.
Verified on Windows: bench:session-write-hot-path and
bench:agent-inspection-cadence now run and print their measurement
tables. The fish mode-2031 trace capture resolves its import too, but its
harness targets macOS/Linux shells and cannot complete on Windows.
Fixes#25235
* style(bench): format the session-write benchmark's file-URL import
---------
Co-authored-by: Paolo <paovalletta@hotmail.it>
Co-authored-by: Orca Worker <orca-worker@localhost>
* fix(native-chat): put chat text on one type scale that follows Text size
Sizes were picked per component, so the same kind of row came out 12, 13 or
14px and the Text size setting reached only a few of them.
* fix(native-chat): scale chat spacing and icons with Text size
Icons and gaps stayed fixed while text grew, so at larger sizes they looked
undersized and sat off-centre.
* fix(native-chat): implement PR #26685 review fixes
- Scale row gap with text size so controls don't overlap
- Clamp 'Match terminal interface' to 12-20 for labels/controls
- Use existing --status-warning token for warning color
- Rewrite stale test for scaled typography tokens
- Use app colors for in-progress tasks in composer dock
* fix(native-chat): leave the task list's dock colour as it was
* fix(native-chat): message text follows the terminal size; controls and spacing stay in 12-20
* test(native-chat): labels and spacing follow the bounded chat size; main's new notice test uses the chat colour
---------
Co-authored-by: Brennan Benson <79079362+brennanb2025@users.noreply.github.com>
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>
* 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>
* 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.
* test(e2e): record mixed-version, two-client and phone layout baselines on main (P3)
* test(e2e): P3 review fixes: reconnect and I/O proof, restart order bounds, Linux-only baselines
* test(e2e): re-record Linux phone and serve-restart baselines
* test(e2e): P3: echo drawn in the desktop pane; bound the group selection after restart
* test(e2e): re-record serve-restart group selection
* test(e2e): name P3 baseline helpers for what they record
* fix: preserve module URLs in the headless server bundle
* test: exercise structured chat on the packaged Node server
* test(orcad): run the packaged chat check from the desktop's server template
The chat check loaded the server bundle directly, so a desktop build missing its
server template still passed. Materialize the slot from the built template the way
the desktop does, and replace the fixed streaming pause with a test-owned
acknowledgement so the partial-reply assertion can't race.
* Connect the packaged chat test through a paired connection
Main now sends remote runtime requests over a paired connection rather than a
pairing offer, so the test converts the server's offer once when it is ready.
* fix: preserve structured chat work during automatic server stops
* fix: require stop-request support before automatic cleanup
* fix: retain unverified session owners until exit is proven
* fix: preserve unreadable work and retry expired prompt stops
* fix: give unheld reservations and queued sends retirement exits
* fix: clear empty recovery leases and preserve unverified owners in tests
* Keep managed servers alive while chat providers are loaded
* Recheck returning clients before managed idle shutdown
* test: run the managed idle exit chat provider test in the Node runtime
It builds on the native chat rest test rig, which opens a real SQLite database.
* ci: the layout oracle gates merges
An oracle-only job joins verify's needs. It runs only the two layout oracle
specs through the existing e2e workflow, routed by layout writer paths; a PR
outside the route skips it and verify treats the skip as a pass. The route is
widened to the terminal store, terminal pane, graph sync, frozen mirror and
compatibility folders while the layout-holder stack is open. The oracle specs
leave the advisory e2e job so a PR does not run them twice.
* ci: the layout oracle tests the merge commit and routes its harness and stack writers
Review fixes: run on the merge commit so a stale-base PR cannot pass on an old
main; route the oracle's own e2e harness and runtime client (a change there broke
main's oracle without running it), and the window-side layout writers the stack
edits; move the route into its own module.
* ci: route the remaining layout writers the stack edits to the layout oracle
* refactor(ipc): filesystem and repo handlers are local or runtime only
* test(ipc): drop two casts in the rewritten listing and import tests
* fix(providers): keep the default error name so IPC callers see the old text
---------
Co-authored-by: m4air <m4air@Mac.localdomain>