A dropped or picked file used to land as its raw @path text. The pill
still serializes to that reference, so what the agent reads is unchanged.
Co-authored-by: Brennan Benson <79079362+brennanb2025@users.noreply.github.com>
* fix(agents): one host-routed agent-detection entry point
preflight.detectAgents / refreshAgents take an optional { worktreeId } and the
host resolves that workspace's project runtime (WSL included) or SSH target.
Gated by preflight.workspace-scoped.v1; old peers keep the host default.
Co-Authored-By: Claude <noreply@anthropic.com>
* fix(agents): route detection by the worktree's own host record
A repo id shared by local and SSH rows made the first row pick the detection
host. Resolve the worktree from the host's list and apply the shared
execution-host routing rule; ambiguous rows refuse. Dashboard falls back to the
host-default list until a workspace's own list loads.
Co-Authored-By: Claude <noreply@anthropic.com>
* fix(agents): refuse an ambiguous workspace instead of probing the host
Only selector_not_found keeps the host default; selector_ambiguous now fails
closed. Adds runtime-level coverage of the lookup.
Co-Authored-By: Claude <noreply@anthropic.com>
* fix(agents): refuse an ambiguously owned folder workspace
Co-Authored-By: Claude <noreply@anthropic.com>
* test(agents): quick-launch runtime target carries the workspace
Co-Authored-By: Claude <noreply@anthropic.com>
---------
Co-authored-by: m4air <m4air@m4airs-MacBook-Air.local>
Co-authored-by: Claude <noreply@anthropic.com>
The open question below a queued card already says the agent is waiting,
so the card's own caption said the same thing twice. A card behind an open
question is now plainly queued (the 'turn' hold): same Steer, Delete and
More actions, same sending rules, no caption. The 'awaiting-answer' hold
and its pending-prompt input fed only that caption, so both go, on desktop
and in the phone app, along with the now-unused locale key.
Co-authored-by: Claude <noreply@anthropic.com>
Extract subprocess execution and process contracts into @orca/process-host with explicit public entry points, independent checks, and one Node 18 compatible CommonJS runtime. Migrate application consumers and integrate package builds, dependency selection, import enforcement, and runtime resource staging.
Preserve daemon startup ownership and profile isolation, and qualify development rebuild recovery and concurrent output publication. Remove unnecessary aliases, test forwarding exports, and a duplicate output scan.
Validation: full typechecks, changed-code quality, package and artifact checks, native Windows builds/types plus 306 runtime tests, and independent native reviews.
Matched full E2E controls on original main and unchanged reviewed head both pass all 17 applicable tests (nine skips). Earlier intermittent E2E failures are documented in the PR; no assertions were weakened. The final combined-tree audit preserves both branches with no import-boundary violations.
* fix(settings): keep MCP repository configs on the repo's own host
Settings read and created MCP configs through the desktop filesystem for
repos owned by a managed host, so it showed the desktop's file at the same
path and could write the starter config onto the desktop. Route inspection,
creation and opening through the existing runtime file client using the
selected repo row's host.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(settings): an unreadable listed MCP config still counts as present
A read failure after the listing found the file marked it absent, which
offered "Create empty config" and could overwrite the real config.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: m4air <m4air@m4airs-MacBook-Air.local>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* fix: preserve streamed turn duration after host restart
* test: run restart duration regression in Node
* fix: preserve provider receipt time in journal updates
* test: verify provider receipt in Claude ownership assertion
* fix(native-chat): time interrupted turns from real provider output, not flushes
Shared-timeline checkpoints stamped the coalescer flush or retry time as the
provider's latest proof of life, so a turn whose provider died before a delayed
flush settled with an inflated duration after a host restart. Carry each delta's
receipt time through the coalescer and timeline lane.
* fix(native-chat): derive interrupted-turn duration from saved output
A reply cut off by a server restart could show "Worked for 0s" because recovery
ended the turn at the last heartbeat or the reply's first timestamp. End it at
the latest time this owner's output was saved to chat history instead, derived
in the existing journal fold from the row save time. Drops the per-frame
receipt plumbing through every provider and buffer.
* fix(chat): exclude host recovery revisions from provider activity
Mark journal-open and connection-cleanup revisions with the existing recovery
annotation so an interrupted turn does not include host downtime. Preserve
buffered provider output and advance restart fixtures to the restart clock.
* fix(native-chat): interrupt turns a replaced server left running
When the managed server died mid-reply and a new one started on the same
host, the cut-off turn settled `unverifiable` with no end whenever the old
provider's identity could not be proven gone, and nothing ever revised it:
the chat showed no turn status and the sidebar said "unconfirmed".
The new server opens the chat records holding the host's instance lock, so
every runtime that wrote them before is gone, and every provider it held
lost its pipes with it. Record that at load (the fences those runtimes
granted, the loaded owner's last renewal, how its runtime ended) and let
the stale settle end those runtimes' running or unverifiable turns as
interrupted at the owner's last saved output or renewal, with the existing
Orca-stop row. Lease adjudication and the live / unverifiable / exited
owner verdicts are unchanged.
* fix(native-chat): require the record store for any journal open that settles
Only an acquisition's open may skip it; a reader open without the store would leave a replaced server's turns unverifiable.
* fix(native-chat): say a remote host's Orca restarted, not that it was closed
A reply cut off when a remote host's Orca server stopped read "Orca on <host> was closed while
this response was in progress." Nobody closed anything there: the server stopped and Orca started
it again before the row could show. For a chat on a remote environment, cause quit now reads
"Orca on <host> restarted while this response was in progress." The desktop's own quit keeps
"was closed". Renderer-only; no wire change.
* fix(native-chat): say a remote host's Orca stopped, true for a shared desktop too
* test(native-chat): register Grok without compaction in the recovery-output rig
Main's #26502 made Grok declare compaction; the rest-test rig adapter has no compact, so the
registry refused it before any recovery case ran.
* chore(native-chat): say a replaced runtime also revises an unverifiable turn
* feat(native-chat): edit a queued message in place on the host
A queued card's Edit used to copy its text into the composer and delete the
card, so the message lost its place, an agent-mail card lost its sender, and
while a question or approval replaced the composer the text vanished.
The execution host now edits the card itself:
- agentSession.queuedMessageUpdate replaces only the card's text, as a
compare-and-set on the body fingerprint the client last saw. It keeps the
id, position, sender, kind, send mode and attachments; a command card is not
editable. A resent Save answers from the card as it stands (unchanged or
changed), so a lost answer never overwrites newer text.
- agentSession.queuedMessageEditHold acquires, renews and releases an
in-memory edit lease per caller and edit. While one is live, automatic
delivery stops at the card (earlier cards still send, later ones wait); an
explicit Send still overrides. Leases expire after two minutes on their own,
re-derive validity from the card on every read, and die with the
conversation. Release uses cleanup admission.
- The published card carries an optional editHeld marker, never `paused`.
- Both methods sit behind the new agent-session.queued-message-edit.v1
capability, independent of the dark mid-turn queue capability, and are on
the mobile allowlist for the phone follow-up.
* feat(native-chat): edit a queued card inline on desktop
Edit message now opens an editor inside the queued card instead of moving the
text into the composer. The card keeps its place and sender, and the editor
stays usable while a question or approval holds the composer's slot (it does
not lose focus to the prompt card).
- Enter or Cmd/Ctrl+Enter saves, Shift+Enter is a newline, Escape cancels;
IME composition keys never save or cancel. Save and Cancel replace the
card's own actions while it is edited; one editor per pane.
- The editor holds the host's edit lease, renewing every 30 seconds and taking
it again after a lapse. Lease failures are only logged: they never block
typing, Save or Cancel.
- If the card is sent or removed elsewhere mid-edit, a clean edit just
closes; changed text goes into the chat box after any existing draft, with
one toast. A Save that lands and then sends counts as success.
- Against a host without agent-session.queued-message-edit.v1 the card offers
no Edit (no copy-and-delete fallback); an empty overflow menu is hidden.
* fix(native-chat): name every queued-edit notice in its switch
* fix(native-chat): keep queued-edit typing safe and stop disabling Send elsewhere
- Save with an unreadable answer (dropped for a moved runtime fence, or lost)
now reads the published card: saved text shown -> close; card gone -> the
sent-as-saved / chat-box path; otherwise keep the editor quietly.
- Send and Steer stay enabled on a card edited on another device; the
published editHeld marker is gone (no reader left). Automatic delivery
still stops at an edited card on the host.
- Save answering changed moves the typing to the chat box and reopens the
editor on the newer text. Notices say when the chat box is hidden by a
question or approval.
- Closing the pane or switching chats with unsaved typing appends it to that
chat's box quietly.
- Lease renewal no longer publishes, so an open editor is not idle-sweep
activity.
- The consume compare-and-set also matches the body fingerprint.
- Saving unchanged text keeps the original blocks.
* fix(native-chat): keep Agent in the Japanese queued-edit notices, as the catalog does
* fix(native-chat): close a queued edit its own Save already landed, wake the queue for a pruned lease, keep typing across a reload
A Save whose answer was lost now closes once the card shows the saved text,
and a card that went out with exactly the typed text closes quietly instead
of copying it to the chat box. A renew that prunes another editor's lapsed
lease wakes the queue in place of the timer it cancels. A reload, which
unmounts nothing, hands unsaved typing to the chat box like closing the pane.
* refactor(native-chat): move the queued-edit notices to their own module
* fix(native-chat): unref the edit-lease timer in a way the phone's typecheck accepts
The lease module reaches the mobile test program through journal-store, where
setTimeout answers a number, so `timer.unref?.()` failed mobile's tests
typecheck ratchet.
* test(cross-version-wire): call the queued-edit methods in both skew directions
The manifest must name every agentSession method; the two edit methods
reach their host methods on a new build and are only called by clients
on a host advertising agent-session.queued-message-edit.v1.
* test(rpc): count the two queued-edit methods on the structured surface
* fix(native-chat): lay out the inline queued-message editor as a header, field and Save footer
While a card is edited, its row shows a pencil, "Editing message", the sender and an X that
cancels; the field at full width; and a footer with the key hint and Save. The card's queue icon
and its status line are hidden until editing ends.
* fix(native-chat): show the Enter key on the queued-message editor's Save button
The editor footer's muted key hint is gone. Save now carries the same inline
key badge as the new-workspace composer's Create button, extracted into a
shared ButtonKeyHint so both buttons render one badge.
* feat(native-chat): add a chat-owned command receipt store
Adds the agent_session_command_receipts table and an insert-if-absent
receipt write that runs inside a command's own journal transaction. A
receipt records the command's identity, canonical hash, status and a
pointer to the row holding its result; it dies with its chat record.
Nothing uses it yet: the existing operation ledger stays authoritative.
* fix(native-chat): key command receipts like today's ledger ids
Rows are keyed by caller and operation id; a send looks across callers and
other methods look up only their caller's row, matching the existing ledger.
Bad rows and newer table shapes read as unreadable instead of throwing.
* fix(native-chat): order receipt lookups by insertion and key by operation id
Global lookups now take rows in insertion order, so a read and an insert
agree on which receipt answers an id regardless of the host clock. The
primary key leads with the operation id, so its own index serves both
lookups.
* fix(native-chat): accept sends through their own receipt
Send (direct, queued, steer) and /compact no longer commit a separate
ledger reservation before the message is saved. Their command receipt is
inserted-if-absent in the same transaction as the submission or queued
draft; a repeated id replays from it, a changed request conflicts.
Operation ids are checked for syntax only, so age, clock skew, pruning
and the reservation's own storage can no longer refuse a send.
The ledger success row they already co-committed stays as a temporary
insert-if-absent write so families not yet switched keep seeing these
ids; it is never read on this path.
* fix(native-chat): answer a send's receipt one way, share /compact's id space with Send, wake delivery once
- After its commit, a send whose publication fails answers through the same receipt replay a retry takes, so one id gets one answer; the in-memory copy and journal reload are gone.
- /compact ids are global like Send's: a retry under a new connection replays instead of needing a second acknowledgment write.
- The receipt route calls its runner directly; the runner wakes delivery when a new message row commits.
- The compatibility ledger row expires from host time; each temporary site is labelled.
- Receipt read errors and post-commit failures are logged.
* test(native-chat): expect receipt answers where tests assumed the send ledger
Restart-continuation tests now model the case Send cannot vouch for (saved but
not loaded into memory) and keep it unconfirmed; a later throw after the message
is in the chat answers continued, as a resend would. Drop /compact's unreachable
legacy replay branch and fix stale comments.
* test: run the queued tests that open the journal database in the Node runtime project
* perf(usage): keep per-source usage caches off the main thread
The usage stores kept every provider's per-source scan cache
(processedFiles / processedDatabases) inside the one JSON file main
loads at startup. On a large Claude history that file is 56 MB, 43 MB
of it per-source records only a scan reads, so main paid a sync
read+parse at launch, a structured clone of the whole cache to and
from the scan worker on every scan, and a pretty-printed rewrite
after every scan.
The usage scan worker now owns those records in a sidecar
(`<cache>-sources.json`): it reads it before a scan, writes it after
(compact, durable rename), and returns only sessions and daily
aggregates. Main keeps and writes only that report, compact. A cache
still carrying inline records is split on the worker the first time
it is loaded (above 8 MB); smaller ones parse on main and rescan cold.
Sync IPC getters and automation lookups wait for the load.
Synthetic 56 MB cache, same machine, interleaved runs:
- first launch: 108 ms sync parse -> 18 ms main busy, 4 ms longest block
- later launches: ~69 ms -> ~7 ms
- forced scan, longest main-thread block: 89 ms -> 11 ms
(real built app, headless: 84-94 ms -> 11-13 ms)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(usage): preserve cache attribution and legacy warm scans
* fix(usage): reclaim cache temps after worker retirement
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Neil <neil@stably.ai>
* perf(daemon): send PTY output to main as raw bytes, not NDJSON
The stream socket's hello can now negotiate binary-v1 framing:
length-prefixed frames carrying PTY data as raw UTF-8 next to a small
JSON header, so neither the daemon nor main escapes or parses the
payload. Older daemons ignore the hello field and stay on NDJSON; older
mains never ask. The hello reader now hands the stream reader the exact
bytes that followed its line, which an event sharing the hello's read
used to lose. ORCA_DAEMON_BINARY_STREAM=0 keeps NDJSON.
Fixes#27003
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(daemon): preserve negotiated output and encode ordinary frames once
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Neil <neil@stably.ai>
* feat(layout): add the inert one layout client for local and remote runtimes
The renderer's RuntimeLayoutClient sends layout commands and reads the
layout.subscribe stream through one transport interface, with the same client
code over the local runtime and a paired server. Frames go to a read-only cache
whose only writer is applyFromRuntime: a snapshot replaces every workspace, a
workspace frame replaces one, removed drops one. A lost stream resubscribes and
the new snapshot replaces the cache; nothing is merged or replayed. Nothing in
the app uses it yet.
- The stream's frame types move to shared so the runtime and the client share them.
- subscribeRuntimeRpc is the streaming twin of callRuntimeRpc, taken unchanged
from the structured session client, which now uses it.
* fix(layout): stop on a refused layout stream and keep equal cached layouts
- A host that answers method_not_found or forbidden is not retried: the
subscription stops and reports a refused status the caller can read; only
other failures back off and resubscribe.
- applyFromRuntime keeps the cached layout when the runtime's is equal by value,
so a reconnect snapshot with unchanged data replaces nothing.
- A snapshot frame's subscriptionId is optional to the reader.
* fix(layout): resubscribe on a desynced layout stream and slim the layout client
A malformed frame of a known type, or a change before the snapshot, now
resubscribes for a fresh snapshot instead of leaving the cache stale. The
retry backoff resets only after a change proves the stream stayed up, adds
jitter, and stops for a revoked pairing or a removed or replaced server.
The client is one function over subscribeRuntimeRpc, the cache has one
writer, and the change-frame type lives in the shared frames module.
* fix(layout): drop the defensive layout-stream handling with no user flow
Unreadable frames are ignored again, the retry delay has no jitter, and
the client no longer tracks server removal or the unauthorized code; the
switch closes a removed server's subscription. The backoff still resets
only after a change frame, with no new state.
* fix(native-chat): resolve every registered agent's floating chat to its pinned folder
Drop the unused showStructuredAgentSessionChoice and say what the built-in agent check means.
* fix(native-chat): launch floating OpenCode, Grok, OMP and Pi chats in their pinned folder
The renderer now resolves every floating chat's links, images and skills
against the folder the chat was pinned to. The ACP (OpenCode/Grok/OMP) and
Pi launchers still started the agent in the current floating folder
setting, so after the setting moved and the agent relaunched, the agent ran
in one folder while links resolved against another (and Pi forked its
conversation into the new folder).
Both launchers now use resolveAgentSessionLaunchDirectory, the helper Claude
and Codex already use: launch in the pin, refuse with the existing
"folder this chat ran in no longer exists" message when it is gone, and pin
an unpinned floating record on first launch. Worktree and folder chats are
unchanged.
The tab-directory reader now calls the status bridge's own isStructuredTab
instead of restating its predicate inline.
#26695 added Kiro to the history-search agent catalog and updated the
unit tests, but the cross-version wire tests still built their expected
older-host agent list as 'everything except the agents added later',
which now included Kiro. Older hosts never accepted Kiro, and the
client correctly withholds it, so those 7 tests failed on main.
* Start plain new-tab agent launches through the host's agent.launch
The tab bar's + menu, Quick Launch, the floating panel and its shortcut, the
new-agent shortcut and dashboard spawn now start a no-prompt agent through
the host, the way main's AI-button launches already do, wherever that is
main's own launch: chat default off, enabled agent, not an SSH workspace.
Everything else keeps the window's startup queue.
* Type the new-tab host route tests without assertions
* Keep a new tab the window launches on another platform on main's path
On a Windows client a WSL-path workspace launches as Linux from the window,
which the host does not infer, so it keeps the window's own launch.
* fix(native-chat): a chat says what its config resolved once, at its start
Every options read re-reported a no-pick chat's configured default: ACP
re-derived it from the session's current state, and Pi re-sent its start
snapshot. A chat left open kept writing its start model back over a newer
default another chat had learned since (for example after the user changed the
agent's own default), so new chats named the old model and then jumped.
A ready child's acquisition now carries its start listing, with the default its
start resolved after restoring the chat's saved picks, and the host saves it
once when the owner is proven (a starting child already hands it on its
`started` event). Options reads carry the model list only. Grok, OpenCode, OMP
and Pi share this path and the one no-pick rule.
* fix(native-chat): only a chat's own effort menu outranks a newer one
The configured-effort fallback took the older listing's menu whenever it had
the effort, so an older background listing could bring back an effort the
agent's own newer menu no longer offers. It now falls back only to the chat's
own listing.
* fix(native-chat): an empty Pi settings file or dotfile-only extensions folder picks nothing
Pi reads an empty settings file as no settings, and a .pi/extensions folder
holding only .DS_Store or .gitkeep loads no extension, so neither keeps the
"Model" placeholder any more. The BOM strip is written as an escape. Tests
cover the packages key and an extensions folder that can't be read.
* fix(native-chat): Claude says what its config resolved once, with its start's settings readback
Claude's configured default rode on every options read, so an open Claude chat
with nothing picked kept writing back the model its settings applied when it
started, over a newer default another chat had learned since. Its default is
known only once the settings readback after `started` lands, so that readback's
`options-reported` event now carries the start listing with the resolved
default, and the host saves it once for the child that reported it. Options
reads carry the model list only, as for every other agent.
* fix(native-chat): a start's learned default is saved behind the attach, and never fails the start
Claude's settings readback, its one report of what the config resolved, was
checked against the host's child index as it arrived. A readback that beat a
slow attach found no child yet and was dropped for good. It now waits in the
session's serialized step behind that attach, as `started` does.
Handing a ready child's start listing to the catalog ran inside the acquire's
cleanup scope, so a throw there would have torn down a proven child. It is
bookkeeping: a failure is logged and the start proceeds.
* refactor(native-chat): name the Claude listing's configured-default option; Pi's start listing is read-only
No behavior change: claudeCatalogListing takes { withConfiguredDefault } instead
of a trailing boolean, and PiRpcSession exposes its start listing through a
getter over a private field.
The runtime PTY controller's listProcesses / supportsForegroundProcessEvidence
and the local daemon-startup waits took a connectionId where null meant
this machine and undefined meant every host. They now take an
ExecutionHostId (omitted = every host for inventory, this machine for
startup waits); id-routed callers pass the PTY's resolved host instead
of re-deriving a raw SSH connection id.
Co-authored-by: m4air <m4air@m4airs-MacBook-Air.local>
The webview registry made every webview click-through on dragstart and only
restored it on dragend or drop. A dragstart that something cancels (for
example pressing and moving an agent logo in the tab strip, which the tab drag
sensor cancels) never starts a drag, so neither event fires and every existing
browser tab ignores clicks until a later full drag or a reload.
A live native drag delivers no pointer events to the page, so any pointerdown
or pointermove now ends the native-drag passthrough. Renderer pointer drags
hold their own passthrough tokens and are unaffected.
* fix(browser): route links and window.open to tabs by Chromium's own disposition
window.open(url, '_blank', 'noopener,noreferrer') opened a floating popup
window because the new-tab check required an empty features string. Chromium
already reports Chrome's tab-vs-window decision as the disposition, so the one
rule is now: a tab disposition opens an Orca tab unless the page keeps a named
handle it may use (OAuth); size/position features and Shift-click stay popups.
The injected click-routing script that relabeled link clicks is gone: native
dispositions route target=_blank, Cmd/Ctrl-click, middle-click and iframe links
identically. Real clicks still bypass the page-initiated tab budget, now via
the browser-process input gesture tracker the route guests already use.
Co-authored-by: Lasha Kvantaliani <lasha@sweenk.com>
* refactor(browser): spend the click on every window open and drop the noopener parser
Review follow-up: a click that opened a popup window stayed unspent and could
fund a later scripted tab; every open now consumes it, as Chrome does. The
noopener parser only changed named noopener opens, which stay working child
windows, so the rule is just an unnamed open with a tab disposition.
* test(browser): table-test the new-tab rule and correct stale comments
---------
Co-authored-by: Lasha Kvantaliani <lasha@sweenk.com>
A paired device's socket passes its bearer credential as clientId, and native
chat used it as the caller key it stores in ledger rows, receipts, chat records
and prompt answers (shown to other clients). Name a paired device by the device
id its socket proved instead.
* fix(mobile): keep chat drafts and the sidebar reachable on iPad
Chat composer drafts now live in a module-level store keyed by host,
workspace and tab, so leaving the session screen no longer drops them.
On iPadOS 26 windowed mode the window buttons covered the top-left
header controls; a native launch hook adds their height to the root
safe area. A hidden sidebar now shows a Show sidebar button in the
session header.
* fix(mobile): preserve draft edits across chat remounts
* test(mobile): type sidebar header fixture as a partial controller
* Show the usage unit once in the status bar
Add formatBareUsagePercentage and a leading Used/Remaining label so each
status-bar usage number renders as a bare percentage. The label stays
mounted while every chip is folded into +N, and measureUsageRow counts it
so density levels don't oscillate.
* Derive status bar usage sections in one place
The status bar chips and the shared used/remaining unit label each had their own copy of the rules for which percentages are shown. The two copies could disagree, so the unit label could stay visible when no percentage was showing. Both now read from getStatusBarUsageSections.
- Collapsing the last visible percentage chip also frees the unit label's width, so the density calculation no longer leaves a gap.
- Verbose bucket chips take a labelFirst flag so the name still renders before the percentage, as before.
- Compact and verbose modes share one section builder, and the verbose-only mini bar is kept as before.
Main's row mint stores an SSH partition without openFilesByWorktree /
browserTabsByWorktree, and the Serializer always writes them as {}. The
load report compared absent with {} and flagged both as unknown changes,
failing the SSH layout-oracle e2e scenarios on every PR that runs them.
Co-authored-by: m4air <m4air@m4airs-MacBook-Air.local>
* fix(native-chat): a second new chat opens even when the first is still empty
* fix(native-chat): a new chat joins another launch only when it re-delivers the same request
A request with text ("Send notes > New agent", browser annotations) used to take over a blank
chat that was still starting in the same tab group, so the same action landed in the just-opened
blank tab or in a new tab depending on how fast the first chat started. Every start now opens its
own chat unless it re-delivers the same request id (or resumes the same conversation).
Removes what only that claim read: the claim branch in the launch join, the attempt's blank flag,
the request's text/group fields, the launch options' targetGroupId and the launch target's
groupId (the tab still opens in the caller's group), the empty-chat and chat-tab-group modules,
and the draft-load-pending probe only the empty-chat check used.
On-call can ask a director where a host is: its relay_assignments row and
open migration (primary-key reads only), the shadow map's seats and recent
leaves with close codes, the compare verdict (pure classification, judged as
/v1/resolve sees the host, recording nothing), and each named cell's poll
status and applied flags. shadow is {state:'off'} when the poller is off.
Deploy, monitor and fence identities may read it; capacity and asia-proof
may not. With no user id, the map names the user; an unknown host answers
empty.
* Catch Claude's sign-in link through BROWSER and wait 10 minutes for sign-in
A hidden claude auth login now runs with BROWSER pointed at a small script in the
account folder that hands the localhost-callback link back to Orca. Orca opens it
in the default browser as before, or hands it to Settings to copy when asked.
Windows host sign-ins keep Claude opening the browser itself.
* Add Copy sign-in link instead to Claude Add Account and Sign in again
* Only catch the Claude sign-in link when copying it
Plain Add Account and Sign in again no longer set BROWSER, so Claude opens the
browser itself exactly as before. A copy-link sign-in that cannot prepare its
BROWSER stand-in now fails instead of opening the default browser, a failed
chmod in WSL no longer stops the login, and a failed clipboard write shows the
existing copy-failed toast.
* End a copy-link Claude sign-in that gets no sign-in link, and simplify its plumbing
A missing or unexpected link now fails the sign-in instead of leaving a 10-minute
spinner. The link check moves to main and only tells the localhost link from the
pasted-code one; the poll is a plain loop, the pending link exists only for
copy-link sign-ins, and copyLink travels as a plain boolean.
* Pass the started Claude sign-in to the copy-link helper and give its copy-failed toast its own key
* Remove a partly written Claude sign-in script when setup fails
After the database answers a reconnect verify or a phone resolve, the
director compares that answer with the shadow seat map and counts the
result by class, logging one count line per route and class a minute and a
few rate-limited samples of unexplained disagreements (cell and epoch only,
never raw ids). It runs only when the seat poller is on, inside try/catch,
and never changes a response.
A map-behind mismatch is explained only while this director first saw the
newer epoch within 10 s (recorded only by the compare; classification is
pure). A seat on a cell not polled within 10 s is cell-unpolled. A null
answer for a seat on a cell placement's liveness rule calls dead is
map-only-cell-unlive, or cell-unlive-pending when its heartbeat ran out
after the last cell-list read.
* Let a Codex chat stop a command it left running in the background
A command Codex started and left running (a dev server, a watcher) showed in
the background-tasks strip above the composer with no Stop, because Codex
reported no stop it could honour. Codex 0.140+ has one: the app-server's
experimental thread/backgroundTerminals/terminate names the process by the
processId on the command's item, and thread/backgroundTerminals/list shows
what still runs.
The command tracker now keeps each command's processId. The first time a
running command carries one, the session probes its own app-server once with
a one-row list; a listing marks the app-server's backgrounded commands
stoppable and the session as offering per-row Stop and Stop all, through the
fields the strip already reads. Any refusal (an older Codex answers "unknown
variant"), timeout or malformed answer leaves it as today: no Stop.
A Stop terminates each named process, then confirms against the thread's own
list. A thread Codex says it unloaded counts as stopped; a dropped connection
does not, since lost contact is no proof a process ended. An app-server that
times out is not asked again. The killed process's own item/completed removes
its row, as before.
* Ask Codex again after a probe gets no answer, and say when a background Stop fails
A background-terminal probe that timed out or lost its request used to hide
Stop for the rest of the app-server's life. Only a refusal by name (unknown
variant, method not found) or a malformed listing now settles it as
unsupported; anything else is asked again on the next update.
A background Stop whose agent failed (a process still listed after its stop,
a timeout) used to answer "nothing stopped" and the button simply came back.
The host now refuses it, so both clients show their usual notice: "The
background task wasn't stopped." This covers Claude's background Stop too.
* Say a background Stop wasn't stopped only when the agent shows it still running
A timeout or lost contact with the agent leaves the outcome unknown, so it now answers
with the existing outcome-unknown refusal instead of "The background task wasn't stopped."
* Keep a background task's Stop in its stopping state until the row leaves
A confirmed Stop re-enabled its button for the moment between the host's answer
and the roster dropping the row. The button now holds while the host says it
stopped the task and the strip still lists it, on desktop and phone; a failed,
unconfirmed or nothing-stopped answer, or the row coming back, offers Stop again.
* Keep Grok's own subagent Stop refusal quiet, and a reply about another subagent unconfirmed
* Report Grok's own refusal of a subagent Stop as not stopped
Grok answers an ended or unknown subagent with a normal reply, so a JSON-RPC refusal means it did nothing and the subagent still runs.
* feat(relay): key id on assignment leases; cells shadow-check the lease behind ticketCheck=shadow (off)
Directors name the lease signing key with a kid. With the per-cell
ticketCheck switch at shadow, a cell verifies the lease a host echoes in
x-orca-relay-assignment-lease, compares it with the database answer that
still decides, and logs per-class counts and the hello's database read
time once a minute.
* feat(relay): flat per-class totals on the lease shadow line, so log metrics can extract them
* refactor(relay): the lease shadow line carries the flat classes and one database-refused count
The per-class-and-verdict map duplicated the flat classes; disagree
already names the valid-lease refusals, and dbRefused gives the total.
Directors can poll each cell's read-only seat feed (#26803) once a second
and keep an in-memory map of which host is seated on which cell, plus a
bounded 24 h recently-left memory. Off unless ORCA_RELAY_SHADOW_SEAT_FEED_CELLS
names cells. Applies a change at or above the seat's generation, follows
cut pages, resyncs on a gap or new incarnation, and checks the map against
counts.seats. A 404 reads as no feed; timeouts and errors keep the last
seats as unverifiable. The cell list is every cell (an existing-only cell
still seats hosts), from RelayAssignmentStore.seatFeedCells with
placement's liveness rule; completeness waits only on live cells, and a
failed list read backs off. Nothing reads the map yet.
* feat(relay): local /ready behind the per-cell readinessLocal switch (off)
With the switch on, a cell's /ready answers 200 while it listens and its
host token verifier holds keys, and reports failing probes as degraded.
The heartbeat keeps the SQL-gated verdict, so directors still stop
placing on a cell that cannot reach the database.
* refactor(relay): the app verifies relay tokens with the server's shared key set
One cached JWKS per process now serves the host-control upgrade, HTTP
token checks and local readiness, instead of a second set in the app.
* Start headless automations' agents through the launch executor
Server-run automations, both the new-workspace and existing-workspace runs, now start their agent
through executeAgentLaunch as a terminal-only launch with automation-owned factories. argv agents
keep the prompt folded into their command; agents that read the prompt after start get it from the
executor's own terminal writer once their composer is ready. The run is recorded as soon as the
terminal exists, as before.
* Deliver headless post-start prompts on Windows hosts and keep session starts out of run completion
- Headless automations pass the write guard's write-unless-shell policy, as worker briefs do,
so a host that cannot find the agent in front (Windows) still gets the prompt; only a proven
shell refuses. agent.launch keeps refusing by default.
- judgeIdleRun ignores session-boundary status rows: a SessionStart row lands before the
prompt and must not settle the run completed.
- A launch failure after the terminal was published is logged instead of dropped.
* Deliver headless post-start prompts through the create's follow-up writer
QA found the executor's composer-readiness writer drops the prompt of a slow-starting post-start
agent (aider): it watches the input-box signal only ~8 s, then tui-idle, which those agents never
satisfy. Main's follow-up writer (wait for the agent process, type the prompt) delivered it.
- Extract the follow-up writer into an awaitable deliverWorktreeStartupFollowup (same waits,
write and log lines); sendWorktreeStartupFollowupWhenReady wraps it. The runtime exposes it as
deliverStartupFollowup, and the headless launch's deliverTerminalPrompt uses it, so the executor
still sequences the launch with main's delivery.
- Revert the round-1 write-guard option and the judgeIdleRun session-boundary rule, which existed
only because of the executor's writer.
The PTY ownership map and provider registry now speak ExecutionHostId
(`local` / `ssh:<target>`), with this machine as an ordinary host
instead of `null`. An unowned PTY id resolves through
getPtyExecutionHost: a bare id stays local by explicit rule (daemon
restores arrive before ownership is recorded), while a paired-runtime
`remote:` id or a malformed `ssh:` id now fails closed instead of
being driven on this machine's provider.
Co-authored-by: m4air <m4air@m4airs-MacBook-Air.local>
Local file work now has a provider object: createLocalFilesystemProvider binds
this machine's runtime file operations to the filesystem contract, with path
authorization injected from the caller's store. The host router's local
filesystem route carries the factory (as the git route does), and
requireRuntimeFileProvider returns a provider for local too, so the runtime
file commands drop their "SSH provider, else node:fs" branches. Behaviour is
the runtime handlers' own, moved unchanged; desktop IPC handlers are untouched.
Preview, chunk reads, mobile reads and upload commit keep their local arms:
their local limits, result shapes or symlink handling differ from the contract.
Co-authored-by: m4air <m4air@m4airs-MacBook-Air.local>
* feat(relay): log metrics and alerts for fenced and fatal rejections and the lease shadow
orca_relay_database_rejection_fenced and orca_relay_process_fatal become
incident counters on directors and cells, each with an alert at the first
event. The lease shadow line gets one distribution metric per class plus
the hello database read p99, and an alert when bad signatures persist on
a cell for 15 minutes.
* fix(relay): one fleet-wide fenced-rejection alert, director-only fatal page, console-only bad-signature alert
A database fault reaches every process at once, so fenced rejections sum
into one series. Cells' fatal rejections already page through the cell
exit alert, so the fatal page covers directors only. The bad-signature
alert can be held up by one client, so it stays in the console.
* fix(relay): fleet-wide fenced-rejection alert as one PromQL sum, like the fix-level alert
* feat(relay): metric for the lease shadow's database-refused count
* feat(relay): per-cell runtime switches read from Cloud Storage every 5 s
Cells read cells/<cellId>.json from the <project-id>-relay-control bucket,
start on all-off defaults, keep their applied values when the object
cannot be read, and report what they applied in the seat feed. No
consumer reads the switches yet.
* test(relay): exempt the flag channel's poll from the floating-promise census; it catches every failure
* fix(relay): release every switch-file reply the reader does not consume, and bound the read while streaming
404, 5xx, malformed-generation and lower-generation replies, and failed
metadata replies, now cancel their bodies. The object read stops and
cancels at the first byte past 64 KiB instead of buffering it whole.
* feat(relay): runtime status reports the applied cell flags for the flag workflow's read-back
* docs(relay): the switch channel reads per-cell objects only
* fix(relay): a lost database reply ends its connection within statement timeout + 2 s
The request pool sets query_timeout to the statement timeout plus 2 s. A
read timeout poisons its transaction: later statements fail at once,
COMMIT never runs (even when the operation caught the error), no ROLLBACK
queues behind the dead statement, and the client is released with the
error so pg-pool destroys it. Autocommit statements destroy their client
too. The server bounds an orphaned idle backend with idle_session_timeout
30 s. A read timeout is answered 503 and never retried, and a renewal
batch that loses its reply fails whole instead of fanning out.
* fix(relay): read timeout margin 10 s, configurable, so routine 5-7 s database stalls never trip it
A whole-VM stall delays replies the server already timed. With a 2 s
margin the read timeout fired inside routine stalls, destroying healthy
clients and failing requests that succeed today. The margin is now 10 s
(ORCA_RELAY_POSTGRES_READ_TIMEOUT_MARGIN_MS), and a real-server test holds
a reply 6.5 s and requires it delivered on the same connection. The fix
level rises to 2 here, with the lost-reply fix.
* docs(relay): name the startup retry that can see a read timeout, and why it is safe
Boot open retries only an unreachable database. The director's startup
cell reconcile does retry a lost reply, which is safe: it re-runs upserts
and limit-change deletes that converge on the same rows.
* fix(relay): answer 503, not 500, when Postgres ends the session mid-request
Staging B1: pg_terminate_backend during /v1/assign returned 500 29 times.
57P01/57P02 and 08000/08003 join the transient SQLSTATEs; pg's
'Connection terminated' and ECONNRESET count only when the database layer
threw them. transaction() still never retries them.
* fix(relay): answer 503 when the pool acquire fails on a dead socket
Staging B1 re-test: a Cloud SQL restart failed PostgresDatabase.transaction's
acquire with 'write EPIPE', answered 500. An acquire failure with EPIPE,
ECONNRESET, ECONNREFUSED, ETIMEDOUT or EHOSTUNREACH is now a pool connect
failure (no statement ran), and acquire failures count as database-layer
errors. EPIPE mid-statement is treated like ECONNRESET.
* feat(layout): read-only layout.subscribe stream and the round-trip self-check (inert)
- layout.subscribe streams each workspace's layout as an explicit published projection
(tabs, groups, panes and their terminals), never the model itself: no runtime records,
closed-tab records, spawn inputs or legacy persistence values. It is re-projected from the
Store's sessions after each write, only while someone subscribes, and only from the
partition that owns each workspace. Not advertised as a capability and not on the mobile
allowlist, so nothing calls it yet.
- Replaces the unused terminal-topology publisher (#25682) it generalizes.
- The round-trip self-check: a partition must load and save to a fixed point, changing only
the kinds of value the Loader's rules change. It runs on Store writes in dev and e2e (logged,
never thrown) and as the oracle's new roundTrip check.
* fix(layout): know the spawn handoff main's minimal row mint stores
* fix(layout): name-based invented ids, no partition on the wire, isolated listeners
- Every id the Loader invents is derived from what it stands in for (a workspace's group from
its key, a pane from its row or tab and position, a repeated tab id from its terminal), so
removing an earlier workspace or tab no longer re-ids later ones.
- The published layout no longer names its partition: the workspace key is the address and
routing stays with the host; each tab still names the host that runs it.
- One throwing stream listener no longer costs the others the event.
- The self-check says plainly it is an idempotence check, and logs each new kind of first-load
change with its count.
* Keep Chat UI users who opened new tabs in the terminal on the terminal
Before the chat upgrade, Experimental Chat UI had a separate Default view
choice, and it defaulted to Terminal. The upgrade retired that choice so Chat
UI on now means new agent tabs open as chat, which moved people who had kept
the terminal default into chat with no obvious way back.
On load, a profile saved by a pre-upgrade build with Chat UI on and Default
view not set to Chat now gets Chat UI turned off, and is left out of the
one-time native chat upgrade tip. The old key is stripped on the first save,
so the migration runs once and a later opt-in sticks.
* Update the saved-settings test for Chat UI on with Default view Terminal
* Show the upgrade tip to terminal-default users, with a switch to turn on chat mode
People who had Chat UI on with new tabs opening in the terminal now keep the
terminal on upgrade, but they still need the tip's directions for moving
between chat and CLI. They now get the same upgrade tip, plus a note that new
agent tabs still open in the terminal and a "Turn on chat mode" switch.
The switch shows when Chat UI is off as the tip opens and stays put while it is
flipped. The tip is taller in that case so nothing scrolls.
* Decide the chat mode switch from the saved tip audience, not the live setting
The upgrade tip's 'Turn on chat mode' switch showed whenever Chat UI was off
when the tip opened, so a chat-view member who had turned Chat UI off also got
it. Main now answers with one tip variant (none, standard, keep-terminal) from
the saved audience, and only keep-terminal shows the note and switch.
Also moves the feature tip panel styling into a DialogContent surface variant
and drops the Label restyle, and tests that the switch stays put after flipping.
* Let the upgrade tip decide the chat mode switch from its variant
The modal now passes the saved tip variant to the dialog, which decides the
switch itself, so the dialog tests cover that decision.
* feat(relay): one-cell flag write workflow with dry run, compare-and-swap and read-back
Operate Relay Production Cell Flags writes cells/<cellId>.json for one
cell: it checks the origin answers as that cell with a flag channel,
refuses a stale expected generation, writes with ifGenerationMatch,
stamps the actor and run on the object, and waits up to 15 s for the
cell's runtime status to report the new generation applied. The deploy
Workload Identity condition admits the new workflow.
* fix(relay): flag workflow retries a transient 5xx, shows the plan before the generation check, and names the failure
Runtime-status reads go through the shared one-retry admin helper, and a
failed read-back poll no longer ends the wait early. The plan line is
logged before the generation check, a failure is logged as one JSON line
the job summary carries, and the object and summary name the triggering
actor.