Commit Graph
13347 Commits
Author SHA1 Message Date
Neil 7c22f6f045 [CI] Consolidate tests, retire static scans, and update evidence (#27109)
* [CI] Consolidate renderer test modules and retire static scans

* [CI] Keep consolidated test within the existing line limit

* [CI] Refresh evidence for the preserved turn-duration gate
2026-10-09 21:16:00 -07:00
OrcaWinandm4air 8ba3cf46ab fix(runtime): worktree create enforces the lineage host boundary (#27114)
Update and hydrate refuse a parent worktree on another execution host and
the renderer drops such an edge, but create recorded it anyway. Create now
checks the new worktree's host before anything is created: an explicit
parent on another host fails with LINEAGE_PARENT_CONTEXT_CONFLICT, and an
inferred one is dropped with a warning. An unknown host on either side
stays permissive, as in the projection.

Co-authored-by: m4air <m4air@m4airs-MacBook-Air.local>
2026-10-09 20:59:59 -07:00
OrcaWinandm4air b23623681b fix(hosts): the unresolved-owner sentinel is never routable (#27111)
* fix(hosts): the unresolved-owner sentinel is never routable

When a worktree's owner publications conflict, the renderer marks it with
runtime:unresolved-owner so nothing treats it as local. That id parses as
a paired runtime, and AI Vault and the port scanner dialed environment
'unresolved-owner'. The sentinel is now a named shared constant with a
routable-host parser that rejects it; AI Vault list/titles/resume and the
renderer's runtime target builder use it, and the scattered string
compares collapse onto the same helpers.

* fix(worktrees): refuse a create captured on the unresolved owner

Without a routable target, the direct-SSH desktop IPC fallback would have
created the workspace on this machine.

---------

Co-authored-by: m4air <m4air@m4airs-MacBook-Air.local>
2026-10-09 20:59:55 -07:00
Jinwoo Hong 3563270132 ci(release): read the watcher child from the supervisor's slot in the fault harness (#27120) 2026-10-09 23:55:13 -04:00
OrcaWinandm4air 4ec973f588 fix(cli): implicit worktree selection is scoped to the caller's host (#27108)
* fix(cli): implicit worktree selection is scoped to the caller's host

A CLI run from a shell on SSH host D matched its cwd against every host's
worktrees, so a same-path twin on host A could win. Candidates are now
limited to the caller's execution host (the relay's SSH stamp, else this
machine), and a pane's forwarded ORCA_WORKTREE_ID is scoped the same way.

* fix(cli): a local caller keeps runtime-stamped checkouts this runtime holds

---------

Co-authored-by: m4air <m4air@m4airs-MacBook-Air.local>
2026-10-09 20:36:36 -07:00
Jinwoo Hong 03408d8570 ci: skip build:packages on release tags that predate it (#27110) 2026-10-09 23:33:41 -04:00
Neil 440cde0f74 Reduce memory used to count Codex usage and avoid partial totals (#26966) 2026-10-09 20:23:25 -07:00
OrcaWinandm4air f1a09aa9c2 fix(session-tabs): a returning paired publisher is repaired by an authoritative census (#27094)
* fix(session-tabs): a returning paired publisher is repaired by an authoritative census, not dropped

* fix(session-tabs): a census never outranks a stream frame received while it was in flight

* fix(session-tabs): a coalesced census uses the newest subscription and is forgotten with its worktree

---------

Co-authored-by: m4air <m4air@m4airs-MacBook-Air.local>
2026-10-09 20:07:39 -07:00
OrcaWinandm4air b4d5b7de7a fix(terminal): honor the OpenSSH DefaultShell on Windows SSH hosts running orcad (#27085)
* fix(terminal): honor the OpenSSH DefaultShell on Windows SSH hosts running orcad (#9327)

* fix(terminal): honor only a PowerShell-family DefaultShell so agent launch quoting still matches

* fix(terminal): import the registry reader's runner from @orca/process-host

* test(terminal): type the spawnSync stub's options

---------

Co-authored-by: m4air <m4air@m4airs-MacBook-Air.local>
2026-10-09 19:53:58 -07:00
OrcaWinandm4air 80992a5df7 fix(session-tabs): a restored terminal row is retired only by its own host PTY (#27101)
Co-authored-by: m4air <m4air@m4airs-MacBook-Air.local>
2026-10-09 19:51:58 -07:00
OrcaWinandm4air 0969f60c8e fix(native-chat): retire a first send's echo in transcript time, not this client's (#27099)
* fix(native-chat): retire a first send's echo in transcript time, not this client's

A send into an empty transcript had no row to anchor on, so its echo was
matched against rows newer than the renderer's send time. Row timestamps
come from the host that writes the transcript; with that host's clock
behind, the echo's own turn looked older than the send and the queued
bubble never retired.

A send made after the transcript read has settled empty now records that
every row is after it. A send made while the read is still loading keeps
the existing rule, so older history arriving later cannot retire it.

* fix(native-chat): bind the empty-transcript claim to the session that was read

'ready' with no rows is also the placeholder before a provider session is
known, which read nothing; a resumed transcript can then hold an older
identical turn that retired the new send, and with it a rejected or
unconfirmed notice. The claim now needs a provider session id, records it,
and is released for any other session, falling back to the existing rule.

---------

Co-authored-by: m4air <m4air@m4airs-MacBook-Air.local>
2026-10-09 19:40:51 -07:00
OrcaWinandm4air 310c5d02e7 fix(ssh): a direct-SSH host republish no longer steals this view's tab selection (#27100)
* fix(ssh): a direct-SSH host republish no longer steals this view's tab selection

* fix(ssh): keep the home screen out of the kept selection

---------

Co-authored-by: m4air <m4air@m4airs-MacBook-Air.local>
2026-10-09 19:40:02 -07:00
OrcaWinandm4air 33d90bb066 refactor(session-tabs): one authority-repair lane shared by every session-tabs mirror (#27093)
Co-authored-by: m4air <m4air@m4airs-MacBook-Air.local>
2026-10-09 19:39:00 -07:00
OrcaWinandm4air 746e4b42a0 fix(agents): a session dropped on a pane edge splits the pane in paired-runtime workspaces (#27102)
Co-authored-by: m4air <m4air@m4airs-MacBook-Air.local>
2026-10-09 19:38:57 -07:00
OrcaWinandm4air dc12495a35 fix(runtime): automatic tab activation no longer wakes a worktree the host put to sleep (#27081)
* fix(runtime): automatic tab activation no longer wakes a worktree the host put to sleep

* fix(pty): a joined user wake no longer inherits an automatic sleep refusal

* refactor(runtime): pass caller spawn guards through one helper

---------

Co-authored-by: m4air <m4air@m4airs-MacBook-Air.local>
2026-10-09 19:38:54 -07:00
OrcaWinandlianshuang-photo 13d8ca87dd fix(native-chat): reconcile attachment paths with pending messages (#27095)
Co-authored-by: lianshuang-photo <188543100+lianshuang-photo@users.noreply.github.com>
2026-10-09 19:28:37 -07:00
OrcaWinandm4air b9abfa9116 fix(session-tabs): a tab stream the host ends resubscribes on the retry backoff (#27098)
Co-authored-by: m4air <m4air@m4airs-MacBook-Air.local>
2026-10-09 19:28:10 -07:00
OrcaWinandm4air 20c0c6a8fc fix(ai-vault): resume a WSL session only in its own namespace, with a Linux Pi path (#27092)
On Windows a WSL workspace and a Windows workspace are both 'local', so the
resume check let a WSL session resume in PowerShell (and a Windows session
inside WSL), opening a tab that does nothing. Local resumes now also require
the session's namespace (its WSL distro, or native) to match the target's;
an unknown target namespace still refuses only what it can prove.

Pi and Prime Agent resume by transcript path, and got the raw
\\wsl.localhost UNC spelling inside WSL, so they silently started a new
session. Their provider session now uses the same normalized path OMP
already used.

Co-authored-by: m4air <m4air@m4airs-MacBook-Air.local>
2026-10-09 19:27:34 -07:00
OrcaWinandm4air c9b7244bb9 fix(native-chat): lazy WSL tier for Claude, Grok and omp lookup by id (#27082)
Only Codex had a WSL fallback when a transcript is looked up by session id,
so Claude running inside WSL with no hook path was searched only on the
Windows profile and chat showed "Transcript unavailable". Every id lookup
now searches this host's roots first and, only on a miss, each running
distro's roots for the same agent, through the shared per-root WSL scan.

One layout table names how each agent files a session (claude, codex, grok,
omp) for both host and WSL lookups.

Co-authored-by: m4air <m4air@m4airs-MacBook-Air.local>
2026-10-09 19:27:30 -07:00
OrcaWinandm4air d724de9d46 fix(ai-vault): bounded OpenClaw discovery; title filter when a host's search is off (#27089)
OpenClaw discovery walked every directory under <state>/agents and accepted
any JSONL under a 'sessions' segment, so an embedded <agent>/agent/codex-home
was walked (thousands of serialized SSH readDirs) and listed as OpenClaw.
Both the local and remote source tables now enter only 'sessions' below each
agent dir; the OpenClaw source entry moves next to that rule.

When a single host answers that its search is off (relays keep their index
off by design) or unavailable, the History box falls back to the title
filter for that host with a notice, instead of an empty wall.

Co-authored-by: m4air <m4air@m4airs-MacBook-Air.local>
2026-10-09 19:17:13 -07:00
OrcaWinandm4air 905a1c7e23 fix(runtime): keep a floating terminal floating when its cwd is inside a workspace (#27088)
Co-authored-by: m4air <m4air@m4airs-MacBook-Air.local>
2026-10-09 19:16:01 -07:00
OrcaWinandm4air 1726a1079d refactor(ai-vault, native-chat): one per-call agent root table (#27079)
* refactor(ai-vault, native-chat): one per-call agent root table

The native-chat transcript resolver kept its own copy of each agent's
session roots, and it had drifted from the AI Vault source table. Both now
read session-scanner-roots: host roots computed per call, and one table of
where each agent's tree sits under a home dir (host or WSL guest).

AI Vault now lists Claude sessions under CLAUDE_CONFIG_DIR, which the CLI
writes to and native chat already read.

* fix(ai-vault): forward CLAUDE_CONFIG_DIR to the scanner child

The desktop scanner runs with an allowlisted env, so the new config-dir
root was resolved in the parent but stripped in the child that lists
History.

---------

Co-authored-by: m4air <m4air@m4airs-MacBook-Air.local>
2026-10-09 19:05:36 -07:00
OrcaWinandm4air 6f8dc43288 fix(agent-hooks): route the first-work branch rename by the repo's execution host (#27080)
* fix(agent-hooks): route the first-work branch rename by the repo's execution host

* fix(agent-hooks): state runtime branch ownership without overclaiming server wiring

---------

Co-authored-by: m4air <m4air@m4airs-MacBook-Air.local>
2026-10-09 19:00:46 -07:00
Brennan Benson cbc565d804 Let a Codex chat stop a sub-agent from the background-tasks strip (#27026)
* Let a Codex chat stop a sub-agent from the background-tasks strip

A Codex sub-agent showed in the background-tasks strip with no Stop, and
the chat's own Stop interrupts only the main turn, so a helper ran on until
it finished. Codex's app-server takes `turn/interrupt` for any loaded thread,
sub-agent threads included, so a sub-agent row's Stop now interrupts the
turn Orca saw that sub-agent run, through the same background Stop route
Codex commands use.

Codex's -32600 refusal (no active turn, another turn, thread not found)
means the run the row showed is over, so the Stop is quiet; any other
refusal says the task wasn't stopped; a timeout or lost contact is
unconfirmed. A live run showed Codex holds an interrupt that lands as the
turn ends until the sub-agent's next turn ends, so a timeout on a run the
session has since seen end is quiet too. Older clients' `codex-agent:` rows
name no host record and stay unstoppable.

* Stop a Codex sub-agent's own sub-agents and commands with it, and hold Stopping per run

A sub-agent Stop interrupted only that helper's turn, so the commands it was
running (Codex keeps a turn's processes when the turn is interrupted) and the
helpers it had spawned kept going, and its row stayed in the strip. The Stop
now interrupts the helper's run and every descendant's run, then terminates
the processes those threads hold where the app-server has background
terminals. The interrupt goes through interruptCodexTurn, and a small
codex-background-task-stops module composes the sub-agent and terminal stops,
so the terminals module is terminals only again.

A Codex helper keeps one row id (its thread id) across runs, so a confirmed
Stop's held button carried over to the helper's next run. Both clients now
hold it for the run it was pressed on, read from the row's invocation.

* Test that a sub-agent Stop leaves siblings alone and skips terminates after a timeout

Adds direct tests of the per-run Stop hold (including an older host's roster),
matches the desktop hold comment to mobile's, and reuses the one 10 s bound.
2026-10-09 18:54:15 -07:00
Brennan Benson c8b8997762 feat(native-chat): offer Codex Ultrafast as a speed choice (#26962)
* feat(native-chat): offer Codex Ultrafast as a speed choice

Codex lists Ultrafast as a third service tier for eligible models and
accounts (GPT-6.1 Sol). Codex chats saved Fast as on/off, which cannot
hold a third tier. The Codex speed is now a Standard/Fast/Ultrafast
choice built from the tiers the model lists; the tier id is still
resolved from the catalog at each send. Older clients keep their Fast
toggle: the host still reports Fast on/off and accepts it as a speed.

* refactor(native-chat): save the Codex tier id instead of a speed name

The chat now keeps the tier id Codex lists (serviceTier: default,
priority, ultrafast) and sends it as-is on turn/start; Codex drops a
tier the model no longer lists with its own warning. This removes the
saved per-model tier map, the send-time lookup and the reconcile code.
The unused fastModeTierByModel catalog field is deleted. Older clients
still get Fast on/off, derived from the model's tier named Fast. Also
drops the pnpm-lock.yaml churn the first commit picked up.

* test(pi): drop the removed fastModeTierByModel catalog field

* fix(native-chat): review round 1 for Codex service tiers

- Accept an older client's fastMode write through the Codex adapter; it
  was refused before reaching the tier alias.
- Bump the saved model catalog schema so catalogs from before
  serviceTiers re-list instead of offering a Fast toggle with no tier.
- Keep only the reported tier on the record, not the Fast it implies.
- Drop a reported tier, not only a picked one, when the next model does
  not list it.
- Localize Standard inside the speed choice only.
- Return the options answer directly instead of a one-field wrapper.

* fix(native-chat): review round 2 for Codex service tiers

Keep a tier Codex reports for the thread (it accepts unlisted ones such
as flex); read the legacy Fast tier beside an empty serviceTiers list.

* fix(native-chat): review round 3 for Codex service tiers

A reported tier the old model listed and the new one does not resets to
Standard on a model switch, and the reported tier itself stays pickable.

* fix(native-chat): keep a re-picked reported Codex tier

The thread's reported tier is never reset by the unlisted-tier drop, so a
re-picked Flex survives the next read, a model switch, and the turn.

* refactor(native-chat): one rule for which Codex tier a model offers

Standard, a tier the model lists, or the thread's reported tier that no
model lists. The setter and the unlisted-tier reset share it, replacing
the separate reported-tier and model-switch guards.

* test(native-chat): drop the removed fastModeTierByModel field from main's new catalog test
2026-10-09 18:45:03 -07:00
Kelvin AmoabaandBrennan Benson cf5dda642d feat(native-chat): show attached files as filename pills in the composer (#26409)
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>
2026-10-09 18:42:28 -07:00
d3e3b173e6 fix(agents): one host-routed agent-detection entry point (#27054)
* 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>
2026-10-09 18:39:13 -07:00
Brennan BensonandClaude 8b3de5bbfb fix(native-chat): drop the redundant 'Waiting for your answer' line from queued cards (#27073)
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>
2026-10-09 18:35:56 -07:00
Neil 1912c90141 Extract subprocess handling into @orca/process-host (#26963)
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.
2026-10-09 18:32:28 -07:00
15823ac5c0 Keep MCP repository settings on their selected host (#26859)
* 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>
2026-10-09 18:29:38 -07:00
OrcaWinandm4air 3e0b828358 refactor(main): route git probe and terminal-artifact provider checks through the host router (#27066)
Co-authored-by: m4air <m4air@m4airs-MacBook-Air.local>
2026-10-09 18:19:54 -07:00
Neil 5ef576c913 [CI] Reduce fixture work and consolidate related test scopes (#27063) 2026-10-09 18:14:52 -07:00
Neil dd9cf36581 Preserve paired workspace ownership in terminal drag sources (#26931) 2026-10-09 17:56:49 -07:00
OrcaWinandm4air e273ec3119 refactor(ipc): route filesystem git handlers through the host router (#27059)
Co-authored-by: m4air <m4air@m4airs-MacBook-Air.local>
2026-10-09 17:52:09 -07:00
Kelvin AmoabaandBrennan Benson d068d852ba fix(native-chat): hide agent message controls while the turn runs (#26404)
Co-authored-by: Brennan Benson <79079362+brennanb2025@users.noreply.github.com>
2026-10-09 17:43:42 -07:00
Brennan Benson b44a5796c1 fix(native-chat): settle a reply cut off by a server restart (#26460)
* 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
2026-10-09 17:38:38 -07:00
Brennan Benson 59631b8b26 fix(native-chat): edit a queued message in place, even while a question is open (#26886)
* 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.
2026-10-09 17:31:28 -07:00
OrcaWinandm4air 6cf1aa00f4 refactor(ssh): register each SSH host's providers as one set (#27044)
Co-authored-by: m4air <m4air@m4airs-MacBook-Air.local>
2026-10-09 17:31:25 -07:00
Brennan Benson 280ef3c6f3 fix(native-chat): accept sends through their own receipt (#26672)
* 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
2026-10-09 17:25:43 -07:00
09aff1b94d perf(usage): keep per-source usage caches off the main thread (#26980)
* 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>
2026-10-09 17:19:29 -07:00
618dc62889 perf(daemon): send PTY output to main as raw bytes, not NDJSON (#27006)
* 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>
2026-10-09 17:18:48 -07:00
Jinwoo Hong 9b0117480a Add the inert layout client for local and remote runtimes (#27000)
* 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.
2026-10-09 20:17:23 -04:00
Kelvin Amoaba 3dd332ec0d fix(chat): keep select-all and copy off the terminal under a chat (#26831)
Cmd+A in the transcript selected the hidden terminal, and every later
Cmd+C copied that instead of the highlighted message.
2026-10-09 17:05:26 -07:00
Brennan Benson 094f589977 Floating-workspace chats of every native-chat agent keep links in the folder they started in (#26981)
* 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.
2026-10-09 16:57:24 -07:00
Brennan Benson a825a84a0f test(search): keep Kiro out of older hosts' expected history-search agents (#27002)
#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.
2026-10-09 16:56:37 -07:00
Brennan Benson 7634bbfffe Start plain new-tab agent launches through the host's agent.launch (#26489)
* 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.
2026-10-09 16:55:28 -07:00
Brennan Benson 7450f40662 Every agent reports its learned default model once, when a chat starts (#26982)
* 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.
2026-10-09 16:53:30 -07:00
OrcaWinandm4air 8e4716aa65 refactor(pty): PTY controller inventory and startup waits take a host id (#26998)
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>
2026-10-09 16:48:14 -07:00
Jinjing 1ad28ce81e Update WeChat group 11 QR code image (#27036)
Replace the docs asset so the WeChat group 11 join QR code points people to a current code.
2026-10-09 16:46:18 -07:00
Jinwoo Hong 9d94a20f84 fix(browser): stop a cancelled drag from leaving every browser tab unclickable (#26986)
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.
2026-10-09 19:39:11 -04:00