Commit Graph
15412 Commits
Author SHA1 Message Date
Kelvin AmoabaandPaweł Kotowski ef950401d3 fix(native-chat): show prompts sent mid-turn and ! commands where Claude read them (#31204)
Co-authored-by: Paweł Kotowski <p.kotowski89@gmail.com>
2026-10-20 23:13:32 +00:00
Kelvin Amoaba be29644ec9 fix(mobile): open a worktree on the tab the desktop is showing (#31215) 2026-10-20 23:12:13 +00:00
Neil 026c45e9e3 fix(ai-vault): give Orca servers reached over SSH the 15 s all-hosts scan budget (#31139) (#31226)
All hosts gave every Orca server a 3 s scan, including the managed servers
Orca reaches through an SSH tunnel, which now serve every SSH host. A healthy
SSH host walking a large home timed out and showed up empty. The saved
environment's connectionDependency now picks the 15 s budget for tunnelled
servers; direct LAN/Tailscale servers keep 3 s.

Reported, with the proposed approach, by @quzard in #31139.
2026-10-20 16:11:11 -07:00
Neil 9693204223 fix(github): a twin project's PR cards, rows and dialogs act on the host that returned the item (#31252)
* fix(github): a twin project's PR cards, rows and dialogs act on the host that returned the item

* fix(github): key twin rows by host so cache sync keeps each copy; a lone row ignores the stamp
2026-10-20 16:07:42 -07:00
NeilandClaude ebc8b06057 Editor tab state is written to its buffer record, not copied into it afterwards (phase 1a) (#31251)
* refactor(editor): make the editor buffer record the source of truth for tab buffer state

Writers of the six buffer fields read and write the record, with the OpenFile
fields written as a mirror in the same store update. The store middleware no
longer copies tab values into the record; it only keeps records keyed by open
tabs (seeded from the tab on create, dropped on close) and, in dev and test
builds, asserts every mirror field equals its record. Host-mirrored web tabs
adopt the host's published values explicitly in the session sync patch.

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

* refactor(editor): check the buffer invariant in e2e builds and tidy the record helpers

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

* test(e2e): give session-sync fixtures the now-required editorBuffers

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

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-10-20 16:01:30 -07:00
Brennan Benson f812c67a05 feat(native-chat): Codex chats list and run the skills Codex loaded (#30937)
* feat(native-chat): Codex chats list and run the skills Codex loaded

A Codex chat's skill list now comes from Codex itself (`skills/list`), live
while the chat runs and refreshed when Codex says its skills changed, and
from a short-lived Codex check while the chat is at rest. Plugin skills keep
Codex's `plugin:skill` names, disabled skills drop out, and each skill file
is its own row. A skill picked from the list reaches Codex as that exact
skill, beside the unchanged `$name` text, on hosts that take it.

* test(native-chat): account for Codex's skill list call; send without a trailing argument

* fix: align skill probes with chat launches and preserve picked mentions

* fix: distinguish skill choices in phone accessibility labels

* fix: clear picked skill files when queued text changes

* fix(native-chat): refresh resting commands when launch settings change

* fix: keep WSL agent refresh on its execution host

* fix: preserve skill selection at read and send boundaries

* fix: narrow agent identity for selected skill notice

* fix: use checked agent predicate for skill notice label

* test: retain locale in composer translation mocks
2026-10-20 15:33:07 -07:00
Neil f320af9825 Update AGENTS.md with Useful Commands section (#31248) 2026-10-20 15:32:49 -07:00
Neil 49e6107ebe ci: stop running WSL host tests for UI-only changes (#31243) 2026-10-20 15:32:46 -07:00
Neil 34f0951f7d fix(automations): a legacy automation on a twin project refuses instead of running on a guessed host (#31234)
* fix(automations): a legacy automation on a twin project refuses instead of running on a guessed host

* fix(automations): capture a legacy run target before the dispatch snapshot, only where it keeps the folder

* fix(automations): re-read a legacy record after capture even when another run captured it first
2026-10-20 15:28:01 -07:00
Neil 1a11050eba docs: remove redundant max-lines agent instructions [skip ci] 2026-10-20 15:27:14 -07:00
Neil 9823949082 fix(orcad): replace a stale terminal daemon once it drains on Linux and WSL hosts (#31235)
* fix(orcad): replace a stale terminal daemon once it drains on Linux and WSL hosts

A daemon forked by an older orcad (before #31135, at umask 077) kept
serving new terminals after an upgrade if it was busy at startup, because
the runtime stale-bundle recycle was macOS-only. orcad now opts in on every
platform; the replacement still waits for exactly zero live sessions and
treats an unverifiable count as keep.

* test(daemon): pin the restart recycle test to linux; tighten wording
2026-10-20 15:27:02 -07:00
Neil 8f56a599e2 docs: slim test agent instructions (#31246) 2026-10-20 15:25:48 -07:00
Neil bc6b2dd84c docs: load Git guidance on demand [skip ci] 2026-10-20 15:21:57 -07:00
Neil fb81ee58fc chore: remove CLAUDE.md (#31242) 2026-10-20 15:18:33 -07:00
Neil 32343cc703 test(skills): synchronize Windows rename contention (#31237) 2026-10-20 15:15:59 -07:00
Neil 64a4d5b0e4 Refactor AGENTS.md for clarity and conciseness (#31238) 2026-10-20 15:13:14 -07:00
Neil d563f8e0e0 feat(speech): set the OpenAI dictation key on a headless host (#31230)
* feat(speech): set the OpenAI dictation key on a headless host (orca speech openai-key)

Cloud dictation needs an OpenAI key on the host that transcribes, and
desktop Settings was the only way to save one. A headless orca serve or
managed server had none. speech.openAiKey.{status,set,clear} save it
through the host's existing key store and secret sealing; the CLI reads
the key from stdin so it never lands in shell history.

* fix(cli): name the update when an older server lacks the speech key methods; one key length cap; PowerShell example (Opus review)
2026-10-20 15:12:28 -07:00
Jinwoo HongandE2E Test c335070de8 Layout-owner build: main answers terminal questions from its own layout (A1, dark) (#31187)
* wip(layout): applyDefaultTabs command in the layout model (dark)

* wip(layout): applyDefaultTabs takes the window's proposed ids; main refuses a taken one

* wip(layout): main closes or keeps an exited split pane by the window's rule (dark)

* wip(layout): a create or split sent again after its close is refused (dark)

* wip: typed test doubles

* wip(layout): main writes a worktree's default tabs in one command and starts them, window or none (dark; twin e)

* wip(layout): main writes a worktree's default tabs in one command and starts them, window or none (dark; twin e)

* wip(layout): main provisions setup and default tabs even when the agent fails to start; no window takes over (dark)

* wip(layout): pane start status in frames; a failed start keeps its pane; a terminal close stands after a failed write (dark)

* wip: lint fixes (max-lines, test throws)

* test(layout): a terminal close stands after a real failed profile write; switch off still refuses

* fix(layout): checked types in the close-failure rule and its tests

* fix(layout): a failed start keeps its pane whatever failed; default tabs start once; a tab's last pane keeps its card (dark)

* test(layout): the agent's kept pane counts as the workspace's terminal; main starts it whatever failed

* fix(layout): the switch-off create path gains no await

* fix(layout): a failed start never writes back a pane the user closed (dark)

* fix(layout): a failed start's pane goes through main's create rules, so a closed tab stays closed (dark)

* test(layout): D1a's switch-on close tests assert the intent main receives (both builds)

* test(layout): the close fixture answers main's command without naming the method

* fix(layout): a re-created pane binds as designed; the id-only ratchet reads a shared import (dark)

* fix(layout): a pane's start status may sit on a pane still bound to its old terminal (dark)

* fix(layout): a pane's start status ends when its pane binds again by any path (dark)

* test(layout): same-id restore ends a status; a replayed split brings back a closed pane by design

* test(layout): terminal reply fixtures captured from the window-owned build, and main's switch-on proofs

* wip(layout): main's terminal surface index feeds the runtime graph (dark)

* wip(layout): main's panes stand in for the window's terminal list (dark)

* test(layout): the window sends main-listed panes as mount facts (dark)

* test(layout): surface proofs use public runtime calls

* chore: lint

* feat(layout): close by handle closes in main's layout; the close-time relist goes (dark)

* feat(layout): main's binding outranks a caller's pane claim on the terminal record (dark)

* test(layout): window-list tests stay on the rollback build; switch-on mount-fact cases (dark)

* wip(layout): main's terminal index reads E1's shared projection and follows its operation ends (dark)

* refactor(layout): the main-surfaces host lives beside the session-tabs readers, keeping the graph file under its line cap (dark)

* fix(layout): main's panes skip the window-era membership fence, follow workspace routing, and list a terminal when it registers; window close only hands the graph back (dark)

* refactor(runtime): explicit-close PTY stops move unchanged to their own module, for the file's line cap

* test(layout): the cold-restore table runs in a layout-owner build too: one push per registration

* test(layout): the parked and automation pane tests assert the same window report in both builds (mount facts with the switch on)

* chore: one layout-owner switch import after the restack on D1a

* feat(layout): main's terminal.list rows carry the host's verdict; a pane with a status is never live (dark)

* chore(layout): fit main's surfaces under the lint gates after the D1a restack

* feat(layout): every pane status D1a publishes keeps a row from reading live; remote host answers pass through (dark)

* feat(layout): a drag-out moves main's own binding and refuses the home copy's last pane; the window's terminal claim is not consulted (L127, dark)

* fix(layout): a tab close by a starting pane's handle stops the tab's terminals; a starting pane is never exited on the inventory alone; terminal.list lists every saved pane main owns; the pane claim lookup leaves the output path (dark)

* fix(terminal): keep shipped exit and default-tab behaviour with the layout switch on

An exited pane closes through the legacy retirement unless the keep rule holds; the
default tab's title and color still reach the window and the phone's live tabs.

* fix(terminal): one id guard for every pane write; keep panes on an unvouched -1

Main's taken and closed-id guard now runs where every pane write goes, so a runtime create
replayed after its tab closed is refused before it spawns. An exit code nothing vouches for
keeps the pane, as the window does; a kept pane sends the phone no retirement. Converts the
switch-on tests that asserted the deleted window hand-off and title bridge.

* fix(terminal): keep the closed-tab guard on every pane write, held-id checks on client commands

A paired host starts each pane of a client's tab as a create under the tab's id, so a held tab
id is refused only for a client's own layout command.

* fix(terminal): a provider-reported -1 keeps the pane, as the window does

Only a requested stop or a host-confirmed death closes a pane on a negative exit code. Pins that
the closed-tab guard never fires in the shipped build.

* chore(terminal): satisfy the exhaustive-switch and test typing checks

* test(terminal): give the replay test's live process a cwd

* fix(layout): main's index reads routing only for a workspace two partitions hold, the fence reads ownership once, a legacy pane keeps its claim; switch-on tests assert main's answers after a window closes (dark)

* refactor(runtime): a worktree stop's terminal collection moves unchanged to its own module, for the file's line cap

* fix(layout): main's index routes every workspace as the layout stream does, re-reading routing only on a write or a terminal registration; a worktree stop names every terminal main binds; a starting pane is unverifiable (dark)

* feat(layout): every pane status main publishes keeps a row from reading live; a resuming pane is unverifiable like a starting one (dark)

---------

Co-authored-by: E2E Test <e2e@test.local>
2026-10-20 18:08:39 -04:00
NeilandClaude 706ea954e8 ci(wsl): run the WSL orcad lane on Debian 12 and Fedora 41 too (#29821)
Matrix the lane over distro families. Debian 12 and Fedora 41 rootfs are exported from their
container images (systemd off only, since the images ship no systemd). The lane test now makes the
systemd distro optional and writes its login-env export to the file a login bash actually reads
(~/.bash_profile on Fedora and Arch), which made new distros fail for the wrong reason.

Co-authored-by: Claude <noreply@anthropic.com>
2026-10-20 14:57:27 -07:00
Neil e02e8f5bcb fix(chat): preserve Claude rewind context after rollback (#31231)
* fix(chat): preserve Claude rewind context after rollback

Fixes STA-11420.

* fix(lint): merge duplicate journal-types import

* fix(chat): skip torn Claude transcript lines when materializing a rewind fork
2026-10-20 14:55:36 -07:00
Jinwoo Hong 695ac882a9 fix(editor): a file from a server no longer reloads once after each restart (#31196)
* fix(editor): name a server's file by its owned id from the first open, so it no longer reloads after a restart

* refactor(mirror): a server file's mirror id no longer needs the bare-path collision lookup

* fix(editor): conflict review reuses an open server file under its owned id instead of opening a second copy

* fix(editor): conflict review overview shows an open server file's unsaved edits under its owned id

* fix(editor): route conflict files by their workspace owner only; keep the overview lookup a plain read

* test(mirror): the browser-placement replay test names its server file by the owned id
2026-10-20 17:50:25 -04:00
Neil 07e6bb8a27 fix(sidebar): show machine labels only for same-named projects (#31233) 2026-10-20 14:48:02 -07:00
Neil 5b861bf4b6 perf(ci): reuse verified Windows builds and trim release history transfer (#31227) 2026-10-20 14:47:02 -07:00
Neil a1b9269525 fix(codex): create Codex config.toml files owner-only (#31228)
* fix(codex): create Codex config.toml files owner-only

The runtime config mirror rewrote Orca's copy of config.toml (which carries
mcp_servers env tokens) at the umask default on every pass, and the trust
writer created a missing ~/.codex/config.toml the same way. New files now
start at 0600; the trust writer keeps an existing file's mode.

* test(codex): pick a kept mode that survives a strict umask
2026-10-20 14:45:03 -07:00
Neil 23cab6aac5 test(claude): accept dispatch metadata after stopping a turn (#31229)
Fixes STA-11421.
2026-10-20 14:43:56 -07:00
Neil 749aadcbc8 fix(mobile): pick a cloud dictation model from the phone (#31221)
* fix(mobile): pick a cloud dictation model from the phone

The dictation setup sheet and Voice settings showed OpenAI models as
read-only text, so a host that lists only cloud models (Windows orca
serve, managed SSH or WSL servers) left the phone with nothing to pick.
A ready cloud row is now selectable through the same speech.dictation.setup
modelId write as a downloaded model; a cloud row without a key says to add
it on the paired computer instead of "Set up on desktop".

Refs #31174
Reported-by: delorean2020 (@delorean2020)

* style(mobile): template literal on the re-indented delete label

* test(mobile): complete the setup fixtures for the tests typecheck ratchet (Codex)

* fix(mobile): short not-ready label that cannot squeeze the model name; button roles and labels
2026-10-20 14:41:09 -07:00
Neil 3be5039eed fix: bump zod to 4.6.7 for recursive schema type compatibility (#31222) 2026-10-20 14:30:10 -07:00
Neil be4e0719b8 test(runtime): SSH health probes keep answering while the SSH layer is stuck (#31179) (#31193)
* test(runtime): SSH probes answer over the Unix socket while the SSH layer is wedged (#31179)

* test(runtime): narrow parsed frames instead of asserting their type

* test(runtime): clean up temp dirs, tolerate socket resets in the SSH wedge guard

* test(runtime): parse wedge-guard response frames into a named type at the socket boundary

* test(runtime): say which frames the wedge-guard parser skips
2026-10-20 14:14:58 -07:00
NeilandClaude 5f40d0c88b fix: apply review feedback for the watcher, guarded-save and chat-start stacks (#31216)
Fixes bugs found reviewing #30270, #30605, #30885, #30417, #31113, #30415,
#30992 and #30997, and removes code those PRs made unnecessary.

Co-authored-by: Claude <noreply@anthropic.com>
2026-10-20 14:07:27 -07:00
Jinjing e507a3342a Remove focus ring from native chat question options (#31219) 2026-10-20 14:06:39 -07:00
Neil 12bf32836b feat(ci): ratchet raw repo connectionId host checks; migrate the cheap ones (#31096)
* feat(ci): ratchet raw repo connectionId host reads; one drop-in resolver

Codex review 6 class fix (#4/#6/#7): host decisions read off a repo row's
legacy connectionId miss rows that name SSH only as executionHostId.

- getRepoRoutingConnectionId (shared/execution-host.ts): drop-in for a raw
  repo.connectionId routing check that also finds executionHostId-only SSH.
- check-repo-host-read-ratchet: per-file count of raw repo connectionId
  reads in src/main, src/shared, src/cli; counts may only shrink.
- Migrated 66 cheap reads: runtime GitHub/GitLab commands, PR/MR head ref
  fetches, preserved-branch cleanup, ref queries, base-ref IPC, worktree
  preparation, trash sweep, scan cache TTL, usage metadata.

* fix(ipc): PR base IPC routes executionHostId-only SSH rows; ratchet is a stated lower bound

Review follow-up (Opus on #31096):
- worktrees:resolvePrBase decided git/gh routing on the raw connectionId while
  its head-ref fetches already used the resolver; one resolved connectionId
  now drives all of it (test fails on main).
- getRepoRoutingConnectionId's contract is stated exactly: never local when the
  raw field is set; on a contradictory ssh row executionHostId wins.
- The ratchet regex no longer matches report.connectionId; header and docs
  say it is a lower bound.
- Regression tests for a runtime GitHub and GitLab command site.

* test(ci): pin the repo host-read ratchet step to the static phase

* fix(ci): repo host-read ratchet skips verbatim field copies into projected rows

A copy of connectionId into a projected row decides nothing; migrating it to
the resolver would change what the projection publishes. The sidebar feed
catalog's copy (added after the baseline) and two older projections no longer
count; baseline pruned to 91.
2026-10-20 14:00:32 -07:00
Neil 8a4ef49a4d fix(wsl-mover): match case-folded WSL paths in the exit proof, report panes without a restore folder, ignore stale agent rows (D1b-5b follow-up) (#31211)
* fix(wsl-mover): match case-folded WSL paths in the exit proof, report panes without a restore folder, ignore stale agent rows

* fix(wsl-mover): attribute moved-folder shells by folder, recheck the fence after the census
2026-10-20 13:45:29 -07:00
JinjingandNeil 0a5a7b1446 fix(orcad): run the remote daemon at the login umask so worktrees it creates are not owner-only (#31135)
* fix(orcad): run the remote daemon at the login umask so worktrees it creates are not owner-only (#31125)

* fix(orcad): keep refreshed Gemini OAuth tokens owner-only now that orcad runs at the login umask

The Gemini usage fetcher rewrote ~/.gemini/oauth_creds.json through a temp file and rename with
no mode, so the replacement took the process umask. Under the old inherited umask 077 that
happened to be 0600 on SSH hosts; at the login umask it would be 0644. Write it through
writeCredentialFileAtomic (0600 staging file, owner-only ACL on Windows).

The hostile-host matrix now checks that the deployed orcad runs at the SSH login's umask and that
its readiness file stays 0600.

* fix(orcad): keep rewritten agent configs at their mode and create the launch PID file owner-only

writeHooksJson now keeps an existing config's mode unless a caller opts out. Rewriting
~/.claude/settings.json (which can hold API keys) and the other agent configs through a temp file
used to take the process umask; inside orcad that was 077, and at the login umask it would widen an
owner-only file to 0644.

The launch runs `umask 077` in the foreground shell rather than inside the backgrounded nohup
list, so the PID file the foreground writes is created 0600 instead of relying on a racing chmod.

The Gemini credential test now pins umask 022 rather than inheriting the runner's.

* fix(agent-hooks): do not create a read-only temp file for a read-only Windows config

A preserved Windows mode can be read-only, which would leave an undeletable temp file behind when
the rename fails. POSIX keeps the preserved mode.

---------

Co-authored-by: Neil <neil@stably.ai>
2026-10-20 13:39:32 -07:00
Jinjing 177acb83f7 perf(persistence): persist a matching terminal binding without re-applying it (#31033)
* perf(persistence): persist a matching terminal binding without re-applying it

A reattach whose binding already matches memory but is not yet on disk now
only marks the selected session domain dirty and relies on the writer to
persist it, instead of cloning the whole session twice for rollback and
re-applying equal values. Adds a defaults-guard miss to the fast-lane
predicate and a binding.path span attribute.

* Preserve host ownership on durability-only PTY binding writes

* Inline PTY binding session dirty tracking
2026-10-20 13:23:40 -07:00
c2cbdbdf33 fix(i18n): prevent Experimental settings crash after language-pack updates (#31202)
Invalidate cached catalogs on i18next resource additions and removals so plugin language-pack updates keep Experimental settings usable without restarting. Preserve cached values only after a successful rebuild.

Fixes #30781.

Contributor approaches reviewed in #30893 and #31190.

Co-authored-by: lorenzozanee <wyz0707@proton.me>
Co-authored-by: Chen <zwq19980411@gmail.com>
2026-10-20 13:16:52 -07:00
Jinwoo HongandE2E Test 94ebd4a5c0 Main starts each restored terminal pane once, at its last size, and names sleeping and held panes (layout-owner build only) (#31171)
* wip(layout): applyDefaultTabs command in the layout model (dark)

* wip(layout): applyDefaultTabs takes the window's proposed ids; main refuses a taken one

* wip(layout): main closes or keeps an exited split pane by the window's rule (dark)

* wip(layout): a create or split sent again after its close is refused (dark)

* wip: typed test doubles

* wip(layout): main writes a worktree's default tabs in one command and starts them, window or none (dark; twin e)

* wip(layout): main writes a worktree's default tabs in one command and starts them, window or none (dark; twin e)

* wip(layout): main provisions setup and default tabs even when the agent fails to start; no window takes over (dark)

* wip(layout): pane start status in frames; a failed start keeps its pane; a terminal close stands after a failed write (dark)

* wip: lint fixes (max-lines, test throws)

* test(layout): a terminal close stands after a real failed profile write; switch off still refuses

* fix(layout): checked types in the close-failure rule and its tests

* fix(layout): a failed start keeps its pane whatever failed; default tabs start once; a tab's last pane keeps its card (dark)

* test(layout): the agent's kept pane counts as the workspace's terminal; main starts it whatever failed

* fix(layout): the switch-off create path gains no await

* fix(layout): a failed start never writes back a pane the user closed (dark)

* fix(layout): a failed start's pane goes through main's create rules, so a closed tab stays closed (dark)

* test(layout): D1a's switch-on close tests assert the intent main receives (both builds)

* test(layout): the close fixture answers main's command without naming the method

* fix(layout): a layout command that changes a pane's terminal raises its binding generation (dark, layout-owner build only)

* fix(layout): main shows unshown live terminals again when a client attaches or the terminal service is replaced (dark, layout-owner build only)

Headless servers had no retry: a terminal the startup pass declined stayed tabless until the
next restart. The pass now also runs when a client authenticates and after the local terminal
provider is installed or replaced, one pass at a time. A terminal whose pane record fails to
save after main bound it is no longer reported as left without a pane.

* feat(layout): main stores each pane's last grid on its tab layout (dark, layout-owner build only)

A new main-only setPaneGrid command records the grid a pane's terminal was last sized to, on
its leaf. It is saved as the layout's optional lastGridsByLeafId, which older builds drop on
read, so a downgrade loses only the hint. It moves with its pane.

* feat(layout): main remembers a resized terminal's grid on its pane and starts the pane at it (dark, layout-owner build only)

Each accepted resize, from a window or a client, is written once per burst as the pane's last
grid. A pane main holds starts at that grid, after a size the caller measured and before its
group's size, then 120x40.

* fix(layout): a re-created pane binds as designed; the id-only ratchet reads a shared import (dark)

* fix(layout): a pane's grid resized just before quit is saved, and a grid change publishes nothing (dark, layout-owner build only)

The desktop and serve quit and orcad's shutdown write any grid still inside its one-second
burst before the final store flush; a quit with none waiting adds no await. A grid is main's
own record: it is not in a published pane, so a resize burst sends no layout frame, and its
write patches only the pane's tab.

* feat(layout): a pane's status names sleeping, resuming and held, and what a status on a bound pane means (dark, layout-owner build only)

* fix(layout): a pane's start status may sit on a pane still bound to its old terminal (dark)

* fix(layout): main starts fresh a restored pane bound to another workspace's terminal (dark, layout-owner build only)

The window's restore rule, now in main for every host: a pane whose stored terminal id was
minted for another workspace is unbound, so it starts fresh, and that terminal, if live, is
placed in its own workspace. The id check moves to shared, which the window now imports.

* fix(layout): a start refused because a newer build's terminal service may still run the pane shows it held, not failed (dark, layout-owner build only)

* fix(layout): a pane's start status ends when its pane binds again by any path (dark)

* feat(layout): main's one start of a restored pane with no live terminal, shared by every non-window trigger (dark, layout-owner build only)

Restores from the pane's own stored terminal and resumes a local agent record; concurrent
triggers share one start, and a start that lost to another starter resolves to the winner's
terminal. Not wired yet.

* feat(layout): a client's first view and a CLI start of a restored pane go through main's one starter (dark, layout-owner build only)

Twin j: the phone's first-view start of a pending row moves into its own runtime layer and,
in the layout-owner build, runs through RestoredPaneStarter, which A1's send, wait, read and
wake call as ensureRestoredPaneStarted. It restores from the pane's own stored terminal, resumes
the agent the host recorded there, and resolves once the terminal is registered. With the switch
off, the phone's start is unchanged.

* test(layout): same-id restore ends a status; a replayed split brings back a closed pane by design

* test(layout): a window restore, a client first view and a CLI send of one restored pane start one terminal (layout-owner build)

* fix(layout): the restore pass releases a pane from another workspace's terminal through main's layout command, and two files stay under the line limit (dark, layout-owner build only)

A new main-only releasePaneTerminal command unbinds a pane from the terminal it names and stops
nothing, so the pass writes no layout outside a command. The stored terminal layout schema moves
to its own file, and a pane's own size (measured, else its last grid) is one helper.

* test(layout): drop a needless spread in the pane release test

* feat(layout): main shows a restored pane whose terminal is gone as sleeping, and a window treats a status-only frame as a binding change (dark, layout-owner build only)

After a census that answered, each local pane bound to a terminal the host no longer runs gets
status 'sleeping' and keeps the id it restores from; any bind ends the status. Relay and paired
server panes are their own host's to judge. A window rebuilds nothing for a frame that only
changes a pane's status.

* test(layout): restored history reaches each start across two host restarts with no client acking the cold restore

A guard, not a fail-first test: the cold-restore ack only drops the daemon's in-memory copy
(daemon-pty-session-control.ts) and the re-anchored checkpoint carries the history
(daemon-pty-spawn-result.ts), so a host with no client keeps it. C1c moves the ack into main.

* test(layout): the activation gate and worktree-reopen tests assert the same behaviour through main's command and frame (layout-owner build)

In a layout-owner build main places live terminals and draws them through its frames, so the
gate's adoption tests now check the window makes no tab or binding of its own, adopts only what
main's frames hold, and resumes no agent over a live terminal main has not placed yet. The
worktree-reopen tests feed main's frame for the restored pane and answer layout.apply, checking
one tab bound to the live terminal and one resumed tab across reopens.

* fix(layout): a restored SSH pane reattaches over its relay, and the restore starter returns the terminal it started (layout-owner build)

An SSH terminal id names its connection, not a workspace, so the rule that drops another
workspace's terminal no longer releases it or starts the pane fresh. The starter now resolves
to the id its start registered instead of re-reading the stored layout, whose bind write can
still be in flight, and a tab saved with no layout counts as one held pane.

* fix(terminal): keep shipped exit and default-tab behaviour with the layout switch on

An exited pane closes through the legacy retirement unless the keep rule holds; the
default tab's title and color still reach the window and the phone's live tabs.

* fix(layout): the restore starter's start result types a missing terminal id as none

* fix(layout): a workspace's first tab waits for main's placement of its live terminals, so activation opens no extra shell beside one (layout-owner build)

A window activating a workspace before main's placement pass had read the host census found no
tab and asked main for the first one; main admitted it into the still-empty workspace, then the
pass added the live terminal's own tab beside it. An initial create now waits out a pass in flight.

* fix(layout): the restore starter never resolves to another workspace's live terminal, and a first view of a pane main dropped is refused (layout-owner build)

* test(layout): the first-tab placement test overrides the worktree map with its declared type

* fix(layout): the first-surface wait lives with main's placement pass and keeps the runtime file under the line limit

* fix(terminal): one id guard for every pane write; keep panes on an unvouched -1

Main's taken and closed-id guard now runs where every pane write goes, so a runtime create
replayed after its tab closed is refused before it spawns. An exit code nothing vouches for
keeps the pane, as the window does; a kept pane sends the phone no retirement. Converts the
switch-on tests that asserted the deleted window hand-off and title bridge.

* fix(layout): a workspace's first surface waits only for the placement pass running when it arrives, and the switch-off build applies commands as before

* fix(terminal): keep the closed-tab guard on every pane write, held-id checks on client commands

A paired host starts each pane of a client's tab as a create under the tab's id, so a held tab
id is refused only for a client's own layout command.

* fix(layout): a quit's pane-grid writes never hold the stop past one second, and orcad awaits nothing for them when none wait

The grid flush before the final save (desktop and serve quit, orcad stop) is now raced against a
1 s budget; a grid it misses only means that pane restarts at the default size. With the switch
off no grid is ever recorded, so the flush is null and orcad no longer awaits even that.

* fix(terminal): a provider-reported -1 keeps the pane, as the window does

Only a requested stop or a host-confirmed death closes a pane on a negative exit code. Pins that
the closed-tab guard never fires in the shipped build.

* fix(layout): a quit's grid flush swallows its own failures, keeps its budget timer live, and the quit's no-await note names it

* chore(terminal): satisfy the exhaustive-switch and test typing checks

* test(terminal): give the replay test's live process a cwd

* fix(layout): a quit writes pending pane grids before the SSH shutdown, and a workspace's first tab waits at most 0.5 s for main's placement pass

The quit's grid flush (still capped at 1 s) now runs before beginSshShutdown, so nothing is awaited
between it and the final save; with no grid waiting, both start synchronously as before. A first tab
waits for the running placement pass at most 500 ms, then is admitted. The quit test's SSH mock now
names the module the quit imports.

* test(layout): the quit tests prove the final save starts in the same turn as the SSH shutdown, in order after the PTY kill

* chore: keep main's bytes for files the merge's format hook rewrote (native-chat formatting is #31184's)

---------

Co-authored-by: E2E Test <e2e@test.local>
2026-10-20 16:16:05 -04:00
Neil 6ac9458d0b fix(orcad): mobile home stats on orca serve hosts, and a parity test that sees every way a host wires a service (#31197)
* fix(orcad): give the headless server a stats collector, and make the parity test see positional and host-port wiring

Mobile home on an orca serve host showed 0 agents and 0 agent time since
1.4.229: orcad built the runtime with no stats collector, so stats.summary
answered {}. orcad now loads the profile's orca-stats.json and counts hook
sessions through the same feed the desktop uses.

The runtime composition parity test only compared constructor options and
runtime/rpc setters in four files, so a positional constructor argument
(stats) and process-wide host ports installed at preflight (speech) were
invisible to it. It now records both, and checks every declared
construction option is accounted for.

Reported-by: Fergus Molloy (@fergusmolloy)

* fix(orcad): subscribe stats before hooks start; parity test ignores teardown resets and finds unlisted host-port files
2026-10-20 13:03:20 -07:00
Brennan Benson 67a12c264c fix(native-chat): a chat is named from its first message at once, so a failed name generation no longer leaves 'Codex Chat' (#30392)
* fix(native-chat): name a chat from its first message at once; the generated name replaces it

A native chat's one stored name was written only by a best-effort, one-shot
generation, so any failure left every surface on the 'Codex Chat' placeholder.
The first prompt now names the chat when its first turn starts, and the
generated name replaces it only while the field still holds that seed.

* fix(native-chat): the chat names setting says a chat shows its first message until it is named
2026-10-20 13:02:41 -07:00
Brennan Benson de849c1416 Rewind Claude chats by forking the kept conversation (#30400)
* fix(native-chat): rewind works at any chat length

Rewind copied the kept part of the chat into its saved record and, for
Codex, re-read and re-translated the whole provider history before and
after the revert. Both were bounded (10,000 rows / 2 MB host, 1,024
entries / 2 MB Codex), so long chats were refused with "too long to go
back in".

The record now names the cut; the kept rows are derived from the
journal, which keeps the old epoch until the rewind completes. Codex
reads only the turns it undoes and trusts thread/revert; recovery
proves an applied revert from the newest turn alone and never
truncates otherwise.

* feat(native-chat): Claude chats launch a rewind's fork of the kept conversation

A rewind record can name the provider session the chat continues in (providerFork). Claude's next
launch forks it with the SDK's own truncating resume at the rewound prompt's parent, and publishes a
forked link once the provider has created it. Removes the transcript proof branch that could never
pass on the real CLI.

* test(native-chat): pin Claude's rewind fork against the real CLI

* feat(native-chat): rewind Claude chats

Claude declares rewind. The adapter answers where a rewind forks (the rewound prompt's parent in
its own transcript); the host saves that fork with the prepared record, stops the agent, and
rebuilds the journal from the rows before the message. Recovery completes a prepared fork, since
nothing outside Orca can have half-happened, and the next launch makes the fork.

* test(native-chat): keep the Claude rewind tests to the flows they pin

* test(codex): type the rewind fixture's page params instead of asserting them

* ci: run the PR checks against main

* fix(native-chat): rewind recovery reads the provider handle only from a record that exists

* fix(native-chat): import the session record type once

* test: run the rewind cut test in the SQLite runtime project

* test(native-chat): run the fork rewind test on SQLite, and give Claude's test double its fork

* fix(native-chat): omit rejected submissions from rewound history

* fix(native-chat): rewind forked chats without restarting the agent

* fix(native-chat): Claude rewinds also leave out messages that never ran

* fix(native-chat): Claude rewind to a mid-turn message drops the rest of that turn

A message sent while Claude is answering joins the running turn. Rewinding to it
forks Claude's conversation at that message, so the chat now drops the rest of
that turn too instead of keeping replies Claude no longer has.

A rewound chat whose kept conversation lives only in another Claude account now
gets the same "history is in another account" refusal as a resume, instead of
Claude's raw start failure on every launch.

* fix(native-chat): rewind only to messages that start a turn

A message sent while the agent is still answering joins that turn. Claude's CLI
holds it until a break and records it after the reply; Orca's chat records it
when it was sent. Rewinding to it therefore cut the chat and Claude's
conversation at different places: the previous fix dropped reply text Claude
still remembered, and the version before kept the message itself. For Codex the
cut started at the turn's own first message, removing a message the user did
not pick.

Rewind now returns only to messages that opened their own turn, the common
pattern: the chat stops offering it on a message sent mid-reply, and the host
refuses one, so a mid-reply message and its turn are always kept or dropped
together. The Claude cut goes back to keeping a kept turn's late rows.

* fix(native-chat): rewind targets follow the turn record's opener; saved rewinds still complete

A message opened its turn when the turn record names it, not only when it sits
at thread level: restored history scopes its openers to their turns, so those
chats lost Rewind.

The opener check now runs only when a new rewind is admitted. Recovery cuts
wherever an already saved rewind points, so one saved by an earlier build
still completes instead of blocking the chat.

* fix(native-chat): recovery's read-ahead reads turns only from a Codex rewind cut
2026-10-20 12:58:21 -07:00
Jinwoo Hong 421455e19d test(orcad): stop Windows daemon teardown from counting processes that reused a parent pid (#31192)
* test(orcad): skip processes older than their claimed parent in the Windows daemon teardown walk

Windows keeps an exited parent's pid in its children's ppid and reuses pids,
so system processes and leftover build helpers could be walked as daemon
descendants and fail teardown. A row created before its claimed parent is now
skipped; a real child the daemon left running still fails teardown.

* test(orcad): skip children of a process that reused the daemon pid; name ppid and age in the leak error
2026-10-20 15:49:37 -04:00
Jinwoo Hong fb21729157 chore(mobile): bump to 0.0.58 and Android versionCode 22 (#31194)
Matches the iOS 0.0.58 build.
mobile-android-v0.0.58
2026-10-20 15:45:03 -04:00
Brennan Benson 2fc2f92a38 test(e2e): follow intentional product changes in stale full-suite specs (#30897)
The daily full E2E run on main fails these specs every time because the
product moved on and PR CI only runs specs a PR touches:
- editor header menu renamed to File actions (#30129)
- terminal attention: an on-screen bell leaves no unread marker (#28637)
- terminal file drops carry their source host (#26830)
- single-choice questions auto-submit (#26288)
- phone terminal rows carry customTitle (#30037)
- gh/git preflight re-reads the login shell PATH (#25183)
- board drag starts after the target column is in view
- chat history anchor read after scroll settles
2026-10-20 12:41:00 -07:00
Brennan Benson 821ab37369 fix(native-chat): open a New Chat's composer at once, before its host answers (#29358)
* WIP: establish local chat draft and first submission ownership

* WIP: retain first submission through preparation and settlement

* Keep new chat drafts local through their first submission

* fix(native-chat): release staged drafts and preserve explicit choices

* test(native-chat): keep distinct draft actions independent

* fix(native-chat): defer cross-window draft announcements

* Fix local chat draft settlement and recovery ownership

* Adopt committed draft presence after a refused allocation

* Simplify first-send recovery and preserve hosted send policy

* Remove launch prompt import cycle and tighten refusal tests

* Align draft storage contracts with typechecked callers

* Preserve definite create refusal feedback and issued recovery fixtures

* Revert the first-Send draft machinery, keeping the synchronous composer focus

The redesign opens the new chat's editor at the action and lets the ordinary at-rest create run in the background, so the local draft record, saved first submission, reserved first-Send sender and pre-session upload capability are no longer needed.

* fix(native-chat): open a New Chat's composer at once, before its host answers

The new chat's tab and composer waited for the host to admit the chat, so keys typed right after New Chat went to whatever had focus. A New Chat the user asked for now opens its chat tab immediately on the host it was routed to, with focus in the composer. Its ordinary launch asks the host in the background, the same probe it already ran before create, and the chat shows that launch's starting, failed and Retry states.

- A launch that carries a prompt still asks first, so a refusal hands the prompt to a terminal as before.
- A refused chat nobody typed in closes and its terminal opens in its place; a chat with typing keeps it.
- A paired host that would run the chat with more access than asked for gets no create; the chat's permission notice is the only one shown.
- A chat closed while its host was being asked issues no create.
- While the chat starts on a paired server, uploads wait and the model picker stays disabled until the server names what it seeds.

* Keep chat launch recovery and composer actions with their owners

* Clarify paired launch admission fixtures and recovery checks

* Apply chat attachment guidance at both paste entry points

* fix(native-chat): finish the immediate New Chat on top of the + menu's chat rows

- The + menu hands focus to the new chat's composer when it closes, instead of a frame later.
- A chat picked by name that its host refuses stays a chat and says why, including the WSL or SSH reason; it never becomes a terminal.
- A chat the user typed in before its host refused keeps its text, and the agent's terminal opens behind it rather than taking the screen; a paired server's refusal says a terminal opened.
- The paired permission check runs once, right before create, for every launch; the chat's own permission notice is the only message.
- A chat closed while its host was being asked shares the existing pre-create gate, so nothing is created.
- Session attachment notice copy lives with its only caller; tests that mirrored the implementation are trimmed.

* fix: preserve chat ownership through launch admission and fallback

* test: type launcher projects without an assertion

* fix: preserve matching refusal details in terminal completion

* fix: preserve fallback terminal placement and appearance

* test: preserve host launch failure notice during integration

* style: format native chat launch changes after integration

* fix: disable paired chat attachments until startup completes

* fix(menus): a closing menu no longer takes focus back from what its item opened

A New Chat chosen from the New Tab menu kept only the first typed key: the
menu stays mounted while it animates out, and Radix focuses its content when
the pointer leaves an item, so the remaining keys went to the closed menu.
Dropdown and context menu items now cancel that pointer focus while their
menu is closing. Trims three hunks back to main's form to stay in budget.

* fix(native-chat): host notices wrap instead of cutting off their instructions

The paired-permission notice now shows on a New Chat as soon as it opens, and
its "Update Orca on this host" instruction was cut off with an ellipsis and no
tooltip. Host notices now wrap like every other composer notice.
2026-10-20 12:39:25 -07:00
Jinwoo HongandE2E Test 997d34b043 Layout groundwork (switched off): main creates, starts and closes terminal tabs and panes itself (#31130)
* wip(layout): applyDefaultTabs command in the layout model (dark)

* wip(layout): applyDefaultTabs takes the window's proposed ids; main refuses a taken one

* wip(layout): main closes or keeps an exited split pane by the window's rule (dark)

* wip(layout): a create or split sent again after its close is refused (dark)

* wip: typed test doubles

* wip(layout): main writes a worktree's default tabs in one command and starts them, window or none (dark; twin e)

* wip(layout): main writes a worktree's default tabs in one command and starts them, window or none (dark; twin e)

* wip(layout): main provisions setup and default tabs even when the agent fails to start; no window takes over (dark)

* wip(layout): pane start status in frames; a failed start keeps its pane; a terminal close stands after a failed write (dark)

* wip: lint fixes (max-lines, test throws)

* test(layout): a terminal close stands after a real failed profile write; switch off still refuses

* fix(layout): checked types in the close-failure rule and its tests

* fix(layout): a failed start keeps its pane whatever failed; default tabs start once; a tab's last pane keeps its card (dark)

* test(layout): the agent's kept pane counts as the workspace's terminal; main starts it whatever failed

* fix(layout): the switch-off create path gains no await

* fix(layout): a failed start never writes back a pane the user closed (dark)

* fix(layout): a failed start's pane goes through main's create rules, so a closed tab stays closed (dark)

* test(layout): D1a's switch-on close tests assert the intent main receives (both builds)

* test(layout): the close fixture answers main's command without naming the method

* fix(layout): a re-created pane binds as designed; the id-only ratchet reads a shared import (dark)

* fix(layout): a pane's start status may sit on a pane still bound to its old terminal (dark)

* fix(layout): a pane's start status ends when its pane binds again by any path (dark)

* test(layout): same-id restore ends a status; a replayed split brings back a closed pane by design

* fix(terminal): keep shipped exit and default-tab behaviour with the layout switch on

An exited pane closes through the legacy retirement unless the keep rule holds; the
default tab's title and color still reach the window and the phone's live tabs.

* fix(terminal): one id guard for every pane write; keep panes on an unvouched -1

Main's taken and closed-id guard now runs where every pane write goes, so a runtime create
replayed after its tab closed is refused before it spawns. An exit code nothing vouches for
keeps the pane, as the window does; a kept pane sends the phone no retirement. Converts the
switch-on tests that asserted the deleted window hand-off and title bridge.

* fix(terminal): keep the closed-tab guard on every pane write, held-id checks on client commands

A paired host starts each pane of a client's tab as a create under the tab's id, so a held tab
id is refused only for a client's own layout command.

* fix(terminal): a provider-reported -1 keeps the pane, as the window does

Only a requested stop or a host-confirmed death closes a pane on a negative exit code. Pins that
the closed-tab guard never fires in the shipped build.

* chore(terminal): satisfy the exhaustive-switch and test typing checks

* test(terminal): give the replay test's live process a cwd

---------

Co-authored-by: E2E Test <e2e@test.local>
2026-10-20 15:01:25 -04:00
Jinwoo Hong 572645396f fix(sidebar): desktop list keeps a paired-server create, stays current while hidden, shows creates started elsewhere (#31090)
* fix(sidebar): the desktop list keeps a paired-server create, stays current while hidden, and shows creates started elsewhere

- A workspace created on a paired server is held in the list until main's
  feed carries the server's row, as local creates already are, instead of
  vanishing between the create reply and the server's list update.
- The window keeps reading main's sidebar feed after the list first mounts,
  so switching back to Workspaces never first draws rows main changed while
  the list was hidden. Main serves the window's reads until it closes anyway.
- A creation main runs that this window holds no request for (a phone's or
  the CLI's) draws a generic "Creating workspace…" row instead of an empty
  gap, so the desktop and phone show the same list.

* fix(sidebar): a creation the window just ended stays off its list while main's feed still lists it

The window drops its own entry on the create reply, but its last copy of main's
feed can list the creation one read longer, with no workspace named yet; for
that read it drew the generic row. The window now keeps the ids it ended off
main's creations until an install no longer lists them, at most ten seconds.

* test(sidebar): give the creation tests' feed domains a project catalog
2026-10-20 15:00:35 -04:00
Brennan Benson c053e53f18 perf(cmd-j): preload the workspace jump palette before first use (#30935)
* perf(cmd-j): preload the jump palette so its first open renders without React's reveal delay

* fix: reuse lazy retry wrapper for preloaded components
2026-10-20 11:57:06 -07:00
Brennan Benson ad1bcfe502 A chat's model, effort and Fast belong to the chat, not to a model (#30561)
* fix(native-chat): a chat's model, effort and Fast belong to the chat, not to a model

Structured chats kept their option values per model on the client, so a value
the host saved without a model (a resumed chat that never picked one, Codex
Fast saved on the conversation) had nowhere to live. The client either dropped
it or labelled the chat with the listing's default model to hold it.

- One flat record per chat holds model, effort, speed and Fast independently;
  a model pick changes only the model and the host reconciles the rest.
- Controls come from the model they apply to, separately from the values; with
  no model known, a saved Codex speed still shows and Standard turns it off.
- The options answer marks values a catalog default supplied
  (current.defaulted). A resumed chat never names or sends such a model as its
  own; a new chat still shows the default the host says it runs. An answer
  without the mark reads as before.
- Every send, including a new chat's first message, carries the chat's own
  model and effort and held picks, never a default read off the picker.
- A held launch pick supersedes only an earlier pick of the same option.

Tracked in STA-10947.

* fix(native-chat): a new chat's first send still pins the default model it shows

A new chat that never picked a model carries the default its picker names, as
before, so it keeps the model it actually ran after the CLI's default changes.
Only a resumed chat's defaulted model is never named or sent.

Tracked in STA-10947.

* fix(native-chat): narrow the host catalog before reading whether its listing names the default

The flag lives only on a listed catalog, so the check confirms the catalog is not the unknown
answer before reading it; the first-frame test reads the chat's flat model value.

* fix(native-chat): a reopened chat whose saved model was refused records the model it now runs

After a retired saved model is reset, the options answer no longer marks the model the child runs
as a default, so the next send records it instead of replaying the refused model on every reopen.

* fix(native-chat): a new chat names and pins the default it shows, and a model pick keeps only what the model offers

A chat this view started names the default model the host fills in, whichever of the model list and
the options answer lands first, and its first send carries the shown model and effort. A resumed
chat still names and sends only its own values. A model pick, held or live, keeps an effort, speed
or Fast only where the new model offers it, so a value the model cannot take is never shown, sent
or remembered.

* test(native-chat): give the no-Fast model fixture its required default flag

* fix(native-chat): an open chat's model pick leaves reconciling to the host

A live pick no longer drops values on the client; the host's reply, when it carries its full
value set, replaces the chat's own values as a read does. Only a pick held before launch, with no
host yet, keeps just what the picked model offers.

* fix(native-chat): a pick reply only adds the values it names

A pick's reply carries only the values the host set or was asked for, so it can't speak for the
rest: it no longer removes values it leaves out. Joined tests pin real reply shapes and a new
Claude chat in a fresh Claude home naming and pinning Opus High.

* fix(native-chat): a tier the drawn model doesn't offer shows as Standard

A Speed value another listed model offers but the named model doesn't is drawn as Standard, which
is what the host reconciles it to, while the chat's record keeps the host's value. Effort, a tier
no listed model offers, an unlisted model and a stand-in model's controls still show what the host
holds. After a lost pick reply the picker no longer shows the old tier as a raw extra choice.

* fix(native-chat): a never-sent chat names and pins its default after a reopen

The options answer no longer marks the default model and effort as stand-ins for a chat this host
started that has sent nothing, so a chat reopened after a restart or on the phone names them and
its first send pins them, as before. A chat taken from history, or one that has sent, keeps the mark.

* fix(native-chat): narrow the missing record as null and type the new-chat fixture's handle chain

* test(native-chat): the unloaded-turn context test's record store answers null for a missing record

* fix(native-chat): a chat's speed shows what its next turn runs

A Codex tier the drawn model lacks shows as Standard, which is how Codex runs it, except Flex, which
Codex passes regardless: established Flex keeps showing, and after a model pick the host hasn't
answered it shows no selection until the reply or a read. Each Codex turn sends the same effective
tier without rewriting the chat's saved one, and a new chat's create carries held picks as the
picker draws them, settling them so they aren't applied again after publish.

* fix(native-chat): one speed rule for picker, host and turns; never-sent needs proof; create keeps held picks

A Codex tier the model doesn't list shows and runs as Standard under one rule shared by the picker,
the host's live reconciliation, each turn and the at-rest answer, which keeps the saved tier for a
model that offers it. An older host's Flex shows as it reports it. A chat counts as never sent only
when no turn, rewind or untrustworthy journal says otherwise. A new chat's create carries held picks
as the picker draws them but keeps them as the user made them until create succeeds, so a refused
create and retry still has the user's own pick, and a newer pick still applies after publish.

* test(native-chat): type the resting journal mock and cut overlapping option tests

One typed resting-journal helper replaces six inline mocks and the broad object parameters the
low-evidence check rejects. Cuts tests the joined flows already pin: the resumed list-or-answer
ordering test, the restart-after-first-send view test, the held effort before a model pick, and
the model-pick and restart loop of the saved-Fast test.

* test(native-chat): cut option assertions the joined flows already pin

Drops the Codex Standard row (the joined tier test pins it), the dispatched-source and literal
effort-choice checks, and the resumed view test restart and model-pick steps; folds the pick table.

* fix(native-chat): an at-rest pick reply and answer name the speed the next start runs

The reply to a pick made at rest now carries the tier the chat model runs, through the same rule as
the at-rest answer, so an older app whose follow-up read fails shows what the next turn sends; the
saved record keeps the pick. A saved tier no listed model offers (a Flex from the user Codex config)
is the agent configured tier at rest too, as the live rule and the picker already read it. Restores
the pin that an accepted Codex reply naming Standard shows with no later read.

* fix(native-chat): a reopened chat with no saved model names its default at once

A reopened chat that never saved a model showed just "Model" until a send started its agent, while
main names the list default straight away. The picker now names the default the host marks for every
chat, as main does. Whether a send carries that default is a separate rule: a new chat sends it, so
its first send pins it; a reopened chat does not, so its next start still runs the agent's own.

* fix(native-chat): a chat with no saved model keeps the model its first turn ran

A chat that never saved a model followed whatever the agent's default was at each start, so a chat
resumed from history or a terminal could quietly switch models after a restart. When a message is
handed to the agent on such a chat, the host now saves the model that turn ran: the message's own
model when it carried one, otherwise the model the agent reports running. A default the host filled
in, no model at all, or a model the agent would refuse as a choice on later sends, is never saved. The write sets the model only while none is saved, inside
the record store's transaction, so a pick made meanwhile always wins and effort, speed and Fast are
untouched. One host step covers Claude, Codex, ACP agents and Pi. No new saved field.

* fix(native-chat): a chat with no saved choices keeps the model and effort its first turn ran

A Codex message that left the model or effort unset was filled from the model list's defaults, not
from what the running thread uses. Once the host saved the model a chat ran, the next Codex turn
carried that model alone and ran the list's default effort for it (a configured xhigh became
medium), and a message carrying only an effort ran the list's default model while the chat saved
the thread's. A Codex message now resolves an unset model or effort to the thread's own, and only a
switch to a different model takes that model's default effort, the same rule the other agents use.

When a chat with no saved model records what its first turn ran, it now records the effort, service
tier and Fast that turn ran too, in the same one-time store write: each only where the chat has none
saved, only from the message's own value or the agent's real report, never a filled-in default, so
a later change to the agent's configured effort no longer pairs the kept model with an effort it
never ran. No new saved field and no wire change.

A Codex follow-up sent while a chat's first turn runs is compared with what that turn actually runs,
so it steers instead of waiting for a new turn just because the host saved the model meanwhile.

Also fixes the delivery loop's optional recording hook type.

* fix(native-chat): key what a child ran by its provider child type

* fix(native-chat): record each option a turn ran where none is saved, and drop what a model pick can't run

A Codex message that only switched the tier set the thread's effort as a lasting override, so a later
switch to a model with no listed efforts was refused; the effort is again written only where the
model brought one or an earlier override needs replacing.

Recording what a turn ran now works per option: a chat that already saved its model still records
the effort it ran, and so on for each option it has none saved for. Tier and Fast come only from the
agent's own report, since a message steered into a running turn runs that turn's, and Fast is kept
only when the agent reports it actually routed on: an off state may only mean Fast is unavailable.

A model pick drops a saved effort, or Fast, that the newly picked model's listed row does not offer,
in the one host step both resting and live picks go through, so no later send carries a choice the
agent would refuse. The saved tier is kept, as before: it runs as the model's effective tier.

* fix(native-chat): save only options a turn really ran, and only ones the chat's model runs

A chat with a saved model recorded the effort its agent's settings named, not the one the turn
ran: with "effortLevel": "high" in Claude's settings and Haiku saved (no effort control), the chat
saved high and every later send was refused. A Codex thread effort its saved model doesn't list
did the same.

What a turn ran now comes from the agent's own dispatch: Claude reports the choices it applied and
the effort the CLI resolved for the model it applies, never the settings level alone; Codex reports
its final request over the thread's own model and effort, and a steer reports its running turn.
This evidence lives only in memory on the dispatch result; agents without it keep using their own
reports.

One admission rule now decides every saved option, inside the ordered record write: under a model
the saved catalog lists, an effort must be one it offers and Fast must be one it replays (Claude
needs positive support). A late record of a turn that ran on another model than the one the chat
now has saves none of that turn's options. A model pick drops inherited options its new model
doesn't run, an explicit pick or a new chat's option the model doesn't offer is refused with the
existing option refusal, and unknown catalogs or unlisted models pass as before. The saved tier
keeps its effective-tier rule, and nothing already saved is cleaned up later.

* fix(native-chat): an attach that founds a chat admits its options against the saved catalog too

* fix(native-chat): Claude and Codex always name what a turn ran, and picks are judged inside the write

The handover type was misnamed, which broke the typecheck. Claude and Codex now attach their
dispatch evidence even when it names nothing, so a Claude turn dispatched before its settings
readback never records the settings effort it did not run; the agent's own report still names the
model and stands in only for agents without dispatch evidence. Recording leaves the saved-model
and set-if-unset checks to the store's one admission rule. An explicit effort or Fast pick is now
checked inside the ordered write, with the same refusal, and only at rest: a running chat's own
menu can offer more than its saved catalog row (Pi's probe menu stops at high).

* test(native-chat): Codex admission tests accept the dispatch's run evidence

* fix(native-chat): a new chat's options are admitted against the folder list its picker showed

* fix(native-chat): a new chat's admission reads the list its picker read, resolved once by the create

* chore: re-run CI against main's formatting fix (#30918)

* chore: re-run CI against main's formatting fix (#30948)

* test(native-chat): explain the seed-options stub host cast

* fix(native-chat): attach keeps its two-argument call when there are no attach options

* Revert "fix(native-chat): attach keeps its two-argument call when there are no attach options"

This reverts commit e633d2065b.

* test(native-chat): create-path attach assertions check the params argument, not the arity
2026-10-20 11:55:34 -07:00
Brennan Benson 36d13f14a5 Background Claude runs no longer start your MCP servers and hooks (#30939)
* fix(claude): keep Orca's background Claude runs off the user's MCP servers and hooks

The model picker and / menu listings, chat naming, and commit message, PR,
branch and Jira text generation each start a short Claude run. Every one but
the / probe loaded the user's MCP servers and hooks, so each run started every
server (one cut off a Notion OAuth refresh and lost the sign-in) and fired
SessionStart hooks.

Every background run now passes --strict-mcp-config and disableAllHooks.
A user's own inline --settings merges into the one settings value Claude
keeps, and their --mcp-config still loads. When Claude refuses strict mode
because an enterprise MCP config is present (which already keeps user
servers out), the run is retried once without the flag.

Tracked in STA-11095 and #30411.

* fix: centralize background Claude generation and retry policy

* fix: rebuild Claude background retries from generated arguments

* test: include branch in generation retry fixture

* fix: preserve user settings arguments in background generation

* test: define retry fixture streams across runtimes
2026-10-20 11:50:07 -07:00
Brennan Benson 52c4e0646b Opening or resuming a Codex chat waits for Codex instead of failing after 30 seconds (#30395)
* fix(codex): opening or resuming a Codex chat waits for Codex instead of failing after 30 s

The handshake (15 s) and thread/start / thread/resume (30 s) had their own deadlines because
the Codex start ignored the host's startup signal. It now honours it, as Claude's start does, so
those requests carry no deadline: the host's startup limits, a close, a Stop or quit end a start
that hangs, and the abort reaps Codex. Calls after the start keep the 30 s default for now: they
run in the session's queue, which a Stop or a close waits behind.

* test(codex): keep the handshake wait test in its own file under the line limit

* fix(codex): the one-shot app-server runner takes the same no-deadline option as the chat connection

* test: accept nullable Codex request timeout in probe fake

* fix(codex): bound direct opens at their caller

* Remove obsolete Codex startup timeout scenario
2026-10-20 11:49:29 -07:00
Brennan Benson 38034167e7 fix(mobile): keep the chat composer's controls inside its row (#28983)
* fix(mobile): keep the chat composer's controls inside its row

The model pill never shrank (React Native items default to flexShrink 0), so a
long model name slid under Mic and Send. The permission pill now shows only its
mode icon, resolved from one shared mode->icon-name table that desktop uses too.

* fix(mobile): resolve the permission icon only when a pill renders

* style: format the permission pill test

* fix(desktop): keep the permission icons a static record
2026-10-20 11:44:35 -07:00