* Add Orca's own bundled-skill planner and installer (not wired in yet)
One planner decides what installing Orca's bundled skills does to each
target folder; execute re-plans under per-path locks and runs only a plan
whose digest still matches; a shared summary derives badge, wording and
exit code. A copy counts as Orca's untouched one only when its SKILL.md
bytes match a version some build shipped (resources/skills/released-digests.json,
generated from git history and checked add-only).
Nothing calls this yet; the npx path is unchanged.
* Report left-alone folders as warnings and keep backups in the home
A leave is no longer a failure: the outcome line says what happened plus
"N left alone: <reason>", and the exit code is 1 only for a failed or
changed op. Backups for every scope go to <home>/.agents/.orca-skill-backups
on the executing host, so a project Replace never writes into the repo;
moving there falls back to copy, verify, then remove across filesystems.
* Update Orca's skills with Orca's own installer instead of npx skills
The app's Update skills dialog, badge, nudge and status pills now read the
bundled-skill plan and summary, and Update executes the reviewed plan's
digest. `orca skills install|update` runs the same planner in-process with
--dry-run, --force, --json and per-summary exit codes. Removes both npx
spawn sites, the freshness inventory modules and the npx round-trip CI.
* Update only where the skill already is; one provider-root rule for app and CLI
installedOnly now drops every location that holds nothing (keeping the shared slot
only when an existing agent link points at it), so Update never adds a skill for
another agent. The app and CLI resolve the Claude skills folder through the same
rule in localBundledSkillInput; a test asserts both plan identically.
* Compact Update skills dialog: one row per skill, locations named by agent
summarizeBundledSkillPlan now returns each skill's locations, labelled by
agent (or "Shared folder") with a ~-collapsed path; the dialog shows one row
per skill that needs something, collapses up-to-date skills into one line, and
lists every location behind "Show all locations". The result reads "Updated N
skills · M left alone". Under installedOnly a Replace with no shared copy
replaces in place instead of adding one.
* fix(skills): keep location paths readable by putting the state under the path
* fix(skills): hold the last plan in state, not a ref mutated during render
Split the update dialog's rows, headline and footer into components, move the Update label
next to the other plan copy, and use z.strictObject for the request schemas.
* Escape non-ASCII characters in bundled-skill regex and wording
The orcad bundle rejects a literal non-ASCII character in a regex.
* fix(cli): escape non-ASCII apostrophes in the skills install help text
* test(orcad): check the bundled CLI plans a skill install without npx
Drop the deleted roundtrip workflow's group from the closed-PR cancellation fixture.
* Make bundled-skill test fixtures link folders the way the product does on Windows
Directory links in fixtures are junctions on Windows, which need no
Developer Mode; the one file-link fixture is skipped there.
* Resolve legacy skill agent keys canonically
* Reject stale skill replacements when auxiliary content changes
* Keep ordinary bundled skill classification shallow
* Leave unreadable skill replacements alone without aborting plans
* Pass force-all through shallow planning and describe unreadable copies
* Clarify skill update statuses and folder details
* Keep selected skill replacements in place with one status
* Report completed skill replacements with their backup confirmation
* Clarify available skill updates and skipped copies
* Keep skill update results in preview order
* Show each skill skip reason once
* Count skipped skills instead of skipped folders
* Make installer input and link tests work on Windows
* Bound bundled skill classification reads and parse names linearly
* Keep workspace skill roots inside the resolved workspace
* Compare workspace skill roots with native path semantics
* Restrict bundled skill backups to their owner on POSIX
* Keep recovered skills needing attention until they are updated
* Count completed skill updates in the status tooltip
* Sync the runtime localization catalog
* fix(automations): a slept headless run opens from a paired desktop, and a half-failed sleep keeps its pane link
A paired desktop now asks the server to open a run whose pane retention slept (its PTY id
is dropped, its pane kept), instead of only opening the workspace. Retention finishes a
sleep that stopped the run's PTY and then failed, as the host's sleep state still holds it,
instead of forgetting the run's pane on the next sweep.
* test: use typed automation retention drain hook
* test(automations): fail the real resume-record write, and recover a slept run whose record failed to save
* revert(automations): drop the half-failed sleep recovery, which no real path reaches
A real headless serve keeps a slept run's pane resolving to its stopped PTY, so retention's
retry already re-sleeps and keeps the pane link; a failed resume-record write is swallowed
inside the sleep. Only a mocked null PTY lookup reached the runSleepHeld branch.
* Add Orca's own bundled-skill planner and installer (not wired in yet)
One planner decides what installing Orca's bundled skills does to each
target folder; execute re-plans under per-path locks and runs only a plan
whose digest still matches; a shared summary derives badge, wording and
exit code. A copy counts as Orca's untouched one only when its SKILL.md
bytes match a version some build shipped (resources/skills/released-digests.json,
generated from git history and checked add-only).
Nothing calls this yet; the npx path is unchanged.
* Report left-alone folders as warnings and keep backups in the home
A leave is no longer a failure: the outcome line says what happened plus
"N left alone: <reason>", and the exit code is 1 only for a failed or
changed op. Backups for every scope go to <home>/.agents/.orca-skill-backups
on the executing host, so a project Replace never writes into the repo;
moving there falls back to copy, verify, then remove across filesystems.
* Escape non-ASCII characters in bundled-skill regex and wording
The orcad bundle rejects a literal non-ASCII character in a regex.
* Make bundled-skill test fixtures link folders the way the product does on Windows
Directory links in fixtures are junctions on Windows, which need no
Developer Mode; the one file-link fixture is skipped there.
* Reject stale skill replacements when auxiliary content changes
* Keep ordinary bundled skill classification shallow
* Leave unreadable skill replacements alone without aborting plans
* Bound bundled skill classification reads and parse names linearly
* Keep workspace skill roots inside the resolved workspace
* Compare workspace skill roots with native path semantics
* Restrict bundled skill backups to their owner on POSIX
* fix(relay): repair cell seat-log drift, and keep a new session's generation above a listed seat
Directors mirror each cell's seat feed, so the feed must list exactly the cell's live sessions.
The cell now sweeps its seat log against its sessions on a director's poll: a seat no session
backs is left (4409) and the live session listed, and a live session the feed lost is listed
again, each once two sweeps in a row find it. A demotion naming an unbacked seat repairs it at
once. A host's new session now starts above any generation the feed still lists, which closes
the drained-session reconnect race that removed a live seat.
Claude-Session: asia-drain-pulse
* fix(relay): refuse a rebind whose session left the map during activation, and keep a per-host generation floor
A rebind whose database activation outlived the orphan grace (or a drain) revived the removed
session outside the map: connected, still in the seat feed, and unreachable by phones or
demotion for as long as the host stayed up. Activation now rechecks the map after its awaits and
refuses with PEER_DROPPED so the host redials. Generations come from a per-host floor that
outlives the session, and an activation at a generation already used here is refused.
Claude-Session: asia-drain-pulse
* fix(relay): log each demote outcome on the director by host digest
The director now reads the cell's existing {v:1, outcome} demote reply and logs it; the cell's
feed stays the only source of its map. The supersede line now reaches the log as well.
Claude-Session: asia-drain-pulse
* fix(mobile): the shell returns to a screen already on its stack
The page's own "back to hosts" chevron calls dismissTo('/') inside the page, which the page's
router hands to the shell as one navigate message. The shell pushed every such href, so leaving
a host from its page left [Home, Host, Home] with the old host page still mounted (WebView and
live subscriptions), and every round trip stacked another one. System Back then walked back
through stale host pages.
The shell now reads the navigation container's state first: an href that is already a screen
below this one (same path and query) is popped back to, and an href that is this screen is
dropped. Anything else is pushed as before. The pop names its navigator and source route
explicitly, because a screen's dispatch otherwise stamps its own key as the source and a parent
stack that cannot find it drops the pop.
The wide host-route dismissTo stays for the hrefs this rule does not cover: a host route whose
query differs from the one below (the missing-worktree bounce), and a cold deep link with no host
route below to return to. Overlay replace is unchanged, and so is the bridge message.
* fix(mobile): a param cleared to empty and a fragment still match a screen on the stack
The session screen clears paneKey with setParams({ paneKey: '' }), which leaves ?paneKey= on the
stack entry, so a later navigate to the plain session href found no match and pushed a duplicate.
Empty query values are now dropped when canonicalising, and the fragment is stripped since
expo-router keeps it as a param no href spells the same way.
* feat(mobile): one download bar whose fill follows progress on the native driver
The page download's replies keep the JS thread busy, so React committed
nothing until the end and both bars sat at zero, then jumped to full.
The session now publishes each progress step to a small feed as it is
dispatched, and one shared bar eases its fill to each value with
Animated on the native driver (scaleX). The caption stays React-drawn.
* fix(mobile): the download bar always moves on the native driver
With reduced motion, the fill jumped through a JS-side setValue on a value
that never became native, so on Fabric it only moved with React commits,
which the download holds off. An unread preference also counted as
reduced, so a freshly mounted bar took that path. Every step is now a
native-driven timing, of zero length when motion is reduced, and the
preference is read through a ref.
* fix(mobile): the download bar starts at most one ease per 50 ms
The feed fires per asset, often well under a millisecond apart. Starting a
new 150 ms timing on each one replaced every ease before it drew a frame,
so on iOS the fill stayed near empty until the last update.
Now a callback inside the 50 ms window only records the latest fraction;
the next callback past the window, or a trailing timer at its end, eases
to it over 100 ms. Full lands at once. The effect cleanup clears the timer.
* feat(mobile): download only the page assets the cached generation lacks
Before paging the bundle, the phone matches the new manifest against the
host's active generation by content (sha256 and length). Matches are read
from disk and hashed again while the changed assets come over the wire;
one whose bytes no longer hash to the manifest is downloaded instead.
Progress counts only the bytes that cross the wire.
* test(mobile): put the older-build manifest note back on its describe
* perf(mobile): copy unchanged page assets natively instead of reading and hashing them
The reuse pass read every unchanged asset into JS and hashed it again,
which kept the JS thread busy for most of an update. The cached
generation was hash-verified when it was written, is never written
again, and is what the page already opens as is, so matching assets are
now copied natively into the staging tree and checked by size. A copy
that fails or comes up short is downloaded instead; stage refuses a
reused copy that is no longer whole. Path rules move to
generation-asset-path.ts and the copying to generation-reuse.ts.
* fix(mobile): copy reused page assets at stage, on the store's queue
Copies made when the manifest was read sat in tmp for the whole download,
where a second shell mount's sweep, or a second download of the same
build, could take them away and fail the stage. The fetch now only checks
that each reusable file is still whole (a native stat) and leaves it to
the stage, which copies it from the generation active at that moment and
checks its size. Copy destinations must not exist, as on both platforms.
* feat(mobile): decode page-bundle download windows natively
Each 384 KiB window of the page-bundle download cost about 200-300 ms of
Hermes JS time (tweetnacl secretbox open, fflate gunzip, a base64 re-encode
check and the TextDecoder polyfill), which kept the JS thread busy for the
whole download.
The phone now opens v2 frames with react-native-libsodium (secretbox open and
strict base64) and decodes ranges with a local Expo module,
orca-bundle-window-codec (bounded gunzip via system zlib on iOS and
java.util.zip.Inflater on Android, plus strict UTF-8). The shared v2 session
takes these as optional primitives; the desktop keeps tweetnacl.
If any native piece is missing or fails a known-answer probe, the phone keeps
today's JS path, with the base64 re-encode replaced by an O(1) canonical check.
* refactor(mobile): name the native decode for every inbound frame, not just bundle windows
The native secretbox, base64 and UTF-8 primitives decode every inbound v2
frame on the phone, so the files and the Expo module are renamed for what
they cover (paired-e2ee-native-*, orca-gzip-utf8-codec). A throw from the
native UTF-8 decode now falls back to TextDecoder, since the frame is
already open and its counter advanced.
* test(mobile): pin the TextDecoder fallback when native UTF-8 throws
* fix(mobile): hand the native decode to the transport from the app entry
The transport modules are reached by shared, desktop and e2e code. Importing
the native bindings there pulled expo-modules-core into the e2e typecheck,
whose global types make NODE_ENV required and broke ~20 e2e files. The app
entry now provides the bindings; without them the transport runs the JS path.
* fix(mobile): a native-codec lookup before the app provides bindings does not pin the JS path
* feat(connectors): reopen a closed or exited agent when its conversation gets a message (B2c)
Behind experimentalConnectors. When a connector message reaches a conversation whose agent tab was
closed or whose agent exited, the seam resumes the agent's own session in the same workspace
through resumeAgentSessionInWorkspace, waits for its composer, delivers the message and answers
accepted with reopened. The session id is copied onto the conversation link as hooks report it.
Refusals: agent_asleep_in_window, no_session_to_reopen, reopen_refused; a session another pane
may still hold (an SSH host out of contact included) stays agent_not_reachable.
Resume claims now end as soon as the hook store reports the resumed session, rather than at the
next claim, since an agent that exits to its shell drops its hook row and kept the claim held.
* fix(session-resume): end a resume claim when the hook store reports it, not at the next claim
A resume claim (#31718) ended only when a later claim found the hook store naming the session in
the claimed pane. An agent that quits to its shell drops its hook row, so that later check found
nothing and the claim stayed held for as long as the shell lived: every other resume of the
session was refused as held_elsewhere. Claims now watch the hook store's provider-session changes
while any is held and end each hold as soon as it is reported.
* fix(connectors): pin reopened process before readiness and capture existing session
* fix(connectors): watch persisted links' sessions from host startup and after re-enable
A fresh seam (connectors turned back on, or a runtime restart) watched only the links its own
calls touched, so a session that changed before the next connector call was missed and a reopen
resumed the old conversation. A new seam now snapshots every persisted link's hook-reported
session and watches it, and the runtime builds the seam when its hook store attaches at startup
rather than on the first connector call.
* fix(session-resume): end a hold on report only when it covers one bound spawn
The report names a pane, not a spawn, so with a second overlapping open of the same pane still
unsettled or bound to its own PTY, ending the hold early could let another pane resume while
that spawn still writes. Those holds keep the existing PTY-exit and next-claim ends.
* fix(session-resume): settle a lost-reply resume attempt when a retry of the same request binds
A targeted resume whose spawn reply was lost keeps its share held, since its process may run.
A retry of the same request reconciles that same create, so when the retry binds its PTY the
earlier attempt's share is accounted for by it: the hold now ends when that PTY exits instead of
never. A different request's spawn still accounts only for itself, and a lost reply is never
read as an exit.
* fix: preserve overlapping resume holds during lazy report checks
* test: verify disabled connector startup and restored capture
* Track transcript content truncation across session readers
* Separate transcript decoding from preview truncation
- Preserve full searchable message content with explicit truncation metadata
- Keep bounded previews independent of transcript publication across parsers
* Fix AI Vault transcript preview parsing and cache upgrades
- Preserve full Devin and Grok transcript turns while bounding preview scans
- Exclude stale previews that may expose reasoning after cache schema changes
* Clarify Devin parser test payload terminology
* fix(session-resume): end a resume claim when the hook store reports it, not at the next claim
A resume claim (#31718) ended only when a later claim found the hook store naming the session in
the claimed pane. An agent that quits to its shell drops its hook row, so that later check found
nothing and the claim stayed held for as long as the shell lived: every other resume of the
session was refused as held_elsewhere. Claims now watch the hook store's provider-session changes
while any is held and end each hold as soon as it is reported.
* fix(session-resume): end a hold on report only when it covers one bound spawn
The report names a pane, not a spawn, so with a second overlapping open of the same pane still
unsettled or bound to its own PTY, ending the hold early could let another pane resume while
that spawn still writes. Those holds keep the existing PTY-exit and next-claim ends.
* fix(session-resume): settle a lost-reply resume attempt when a retry of the same request binds
A targeted resume whose spawn reply was lost keeps its share held, since its process may run.
A retry of the same request reconciles that same create, so when the retry binds its PTY the
earlier attempt's share is accounted for by it: the hold now ends when that PTY exits instead of
never. A different request's spawn still accounts only for itself, and a lost reply is never
read as an exit.
* fix: preserve overlapping resume holds during lazy report checks
judgeIdleRun's rule (fresh done since the prompt went in, shell refusal of the
agent command, 2-minute grace for agents that never report status) moves into
judgeIdleAgentTurn, keyed by rows and send time instead of an AutomationRun.
Automations keep their wording and call order. No behaviour change.
The transition-to-announcement rules (lead-agent finish under background work,
all-clear dedupe, quiet window for goal/Pi agents, restored-pane seeding) move
into TerminalAgentTurnNews; the phone push keeps its wording, window/mute gate
and burst cooldown and composes it. No behaviour change.
* fix(sidebar): land flip-a's switch-on fixes on main, still dark (no flip)
The fixes found while the sidebar ran from main's feed, without turning
that on: ORCA_SIDEBAR_INPUTS_IS_MAIN stays false, and none of flip-a's
stop-gaps come along.
- Main stops counting a closed tab's terminal once it was asked to stop
and no pane shows it; a failed or refused stop takes the request back,
and a host out of contact stays unverifiable.
- Main fetches a paired server's rows with the transport's normal bound,
so a server slower than 5 s still lists.
- A deferred unread raise flips the folder row it was decided for, and a
read while the owner check is out cancels it.
- Phone: no worktree.ps flash before the feed; a host's last installed
feed stands in while a fresh read is out; it highlights its own
last-opened workspace and a fold never scrolls; projects file under
their project group with the desktop's indent; the collapse spinner
shows only after 500 ms; "Reopen to update" keeps the rows on screen.
- e2e helpers and journeys for the switch-on suite.
* fix(sidebar): keep an owed relay stop marked, clear a thrown stop, and prune pending unread raises
- A stop-and-wait that throws takes its stop request back, like one that fails.
- A final stop the legacy relay still owes stays marked, since the relay retries it.
- Pending unread raises are tracked only while one waits, so the map no longer grows.
- The paired-server fallback poll test gives its third fetch a reply and asserts live.
* feat(sidebar): main publishes cached snapshots and announces its own stores; detected, view and live-content domains (inert)
* fix(sidebar): keep links confirmed only by a new git run, no rescan on metadata writes, no phone wait on detected checkouts
* fix(sidebar): rescan the workspace rows when the Windows runtime default changes
* feat(sidebar): one lifecycle queue per workspace in main, with in-progress hints (switch-gated)
Create, delete, terminal.closeAll, terminal.sleep and worktree.sleep now run through one
first-in, first-out queue per host-qualified workspace: strict order, no merging, delete
cancels what has not started and waits for what has, an unconfirmed outcome pauses until
the host's own answer. Hints publish in a new inProgress feed domain (no reader yet); the
creations domain is its projection. A finished create or delete keeps its hint until a scan
started after it applies, in the same revision. Adds the paired-server metadata serializer
(saving domain, no caller yet). Replaces the creation registry.
* fix(sidebar): a create never queues behind the previous occupant's delete
* feat(sidebar): main runs sleep; the window follows (P3b)
* fix(sidebar): keep electron out of the headless server's sleep path
The desktop registers how sleep reaches paired servers; a headless serve has none.
Co-Authored-By: Claude <noreply@anthropic.com>
* test(sidebar): type-check the sleep e2e specs; drop the removed VM suspend stub
Co-Authored-By: Claude <noreply@anthropic.com>
* fix(sidebar): resume a paused workspace by re-observing, never replaying
- A command issued while paused waits behind the pause; when the owner answers again main
re-reads terminal liveness and the listing, then carries on. A forced delete runs past it.
- Commands keep their arrival order across a workspace lookup; deletes by name key by the
resolved id, so they wait for that workspace's running sleep.
- A second delete with different options is refused as already in progress.
- Close and delete read lost contact as sleep does.
- A create always frees its own queue entry, even after a retry took over its id.
- Drops the unused opId, and the paired-server write serializer and its feed section,
which land with their first caller.
Co-Authored-By: Claude <noreply@anthropic.com>
* fix(sidebar): main is the only writer of a sleep's wake records; sleep classifies only lost contact as unconfirmed
- The window no longer captures wake records for a sleep main runs; it takes main's records
from main's answer, which its wake and tab-open resume then read.
- A workspace reopened during its sleep keeps its VM running (the window and a phone's
activation tell main).
- Only lost contact pauses: a definite chat-stop failure or a server's still-live answer is a
plain failure with today's toast.
- A paired server's chats are asked with the Sleep mark, and again without it when an older
server refuses the field, instead of reading an unprobed status.
- A phone's worktree.sleep is answered on acceptance and its activation no longer waits on a
queued wake, so neither can outlast the phone's request timeout. The web client passes
markChatsAsleep and keeps the receipts.
- One desktop adapter, passed where the desktop builds the runtime, replaces the two module
slots; a headless server gets none.
Co-Authored-By: Claude <noreply@anthropic.com>
* chore(sidebar): share worktree.sleep's params with the RPC catalog; lint fixes
Co-Authored-By: Claude <noreply@anthropic.com>
* test(sidebar): test the paired-server sleep where main runs it; drop the window's copies
The window no longer sleeps a paired server's terminals, so its convergence and order tests for
that path move to main (remote-workspace-terminal-sleep.test.ts) or follow main's answer.
Co-Authored-By: Claude <noreply@anthropic.com>
* test(sidebar): retry the sleep spec's liveness read while terminals stop
Co-Authored-By: Claude <noreply@anthropic.com>
* fix(sleep): main resumes every workspace-sleep wake; the window only attaches
- Main claims and starts each slept pane through its windowless resume path, with or
without a window: the "window present" check is gone.
- A phone's open queues main's wake, which finishes only once the resumes have started,
and always notes the reopen so a running sleep leaves the VM up.
- The window no longer relaunches a workspace sleep's records in fresh tabs; opening a
slept pane asks main to resume it and the pane's spawn attaches to main's terminal.
- A window save cannot overwrite main's workspace-sleep records.
- A VM that would not suspend no longer reads as "kept open": the terminals slept, and
the toast says the machine is still running.
* test(e2e): main resumes a slept pane once, in place, and keeps its record
* chore(i18n): add the VM-still-running sleep message
* fix(sleep): a phone's or the CLI's failed sleep shows the desktop toast again
* fix: restore main's final P2 test fixes a merge reverted
* fix(sleep): window saves never write or clear main's sleep records; headless wake report counts only real sleeps
* test: a same-id twin's grace follows the rows' change, not the removed catalog hook
* fix(sleep): drop the 1 s settle wait after an observed exit, and wake a slept workspace's mounted agents on phone open
A workspace sleep stopped each terminal through main's stop-and-verify, which polled for a
further second after the exit it had already seen. And a phone opening a slept workspace no
longer reached the window's mounted panes of agents that had finished before the sleep, so
they stayed asleep until the window opened their tab.
* test: give the workspace-sleep wake record its prompt field
* refactor: match slept-agent records to the workspace with worktreeIdsEqual
* chore(sleep): one import from sleep-record-pane-claim
* test(sleep): route the disconnected-sleep SSH spec, declare the desktop sleep adapter, and open the workspace after its sleep starts
* test(sleep): delete a workspace the spec creates, not the seeded one later specs share
---------
Co-authored-by: Claude <noreply@anthropic.com>
* feat(session-lifecycle): one targeted resume of a recorded agent session, sharing an in-flight claim with pane resume
resumeAgentSessionInWorkspace composes the lifecycle owner's existing
ownership check, window-wake claim, resume builder and terminal create, and
returns the new binding or a typed refusal, never a shell or fresh agent.
Pane resume now takes and consults the same in-memory claim, so an SSH
resume before its first hook (or any spawn in flight) cannot be resumed
twice. The explicit-command create path now carries launchRuntime.
* fix(session-lifecycle): claim before deciding, create runtime-owned, keep ambiguous spawns held
Both resumers take the in-flight claim before any awaited check, so a
settled claim always hands off to a fresh holder decision. The targeted
resume creates through the runtime-owned create with a pane identity
derived from its request, keeping launchRuntime with a window attached and
letting a retry reconcile the pane it may have started. A failure after the
spawn request left keeps the session held; a bound hold ends on its PTY's
exit as it happens.
* fix(session-lifecycle): bind a failed create's spawned PTY, retry over its own pane, forward abort
A create that spawned and then failed binds the hold to the PTY its pane
gained since the claim, so that PTY's exit still ends it (both resumers).
The targeted resume's holder check excludes its own request-derived pane,
so a same-request retry reaches create reconciliation after its hook. The
request signal reaches the runtime-owned create; an abort before dispatch
refuses as aborted.
* fix(session-lifecycle): settle resume claims only from the create's own PTY
A claim binds only to the exact PTY the runtime-owned create produced,
reported before publication can fail; its exit ends the hold. A spawn that
proved its exit during start, or a failure before dispatch, releases it.
Any other dispatched-but-unidentified outcome stays held. Drops the
pane-difference fallback, which could bind an unrelated shell. Signals
dispatch only after the create-mutation recipe check, which can still
refuse before dispatch.
* test(runtime): remove duplicate review-only test runner
* fix(session-lifecycle): hold a resume until every PTY its overlapping opens bound exits
Overlapping opens of one pane can each spawn; the hold now watches every
bound PTY and ends only when all have exited and no open is still
spawning. Test helpers replace Reflect calls with typed guards.
* fix(session-lifecycle): key resume claims by destination pane
* feat(connectors): connector seam lifecycle, conversation links and start behind experimentalConnectors (B2b-1)
* feat(connectors): connector follow-ups, held while the agent asks, behind experimentalConnectors (B2b-2)
* feat(agent-write): opt-in launch cancel, approval wait and binding fence on the shared writer
executeAgentLaunch takes an optional signal; sendTerminalAgentPrompt (and
sendAgentTurn) take optional onPermission: 'wait' and expectedBinding. No
existing caller sets them, so every current path is unchanged.
* fix(agent-write): keep the default writer path main's; fence cancel and real submit boundary
Only opted-in writes take the submit layer, so default timing and the leaf
re-read are unchanged. Cancellation is rechecked before a structured-refusal
terminal fallback, and an opted-in launch learns Enter from an onSubmitWritten
callback at the write itself, OMP's combined frame included.
* fix(connectors): pass the seam's signal to the launch, read links from each host's rows, and keep delivery ids on orchestration's durable receipts (B2b-1)
* fix(agent-launch): a cancel after a failed startup spawn recovers the created workspace
* fix(connectors): keep an unknown receipt once the launch published, and restore local-only metadata ownership (B2b-1)
* fix(connectors): settle a lane when its process is lost; removal stays host-qualified; folder delete announces removal
* fix(plugins): keep folder removals out of public worktree events
* fix(connectors): a launch admits its process for the lane, so queued sends never adopt a successor
* fix(connectors): classify connector RPCs as host-owned; CLI start output claims a start only when definite
* Transfer keyboard focus when a sibling chat pane opens
* fix(native-chat): hand focus to newly opened chat composer
* fix(native-chat): keep tab bar controller within line limit
* fix(chat): split into a fresh conversation beside the current chat
* test(chat): declare the split fixture store without an assertion
* fix(chat): route the split to the source conversation host
* test(chat): keep the split regression focused on the launch contract
* test(chat): give the command palette split fixture its conversation agent
* test(chat): seed the legacy command fixture without expanding its asserted shape
* test(chat): isolate pane identity commands from the split launch boundary
* Prevent chat splits from starting a terminal on host refusal
* Cancel structured launches when their destination pane closes
* Model live destination groups in agent launch test fixture
* Capture split chat focus intent at the user action
* fix(chat): hand input to the split pane during admission
* fix(chat): Split opens a fresh chat beside the current one
Split Right/Down on a native chat used to move the chat into a new pane.
It now opens an empty pane beside the chat and launches a new chat of the
same agent on the same host into it through the New Chat launch, so the
new composer takes focus at once. A launch that never opens a tab closes
the empty pane. Drops the earlier admission and focus-transfer changes
that main now covers.
* fix(chat): Split palette search words describe a new chat, not a move
* fix: remove misleading chat move shortcut hints
* fix(chat): a new chat stays in its pane until its host lists it
A host tab list built before a chat's create landed can arrive after the
create reply. Once the reply settled the launch, nothing protected the new
tab, so that older list dropped it: Split's new pane collapsed and focus
moved to another chat, and the next list re-added the chat elsewhere.
Keep a small in-memory record for a chat this client created that no host
list has shown yet. While it exists, a list that omits the chat keeps its
tab. The record ends when an accepted list from that host includes the
chat, when the tab closes, on workspace teardown, or 30 seconds after the
reply, when the worktree is re-read from its host (including an unchanged
version) so a chat the host really closed is dropped then. The launch
lifecycle, status, persistence and settlement are unchanged.
* test(chat): type the faked create replies without an assertion
* Keep new chat protection tied to accepted host lists
* Update paired chat close regression to current suppression API
* fix(chat): Portuguese Split palette words describe a new chat
---------
Co-authored-by: Orca Worker <orca-worker@localhost>
* fix(orchestration): let a worker start helpers on the Run it works for
A dispatched worker could only start helper workers by taking a coordinator
seat on a new Run, and taking a seat moved its inbox. worker-start now also
accepts a worker on an active Dispatch: its helpers run in the same Run, report
to it (worker_done, heartbeat, escalation, status, ask), and it answers their
questions. The worker's own mailbox never moves. When it settles, whatever its
helpers sent that it never handled goes to the Run's coordinator.
Authority comes from the worker's own Dispatch, re-checked inside the
reservation transaction. Helpers are fresh agents on this host; a worker may
retry only its own helper's latest attempt. Remote workers keep the old
own-Run instructions. No saved field or wire change.
* fix(orchestration): read helper rows without type assertions
* fix(orchestration): hand a settled helper creator's mail to the Run as live delivery
When a worker that started helpers settled, its helpers' unhandled mail was
re-addressed to the Run by settlement SQL alone. Nothing woke the Run's
coordinator, delivered rows kept their old delivery stamp so pointers and the
restart scan skipped them, payload-free status reports were left behind
because selection required payload.dispatchId, and a question asked while the
creator was being stopped stayed with the stopped creator.
- Select handed-back mail by its recipient mailbox and Run (unread reports
plus pending questions, even ones already read), reset delivery and pointer
state on the moved rows, and fence the old mailbox's Deliveries.
- After the settling transaction ends, the runtime re-reads exactly the moved
rows and wakes the Run with their real types; a rollback wakes no one and a
failure falls back to the existing repoint retry. Restart recovery is the
existing unread-undelivered scan.
- Hand mail back again at the final stop settlement, which also closes the
stopped Dispatch's own questions asked during the stop.
- A worker_done receipt replay re-offers the handed-back targets.
- Scope markAsDelivered to the mailbox it pointed, so a late pointer stamp
from the old owner cannot hide mail now owned by the Run.
* test(orchestration): drop an unused inbox type
* fix(orchestration): read only the Run when handing a settled worker's mail back
Every settlement runs the hand-back, so a full dispatch row read there added a statement per settlement. Also expect the mailbox-scoped delivered stamp in two delivery tests from main.
* fix(orchestration): re-offer handed-back Run mail on every worker_done replay
A helper creator's worker_done hands its helpers' unread reports and pending
questions to the Run. The send stored the Run's extra wake targets in the
mutation receipt, but the executor rewrites that receipt on completion and on
each replay, so an identical retry only woke the original destination.
Derive the targets at replay time instead: for the settled Dispatch (when it
has same-Run helpers), notify run:<id> for each type of unread report or
pending question there. Notification only; nothing is re-settled or inserted.
The receipt augmentation is removed.
* style(orchestration): format with the formatter version main now pins
* feat(connectors): connector seam lifecycle, conversation links and start behind experimentalConnectors (B2b-1)
* feat(agent-write): opt-in launch cancel, approval wait and binding fence on the shared writer
executeAgentLaunch takes an optional signal; sendTerminalAgentPrompt (and
sendAgentTurn) take optional onPermission: 'wait' and expectedBinding. No
existing caller sets them, so every current path is unchanged.
* fix(agent-write): keep the default writer path main's; fence cancel and real submit boundary
Only opted-in writes take the submit layer, so default timing and the leaf
re-read are unchanged. Cancellation is rechecked before a structured-refusal
terminal fallback, and an opted-in launch learns Enter from an onSubmitWritten
callback at the write itself, OMP's combined frame included.
* fix(connectors): pass the seam's signal to the launch, read links from each host's rows, and keep delivery ids on orchestration's durable receipts (B2b-1)
* fix(agent-launch): a cancel after a failed startup spawn recovers the created workspace
* fix(connectors): keep an unknown receipt once the launch published, and restore local-only metadata ownership (B2b-1)
* fix(connectors): classify connector RPCs as host-owned; CLI start output claims a start only when definite
* refactor(runtime): every reader of a terminal's pane goes through paneOfPty (L80)
With the layout-owner switch on, main's layout answers which tab and pane hold a terminal, and
nothing reads or writes the PTY record's tabId/paneKey copies or their graph stamp. Switch-off
reads the copies exactly as before.
* refactor(runtime): read a terminal's pane through the runtime's paneOfPty method
* fix(runtime): main's pane index answers paneOfPty before any window graph
A headless host never installs a window graph, so the query asked a publisher that did not exist
and every terminal read as paneless. The index is now made on first query; it publishes only once a
graph is installed, as before.
* test(runtime): pin that a query with no window publishes nothing
* test(runtime): a terminal summary reads its pane once (L80)
* feat(runtime): pin bind-first at registration, and one own-handle rule with or without a window (switch-on only)
* test(runtime): census and tests that every create path binds its pane before it registers (L80)
* test(runtime): a bind-first fixture that stands in for the spawn commit (L80)
* test(runtime): fixtures bind a terminal's pane before registering it, as the spawn commit does (L80)
Also, with the switch on: skip the bind-first check for a paired server's workspace, release a
closed tab's terminal ownership once main has dropped the pane, and publish a pane's agent status
from the terminal main's layout binds it to.
* feat(runtime): an exit after main's close still finds the pane it retires (switch-on only, L80 Q2)
Main's layout write records every pane it drops whose terminal no pane holds any more, in a new
optional session field closedTerminalPanesByPaneKey (same TTL and cap as closed-tab records).
Exit cleanup reads main's layout first, then that record by terminal id and incarnation, and
removes the entry it used. Older builds ignore the field.
* refactor(layout): one dropped-leaf step every live layout edit runs (switch-on only, L80 Q2)
The step receives each dropped pane (host, worktree, tab, leaf, terminal, incarnation), computed
across the whole layout so a moved pane is not dropped, and runs only for a commit that declares a
live edit. Today it records the closed-pane entries; a drop is recorded once per terminal.
* fix(runtime): a phone or headless tab close still stops and releases its live terminals after main's layout drops the panes (switch-on only, L80)
The headless close reads each row's live terminal before main's close drops the panes it is found
by, and a renderer-retired tab releases the owned terminal main left in no pane, as the explicit
close already did. Close tests now commit through main's layout and bind panes first.
* fix(runtime): a local workspace's terminal tab left in a paired server's partition is main's pane (switch-on only, L257)
Older builds could save a local worktree's tab rows into a runtime partition (#18202); its terminals
are local, so main's index takes those tabs for workspaces the router homes locally. A tab or
terminal the local partition also holds is indexed once, from there; SSH and server-owned
workspaces are skipped as before.
* test(layout): the legacy-close comparison leaves out the closed-pane record only layout-owner builds write (L80)
* test(pty): a pane whose failed start withdrew its tab resolves to terminal_not_found in layout-owner builds (L80 #7)
Main's layout no longer holds the pane, so there is no pane to answer for; window-owned builds keep
the disconnected stub from the exited terminal's record.
* test(runtime): orca-runtime suites bind panes first, as the real store and spawn commit do (L80)
Fake PTY spawns commit the binding into the pane the runtime wrote, stores list their partitions,
and SSH sessions sit in their ssh:<conn> partition; terminals registered directly bind first.
* fix(runtime): main's layout names a pane's terminal only when the workspace and stamped incarnation match (switch-on only, L80)
A record made before its adoption registers (a host restart) or a local id reused after one
matched a saved binding by id alone, so a phone or paired desktop could be handed a second handle
for the pane before the terminal's own handle arrived and stay on it.
* test(runtime): a restored pane is only unnamed in the adoption gap, and no local record rescues a paired server's tab (L80)
* test(runtime): recovery, close and routing suites set up terminals as production does (L80)
A recoverable pane is one the host inventory dropped with no exit, live terminals register
rather than appear only in the window graph, and SSH stores route as the real Store does.
* test(runtime): suspects 3, 4 and 5 bind, register and apply as production does; PR-1's main-layout closes in the part-10 and part-11 close tests (L80)
* test(runtime): a fixture store applies main's layout commands as a profile does; PR-1's own closes in the part-05, part-08 and UUID-leaf close tests (L80)
* test(runtime): the profile-store stand-in moves to its own fixture, keeping bind-first under the line limit (L80)
* test(runtime): orphan adoption reads the pane from main's layout with the switch on, not the record copy (L80)
* test(runtime): a phone's pane close and a pinned tab close are main's layout closes with the switch on (#31571), in the no-widening and surface-close tables (L80)
* test(runtime): a paired phone close commits in main's layout with no relay ack when the switch is on (#31571) (L80)
* fix(terminal): a parked pane resumes its agent once main has closed the other pane that held the session (layout-owner build, L80 P1)
* test(runtime): the parked-pane suite's restart holder is a saved pane with no registered terminal, under the line limit (L80 P1)
* fix(terminal): a spawn commit that binds a reused local terminal id unbinds it from the saved pane of the older process (layout-owner build, L80 P2)
* test(runtime): the legacy OSC row test uses the UUID leaf main's layout holds and registers its live terminal with the switch on (L80 E5)
* test(runtime): the pty-exit echo close of a dead headless tab is expected to fail with the switch on until W4's close PR (L255)
* fix(terminal): a terminal record that arrives, changes incarnation or moves workspace after main composed its panes re-composes them, so a restored pane lists once (layout-owner build, L80)
* fix(terminal): a pane bound to a terminal a paired server runs keeps that binding and host, and is never indexed as a local spilled tab (layout-owner build, L80/L257)
* fix(terminal): a pane main owns no longer proves its terminal runs; the host's inventory and the terminal's record decide, so a dropped shell reads disconnected (layout-owner build, L80)
* fix(terminal): an SSH pane whose host says its shell ended keeps its leaf on a negative or failed code, as the window does, so recovery starts a shell in it; a stale handle learns the connected replacement (layout-owner build, L80)
* fix(agent-launch): a terminal main's layout still binds counts as running through a host listing gap, so a launch never starts a second shell in its pane (layout-owner build, L80)
* fix(terminal): a disconnected terminal main still binds stays a stop target, never reads stopped unproven, and keeps an exit wait waiting until a certified exit (layout-owner build, L80)
* test(terminal): the wait unit fixture answers the exit-settled dependency
* test(terminal): realistic fixtures for three phone-row tests: unique leaf ids per pane, and a window that draws main's own panes (layout-owner build, L80)
* fix(terminal): a stale handle learns a pane's replacement only when it was resolved for that pane, its own terminal ended, and the replacement is connected there now (layout-owner build, L80)
* test(session-tabs): switch-on variant for a window's chat placement (main's layout order rules); headless phone tab order is L283
* test(terminal-close): switch-on close reds name their ledger rows (L255, L256, L259a, L259b); a same-id respawn's commit stamps its incarnation; the crossing terminal binds first
* test(terminal): the crossing registration is refused by main's workspace fence (L256 switch-on variant); the placement test's tabs are main's saved layout
* refactor(terminal): the stale-handle guard reads an issue-time pane stamp on the handle record, not a second map; moved-pane, restart and single-reader tests (layout-owner build, L80)
* test(session-tabs): switch-on variants: the desktop's tab name is main's rename, and a mobile-created tab's live shell is the one its commit registers (layout-owner build, L80)
* fix(terminal): a re-issued handle keeps its issue-time pane while no pane holds its terminal (review)
* fix(terminal): stop targets and exit waits use main's holds check (workspace and incarnation), and any exit event settles a later wait (review)
* test(session-tabs): name the switch-on placement and split-group reds L288 and L289
* fix(terminal): an exit drops a closed pane's record in the same write that retires the pane, and an unbind folds into the binding write (layout-owner build, L80)
The exit used to remove main's closed-pane entry in a session write of its own, before the
retirement write. It now reads the entry, and the retirement write for that pane drops it,
re-checked by terminal id and incarnation. An exit that keeps the surface (relay contact lost,
intentional stop) keeps the entry until a confirmed exit or its TTL.
A reused terminal id's unbind is now a pure computation that applyPtyBinding applies through
its existing writes. Two split-pane tests still expected a -1 exit to close the pane; they now
follow the ruling that any negative code keeps it. Static-gate fixes: test fixtures and tests
use typed member access instead of Reflect, and no longer use type assertions.
* test(runtime): the shared fixture store announces its session writes and the L283 store lists its partitions, as a profile Store does; L289's test passes on that store (layout-owner build, L80)
The fixture store bindPaneFirst fills now tells write listeners of every session write, direct or
by a layout command, so main's post-commit merge runs as it does on a real Store. The notice is
not enumerable, so a test's own `{ ...store }` copy, which keeps its writes elsewhere, does not
inherit a subscription it would never fire. With it, the targeted split-group create (L289) passes
at this head, so its it.fails mark goes. L283's test runs on a store that names its partitions and
stays it.fails("L283").
* fix(terminal): recovery checks the caller's handle after the cheap guards again, so the switch-off build re-issues no handle on a refused recovery (layout-owner build, L80)
The stale-handle switch point had moved the handle lookup ahead of the pane, terminal and
workspace checks. That lookup can re-issue a handle, so a switch-off recovery refused on those
checks gained a side effect base never had. The lookup runs after them again, and the switch-on
replacement check lives in one method.
* test(runtime): the layout-owner start fixture's spawn commits the pane binding before main registers the terminal, as the host does (L80 switch-on variant)
The window-tab start test came in with the main merge after bind-first; its fake spawn returned
a terminal no pane was bound to, so registration refused it. The fixture's spawn now binds the
pane first, as every other bind-first fixture does.
* fix(terminal): a new process under a reused terminal id starts with no exit code, so an exit wait on it cannot settle on the old process's code (layout-owner build, L80)
The record of a reused id kept the earlier process's lastExitCode, and with the switch on an exit
wait settles on any recorded code once the terminal reads disconnected. A new incarnation now
clears the code and cause. Also drops the L283 test's partition-list option: the fixture store
already lists its partitions.
* test(runtime): switch-on variants for four tests the full local run never loaded (L80)
- A restored pane's host lists the terminal it started; a pane main owns vouches for none.
- The other pane holding a slept session is in main's layout, so an unverifiable holder blocks
the resume as it does through a window.
- Main sleeps a workspace whose panes it lists itself, even with a renderer attached (#31571).
- A graph sync no longer reconnects a disconnected terminal of main's pane; the inventory decides.
* test(runtime): the phone Markdown route tests use #31667's realistic store, and the untitled cases wait on L268 with the switch on
The window's tab uses main's id (the file path), and the store lists its partitions and saves each
open editor's unified tab and group, as the window does (the fixture from #31667). With the
switch on, the three untitled cases fail on L268, main's row merge dropping the untitled flag,
which #31667 fixes; they are it.fails until A1b's history includes it.
* refactor(runtime): two merged files back under the line limit, by moving A1b's additions to their helpers (L80)
After the main merge, the holder-pane check and a closed tab's live-terminal lookup pushed two
files past the line limit. The holder check is now paneHoldsSession (main-terminal-surfaces.ts)
behind one method, and the closed tab's lookup is one parent-class method. Behaviour unchanged.
* test(runtime): the headless chat tests open the chat on the publish path, as every product path does; L283 stays it.fails for the main-owned reorder
openChat recorded the chat only in the runtime's snapshot, so main's layout never held it and the
phone's reorder was refused as an unknown tab. On the publish path main's layout holds it; the
reorder is accepted but does not survive the row rebuild after a close (L283, reopened).
* test(runtime): bind-first census follows the one spawn-commit step (#31596); the unbound-registration test keeps its spawn from the suites' stand-in commit
After the main merge both start lanes register in spawn-commit-assembly.ts, after
savePtySpawnPaneBinding (or, adopted, on the live owner's pane); the census lists that file. A
phone create now writes its pane before the spawn, so the suites' stand-in spawn commit bound it
and the test of a path that registers unbound passed for the wrong reason; that test's controller
now opts out of the stand-in commit.
* test(runtime): convert four switch-on tests the disguised full runs hid
Real switch-on runs showed four files whose fakes registered terminals unbound
or confirmed a stop without its exit. Bind first, stamp the source as main
does, and deliver the exit a confirmed stop delivers. Switch-off unchanged.
* test: type the two converted fakes instead of casting them
The PTY controller fake now takes the controller's optional pane fields, and the
switch-on store is a typed RuntimeStore, so neither needs an assertion.
* Revert "Preview HTML files by default" (#31575)
Restore source/diff opening for general HTML file-opening routes for the release.
This reverts commit 524fa0039b, retaining the
published files.readDocPreview capability for mixed-version clients and removing
an unnecessary restored test assertion to satisfy the current lint gate.
Existing explicit previews and terminal HTML links retain their previous behavior;
the underlying local-file preview exposure remains tracked in STA-11670.
* Preserve file ownership and symlink guards during HTML preview rollback
Keep the existing owner checks after file-drop uploads and tab-entry stats so
an ownership change cannot reopen a file through a stale host. Preserve local
preview source checks so symlinks outside the workspace keep user-file access.
The race and symlink tests reproduce failures on the mechanical revert before
these guards are retained. Cover HTML source opening for folder workspaces,
Explorer, and Source Control without restoring preview-by-default.
* fix: split tab entry browser requests to satisfy lint limit
* feat(agent-write): opt-in launch cancel, approval wait and binding fence on the shared writer
executeAgentLaunch takes an optional signal; sendTerminalAgentPrompt (and
sendAgentTurn) take optional onPermission: 'wait' and expectedBinding. No
existing caller sets them, so every current path is unchanged.
* fix(agent-write): keep the default writer path main's; fence cancel and real submit boundary
Only opted-in writes take the submit layer, so default timing and the leaf
re-read are unchanged. Cancellation is rechecked before a structured-refusal
terminal fallback, and an opted-in launch learns Enter from an onSubmitWritten
callback at the write itself, OMP's combined frame included.
* fix(agent-launch): a cancel after a failed startup spawn recovers the created workspace