* feat(acp): hosts install a managed runtime over RPC, and mark agents whose program Orca provides
managedRuntime.status / managedRuntime.install / managedRuntime.cancel, negotiated by
managed-runtime.install.v1, act on the host that receives them, the one that runs the agent's
chats, with no workspace needed. A status read naming the revision it shows is a long-poll on its
own call-queue lane, so every client sees another's start, progress and outcome.
agentSession.agents marks an agent whose program Orca provides on that host.
* fix(runtime): install status says a copy outside Orca's runs only when a start would really run it
* fix(runtime): install status is pushed to subscribers instead of long-polled, and Cancel answers with the outcome
managedRuntime.subscribe {agent} sends a full snapshot on subscribe, then one
snapshot per install state change, and closes with the transport. It replaces
the managedRuntime.status long-poll and its slot registrations.
* fix(native-chat): a dot between the queue header's label and its count
* style(mobile): wrap the queue header dot's props
* refactor(mobile): move the queued card's text into its own file
The queue file sat at the 400-line cap, so the header dot had no room.
* fix(orcad): never claim or reclaim another machine's orcad, and serialize stale-lock recovery
* fix(orcad): judge data-root locality without a synchronous spawn
* fix(orcad): break a recovery lock only once its owner provably exited
* fix(orcad): publish the recovery lock with its owner in one rename
* fix(orcad): recover a stale lock in generations so no live reclaimer is displaced
* fix(orcad): keep a released recovery generation until the stale lock is replaced
* fix(orcad): fail closed on an unreadable recovery owner, and never let cleanup fail a won lock
* fix(orcad): key recovery locks by the stale record alone
* feat(mobile): Resume in New CLI from a phone chat tab
A chat tab on the phone had only Close in its long-press menu, so a chat
that would not load left no way to reach its conversation. The phone now
offers Resume in New CLI, like the desktop tab menu: the chat's host
prepares the conversation and the phone opens a copy in a new terminal tab.
The resume and fork startup builders move to src/shared so the desktop and
the phone build the same command.
* test(mobile): type the fake terminal create instead of casting its calls
* fix(mobile): pass the host's fork source as is; match the quoted args in the test
* test(mobile): give the fake host client the untyped mock the other client fakes use
* style: format the shared resume builder and phone fork files
* Fix mobile chat CLI fork creation on owning host
* Fix mobile chat fork lint and split create owner helpers
* Keep chat fork beside phone anchor across tab groups
* Combine mobile terminal runtime type imports
* Repair mobile create CI contracts after chat fork extraction
* Classify chat fork preflight refusals and preserve mobile create recordings
* Keep route-owned create cleanup quiet after unmount
* Preserve typed runtime repair refusal in chat fork preflight
* Preserve checked store through mobile chat fork preflight
* Register mobile chat fork runtime fixture before test setup
* Remove unused mobile create recording value
* style(mobile): format the chat fork create files; move fork reply messages into the fork feedback module
Formatting pushed the create hook past its line limit, so the fork-only reply message, partial-success warning and host capability check now live beside the other fork messages. Same order and copy as before.
* Keep quick command mutation aligned with formatted request
* Keep mobile chat CLI forks in terminal view
* Pass mobile chat resolution its existing controller arguments
* Match the mobile create fixture mock layout
* Keep mobile chat CLI forks open when placement is unavailable
* Brace placement contract fixture checks
* test(mobile): keep the prompt controller mock under its line limit after the main merge
* Expose pane admission in the runtime test fixture type
* Complete the chat fork contract controller fixture
* fix(native-chat): retire acknowledged send echoes after catch-up
* test: follow phone lint rules in reconnect regression
* test: format reconnect message expectation
* Keep current chat requests independent of loaded history
* Read current chat requests in bounded cursor pieces
* Bound desktop catch-up after independent request support
* Fix current request subscription static checks
* Match generated request schema import ordering
* fix(native-chat): correct current request type boundaries
* Accept readonly client capability fixtures
* Exercise current request reads in wire compatibility inventory
* Align current request fixtures with provider commit and RPC surface
* test(native-chat): include current request browser capability
* Keep the mobile chat session hook within its line budget
* Format mobile current request readers
* fix(native-chat): reopen once for a current-request version whose read keeps failing
A deterministic read failure reopened the subscription, whose first frame re-read the same version and reset the backoff, so the pane resubscribed every 750 ms forever.
* test(native-chat): type the current-request snapshot fixture
* test(native-chat): wait for the hydrated open before driving the subscription
* feat(acp): a managed runtime installs only when asked, with progress and cancel
One installation per runtime owns every change to its releases: it reclaims old ones once, first,
then installs one at a time. An install checks free space, downloads with byte progress, extracts,
hashes each member (abortable), writes the receipt, then publishes by rename, the commit point: a
cancel after it reports success. Reinstall rehashes the pinned release without downloading. A
status read waits, bounded, for a newer revision and rereads the installed release from disk.
* fix(runtime): Install repairs a damaged copy, says when unzip is missing before downloading, and reads a failed download plainly
- Install over a damaged pinned copy downloads and verifies a fresh one, then swaps the damaged
folder into staging, so there is no dead end; status reports the damaged fact.
- A host that cannot unpack the archive (no unzip) reports it in status and refuses Install
without downloading.
- A network failure while downloading reads "Couldn't download …"; the raw error goes to the log.
- Reclamation skips the whole pass when a release folder could not be read.
* fix(runtime): a status read that can't read the installed files fails instead of saying not installed, and extraction uses the extractor the preflight found
* fix(runtime): a host that can't report free disk space still installs, and says so in the log
* fix(runtime): a download that can't be fixed by retrying says so instead of blaming the network
* fix(runtime): Install unpacks the release itself, checks it starts before publishing, and Cancel answers with the outcome
- The managed install unzips in-process (fflate, bundled into main), so a host without unzip can install; the unzip preflight and the missingProgram status field are gone. The Node runtime download keeps its own extractor.
- A runtime may provide validate(): Install starts the downloaded copy once before publishing it, and an existing copy before reusing it; a failure reads "<agent> couldn't start on this host".
- No 'cancelling' phase: Cancel resolves once the work has unwound and returns cancelled, or succeeded when it had already committed.
- Byte progress every 250 ms; status rereads disk and PATH only on a first read or a phase change.
- The declared download size is checked only for an uncompressed body.
- An existing copy whose files hash correctly but whose receipt is missing gets its receipt rewritten instead of reading as damaged forever.
* refactor(runtime): move the steps that put one release on disk out of the installer
The installer class now holds only its lifecycle: the shared operation state, the one-at-a-time
queue, install, cancel, status, reclaim and progress publishing. Reusing a whole copy, the free
space check, the download, unpacking, checking each file, the start check, the receipt, the
publishing rename and the failure sentences move unchanged into managed-runtime-release-install.ts,
which gets the pin, folder, label, signal and two callbacks (progress, and the commit point right
after the rename) instead of the class. No behavior change; this keeps the installer well under
the file-length limit once Remove lands in it.
* chore(deps): lock fflate for the in-process runtime unzip
* fix(runtime): a failed download blames the release only for a proven mismatch, so a refused proxy reads as a network problem
* fix(runtime): runtime downloads skip the HTTP cache, so Remove frees the space and nothing is stored twice
* fix(orcad): resolve folder workspaces for scoped browser commands
Both orcad browser adapters resolved --worktree with the git-only selector, so
a folder workspace failed with selector_not_found before reaching the provider.
* test(orcad): parse browser results instead of asserting their types
On cold start the conversion skips its own worktree fetch and waits for
startup's scan, so the one rehome attempt found no server row and left
the active workspace on the retired SSH owner. Retry the move once the
startup scan completes, while the conversion is still current.
Co-authored-by: Claude <noreply@anthropic.com>
* fix(session-tabs): seed the first terminal when a warm empty worktree is selected again
A warm stream that got its only empty answer while its worktree was
unselected correctly held the bootstrap, but selecting the worktree again
reused the stream without re-deciding it, so the workspace stayed empty.
Keep the held verdict and re-decide it when the warm entry is selected.
Co-Authored-By: Claude <noreply@anthropic.com>
* fix(session-tabs): drop a held first-terminal verdict superseded by newer host evidence
Co-Authored-By: Claude <noreply@anthropic.com>
* fix(session-tabs): hold only single-worktree frames as deferred seeds
Co-Authored-By: Claude <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
* fix(wsl): run wsl.exe probes from the system root, never the profile
WSL holds wsl.exe's working directory busy for about its idle timeout
after the probe exits. Since #29209 the orcad startup probe ran in the
profile, so a temporary orca serve profile could not be deleted for about
a minute (diagnosed in #29845).
* test(orcad): drop the WSL profile-hold wait from serve parity
Reverts #29845's extended EBUSY retry: wsl.exe no longer runs in the
profile, so nothing outside the test's process tree holds it.
* build(cli): list the WSL spawn-directory module in the CLI typecheck project
* feat(layout): report window saves that still change main's layout (dark)
In a layout-owner build, each window session save (session:set, session:patch, the shutdown
checkpoint) is projected to main's published layout before and after it is written. Any workspace
whose layout the save changed is logged, and appended to ORCA_LAYOUT_SAVE_DIVERGENCE_LOG when set,
so evidence runs can show which tab changes still reach main only through the save.
* feat(layout): name the fields a diverging save changed
* fix(layout): the save check diffs main's whole layout model and never fails the save
Review: a failed log write could reject a save that already landed; and the published layout omits
editor views, pane records (sleeping agents, incarnations, titles), closed-tab tombstones and the
workspace records, all of which a later save trim would drop.
* fix(layout): the save check covers record-only workspaces and row owners, and counts rebinding last
* fix(layout): the save check counts first binds as rebinds and keeps the count past the cut
The test raced two prepare() calls for the migration lock; when the failing-keyring start won,
it correctly started a fresh key and the old credential could not open.
A failed or deferred first add kept its detached distro holder with no record to find it; an add in flight when WSL hosts were turned off still finished as a ready host.
* fix(ssh): leave a stopped pre-claim install on a shared home unclaimed until its machine is known
* fix(ssh): judge a pre-claim install by its activation record; the printed claim creates its directory
orcad built its runtime without the persistent claim signer, so each restart
minted a new key and the surviving terminal daemon could not match the old
owner: resuming the same conversation started a second writer. Load the same
signer the desktop uses from orcad's own user data.
A workspace-slept running agent's record was left for a window wake that a
windowless host never has, so opening its tab started a fresh agent. Such a
host now resumes it in the pane, under the existing duplicate-writer checks
and recorded-flags-only rule.
* fix(terminal): replay a viewport claim made during remote recovery before typing
Recovery released typing at the host's subscribed ack, but claims and
resizes still returned early until the snapshot finished, and nothing
replayed them. Typing then reached the host at another viewer's grid.
Defer the intent and replay it on the stream before releasing input;
send claims directly once the subscription accepts input.
Co-Authored-By: Claude <noreply@anthropic.com>
* fix(terminal): spend a deferred claim only once the host acks the subscribe
Co-Authored-By: Claude <noreply@anthropic.com>
* fix(terminal): stop the ackless replay from marking a closed stream healthy
Co-Authored-By: Claude <noreply@anthropic.com>
* fix(terminal): drop a queued resize when recovery defers the viewport
Co-Authored-By: Claude <noreply@anthropic.com>
* fix(terminal): keep a deferred viewport claim when its replay write fails
Co-Authored-By: Claude <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
A server-pushed recovery the host marks unavailable carries an empty
image that is not the buffer. Applying it cleared screen and scrollback.
Classify recovery replies like tagged requests and retry instead.
Co-authored-by: Claude <noreply@anthropic.com>
* fix(mobile): stop rewriting the cached manifest on a same-build hit
A same-build cache hit rewrote manifest.json (write next, delete, move)
while the native view was reading it. A read inside the gap reported
generation-unreadable, which discarded the cache and redownloaded the
same build. A matching build id now writes nothing to the generation
directory; the fresh routes still apply to the current session.
* refactor(mobile): drop code the manifest rewrite left behind
Opens a same-build hit straight from session.cached, folds the manifest
file name and parser back into the generation store, removes the unused
moveFile adapter and fake hooks, and trims tests that passed on main.
#29335 gave Grok chats their own permission modes, so on a paired host
that predates per-chat modes a Grok chat is now checked like Claude and
Codex; that is intended. #29442's tests, merged a day earlier, used Grok
as the agent without modes. Use Gemini, which still has none.
* fix(native-chat): decide provider failures by kind, never by searching the payload
A Claude chat opened with a red "claude · message:system:init" card whenever one of the
user's MCP servers failed to connect. The classifier searched every nested field of a
frame for failure-looking values and let that outrank the per-kind catalogue, so the
session-start roster's `status: "failed"` turned the whole frame into an error row. The
same search caused the earlier failed-background-task opcode cards.
A frame is now a failure only because its kind is one, or because a field its kind
declares as its outcome says so (Claude `hook_response.outcome`, Codex `hook/completed`
`run.status`). A Claude turn result is a failure when the result journaling says so,
since that depends on whether the user stopped the turn. With nothing left to override,
the typed-translator coverage list, its option and the task-frame sentence seam go.
Failed MCP servers write no row for Claude or Codex.
* test(claude): an uncatalogued frame keeps its sentence without becoming a failure
* fix(native-chat): keep Claude authentication errors visible
* Persist session parse results beyond the memory cache limit
* Expire Session History parse rows by idle time instead of a count cap
The parse cache dropped its oldest row once it held 4,096, so a scan over
more transcripts evicted its own upcoming hits and re-parsed every file on
every scan. Rows now stay until no reader has used them for 24 hours, swept
after each completed History scan and each search-index pass. Only the live
parser state keeps a 4,096 count bound; dropping it costs one whole re-read
of a file that changed, never a re-parse of one that did not.
Removes the overflow directory and the orcad persistence wiring from the
earlier revision of this change.
* Expire only rows History lists, and keep the newest transcripts under bounds
The search pass ran the idle sweep every 20 s but never re-stores History's
unchanged rows, so with search on a day away from History dropped them all
and reopening it re-parsed everything. Only a completed History scan sweeps
now, and it expires only rows History listed (on a monotonic clock). Rows
only the search index or a title lookup created keep main's 4,096 count
bound, so an index build can't grow the scanner process without limit.
Scans store newest transcripts first, so every store-order bound dropped the
newest, most active files: the saved snapshot kept the oldest ~11k rows and
the resume bound stripped the newest files' parser state. The snapshot is now
ordered by transcript mtime, and both count bounds drop the tenth of their
rows with the oldest transcripts.
* Drop the 24 h idle sweep; History rows live for the process
The sweep let whichever listing ran last decide when a row died, and
listings differ in scope: a smaller one (the phone's Agent History, a lower
desktop limit) expired rows only a larger one used, so the next Unlimited
scan re-parsed them. Rows History lists now live as long as the scanner
process; the snapshot's newest-transcripts cap bounds what survives a
reload, and rows other readers made keep the 4,096 bound.
Also clears the resume bookkeeping for a row the unlisted bound drops in the
same store.
* Skip resume bookkeeping for a row the unlisted bound just dropped
Running the resume trim first let a row the unlisted trim then deleted push
the resume set over its bound and cost 409 live resume points.
* perf(ai-vault): store the Session History parse cache one row per transcript
The parse cache was persisted as one JSON file, rewritten whole after any
change and capped at 64 MB / 1M tokens (about 11k rows). Past that cap, a
restart re-read the oldest unchanged transcripts, and an agent writing while
History was open rewrote 10-27 MB every few seconds.
The scanner child now keeps the rows in their own SQLite file. A miss in the
in-memory map reads the row by path; changes are written per row, debounced
and chunked; deletes run immediately. A used_at column, refreshed at most
daily, expires rows nothing has used for 30 days, so rows for transcripts
deleted outside Orca have a way to die. The old JSON file is imported once
and removed. Electron main only records the path for the child.
The rebuild-on-corruption open moves out of the search index into a shared
versioned cache database helper used by both.
* fix(ai-vault): import the old parse cache at open and expire rows only for missing transcripts
Review round 1 on the per-row parse cache store:
- The old JSON was imported only by a History scan or search pass, and only
into an empty database, but deleted either way. Tab-title sync writes a row
first, so the import was skipped and the first History open after updating
re-read everything. The import now runs when the database opens, before any
lookup, write or delete can reach it, keeping rows already present.
- A 30-day last-use expiry also dropped rows for transcripts that still exist,
so opening History less than monthly re-read them. Rows unparsed for 30 days
are now checked on disk and dropped only when the transcript is gone, then
the freed pages are reclaimed. The daily last-use rewrite goes away.
- 8 KB pages (a ~2.3 KB row filled a 4 KB page), busy_timeout before the WAL
switch, and lookups served from rows still waiting to be written.
- Revert accidental pnpm-lock.yaml churn.
* chore: revert accidental pnpm-lock.yaml churn
* fix(ai-vault): never stat WSL transcripts from the parse cache recheck
A stat on a \\wsl.localhost path outside the scanner's running-distro
filter and gated WSL lane could boot a stopped distro or hang on a stuck
one. WSL rows now skip the existence check and age out after 180 days
unparsed. SQLite-backed session keys use the shared OpenCode path splitter.
* fix(ai-vault): register the parse cache database with the WSL mover census
Each schema keeps its own meta table again, so the shared open helper
creates none and the search schema's SQL is unchanged. The parse cache's
tables are recorded as not moved: a cache rebuilt from transcripts.
* fix(ai-vault): a forced whole read never reuses a row a failed database delete left behind
* fix(layout): E1 follow-ups: closed waiting splits, paired-server tabs by host, first-tab e2e wait
- A pane main closes before its tab mounts drops that tab's waiting split for it.
- Mounted tabs skip main's pane tree for paired-server workspaces by host kind, as frames do.
- ensureTerminalVisible waits for the activation tab before creating one.
- Main-asked splits write their pane through admitRuntimePane; startInAdmittedPane loses its single-caller `shown` option.
* refactor(layout): paired-server check in its own module, out of the frame sync
* fix(acp): OMP, OpenCode and Grok chats run their own permission mode
An ACP chat's full access came from its agent's terminal Arguments, so OMP
and OpenCode, which have no bypass flag, asked on every tool call even when
the user chose Yolo. ACP chats now offer Ask for approval and Full access,
seeded from "Permissions for new chats", as Claude and Codex chats do; chats
from before take what they ran with, or the new-chat setting for an agent
that never had a bypass flag. Terminal launches are unchanged.
Co-authored-by: Jae <38941444+JaeCoding@users.noreply.github.com>
* fix(acp): hand the chat's permission mode to the ACP launch
The adapter narrowed the start to its identity and spawn token, so the mode
the host decided never reached the launch. Mobile's picker also showed
Claude's Ask wording for the other chat agents.
* fix(onboarding): Yolo mode also sets new chats' permission mode
Onboarding's Yolo mode says agents run without asking, but it wrote only the
terminal Arguments, so turning it off left new chats on Full access. It now
sets Permissions for new chats too, keeping an Accept edits or Approve for me
choice as it keeps custom Arguments. Pins that every ACP chat agent offers
Full access, and renames the backfill rule for what it decides.
* fix(onboarding): a replay left untouched keeps new chats' permission mode
Replaying onboarding, including from a paired browser whose Yolo switch starts
from its own defaults, rewrote the host's Permissions for new chats. It now
changes only when the person flips Yolo mode in this flow.
* refactor(onboarding): the Yolo switch's state lives in its own hook
Keeps the onboarding flow hook under the file-length limit after it began
passing whether the switch was flipped.
* test(native-chat): pin an OMP chat's pre-mode backfill at the record store
The host-install test's OMP row read no record; the backfill itself is
agent-agnostic, so pin it where the store writes it.
---------
Co-authored-by: Jae <38941444+JaeCoding@users.noreply.github.com>
#29121 and #27925 each added lines to structured-agent-session-host.ts and passed CI on their own; together they left it at 302 countable lines, so static analysis fails on main. Move the late-dispatch and unanswered-dispatch delegates into the mutation delegates that already wrap the same context.
* fix(ssh): allow a host's orca CLI to control Orca by default, and stop resetting saved choices
An absent allowRemoteCliControl now means on; only an explicit false turns the
desktop browser bridge and relay-era orchestration commands off. Removes the
one-time reset of saved true values, the consent stamp writes and the reset
notice, which would have silently turned off hosts upgraded from stable.
* fix(ssh): keep the CLI control choice when adding a host from the sidebar
* fix(ssh): read an older client's omitted CLI control switch as off over RPC
* fix(ssh): check the managed fence before routing settings in browser bridge candidates
* fix(native-chat): retire acknowledged send echoes after catch-up
* test: follow phone lint rules in reconnect regression
* test: format reconnect message expectation
* Keep current chat requests independent of loaded history
* Read current chat requests in bounded cursor pieces
* Fix current request subscription static checks
* Match generated request schema import ordering
* fix(native-chat): correct current request type boundaries
* Exercise current request reads in wire compatibility inventory
* Align current request fixtures with provider commit and RPC surface
* test(native-chat): include current request browser capability
* Keep the mobile chat session hook within its line budget
* Format mobile current request readers
* fix(native-chat): reopen once for a current-request version whose read keeps failing
A deterministic read failure reopened the subscription, whose first frame re-read the same version and reset the backoff, so the pane resubscribed every 750 ms forever.
* test(native-chat): type the current-request snapshot fixture
* test(native-chat): wait for the hydrated open before driving the subscription
* perf(native-chat): sync durable actions without flushing streamed revisions
* test(native-chat): provide the pending dispatch turn scope
* refactor(native-chat): reuse the journal transaction for durable writes
* fix(native-chat): save user intent before provider effects
* perf(native-chat): skip the per-commit fsync only for streamed assistant text
Every chat-journal commit ran a full fsync on the main thread. Replace the earlier allowlist of FULL rows (which missed control facts riding generic rows) with the inverse: FULL stays the default, and a transaction commits at NORMAL only when every row is a later revision of an assistant message that changes nothing but its content. An OS crash can lose only that streamed text; the next FULL commit makes it durable.
* fix(native-chat): keep image revisions fully durable
* fix(native-chat): preserve marked assistant revisions at FULL
---------
Co-authored-by: m4air <m4air@Mac.localdomain>
* fix(orcad): keep the integration credential key only in the owner-only key file
Managed servers no longer create, read or wait on the macOS login Keychain or
a Secret Service keyring for their credential master key. A record an hourly
build pointed at the keyring is migrated once: the key is copied into the key
file when it reads back without UI within 5s, otherwise a new key file is
started and old credentials show the usual reconnect error. Removes the
locked-Keychain status fields, retry, the integrationCredentials.moveKeyToFile
RPC and the Settings notice.
* fix(orcad): serialize the keyring-record migration so concurrent starts agree on one key
* docs(orcad): note the migration lock
* fix(orcad): publish the migration lock with its PID and bound every wait
* refactor(orcad): reuse the file transaction lock for the keyring-record migration
* fix(projects): refuse a folder-to-Git type change that would hide folder workspaces (STA-10638)
A folder project's extra workspaces exist only as metadata rows that the folder listing shows. Changing the project to Git switches it to `git worktree list`, so those workspaces drop out of the next refresh and their saved tabs are purged as if they had been deleted. Only the automatic upgrade checked for this; `orca project setup-update --kind git`, repo updates and clone-into-folder did not.
The check now lives in the one repo write every type change goes through, so any caller gets the same refusal and nothing is written. The automatic upgrade and both clone paths use the same check and keep the project as a folder project.
* test(repos): give the repo handler store mock the metadata read the clone path now uses
* fix(projects): a manual folder-to-Git change proves the folder is the Git root first (STA-10638)
Only the automatic upgrade checked that a folder project's path is the top of a Git repository and recorded Git's spelling of that root. A manual change (orca project setup-update --kind git, repo updates, clone into a folder project) flipped the type without either, so a project inside another repo became a broken Git project, and a project whose path Git spells differently lost its main workspace's id and tabs on the next refresh. All of them now run the same proof and write the proven root with the type change. SSH projects keep the unverified change because their Git runs on a host this process can't probe.
* Revalidate folder-to-Git conversion targets after Git proof
* Explain when a project changes during its Git check
* Fix clone conversion for replaced folder project
* test(runtime): create real Git root for clone upgrade fixture
* test: normalize runtime clone root stamp expectation
* test: check clone registration identity without unknown access
* chore: restore main's pnpm-lock.yaml that the main merge reverted
A Pi or OMP agent started from the window inside WSL, with the terminal
daemon on, could start before the distro's hook relay was up. It then
reported no working/idle status for that session. Host starts and
local-provider starts already waited (#21721); the window's daemon path
was the one left out.
The shared host-env step now always runs the wait on the daemon spawns
it builds, so the lane input that skipped it for window starts is gone.
The wait is a no-op for anything but an explicit Pi/OMP launch in WSL.
* fix(unread): one rule decides when an agent row is unread; title-only rows never are
The workspace card bolded a row Orca knows only from its terminal title or
process ("Claude Code - Idle") because that row restamps its state time on
every rebuild, so no acknowledgement ever covered it: opening the workspace
left it bold, clicking the row cleared it, and the next rebuild re-bolded it.
The dashboard already skipped such rows.
isAgentRowUnseen is now the single rule for the card, the dashboard snapshot
and counts, and the pop-out patch. It is built on the acknowledgement
module's own turn test, so a row is bold only when viewing it can clear it.
Fixes#28306
* fix(unread): bold an agent row only while auto-acknowledge can clear it
The row unread rule now reads the store's live and retained turn records, the
same records auto-acknowledgement clears, instead of a startedAt === 0 marker
on the row. Rows with no record of their own (title/process-derived, running
subagent children, migration-blocked panes) are never bold, so none can stay
bold with nothing able to clear it. Running subagent rows previously stayed
bold for their whole run.
* chore: drop accidental pnpm-lock.yaml churn
* perf(unread): build the row turn records once per pass, not once per row
* docs(unread): name the rule in the dashboard snapshot comment
* Window pane trees drawn from main's layout frames (dark, build switch off by default)
Co-Authored-By: Claude <noreply@anthropic.com>
* Review fixes: paired-server saves keep their trees, last-pane guards count pending closes, draw main's answer when no frame is coming, redraw main's tree after a zoom
Co-Authored-By: Claude <noreply@anthropic.com>
* Delta review: a closed stream's frozen cache never redraws pane trees; the frame reconciler never closes a tab's last pane
Co-Authored-By: Claude <noreply@anthropic.com>
* Second delta review: draw the activation tab only when no frame showed it; a stream not yet open counts as coming; a redraw re-queues only its tab's workspace
Co-Authored-By: Claude <noreply@anthropic.com>
* Independent review: a late record of a split main asked for restores it only if main took it back; a closed stream never redraws pane trees
Co-Authored-By: Claude <noreply@anthropic.com>
* Delta review: deny a late record only for a split main wrote and did not take back itself; pane_closed only where the tab is; a reply before the stream opens waits for its first frame or end
Co-Authored-By: Claude <noreply@anthropic.com>
* Re-review: replies wait only once the window opened its stream; a record that lands while main takes its split back waits for the outcome
Co-Authored-By: Claude <noreply@anthropic.com>
* A record waits only while main is taking its split back
Co-Authored-By: Claude <noreply@anthropic.com>
* refactor(layout): main keeps the pane of a split it asked for
Drops the timed withdraw and the window's late record of a main-asked split: main writes the pane and keeps it, a refused write fails the split, and a tab's first mount leaves queued main-asked splits to their request so the startup command still runs.
* refactor(layout): drop the start-without-pane option no caller uses
* refactor(layout): drop an always-false check in the queued main-split filter
* test(layout): main-asked splits into an unmounted tab, with the stream down, and with no start
* test(layout): type-safe withdraw stub
* fix(layout): equalize draws itself when no frame is coming
* refactor(layout): name the frame-tree pane moves for their role
* refactor(layout): frames' pane trees reach the store through the frame store patch
---------
Co-authored-by: Claude <noreply@anthropic.com>
* fix(worktree): remove a clean checkout that contains submodules
Git refuses to remove any worktree with an initialized submodule unless --force is set, even when nothing is uncommitted. After that specific refusal, re-check the checkout including files inside the submodule and pass --force once. A dirty submodule still blocks the delete. A second --force is not added, because that would also clear a Git lock.
* fix(worktree): offer Force Delete for a checkout Git refuses over submodules
Drop the automatic --force retry. A forced removal deletes the submodule's
repository under .git/worktrees/<id>/modules, including commits, local
branches and stashes that were never pushed, and the parent's dirty check
cannot see them (the retry #24602 removed for the same reason).
Instead classify Git's "working trees containing submodules cannot be moved
or removed" refusal as a new Force Delete reason, 'submodules', after the
lock check and the force guard. The renderer classifies the error string,
so older SSH and paired hosts get it too with no wire change. The toast,
the delete dialog and the Space manager row show one plain warning naming
what Force Delete would lose instead of Git's text.
Keep --ignore-submodules=none in the pre-teardown dirty check so a user's
ignore setting cannot hide submodule edits from it.
Replace the retry test with one real-Git test: a clean checkout whose
submodule holds a local-only commit is refused without --force, stays
registered, keeps the commit, and its refusal classifies as 'submodules'.
* fix(worktree): clarify the submodule Force Delete warning and hide View for it
The warning now says Git deletes the workspace only with Force Delete, the submodule
refusal no longer offers View (there are no changes to show), the real-Git test pins
that Force Delete then succeeds, and a comment no longer describes the removed retry.
* fix(worktree): name submodules when a changed submodule blocks the delete, and clear a stale delete toast
With a commit inside a submodule that the parent has not recorded, the parent sees `M vendor`, so
Orca's dirty check refused first with the generic "It has changed files". Force Delete then destroyed
the never-pushed submodule commit without warning. On a dirty refusal the check now asks
`git status --porcelain=v2` whether a changed entry is a submodule, and if so reports a refusal the
app shows with the submodule Force Delete warning. The detail keeps the dirty sentence, so older
apps still offer Force Delete.
After the Space manager deleted a workspace (its row Force, or a later delete), the failure toast for
that workspace stayed up and still offered Force Delete. A Space delete now dismisses it.
* fix(worktree): warn about submodules on any refused delete of a checkout that has them
Review found the warning still vanished when a submodule commit was already recorded and some other
file was dirty: the parent's status shows no submodule change, yet Force Delete destroys the commit.
The changed-files check now asks whether the checkout has a populated submodule (a .gitmodules path
holding a .git, as Git's own refusal checks) instead of whether one changed. The refusal keeps the
changed paths after the detail, and View stays available since there are changes to view.
* test(worktree): mock the .gitmodules probe as missing in the preflight unit tests
* refactor(worktree): attach the dirty-check status without a type assertion
* fix(worktree): key the delete failure toast by host and worktree id
A Space delete on one host must not dismiss another host's failure toast for a worktree with the same id.
* fix(worktree): dismiss the failure toast by the store row's host after a Space row Force
* Persist session parse results beyond the memory cache limit
* Expire Session History parse rows by idle time instead of a count cap
The parse cache dropped its oldest row once it held 4,096, so a scan over
more transcripts evicted its own upcoming hits and re-parsed every file on
every scan. Rows now stay until no reader has used them for 24 hours, swept
after each completed History scan and each search-index pass. Only the live
parser state keeps a 4,096 count bound; dropping it costs one whole re-read
of a file that changed, never a re-parse of one that did not.
Removes the overflow directory and the orcad persistence wiring from the
earlier revision of this change.
* Expire only rows History lists, and keep the newest transcripts under bounds
The search pass ran the idle sweep every 20 s but never re-stores History's
unchanged rows, so with search on a day away from History dropped them all
and reopening it re-parsed everything. Only a completed History scan sweeps
now, and it expires only rows History listed (on a monotonic clock). Rows
only the search index or a title lookup created keep main's 4,096 count
bound, so an index build can't grow the scanner process without limit.
Scans store newest transcripts first, so every store-order bound dropped the
newest, most active files: the saved snapshot kept the oldest ~11k rows and
the resume bound stripped the newest files' parser state. The snapshot is now
ordered by transcript mtime, and both count bounds drop the tenth of their
rows with the oldest transcripts.
* Drop the 24 h idle sweep; History rows live for the process
The sweep let whichever listing ran last decide when a row died, and
listings differ in scope: a smaller one (the phone's Agent History, a lower
desktop limit) expired rows only a larger one used, so the next Unlimited
scan re-parsed them. Rows History lists now live as long as the scanner
process; the snapshot's newest-transcripts cap bounds what survives a
reload, and rows other readers made keep the 4,096 bound.
Also clears the resume bookkeeping for a row the unlisted bound drops in the
same store.
* Skip resume bookkeeping for a row the unlisted bound just dropped
Running the resume trim first let a row the unlisted trim then deleted push
the resume set over its bound and cost 409 live resume points.
* fix(orchestration): steer mail notices into a busy native chat's running turn
A native-chat coordinator's mail notice always queued as a card until the whole turn ended, so a
coordinator busy in a long command never saw its workers' reports. Agents that take a message
mid-turn (Claude, Codex, Pi) now get the notice at once; agents that would cancel their turn for
it (ACP) still queue it.
* fix(orchestration): drain mail that arrives while a notice is pending
* Send orchestration mail the same way to every chat agent
A mail notice is a plain send for every agent, as a person's own mid-turn message is.
While a pending approval or question waits on the person it is held, and the turn's
working edge after the answer sends it.
* Hold orchestration mail at live prompt admission
Since #29209 a full orcad on Windows runs wsl.exe in its profile directory.
On windows-11-arm the WSL service keeps that empty directory busy for ~60s
after every process in the test's tree has exited, past removeTestRoot's
~45s retry budget, so the recipe and D7 cases fail cleanup with EBUSY.
The warm-match (220 ms) and search+sort (180 ms) ceilings failed on shared
CI runners at 211-245 ms for unchanged code. Compare one full-corpus pass to
four quarter-corpus passes timed in the same run: ~1 for linear work, ~4 for
quadratic, independent of runner speed.
* perf(dev): keep pairing web bundle after test file changes
* fix(dev): detect web input replacement within one timestamp tick
* test(dev): use real files for web replacement coverage
---------
Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
* perf: reduce sidebar agent status subscription work
* test: align sidebar store mocks with subscription API
---------
Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>