Commit Graph
2779 Commits
Author SHA1 Message Date
NeilandMatheus Breguêz b3aa719a57 Preserve Git history lanes through unrelated roots (#26932)
Preserve active graph lanes through unrelated root commits and share node/reference colors.

Isolate real-Git fixtures from inherited Git settings and use the existing public process-host import after the current-main package migration.

Co-authored-by: Matheus Breguêz <matbrgz@gmail.com>
2026-10-10 01:10:30 -07:00
cb69d52455 fix(repos): deleting a project with live terminals warns and needs --force (#27172)
* fix(repos): deleting a project with live terminals warns and needs --force

orca project setup-delete now refuses a repo-backed project that still has
terminals not known to have exited or saved workspace details, unless --force.
The sidebar remove dialog says how many open terminals will close.

Co-Authored-By: Claude <noreply@anthropic.com>

* fix(repos): count a sibling host's host-qualified workspace metadata in the delete guard

Co-Authored-By: Claude <noreply@anthropic.com>

* test(sidebar): cover the remove dialog's open-terminal warning

Co-Authored-By: Claude <noreply@anthropic.com>

* test(sidebar): type the dialog mock state without assertions

Co-Authored-By: Claude <noreply@anthropic.com>

---------

Co-authored-by: m4air <m4air@Mac.localdomain>
Co-authored-by: Claude <noreply@anthropic.com>
2026-10-10 00:44:55 -07:00
f307391637 fix(pairing): pairing with this same Orca profile is refused (#27166)
Every add/re-pair path goes through the environment store; it now compares the
offer's public key with this profile's own runtime key before saving.

Co-authored-by: m4air <m4air@m4airs-MacBook-Air.local>
Co-authored-by: s546126 <s546126@users.noreply.github.com>
2026-10-09 23:46:14 -07:00
Neilandggbdpq c857fcc578 Display valid multi-day automation schedules (#26850)
Co-authored-by: ggbdpq <ggbdpq@gmail.com>
2026-10-09 23:41:33 -07:00
OrcaWinandm4air 1a6d570412 fix(shell): OS open actions follow the file's owner, not the focused server (#27165)
* fix(shell): OS open actions follow the file's owner, not the focused server

Every shell open request (openPath, openInFileManager, openInExternalEditor,
openFilePath, openFileUri) now carries the path's owning host. Main allows
only a local owner, plus the SSH editor launch when the owner is that same
SSH target, and no longer consults the focused server. The renderer reveal
and Open-in checks drop their focused-server check to match.

* test(e2e): name the owner on the local editor control open

* fix(explorer): Open in follows the catalog owner for folder and host-only SSH workspaces

The explorer toolbar and the source-control Open in menu took their SSH
connection from the repo row, which a folder workspace or a host-only SSH
repo does not have, so the route read as local. Both now derive the route
from the catalog-confirmed owner.

* fix(sidebar): Open in keeps the SSH target of folder and host-only SSH rows

The row resolver now returns the SSH connection it resolved alongside the
runtime owner, so a folder workspace or host-only SSH repo with no repo
connection no longer reads as local.

---------

Co-authored-by: m4air <m4air@m4airs-MacBook-Air.local>
2026-10-09 23:39:05 -07:00
OrcaWinandm4air 36d106a8bc fix(mobile-pairing): preselect LAN for a signed-out desktop (#27145)
* fix(mobile-pairing): preselect LAN for a signed-out desktop; sign-out clears the Relay region cache

* fix(mobile-pairing): give the changed Relay sign-in copy its own localization key

---------

Co-authored-by: m4air <m4air@m4airs-MacBook-Air.local>
2026-10-09 22:23:10 -07:00
Neil 15717a4e53 Add CLI commands to manage and find workspace references (#26807)
* Add CLI commands to manage and find workspace references

* Fix reference review findings: source-context twins, legacy link collapse, closed chats, old-host errors

- Keep same-URL links with different source contexts separate; CLI add/remove no longer drops a twin
- Restore merging a bare legacy link into its single matching URL link (no duplicate PR rows, no resurrection on older-client removal)
- Carry absorbed data through collapse chains; resolve repoId conflicts deterministically
- Identity: ignore www., case-insensitive GitHub/GitLab/Gitea paths (stored URL keeps case), no Jira match on git hosts; find accepts self-hosted issue URLs
- reference find: skip closed native chats, require a session id for linked, include an explicitly named archived workspace, warm-cache fallback for --worktree
- Selector parity: collapse same-host duplicate paths, issue: ignores unlinked rows
- CLI: clear update-host error for hosts without reference.*, local flag checks before host probe, help text fixes

* Fix CI: add reference rm alias for verb policy; expect linkedItems in worktree ps summary

* Simplify reference CLI: share identifier/legacy-match helpers, drop redundant dedupe
2026-10-09 22:22:01 -07:00
OrcaWinandm4air 16e4d32978 fix(hosts): a server whose status answers is selectable while shared control connects (#27137)
A runtime host whose status probe verified is reachable even while its
shared-control socket is still connecting; the status owner already withdraws
that verification whenever a ready socket drops, so a status retained from a
previous socket still reads as connecting.

Fixes #10704

Co-authored-by: m4air <m4air@m4airs-MacBook-Air.local>
2026-10-09 21:59:12 -07:00
Neil 8452fc3315 Keep tool results with the right command and reveal all results on mobile (#26968)
* Pair native chat tool results in linear time with provider identity

* Preserve orphan tool output and independent disclosure identity

* Keep mobile tool detail attached to its command after row changes

* Keep named mobile results open across copied history refreshes
2026-10-09 21:47:51 -07:00
OrcaWinandm4air 7bd3928159 fix(runtime): a terminal close commits in the session partition that holds the tab (#27119)
* fix(runtime): a terminal close commits in the session partition that holds the tab

One worktree's tabs can be split across the local and runtime:<id> saved
partitions. The host close searched only the routed partition, so a tab
in the other one was reported tab_not_found (or recorded nowhere useful)
and the next snapshot brought it back. The close now resolves every
partition that holds that exact tab id and commits there.

Fixes #18202

* fix(runtime): refuse a close when a second copy of the tab was rebound since it was asked

The owner fence checks only the first partition holding the tab, so a
second legacy copy rebound before admission was retired and its PTY
collected for kill. Owner-fenced closes now capture every holder and
refuse with terminal_pane_owner_changed when a later copy is new or
changed.

* fix(runtime): fence a close's second copies on snapshots, not live sessions

The binding writer mutates a session in place, so comparing a captured
session reference with the current one compared a replacement with
itself. Capture an immutable copy of each holder's row, bindings and
incarnations when the close is asked.

---------

Co-authored-by: m4air <m4air@m4airs-MacBook-Air.local>
2026-10-09 21:20:11 -07:00
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 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
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
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
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
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
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
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
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
OrcaWinandm4air 0a1d09d237 fix(layout): an absent editor or browser map is not a round-trip change (#27007)
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>
2026-10-09 16:07:56 -07:00
Jinwoo Hong 000cfe5193 Add "Copy sign-in link instead" for Claude accounts (STA-9912) (#26990)
* 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
2026-10-09 18:57:35 -04:00
Brennan Benson e2234a37ef Let a Codex chat stop a command it left running in the background (#26780)
* 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.
2026-10-09 15:44:34 -07:00
Jinwoo Hong 0703073b61 Add an inert read-only layout stream and the layout round-trip self-check (#26964)
* 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.
2026-10-09 17:29:02 -04:00
Brennan Benson 5ffc8d7972 Keep Chat UI users with a Terminal default on the terminal, with a chat mode switch in the upgrade tip (#26915)
* 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.
2026-10-09 14:21:41 -07:00
62878c3b0c fix(folders): folder workspaces can answer "paired server" (#26958)
* fix(folders): folder workspaces can answer "paired server"

* fix(folders): name a server folder's nested target only when its id is unambiguous

* fix(folders): read the folder group only when the answer needs it

---------

Co-authored-by: m4air <m4air@Mac.localdomain>
Co-authored-by: m4air <m4air@m4airs-MacBook-Air.local>
2026-10-09 13:50:36 -07:00
Brennan Benson 2356baa181 Open a chat visual in its own tab instead of the sidebar (#26728)
* Open a chat visual in its own tab instead of the sidebar

* Keep chat visual tabs alive across tab switches and finish editor-tab consolidation

- Render chat visual tabs from a worktree-level retained overlay so switching
  tabs or moving the tab to another split never reloads the page.
- Reopening a visual focuses it in whichever split holds it, takes its newest
  title, and opening from the floating panel no longer takes the main
  window's editor selection (check-details opener uses the same helper).
- Visual tabs return to the reopen-closed stack and show in the tab palette.
- One editor-family list and one virtual-tab predicate: no Copy Path, Copy
  Relative Path, Reveal or git status decoration for check-details and chat
  visual tabs.

* Give chat visuals one open path and re-read them when shown

- Reopen-closed (Cmd/Ctrl+Shift+T) of a chat visual goes through the visual
  opener, so it focuses the visual in whichever split holds it and keeps its
  id when a runtime environment is active.
- A visual tab re-asks its host each time it is shown or reopened; an
  unchanged revision keeps the page as is, a changed one shows the new page,
  and an unavailable tab recovers.
- The tab palette no longer shows or matches a virtual tab's internal id; the
  new-tab search shows the chart icon for visuals.
- Cmd/Ctrl +/- uses app zoom and Alt+Z leaves word wrap alone while a virtual
  tab (CI details, chat visual) is active.
- A tab dragged over a visible visual passes through the frame, using the
  existing drag passthrough.

* Slim the chat visual tab back to the minimum

Remove the retained overlay, the visibility re-read, the zoom and word-wrap
rerouting, palette inclusion, reopen-closed routing and the check-details
opener change. The visual tab renders through the editor area again, stays
off the reopen-closed stack, and keeps: one tab per visual across splits,
no main-window selection change from the floating panel, the newest title on
reopen, and no Reveal or git status letter on virtual tabs.

* Re-read an open chat visual when it is opened again; no watcher for virtual tabs

- "Open in new tab" on a visual whose tab is already on screen in another
  split now re-asks the host, so a rewritten visual shows its new page.
- CI details and chat visual tabs no longer start a workspace file watcher.
- Drop the unreachable chat-visual case from the restore-time transient check,
  use the shared virtual-tab check in the header path, and say why visuals
  stay off the reopen-closed stack.

* Let tab drags pass over chat visuals; hide path copies on virtual tabs

- A chat visual's frame joins the existing tab-drag pointer pass-through, so
  a tab dragged over a visual (in its tab or inline in a chat) can drop or
  split there instead of the frame swallowing the drag.
- The tab menu no longer offers Copy Path / Copy Relative Path for CI details
  and chat visual tabs, whose path is an internal id; no stray separator.

* Reuse the hover icon for editor tab icons, so a chat visual shows its icon in both
2026-10-09 13:27:08 -07:00
Brennan Benson 573ff0bca3 Save a new chat's first message during create (#26332)
* fix(native-chat): the host owns the send queue; the window keeps no saved outbox

The desktop kept each structured chat's unsent messages in localStorage and
sent them in order, so one message whose fate was unknown froze every later
send, Retry dropped it silently, failed sends could not be discarded, and an
offline chat could send hours later. The host already records every message
and owns the queue; the window now only sends.

- One in-memory sender for composer, launch prompts and messages sent from
  outside the chat. One send in flight per chat; a transport failure resends
  the same id for up to 30 s; a refusal that proves nothing was recorded puts
  the text back in the composer with the reason; a send that went out and was
  never answered shows an in-doubt line with Send again and holds nothing up.
- The host's "unknown" rows get the same in-doubt line and Send again, from
  the journal, in every window.
- A message an older build left in localStorage is never sent: the host's
  conversation outline decides what goes back to the composer, and the copy is
  deleted once that is saved.

* fix(native-chat): send through the structured chat RPC wrapper, with its timeouts

The sender called the runtime RPC directly, skipping the per-method timeouts
every other structured chat call gets. Only a remote host's request skips the
compatibility check the sender already ran.

* fix(native-chat): an unconfirmed send goes back to the composer, with no new row line

The common pattern draws nothing extra on a message whose delivery is in
doubt once its turn is over, and puts a failed send's text back in the
composer with the reason. So a send nobody answered in time, or one a host
answers in a way that proves nothing, goes back to the composer worded as
unconfirmed, and a host "unknown" row shows nothing extra. Removes the in-doubt
phase, Send again, and its strings.

A host's made-up row for an id its journal lost now reads as unconfirmed, not
as recorded, so that message comes back instead of vanishing.

* test(native-chat): pin that nothing resends a send given back as unconfirmed

* fix(native-chat): sends survive a tab close, never resend after a Stop, and keep remote images

- A normal tab close lets sends on their way settle; what the host never took
  goes back to the conversation's draft. Only a cancelled launch or a worktree
  purge drops them.
- After any Stop, a send already on its way is never sent again under its id:
  a doubtful answer, or a resend that was due, hands its text back worded as
  unconfirmed.
- A returned image keeps the SSH connection it lives on; the connection never
  goes to the host.
- An older build's saved message the host recorded and then rejected is left
  to the host's own row, never handed back.

* fix(native-chat): a Stop or a tab close never resends a send already out, and a kept card is the card's

A Stop that landed while a same-id resend was being readied (its timer
fired, its request not out yet) let that resend go out after the Stop.
The sender now tracks whether a request is awaiting its answer: a Stop
hands back every send between attempts as unconfirmed, lets one whose
request is out settle from its answer, and never issues a request after
it. A normal tab close withdraws the same way instead of doing nothing,
so nothing more goes out and nothing is dropped.

A send the host rejected but kept as a card (keptAsQueuedMessageId) is
the card's, from its reply or the journal, and never goes back to the
composer.

Adds the freeze tests: a send whose fate is unknown holds later sends
no longer than its deadline, and one the host can neither confirm nor
deny releases the next at once.

* fix(native-chat): read a resend's turned-away call or reused id as unproven

Ports the send-answer proof contract. A call the host turned away before
running it (method_not_found, invalid_argument, unauthorized) proves only
that this request wrote nothing, so it reads as never sent on a first
attempt only; on a resend an earlier attempt may have landed, and it goes
again under the same id. A resent id the host says was already used for
other content (messageIdReused) proves nothing either, like an expired or
conflicting id.

Pins the rest of the contract: any row the host returns is its own, a
thrown error is no answer whatever its code, and an older build's
refused, held or outlived-Stop copy is handed back once and never sent.

* refactor(native-chat): type the structured composer's send with its attachment type

Keeps NativeChatStructuredSession.tsx within the file length limit.

* refactor(native-chat): drop the kept-card guards the sender never needed

A kept send's row is a rejection that was never a Stop's, so the sender
already reads it as recorded, from its reply or the journal. The test
that pins it stays; the two extra checks only covered a kept row that is
also a withdrawal, which the host never writes.

* test(native-chat): route launch tests' sends through the client wrapper the sender calls

The sender sends through callStructuredAgentSession, but these launch
tests replaced that module with a factory that answered nothing (and still
named a probe that no longer exists), so every launch prompt resent until
its 30 s deadline and the tests timed out. Each factory now hands sends to
the runtime RPC mock the tests already answer, and expectations of a local
send no longer ask for the remote-only compatibility option.

* fix(native-chat): hand a message back without importing the composer's attachment hook

The worktree purge reaches the launch prompt, which hands text back, and
the attachment hook's imports reach the store. A test that builds the
real store behind a mocked one then waited on itself and hung. Handing
back now writes images to the draft store directly, as the hook's helper
did, and a test pins that each returned image keeps its SSH connection.

* fix(native-chat): one send per chat, with no line of sends behind it

A chat with a send out took further messages into an in-memory line and
sent them one by one. A message typed behind one in doubt then hit its
own 30 s deadline and came back as not sent without ever going out.

Now, as the common pattern does, a chat takes one send at a time: while
it is out, Send is disabled and Enter leaves the text in the box. The
sender refuses a second send instead of lining it up, so the 30 s
deadline always runs from the send itself. A Stop or a tab close stops
the one send: settled from its answer if its request is out, handed back
as unconfirmed between attempts, or silently if it never went out.

Notes sent from outside the chat while its send is out stay with their
sender (not ready); a launch prompt that meets the person's own first
message waits in the composer instead of being lost.

* fix(native-chat): give a Stop-withdrawn note back to its chat once its notes were cleared

Notes sent from outside a chat clear once their message is recorded. A
message the host recorded as pending and a Stop then withdrew came back
only to its sender, which had already let go of it, so the text was
lost. The chat's draft now takes it, as an earlier build's outbox did.

* test(native-chat): type the composer-actions probe without a cast

* chore(native-chat): drop outbox wording left in comments and an empty locale group

* fix(native-chat): pace failed checks, keep the host's reason, and hold the chat for its launch prompt

- A send whose checks failed before its request went out (an unreachable
  or incompatible host, an unreadable history) was tried again at once,
  about a thousand times a second for 30 s. Attempts are now paced by
  the attempts made, whether or not their request went out.
- A host that refuses every resend by throwing (native chat turned off,
  a journal that won't open) gave back only "couldn't confirm". The
  host's reason now comes first, still without claiming not sent.
- A launch's prompt now holds the chat's one send from the click, drawn
  as sending, so a message typed while the chat starts can't overtake
  it; it goes out once the chat exists, and a cancelled launch frees it.
- Notes whose send nobody could confirm say so instead of "did not
  accept", and notes launched into a new chat let go of their text once
  that chat's composer holds it.
- An open chat keeps drawing a recorded send until its row arrives, so a
  reply that beats the history no longer makes the message flicker out.

* fix(native-chat): hold a chat's sends until it has started, and send a failed chat's message with its restart

A message sent to a chat still starting went out at once to a host that
had no record of the chat yet, read for a fence it could not get, and
came back as not sent. A message sent to a chat whose start failed
restarted it but no longer went with the restart.

While a chat starts, Send stays off and Enter leaves the text in the box,
as with a send already out. A text message sent to a chat whose start
failed restarts it and goes as the restart's first message, through the
same staged-prompt path a launch prompt takes: it holds the chat's one
send slot, drawn as sending, until the chat exists, and comes back to
the composer if the restart fails again. Notes wait while a chat starts
and ride a failed chat's restart, keeping their text until it is sent.

* chore(native-chat): test the queue request, rejection words and card hand-offs; drop an unused clear

- Pin which sends ask the host to queue, the moved rejection wording,
  and that a queue send whose card was handed off and then refused or
  withdrawn, or whose replay names a withdrawn card, is never handed
  back.
- The send-at-most-once gate now says what the desktop promises after a
  reload: it never sends the id again, and hands the text back.
- The legacy read names when it goes, and drops the notice clear nothing
  called.

* fix(native-chat): keep a sent launch prompt's entry, and give back at once what can't go out

- Cleaning up a launch prompt released its send slot even after the
  prompt had gone out, which deleted the entry the sender keeps once the
  host records it. A launch prompt recorded as pending and then
  withdrawn by a Stop was lost from both the chat and the box, and an
  open chat dropped the new chat's first message before its row arrived.
  The slot now gives back only a reservation that was never sent.
- A send stopped before its request went out by something trying again
  won't clear (this client and the server can't talk, or the host
  refused the history read) kept Send off for 30 s and then said Orca
  couldn't reach the agent. It now comes back at once with its own
  cause: not sent, since nothing went out, or unconfirmed if an earlier
  attempt did. Transport errors keep the paced retries.

* fix(native-chat): notes keep their own text through a new agent's launch

Notes sent to a New agent whose start failed came back to the notes and
also sat in the new chat's composer, so sending both delivered the text
twice. The notes keep their text until it goes out (they hold it from
the click), so the launch now says so and no composer gets a copy, on a
failed start or a refused prompt alike. The tests that asserted the copy
in the composer pinned the old double ownership and now assert the
notes are its only owner.

Notes that rode a failed chat's restart and ended unconfirmed now say
Orca couldn't confirm them, as notes sent directly do, instead of that
the agent did not accept them.

* chore(native-chat): the send-once gate says what the desktop keeps across a reload

The desktop keeps a send's id in memory only: a send still unsettled at a
reload or crash is not resent and not handed back. The coverage notes no
longer credit the renderer tests with durable identity across a remount.

* fix(native-chat): say once why notes sent to a new agent did not go

Since notes keep their own text through a new agent's launch, a prompt
the host refused, nobody could confirm, or that found the chat's send
taken came back to the notes with nothing said anywhere: the chat shows
no notice for text its caller keeps. The notes menu now reports it once,
with the toast a send to an existing chat already uses: not accepted,
couldn't confirm, or not ready. A start that failed still says so in the
new chat instead.

* fix(native-chat): retry a history refusal the host says clears, and name it at the deadline

A send stopped before its request went out came back at once for any
refusal of the history read, including ones the host names as clearing
(its journal briefly unavailable, a chat detached while the host quits),
so the automatic retry was lost. Only a version mismatch and refusals
that won't clear come back at once now; the rest go again on the paced
schedule, and if the deadline still finds nothing sent, the words are
the host's refusal rather than Orca couldn't reach the agent.

* feat(native-chat): a send makes one request, and nothing ever sends it again

The desktop resent a message under its own id for up to 30 s when the
answer was lost. A send now makes exactly one request, as the common
pattern's clients do:
- An answer that is lost, dropped or proves nothing hands the text back
  at once with "couldn't confirm… check the chat".
- A failure before the request goes out (the environment check, the
  history read for the fence, a refused connection) means nothing went
  out: the text comes back at once with its own reason, or as not sent.
- The 30 s cap stays on the one request, so a host that never answers
  can't hold Send.

Nothing ever resends, so nothing can go out after a Stop: a Stop only
takes back a send still in its pre-send checks. The resend state goes
with it (tries, generation, awaiting, stopped, the resend timer, the
send-answers-proof probe, and the first-attempt/resend split in the
evidence), and the send-once gate says the desktop never resends.

* fix(native-chat): notes keep the chat's line, and a send held behind a rewind says it was not sent

- Only a send whose text belongs to the chat's composer clears the chat's line. Notes sent from
  outside the chat no longer wipe a "couldn't confirm... Check the chat" line that explains text
  already back in the box.
- A send the host turns away behind a rewind it could not confirm went back as "not sent" but said
  "couldn't confirm what happened. Check the chat". It now gives the rewind's reason and says the
  message was not sent.
- Reliability gate names the single-request test and records a fresh evidence run; the sender
  test drops its leftover resend mocks and a duplicate Stop test.

* fix(native-chat): a send ends even when putting its text back fails

If writing the returned text into the chat's draft threw, the send never settled: it stayed
"sending", the chat refused every later send until a reload. The send now always ends after a
hand-back, the failure is logged, and the chat's line adds "Couldn't save your message."

* fix(native-chat): a message typed during /clear stays in the box and follows the chat

A /clear moves the chat to a new conversation, and its host refuses any send while it runs. A
message sent then went out, came back refused into the old conversation's draft with its line, and
vanished when the chat moved on: the box and the line now belong to the new conversation.

- A /clear holds the chat's one send slot while it runs: Enter does nothing, Send shows busy and
  the text stays in the box, as for a send that is out. Notes sent from outside get "busy".
- Once it moves the chat, the old conversation keeps taking no send until the view leaves it, and
  what is left of its draft moves into the new conversation's draft, after anything there: when the
  composer's /clear settles, and again when the view moves.

* fix(native-chat): no "Send message?" or queue clear while the chat's send is out

While a send is out (or a /clear runs) the chat takes no message, yet Enter over a held queue still
opened "Send message?", and Clear queue deleted every card before its message was refused. Enter now
does nothing there and the text stays; Clear queue re-checks and deletes nothing if a send went out
after the question opened.

* refactor(native-chat): take the /clear draft carry out of this change

Moving the old conversation's draft into the one a /clear replaces it with fixes a bug main has too
(text left in the box during a /clear stays with the old conversation), so it goes in its own change.
Kept here: a /clear holds the chat's send slot while it runs, and the conversation it moved away from
takes no send until the view leaves it.

* fix(native-chat): a /clear's hold on sends always ends

- A view that unmounted while a /clear that moves the chat was out left the old conversation
  holding its sends until a reload: the late reply kept the hold for a view that was gone. The
  reply now releases it.
- The local call for a conversation command has no deadline of its own, so a /clear that never
  answers kept Send off for good. The hold now also ends at the command's deadline (195 s, the one
  the remote call already uses), whichever comes first.

* test(native-chat): opening a chat an older build left stuck; hand back its copy in send order

Pins, through the real chat hook, sends, legacy recovery and draft store,
what opening such a chat does: the queued messages behind a message the
host recorded in doubt come back to the composer once, in order, with the
"not sent" or "couldn't confirm" wording; the chat is free to send under
its read's fence; nothing happens while the chat has no fence here.

The legacy reader now hands entries back in the order they were sent
(queuedAt), as the older build's reader did, instead of array order.

* fix(native-chat): a send this window turned away for a re-paired server comes back as not sent

A managed server's update rotates its pairing, and this window's main
process then answers the next call itself, before forwarding anything,
with runtime_environment_changed. The send read that thrown answer as
proving nothing, so the person was told Orca couldn't confirm a message
that never left. It is now handed back as not sent, with the reason.
Every other thrown answer still reads as unconfirmed once the request
may have gone out.

* Save a new chat's first message during create

* Align opening-message types with the user-message wire

* Keep chat publication and cleanup outside store initialization

* Complete the create refusal fixture ledger

* test(native-chat): prove a created first message waits for the agent's start and is handed over once

* refactor(native-chat): keep the host and session hook under the line limit after the main sync

* test(native-chat): a chat created with its first message and no pick still teaches its agent's default

After the main sync, a created chat's agent starts from the delivery loop rather than the create
call. Grok (through the real ACP adapter and model catalog) and Claude (through the real adapter)
each still report the model their own config resolved to once the chat reads its options.
2026-10-09 13:05:35 -07:00
OrcaWinandm4air 6a929b2023 fix(hosts): host registry stops minting client SSH hosts from a server's nested target (#26952)
Co-authored-by: m4air <m4air@Mac.localdomain>
2026-10-09 12:59:19 -07:00
Jinwoo Hong 8b2a5e45f1 Re-land Claude per-account folders (#24434, #26405) with the upgrade and proxy fixes (#26801)
* Revert "Revert Claude per-account folders (#24434, #26405) before release (#26638)"

This reverts commit 9ca9b4e36b.

* Move the terminal daemon protocol to 44 for the re-landed claude function

v43 daemons (the revert) lack the claude account function, so new terminals must not reuse them. v42 and v43 stay attachable.

* Move the local build compatibility contract to daemon protocol 44

* Point the old-terminal test at daemon protocol 44

* Claude accounts re-land: never sign anyone out, keep proxies working, accounts mirror ~/.claude (#26769)

* Run a Claude account on System default until it has its own login

An account saved before per-account folders has no login in its folder. While System default (~/.claude, or the user's own CLAUDE_CONFIG_DIR) is signed in to the same email, the router sends every launch there and writes that choice into the pointer file, so nobody is logged out by the update. Once the account signs in to its own folder, the next launch uses it. The WSL router follows the same rule with the guest's ~/.claude.

* Refresh each Claude account folder from ~/.claude before every launch

~/.claude (or the user's own CLAUDE_CONFIG_DIR) is now the master copy. Each account folder gets its whole settings.json, including the proxy address and its key, and every .claude.json key except the login, Claude's account caches and install ids, so first-run questions are not asked again. Every other top-level entry is linked (folders) or copied (files), except Claude's per-folder daemon, jobs, live sessions, login files and throwaways. sessions is no longer shared, and an earlier link is undone.

The refresh runs before every Claude Orca starts, for each new terminal and on account switch. Orca never writes back to ~/.claude. The ledger that tracked selective sharing is gone.

* Route chat auth and unselected-account usage through the Claude router

Chats decided whether to drop inherited Anthropic auth from the selection alone, so an account running on System default lost the user's own key. They now ask the router, as terminals, AI commit messages, automations and usage already do. Usage for an unselected account is read where its launches would run. A test reads every entry point through the fallback and fails if a launch path outside the router reads the Claude selection or builds an account folder. The router's two launch errors move to their own file to keep it under the line limit.

* Show Claude account sign-in prompts only when an account cannot run

With the fallback, an account System default is signed in to already works, so:
- The one-time "Finish setting up your Claude accounts" toast is gone.
- Accounts System default covers no longer need a sign-in: no Sign in in the status bar menu or Settings, and they can be selected.
- The old-terminal banner shows only when claude in that terminal would run a different account than the selected one, and asks again after a switch. Its wording is unchanged.

The retired toast's saved flag stays in the paired-client schema so an older client's write is still accepted. The email comparison moves beside the login reader to keep the router under the line limit.

* Keep the user's shell Anthropic auth on Claude account launches

A signed-in account's launches used to drop ANTHROPIC_API_KEY, ANTHROPIC_AUTH_TOKEN and CLAUDE_CODE_OAUTH_TOKEN from the inherited environment while ANTHROPIC_BASE_URL passed through, so a proxy set in the shell got the subscription login and answered 401. Host launches on an account now keep the shell's auth with its address, as System default does. Chats still name the account kind in a failed sign-in, now from the router rather than from whether auth was dropped.

* Share from ~/.claude only what Claude itself would seed into a new folder

The deny list missed much of Claude's runtime state, so tokens, locks and live files such as .session_ingress_token, server-sessions.json and remote-control were copied or linked over each account's own every launch. The list now mirrors the one Claude uses when it seeds a fresh config folder from ~/.claude: its runtime entries, every hidden entry, daemon files and agent memory. teams, ide, downloads and scratch are on it, so they stay per folder.

* Merge trusted folders and MCP servers into an account instead of replacing them

Each refresh replaced the account's projects and mcpServers with ~/.claude's, so folder trust and allowed tools recorded only in the account were lost, and a refresh landing between Orca's folder-trust write and Claude's start brought the trust dialog back. Both are now merged per folder path or server name: ~/.claude wins for entries it has, the account keeps the rest, and a folder trusted in the account stays trusted. Both writers take Claude's lock on the file, so the order no longer matters.

* Require the same organization before System default stands in for an account

The fallback compared emails only, so one email saved for two organizations made both accounts run on whichever organization System default was signed in to. When the saved account and System default's login both name an organization, it must now match too, on the host and in WSL, and the old-terminal banner uses the same comparison. The saved-account lookup the three checks repeated is now one helper.

* Treat v42 terminals as having the claude account function

v42 daemons shipped the same claude function and pointer path as v44, so claude in those terminals already follows the selected account. The old-terminal banner now shows only for daemons before v42 and for v43, the revert, which dropped the function.

* Fail the routing test when a new module resolves the claude binary

The census only caught code that read the Claude selection, not a new launcher that runs claude without asking the router. A second check now lists every module that resolves the claude binary or loads the Agent SDK, each with why it is routed, and fails on any new one, or on a listed one that no longer matches.

* Skip the account refresh on launches that run on System default

A launch that falls back to System default never uses the account folder, so starting a setup for it on every launch was wasted work. Account switches still set the folder up for a later sign-in.

* Give a terminal opened before the login shell's env only the account pointer

At startup, a pane could be routed before the login shell reported the user's own CLAUDE_CONFIG_DIR, so the fallback compared against ~/.claude.json instead. Until that env arrives a pane gets only the pointer, and the pointer is rewritten once it does, so claude typed there reads the right folder. The recent claude --version answer moves beside the version probe to keep the router under the line limit.

* Drop the chat-only Anthropic auth stripping and override refusal

Chats no longer drop the shell's Anthropic auth on any account, so the chat launch's override refusal and the policy's stripAuthEnv could only be reached from test fixtures. The structured auth policy is now just which login a failed sign-in names. Terminals keep their refusal, which still guards WSL launches; the failure reason stays in the shared vocabulary for older peers.

* Publish a new account state file whole

The first .claude.json write went straight to the target, so a crash mid-write could leave Claude a truncated state file. It is now written to a staged file and linked into place without replacing one Claude created meanwhile.

* Read an unselected WSL account's usage where the WSL router would run it

Usage for an unselected WSL account read its saved folder directly, so an account the guest's ~/.claude covers showed no usage, and the read bypassed the router. It now asks the WSL router, after the same check that the distro is already running.

* Keep the shell's Anthropic auth when the claude function runs an account

The claude shell function (POSIX, fish and PowerShell) unset ANTHROPIC_API_KEY, ANTHROPIC_AUTH_TOKEN, CLAUDE_CODE_OAUTH_TOKEN, AWS_BEARER_TOKEN_BEDROCK and auth-like ANTHROPIC_CUSTOM_HEADERS before running Claude in an account folder, so a proxy set in the shell lost its key while keeping its address. It now only sets CLAUDE_CONFIG_DIR and Orca's marker, as on System default. Protocol 44 is unreleased, so no new bump; the shell-wrapper snapshots are regenerated.

* Never drop the user's Anthropic auth from a Claude launch

Orca's launches stripped ANTHROPIC_API_KEY, ANTHROPIC_AUTH_TOKEN, CLAUDE_CODE_OAUTH_TOKEN, AWS_BEARER_TOKEN_BEDROCK and auth-like ANTHROPIC_CUSTOM_HEADERS for WSL terminals, terminal splits and through the preparation's stripAuthEnv flag, and refused a launch whose agent env set them. If the user's shell overrides the login, that is their setup: every launch now keeps it, on accounts, System default and WSL alike. The flag, the strip paths and the override refusal are gone; the refusal's reason stays in the shared vocabulary so older peers still decode it.

Orca's own process env and the claude --version probe still drop these variables: they are Orca's, not the user's launch.

* Leave out of an account's state everything Claude resets when it signs out

The .claude.json copy left out the login and keys matching cache patterns, but still carried account state such as additionalModelOptionsAnsweredAt, artifactRosterDenied, lastSeenOrgDefaultUpdatedAt and the subscription notices. The copy now leaves out the exact keys Claude resets on logout, beside the install ids, Console API key and first-token date; the cache patterns stay for account caches logout does not reset. Onboarding is still copied.

* Judge an old terminal's Claude banner by the pane's own runtime

The banner compared every old pane with the host selection and showed whenever only a WSL account was selected, so old host panes showed it with nothing to say and old WSL panes were judged by the host's account. The pane now names its runtime: a host pane is compared with the host selection, a WSL pane with its distro's selection through the WSL router, and a pane whose runtime is unknown, or with no account selected for it, stays hidden.

* Leave an account's settings file alone when only Orca's hooks differ from the default home's

* Reread a WSL account's Claude state only when the file changes

* Move tests merged from main onto the account-only Claude auth policy
2026-10-09 15:57:47 -04:00
OrcaWinandm4air 6dab0cb7fe refactor(ipc-persistence-shared): clean up Phase 3-3.8 code (#26902)
- Install store domain contexts with one generic symbol copy
- Share one import-source classifier and one copy-name deconflicter across local, SSH and runtime-upload imports
- Collapse kill.ts synthetic-exit and stop-failure repetition into helpers
- Use Promise.allSettled in SSH transport teardown
- Use the shared errorMessage() instead of inline copies
- Fold isLiveSshPtyLease into ssh-types and drop the duplicate predicate

Co-authored-by: m4air <m4air@Mac.localdomain>
2026-10-09 12:33:19 -07:00
Jinwoo Hong a8ce574c77 Add inert layout commands and runtime transitions (#26749)
* feat(layout): add the inert workspace layout model, Loader and Serializer

One model of a workspace's tab layout (all tab kinds, groups, split trees,
pane bindings, titles/colors/pin, sleeping records) in src/shared/workspace-layout,
with a Loader from today's stored session partition and a Serializer back to it.
The structural rules check and the pure pane/group tree helpers move into the
same folder so the module can run them; their callers import the new paths.
Nothing in the app calls the new module yet.

* feat(layout): add inert layout commands and runtime transitions

applyLayoutCommand applies every layout command a client can send (create,
split, close, move, reorder, rename, recolor, pin, open editor/browser/agent
tabs, start/restart, sleep/wake) to the layout model, and
applyLayoutTransition applies the facts the runtime sees (process started or
exited, SSH lease ended, orphan adopted, owner removed or renamed, agent launch
verdict, sleeping records). Pure tab-order, group-move and pane-split helpers
move into the shared module so the renderer, today's headless host and the new
commands share one implementation. Nothing in the app calls the new commands.

* fix(layout): avoid toSorted in shared code and drop an unused helper

The mobile bundle includes src/shared and Hermes lacks Array#toSorted.

* refactor(layout): name tree comparisons for what they ignore

* fix(layout): one stored home per layout fact; Loader reports every rule it applies

The row's terminal id, preview flag, worktree id and execution host are now
derived on save instead of kept a second time. Groups never stay empty. The
Loader reports row/tab-bar disagreements, re-minted ids, worktree id and host
disagreements, keeps a row stored in two workspaces where the tab bar names it,
and gives a pane id repeated in a second tab a new unbound pane. The rules check
runs on what the Serializer writes. Fixtures become test fixtures, and a test
lists every model field with the one fact it holds.

* fix(layout): commands keep one home per fact and refuse instead of throwing

createGroup is gone (a group only exists while it holds a tab); a lone tab
split beside another group moves its group leaf and keeps the id; moveTab
refuses when the move cannot be built; renaming onto an existing workspace
is refused; worktree and execution host ids come from the workspace.

* fix(layout): derive each tab's host from its owner and keep one preview fact per editor tab

An editor tab names its file's owner (an SSH target or paired runtime), every
other tab its partition. Where the stored tab and file preview flags disagree
the tab loads permanent, so a draft can never be replaced. A row terminal that
differs from the one derived from its focused pane is now reported.

* fix(layout): one binding kind, one title, one host rule; records follow a reassigned pane

A legacy row with no pane layout now loads as a one-pane tab bound to its
terminal (reusing the leaf its records name), so the null-panes branches and
legacyPtyId are gone. A pane given a new id because two tabs shared it takes
its sleeping and incarnation records, scrollback and focus with it. A terminal's
live title is one fact (the tab-bar label wins on load). Every tab-bar entry
names its execution host by one rule. The fact-location test is now keyed by
on-disk field and checked by mutating every source.

* fix(layout): commands drop the legacy binding branches; rename carries closed-tab records

Every terminal tab has panes now, so commands lose their null-panes branches
and a close always releases the tab's terminals. The pane-record rekey is
shared with the Loader. Renaming a workspace renames its closed-tab records'
worktree id, so a later removal or partition move still selects them.

* fix(layout): one change report by construction; records without copies

The Loader now picks values by fixed precedence only and reports one
field-by-field diff of the stored session against what the next save
writes, over the disk-field tables (now in workspace-layout-disk-fields.ts,
with session fields derived from SESSION_FIELD_OWNERS). Sleeping and
closed-tab records live in their workspace without the pane key, tab and
worktree id their key and workspace already give; the Serializer fills them.
Tab-bar entries are written in the one tab order.

* fix(layout): drop transitions with no producer yet; group moves hold no selection

orphanAdopted, legacyWorkerRecovered, clientHostedPageAnnounced,
agentLaunchVerdict, sleepingRecordsCaptured and moveWorkspaceToPartition
ship with the PRs that produce them. The shared group moves take and return
LayoutGroup only; the headless host keeps its selected tab beside them.
Commands keep sleeping and closed-tab records in their workspace, so rename
and removal need no record patching.

* fix(layout): name the change report's key-list parameter's type

* refactor(layout): key pane data by leaf, check the model itself, one legacy record

- Each pane's data (terminal, incarnation, title, sleeping agent) is one leaf record keyed by
  leaf id, and scrollback beside it is keyed the same way. Pane keys are derived only when
  saving. The Loader gives a repeated leaf id a new id before attaching records, so
  rekeyPaneRecords is gone; records naming no pane are carried as stored.
- The rules check runs on the model directly (ungrouped tab, empty group, group tree that
  disagrees with the groups, pane data without a pane); the disk-format rules stay separate.
- The row-ownership marker and topology revision move into one legacy record outside
  WorkspaceLayout, updated by updateLegacyPersistence after each apply.
- Fixtures include a terminal tab whose tab-bar id and terminal id differ.

* refactor(layout): commands on leaf-keyed pane data and the one legacy update

- Commands and transitions read and write each pane's one leaf record; drag-out moves only the
  tree, so the pane's data and scrollback follow by structure. No command advances the topology
  revision itself: applyLayoutCommand and applyLayoutTransition run updateLegacyPersistence after
  each apply, which also marks a workspace as owning its rows once it holds a terminal.
- Preview replacement closes the preview through closeTabs, so an unsaved draft refuses it as
  any close does.
- Tests: create then close then save with and without a reload, commands on a tab whose
  tab-bar and terminal ids differ, drag-out scrollback, the preview refusal.

* fix(layout): join the group ids the tree-mismatch detail names

* fix(layout): leave the stored revision map as is when no repo advances
2026-10-09 15:22:39 -04:00
Willian FelipeandNeil 150bf49cb2 feat(kiro): report Kiro agent status and list Kiro sessions in history (#26695)
* feat(kiro): report Kiro agent status through managed hooks

Kiro keeps hooks inside each agent config, so Orca installs one managed
hook per event into $KIRO_HOME/agents/*.json (locally and on SSH hosts
under the remote KIRO_HOME) and normalizes the payloads on /hook/kiro.
Pets, sidebar, notifications and mobile now see Kiro working and done.
Kiro resume is gated on a new agent-session.kiro-resume.v1 capability.

* feat(kiro): list Kiro sessions in Agent session history

Reads $KIRO_HOME/sessions/cli/<id>.json metadata plus the streamed
<id>.jsonl transcript locally, under WSL and over SSH, skips subagent
child sessions, and resumes with kiro-cli chat --tui --resume-id <id>.

* fix(kiro): report a remote agent deleted mid-install as unreadable

* fix(kiro): resume history rows by the validated session file name

* fix(kiro): preserve hook configs and restore sessions correctly

* test(kiro): use typed filesystem callbacks

* test(kiro): preserve older-peer search expectations

---------

Co-authored-by: Neil <neil@stably.ai>
2026-10-09 12:08:18 -07:00
Jinwoo Hong 9021133929 Add the inert workspace layout model, Loader and Serializer (#26748)
* feat(layout): add the inert workspace layout model, Loader and Serializer

One model of a workspace's tab layout (all tab kinds, groups, split trees,
pane bindings, titles/colors/pin, sleeping records) in src/shared/workspace-layout,
with a Loader from today's stored session partition and a Serializer back to it.
The structural rules check and the pure pane/group tree helpers move into the
same folder so the module can run them; their callers import the new paths.
Nothing in the app calls the new module yet.

* fix(layout): avoid toSorted in shared code and drop an unused helper

The mobile bundle includes src/shared and Hermes lacks Array#toSorted.

* fix(layout): one stored home per layout fact; Loader reports every rule it applies

The row's terminal id, preview flag, worktree id and execution host are now
derived on save instead of kept a second time. Groups never stay empty. The
Loader reports row/tab-bar disagreements, re-minted ids, worktree id and host
disagreements, keeps a row stored in two workspaces where the tab bar names it,
and gives a pane id repeated in a second tab a new unbound pane. The rules check
runs on what the Serializer writes. Fixtures become test fixtures, and a test
lists every model field with the one fact it holds.

* fix(layout): derive each tab's host from its owner and keep one preview fact per editor tab

An editor tab names its file's owner (an SSH target or paired runtime), every
other tab its partition. Where the stored tab and file preview flags disagree
the tab loads permanent, so a draft can never be replaced. A row terminal that
differs from the one derived from its focused pane is now reported.

* fix(layout): one binding kind, one title, one host rule; records follow a reassigned pane

A legacy row with no pane layout now loads as a one-pane tab bound to its
terminal (reusing the leaf its records name), so the null-panes branches and
legacyPtyId are gone. A pane given a new id because two tabs shared it takes
its sleeping and incarnation records, scrollback and focus with it. A terminal's
live title is one fact (the tab-bar label wins on load). Every tab-bar entry
names its execution host by one rule. The fact-location test is now keyed by
on-disk field and checked by mutating every source.

* fix(layout): one change report by construction; records without copies

The Loader now picks values by fixed precedence only and reports one
field-by-field diff of the stored session against what the next save
writes, over the disk-field tables (now in workspace-layout-disk-fields.ts,
with session fields derived from SESSION_FIELD_OWNERS). Sleeping and
closed-tab records live in their workspace without the pane key, tab and
worktree id their key and workspace already give; the Serializer fills them.
Tab-bar entries are written in the one tab order.

* fix(layout): name the change report's key-list parameter's type

* refactor(layout): key pane data by leaf, check the model itself, one legacy record

- Each pane's data (terminal, incarnation, title, sleeping agent) is one leaf record keyed by
  leaf id, and scrollback beside it is keyed the same way. Pane keys are derived only when
  saving. The Loader gives a repeated leaf id a new id before attaching records, so
  rekeyPaneRecords is gone; records naming no pane are carried as stored.
- The rules check runs on the model directly (ungrouped tab, empty group, group tree that
  disagrees with the groups, pane data without a pane); the disk-format rules stay separate.
- The row-ownership marker and topology revision move into one legacy record outside
  WorkspaceLayout, updated by updateLegacyPersistence after each apply.
- Fixtures include a terminal tab whose tab-bar id and terminal id differ.

* fix(layout): join the group ids the tree-mismatch detail names

* fix(layout): leave the stored revision map as is when no repo advances
2026-10-09 15:05:22 -04:00
Jinwoo Hong 6013d093e2 fix(mobile): one host sidebar on iPad, with the page owning the host area (#26761)
* feat(mobile): let the page own the wide host area, one sidebar only

On wide layouts a detail route's page session ran app/h/_layout.tsx inside
the detail pane, judged itself wide, and drew a second host sidebar beside
the native one.

- The page draws the host sidebar only when init carries the new optional
  `ownsHostArea` fact, never from its own width.
- The page manifest's `/h/[hostId]` route declares `canOwnHostArea`.
- On wide layouts the native host route mounts one host-area page session.
  A declaring bundle serves it with every served route's grants, so hops
  stay in the page, and the native layout drops its sidebar. Without the
  declaration, or without a bundle, the session answers native-route and
  every wide detail route stays native.
- The owner is recorded per host so the layout and switches agree and a
  relaunch does not flash.

Claude-Session: lane-ipad-page-owns-host

* fix(mobile): decide the wide host area per session, without a stored owner

Round 1 kept a per-host owner record that the native layout trusted. When
the record said the page owned the area, a detail route opened natively
(deep link, notification, handoff, narrow-to-wide with a detail pushed)
showed no sidebar at all.

- Drop the record. A wide session decides from the manifest it already
  read: it is served only when the host route declares canOwnHostArea,
  and it rebuilds when the layout class changes.
- The native layout hides its sidebar only on the host route, while that
  host's page session is serving, as the shell screen reports through a
  layout context. Detail routes keep the native sidebar, and a page there
  draws none, so there is exactly one.
- Phones read and render nothing new.
- The host-area page can read and write every workspace-scoped key of its
  host (chat tabs, live-input handles), as a per-workspace session does.

Claude-Session: lane-ipad-page-owns-host

* test(mobile): type the layout-class doubles rather than assert them

Claude-Session: lane-ipad-page-owns-host

* fix(mobile): keep detail pages through a layout flip, guard capped keys

- A wide/narrow flip rebuilds a session only on the host route or under a
  bundle without canOwnHostArea, where the answer changes. A detail page
  under a declaring bundle keeps its document, scroll and draft.
- In the host-area page, a workspace key the 32-entry init cap leaves out
  is named in storageOversize, so the page cannot replace the device's
  chat-tab list from a default. The init frame bounds that list to the
  page's schema; the host still refuses every key in the full list.
- A leaving host-area session clears only its own report.

Claude-Session: lane-ipad-page-owns-host

* refactor(mobile): host-area ownership from the reducer, storage read on demand

- The serving report is a focus-scoped boolean, so a host pushed over
  another no longer leaves the first one's area unclaimed after the pop.
- The reducer takes `layout-changed {wide}`: it keeps the session when the
  route view is unchanged and restarts it otherwise; `routeViewOf` returns
  `ownsHostArea`.
- One web override, `usePageOwnsHostArea`; the native rule is inline in the
  host layout.
- A host-area page reads a host workspace key from the shell on demand
  (`native.storage.read`, granted to that session only) instead of init
  enumerating the store.

Claude-Session: lane-ipad-page-owns-host

* fix(mobile): host-area page reads back its own workspace-key writes

A page read right after a page write of the same key is answered from the
page's own value, not from a shell that may not have the notify yet. A
refused read crosses as an error, never as an unset key.

Claude-Session: lane-ipad-page-owns-host

* refactor(mobile): read-your-writes from the shell's mirror, final review fixes

- The shell answers a workspace-key read from the mirror, which every
  shell-side write updates synchronously (removals held as known-absent),
  and reads the store only on a miss. The page shim's written-keys set
  goes.
- `onLayoutChanged` compares the route view field by field; the session
  is created with its layout and the layout effect fires on `wide` alone.
- The device verb dispatcher's type excludes the storage read.
- The sidebar matrix mounts the real shell screen to decide the report.

Claude-Session: lane-ipad-page-owns-host

* feat(mobile): host-area page takes the whole screen from the download on

The native sidebar steps aside once the manifest says the page owns the
host area: fetching, activating, ready and the failure screens after them
are full screen, as on a phone. It stays while the host is being checked.

Claude-Session: lane-ipad-page-owns-host

* fix(mobile): host-area workspace-key read survives a write during it

On a mirror miss the read went through the mirror's hydrate, which drops
its answer when any mirrored write lands meanwhile, so a stored key read
as unset right after a relaunch. A miss now reads the store directly, and
a store failure rejects rather than reading as unset.

Claude-Session: lane-ipad-page-owns-host

* fix(mobile): a wide detail session hands the host route to the shell

A session opened on its own under a page that can own the host area
kept a hop to the host route in-page (a missing worktree's notice, or
leaving the session), drawing a second host list beside the native
sidebar. Its pairs now leave the host route out, so the hop goes to the
shell, which opens the area-owning session. Phones keep the hop in-page.

Claude-Session: lane-ipad-page-owns-host

* test(mobile): a phone mount's layout dispatch leaves the session alone

Claude-Session: lane-ipad-page-owns-host

* fix(mobile): leaving a wide detail session returns to the host area

The leave-session chevron and the missing-worktree bounce hand the host
route to the shell on a wide layout, which pushed it: a second host-area
session over the session, with Back reopening the session. On a wide
layout the shell now dismisses to this host's route instead, taking the
href's params, or replaces the session when nothing is below it. Phones
still push.

Claude-Session: lane-ipad-page-owns-host

* fix(mobile): a narrow-wide flip restarts a detail session's page

The route view comparison now counts the paired routes, so a detail
session under a page that can own the host area restarts on a flip
instead of keeping the narrow pairs, whose host-route pair let the page
draw a second host list beside the native sidebar.

Claude-Session: lane-ipad-page-owns-host
2026-10-09 14:57:07 -04:00
OrcaWinandm4air fbcf0edc60 refactor(pty): one remote-runtime PTY id predicate (#26930)
Co-authored-by: m4air <m4air@m4airs-MacBook-Air.local>
2026-10-09 11:46:12 -07:00
OrcaWinandm4air 54b623b6a3 refactor(execution-host): one connection-to-host helper (#26929)
Co-authored-by: m4air <m4air@m4airs-MacBook-Air.local>
2026-10-09 11:45:37 -07:00
cbc6f54e1c refactor(runtime-core): clean up Phase 3-3.8 code (#26894)
* refactor(runtime-core): clean up Phase 3-3.8 code

- worktree.ps: summary lookup is a closure, not a 4-arg callback
- relay: shared RelayStreamAckWindow for fs and git response streams
- relay: terminal-frame slots live on RelayStreamRegistry
- one isBinaryBuffer/BINARY_PROBE_BYTES in shared/binary-buffer
- one automation owner fence on the controller, no casts
- drop unchecked Store cast in remote repo registration
- speech download reuses the shared abort-aware delay, no casts
- RelayRuntimeServices: drop leftover handler list and param casts
- use shared errorMessage() in audited files

Co-Authored-By: Claude <noreply@anthropic.com>

* test(cross-version-wire): pass worktree ps summary lookup in both builds' shapes

---------

Co-authored-by: m4air <m4air@Mac.localdomain>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: m4air <m4air@m4airs-MacBook-Air.local>
2026-10-09 11:40:51 -07:00
NeilandJustas Brazauskas 65580c5315 Honor OpenCode 2 launch mode when resuming sessions (#26843)
Let the configured launch command own the server mode when resuming OpenCode 2 sessions. Resume arguments select only the session, while Session History uses the existing default launch command so default standalone behavior is preserved.

Fixes #25003. Retains the contribution from #25004.

Co-authored-by: Justas Brazauskas <brazauskasjustas@gmail.com>
2026-10-09 11:16:28 -07:00
5f62f498f3 refactor(orcad-daemon-startup-cli): clean up Phase 3-3.8 code (#26892)
* refactor(orcad-daemon-startup-cli): clean up Phase 3-3.8 code

- One helper (getAllDaemonAdapters / listEveryDaemonGeneration) for enumerating
  daemon generations, replacing five hand-rolled copies.
- Shared serve pairing/endpoint readiness for Electron serve and orcad.
- One option list builds both --serve-* and orcad serve argv.
- holdDaemonAdoptionLease takes an options object; reuse childProcessHasExited.
- Drop orcad-entry's dead re-exports and repeated version fallback.
- Reuse the shared withTimeout in daemon retirement.
- Share the install-wait step in the serve update supervisor.
- Trim stale or misplaced comments.

Behavior unchanged; production code is net -132 lines.

* test(serve): keep real serveOptionArgs in launch mock; pin Electron serve argv

---------

Co-authored-by: m4air <m4air@Mac.localdomain>
Co-authored-by: m4air <m4air@m4airs-MacBook-Air.local>
2026-10-09 10:15:07 -07:00
Brennan Benson 7b91405547 Let in-page links switch sections inside a chat visual (#26729)
* Let in-page links switch sections inside a chat visual

* Make in-page links work in chat visuals on mobile and in WebKit

The first fix (an about:srcdoc base in the visual) only reached desktop
Chromium. On mobile, the page hosting the visual carried a policy with
base-uri 'none', and a srcdoc visual inherits its host's policy before
its own, so the base was refused and an in-page link still took the
visual off its page and showed it as failed. The mobile web shell sends
the same rule as a header. Separately, WebKit (iOS, and the web client
in Safari) fires the frame's load event for an in-page link inside a
sandboxed frame, and both hosts treated any second load as the visual
navigating away and retired it.

- The mobile host page and the mobile web shell header now allow only an
  about: base, so the shell's own base applies; the visual's own policy
  still refuses any base it declares.
- Instead of retiring a visual on its second load, the host now asks the
  visual's shell after every load. A later load must be answered by the
  same run of the shell within the plugin panel watchdog's 5 s deadline;
  no answer, or an answer from a new run (a reload), still retires it.
- Tests: unit tests for the policy, the answer reader and the frame's
  keep/retire rules, and a Chromium + WebKit test of both mobile surfaces
  that runs in the mobile web app CI job.

* Keep any answered later load, and say plainly what the check is for

The previous commit tagged each run of a visual's wrapper with a random
id so a reload would still retire it. The visual shares a window with
its wrapper, so it could read or set that id itself, and a reload only
reruns the same Orca-written document, which is not an escape. Drop it:
after any load past the first, the host asks once, and any answer on
this visual's channel within the wait keeps it; silence retires it.

- Comments now say the check is a tripwire for a frame gone blank or to
  another page; the frame guard (desktop), the host page's policy plus
  the app's load filter (mobile) and the sandbox are the boundaries.
- Visuals get their own 5 s wait instead of borrowing the plugin panel
  watchdog's, because a 1 s wait retired a busy visual in WebKit.
- Tests: a reload is now kept; going blank, or dropping the base and
  following a link, still retires. The CI routing for the shared wrapper
  is now pinned in the path-classification test.

* Load the visual links check's mobile modules only where they can build

The unit shards run every config/scripts test with only the root
dependencies installed. This check imported a mobile/ module at load,
and transforming it reads mobile/tsconfig.json, which extends a package
those shards do not have. The transform error could not be sent back to
the test worker under Bun, so the whole shard stopped with
"DataCloneError: The object can not be cloned".

Import the visual shell and the mobile host page inside the dependency
gate, as the session-dictation check already does, so the shards skip
the file without loading them. The mobile web app job, which installs
mobile dependencies, still runs it in full.
2026-10-09 10:07:59 -07:00
Neil 9d1cd1e8b1 [CI] Consolidate related tests and advance transcript waits with the existing virtual clock (#26874)
* [CI] Consolidate related tests and advance transcript waits

* [CI] Refresh reliability gate references after test consolidation
2026-10-09 06:12:22 -07:00
6aef5c569c Phase 3.7: fix SSH/remote failures found by adversarial real-host testing (#26847)
* fix(remote-terminal): keep a reconnected pane's size after an SSH outage (P1-4) (#26625)

* fix(remote-terminal): keep a reconnected pane's grid when its stale stream is reaped (P1-4)

The host's width floor only handed ownership to viewers that had claimed
since subscribing. A reconnecting desktop pane re-subscribes without
claiming, so when the heartbeat later reaped the dead socket that owned the
floor, the host reclaimed the PTY to its spawn size (120x40) and held the
pane there. The owner's same-client replacement stream now inherits the
floor, and a pane whose stream was reaped before it reconnected resumes
ownership on re-register unless the host or another viewer claimed since.

* fix(remote-terminal): host typing during an outage revokes the reaped owner's resume

Host input reaches the PTY without a renderer viewport claim once the host has
reclaimed its grid, so the orphaned-owner entitlement survived and a passive
reconnect resized the host pane back to the remote grid. The host write path
now clears that entitlement on driving input (query replies, focus reports and
launch writes excluded).

* test(pty): stub the runtime's host-input note in write-path handler tests

---------

Co-authored-by: m4air <m4air@Mac.localdomain>

* fix(remote): an oversized RPC reply fails that request instead of closing the host connection (#26624)

Expanding a folder with ~80k entries on a managed SSH host produced a
files.readDir reply over the 4 MiB encrypted-reply cap. The channel treated
that as a transport fault and closed the whole control socket (1013), and the
explorer rendered the failed folder as empty.

- The WebSocket RPC transport now answers an over-cap reply with a
  reply_too_large error for that request id, so the channel never sees it.
- files.readDir checks remote listings against the request's content budget
  and names the entry count.
- The explorer shows a subfolder's read error on its expanded row.

Finding: HH-2 (Phase 3.7 hostile-host).

Co-authored-by: m4air <m4air@Mac.localdomain>

* fix(ssh): let a slow host finish installing the pinned Node runtime (HH-3) (#26631)

* fix(ssh): let a slow host finish installing the pinned Node runtime

The POSIX promote (extract, hash, self-test and publish the ~110 MiB node
binary) ran under the 30 s default exec timeout. On slow home storage it
timed out, the remote work kept running, and the runtime store lock it held
stayed until the 20-minute stale rule, so relay rung A queued behind it past
its own deploy bound.

Give both platforms the 300 s promote bound and have the host release the
store lock itself when the promote exits, with owner-checked releases so a
late release never removes a lock someone else took. (HH-3)

* fix(ssh): let the host release the store lock after a slow under-lock re-hash

The under-lock re-check of a sibling-published runtime hashed the ~110 MiB
executable as its own command under the 30 s exec default and outside the
host-side lock release. On slow storage it timed out, the client correctly
kept the lock, and nothing on the host freed it until the 20-minute stale
rule, so later installers ran out their deploy bound.

Run the re-hash and the conditional promote as one host command wrapped in
the host release, and give every full-runtime hash, including the initial
probe, the slow-storage bound. (HH-3)

* test(ssh): count the combined under-lock re-probe and promote as one host command

---------

Co-authored-by: m4air <m4air@Mac.localdomain>

* fix(ssh): shell startup output on stdout no longer breaks SSH connect (HH-1) (#26617)

* fix(ssh): fence captured exec stdout so shell rc output cannot break connect

sshd runs the user's login shell before Orca's /bin/sh wrapper, so any
startup-file stdout (fish/tcsh/zsh/bash echo, or a bare terminal-title escape)
was glued onto every captured answer. The home probe read "banner\n/home/u"
and connect failed with "Remote home is not a valid path".

execCommand now wraps wrapped POSIX commands in a per-call nonce fence and
returns only the payload between the markers; the SSH folder browser gets the
same fence. The fence is the existing WSL captured-login-shell fence, moved to
src/shared/posix-stdout-fence.ts so both transports share it.

Finding: HH-1 (phase 3.7 hostile-host). Refs #8324.

* fix(ssh): fence the login-shell Node probe and fall back from a noisy SFTP handshake

The login-shell 'command -v node' probe ran unwrapped, so output from sshd's
shell and the nested login profile became the Node path. It now prints a
one-line fence every login shell parses, inside that shell.

An external sftp-server is started through the user's shell, so startup output
corrupts the SFTP handshake. ssh2 reports that parse failure only before the
session is ready, so SshConnection.sftp tags it, and relay install transfers
treat it as SFTP unavailable and stream over exec stdin.

Finding: HH-1 follow-up (phase 3.7 hostile-host). Refs #8324.

* fix(ssh): keep the stdout fence marker out of the command text

The runtime-store inventory lists processes with ps, which prints its own
script, so a literal marker in the script was read as the fence and the
store GC skipped every pass. printf now joins the marker from two halves.

---------

Co-authored-by: m4air <m4air@Mac.localdomain>

* fix(orcad): finish an abandoned update on connect, and refresh renderers after a main-side re-pair (P1-A, P1-C) (#26635)

* fix(orcad): finish an update or rollback its client abandoned, and tell renderers about main-side re-pairs

P1-A: a wake that finds the activation fence now runs crash recovery, which takes
only a stale, previous-boot or exited-own fence. Recovery of an activation
interrupted after the candidate launched resumes it: the candidate commits if it
passes the readiness gate, else the activation is undone as before.

P1-C: invalidating a still-linked environment's transport restarts its status
owner, and a renderer that sees status under a newer pairing revision re-reads
the catalog before applying it.

* fix(orcad): a resumed update keeps the app version that started it

Recovery that commits an abandoned candidate stamped no activeAppVersion, so an
older desktop read the host as pre-tracking and downgraded it over state the
newer build had migrated. The activate journal now records the initiating app
version; recovery stamps it, and a journal too old to carry it is undone instead
of committed.

* test(runtime-env): answer nothing for the status restart a re-pair now triggers

* fix(orcad): recovery leaves running an update server a too-old journal cannot commit

A journal without candidateAppVersion was sent to undo, which stopped a
healthy candidate that had already migrated state and then refused to
restart the incumbent, leaving the host serving nothing. Refuse instead,
keeping the fence and the candidate, until the operator accepts Restore.

---------

Co-authored-by: m4air <m4air@Mac.localdomain>

* fix(terminal): a remote recovery never splices stale history above the new screen (P2-5, P2-7) (#26647)

* fix(terminal): a remote recovery never splices stale history above the new screen

A pushed remote snapshot (overflow recovery, seq-gap resync, first attach) is
sent because the client's stream broke, so the pane's own history ends before
that image. Since #26319 a screen-only image kept that history, splicing a
stale block directly above the final screen with a silent gap of up to ~270k
lines (P2-5), and a first attach to an older host left silent holes (P2-7).

- Host: the ACK-overflow recovery serializes up to 1000 history rows.
- Client: the seq-gap resync asks for the same 1000 rows.
- Client: a pushed snapshot always replaces the pane's scrollback; the
  keep-local-scrollback path is removed. A screen-only image from an older host
  now shows only its screen, which is lossy but never misleading.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix(terminal): a pushed image replaces the normal buffer over a live alt screen too

Over a pane already on the alternate screen, an image that also ended on alt
repainted only its alt frame and kept the pane's normal buffer, inferring the two
had frozen together. Inside the gap the TUI can exit, the shell print and a second
TUI start, so once that TUI exited the pane showed its stale history where the host
had the missed output. A pushed image now always rebuilds the normal buffer and
history from the host before entering alt itself.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix(terminal): bound recovery history by link slack and keep pre-TUI scrollback under screen-only images

A recovery that carries history now goes out only when the stream's ACK
window has drained; under a sustained flood recoveries stay screen-only and
the owed history follows once the client catches up, so snapshot bytes no
longer outgrow the client's intake.

A pushed image without history (scrollbackRows absent or 0) over a live alt
screen repaints only the alt frame again, so a reconnect to an older host
no longer wipes the pane's pre-TUI scrollback.

---------

Co-authored-by: m4air <m4air@Mac.localdomain>
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix(source-control): Stage All stages every change when the listing hit the 1,000 cap (HH-4) (#26618)

* fix(source-control): Stage All stages every change when the listing hit the 1,000 cap

The status listing stops at 1,000 rows, but Stage All built its path list
from those rows, so it staged 1,000 of 20,000 changes and the next commit
was silently partial. When the listing is capped, Stage All (and the
Changes header Stage all) now asks the host to stage the whole worktree
via an optional scope on git.bulkStage; unresolved conflicts stay
unstaged. Older hosts ignore the field and stage the listed paths.

Fixes HH-4.

* fix(source-control): whole-worktree Stage All needs a host receipt, skips shared links, and reaches mobile

- Scoped stage sends no paths and requires a stagedScope receipt, so an older
  runtime or relay stages nothing and the client reports it instead of a
  silent partial stage.
- Scoped 'all' skips Orca's shared symlinks (configured and really a link),
  matching the status listing.
- The capped-listing banner offers Stage All, reachable when the visible
  prefix is entirely staged.
- Mobile reads didHitLimit and uses the same scoped stage and receipt.

* test(source-control): type the Stage All test stubs so lint and the mobile test typecheck pass

* fix(source-control): match Shared Paths saved as ./name or name/ against git's spelling

The Shared Paths setting keeps entries as typed, but git prints worktree paths
without a leading ./ or trailing /. Stage All's conflict path compared the raw
spelling to git's untracked list and staged the shared node_modules link, and
an exclude pathspec ending in / made git refuse the add after tracked changes
were already staged. Status listing had the same mismatch. getSafeRelativePath
now returns git's spelling, so every caller gets it, including saved settings.

---------

Co-authored-by: m4air <m4air@Mac.localdomain>

* fix(ssh): redeploy a managed server another desktop stopped; forget one whose host is gone (P1-D, P1-E) (#26628)

* fix(ssh): redeploy a server another desktop stopped; let a dead managed host be forgotten

P1-D: a connect whose linked server the host reports as not activated (another desktop
stopped it) now goes on to the update check, which redeploys it in place.
P1-E: add Forget (managedServer.forget, orca environment forget, Settings row) that runs
Stop's local unlink without contacting the host; the verdict stays unverifiable.

* test(ssh): type the forget test's hoisted store without an assertion

* fix(ssh): never downgrade a stopped host; redeploy it mid-session and retry after a failure

A stop now keeps the stopped build's Orca version, so an older desktop's
reconnect skips as host-newer instead of starting an older server on a
store a newer build migrated. A stop recorded without that version is not
guessed at. A stopped host retries the redeploy on each connect instead of
the once-per-version failure suppression, and a runtime call that finds
the server stopped mid-session redeploys it before the call.

* fix(ssh): rebuild the managed forward after redeploying a stopped host

A redeploy can bind a different port than the reused forward targets.

* fix(ssh): keep stop journals readable by older desktops and recheck version policy under the fence

- The stop journal's recordAfter is the core deactivated record older peers compute; the
  committed record keeps activeAppVersion (which released planners already honour) and
  rolledBackFrom, rebuilt from recordBefore on recovery.
- Auto-update re-applies its version policy to the record read under the host fence, and a
  census counted against another incumbent is treated as unverifiable.

* fix(ssh): file a dispatched stop where older desktops cannot commit it, so their recovery never drops the version guard

* fix(ssh): refuse a fresh deploy of an older orcad onto a host a newer desktop stopped

The unlinked deploy path never consulted the stopped record's activeAppVersion, so a desktop
without a link to the host could activate its older build over state a newer build migrated.
Both the auto-update planner and the fresh deploy now apply one downgrade rule under the fence.

---------

Co-authored-by: m4air <m4air@Mac.localdomain>

* fix(ssh): redeploy a managed server a pre-version Stop left; let a forced deploy run over a newer one (#26766)

A stopped activation record without activeAppVersion is what every Stop on
main writes, so refusing it stranded those hosts behind an unforceable
policy refusal. Only a provably newer host is now refused; a forced fresh
deploy runs this version anyway, and the refusal names both versions.

Co-authored-by: m4air <m4air@m4airs-MacBook-Air.local>

* fix(remote-terminal): no keystroke lost, duplicated or split across a silent outage (P1-1, P1-2, P1-3) (#26630)

* fix(remote-terminal): no keystroke lost, duplicated or split across a silent outage

Remote pane input was fire-and-forget: keys handed to a socket that had
silently died were gone, keys still in the 8 ms debounce at detection were
cleared, and held keys were discarded when the auto-recovery window ran out.

- New negotiated `inputAck` capability: the client names an input session
  and sequences Input frames (existing frame header `seq`); the host records
  the last applied sequence per (PTY, session) across connections, writes
  each sequence once and acks it with new opcode InputAck (18), sent only
  to clients that advertised the capability.
- The pane keeps sent-but-unacked input and replays it to the same
  terminal on reattach; the host dedupes. Old hosts keep current behavior.
- Debounced and claim-queued input at detection is held, not cleared.
- Held input survives the 'disconnected' phase; the hold coalesces keys so
  minutes of typing do not hit the per-entry cap.
- Banner copy no longer claims a one-minute retry window.

Fixes P1-1, P1-2, P1-3 (phase 3.7 network findings).

* fix(remote-terminal): never replay into a restarted runtime; keep Ctrl+C and validating paste in order

- Host publishes inputLedgerId with inputAck; the client drops unacknowledged input sent to a
  different ledger (runtime restart with the same PTY) instead of running it twice.
- A replayed duplicate is acked only after the original write settles.
- Accepted input (Ctrl+C, Escape, paste) rides the sequenced stream and resolves on the host ack.
- Keys typed while held queue behind a paste still in size validation.

* fix(remote-terminal): parse subscribed capabilities into a typed shape instead of Reflect.get

* fix(remote-terminal): ack only applied input; withdraw writes their caller gave up on

- Host ledger records each write's outcome: a refused write is rolled back and
  never acked, so the client's replay writes it; nothing queued behind it runs or
  is acked. A write that threw is acked as delivery-unknown, so the client
  reports it failed and never replays it.
- Paste and file-drop timeouts abort their accepted write: a copy still held for
  an outage is withdrawn, and a journaled copy is left out of the replay.
- A batch of sequenced input keeps one endpoint and ledger, so a send that
  retires the stream mid-batch no longer discards earlier unacked input.

* fix(remote-terminal): host writes sequenced input strictly in order and asks the live stream to resend after a refusal

- The ledger admits only the next sequence; a refusal or gap answers with an InputAck resend request instead of WriteUnavailable, so the pane keeps its journal and resends on the same stream (no remount, no carrier bookkeeping).
- Sequenced input uses the provider's settled write; an unverifiable handoff is delivery-unknown, not refused.
- Replays fill given-up slots with empty input so the host can continue; the client remounts only after every resend backoff is refused.
- Typing batched ahead of a paste goes out as its own sequences, so cancelling the paste keeps it.

* refactor(remote-terminal): deliver sequenced input with standard go-back-N

Replace the hand-built "block on refusal" bookkeeping with go-back-N:

- Host: per (PTY, input session) the ledger keeps only appliedSeq, the
  highest contiguous confirmed write. Frames run one at a time in arrival
  order: a duplicate is re-acked, the next sequence is written and acked
  on confirmed success, and a refusal or gap is answered with a resend
  request carrying appliedSeq. No blocked state; every stream is equal.
  A settlement that proves no byte left is a refusal; one that may have
  written advances as delivery-unknown and is never retried.
- Client: one journal keeps unacked input in order and resends from
  appliedSeq + 1 on a resend request (with backoff), on a new stream, or
  when acks stall. Refusals never remount the pane. Withdrawn pastes,
  stale query replies and over-budget bytes resend as empty slots.
- The recovery hold pauses (keeps the rest) when a release is cut short
  by another outage, and keeps delivering typing after an accepted write
  a sequenced stream withdrew or could not confirm.
- Add a seeded model-based test that drives the real pane transport,
  multiplexer and journal against the real host dispatcher and ledger
  through 2,000 random fault schedules.

* test(remote-terminal): cover the refusal remount on a stream without sequenced input

A sequenced stream now resends refused input on the same stream instead of remounting, so the end-to-end remount check negotiates no inputAck, as an older peer does.

* fix(remote-terminal): escalate persistent input refusals, drop input once recovery gives up, and never report a lost unknown ack as applied

- The pane falls back to its remount path when the host keeps refusing input past the resend backoff ladder.
- Held and journaled input is discarded when auto-recovery gives up, and input typed under the disconnected banner is not held.
- The host ledger answers a duplicate for its own sequence and remembers delivery-unknown outcomes; the client settles a write as applied only from its own ack.
- The input model now requires a write reported applied to have reached the PTY whole, and covers a refusal that never clears.

Co-Authored-By: Claude <noreply@anthropic.com>

---------

Co-authored-by: m4air <m4air@Mac.localdomain>
Co-authored-by: m4air <m4air@m4airs-MacBook-Air.local>
Co-authored-by: Claude <noreply@anthropic.com>

* fix(remote-terminal): keep input typed past the auto-recovery window for the same terminal, within a bounded grace (P1-3) (#26832)

The last round of #26630 discarded held and journaled input as soon as auto-recovery gave up,
and refused input under the disconnected banner. On a real host a 200 s outage lost 102 of
120 keystrokes, although the pane reattached the same terminal on its own seconds later.

Input now stays held for the same handle and incarnation while the pane is disconnected, for
up to 5 minutes after it gave up. Past that grace it is discarded, and typing into the dead
pane is refused, so stale keys never run at an arbitrary later reconnect. The grace is a
timestamp checked when input moves, so a latched pane stays free of timers.

Co-authored-by: m4air <m4air@Mac.localdomain>
Co-authored-by: Claude <noreply@anthropic.com>

---------

Co-authored-by: m4air <m4air@Mac.localdomain>
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-authored-by: m4air <m4air@m4airs-MacBook-Air.local>
2026-10-09 04:58:48 -07:00
Neil 7b1242ba94 [CI] Reduce repeated test work and use pinned Bun for localization extraction (#26806)
* [CI] Reduce repeated test setup and retire source inventories

* [CI] Give consolidated floating panel suites distinct titles
2026-10-09 02:37:53 -07:00
Brennan Benson f16212a29c feat(acp): compact Grok, OpenCode and OMP chats (#26502)
* feat(acp): compact Grok, OpenCode and OMP chats

* test(acp): expect Grok compaction capability

* fix(chat): keep skipped compaction outcomes visible across history pages

* fix(acp): a signed-out /compact reports not signed in, as a send does

A /compact the agent refused for sign-in now records the same notSignedIn
fact as an ordinary send, so the chat shows sign-in guidance and the next
send starts a new agent that reads the new login.

* refactor(acp): keep the timeline translator under its line limit after main's growth

Main's OMP per-channel message identity grew acp-timeline-translator.ts past
max-lines with this PR's compaction handling. The running compaction's frame
folding moves beside its ending in acp-compaction-turn.ts, and the
compacting-prompt lookup into AcpPromptTurns; behaviour is unchanged.
2026-10-09 02:05:31 -07:00
Neil 1578716eb0 revert(runtime): temporarily restore the previous host Node minimum (#26823)
This reverts commit 46460d49c7 (#26171).

Pause the Node 24 requirement so existing SSH and WSL users are not
surprised by a host-runtime upgrade requirement in the next release.
This is a precautionary rollout hold requested to avoid near-term user
disruption, not a cancellation of the Node 24 transition.

Restore the prior Node 18 host minimum and companion build targets.
The previously shipped bundled Node 24 runtime and compatibility launcher
remain in place. Preserve the original change in a separate draft PR for
reintroduction after compatibility and rollout validation.
2026-10-09 02:00:57 -07:00
Brennan Benson 39a01f9c0b feat(agent-launch): create a folder workspace and start its agent in one host operation (#26083)
* feat(agent-launch): accept a create-folder-workspace target

agent.launch can now create a folder workspace and start its agent there,
the same operation it already runs for a new git worktree. The host
creates the folder workspace (metadata only, no startup terminal), resolves
it the way an existing target is resolved, then settles the mode and builds
the surface as for any existing folder workspace.

Hosts advertise agent.launch.create-folder-workspace.v1 so a client offers
the target only to a host that accepts it.

* fix(agent-launch): authorize, classify and navigate folder creates like their create method

A create-folder-workspace launch could do three things its two-call
equivalent never did:

- A phone could create folder workspaces. The mobile allowlist admits
  agent.launch but not folderWorkspace.create, and is checked by method
  name only. A create target now needs the authorization of the create
  method it stands for, read from the same allowlist, and a phone is
  refused with agent_launch_target_forbidden before admission, so no
  ledger row and no create. create-worktree maps to worktree.create,
  which phones already may call, so nothing changes for it.
- A create refused before anything was stored (group gone, folder
  missing, SSH host unreachable) replayed as
  agent_session_operation_unknown, and a retry under the same operation
  id was refused until the row expired. runtime.createFolderWorkspace now
  raises those refusals as FolderWorkspaceCreateRefusedError, with the
  same message text, and the launch records it as failed for any target.
  A failure after the create still replays unknown.
- A paired caller's folder create activated the new chat on the host
  window and did not select the caller's own tab. Only create-worktree,
  whose host activation runs setup, keeps the host navigation now; a
  folder create navigates like an existing workspace.

Tests cover the phone refusal, replay of a success, a refused create
settling failed and replaying that failure, an after-create failure
staying unknown, an SSH connection reaching the agent's view mode, paired
navigation, and an older host refusing the target at parse.

* fix(agent-launch): replay a refused folder create with its whole path

A folder create refused before anything was stored is recorded as the
launch's failure, and a retry under the same operation id answers from
that record. The record went through the 128-character cap every launch
failure code gets, and folder refusals carry the folder's absolute path,
so a retry could show the user a path cut short: a different folder.

The refusal is now recorded whole when it fits a path as long as Linux's
PATH_MAX after its code. Past that, which no folder a stat accepts
reaches, only the code before the path is kept, so the replay degrades to
the bare code instead of a partial path.

Also: the controller test asserts the exact refusal message, the
create-folder-workspace capability comment says a phone needs its own
gate, and the forbidden-target code says why it is not `forbidden`.

* test(folder-workspace): type the controller test's store stub without an assertion

The changed-code quality gate refuses the `as never` cast on the stub
store. It is now a RuntimeStore whose unreached members throw.

* test(agent-launch): run the folder-create launch tests in the Node runtime project

The operationId replay tests open the real SQLite record store, so the
file belongs with the other ledger-backed agent.launch tests.

* test(agent-launch): pin that a paired folder create's terminal stays off the host window

The folder-create test now asserts surfaceOwner: false on the terminal a
paired caller's launch creates, rather than matching any options. The
caller-selection header now says only an in-process worktree create keeps
worktree.create's navigation.

* test(agent-launch): drop the per-connection view-mode expectation main removed
2026-10-09 01:23:06 -07:00
Brennan Benson 6ba86414bf Show model and effort choices for every agent before its session starts (#26407)
* feat(native-chat): one saved model catalog for every structured agent

Every agent registration now declares how its models are listed without a
session (a probe, or explicitly none). Probes share one supervised runner
(15 s budget, bounded output, teardown on every path). Grok lists from
initialize (or x.ai/models/list), OpenCode from models --verbose, Pi from
--list-models; Claude moves onto the shared runner. ACP option reads no
longer copy the running model's effort menu or selection onto every model.
Desktop and phone pickers read the same catalog for every agent, with a
non-pickable provider-default model pill before any list exists.

* fix(native-chat): host names a listing's configured model; live listings saved for every agent

- The catalog answer carries an optional listingNamesConfiguredModel field, set
  per registration (Codex, Grok) and per workspace; pickers name the listed
  default from it instead of a Codex-only client flag (older hosts fall back).
- Grok, OpenCode and Pi live option reads save their model list through one
  host step, keeping the account's known default and per-model efforts.
- Phone reads the host catalog like the desktop and seeds every agent.
- Saved picks accept any known agent id (OpenCode, Pi).
- ACP agents with no session-free listing register as unavailable; Grok's live
  effort menu falls back to the session's when the advertised one is empty.

* fix(native-chat): discovery and live listings kept apart; every agent's live listing saved by the host; phone-created chats name their model

* fix(native-chat): phone new-chat mark clears on the chat's first live answer; Claude's startup listing reaches the host catalog

* fix(native-chat): live-listing type lives beside the catalog entry so wire types don't pull the store's persistence into the phone

* fix(native-chat): the model picker never waits on a listing; a first listing lands in place

* feat(native-chat): the host lists every agent's catalog ahead of use and traces each listing

* feat(native-chat): a new Claude chat names the model and effort its config resolves to

* test(native-chat): catalog service fakes gain prewarm

* test(native-chat): prewarm span recorder without a type assertion

* fix(native-chat): project agent config overrides the account default only when it sets a model or effort

A committed .claude/settings.json for permissions or hooks, or the settings.local.json Claude writes on always-allow, no longer hides the configured Claude model in that project. Unreadable config still counts; Codex config.toml follows the same rule.

* test(native-chat): the Codex picker read and the launch it starts resolve one account key, through the real resolvers

Runs resolveHostCodexHomePathForLaunchReadOnly and prepareCodexRuntimeHomeForLaunch as the runtime service wires them, for the system account on the real home, on the shared runtime home, and a managed account. Also scopes the prewarm suite to agents in use.

* fix(native-chat): Claude's configured model and effort are saved together behind the project check; prewarm lists only agents in use and stops with its host

A live Claude chat no longer stamps its effort onto the account catalog; the effort travels with the configured default, which a no-pick chat whose config resolves to no listed model now forgets. Prewarm lists agents with a saved catalog or a chat, and host teardown (quit included) starts no queued listing and stops running ones through an abort signal the listing runners honor.

* test(native-chat): new catalog tests use typed hoisted state and type-only namespace imports

* test(native-chat): a prewarm probe sets and clears the sign-in verdict like a read's, and re-checks an aged one beside a fresh catalog

* fix(native-chat): an aged sign-in verdict re-probes only from probe paths, not a Codex chat's picker

The main merge put the held-verdict check into store.shouldRefresh, which also gates
the live Codex picker. A chat's own listing never clears the verdict, so a signed-out
Codex whose chat works re-listed model/list on every picker read. shouldRefresh is
back to main's rule; the catalog read's probe and prewarm ask probeDue instead.

* fix(native-chat): Claude project settings with an effort cap, a main-thread agent or an Anthropic cloud provider count as an override

maxEffortLevel clamps every effort including the model default, and agent applies that
agent's model (both from the bundled SDK settings type). CLAUDE_CODE_USE_ANTHROPIC_AWS
and CLAUDE_CODE_USE_ANTHROPIC_GOOGLE_CLOUD sit in the SDK's provider-switch list beside
Bedrock and Vertex. A project that sets only these no longer shows, or saves as the
account default, the account's model or effort.

* refactor(native-chat): the catalog store's picker waiters live in their own module

No behavior change: pendingListing and the waiter notifications move as they were,
which keeps the store under its line limit for the fixes that follow.

* fix(native-chat): changing an agent's command or environment re-lists its catalogs and re-checks its notice

The settings change called prewarm, but prewarm skipped any catalog still fresh (the
catalog key is agent + account, not the command), and a held "not installed" verdict
kept its 30-second window. The change now marks that agent's catalogs due and ages its
held verdicts, so the prewarm it triggers re-lists them and re-checks the notice. Only
agents whose command or env actually changed are touched.

* test(native-chat): the sign-in notice tests check the model picker is really usable

They checked only that a model entry existed, which a disabled or empty pill also
passes. They now check it is settable, carries no disabled reason, and offers the
catalog's or the seed's model.

* fix(native-chat): a probe the host stops keeps the held notice reason, and a stopped prewarm reads nothing

A probe aborted because the host is going away says nothing about the account, so it
no longer replaces a held sign-in or not-installed reason with a plain failure. A
prewarm after stop returns before reading chat records.

* test(codex): the catalog account-key test uses the runtime's real Codex launch-home resolvers

The resolvers move to codex-launch-preparation as one export the runtime service
spreads, so the test runs exactly what the runtime is handed instead of a copy.

* feat(native-chat): keep each host's saved model lists in the renderer

A chat picker could only learn the host's saved model list by reading it
after it mounted, so its first frame was a guess. The renderer now keeps
the host's answers per host and agent: this machine's lists are read once
its runtime answers, for the agents its chats were used with; a paired
host's again when this client regains contact; and an account or launch
setting change drops this machine's lists. Every entry is a host answer.

* fix(native-chat): render the model pills once, from the host's list

A new chat's composer painted its first frame from the built-in model list:
a saved pick that list lacks (Claude's `opus[1m]`) showed as a raw id with
no effort pill, then jumped to "Opus (1M context) · High" when the host's
list landed. Every new chat painted a guess and then corrected it.

Now the first frame reads the host's saved list the renderer already holds,
so it is the final frame. Without one, the quiet "Model" placeholder stands
until the host answers; a host with nothing saved makes the built-in list
usable, naming nothing, until its listing lands. A saved pick is named only
from a host or session list that names it. The phone follows the same rule
and keeps the host's list across a new fence.

* feat(native-chat): answer a saved-only model catalog read

The app could preload a host's saved model lists only for agents with a
saved pick, because the catalog read starts a listing (spawns the agent's
CLI) when nothing is saved. Grok, OpenCode and OMP chats with no saved pick
were never preloaded, so their pickers showed "Model" before the host's list.

`agentSession.modelCatalog` now takes `savedOnly`: the host answers from
what it saved (the entry, configured default and held reason) and starts,
joins or re-checks no listing. Its params are strict, so the host
advertises `agent-session.model-catalog.saved-only.v1` and clients send it
only to hosts that do.

An answer also says when its named default holds in every workspace (Grok
reads no project config for its model), so a new chat in any workspace can
use it on its first frame.

OMP stays without a session-free listing: `omp models --json` lists every
model without the `enabledModels` filter its chats apply.

* fix(native-chat): preload every registered agent's saved model list

Grok, OpenCode and OMP chats painted the "Model" placeholder, then the
host's pills, because the app preloaded only agents with a saved pick.

Now the app reads every agent a host registered through the saved-only
read: this machine once its runtime and agents are known, a paired host at
connect and on each reconnect, and this machine again after an account or
agent-setting change. A host that does not advertise the saved-only read
keeps the earlier rule (saved picks only; a paired host's answered agents).
An agent the host saved nothing for stays unknown. An answer sent before
the host's lists were dropped is not recorded.

* test(native-chat): drop a type assertion from the snapshot sync test

* fix(native-chat): learn an ACP agent's default from a chat started with no pick

Grok's session-free listing named the model `initialize` computed (grok-4.7), but chats ran
another (grok-4.6), so a new chat painted 4.7 and then jumped. The listing no longer names a
default; a new ACP session with no model pick reports what it runs, and the host saves that as
the account's configured default behind the workspace check. A catalog read waits for a
recording still in flight, so the chat's own follow-up read names it.

* feat(native-chat): check OpenCode and OMP project config before naming their default

OpenCode reads opencode.json(c) and .opencode/ in every ancestor of the workspace; OMP merges its
.omp settings and other agents' project settings. A workspace whose config sets a model, effort
or provider key (or can't be parsed) names no default; any other lets a no-pick chat's model
become the account's default, so the next new chat names it on its first frame.

* fix(native-chat): re-read the host's list once a no-pick chat reports its model

The renderer kept the answer it read before the chat started, so the next new chat's first frame
still named no model. A new chat that named no model reads the host's list again once it reports
what it runs; a default no workspace can replace also serves chats in other worktrees.
2026-10-09 01:20:52 -07:00
Brennan Benson c586cce8cc fix(acp): start chats for agents that report no model (#26587)
* fix(acp): start chats for agents that report no model

* fix(acp): omit unknown models from resting session reads

* test(acp): expect absent models for resting Claude chats

* refactor(native-chat): move known agent discovery out of session host
2026-10-09 00:42:23 -07:00