Skip to content

Phase 3.7: sync main into phase37 - #26828

Merged
OrcaWin merged 91 commits into
OrcaWin/phase37from
OrcaWin/phase37-sync1
Oct 9, 2026
Merged

OrcaWin merged 91 commits into
OrcaWin/phase37from
OrcaWin/phase37-sync1

Conversation

@OrcaWin

@OrcaWin OrcaWin commented Oct 9, 2026 •

Copy link
Copy Markdown
Collaborator
Files Added Deleted Net
Test 742 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​56314 $\color{#cf222e}{\Huge{\mathbf{−}}}$​10654 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​45660
Prod 1003 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​32500 $\color{#cf222e}{\Huge{\mathbf{−}}}$​10252 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​22248

ELI5

Phase 3.7 has been waiting on its own branch while main moved ahead by 90 commits. This brings main into the Phase 3.7 branch so the landing PR shows only the Phase 3.7 work and is tested against current main.

What Changed

A merge of origin/main (e43997c) into OrcaWin/phase37 (64d3f55). No new behavior. Three files conflicted, and a few more were changed on both sides and merged on their own. Each one keeps what both sides meant.

Conflicts resolved by hand

  • src/main/ipc/runtime-environment-managed-tunnel.ts: keeps Phase 3.7's redeploy of a server another desktop stopped (redeployStoppedManagedOrcad, MANAGED_ORCAD_NOT_ACTIVATED_DETAIL, OrcadManagedServing, rebuildOrcadManagedTunnel). Also keeps main's getSshHostServerStatus, which backs the new clearManagedServerNotes. publishHostServerStatus now takes a target id (main's version), and the Phase 3.7 callers type-check against it.
  • src/shared/wsl-login-shell-command.ts: main reverted the per-account Claude folders (Revert Claude per-account folders (#24434, #26405) before release #26638), which removed the fish claude function import. Phase 3.7's stdout fence import (buildPosixStdoutFence, HH-1) is kept.
  • source-control-too-many-changes-banner.test.tsx: main moved TooManyChangesBanner into source-control/listing/too-many-changes-banner.tsx. The test now imports it from there and keeps Phase 3.7's SourceControlContentStatus import for the Stage All cases. The moved banner keeps the onStageAll prop from fix(source-control): Stage All stages every change when the listing hit the 1,000 cap (HH-4) #26618.

Changed on both sides, merged automatically and checked by hand

  • managed-orcad-actions.ts: main's "clear the stale update/serving notes after an update" and Phase 3.7's forget action with the shared unlink policy both stay.
  • orcad-managed-tunnel-manager.ts: main's superseded-tunnel check after checkServing and Phase 3.7's rebuild() sit in separate methods.
  • runtime-environments.ts, the i18n catalogs, en-runtime-required.json, rpc-params-catalog.generated.ts, reliability-gates.jsonc: these are additions from both sides. The catalogs pass their verifiers.

RPC permissions: the only new RPC method in Phase 3.7, managedServer.forget, declares permission: 'host-admin'. defineMethod requires the field, and a scan of every method finds none without it.

Why

A merge, not a rebase, because phase37 is a shared branch that already has squash-merged PRs on it. Bringing main in before the landing PR means the final review and CI run against what will actually ship.

Linked Issue

N/A (maintainer sync of an integration branch)

Visual Proof

N/A: a merge with no behavior change of its own.

Testing

Local, macOS, one worker:

  • pnpm install --frozen-lockfile, pnpm tc: pass

  • oxlint, pnpm run check:code-quality:changed (0 new findings across 224 files), pnpm run audit:anti-slop: pass

  • verify:rpc-params-catalog, verify:localization-catalog, verify:localization-runtime-catalog, verify:localization-extraction, verify:localization-coverage, check:max-lines-ratchet, check:reliability-gates, check:ts-nocheck-ratchet, check:runtime-electron-ratchet: pass

  • Runtime boundary tests (vitest-node-runtime-boundary, vitest-runtime-boundary, vitest-sqlite-runtime-boundary, plain-node-entry-guard): 46/46 pass

  • Every test file Phase 3.7 touched, plus the tests for the overlapping files: 99 files, 1069 tests pass. The mobile capped Stage All test passes 4/4.

  • check-runtime-launcher-protocol-ratchet --base origin/main: OK. Against the phase37 base it flags main's own protocol 43 bump and launcher change, which come in through this merge. They are not new in this PR.

  • I manually tested these changes locally

  • Automated tests added/updated, or explained why not below (a merge; the existing tests cover it)

Review

Conflicts were resolved by hand, and each file changed on both sides was read.

Agent skill upstream boundary

  • Not applicable, or this change copies or mechanically translates no upstream skill-installer source, tests, fixtures, registry entries, path tables, comments, or documentation.

Notes

SSH/remote: the conflicting code is the managed-server status line and the WSL login shell. Both sides' behavior is kept as described above.

Checklist

  • This PR is small and focused
  • I explained what changed and why
  • Before/after screenshots or videos attached for UI changes, or N/A with reason
  • Self-reviewed for correctness, security, and performance
  • Cross-platform, SSH/remote, and path/shortcut impact considered (or N/A)
  • pnpm lint, pnpm typecheck, pnpm test, and pnpm build pass (or CI will cover; local preferred)

nwparker and others added 30 commits October 8, 2026 06:14
* ci: reduce unit runner demand and reuse the loaded preview frame

* test: remove source spelling probes without behavioral assertions
…d is full (#26543)

* fix(native-chat): never refuse a user's write because the retry record is full

The record that lets a retried chat write or agent launch replay instead of
running twice refused every new write once it held 512 ids for one caller or
4,096 in total. Background agent traffic fills that in a normal day, so the
user's own message was refused with "too many requests in the last day".

Remove the count limit from the durable record, the in-memory launch record
and the SSH relay's launch record; each entry still lives exactly as long as
the retry window it protects. Clients still understand the refusal from older
hosts, and its message now says to update Orca on that computer.

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

* fix(native-chat): drop leftover cap wording and dead launch fallback; cover create records past the old cap

* fix(native-chat): keep refusal and mailbox comments true after merge

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
… and keep the queue from covering the chat (#26553)

* fix(orchestration): keep automatic mail out of the user's message queue

Orchestration mail reached a busy chat as ordinary queued message cards, and
the queue refused at 20 unsettled cards or 1 MiB with a bare invalid
refusal, so automatic notices could fill it and refuse the user's own next
queued message. Mail now stays unread while the chat is working, waiting on
a prompt or holding unsettled cards, and is coalesced into one delivery when
it becomes free; batch identity is recorded so nothing is delivered twice.
Human drafts no longer share an aggregate quota, and a failed settled
receipt keeps its code, details and message for diagnosis.

Co-Authored-By: OpenAI Codex gpt-6.1-sol <noreply@openai.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(native-chat): never refuse a message because automatic mail filled the queue

Drop the 20-card queue limit and the mail-delivery rework; keep only a byte
bound on what queued cards publish (it rides every opening frame beside the
history page), refused with a reason that says what to do. The desktop queue
list now scrolls instead of squeezing the transcript.

* fix(native-chat): keep room for the person's message in a queue other agents fill

Other agents' cards stop one maximal message short of the queued-bytes bound,
so background work alone can never refuse a person's send. The person's newly
queued card scrolls into view, and the refusal's step and host message now name
only what the person can do.

* fix(native-chat): open a loaded queue on its next card and scroll before paint

---------

Co-authored-by: OpenAI Codex gpt-6.1-sol <noreply@openai.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* Native chat: give Grok, OpenCode and OMP chats inline visuals

Each ACP agent loads the chat-visuals skill its own way for one launch,
without touching the user's agent config: Grok through agent
--plugin-dir, OpenCode 2.x through inline skills.paths, OMP through a
PI_CONFIG_FILES overlay that keeps the user's own skill folders. A chat
whose agent can't load it starts without visuals.

* Native chat: load OMP chat visuals through --plugin-dir

OMP's settings layers replace lists, so the PI_CONFIG_FILES overlay hid
a repo's or profile's own skill folders in Orca's OMP chats, and its
files were never removed. omp acp takes --plugin-dir like Grok, which
adds the skill beside the user's own plugins and skill folders and
leaves their config untouched.

* Native chat: note OMP releases that skip --plugin-dir skills
…ore at once (#26361)

* feat(native-chat): show each chat's resume as it happens, and start more at once

Resuming interrupted chats sent one request for the whole batch, and the
host answered only after every agent had replied, so "Resuming 21 chats"
stayed up until the slowest chat answered and the dialog could only say
"Resuming…" for every row. The host also started 3 agents at a time.

The desktop app now sends one request per chat, so each chat's result
lands as soon as it has one. The host keeps one start limit across all
of those requests, sized to the machine's cores instead of a fixed 3.

The status bar reads "Resuming chats 9/21". Reopening the dialog mid-run
shows a gray spinner where each resuming chat's checkbox was, with its
step (waiting to start, starting, waiting for a reply) and time in the
tooltip, then a check or the failure row when it settles. A line above
the list counts where the chats stand, with filters for In progress,
Resumed and Need you.

* fix(native-chat): publish restart progress from one host action

* Keep chat resume progress stable through reply handoff

* Fix restart progress ownership after skipped requests

* Keep completed chat resume summaries until viewed and closed

* chore(i18n): drop status-bar keys the merge restored
* Fix Mermaid diagram recovery after streaming replies

* Clarify when the Mermaid render queue releases tasks
…se (#26538)

* Preserve native chat skills and uploads when composers close

* Own menu image pastes before saving begins

* Keep paste ownership independent of image presentation

* Show pending pastes so stalled saves can be removed

* Register confirmed image pastes and preserve preview ownership

* Keep clipboard image presence and saving on one source selector

* Describe clipboard image formats in upload routing fixtures
…o Another Agent" (#26375)

* feat(session-history): resume a native chat's conversation in a new CLI session

Agent Session History offered "Resume in New Chat" to move a terminal session
into native chat, but nothing went the other way: once a native chat owned a
conversation, its row only reopened the chat, and Orca refused any terminal
resume of it so two programs never write one conversation.

Rows a native chat owns (Claude, Codex) now offer "Resume in New CLI". It forks:
the terminal runs `claude --resume <id> --fork-session` or `codex fork <id>`,
so the CLI gets the full history under a new id and the chat is unchanged. The
host's resume guard lets a Claude fork through, and the fork tab records no
provider session, so a restart never re-enters the chat's conversation.
"Resume in New Chat" is renamed "Resume in New Native Chat".

* fix(session-history): size the Resume in New CLI tooltip to its text

The tooltip balanced one wrapped string inside a box sized before balancing,
leaving a gap beside its lines. It is now two block lines with no fixed width.

* feat(agent-sessions): rename "Continue in New Session" to "Hand Off to Another Agent"

Agent Session History rows now offer "Resume in New Native Chat", "Resume in
New CLI" and "Continue in New Session…", which read as three versions of one
action. Only the first two carry the same conversation; the third starts a new
session, often with another agent, seeded with a summary or the transcript.

Rename it everywhere it appears (history sidebar, terminal pane header and
menu, native chat menu, the dialog title) and, in the history row menus, move
it below "Copy Resume Command" behind a separator so the Resume items read as
one group.

* fix(session-history): run a CLI fork under the selected Codex account and tighten the fork guard

- Resume in New CLI now asks the host for the same Codex account home plain
  Resume uses, so a fork no longer starts under whichever account's copy of the
  rollout sorted first. The preparation call carries an optional `fork` flag
  that skips only the owned-conversation refusal; plain resume still refuses.
- An older paired host that refuses the fork now shows "Update Orca on the host
  that runs this chat to resume it in a new CLI." instead of a raw error code.
- The host guard admits `--fork-session` only before a `--` terminator and
  never alongside `--session-id`, since either shape lets Claude write the
  owned conversation.
- Tests for the click path, the menu item's eligibility, the fork home
  preparation, and the tightened guard.

* fix(session-history): read a Codex rollout's identity from its first session record

A fork of an older (non-paginated) Codex thread writes its own session record
first and then copies the parent's history, the parent's session record
included. The history scanner kept the last one it saw, so the fork's row took
the parent's id, title, folder and branch: on a native chat's fork it looked
like the chat itself, and Resume on it opened the chat instead of the fork.
Codex itself treats the first session record as the thread's own and ignores
later ones; the scanner now does the same, as Orca's duplicate-rollout check
already did.

* fix(session-history): set up status hooks in the Codex home a CLI fork keeps

When Resume in New CLI can't move a Codex fork to the selected account (that
account has no copy of the conversation yet, or the system default is
selected), the fork runs in the account home the history row came from. A new
Codex tab only gets Orca's status hooks in the selected account's home, and the
fork carries no provider session, so nothing set up the hooks in that row home.
In a home only native chat had used, the fork tab could show no working or
idle status.

The fork's home preparation now readies that home the same way a resumed
session's home is readied (hook install or refresh, plus the daemon-socket
guard). That code is moved into prepareCodexPinnedLaunchHome and shared by both
paths. The fork tab still carries no provider session, so it never reopens the
chat's conversation on restart.

* fix(session-history): ready only a host-verified Codex home for a CLI fork

The fork's hook setup ran in whatever Codex home the request named, so a paired
client could have Orca write hook and config files into any directory. Plain
Resume only readies a home it has verified: one this host lists for Codex
sessions that actually holds the rollout. The fork now uses that same check,
through a shared trustedCodexResumeHomes list, and readies the home only when
the verified home is the one the fork will run in. With no Codex runtime home
service it does nothing, as plain Resume does. A failed check or setup is
logged and never stops the fork from launching.

Tests cover the untrusted home, a missing runtime home service, a rollout on
another host, a WSL home, a fork moved into the real home, and a failed setup.

* fix(session-history): show the host-update message when the desktop wraps the fork refusal

When the local preparation step for Resume in New CLI is refused, Electron
hands the renderer the error as "Error invoking remote method '…': Error:
agent_session_conflict". The fork's failure text matched only the bare code,
so the toast showed that raw line instead of asking the user to update Orca on
the host. It now unwraps the message with the shared IPC error helper before
matching.

* test(agent-session-history): cover Hand Off placement and separator; rename Codex docs heading

* fix(agent-session-history): drop the ellipsis from the Hand Off to Another Agent label
…nk menu (#26511)

* Native chat: open tool-row files through the terminal's file link popover

Tool rows that name a file (Read, Write, Edit, edit cards) now link the
file name. Chat file links, in tool rows and in reply text, show the same
popover the terminal shows for file paths, built from one shared function.

* Fix native chat filename activation and path resolution
* Stop timing out slow profile-state writes; warn instead

Profile-state writes used to fault after 30 seconds. A stalled main loop or a slow disk could then drop the writer and show a "could not confirm your save" dialog, even when the write was still in progress. Slow requests now record a diagnostic breadcrumb and keep waiting for the worker's reply. Close waits for the worker to actually exit, and a termination request alone no longer counts as proof.

Pane-alias and migration-listener changes now save only their own sections, not a full state rebuild. Quit teardown also keeps the profile admission held until the final flush finishes, so a pending write is no longer released early at the deadline.

* fix tests

* Warn when profile saves run slow

Slow profile-state writes used to leave only a diagnostic log entry, so users had no way to tell that recent changes were not yet confirmed saved. The writer now reports when a save write passes the slow-warning threshold and reports recovery when the reply arrives. Writes are never failed or retried because of the delay.

- Main keeps the current delayed/healthy status and publishes changes to the renderer, so a reopened window gets the correct state.
- The renderer shows one persistent warning toast while a save is delayed and dismisses it on recovery.
- Observer errors are logged and do not interrupt saving.
- The web build reports saving as healthy because it does not use the desktop profile writer.

* Fix save-delay toast dismissal racing with a re-shown warning

The save-delay warning used one fixed toast ID. Sonner applies a dismissal
a frame late, so a dismissal could remove a warning that had just been
shown again with the same ID. The hook now tracks the ID of each delayed-save
warning it shows and dismisses only that toast. A new delay gets a fresh ID.

Add Sonner-level tests for remount, StrictMode cleanup, and recover-then-
delay races. Harden the e2e spec with a paused clock and a retry around
main-process evaluate.

* Report slow profile revision checks and exports as save delays

Revision checks and JSON exports run on the same profile-state worker as
saves, so a slow one holds up later writes just like a slow write does.
The save-delay warning only tracked write-* commands, so those stalls
went unreported. Every command except initialize and close now raises
the warning.

Tests cover the export-latest path and assert the delay signal fires
once with true before clearing.

* Clarify slow-save toast to name profile storage

The delayed-save warning said recent changes were not yet confirmed saved, which overstated the risk while Orca was still retrying. The real condition is a slow profile-state write, so the toast now names profile storage and says later saves may wait until that operation finishes. The copy is updated in every locale, and the unit and e2e tests that assert it are updated to match.
* fix(omp): show exit 0 for a command that finished normally

* fix(omp): require foreground timing before inferring success
…6638)

Users with a saved Claude account were signed out of every Claude feature in Orca until they signed in again, and proxy setups (API key plus ANTHROPIC_BASE_URL) got 401s after signing in. Reverting before release to rework the upgrade path. The terminal daemon protocol moves forward to 43 so dev builds' v42 daemons with the claude function are not reused; v41 and v42 stay attachable. Nothing is deleted: per-account folders are left unused and the old saved-account store was never touched.
* refactor(native-chat): make structured chat the only Chat UI runtime

Retire the experimentalStructuredNativeChat opt-in. Chat UI on is now the
one gate for structured chat: new agent tabs that default to Chat UI start a
structured session wherever the launch is supported, and unsupported
launches keep falling back to terminal-backed chat.

- Shared isNativeChatEnabled predicate replaces prefersStructuredNativeChatByDefault;
  host admission, renderer routing, orchestration mode, startup projection,
  tab sync and the resume offer all read it.
- The retired key is stripped on load and on save, dropped from the settings
  type, defaults, the published client-settings projection and telemetry.
- Settings no longer offer the structured toggle; resume-after-restart and
  shell-environment rows show under the Chat UI default view.
- Startup structured-tab projection moves beside its sync hook so both gates
  read the same predicate.

* docs(native-chat): describe structured chat as the Chat UI runtime

New local Claude and Codex chats no longer sit on a PTY, so the page stops saying the terminal is always the source of truth and scopes the chat/terminal toggle to terminal-backed chats.

* fix(native-chat): stop promising a Windows fallback in the Chat UI scope note

A host refusal after the route picks structured chat still ends as a failed launch.

* fix(native-chat): keep open structured chats reachable when Chat UI is off

Turning Chat UI off hid open Claude/Codex chat tabs while their provider
processes kept running, because the host and the desktop mirror gated
serving existing sessions on the same setting that decides how new
launches open.

The host now asks two separate questions. Creating a new structured
session (agentSession.create, createSupport, ensure) needs the negotiated
capability plus the Chat UI setting. Serving a session that already
exists (listing, reading, sending, resuming, closing, and the mobile
projection and restore) needs the capability only. The desktop mirror,
startup restore and resume-on-restart offer no longer read the setting,
and the toggle-off teardown and the mobile re-publish on setting change
are removed as dead.

* fix(native-chat): run the local structured tab sync only where chats may exist

The previous commit started the every-worktree session-tab census and
subscription, the startup restore, and the resume-offer host read for every
desktop user. Gate them on a derived fact instead: Chat UI is on, or the
local host holds at least one structured session record. With Chat UI off
nothing can create a session, so the answer can only go from yes to no.

The host answers from its installed record store, or from whether the store
file exists when the host is not installed, so a machine that never had a
structured chat pays one existence check and never starts the host. Chat UI
turning on starts the sync; turning it off never stops it.

* fix(native-chat): answer an empty restart offer without starting the chat host

With every Chat UI user now on structured chat, the launch-time resume offer read built the host even on machines with no stored chat, which on Linux scans every process environment. No store on disk means no offer, so the listing answers empty without installing the host.

* fix(native-chat): release the structured tab sync when it never subscribed

A failed capability probe reads as unsupported, so the sync returned without subscribing while still holding the one-sync slot. Nothing could start another, and chat tabs stopped live-updating until relaunch. The start now reports whether it subscribed, and a sync that did not releases the slot so the next Chat UI change retries.

* fix(native-chat): let the host answer the restart offer whatever Chat UI says

The renderer asked main whether a structured store existed before reading the restart offer, so it would not start the chat host for users without one. The host now answers an empty offer without starting itself, which made that probe an extra round trip. In paired web it was also wrong: the probe is hard-coded to no and the gate read the browser's own Chat UI setting, so a paired host's chats were never offered back once that was off.

* refactor(native-chat): drop the structured tab sync generation fence

The fence existed so a Chat UI toggle-off could retire in-flight inventory replies before it cleared the mirror. That teardown is gone: turning Chat UI off keeps open chats, and only a test reset still bumped the generation, so every production check compared equal. The sync's own disposal still fences its subscription, and the startup restore latch and publication cursors stay.

* fix(native-chat): retry the structured tab sync when the capability probe fails

A failed runtime status probe maps to an empty capability list, which the sync read as "unsupported" and gave up on. The release only let a later Chat UI change try again, so one transient startup failure left chat tabs without live updates for the whole session. The sync now tells an unknown answer apart from a host that answered without the surface, re-asks unknown answers on its existing subscribe backoff, and stops only on a definite no.

* refactor(native-chat): begin single-switch structured launch routing

* Make Chat UI the single structured launch default

* Remove retired chat settings copy from localization catalogs

* Keep locale checks aligned with the single Chat UI switch

* Keep explicit chats accessible and older client defaults terminal

* Align newly landed Settings tests with the Chat UI switch

* Expect terminal fallback on older paired structured hosts

* Correct terminal fallback launch expectation

* Update tests that still expected the retired chat settings

* Keep chat composer options off terminal launches and restore route coverage

Chat composer model and effort no longer become CLI flags on terminal
launches, since a new launch can no longer open in the terminal chat
view; structured launches seed their own options. Restores dropped
route coverage, including that an explicit resume-in-new-chat request
stays available while Chat UI is off.

* Replace the stale remembered-options test with terminal fallback coverage

The test expected chat composer model and effort on a codex launch with
Chat UI on, but its mocked host has no structured capability, so that
launch is the terminal fallback that must not carry them. It now checks
the fallback with Chat UI on and off, with shipped and stored arguments;
structured creates are covered where the seed is applied.

* Lower the terminal pane listener budget to the measured 15

The explicit-chat change retired the per-pane Chat UI setting read, so
each mounted pane opens one fewer store listener. Pin the budget to the
measured count and count that read among the saved listeners.
#26447 pushed src/main/ipc/app.ts one line over the 300-line limit, so main's
oxlint is red and merges from main fail the commit hook. Move the keyboard
input-source probe and its bounded command runner, unchanged, into
macos-keyboard-input-source.ts beside the other macOS keyboard probes.
…26416)

* feat(native-chat): host-owned startup attempt and hold-and-deliver barrier

The host now mints one startup attempt before any adapter acquires (lease
identity, spawn token, pinned launch context, cancellation, a 60 s deadline
from the mint that activity never renews, option intent, and the event and
child-work sinks), and the shared delivery loop hands nothing to a child
still proving its start. A send is still accepted and journaled at once;
readiness (the normalized `started` settlement) wakes delivery, which
re-checks the child, fence, close and Stop before handover. A start that
fails, crashes, or misses its deadline leaves held messages definitely
unsent; a dispatch that throws after handover is `unknown`, never inferred
unsent from the child's phase.

* fix(native-chat): time agent starts from spawn by progress, and release Claude on initialize

Review round 1 on the host-owned startup attempt.

- The startup clock now starts when the process exists and fails a start
  only after 60 s with no stdout frame or stderr line, or 10 minutes after
  spawn however chatty it is. Every agent's child reports output through
  the one managed-process spawn, so a slow migration or history replay is
  no longer killed mid-way. Each start's spawn-to-outcome time is recorded
  as an `agentSession.startup` span for tuning.
- Claude reports `started` on its initialize answer; the settings read
  follows and is applied and persisted as a later `options-reported`
  update, so messages no longer wait on it.
- A started child's reported options are persisted in their own step after
  the held messages are handed over, and only if no pick changed the
  record's options since.
- An attempt now ends whenever its child ends or an expiry stopped it.
- Removed `optionsAsOf`, which was not an option revision, and comments
  that described the old publish-and-write-at-once behaviour.

* fix(native-chat): keep the startup attempt's contract free of host and process code

The adapter contract imported the attempt type from the module that also runs
its clock and imports the host types, which pulled the model catalog and
process-scan code into the mobile test typecheck. The attempt's shape and limits
now live in a dependency-free contract module.

* fix(native-chat): never let a stale option report undo a later pick

A report of the child's options is now persisted only if no pick, no
model-not-found heal and no newer report reached the chat since its read
began. The host keeps one option revision per chat; the startup attempt
hands the adapter a reader for it, and each report carries the revision
it was read at. This replaces the record-snapshot check `started` had and
covers the settings report, which had none, so a settings read answered
just after a pick can no longer write the older model back.

Also: the option read a ready child does inside its start now ends with
the start (limit, Stop or close); a start that turns ready as the limit
fires is recorded as ready; the startup-attempt host tests run under the
Node SQLite runtime; and the retired-model test follows the new startup
order.

* fix(native-chat): require the option revision and order reports by arrival

The host can no longer mint a startup attempt without the conversation's
option revision, so no host path can stamp a report at a stale zero and
drop every report after the first pick. Adapter tests may still omit it.

A report is now dropped only when a pick or a value the child showed it
cannot run came after its read. Reports are ordered among themselves by
when the host takes them, and only the newest admitted one is persisted.
The settings read's stamp therefore holds however late the host takes
the start's report, instead of relying on that report being handled
synchronously before the read begins.

Tests: a settings read stamped before the host took the start's report is
kept; a report read after a failed pick is persisted; the minted attempt's
revision reader answers the host's current revision.
#26416 pushed structured-agent-session-host.ts one line over the 300-line limit,
so main's oxlint is red and merges from main fail the commit hook. Move the
pure knownAgentIds logic into structured-agent-session-provider-support.ts
beside the other agent-support checks.
* Keep foreground Codex commands out of the background strip

* Run the command-strip host regression in Node

* Exercise retained commands after their source turns end
* fix(editor): route zoom gestures and app zoom to the PDF viewer

Ctrl-wheel / trackpad pinch over a PDF scrolled it instead of zooming, and
Cmd/Ctrl +/-/0 changed the (invisible) editor font zoom because the main
process intercepts zoom shortcuts before the PDF's keydown listener runs.

- The pdf.js session owns a non-passive ctrl-wheel listener for its
  container and zooms to an absolute scale anchored at the pointer.
- App zoom commands on an editor tab are first offered to the PDF in the
  command-owning (focused) editor pane; unclaimed commands keep editor font
  zoom.
- Wheel, toolbar and shortcut zoom all record the persisted scale.

Co-authored-by: Infinity_X <Infinity_X@sinomo.net>

* fix(editor): make pinch zoom track the trackpad more closely

Halve the wheel-zoom divisor so pinch zoom in PDFs and images moves twice as fast, and halve the per-event delta cap so a single event still zooms less than 2x.

Co-authored-by: Infinity_X <Infinity_X@sinomo.net>

* test(editor): type the PDF ownership test's document fixture

* fix(editor): keep fast pinch bursts anchored to the pointer

pdf.js refreshes its saved location on the next frame, so a second wheel event in the same frame zoomed around the old location and drifted. Call viewer.update() after the anchor scroll. Also make the ownership test's reset assertion depend on reset running.

---------

Co-authored-by: Infinity_X <Infinity_X@sinomo.net>
Radix restores focus to the trigger on close, so the model and effort
pills kept it. Escape and Tab still land there; only a choice moves on.
…es (#26596)

Placement only measured the gap below the target, so a tall target or a short
pane sent the card to a side with no room and Radix slid it back over the
selection.
…keep a streamed foreground tab live (#26657)

* fix(window): re-hide a covered window when the renderer-throttle lease is released

Restoring background throttling only clears Electron's disable_hidden flag; the
occlusion or minimize hide swallowed while the lease was held is never replayed,
so a covered window kept compositing at ~60 rAF/s until the next cover cycle.
After the last release, a discarded empty-rect stayHidden capturePage makes
Chromium's capturer-count decrement re-apply the current visibility, so the
pending hide lands without the page ever reporting visible. Skipped for a
focused renderer, best-effort and never awaited.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* fix(window): let a broken re-hide capture surface instead of swallowing it

Drops the synchronous try/catch around the stayHidden capture so a missing
capturePage fails loudly; only the promise rejection is swallowed. Test
doubles that reach a final lease release now provide capturePage. Keeps the
focused-window skip (a covered window loses focus) and the empty rect.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* fix(browser): keep a streamed active tab painting, and re-hide it when the stream ends

When the streamed tab is the foreground desktop tab, the guest's own widget is
what a cover hides, and lifting the window throttle does not reach it, so the
phone froze after one or two frames. The page stream now re-applies the guest's
setBackgroundThrottling(false) at start and on the window's hide/minimize while
live. At stream end it drops those listeners, re-throttles the guest and replays
its swallowed hide with the same stayHidden capture the window lease uses
(now exported as replaySwallowedHide), so the guest stops painting.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* refactor(browser): hold the streamed guest through the renderer-throttle lease

The page stream now takes a rendererPublicationThrottle lease on the guest
instead of a bespoke guest-paint helper. The start unthrottle sets the guest
widget's disable_hidden flag (copied onto new widgets), so the window
hide/minimize re-apply listeners were no-ops and are gone. The guest gets the
lease's end-of-stream re-throttle and stayHidden re-hide, including the focus
skip, which holds for a guest because its isFocused requires the embedder's
window to be key and its root view focused.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* test(runtime): give the mobile-session window doubles a capturePage

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
… instead of being refused (#25704)

* fix(native-chat): a /compact sent while the agent works waits in line instead of being refused

* test(native-chat): build the admission test's submissions without a type assertion

* fix(native-chat): a /clear refused while the agent works says so plainly; a /compact behind an unsent message waits for it quietly

* test(native-chat): name the admission test's cases for the sends they model

* fix(native-chat): a /compact refused while the agent works reads plainly too; a command card offers no Edit

* test(native-chat): a refused /compact reads the plain sentence

* fix(native-chat): a refused command card is spent and said once; a command card offers Send while the agent is idle; /compact behind an unsent message is not taken

* test(native-chat): keep the queued receipt's narrowing inside the wait

* fix(native-chat): spend a command card only when its own turn says why; refuse a queued command without a hand-off, with its reason; a queue send on its way reads as sending

* fix(native-chat): a recorded send is no sending card; queue lists land at once; a spent card's wait says to run it again; only /compact or /clear give way to returned text

* refactor(native-chat): derive the sending cards inside the queued-messages hook

* fix(native-chat): keep protocol-version and the agent-session params under the line limit; the send capabilities get their own module

* fix(native-chat): with the agent idle, a command behind this window's own unsent message says it is still being sent

* fix(native-chat): the queued-commands capability stays a plain constant beside the others; main's send capabilities back to its text

* test(native-chat): keep the rewind suite under its line limit after main's growth

* fix(native-chat): one import per module in the command send; a rewind on its way holds commands, tested

* test(native-chat): type the rewind module mock with a type import

* test(native-chat): a command card cut short by a restart waits or is kept like any card; kept-card captions name only controls shown

* fix(native-chat): a held command card, which the queue skips, no longer forces later sends to queue

* fix(native-chat): a command's refusal line goes once what it waits on is gone

A refusal that names something the chat shows (the agent working, a pending prompt, background
tasks, an unsent message) is said only while that still holds, derived each render, and dropped
once it ends. Other composer errors are unchanged.

* fix(mobile): a command's refusal banner goes once what it waits on is gone

The phone's banner keeps a refusal naming the agent working or a pending prompt only while the
chat still shows it, dropped each render once it ends. Background tasks, which the phone can't
see, and generic errors behave as before. The host-reason mapping is shared with desktop.

* fix(native-chat): a refusal behind an earlier message names whether it is being sent or offers Retry

Each line now goes when its own state ends: a send that fails stops being sent, and a resend
stops offering Retry.

* test(native-chat): a Stop behind a typed /compact keeps the taken-back message on screen; the in-doubt case says so

* fix(native-chat): a command refused while the queue is about to send reads working, as the pane does

* Update held command fixture for the merged queue model

* Restart one host at a time in the recovery ordering test

* fix: preserve person queue reserve for chat commands
* Let automations pass extra agent arguments on each run

Automations could only use the host's default agent Arguments, so a
per-automation choice such as a different model needed a settings change
that affected every run. Automations can now save extra arguments that
are appended to the host's defaults on each fresh run. Extras are
allowlisted to model, effort, and Claude's --add-dir, and are refused for
permission, config, prompt, and session options.

- Add the field through the CLI (`automations create/edit
  --extra-agent-args`), the RPC contract, the automation store, and the
  editor's Advanced section, with validation at save and at launch.
- Merge extras over the defaults by replacing a model or effort option
  the defaults already set, then re-tokenize to confirm the launch argv
  matches what was validated.
- Gate writes behind a new runtime capability, and refuse to save or
  migrate extras to a host that would silently drop them.
- Require a fresh session, since reused sessions cannot take new
  arguments.

* Simplify automation argument validation and launch handling

* improve the extra args display

* fix translation

* Validate automation extra args during ORCA migration

Dormant automations imported from the migration manifest skipped the extra-args check. A disallowed flag, or Reuse combined with extra args, could persist and then fail every later run. The manifest parser now rejects those rows.

Also move the automation runtime capability constants into `automation-runtime-capabilities.ts`. `protocol-version.ts` re-exports them, so existing imports keep working.

* add more demo with effort
* Recognize native chat sign-in failures across structured agents

* Fix authentication failures and chat sign-in guidance

* Preserve sign-in diagnostics and command retry context

* fix(native-chat): keep visible sign-in guidance and localize startup rows

* fix(native-chat): separate auth diagnostics from retry guidance

* test(native-chat): align startup guidance expectations

* fix(native-chat): start a new agent for the send after it reports not signed in

* fix(native-chat): scope the signed-out check by fence and read without a snapshot

* fix(native-chat): keep a signed-out agent while it owes work, and read only its own rows

* fix(native-chat): keep a signed-out agent that may owe a wake-up turn, and skip the check during a Stop
* Recognize native chat sign-in failures across structured agents

* Fix authentication failures and chat sign-in guidance

* Preserve sign-in diagnostics and command retry context

* fix(native-chat): keep visible sign-in guidance and localize startup rows

* fix(native-chat): separate auth diagnostics from retry guidance

* test(native-chat): align startup guidance expectations

* fix(native-chat): treat OMP's signed-out 'No model selected' as not signed in

* fix(native-chat): start a new agent for the send after it reports not signed in

* fix(native-chat): scope the signed-out check by fence and read without a snapshot

* fix(native-chat): keep a signed-out agent while it owes work, and read only its own rows

* fix(native-chat): keep a signed-out agent that may owe a wake-up turn, and skip the check during a Stop
* Recover installed forge CLIs after launcher timeouts

* Preserve healthy CLI timeouts and add native platform checks

* Resolve Windows tree termination outside PATH

* Use native absolute paths in the relative PATH fixture

* Expect the absolute System32 taskkill in tree-kill tests

---------

Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
…t glide when following (#26238)

* feat(native-chat): pace and fade streamed reply text

Snapshots land a clause at a time, 50-700 ms apart. Drawing behind them
at an eased speed is what makes a reply read as flowing.

* feat(native-chat): glide the transcript when following and jumping to latest

The offset is still pinned at once; only what the pin moved is eased, so
nothing races the virtualizer or a reply that keeps growing.

* chore: list remend with the other bundled renderer packages

dependencies is for packages the main process loads at runtime.

* Bound streamed reply reveal and preserve chat interactions

* Smooth overlapping chunks without postponing reveal deadlines

* Fix first-reply following and pin settled layout changes

* Pin concurrent media layout changes without a follow glide

* Keep native chat layout stable when replies complete

* Model delayed browser scroll events in navigation tests

---------

Co-authored-by: Brennan Benson <79079362+brennanb2025@users.noreply.github.com>
brennanb2025 and others added 25 commits October 8, 2026 23:25
…e, clickable on the queued card too (#26357)

* feat(native-chat): name an agent's message by its current title, and open it from the queued card

The sender line on another agent's message and on its queued card now shows the
name Orca shows for that agent right now (its sidebar agent row or chat tab
name), falling back to the name recorded when it was sent. The queued card's
names open the sender, as the transcript row's already do.

* fix(orchestration): name a CLI agent sender by its tab's own name, not just its agent

A terminal agent's recorded sender name used only a manual tab rename, else the
agent type, so mail from CLI agents read as "Codex" or "Claude". The host now
names every sender the way Orca shows it: a chat by its rename or saved name, a
terminal agent by its sidebar agent row's name (rename, quick command, generated
or meaningful live title; a split pane only by its own title). The task title and
generic agent label are fallbacks. The renderer shows the live own name, else the
recorded one.

* fix(orchestration): preserve known sender names in message snapshots

* Preserve sender names across chat clears and terminal reuse

* fix: keep sender links on the recipient host across older runtimes

* test: type sender naming fixtures with their declared identities

* Fix sender ownership publication after clearing a chat

* Drop /clear sender lineage now that /clear keeps the same conversation

Main's #26579 keeps a native chat in the same session across /clear, so a
message's sender id still names the live chat. Remove the host's
orchestration-root projection, the optional status field, and the renderer's
lineage lookup and feed capability probe that only served it.
* Start worktree.create's agent through the launch executor

The host's worktree.create handler now starts a requested agent (startupAgent,
or a linked startupDraft) through executeAgentLaunch instead of calling the
create directly. Nothing on the wire changes: the request, the reply and
agentTerminalHandle are the same, the agent is still the workspace's startup
terminal whatever the chat default says, and the create still delivers the
text itself (folded into the command at any length, a follow-up for agents
that read it after start, and the draft pasted unsent).

Adds three host-internal executor inputs: terminalOnly, the legacy-host prompt
policy, and a shared entry (createWorktreeWithStartupAgent) that host
producers can call in place of createManagedWorktree.

* Keep a linked-draft create recording only the agent it actually started

Problem: after worktree.create moved onto the launch executor, a create from
a linked issue (phone "create from issue", or a raw worktree.create with
startupDraft) could differ from before in two narrow cases. When the draft's
agent came from the settings default or from detection and its launch command
could not be built (for example unbalanced quotes in the saved agent
arguments), the workspace was recorded as created with that agent even though
it never started, and a background create opened no terminal at all instead
of its plain shell. When no agent was found, detection ran twice (an extra SSH
round trip on remote repos).

User-facing change: none intended; both cases now behave exactly as before
the executor move.

Mechanism:
- The entry hands the draft's chosen agent to the create as a main-internal
  startupDraftAgent, which steers only the draft; createdWithAgent stays what
  the caller sent. A draft that resolves no agent is created without the
  draft, so the create does not detect again.
- AgentLaunchExecution is a discriminated union: every normal launch must
  bring its surface factory, and a legacy-host launch must be terminal-only,
  bring a workspace factory, and cannot bring surfaces. The runtime guards
  the types replace are gone; the target kind is checked before any create.
- Legacy-host receipts follow the delivery module's rule: a blank prompt is
  no prompt, a submit folded into the launch command is handed-to-terminal,
  a post-start send the create does not await is unconfirmed (host-internal),
  a draft is not-delivered.
- The no-agent error lives with the legacy-host policy and is caught only by
  the shared entry.
- Tests: a real-runtime parity test (stored agent, terminal spawns,
  detection count vs the create alone, including the unbuildable plan),
  exact-argument checks over every non-agent startup field, compile-time
  checks for the union, and direct tests of the draft agent resolver.

* Say which creates still bypass the shared agent entry

Two comments stated that headless automations and federation already go
through createWorktreeWithStartupAgent; both still call
createManagedWorktree directly. Reword them as the plan. Comment-only.
* feat(runtime): default to bundled Node 24 and require modern hosts

* Update SSH deployment expectations for Node 24 guidance

* Expect Node 24 in Windows runtime installation guidance
…at always fits (#26664)

* fix(native-chat): settle a dead agent's leftover work in one write that always fits

A dead generation's cleanup was several separately committed writes, and a
single leftover item larger than one batch row was rejected on every try, so
the chat stayed stuck. Plan every row first and commit them in one
transaction: bounded batch rows, an oversize item as an ordinary row with its
full body, and the settlement's own send verdicts. Readers see it only after
the commit. Settlement rows are rebuilt only from schema-known fields, address
saved items by their stored key, never invent an end time, and new writes
require safe revisions, so cleanup never builds a row it cannot save.

* fix(native-chat): settle Codex exits in one write and keep turn fields a newer build saved

- Codex's own exit cleanup hands the whole settlement to the journal's one-transaction planner instead of committing chunk by chunk.
- One shared planner rejects an item named twice, on both the direct and resolved paths.
- Rewriting a turn keeps fields this build does not know, as the journal schema promises; the legacy carrier's kind still cannot override it.
- Drop the write-only revision admission and the planner skip that worked around it: insert admission is the reader's again, and a saved revision that cannot advance is skipped by the reducer while the rest settles, as before.
- Remove two dead-generation journal members settlement no longer calls.

* fix(native-chat): advance repeated items across a settlement's rows and keep nested turn fields

Repeated Codex question ids in one write fold in order again instead of failing the write,
and a saved turn's context usage keeps fields a newer build stored inside it.

* test(native-chat): run the new Codex journal SQLite tests in the Node runtime project
* Guard desktop open actions with the target workspace owner

* Wait for the folder catalog only in the folder menu exercise

* Resolve the row owner only while its Open in menu is open

---------

Co-authored-by: m4air <m4air@Mac.localdomain>
…vidence (#26763)

Enable the independent hang watchdog on Windows and preserve the previous run's marker until the startup trace sink is ready. Retain local consent, heartbeat timing, worker cleanup and bounded marker validation. Operation tracking is deferred.

Adapt the startup-order regression coverage from community PR #26468.

Co-authored-by: fanzzzd <fanzzzd@outlook.com>
Run Muse Windows hooks through the existing direct launcher on safe profile paths and refresh managed installations. Keep the PowerShell compatibility fallback for paths requiring special quoting. Verified on Windows; CI passed.

Adapted the direct-launcher change from community PR #26734.

Co-authored-by: Naveen Chatlapalli <naveen.aifanatic@gmail.com>
…26775)

The host header lays out as a 32px card in 4px top padding (36px), but
estimateRenderRowSize sized the row at 32px. The group tier's 36px pin
offset then fired 4px early, so a project header could pin while its
in-flow copy was still on screen — two headers at one coordinate.

Size the host row 36px so it matches the pinned slot, and resolve each
sticky handoff from the row's real start offset instead of only the
lagging range start, so a header pins exactly when it reaches the slot.
* feat(grok): show Grok's subagents as subagent rows

* fix(grok): retain live subagent roster ownership

* fix(grok): retain child outcomes during shutdown

* fix(grok): show live subagents in running task surfaces

* fix(grok): settle owned child session completion notices

* refactor(grok): fold child-session guards under the translator line limit
* fix(remote): retry failed SSH conversion catalog loads

* fix(remote): keep the loaded catalog across ordinary disconnects

* fix(remote): replay a reconnect that lands while a failed catalog read is pending

---------

Co-authored-by: m4air <m4air@Mac.localdomain>
…cutor (#26724)

* Start worktree.create's agent through the launch executor

The host's worktree.create handler now starts a requested agent (startupAgent,
or a linked startupDraft) through executeAgentLaunch instead of calling the
create directly. Nothing on the wire changes: the request, the reply and
agentTerminalHandle are the same, the agent is still the workspace's startup
terminal whatever the chat default says, and the create still delivers the
text itself (folded into the command at any length, a follow-up for agents
that read it after start, and the draft pasted unsent).

Adds three host-internal executor inputs: terminalOnly, the legacy-host prompt
policy, and a shared entry (createWorktreeWithStartupAgent) that host
producers can call in place of createManagedWorktree.

* Keep a linked-draft create recording only the agent it actually started

Problem: after worktree.create moved onto the launch executor, a create from
a linked issue (phone "create from issue", or a raw worktree.create with
startupDraft) could differ from before in two narrow cases. When the draft's
agent came from the settings default or from detection and its launch command
could not be built (for example unbalanced quotes in the saved agent
arguments), the workspace was recorded as created with that agent even though
it never started, and a background create opened no terminal at all instead
of its plain shell. When no agent was found, detection ran twice (an extra SSH
round trip on remote repos).

User-facing change: none intended; both cases now behave exactly as before
the executor move.

Mechanism:
- The entry hands the draft's chosen agent to the create as a main-internal
  startupDraftAgent, which steers only the draft; createdWithAgent stays what
  the caller sent. A draft that resolves no agent is created without the
  draft, so the create does not detect again.
- AgentLaunchExecution is a discriminated union: every normal launch must
  bring its surface factory, and a legacy-host launch must be terminal-only,
  bring a workspace factory, and cannot bring surfaces. The runtime guards
  the types replace are gone; the target kind is checked before any create.
- Legacy-host receipts follow the delivery module's rule: a blank prompt is
  no prompt, a submit folded into the launch command is handed-to-terminal,
  a post-start send the create does not await is unconfirmed (host-internal),
  a draft is not-delivered.
- The no-agent error lives with the legacy-host policy and is caught only by
  the shared entry.
- Tests: a real-runtime parity test (stored agent, terminal spawns,
  detection count vs the create alone, including the unbuildable plan),
  exact-argument checks over every non-agent startup field, compile-time
  checks for the union, and direct tests of the draft agent resolver.

* Say which creates still bypass the shared agent entry

Two comments stated that headless automations and federation already go
through createWorktreeWithStartupAgent; both still call
createManagedWorktree directly. Reword them as the plan. Comment-only.

* Start orchestration workers and remote workers through the launch executor

A fresh worker (new worktree or existing, terminal or structured) and a
federated worker on its receiving host now start their agent through
executeAgentLaunch with the worker's own workspace and surface factories.
The worker hands in the mode it decided before its dispatch record existed
(decidedMode), so the host is asked once and the stored receipt is the one
that ran. Effects, setup receipt, stage rows and their order are unchanged;
a refused structured worker still fails rather than becoming a terminal.

* Tighten the worker launch contract and discard a worker session a later launch step strands

- decidedMode is now exclusive with terminalOnly and callerRendersStructured in the type, labelled temporary until one planner settles every launch.
- connectionId's doc says what is true: the executor carries it and nothing reads it yet.
- A worker launch that throws after its structured session exists discards that session itself, since the caller only tears down a returned placement.
- A federated create refuses a launch with no startup agent and narrows repo/name before recording its first stage.
- Tests: exact federated existing-path stages, a created-worktree host refusal that starts a worker terminal, and the stranded-session discard.

* Say exactly which inputs a decided launch mode refuses, and pin the refusal at compile time
* 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
* Preserve host Git identity cache when reimporting repositories

* Keep the destination's automatic GitHub avatar when reimporting a renamed remote

---------

Co-authored-by: m4air <m4air@Mac.localdomain>
…ts (#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.
…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
…26823)

This reverts commit 46460d4 (#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.
…scard (#26677)

Single and bulk discard quiesced editor autosaves for the globally focused
runtime, so a managed repo's in-flight save could land after git restored the
file and bring the discarded edits back. Use the repo-owner-routed settings
the discard itself already uses.
Co-authored-by: m4air <m4air@m4airs-MacBook-Air.local>
# Conflicts:
#	src/main/ipc/runtime-environment-managed-tunnel.ts
#	src/renderer/src/components/right-sidebar/source-control-too-many-changes-banner.test.tsx
#	src/shared/wsl-login-shell-command.ts
@OrcaWin

OrcaWin commented Oct 9, 2026

Copy link
Copy Markdown
Collaborator Author

CI notes for this sync:

  • static analysis and typecheck / verify (changed-code quality): the 341 findings (331 casting, 10 React Doctor) are all main's own lines. The gate compares against the PR base (OrcaWin/phase37), so every line main changed in the last 90 commits counts as "added". The top files are src/main/claude-accounts/*.test.ts, which main restored in its revert Revert Claude per-account folders (#24434, #26405) before release #26638. Locally, pnpm run check:code-quality:changed against origin/main reports 0 new findings across the 224 files this branch changes. The landing PR into main runs the gate against main.
  • test (cloud relay-postgres drain-release-cell-row-contention-postgres.test.ts, expected 180 got 178): a timing-based cloud relay test from main, not touched by Phase 3.7. Rerun once.

@OrcaWin
OrcaWin merged commit 2c0a24f into OrcaWin/phase37 Oct 9, 2026
68 of 72 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

7 participants