* 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
Client-hosted browser pages for a server Orca deployed (WSL hosts today; managed
SSH desktop tabs use the SSH route) now derive their partition from the
environment and the server's public key, not the pairing revision and device id
that rotate on every re-pair. Existing jars are adopted, and each binding keeps
the shipping build's fingerprint (with the stable one as an alias) so a
downgraded build still opens the same jar.
* perf(ci): accelerate adhoc release builds and runner scheduling
* Overlap adhoc release preparation and reuse qualified build inputs
* Bind qualified template reuse to its embedded CLI source commit
* Use the available CPUs for native compilation in fast dev builds
* Verify qualified template cache provenance and daily invalidation
* Avoid incremental MSBuild tracking for fresh fast-release builds
* Right-size Intel macOS and Windows ARM release qualification
* Route Intel qualification through the standard larger macOS label
* Overlap Windows preparation and initialize the server native compiler once
* Test server compiler invocation through its process-runner interface
* Align adhoc signing contract with its larger packaging runner
* Recognize the native cache fingerprint as batch-file metadata
* 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
* fix(native-chat): name a chat from its first message at once; the generated name replaces it
A native chat's one stored name was written only by a best-effort, one-shot
generation, so any failure left every surface on the 'Codex Chat' placeholder.
The first prompt now names the chat when its first turn starts, and the
generated name replaces it only while the field still holds that seed.
* fix(native-chat): the chat names setting says a chat shows its first message until it is named
* fix(native-chat): retry a failed first chat name twice before keeping the first message
A failed name generation was final for the chat's lifetime. The first name now
gets two retries, after 2 and 4 seconds, and stops early once the
first-message name was replaced.
* fix(native-chat): show a failed chat name on the tab hover card, with Regenerate name
A chat whose name generation failed after every retry gave no sign of it, and
nothing could run it again. The host now keeps the last failure in memory and
publishes it with the chat's status while the chat still has the name it failed
to replace; the tab hover card shows it with the error dialog, and a new
agentSession.regenerateName (behind a capability) makes one more try.
* fix: derive chat naming progress from host jobs
* fix(native-chat): merge duplicate import CI caught
* fix(native-chat): declare agentSession.regenerateName in the cross-version surface manifest
* fix(native-chat): stub naming-failure hook in SortableTab rename shortcut test
* fix(github): an issue-source change on a twin saves to the row the user changed
setIssueSourcePreference resolved its target by repo id alone, and since
#31119 that refuses a repo id with rows on two hosts. Callers now pass the
row's host, the write goes through updateRepo with that host, and the
optimistic patch touches only that row.
Co-Authored-By: Claude <noreply@anthropic.com>
* fix(github): find a twin's Tasks row by host, not path
Co-Authored-By: Claude <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
* feat(editor): unused editor-buffer identity key (phase 1b-1)
Adds `editorBufferIdentityOf`, not consumed yet: [environment, host, pairing root, path key] with
exact case, normalized Windows separators and drive letter, WSL alias folding only for a local
Windows client, SSH keyed with its runtime environment, and a detached record id for retired hosts.
Splits `resolveEditorModelOwner` out of the owner-key function; the owner key is unchanged.
Co-Authored-By: Claude <noreply@anthropic.com>
* fix(editor): editor-buffer identity keys can't falsely merge files (phase 1b-1 review)
- A folded WSL alias is a tagged tuple, so a real `\\wsl\<distro>` share can't collide with it.
- `//` is ambiguous between POSIX and UNC, so it folds only for a known-Windows (local) host;
drive and `\\` paths still fold separators and the drive letter on any host.
- A proven UNC path folds its server name, matching the Monaco URI authority today.
- SSH identity includes the target's endpoint incarnation: a target id edited to point at another
machine gets a new key, a reconnect keeps it; an unknown endpoint never shares.
- Documents the live pairing resolution and per-event retirement ids.
Co-Authored-By: Claude <noreply@anthropic.com>
* fix(editor): identity takes the tab's captured SSH endpoint and a verified host path flavor
- The SSH endpoint is the one the tab captured at open, passed in by the caller; a live lookup
would match an old tab to whatever machine the target id names now. Missing means never share.
- Remote hosts accept a verified path flavor (`windows` | `posix` | `unknown`), so a Windows host's
`//SERVER/share` folds like `\\SERVER\share`, as the Monaco URI authority does today.
Co-Authored-By: Claude <noreply@anthropic.com>
* refactor(editor): route close, discard, pending flush, restart save and save keys through the buffer view index (phase 1b-2)
No behaviour change. Each tab still has its own record; this adds the indirection the sharing
switch will change instead of call sites:
- editorBufferIdOf / editorBufferViews, maintained by the record middleware with openFiles
- the invariant now covers the index, records without views, and drafts across a record's views
- writeEditorBuffer, setEditorDraft and clearEditorDraft(s) write every view of the record
- needsSavePromptOnClose at every close site, discardEditorBuffer, record-wide pending flush,
restartSaveTargets, and injected save-queue keys (queue = model URI, timer/generation = record)
Parity tests check each entry point against the old per-tab expression across a fixture matrix.
Co-Authored-By: Claude <noreply@anthropic.com>
* refactor(editor): require the buffer view index, close in processing order, cheaper draft checks (phase 1b-2 review)
- Lookups take a required EditorBufferViewIndex; tests pass an explicit empty index.
- needsSavePromptOnClose drops the batch set: every site asks against current state as it
processes each view, so a record's last open view prompts once in any order.
- Drafts-only writes check draft agreement only; the index rebuild is skipped when no tab
joined, left or moved record.
- Artifact upload flushes through the record; the per-tab flush/has functions are gone.
- Membership and adoption read record ids from the index; drop the save-key injection and
the test-only useTerminalSaveDialog migration.
Co-Authored-By: Claude <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
* The window shows each layout records frame record by record, without reading its session back (dark, layout-owner build only)
A records frame no longer re-serializes the window's whole session and runs the
Loader. Panes' terminal bindings and live titles apply alone, to the tabs the
frame names; any other record builds only its own workspace's slices, with the
view and facts read from the store. Only a workspace the store does not show
from frames yet (startup, a reopened stream, a first visit, one sent whole)
takes the full build.
Tests install an apply check that, after every flush, compares the store with
the full build for the same cache and pre-flush store; the shared layout-apply
transport now sends main's records frames and fails a flush that differs.
* A frame of only tab rows shows without reading the window's session back or running the Loader (test, layout-owner build only)
* The account-restart test's fake binding runtime carries per-pane binding generations, so it passes with the switch on (test only)
* The account-restart test types its store writes, so its five old e2e type errors are gone (test only)
* A workspace build reads the editor selection and groups through the save's own writers, as the full build does (dark, layout-owner build only)
* A tab released from the window's hold shows the binding main wrote meanwhile (test, layout-owner build only)
* The frame-apply check still checks a flush's other workspaces when it builds one in full, and the shared transport keeps a test's own check (test only)
* The frame-apply check is proven to catch a mismatch beside a full build, and the shared transport restores only the check it installed (test only)
* A tab move shows from main's frame alone; the window no longer re-applies it or adopts main's group id (dark, layout-owner build only)
On main's answer the window only selects the moved tab in its new group, a view choice the
frame does not carry. While main's stream is down it still moves the tab itself, as the frame
sync's closed-stream rule says, but under its own group id: the reopened stream's snapshot
redraws main's copy.
The three switch-on census files for tab moves now pass without a switch mock: the context
menu's move routes to the tab bar's move, the drag hook's pane-column split goes to main in a
layout-owner build, and the tab bar's own move is pinned on a paired server's workspace. A new
frame test pins each move through main and its frame.
* A tab's rename, color, pin, view and a pane's title show from main's frame alone; the window no longer applies them again (dark, layout-owner build only)
* A divider drag sends main the ratios it moved, not the window's tree, and an in-tab pane move shows from main's frame; the ratio hold is gone (dark, layout-owner build only)
* Divider ratio params and their command tests get their own modules (file size)
* Layout-writer baseline pruned: the tab move no longer writes the layout from the window
* The window never draws a tab prop, pane title or in-tab move itself; while main's stream is down the reconnect's snapshot shows them (dark, layout-owner build only)
* View-mode test seeds its paired-server workspace without a type assertion
* A moved tab is selected in the frame flush that shows the move, unless the user selected since; main's answer no longer selects it (L120, dark, layout-owner build only)
* Frame sync stays within its line budget
* Frame sync writes its patches through the moved-tab selection, within its line budget
* The moved tab's selection reuses activateTab's own store update, so no new layout writer is added (L120)
* Format the tab-move frame test
* A tab moved while main's stream is down is selected by the reconnect's snapshot that shows the move (L120)
* A generated tab title main gave is drawn by its frame alone; the window writes only titles left to it as today, and the rename test checks the phone graph republishes (dark, layout-owner build only)
* A divider ratio names the split it means, and main refuses it where that path holds another, so the window snaps back to main's ratios; a drag back to main's ratio before it answers is sent too (dark, layout-owner build only)
* test(layout-owner): tab moves drawn from frames make no window tab or group writes
* test(layout-owner): a reopened tab shows its title and color from main's frame
* test(layout-owner): the tab bar's preview pin through main ends the preview and counts a use
* test(layout-owner): read main's generated-title renames without a type assertion
* test(layout-owner): type the sent renames so the title helper reads them without an assertion
* The frame-apply check selects the moved tabs the flush selected, so the tab-move tests exit clean and a wrong selection still fails (dark, layout-owner build only)
* test(layout-owner): the title batch's live labels are checked in both builds; only the generated title differs
* Main keeps a tab's first agent-generated title unless the window's title replaces it, so a second prompt while main's stream is down no longer renames the tab (dark, layout-owner build only)
* test(layout-owner): a revealed terminal's tab name is drawn from main's frame, not its reply (dark, layout-owner build only)
---------
Co-authored-by: E2E Test <e2e@test.local>
* wip(layout): applyDefaultTabs command in the layout model (dark)
* wip(layout): applyDefaultTabs takes the window's proposed ids; main refuses a taken one
* wip(layout): main closes or keeps an exited split pane by the window's rule (dark)
* wip(layout): a create or split sent again after its close is refused (dark)
* wip: typed test doubles
* wip(layout): main writes a worktree's default tabs in one command and starts them, window or none (dark; twin e)
* wip(layout): main writes a worktree's default tabs in one command and starts them, window or none (dark; twin e)
* wip(layout): main provisions setup and default tabs even when the agent fails to start; no window takes over (dark)
* wip(layout): pane start status in frames; a failed start keeps its pane; a terminal close stands after a failed write (dark)
* wip: lint fixes (max-lines, test throws)
* test(layout): a terminal close stands after a real failed profile write; switch off still refuses
* fix(layout): checked types in the close-failure rule and its tests
* fix(layout): a failed start keeps its pane whatever failed; default tabs start once; a tab's last pane keeps its card (dark)
* test(layout): the agent's kept pane counts as the workspace's terminal; main starts it whatever failed
* fix(layout): the switch-off create path gains no await
* fix(layout): a failed start never writes back a pane the user closed (dark)
* fix(layout): a failed start's pane goes through main's create rules, so a closed tab stays closed (dark)
* test(layout): D1a's switch-on close tests assert the intent main receives (both builds)
* test(layout): the close fixture answers main's command without naming the method
* fix(layout): a re-created pane binds as designed; the id-only ratchet reads a shared import (dark)
* fix(layout): a pane's start status may sit on a pane still bound to its old terminal (dark)
* fix(layout): a pane's start status ends when its pane binds again by any path (dark)
* test(layout): same-id restore ends a status; a replayed split brings back a closed pane by design
* test(layout): terminal reply fixtures captured from the window-owned build, and main's switch-on proofs
* wip(layout): main's terminal surface index feeds the runtime graph (dark)
* wip(layout): main's panes stand in for the window's terminal list (dark)
* test(layout): the window sends main-listed panes as mount facts (dark)
* test(layout): surface proofs use public runtime calls
* chore: lint
* feat(layout): close by handle closes in main's layout; the close-time relist goes (dark)
* feat(layout): main's binding outranks a caller's pane claim on the terminal record (dark)
* test(layout): window-list tests stay on the rollback build; switch-on mount-fact cases (dark)
* wip(layout): main's terminal index reads E1's shared projection and follows its operation ends (dark)
* refactor(layout): the main-surfaces host lives beside the session-tabs readers, keeping the graph file under its line cap (dark)
* fix(layout): main's panes skip the window-era membership fence, follow workspace routing, and list a terminal when it registers; window close only hands the graph back (dark)
* refactor(runtime): explicit-close PTY stops move unchanged to their own module, for the file's line cap
* test(layout): the cold-restore table runs in a layout-owner build too: one push per registration
* test(layout): the parked and automation pane tests assert the same window report in both builds (mount facts with the switch on)
* chore: one layout-owner switch import after the restack on D1a
* feat(layout): main's terminal.list rows carry the host's verdict; a pane with a status is never live (dark)
* chore(layout): fit main's surfaces under the lint gates after the D1a restack
* feat(layout): every pane status D1a publishes keeps a row from reading live; remote host answers pass through (dark)
* feat(layout): a drag-out moves main's own binding and refuses the home copy's last pane; the window's terminal claim is not consulted (L127, dark)
* fix(layout): a tab close by a starting pane's handle stops the tab's terminals; a starting pane is never exited on the inventory alone; terminal.list lists every saved pane main owns; the pane claim lookup leaves the output path (dark)
* fix(terminal): keep shipped exit and default-tab behaviour with the layout switch on
An exited pane closes through the legacy retirement unless the keep rule holds; the
default tab's title and color still reach the window and the phone's live tabs.
* fix(terminal): one id guard for every pane write; keep panes on an unvouched -1
Main's taken and closed-id guard now runs where every pane write goes, so a runtime create
replayed after its tab closed is refused before it spawns. An exit code nothing vouches for
keeps the pane, as the window does; a kept pane sends the phone no retirement. Converts the
switch-on tests that asserted the deleted window hand-off and title bridge.
* fix(terminal): keep the closed-tab guard on every pane write, held-id checks on client commands
A paired host starts each pane of a client's tab as a create under the tab's id, so a held tab
id is refused only for a client's own layout command.
* fix(terminal): a provider-reported -1 keeps the pane, as the window does
Only a requested stop or a host-confirmed death closes a pane on a negative exit code. Pins that
the closed-tab guard never fires in the shipped build.
* chore(terminal): satisfy the exhaustive-switch and test typing checks
* test(terminal): give the replay test's live process a cwd
* fix(layout): main's index reads routing only for a workspace two partitions hold, the fence reads ownership once, a legacy pane keeps its claim; switch-on tests assert main's answers after a window closes (dark)
* refactor(runtime): a worktree stop's terminal collection moves unchanged to its own module, for the file's line cap
* fix(layout): main's index routes every workspace as the layout stream does, re-reading routing only on a write or a terminal registration; a worktree stop names every terminal main binds; a starting pane is unverifiable (dark)
* feat(layout): every pane status main publishes keeps a row from reading live; a resuming pane is unverifiable like a starting one (dark)
* fix(runtime): a CLI send, wait or read on a saved pane main holds starts it once first (switch-on builds only)
* fix(runtime): the caller's handle names the terminal a saved pane starts; terminal.send starts it before its checks (switch-on builds only)
* fix(runtime): act on the started terminal's handle until main binds the pane; bound the start by the caller's abort and wait deadline (switch-on builds only)
* fix(runtime): a restored pane's start leaves the rest of a wait's deadline and cleans its bounds on first settle; tighter A1c tests
* fix(runtime): a restored pane's start is capped by the pending-terminal create bound for every CLI entry point (runtime_timeout)
* fix(runtime): only a handle whose saved pane needs a start goes async; every other send, wait and read stays synchronous as before
---------
Co-authored-by: E2E Test <e2e@test.local>
* feat(editor): unused editor-buffer identity key (phase 1b-1)
Adds `editorBufferIdentityOf`, not consumed yet: [environment, host, pairing root, path key] with
exact case, normalized Windows separators and drive letter, WSL alias folding only for a local
Windows client, SSH keyed with its runtime environment, and a detached record id for retired hosts.
Splits `resolveEditorModelOwner` out of the owner-key function; the owner key is unchanged.
Co-Authored-By: Claude <noreply@anthropic.com>
* fix(editor): editor-buffer identity keys can't falsely merge files (phase 1b-1 review)
- A folded WSL alias is a tagged tuple, so a real `\\wsl\<distro>` share can't collide with it.
- `//` is ambiguous between POSIX and UNC, so it folds only for a known-Windows (local) host;
drive and `\\` paths still fold separators and the drive letter on any host.
- A proven UNC path folds its server name, matching the Monaco URI authority today.
- SSH identity includes the target's endpoint incarnation: a target id edited to point at another
machine gets a new key, a reconnect keeps it; an unknown endpoint never shares.
- Documents the live pairing resolution and per-event retirement ids.
Co-Authored-By: Claude <noreply@anthropic.com>
* fix(editor): identity takes the tab's captured SSH endpoint and a verified host path flavor
- The SSH endpoint is the one the tab captured at open, passed in by the caller; a live lookup
would match an old tab to whatever machine the target id names now. Missing means never share.
- Remote hosts accept a verified path flavor (`windows` | `posix` | `unknown`), so a Windows host's
`//SERVER/share` folds like `\\SERVER\share`, as the Monaco URI authority does today.
Co-Authored-By: Claude <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
* test(wsl-hosts): pin every add-flow branch and each liveness building block
* test(wsl-hosts): tighten add-flow and liveness characterization per review; pin main's release on a failed connect
* refactor(wsl-hosts): move the add flow to wsl-host-registration; split liveness into state, evidence and monitor
* fix(process-host): let a slow Windows taskkill finish instead of orphaning the grandchild
Co-Authored-By: Claude <noreply@anthropic.com>
* fix(process-host): abandon taskkill shortly after the root exits so it cannot walk a reissued pid
Co-Authored-By: Claude <noreply@anthropic.com>
* test(process-host): log the taskkill outcome and caller settle time under load
Co-Authored-By: Claude <noreply@anthropic.com>
* ci: register the late-taskkill Windows test in the package-windows path list
Co-Authored-By: Claude <noreply@anthropic.com>
* fix(process-host): release the git admission slot on root close again; cap the background taskkill at 30 s
Co-Authored-By: Claude <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
tsconfig.cli.json listed 284 src/main files because composite made any
unlisted import fail the parallel typecheck with TS6307. Nothing references
it as a project, so drop composite and keep only the CLI's roots plus the
backup worker entry, which is loaded by path. The Electron import ratchet now
also walks the CLI entry, replacing the fence the list implied.
* fix(github): PR conflict previews fetch the base branch through the fetch controller
The conflict summary on a PR card spawned its own `git fetch origin <base>`,
outside RuntimeRemoteFetchController: it refetched a base a worktree create
had just refreshed (and vice versa), ran git's auto-maintenance in the
background, and never armed the idle loose-ref sweep.
It now asks the runtime's controller for the exact-base refresh creates
already use (same argv, per-remote queue, dedupe and freshness), waiting at
most the old 10 s before falling back to the local ref. The runtime registers
its controller at construction, since the IPC and poller paths have no
runtime handle.
Also corrects the controller comment that claimed every fetch skips
auto-maintenance: full fetches keep it on purpose (#8039).
* test(github): read the mocked fetch's WSL option without a type assertion
* test(github): give the conflicting fallback-lookup test a git mock for its conflict summary
* test(github): check WSL routing without duplicating fetch policy
* fix(git): bound preview fetch ownership without cancelling create
* Preserve repository identity after preview cancellation
* refactor(native-chat): remove three chat mechanisms nothing drives
- The adapter's optional startAnswered hook lost its only implementer in
#27925; it was still forwarded through four layers and always read as
answered. A start that never answered is read from the child's phase, as
the exit already does.
- The saved lease's journalCheckpoint is only ever written as null and read
only by validation. Records an older build saved with it still load: it
joins the retired lease fields dropped at load, so the next write sheds it.
- The record store's setConversationCommand had no production caller. The
tests that stood in for an older build with it now seed the saved row and
reopen the store; three that only re-pinned another test are removed.
* test(native-chat): an older build's null journal checkpoint also loads
* Keep startAnswered cleanup in its owning PR
* Keep historical chat fixtures aligned with their readers
* 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
* fix(native-chat): a chat's visuals folder lives as long as the chat
A chat's scratch folder was deleted once the chat had gone 30 days
unused, so an old chat's inline visuals turned into "Visualization
unavailable" while the chat and its history stayed. The common pattern
keeps a chat's files for as long as the chat and never deletes them on
an "unused for N days" timer.
The sweep now removes only folders no chat record maps to, plus
leftover .expired folders older builds parked. Everything that existed
only to make the age rule safe goes with it: the per-folder save lock
and its expiry candidates, the session-lane idle check, the journal
last-activity query, and the migration's fresh-window timestamp. The
visuals skill no longer tells agents the folder is temporary.
* fix(native-chat): an image sent in a local chat lives as long as the chat
A local chat's pasted images lived in one paste folder that deleted
every file older than 30 days, sent or not, so an old chat's images
turned into label chips. Paired-server chats already keep pastes in the
host's attachment store, where the send that names a file claims it for
its chat and only unclaimed files are ever swept: the common pattern.
A local composer paste now goes into this machine's own attachment
store (starting the local chat host first), so its send claims it the
same way. The old paste folder is no longer written or swept: messages
sent before this change may name its files. Restored drafts still read
it.
An unclaimed upload now waits 30 days instead of one before the sweep
takes it. A draft names its image by this path, so the window is how
long an unsent draft keeps its image; 30 days is what local drafts had,
and it outlasts the host's send-id window so a resent draft still finds
its image.
A chat folder an older build set aside as unused (<hash>.expired) goes
back to its chat instead of being deleted.
* fix(native-chat): a terminal-backed chat's pastes are never swept
A terminal-backed chat has no journal, so no send claims its pastes and
nothing can tell a sent one from an abandoned one. Its pastes therefore
stay in the paste folder, which is no longer swept, rather than in the
attachment store, whose sweep would take them after 30 days unclaimed.
Only a local structured chat's paste (forStructuredChat) goes to the
store, where its send claims it.
* fix(native-chat): keep the attachment owner's literal kind for structured local chats
* fix(native-chat): a screenshot dragged into a chat lives as long as the chat
On macOS a screenshot dragged from its thumbnail is copied into
$TMPDIR/orca-drops, and that copy was deleted 7 days after the drop, so a
sent dropped screenshot turned into a label chip after a week. The common
pattern keeps a file sent in a chat as long as the chat.
A local structured chat's composer now moves each such copy into this
machine's chat attachment store, where the send that names it claims it
for the chat. Every other copy (terminal panes, terminal-backed chats,
editors, startup prompts) has no owner Orca can tell, so copies are no
longer swept at all. The copy budget now bounds one drop instead of every
copy still on disk: without a sweep, counting earlier copies would refuse
every later drop.
* style: wrap the drop-copy call
* test: drop an unused import
* test(ipc): expect the local chat store option on the clipboard registrar
* test(ipc): mock the drop-copy registrar in the core handler test
* Store fresh chat drops before publishing their paths
* Type the drop preparation fixture with its request contract
* Capture immutable drop batches before attachment publication
* Set explicit buffers for cross-volume attachment imports
* Control the deadline in the drop abandonment test
* Return void from the drop test timer cleanup
* Use the captured attachment destination for queued drops
* Register the paired runtime in the queued drop test
* test(daemon): run the stalled-reader backpressure test over a Unix socket, not TCP loopback
Linux autotunes TCP loopback buffers to several MiB, so the kernel absorbed
most of the 8 MiB flood, write callbacks completed, and the producer never
reached the 4 MiB high-water mark on some CI runners. The daemon stream runs
over a Unix socket or named pipe, whose small fixed kernel buffers keep the
backlog in the daemon's own accounting.
* test(daemon): bound Unix socket fixture paths
* feat(layout-owner): a CLI or phone file on a layout-owner server opens in the group a paired desktop shows
Each paired desktop reports, through a new layout.reportView (served only by layout.intents.v1
servers, never to phones), the group it shows in each workspace the server's frames draw. The
server keeps reports per connection beside the window's, opens files in the latest one's group,
and drops a connection's reports when it closes, falling back to the saved view.
* test(e2e): a CLI file open on a switch-on server lands in the group a paired desktop shows
* fix(layout-owner): layout.reportView refuses a caller with no connection, as sidebarInputs.reportView does
* feat(layout-owner): a desktop's re-sent view after a reconnect never outranks another desktop's live report
layout.reportView takes an optional refresh flag. The desktop's first report of each workspace
after a stream snapshot carries it; the server ranks such entries below every live report, while
they still replace the connection's own earlier entries and beat the saved view.
* feat(layout-owner): a paired desktop re-sends its view, as a refresh, when its control connection to the server comes up on a new socket
The server drops a connection's view reports when it closes, and the layout stream can stay up
across that, so the desktop watches the shared control diagnostics for a new socket.
* fix(layout-owner): a desktop's refresh keeps its own live rank; stop reporting to a server without layout.reportView
A same-connection resubscribe re-sends its view as a refresh, which demoted the connection's
own live report below an older desktop's. Ranks are now an explicit live/refresh tier. A pre-PR
switch-on server answers method_not_found; the desktop then stops sending for that stream.
* fix(layout-owner): a group the server's own frames move is re-sent as a refresh, never a live report
The snapshot resend read the store before the sync applied the snapshot, so the flush's group
change reached the store subscription as the user's. Writes made by the frame sync now report as
refreshes, and the snapshot resend runs after the flush.
* test(layout-owner): a change frame's group move goes out as a refresh; the frame-write mark survives nested writes
GitLab task items now carry the host of the repo row that fetched them, and the
project-view dialog stamps the row its slug matched. Both resolve through one
provider-neutral helper (getWorkItemRepo, generalized from #31252's GitHub-only
getGitHubWorkItemRepo), so a repo id published by two hosts never routes an
action to the other host's row.
* 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(ai-vault): name the host each search hit came from when hits span several hosts (#31139)
An All hosts full-text search returned one flat list, and the same title or
path often came up on two machines, so you could resume the wrong copy. When
hits come from two or more hosts, each row now carries the same host chip the
sidebar uses, labelled the way the host menu names that host. Hit order is
unchanged; a single-host result shows no chip.
Reported, with the proposed approach, by @quzard in #31139.
* fix(ai-vault): let the search-hit host chip shrink in a narrow sidebar
* fix(ai-vault): keep sessions recorded under a symlinked workspace's real path in Workspace scope (#31139)
A workspace registered through a symlink (~/work/api -> /data/src/api, or
macOS /var -> /private/var) lost every session whose agent recorded the
resolved cwd: scope matching compared strings, and those rows also counted
against the out-of-scope depth cap.
The host that owns the files (the desktop for local scans, orcad for SSH and
server hosts) now resolves each scope path's real path, scans both spellings,
and gives matching sessions optional matching-only cwdAliases in the
registered spelling. Workspace scope, the depth cap and project attribution
read cwd plus aliases; resume still uses the recorded cwd. Older hosts send
no aliases and older clients strip the field.
Reported, with the proposed approach, by @quzard in #31139.
* fix(ai-vault): rank aliased attribution by closest root; never resolve recorded cwds
Review feedback: a longer-spelled parent link could take a nested project's
session, and resolving every out-of-scope cwd could stall a listing on a hung
network mount or raise macOS privacy prompts.
* perf(worktrees): gate the 30s git-common reconciliation sweep on each entry's dir signature
The native-watch reconciliation forced an ungated stat of every leaf of every
worktree admin entry each tick (~7 stats/entry). It now uses the entry-dir
signature gate plus the two leaves git writes in place (gitdir, logs/HEAD),
runs the ungated sweep every 10 ticks (~5 min), and falls back to sweeping
every tick if a sweep ever finds a change the dir signature missed.
Co-Authored-By: Claude <noreply@anthropic.com>
* fix(worktrees): only distrust the reconciliation dir gate on a confirmed, repeated miss
Detect a missed change on any entry rescan, including ones the in-place leaf
probes trigger, not just full sweeps. Re-stat the admin dir after the leaf
rescan so a rename racing the scan isn't counted, ignore transient missing
leaves, and require misses on two separate ticks before sweeping every tick.
Co-Authored-By: Claude <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
* refactor(editor): drop isWholeEditorState and the diff-signature re-export; bound superseded restore re-reads
- Remove isWholeEditorState; its callers take the real AppState. Tests that
mocked partial stores now build full states from createTestStore.
- Import getDiskBaselineSignature from shared/disk-baseline-signature
directly and delete the re-export from diff-content-signature.ts.
- Restored-tab conflict scan: count superseded re-reads toward
MAX_VERIFY_ATTEMPTS, so a read slower than the re-verify interval gives up
with the conflict banner instead of looping forever. Adds a regression test.
Co-Authored-By: Claude <noreply@anthropic.com>
* fix(editor): keep the last read at the superseded re-read cap instead of giving up
A matching read at the cap cleared nothing and showed a false changed-on-disk
banner; it now resolves like any read. Only read failures give up.
Co-Authored-By: Claude <noreply@anthropic.com>
* fix(editor): back off on superseded restore re-reads instead of capping them
A superseded read may predate the newer check, so it never resolves the
verification, even at the attempt cap. Overlapping checks now delay the next
fresh read (bounded, one timer per tab) and keep verification pending; only
failed reads count toward giving up.
Co-Authored-By: Claude <noreply@anthropic.com>
* test(editor): address the restored tab by its owned id in the overlapping-check tests
After #31196 a server's tab is no longer keyed by its path, so these tests
flagged a tab that does not exist and no read was ever issued.
Co-Authored-By: Claude <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
All hosts gave every Orca server a 3 s scan, including the managed servers
Orca reaches through an SSH tunnel, which now serve every SSH host. A healthy
SSH host walking a large home timed out and showed up empty. The saved
environment's connectionDependency now picks the 15 s budget for tunnelled
servers; direct LAN/Tailscale servers keep 3 s.
Reported, with the proposed approach, by @quzard in #31139.
* fix(github): a twin project's PR cards, rows and dialogs act on the host that returned the item
* fix(github): key twin rows by host so cache sync keeps each copy; a lone row ignores the stamp
* refactor(editor): make the editor buffer record the source of truth for tab buffer state
Writers of the six buffer fields read and write the record, with the OpenFile
fields written as a mirror in the same store update. The store middleware no
longer copies tab values into the record; it only keeps records keyed by open
tabs (seeded from the tab on create, dropped on close) and, in dev and test
builds, asserts every mirror field equals its record. Host-mirrored web tabs
adopt the host's published values explicitly in the session sync patch.
Co-Authored-By: Claude <noreply@anthropic.com>
* refactor(editor): check the buffer invariant in e2e builds and tidy the record helpers
Co-Authored-By: Claude <noreply@anthropic.com>
* test(e2e): give session-sync fixtures the now-required editorBuffers
Co-Authored-By: Claude <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
* feat(native-chat): Codex chats list and run the skills Codex loaded
A Codex chat's skill list now comes from Codex itself (`skills/list`), live
while the chat runs and refreshed when Codex says its skills changed, and
from a short-lived Codex check while the chat is at rest. Plugin skills keep
Codex's `plugin:skill` names, disabled skills drop out, and each skill file
is its own row. A skill picked from the list reaches Codex as that exact
skill, beside the unchanged `$name` text, on hosts that take it.
* test(native-chat): account for Codex's skill list call; send without a trailing argument
* fix: align skill probes with chat launches and preserve picked mentions
* fix: distinguish skill choices in phone accessibility labels
* fix: clear picked skill files when queued text changes
* fix(native-chat): refresh resting commands when launch settings change
* fix: keep WSL agent refresh on its execution host
* fix: preserve skill selection at read and send boundaries
* fix: narrow agent identity for selected skill notice
* fix: use checked agent predicate for skill notice label
* test: retain locale in composer translation mocks
* fix(automations): a legacy automation on a twin project refuses instead of running on a guessed host
* fix(automations): capture a legacy run target before the dispatch snapshot, only where it keeps the folder
* fix(automations): re-read a legacy record after capture even when another run captured it first
* fix(orcad): replace a stale terminal daemon once it drains on Linux and WSL hosts
A daemon forked by an older orcad (before #31135, at umask 077) kept
serving new terminals after an upgrade if it was busy at startup, because
the runtime stale-bundle recycle was macOS-only. orcad now opts in on every
platform; the replacement still waits for exactly zero live sessions and
treats an unverifiable count as keep.
* test(daemon): pin the restart recycle test to linux; tighten wording
* 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)
* wip(layout): applyDefaultTabs command in the layout model (dark)
* wip(layout): applyDefaultTabs takes the window's proposed ids; main refuses a taken one
* wip(layout): main closes or keeps an exited split pane by the window's rule (dark)
* wip(layout): a create or split sent again after its close is refused (dark)
* wip: typed test doubles
* wip(layout): main writes a worktree's default tabs in one command and starts them, window or none (dark; twin e)
* wip(layout): main writes a worktree's default tabs in one command and starts them, window or none (dark; twin e)
* wip(layout): main provisions setup and default tabs even when the agent fails to start; no window takes over (dark)
* wip(layout): pane start status in frames; a failed start keeps its pane; a terminal close stands after a failed write (dark)
* wip: lint fixes (max-lines, test throws)
* test(layout): a terminal close stands after a real failed profile write; switch off still refuses
* fix(layout): checked types in the close-failure rule and its tests
* fix(layout): a failed start keeps its pane whatever failed; default tabs start once; a tab's last pane keeps its card (dark)
* test(layout): the agent's kept pane counts as the workspace's terminal; main starts it whatever failed
* fix(layout): the switch-off create path gains no await
* fix(layout): a failed start never writes back a pane the user closed (dark)
* fix(layout): a failed start's pane goes through main's create rules, so a closed tab stays closed (dark)
* test(layout): D1a's switch-on close tests assert the intent main receives (both builds)
* test(layout): the close fixture answers main's command without naming the method
* fix(layout): a re-created pane binds as designed; the id-only ratchet reads a shared import (dark)
* fix(layout): a pane's start status may sit on a pane still bound to its old terminal (dark)
* fix(layout): a pane's start status ends when its pane binds again by any path (dark)
* test(layout): same-id restore ends a status; a replayed split brings back a closed pane by design
* test(layout): terminal reply fixtures captured from the window-owned build, and main's switch-on proofs
* wip(layout): main's terminal surface index feeds the runtime graph (dark)
* wip(layout): main's panes stand in for the window's terminal list (dark)
* test(layout): the window sends main-listed panes as mount facts (dark)
* test(layout): surface proofs use public runtime calls
* chore: lint
* feat(layout): close by handle closes in main's layout; the close-time relist goes (dark)
* feat(layout): main's binding outranks a caller's pane claim on the terminal record (dark)
* test(layout): window-list tests stay on the rollback build; switch-on mount-fact cases (dark)
* wip(layout): main's terminal index reads E1's shared projection and follows its operation ends (dark)
* refactor(layout): the main-surfaces host lives beside the session-tabs readers, keeping the graph file under its line cap (dark)
* fix(layout): main's panes skip the window-era membership fence, follow workspace routing, and list a terminal when it registers; window close only hands the graph back (dark)
* refactor(runtime): explicit-close PTY stops move unchanged to their own module, for the file's line cap
* test(layout): the cold-restore table runs in a layout-owner build too: one push per registration
* test(layout): the parked and automation pane tests assert the same window report in both builds (mount facts with the switch on)
* chore: one layout-owner switch import after the restack on D1a
* feat(layout): main's terminal.list rows carry the host's verdict; a pane with a status is never live (dark)
* chore(layout): fit main's surfaces under the lint gates after the D1a restack
* feat(layout): every pane status D1a publishes keeps a row from reading live; remote host answers pass through (dark)
* feat(layout): a drag-out moves main's own binding and refuses the home copy's last pane; the window's terminal claim is not consulted (L127, dark)
* fix(layout): a tab close by a starting pane's handle stops the tab's terminals; a starting pane is never exited on the inventory alone; terminal.list lists every saved pane main owns; the pane claim lookup leaves the output path (dark)
* fix(terminal): keep shipped exit and default-tab behaviour with the layout switch on
An exited pane closes through the legacy retirement unless the keep rule holds; the
default tab's title and color still reach the window and the phone's live tabs.
* fix(terminal): one id guard for every pane write; keep panes on an unvouched -1
Main's taken and closed-id guard now runs where every pane write goes, so a runtime create
replayed after its tab closed is refused before it spawns. An exit code nothing vouches for
keeps the pane, as the window does; a kept pane sends the phone no retirement. Converts the
switch-on tests that asserted the deleted window hand-off and title bridge.
* fix(terminal): keep the closed-tab guard on every pane write, held-id checks on client commands
A paired host starts each pane of a client's tab as a create under the tab's id, so a held tab
id is refused only for a client's own layout command.
* fix(terminal): a provider-reported -1 keeps the pane, as the window does
Only a requested stop or a host-confirmed death closes a pane on a negative exit code. Pins that
the closed-tab guard never fires in the shipped build.
* chore(terminal): satisfy the exhaustive-switch and test typing checks
* test(terminal): give the replay test's live process a cwd
* fix(layout): main's index reads routing only for a workspace two partitions hold, the fence reads ownership once, a legacy pane keeps its claim; switch-on tests assert main's answers after a window closes (dark)
* refactor(runtime): a worktree stop's terminal collection moves unchanged to its own module, for the file's line cap
* fix(layout): main's index routes every workspace as the layout stream does, re-reading routing only on a write or a terminal registration; a worktree stop names every terminal main binds; a starting pane is unverifiable (dark)
* feat(layout): every pane status main publishes keeps a row from reading live; a resuming pane is unverifiable like a starting one (dark)
---------
Co-authored-by: E2E Test <e2e@test.local>