mirror of
https://github.com/stablyai/orca.git
synced 2026-10-31 00:57:45 +00:00
92bbb380a69d74a0cfcc3d0de970787cbcfd1dfc
2337
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
92bbb380a6 |
Sidebar: land the switch-on fixes from the flip branch on main, switch still off (#31466)
* fix(sidebar): land flip-a's switch-on fixes on main, still dark (no flip) The fixes found while the sidebar ran from main's feed, without turning that on: ORCA_SIDEBAR_INPUTS_IS_MAIN stays false, and none of flip-a's stop-gaps come along. - Main stops counting a closed tab's terminal once it was asked to stop and no pane shows it; a failed or refused stop takes the request back, and a host out of contact stays unverifiable. - Main fetches a paired server's rows with the transport's normal bound, so a server slower than 5 s still lists. - A deferred unread raise flips the folder row it was decided for, and a read while the owner check is out cancels it. - Phone: no worktree.ps flash before the feed; a host's last installed feed stands in while a fresh read is out; it highlights its own last-opened workspace and a fold never scrolls; projects file under their project group with the desktop's indent; the collapse spinner shows only after 500 ms; "Reopen to update" keeps the rows on screen. - e2e helpers and journeys for the switch-on suite. * fix(sidebar): keep an owed relay stop marked, clear a thrown stop, and prune pending unread raises - A stop-and-wait that throws takes its stop request back, like one that fails. - A final stop the legacy relay still owes stays marked, since the relay retries it. - Pending unread raises are tracked only while one waits, so the map no longer grows. - The paired-server fallback poll test gives its third fetch a reply and asserts live. |
||
|
|
de7a2d24f9 |
Phones get main's tab order, groups and names after every layout change (dark, layout-owner build only) (#31529)
* fix(session-tabs): a windowless layout-owner server lists and pushes main's tab groups and order With the layout-owner switch on and no window (orca serve, orcad), session.tabs rows came from the snapshot hydrated from the saved session; main's layout was merged in only when a window published. A routed reorder or Move to Split reached main's layout but not session.tabs.list or the phone. Main now merges its layout into the rows it holds on each layout change and before each list, with the same merge a window publication gets. * refactor(session-tabs): keep the windowless merge with main's publisher, under the file's line cap * WIP * fix(session-tabs): every stored tab row follows main's layout, with or without a window With the layout-owner switch on, the one writer of the rows phones read now merges main's layout into every write for a workspace main owns, and main re-merges the held rows after each layout commit on any host. A CLI rename, create or close reaches phones without a window publish, and a row main's layout dropped cannot come back from an older snapshot. A write that main's layout wholly overrides stores nothing, so list calls on settled rows bump no version and push nothing. A pane's own title now counts as a manual name. This commit also untracks a local vitest config and a scratch test that the previous work-in-progress commit picked up by mistake. * test(session-tabs): a rebuild from the saved session and a CLI rename keep main's groups and names on phones * fix(session-tabs): the row writer only reorders and regroups; layout commits add main's new rows With the layout-owner switch on, the one writer of the rows phones read now takes only main's tab order and groups. It never adds or drops a row and never replaces a terminal id, so a freshly started terminal keeps its id and rows the exit filters removed stay removed. After each layout commit, on any host, the held rows take main's new rows, names and order, keep any row main's layout lacks (closes are the close path's job), and pass any added terminal through the same exit filters the window path uses. A main binding that runs nowhere no longer replaces a running terminal's id. Both read main's last projection, so neither adds a session read per write or per workspace. A CLI close still leaves its row until the dropped-leaf hook lands; its test is marked as an expected failure until then. * test(session-tabs): the row writer's order and a commit merge's keep rules, unit by unit * fix(session-tabs): a kept row keeps its group, and an unchanged commit pushes nothing A layout commit's merge placed a row it kept (a tab main closed, until the dropped-leaf hook removes it) by its row id, but groups list a terminal by its tab, so the row fell into the active group. Both the writer's reorder and the merge now place rows through one shared group-placement step keyed by the tab. A commit now pushes only when the writer stored something. The writer skips a write only when it keeps the stored occupant, publication and retirement proofs. A terminal row a commit rebinds also passes the exit filters, a main binding the host saw exit no longer replaces a running terminal, and a binding main made before its terminal connected reaches the rows when it connects. * test(session-tabs): a kept row's place and the no-push commit are separate tests * fix(session-tabs): a rebuild with a new publication stamp is still skipped; simpler commit merge The writer's skip compared publication stamps, but a rebuild from the saved session mints a new stamp from the clock each time, so every rebuild minted a version and pushed. It now stores only for a different workspace instance or new retirement proofs; an absent one keeps what is stored. The commit merge again runs the exit filters only for added terminal rows (they admit any row the held rows list, so a rebind changed nothing), and a terminal connecting now schedules the merge itself rather than a separate check, since an unchanged merge stores and pushes nothing. * refactor(session-tabs): a commit merge pushes after every store again; name the window-path exit test No reachable case was found where a commit merge's rows differ from the held rows while the writer keeps the stored ones, so the extra "only when stored" check had nothing to guard and goes. The exit-rule test now says it covers the window's publication path too. * fix(session-tabs): a window reload's new publication stamp is stored, not skipped The writer skipped a write that main's order wholly undid without comparing publication stamps, so a window reload that republished the same rows under a new stamp kept the old one. The reconciler then saw its accepted stamp missing and asked the window to republish on every sync, and a host-built stamp could stay on window rows. The skip now treats stamps as unchanged only when both are host-built, so repeated rebuilds from the saved session still mint nothing. The windowed test of a row main's layout lacks also replays the window's older snapshot. |
||
|
|
ad269f471c |
A paired desktop shows a server's browser tab without opening a second page (dark, layout-owner build only) (#31509)
* fix(browser): a paired desktop shows a server's browser tab without opening a second page (switch-on builds only) With the layout-owner switch on, a server browser tab drawn from the server's layout reaches the desktop without a saved page handle. Showing it made the desktop create a new page on the server, so the server went from one page to two. Before creating, the desktop now asks the server for its browser pages and adopts the one already named by the tab's page or browser tab id. It creates a page only when the server has none, when the lookup fails, or when the server gives no list. A pane that moved on during the lookup adopts nothing. Switch-off builds keep creating directly. * test(browser): explain the fake runtime call's cast in the page session test * fix(browser): adopt a server's browser page by the id its layout already names, not by listing pages (switch-on only) This replaces the tab-list lookup. A tab list misses saved pages a restarted server has not started, wakes the active saved page as a side effect, and can match a page another client hosts. The server's layout already records the page id on the browser tab. Desktop hydration ignored it whenever the page record had not landed yet, and made up a new id on every frame build. With the switch on, hydration now uses the tab's recorded page id, so the desktop row is named by the server's page from the first frame and stays named by it. A frame-drawn row still has no saved page handle: hydration has none for server-hosted pages, and the server environment is known only to the pane. So, with the switch on, the pane probes its row id with browser.tabShow. tabShow also starts a saved page the server has not opened since a restart. On success the pane stores the handle and streams that page. On not-found, or any other error, it creates a page as before and closes nothing. A pane that moved on during the probe adopts nothing. Why not reorder the server to save the page record before opening the tab: it would only fix the id, not the missing handle, so the probe is needed either way. The page record write also depends on the tab main writes asynchronously, so the order cannot be made certain from the server alone. * test(browser): explain the lifecycle harness's fake runtime call cast * fix(browser): create a server browser page only when the server lacks it, and prefer the tab's active page id The switch-on probe created a page after any tabShow failure, so a timeout or a brief connection error could duplicate a page the server still runs. Now only a page-missing answer creates one. Any other error is raised, as the stored-handle path already does, and the pane shows its stopped state with Reconnect. Hydration now names a tab's page by its recorded active page before its first page id, which matches the page the server starts for a saved tab. |
||
|
|
5dd5294294 |
perf(orcad): keep the server bundle ASCII so V8 stores it at half the size (#31495)
* perf(orcad): keep the server bundle ASCII so V8 stores it one byte per character 129 non-ASCII characters in regex literals (and three in a String.raw plugin template) made V8 hold the whole ~11 MB orcad-server.js source as a two-byte string. Write them as \uXXXX escapes and fail the build if any return. Co-Authored-By: Claude <noreply@anthropic.com> * fix(orcad): keep license comments ASCII and name the module the guard trips on esbuild gathers legal comments at the end of the bundle; escape that comment so a dependency's license text cannot fail the ASCII guard, while licenses still ship with the code. The guard now reports the offset and, from an unminified rebuild, the module the character came from. A dark-switch build is covered. Co-Authored-By: Claude <noreply@anthropic.com> --------- Co-authored-by: Claude <noreply@anthropic.com> |
||
|
|
abce679f88 |
feat(accounts): add Claude and Codex accounts on a remote server from a paired client (#31461)
* feat(accounts): add Claude and Codex accounts to a remote server from a paired client The server runs the login (Claude's pasted-code sign-in, Codex's device code) and keeps the credentials; the client only opens the page and relays the code. New capability-negotiated accounts.hostSignIn* methods; older servers degrade to an update message. Fixes #20009 * fix(accounts): settle host sign-ins by the login's outcome, pin pairing, guard stdin Review fixes: a cancel the login refused still reports the added account; the client pins each flow to its pairing revision; Claude's stdin EPIPE no longer crashes the host; both sides accept only the provider's sign-in host; pasted codes reject whitespace; a host-side supersede says so; Codex cancels its own login handle; opt-in real-CLI test. * fix(accounts): keep an early Cancel, retry polls through the host's window, scan Claude stdout only A Cancel clicked before the host answers the start is applied once it does; the client keeps polling through a short outage until the host would have ended the flow; Claude's link is read from stdout only; a timeout after the code was sent keeps its own message. * test(accounts): name the stdin sender and allow its stream write in the persistence ratchet * fix(accounts): register each Add Account attempt before its first await One attempt per click owns the pairing pin, the cancel flag and the host's flow, so a Cancel during the capability check or the start is never lost; polls retry only lost answers; the stdout forwarder is removed on cleanup. * refactor(accounts): drive the client sign-in as one explicit state machine per server and provider checking -> starting -> following -> settled, with Cancel as a flag the run loop delivers once the host names its flow and re-asks after any answer until the host takes it. A second Add joins the running attempt; a Cancel the server could not be told about is surfaced; retry policy tested. * fix(accounts): retry a lost start answer, and report a cancelled sign-in as cancelled however it ends * fix(accounts): Cancel reaches the server a sign-in runs on, and a typed code stays with its flow The host picker can move while a sign-in runs: Cancel now targets the running server sign-in (or the desktop's own login when none runs), and the pasted-code form is keyed by server and flow. * fix(accounts): run at most one server sign-in per provider, so Cancel has one meaning * fix(accounts): the server sign-in prompt owns its Cancel Cancel for a server sign-in now lives in the prompt bound to that server, which also reappears with its Cancel when Settings reopens mid-sign-in; the header Cancel is only for this desktop's own login. * fix(accounts): hide the desktop-login Cancel while a server sign-in runs; narrow the prompt's status * fix(accounts): keep the desktop-login Cancel in local scope even while a server sign-in runs * fix(accounts): the header Cancel follows the running addition, wherever Settings now points Each Add Account records where it is signing in. Cancel stops that sign-in, on a server or this desktop, and is hidden only where that server's own prompt already offers Cancel. |
||
|
|
c149395c20 |
fix(remote-terminal): repaint an idle full-screen program when a reconnect can't image its pane (#31482)
* fix(remote-terminal): repaint an idle program when a reconnect can't image its pane After an SSH reconnect the pane is repainted from the host's screen snapshot. When the host cannot give one (too large for the replay limit, no buffer, unanswered, overflowed), the pane only printed a loss banner and an idle full-screen program that writes diffs kept its pre-disconnect frame until the user resized. The two places a remote pane declares it has no host image now ask the program to repaint through one choke point: a one-row viewport claim and back, which every TUI honours (Node's tty ignores a same-size SIGWINCH). It respects a phone or another desktop holding the size, skips hidden panes, and puts the row back even if the pane unmounts mid-nudge. Fixes #26887 * test(e2e): idle full-screen program frame after a managed SSH outage * test(e2e): wait for the pane before typing in the idle-frame spec * fix(remote-terminal): pick the redraw height on the host's clamped grid * fix(remote-terminal): wait for the host's grid before putting the redraw row back * test: give transport mocks a connection status for the redraw gate * test(e2e): keep the orcad idle-frame proxy typed for the e2e ratchet * fix(remote-terminal): let a new PTY's redraw supersede a swapped-out nudge * test: keep new transport-mock lines free of type assertions * ci(e2e): route the managed-SSH idle-frame spec to its sources |
||
|
|
bbc8303b47 |
A running agent's resume note is not a sleep, so switch-on builds report it connected (dark, layout-owner build only) (#31485)
* fix(layout): a running agent's resume record is not a sleep in switch-on builds The shared layout load put every pane resume record into leaf.sleeping, so main's terminal index read a live or quit capture as a slept pane and terminal.list reported running agents as disconnected and unverifiable. Records of slept panes (isPaneSleepRecord) now load to leaf.sleeping and running agents' records to leaf.resume; both save to the same sleepingAgentSessionsByPaneKey entry, so the disk format is unchanged. The disk-field census names both holders, and a model rule refuses a pane holding both. * fix(layout): a waking start wakes only slept panes; a recordOnly wake drops either record * fix(layout): main's shutdown reconnect facts read a running agent's resume record as awake |
||
|
|
a58789d0c1 |
fix(mobile): keep a Codex chat addressable under Codex's own idle title on headless hosts (#31460)
On a headless host the mobile status builder treated any title without an agent marker as the shell reclaiming the pane. Codex's idle title is its project name, and since OSC 7501 program status gives the pane a PTY state, a Codex turn with no tool call ended with the hook's transcript address withheld, so the phone showed "Can't find this conversation yet". Only a shell or management title now retires the hook identity, matching the renderer projection. Refs #31136. |
||
|
|
7f21cc91b8 |
fix(codex): Codex copy from a remote pane reaches the viewer clipboard (#31453)
* fix(codex): copy from a paired client's or WSL Codex reaches the viewer's clipboard * test: type the Claude launch fixture |
||
|
|
c7f722bbdf |
fix(pairing): offer only addresses the runtime listener answers (#31447)
The runtime WebSocket listener widens to 0.0.0.0, which never accepts IPv6, yet the Connection address picker and the default pairing address offered global IPv6 interfaces, minting links nothing answers. Filter the interface list by the family of the host the listener binds when serving the network. Reported-by: @fongyuan-2011 |
||
|
|
92732bb0f9 |
fix(native-chat): bring back Working-only-while-held (#30134) without shutting down Claude chat workers (#30577)
* fix(native-chat): a Claude chat worker keeps running after Claude acknowledges its task
Claude's adapter dropped an echoed send from the sends it holds before
reporting the send's answer to the host, and emitting the echo's turn
can publish at once. That publish read the still-pending send as held
by nobody, so it read as "couldn't confirm": orchestration worker-start
then closed the new Claude worker, and every other answer wait and
client saw a one-frame doubt.
A frame's settled sends now stay held until their answers are reported,
when the host's handover window takes them over; a Stop's rejected
sends are released and reported one at a time, as Pi and ACP already do.
Tracked in STA-11160.
* test(native-chat): assert the settled send's dispatch state through the send result
* Reapply "fix(native-chat): Claude, Pi and ACP chats are Working only while the agent holds the send (#30134)" (#30572)
This reverts commit
|
||
|
|
cfdc347bd8 |
Layout-owner build: main shows live terminals that lost their tab, at the same moments the window did (dark) (#31430)
* feat(layout): main places a swept workspace's live terminals; the window sweep only reads (switch-on builds only) The window's activation sweep (first visit, an empty or sleeping activation, a phone wake) now asks main, through its existing terminal.list call, to place that workspace's unshown live terminals before listing them. With the switch on the sweep writes nothing to the window store and counts a pane main names as shown, so it no longer seeds a fresh shell beside a terminal main just placed. Main's "still binding" check also covers a registration in flight. * fix(layout): a sweep's placement pass holds back only its own workspace's first surface (switch-on builds only) * test(layout): switch-on activation-gate and census-call tests read main's listing, not window binds |
||
|
|
505d6719c0 |
Refuse a workspace parent that would close a loop, even when the worktree cache is cold (#29340)
* fix(worktree): refuse a parent loop even when the worktree cache is cold The parent cycle walk read each workspace's current instance from the resolved-worktree cache, which the reparent itself invalidates, so it only knew the two endpoints and accepted A->B->C->A. Read saved worktree metadata instead. * fix(worktree): fall back to locator metadata and tolerate loops saved earlier * fix(worktree): skip another host's locator metadata in the parent loop walk * fix(worktree): read loop-check identities from a read-only, ownership-scoped view The per-host metadata accessor migrates a hostless legacy row into the queried host, so the loop check could adopt another host's worktree and refuse a valid parent. Read one read-only host snapshot instead, counting a row only when its project is registered on that host (the resolved list's ownership rule). * fix: import host-qualified worktree meta helpers once * test: match main's host-targeted lineage write contract |
||
|
|
fc8439e05f |
fix(notifications): a pane with hooks gets its finished banner from its hook turn only (#30620)
* fix(notifications): tie a completion banner to the hook turn it announces, not a 10 s window A title-only working->idle edge (Claude's /btw side question fires no hook) raised a generic "Task complete" banner because the pane's hook row was older than 10 s. On a hook-reporting pane the hook row now decides: a row not yet announced supplies the banner content however old it is; an already-announced row means no turn ended, so no banner. Hookless agents keep the generic title fallback. * fix(notifications): a pane with hooks gets its completion banner from its hook turn only The title lanes (mounted pane and parked watcher) announced a finished task on every working->idle title edge, even on panes whose agent reports turns through hooks. A title spin that ends no hook turn (Claude's /btw side question, Codex's /compact) raised an extra banner that could only be filled with the workspace name and tab title. On a pane the hook lane is tracking, with a settled Done row from the same agent, the title lane now defers before it claims the turn, so the hook lane's banner (with the last message) is the only one. A BEL in the same burst waits on the hook lane's pending Done. Hookless agents, restored rows, hooks switched off and the post-restart re-arm keep the title banner. This replaces the previous commit's announcement memory in the dispatcher. * fix(notifications): hook-owned turn ends settle pane bookkeeping and announce re-entered Stops - A title idle edge that defers to the hook lane now settles the pane's turn, so quitting the agent after a /btw no longer raises a generic "Task complete" banner. - A BEL held while the hook lane's Done is pending is superseded by that Done at fire time instead of being held until the next turn start. - A Stop re-entered after a blocking Stop hook carries its new final message in its identity, so the hook lane announces it instead of deduping it against the first Stop. - Title lanes ask the hook lane whether it can announce the turn end (live, agent evidence, not re-arming) instead of copying its conditions, and fall back when the agent's own hooks are switched off. - The hook lane's non-quiet Done forwards its own snapshot, so its banner content no longer depends on the stored row's age. - The whole-flow test now drives the real pane wiring, the real hook lane and the real dispatcher. * fix(notifications): the host marks each real turn end, so a repeated status can't raise a second banner The renderer told a Stop re-entered after a blocking Stop hook from a restatement by its final message. The message also changes when the listener's cache resets (a session reset, then an idle reminder), so a restated finished turn raised a second, empty "finished" banner. The execution host now classifies each provider's own turn-end event by name and stamps an optional `turnEndedAt` on the status row. Idle reminders, session events, replays, child re-emits and result retries restate it; a turn-end event on an already finished row is a new end only when the agent says it re-entered (`stop_hook_active`). Local hosts and SSH hosts (orcad) mint it in `attachStatusTiming`; the WSL relay mints it at admission and sends it on its envelope. The field is persisted, forwarded over IPC and the paired-server session projection, and the renderer's done identity is `turnEndedAt ?? stateStartedAt` (after the existing `turnCompletedAt` precedence). `stateStartedAt` and every other clock are unchanged. Paired servers that predate the stamp advertise no `agent-status.turn-ended-at.v1` capability; their panes keep the terminal-title completion fallback exactly as before. * test(notifications): type the old-server pane setup in the completion flow test * fix(notifications): a blocking Stop's final banner reaches the user; /compact and quitting an idle agent stay quiet Real-agent QA on the hook-only completion design found three gaps: - A Stop re-entered after a blocking Stop hook could be dropped twice over: inside 1 s of the first banner by the replay guard (a new host turn-end stamp is now a real end), and by main's 5 s per-workspace desktop cooldown. A banner with the same notification id and changed text now replaces the one already shown instead of being dropped; every other cooldown rule is unchanged. - On a mounted pane, a hook-owned idle edge (Codex /compact) no longer claimed a completion, so the burst's BEL went out as a Bell banner. The deferred idle edge now absorbs that BEL like a claimed completion did. - Quitting an idle agent after a title spin raised a generic process-exit banner. On a pane whose hook lane owns turn ends and whose last turn is done, an exit ends no turn; a crash mid-turn still gets its banner. * test(runtime): the projection reference keys on the host turn-end stamp too * test(relay): restore the Copilot retry test while CI isolates a runner error * test: key the hot-path projection reference on the turn-end stamp; relay retry restates the end * fix(notifications): host turn identity per agent session; keep later blocking-Stop banners past main's cooldown - The execution host (local main and the WSL relay, one shared reducer) now tracks which turn an agent session is in: Codex by its own turn id, Command Code by its transcript prompt position, every other agent by its prompt-opening hook. A turn end mints a new end when its turn differs from the last ended one, so a Codex turn woken by agent mail and a text-only Command Code turn get their banner; duplicates, replays, tool reopens and result retries restate it. The record is saved with the status row (validated on restore) and never published. - Main's desktop and mobile notification cooldowns are back to main's code exactly. - A later end in the same done segment (a Stop re-entered after a blocking Stop hook) that lands inside main's cooldown is held, latest only, and sent once after that cooldown through main's unchanged gates; the renderer learns main's answer to anchor it. - One pane check gates every changed pane behavior: the pane's bound PTY runs on a host that stamps turn ends and its agent's continuation signal is verified (Claude, Codex). Every other pane, and an unbound PTY, keeps main's behavior. - A hook-owned idle edge absorbs its burst's BEL for main's full 1.5 s; exits and replacements ask the owner check with the exiting agent; a title spin on a hook-owned pane opens no turn. * test(notifications): run the whole-pane harness through main's shared desktop cooldown, not main's service module * fix(notifications): decide hook ownership from how the host identified each turn end The execution host now labels every classified turn end with one optional disposition beside its end stamp, by the same rule for every agent: identified (an unused prompt opening, a new native turn id, or a verified re-entered Stop) mints a new end; a same-turn repeat is a proven duplicate only when the agent supplies both a turn id and a verified continuation flag; anything else is ambiguous and never mints. Replays and retries restate the decided answer, and local and relay hosts carry an end only within a done segment. The renderer replaces the Claude/Codex pane gate with one evidence decision: identified ends are hook-owned, ambiguous ends keep the title lane's role, and rows without a disposition (older hosts, unclassified agents) keep the previous behavior exactly. A title working edge on a hook-owned pane keeps its turn bookkeeping and only the hook receipt; an owned idle edge settles after the existing max wait, or announces through the title path if the end turned out ambiguous. Idle and working title edges resolve the same owner (the title's agent, else the foreground one). The held later banner for identified continuations is unchanged. * test(notifications): give the whole-pane harness's host event its local connection id * fix(notifications): a settled cancellation ends the open turn, so its late Stop folds as on main A cancel Orca inferred from Esc or Ctrl+C writes a done row with no turn-end event. The host reducer now completes the current turn key on that settled cancellation, so the same turn's late Stop is a repeat (ambiguous, no new stamp) instead of a second identified end that requested a second banner. * fix(notifications): a settled cancellation ends the open turn on every lane, so a relay's late cancel end restates none The desktop restates a WSL relay's end as decided. The relay never learns a cancel the desktop inferred for a non-Claude agent, so Grok's own late stop_cancelled reached the desktop as an identified end and gave a second banner. One predicate now decides both lanes: after a settled cancellation that carried no end, a restated end keeps the shown one, and a child event carrying the cancellation also ends the open turn. The relay's own cancel row is the main agent's, never a child's. * fix(notifications): a relayed done segment shown without its own end keeps showing none The restatement fold tested the previous row's cancel verdict, so the row it left behind (a late plain stop) no longer matched and the relay's next restatement brought its end back. Fold on the row's turn-end fact instead, which the folded row satisfies again. |
||
|
|
ae9bc8d120 |
A slept native-chat workspace reads asleep and hides (#28457)
* fix(sidebar): a slept chat workspace reads asleep and hides * fix(sidebar): a wake during Sleep's teardown keeps the workspace awake * test(sidebar): give the activation-race mock store the chat tab map Sleep reads * feat(native-chat): the host marks the chats an explicit Sleep stopped The fact "the user's Sleep put this chat to rest" now lives on the host that runs the chat, on its session record, instead of in the window. - New runtime capability agent-session.workspace-sleep-mark.v1 and an optional markAsleep on agentSession.sleepWorkspace (strict params, so only hosts advertising the capability are sent it). - The Sleep close decides each chat's receipt inside the same per-session step that stopped it; with markAsleep it writes sleptAt in that step's record write, so a chat is marked exactly when it is in the receipt. - The next send the host hands over clears sleptAt, whichever client sent it; the idle sweep and plain stops never mark. - sleptAt rides the status summary (optional field), its equality, and the retained summary of a closed chat, so it survives restart through the persisted tab index. * refactor(sidebar): read a slept chat from its host's mark, not a window record The window no longer keeps its own saved record of which chats Sleep stopped (explicitlySleptChatsByWorkspace). That record had to mirror every host event that should end it (sends, replaced conversations, renames, profile and WSL moves, removal, wakes during teardown), and each missed mirror was a bug. The host now owns the fact (previous commit). - Delete the record, its schema, store slice, workspace-session field registrations, moves, renames, purges, migrations and hydration, and the renderer clearing hooks. Those files are back to main. - Opening a workspace no longer wakes its slept chats (product ruling): drop the chat Sleep-attempt tokens and their set-active-worktree hook. A slept chat wakes only when a send reaches its host, from any client. - The status bridge mirrors each chat tab's published sleptAt into a store map keyed by the chat's pane key, dropped when its projection unmounts and never saved; a chat whose host runs it again is not slept. - The asleep derivation, hide filter, palette, board and context menu read that map. Explicit Sleep sends markAsleep only to the local host and to hosts last probed with the new capability; a project move never does. * fix(native-chat): preserve host-owned Sleep across fallback and legacy tabs * fix(native-chat): clear Sleep when a new send is admitted * chore: restore incoming documentation after merge hook formatting * fix(native-chat): let admitted sends supersede pending Sleep receipts * fix(sidebar): wait for host Sleep state before showing restored chats * fix(sidebar): retain host Sleep answers when chat projections remount * fix(native-chat): clear Sleep when queued drafts become sends * test(sidebar): cover unknown and known chat Sleep state on the board * test(vault): expect both host status capability checks * test(native-chat): consolidate Sleep behavior coverage * fix(native-chat): commit Sleep wake with send admission * test: pin visibility while chat host is unidentified * test: consolidate workspace sleep regression flows * test: keep status feed regression within renderer boundary |
||
|
|
e2196403c3 |
fix(terminal): realign mobile snapshot residual metadata after query stripping (#31348)
Align plain mobile/legacy residual metadata after snapshot-boundary query stripping; preserve genuinely transformed span metadata. Fixes #9682. Based on the report and suggested correction by @bbingz. Co-authored-by: bbingz <zzb@gxsmjx.com> |
||
|
|
87fe8424eb |
fix(runtime): a server with no window answers its first terminal create, not 'handle stale' (switch-on builds only) (#31391)
* fix(runtime): a server's new terminal keeps answering under its own handle after main's pane adopts it (switch-on builds only) * fix(runtime): keep handle branching on the leaf path; only callers with no leaf fallback read the adopted own handle (switch-on builds only) * fix(runtime): with no window, a terminal's own handle keeps naming it after main's pane adopts it, as before main listed panes (switch-on builds only) |
||
|
|
8bec7323ef |
fix(mobile): preserve Claude chat after renaming its pane (#31358)
fix(mobile): retain Claude chat when its pane is renamed Distinguish a manual title override from a later shell OSC title, keeping transcript identity and live hook selection while preserving exit retirement. test: cover restored mobile chat after manual shell-like rename Co-authored-by: Wooseong Kim <innocarpe@gmail.com> |
||
|
|
d1ed73ba9b |
fix(orcad): refuse new terminals while an update stops the server (R32) (#29834)
* fix(orcad): refuse new terminals between an update's last census and the stop The last census before an SSH or WSL update stops the server now closes the server's terminal admission first. Every spawn lease (acquireWorktreeTerminalSpawn, which both spawn paths hold across the provider spawn) passes the fence, the census waits for leases taken before the close, and an update that leaves the incumbent serving reopens it. A reattach to a session the daemon still runs is still admitted. * fix(orcad): fence-exempt reattaches spawn attach-only; reopen admission when no stop was sent * fix(orcad): stop nothing once the recount's admission fence may have lapsed |
||
|
|
ce71535bbf |
Layout groundwork (switched off): main starts every split and new pane, the window only attaches (#31258)
* feat(terminal): the window's store shows main's pane start status (layout-owner build) A status-only frame changes one store entry and no pane tree, so the pane's view stays mounted. * refactor(terminal): keep the frame sync within its line budget * feat(terminal): every pane main starts shows 'starting' from before it is written (layout-owner build) * feat(terminal): a pane main is starting waits and attaches; a failed start shows why (layout-owner build) Fixes owner-visible row 15: a failed agent or setup start no longer opens a plain shell. * test(terminal): iterate store listeners directly * fix(terminal): an exit that reports no code keeps its pane, as lost contact (layout-owner build) * test(terminal): a pane main started attaches to main's terminal at its own grid * test(terminal): detach census tests assert main's move and frame with the switch on * test(terminal): exited-pane tests assert main's close with the switch on These 8 have failed switch-on since #31130 moved the exit close to main. * feat(terminal): main writes and starts every split of a pane it holds; the window attaches (layout-owner build) The window's split, a startup split with its command, and a CLI split all go through main's terminal.split; main's frame mounts the pane, with or without a window intent. Deletes the window's split wait and withdraw, the split appear intents, and the queued-split skip and cancel. The e2e helper uses the real split. A startup split leaves focus on its source pane. * fix(terminal): the attach-only wait leaves sleeping, resuming and held panes to their restore path * fix(terminal): a split pane attaches to the terminal main bound it to; a taken pane id is refused Also restores the proof that a split closed while it starts gets one answer and leaves no pane or start status behind. * fix(terminal): a window split main cannot start still shows its pane, failed, with why * fix(terminal): check a taken pane id first, keep the start's own error, clear a gone pane's status Three test stores gain the pane-start field the window's store always has. * test(terminal): the admission stub answers whether its pane is still written; a closed pane's failed start clears * refactor(terminal): the rebound workspaces' bindings show through one call in the bindings module * test(terminal): a pane main wrote and the user closed mid-start drops its failed status * fix(terminal): a split main starts is listed to phones and session.tabs without a window's report * fix(terminal): a reconnect shows its pane starting and why it failed; a lost owner race shows no failure; a split no one asked to see keeps the phones' tab * fix(terminal): a kept create another owner won shows no failure; the window attaches to the winner |
||
|
|
046f29b9e5 |
fix(runtime): a phone's Markdown save keeps the route it started on (#31376)
forSave captures the workspace's route (and its pairing pin) once; every read, stat, status.get and write of that save uses it, so a workspace that moves between local and a paired server mid-save never sends the write to the other host unpinned. |
||
|
|
b99552cabc |
fix(runtime): a phone reads and saves Markdown tabs on a headless server (#31101)
* fix(runtime): a phone reads and saves Markdown tabs on a headless server With no window to relay through, markdown.readTab and markdown.saveTab answered renderer_unavailable. Main now reads the tab's file (or its saved draft) from disk and saves with a version compare plus the disk write precondition, in the shapes the window's editor relay returns. With a window attached the relay is unchanged. * Headless Markdown save: keep the draft's disk baseline, honour read-only tabs, re-find the record to clear * Use the disk baseline only for a draft * ci: run the phone Markdown spec's orcad case in the orca serve mode-switch job * Headless Markdown: answer from disk only; a window's saved draft stays its view state Clearing a saved draft wrote workspace layout from main, which no layout command carries. Main now never shows or edits a draft; the desktop's restore check reports a disk change. * Phone Markdown: one path through main and the disk, window or not A paired server's workspace is read and saved on that server with files.read / files.writeIfUnchanged, so it answers from its own disk and keeps the precondition. The window relay (ui:mobileMarkdownRequest and the renderer bridge) is removed. * e2e: a phone saves a Markdown tab the desktop holds unsaved edits in The phone reads the disk with the tab marked dirty; its save lands on disk, and the desktop keeps its draft and shows the changed-on-disk banner. * Group the Markdown disk host's imports * Drop an unneeded store cast from the dirty-readers test * Phone Markdown: refuse a save on a server too old to check the disk; cover the untitled tab A server before #30605 has no files.writeIfUnchanged, so the save is refused with the existing update-required message rather than written unchecked. * Phone Markdown: refuse a file an SSH host holds outside the workspace; pin the untitled answer * Phone Markdown: untitled tabs stay read-only; older servers save like the desktop editor; a closed-source preview still reads - The desktop lists an untitled document's tab with isUntitled (optional, additive); main answers it read-only with unsupported_untitled and refuses its save before touching the disk. - A server without the files.write.disk-precondition capability gets the desktop editor's guarded save: compare from here, then a plain files.write, or a conflict. Every call of a save is pinned to the pairing it probed. - A preview whose source tab was closed has no saved record; it reads its own file, read-only. * e2e: read the untitled tab's answer without a cast * Phone Markdown: pin a save's pairing before its first read A save captures the server's pairing before it reads, and every call of the save, the read included, is pinned to it, so a re-pair between the read and the write is refused. * Test: with no window, a phone saves an existing untitled.md and never creates a deleted one --------- Co-authored-by: E2E Test <e2e@test.local> |
||
|
|
fece03b5ac |
fix(orcad): voice dictation on headless orca serve hosts (#31217)
* fix(orcad): voice dictation on headless orca serve hosts
Mobile dictation against an orca serve host answered speech_unavailable
since serve moved to orcad: orcad never installed the speech service
factories, and the model manager and download imported Electron.
The model download now takes a host request opener (Chromium net on the
desktop, Node https on orcad, with redirects mapped to the same event),
the STT worker ships in the orcad slot, and orca serve hands orcad the
installed app's on-device recognizer. A host without a recognizer (an
SSH or WSL managed slot) lists only cloud models and sends a phone that
picked an on-device model to the setup sheet. Mobile names the fix when
an older server still has no speech at all.
The parity test now requires both roots to install speech.
Reported-by: delorean2020 (@delorean2020)
* fix(orcad): retry Node download drops, stage the recognizer on Windows, review fixes
- Node https errors (ECONNRESET, ETIMEDOUT, ...) are retryable, so an
interrupted orcad model download resumes (Codex P2).
- On Windows orca serve hands orcad a per-version copy of the recognizer
in the profile, so a loaded addon never locks the app's files against
an update (Opus).
- Drop a recognizer candidate path that could never match; one SAFETY
helper per test file instead of repeated casts; comments.
* fix(orcad): cloud speech is ready only with a key this host can decrypt; prune superseded recognizer copies
Codex round 2: a desktop-sealed OpenAI key exists on disk but orcad's
secret store cannot open it, so the catalog reported cloud models ready
and dictation failed at the end. orcad's model manager now proves the
key decrypts. The desktop keeps its existence check (no keychain prompt).
* fix(orcad): never prune staged recognizers before the profile lock; copy linked packages as real files
Codex round 3. ref-oss check: versioned copies are cleaned only by the
process that owns them, so the pre-lock prune is dropped rather than
patched.
* fix(orcad): remove a partial recognizer copy when staging fails (Codex round 4)
* fix(speech): never hand out another store's ciphertext as the OpenAI key (Codex round 5)
* refactor(orcad): no recognizer copy on Windows; Windows orcad keeps cloud dictation only
Design check after review round 5: every remaining finding came from the
Windows copy that dodges addon file locks. Dropping it shrinks the
surface to a path lookup; Linux and macOS serve keep on-device models.
* fix(speech): name the fix when an old server refuses a mic tap; always set the recognizer env; retry ENOTFOUND (final Opus review)
* fix(managed-server): probe install completeness relative to the slot dir
CI: shipping stt-worker.js added one more absolute slot path to the
install-complete probe, pushing it further past the exec-size budget.
Testing each artifact relative to the slot (inside a subshell cd) cuts
the probe from ~12.2 KB to ~3.6 KB, so it drops off the over-budget
list instead of raising a ceiling.
* fix(mobile): pin host.platform's read to a platform type, independent of which zod copy resolves
With mobile's own dependencies installed (which CI does for any PR that
touches mobile/), the e2e program resolves two zod copies, and the
salvaged platform arm from src/shared inferred as {}. The schema now
narrows through an explicit platform guard.
* fix(mobile): keep the host.platform read line intact for its RPC mutant; narrow in a second step
Mobile verify: the recorded-RPC mutant anchors on the read line, which
the previous guard rewrote, so the mutant could not apply.
* refactor(cli): app-executable resolution in its own module; drop the platform guard now zod is aligned
launch.ts went one line past max-lines with main; resolveAppRoot,
resolveForegroundOrcaExecutable, getExecutableAppArgs and
stripElectronRunAsNode move to orca-app-executable.ts (re-exported, so
callers are unchanged). With mobile and root on one zod (#31260), the
host.platform schema no longer needs the extra narrowing step.
* refactor(cli): import app-executable helpers from their module; no re-exports from launch.ts
|
||
|
|
379ad0c5cd |
test(terminal-surfaces): pin why main's pane republish is deferred to a microtask (#31333)
The deferral keeps a terminal's connected flip, noted inside syncWindowGraph's record update, from re-entering syncWindowGraph and recursing. Layout-owner (switch-on) builds only. |
||
|
|
018c6e0c42 |
fix(mobile): create and edit notes on headless hosts (#31291)
Create, read, and save Markdown notes on headless hosts through authorized file operations and persisted editor tabs. Addresses the Markdown-note reproduction in #22186, reported by @shota46. Thanks to @Jinwoo-H (#31101), @brennanb2025 (#25436), and @Meapri (#23357) for related community implementations and regression coverage. Those PRs retain broader/distinct scope. Validation: focused tests, paired-headless E2E, iPhone simulator proof, and all CI checks passed. |
||
|
|
065e6873f6 |
A paired desktop shows the held-terminal banner and Reopen for a server's held terminals (switch-on builds only) (#31275)
* A paired desktop shows a layout-owner server's held panes on the existing banner; Reopen asks the server (switch-on builds only) The server's frames carry status 'held' on a pane a newer build's terminal service may still run. The desktop now lists those panes on its existing newer-build banner, with the same wording and states. Reopen calls the server's new `layout.reopenHeldPanes` under the server's ids: the server stops its newer service, as the desktop's own Reopen does, and starts each pane still held. The desktop starts nothing; the server's next frames clear the status and bind the pane. A refusal or failure shows the banner's failed state. Served and advertised (`layout.reopenHeldPanes.v1`) only by a layout-owner build, to paired desktops (not phones). A server without the capability, or that publishes no status, shows no banner, as today. * test(layout): held server pane stays Reopening until the reattach frame, stays failed after a refusal, and shows nothing without the reopen capability * fix(layout): a held pane's Reopen acts only on its own host and on main's published status (switch-on builds only) - Reopen needs the clicked pane's key; a pane that already left the banner's store does nothing, so a server banner clicked as its frame drops the pane never stops this machine's newer service. - Local versus server is decided by the pane's host, not by whether it has a release. - The banner portal for a pane with no terminal handle exists only in a layout-owner build. - The server reopens only panes its published layout shows held, the status the desktop saw. * test(layout): Reopen ignores a pane rebound to a new terminal whose held entry is still stored * test(layout): the dropped-pane Reopen test checks for the reopen method, since the view report shares the RPC client |
||
|
|
6e589bb4ba |
Layout-owner build: phones see main's terminal and editor rows (A2, dark) (#31276)
* 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): a workspace main owns publishes its terminal and editor session.tabs rows from main's layout; the window still supplies browser, chat and view-only rows until D3b (A2, dark) * chore: lint * test: type the row fixtures * chore(layout): main's session.tabs projection is an old-client entry, imported only by main's terminal surface index * fix(layout): main's session.tabs rows carry the tab's name, the window's agent status and markdown file id, list rows in tab order, and keep groups and selection consistent (A2, dark) * refactor(layout): the projection's group order moves unchanged to its own old-client module, for the file's line cap; lint * feat(layout): every pane status main publishes keeps a row from reading live; a resuming pane is unverifiable like a starting one (dark) * test(layout): a windowless server's layout change reaches a subscribed phone's session.tabs rows (dark) * fix(layout): a window-only row keeps its place in the rows, the window's selected row lands in a group that selects it, and a workspace with no main group keeps the window's groups (A2, dark) * fix(layout): a pane main has no binding for yet keeps the window's terminal in its phone row, so an SSH restore lists it pending (A2, dark; ledger L243) * fix(session-tabs): phones show the window's live selection, focused pane, dirty state and titles over main's saved view; exited terminals stay gone; stray rows join the active group (switch-on builds only) * fix(session-tabs): main's groups stand with main's selection; certified exits only; a selected stray row joins main's active group --------- Co-authored-by: E2E Test <e2e@test.local> |
||
|
|
8e5992663b |
A terminal pane that loses its reconnect to another connection attaches to the winner instead of showing terminal_pane_owner_changed (#30448)
* fix(terminal): a pane that loses its attach to another connection attaches to the winner When a terminal pane reconnected to a saved process that had exited, and another writer re-bound the pane meanwhile (most often the window's own session save), main threw terminal_pane_owner_changed. The window showed it as a raw red banner over a pane that was otherwise working, and nothing cleared it. Main now re-resolves the pane's owner once and attaches to it, with fresh absence evidence even when only the saved binding changed. Retiring a dead id's binding is fenced on its incarnation so it can never clear a newer process that reused the id. A second loss is still reported; the window asks once more for a local pane (never a fresh launch) and otherwise shows the existing reconnect banner with Retry instead of the raw code. Tracked in STA-4139. * refactor(terminal): move the stable-pane attach step into its own file (max-lines) * fix(terminal): fence an absent owner's retirement on an explicit incarnation, not the binding's own field * Complete bounded pane ownership recovery through host publication * Apply repository style rules to pane recovery changes * fix: preserve terminal adoption type contracts * fix: complete terminal recovery ownership and acceptance contracts * fix: keep terminal registration evidence with its candidate * fix: satisfy terminal recovery lint gates * fix: consolidate terminal recovery imports * test: explain bounded terminal fixture assertions * test: handle optional pane binding maps in assertions * fix: scope remote pane ownership refresh to recovery epoch * fix: reject remote pane replies after recovery epoch ends * Fix static checks for pane recovery * Reuse session partition assignment in binding retirement * fix: acquire returned terminal receipt before rejecting early exit * Release superseded terminal spawn preparation before retry * test: type preparation cleanup callbacks as functions * test: initialize telemetry classifier in preparation collision cases * test: identify preparation resources in reentry assertions * fix(terminal): let a recovered pane replace the binding its host proved dead Reopening an agent terminal whose process had exited showed "Orca couldn't verify this terminal's owner", and the next relaunch lost the tab. The pane operation proved the old PTY absent, but a window session save wrote that dead binding back before the fresh commit, and the commit treated it as a rival owner. The refused candidate was then shut down without an intentional-stop mark, so its exit retired the whole tab. The operation now carries its absence proof into the fresh launch, and both the pre-launch owner check and the commit accept a replay of that dead binding. A different or unproven owner is still contention. Discarded candidates are marked as replaced, so their exit keeps the tab. * test(terminal): type the discard test's runtime mocks * fix: expose reconnect for retained pane recovery * test: expect replaced stop for discarded fresh terminals * fix(terminal): type the absent owner's retirement fence after the main merge * test(terminal): follow main's Reconnect handler in the pane binding ownership test |
||
|
|
1c76c71409 | fix(mobile): list session tabs in the desktop tab bar order (#31269) | ||
|
|
5c666d2974 |
fix(native-chat): retry a failed chat name, show the failure on the tab's hover card, and add Regenerate name (#30646)
* 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 * fix(native-chat): retry a failed first chat name twice before keeping the first message A failed name generation was final for the chat's lifetime. The first name now gets two retries, after 2 and 4 seconds, and stops early once the first-message name was replaced. * fix(native-chat): show a failed chat name on the tab hover card, with Regenerate name A chat whose name generation failed after every retry gave no sign of it, and nothing could run it again. The host now keeps the last failure in memory and publishes it with the chat's status while the chat still has the name it failed to replace; the tab hover card shows it with the error dialog, and a new agentSession.regenerateName (behind a capability) makes one more try. * fix: derive chat naming progress from host jobs * fix(native-chat): merge duplicate import CI caught * fix(native-chat): declare agentSession.regenerateName in the cross-version surface manifest * fix(native-chat): stub naming-failure hook in SortableTab rename shortcut test |
||
|
|
70b2197c97 |
The window shows tab, title, move and divider changes only from main's update, never drawing them itself (dark, layout-owner build only) (#31257)
* The window shows each layout records frame record by record, without reading its session back (dark, layout-owner build only) A records frame no longer re-serializes the window's whole session and runs the Loader. Panes' terminal bindings and live titles apply alone, to the tabs the frame names; any other record builds only its own workspace's slices, with the view and facts read from the store. Only a workspace the store does not show from frames yet (startup, a reopened stream, a first visit, one sent whole) takes the full build. Tests install an apply check that, after every flush, compares the store with the full build for the same cache and pre-flush store; the shared layout-apply transport now sends main's records frames and fails a flush that differs. * A frame of only tab rows shows without reading the window's session back or running the Loader (test, layout-owner build only) * The account-restart test's fake binding runtime carries per-pane binding generations, so it passes with the switch on (test only) * The account-restart test types its store writes, so its five old e2e type errors are gone (test only) * A workspace build reads the editor selection and groups through the save's own writers, as the full build does (dark, layout-owner build only) * A tab released from the window's hold shows the binding main wrote meanwhile (test, layout-owner build only) * The frame-apply check still checks a flush's other workspaces when it builds one in full, and the shared transport keeps a test's own check (test only) * The frame-apply check is proven to catch a mismatch beside a full build, and the shared transport restores only the check it installed (test only) * A tab move shows from main's frame alone; the window no longer re-applies it or adopts main's group id (dark, layout-owner build only) On main's answer the window only selects the moved tab in its new group, a view choice the frame does not carry. While main's stream is down it still moves the tab itself, as the frame sync's closed-stream rule says, but under its own group id: the reopened stream's snapshot redraws main's copy. The three switch-on census files for tab moves now pass without a switch mock: the context menu's move routes to the tab bar's move, the drag hook's pane-column split goes to main in a layout-owner build, and the tab bar's own move is pinned on a paired server's workspace. A new frame test pins each move through main and its frame. * A tab's rename, color, pin, view and a pane's title show from main's frame alone; the window no longer applies them again (dark, layout-owner build only) * A divider drag sends main the ratios it moved, not the window's tree, and an in-tab pane move shows from main's frame; the ratio hold is gone (dark, layout-owner build only) * Divider ratio params and their command tests get their own modules (file size) * Layout-writer baseline pruned: the tab move no longer writes the layout from the window * The window never draws a tab prop, pane title or in-tab move itself; while main's stream is down the reconnect's snapshot shows them (dark, layout-owner build only) * View-mode test seeds its paired-server workspace without a type assertion * A moved tab is selected in the frame flush that shows the move, unless the user selected since; main's answer no longer selects it (L120, dark, layout-owner build only) * Frame sync stays within its line budget * Frame sync writes its patches through the moved-tab selection, within its line budget * The moved tab's selection reuses activateTab's own store update, so no new layout writer is added (L120) * Format the tab-move frame test * A tab moved while main's stream is down is selected by the reconnect's snapshot that shows the move (L120) * A generated tab title main gave is drawn by its frame alone; the window writes only titles left to it as today, and the rename test checks the phone graph republishes (dark, layout-owner build only) * A divider ratio names the split it means, and main refuses it where that path holds another, so the window snaps back to main's ratios; a drag back to main's ratio before it answers is sent too (dark, layout-owner build only) * test(layout-owner): tab moves drawn from frames make no window tab or group writes * test(layout-owner): a reopened tab shows its title and color from main's frame * test(layout-owner): the tab bar's preview pin through main ends the preview and counts a use * test(layout-owner): read main's generated-title renames without a type assertion * test(layout-owner): type the sent renames so the title helper reads them without an assertion * The frame-apply check selects the moved tabs the flush selected, so the tab-move tests exit clean and a wrong selection still fails (dark, layout-owner build only) * test(layout-owner): the title batch's live labels are checked in both builds; only the generated title differs * Main keeps a tab's first agent-generated title unless the window's title replaces it, so a second prompt while main's stream is down no longer renames the tab (dark, layout-owner build only) * test(layout-owner): a revealed terminal's tab name is drawn from main's frame, not its reply (dark, layout-owner build only) --------- Co-authored-by: E2E Test <e2e@test.local> |
||
|
|
aec7a4249e |
Layout-owner build: a CLI send, wait or read starts a saved pane that has no terminal yet (dark) (#31307)
* 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) * fix(runtime): a CLI send, wait or read on a saved pane main holds starts it once first (switch-on builds only) * fix(runtime): the caller's handle names the terminal a saved pane starts; terminal.send starts it before its checks (switch-on builds only) * fix(runtime): act on the started terminal's handle until main binds the pane; bound the start by the caller's abort and wait deadline (switch-on builds only) * fix(runtime): a restored pane's start leaves the rest of a wait's deadline and cleans its bounds on first settle; tighter A1c tests * fix(runtime): a restored pane's start is capped by the pending-terminal create bound for every CLI entry point (runtime_timeout) * fix(runtime): only a handle whose saved pane needs a start goes async; every other send, wait and read stays synchronous as before --------- Co-authored-by: E2E Test <e2e@test.local> |
||
|
|
1a0e1e9414 |
PR conflict previews fetch the base branch through the shared fetch controller (#30654)
* fix(github): PR conflict previews fetch the base branch through the fetch controller The conflict summary on a PR card spawned its own `git fetch origin <base>`, outside RuntimeRemoteFetchController: it refetched a base a worktree create had just refreshed (and vice versa), ran git's auto-maintenance in the background, and never armed the idle loose-ref sweep. It now asks the runtime's controller for the exact-base refresh creates already use (same argv, per-remote queue, dedupe and freshness), waiting at most the old 10 s before falling back to the local ref. The runtime registers its controller at construction, since the IPC and poller paths have no runtime handle. Also corrects the controller comment that claimed every fetch skips auto-maintenance: full fetches keep it on purpose (#8039). * test(github): read the mocked fetch's WSL option without a type assertion * test(github): give the conflicting fallback-lookup test a git mock for its conflict summary * test(github): check WSL routing without duplicating fetch policy * fix(git): bound preview fetch ownership without cancelling create * Preserve repository identity after preview cancellation |
||
|
|
d1aeba939f |
Native chat: remove unused journalCheckpoint and setConversationCommand (#30645)
* refactor(native-chat): remove three chat mechanisms nothing drives - The adapter's optional startAnswered hook lost its only implementer in #27925; it was still forwarded through four layers and always read as answered. A start that never answered is read from the child's phase, as the exit already does. - The saved lease's journalCheckpoint is only ever written as null and read only by validation. Records an older build saved with it still load: it joins the retired lease fields dropped at load, so the next write sheds it. - The record store's setConversationCommand had no production caller. The tests that stood in for an older build with it now seed the saved row and reopen the store; three that only re-pinned another test are removed. * test(native-chat): an older build's null journal checkpoint also loads * Keep startAnswered cleanup in its owning PR * Keep historical chat fixtures aligned with their readers |
||
|
|
a9310986cd |
A CLI or phone file on a layout-owner server opens in the tab group a paired desktop shows (switch-on builds only) (#31264)
* feat(layout-owner): a CLI or phone file on a layout-owner server opens in the group a paired desktop shows Each paired desktop reports, through a new layout.reportView (served only by layout.intents.v1 servers, never to phones), the group it shows in each workspace the server's frames draw. The server keeps reports per connection beside the window's, opens files in the latest one's group, and drops a connection's reports when it closes, falling back to the saved view. * test(e2e): a CLI file open on a switch-on server lands in the group a paired desktop shows * fix(layout-owner): layout.reportView refuses a caller with no connection, as sidebarInputs.reportView does * feat(layout-owner): a desktop's re-sent view after a reconnect never outranks another desktop's live report layout.reportView takes an optional refresh flag. The desktop's first report of each workspace after a stream snapshot carries it; the server ranks such entries below every live report, while they still replace the connection's own earlier entries and beat the saved view. * feat(layout-owner): a paired desktop re-sends its view, as a refresh, when its control connection to the server comes up on a new socket The server drops a connection's view reports when it closes, and the layout stream can stay up across that, so the desktop watches the shared control diagnostics for a new socket. * fix(layout-owner): a desktop's refresh keeps its own live rank; stop reporting to a server without layout.reportView A same-connection resubscribe re-sends its view as a refresh, which demoted the connection's own live report below an older desktop's. Ranks are now an explicit live/refresh tier. A pre-PR switch-on server answers method_not_found; the desktop then stops sending for that stream. * fix(layout-owner): a group the server's own frames move is re-sent as a refresh, never a live report The snapshot resend read the store before the sync applied the snapshot, so the flush's group change reached the store subscription as the user's. Writes made by the frame sync now report as refreshes, and the snapshot resend runs after the flush. * test(layout-owner): a change frame's group move goes out as a refresh; the frame-write mark survives nested writes |
||
|
|
6e973fa6d4 |
A launch can name chat or terminal: agent.launch gets a surface pick and the CLI gets --surface chat|terminal (#31191)
* feat(agent.launch): a launch can name chat or terminal, and the CLI gets --surface chat|terminal agent.launch gains an optional `surface` field, read by hosts advertising agent.launch.surface.v1. `terminal` always runs the agent's CLI; `chat` runs a chat even when the user's default is a terminal, and one the host can prove it cannot open is refused with a typed no-effects error before anything is created, so a retry answers with the same refusal. The pick is part of the replay fingerprint. `orca agent launch` and `orca worktree create --agent` accept --surface chat|terminal and refuse it on a host that would drop it, including the legacy worktree.create fallback. * test(agent.launch): pin the surface pick beside the other pre-start failures * fix(agent.launch): settle requested surfaces from resolved workspace facts * fix(agent.launch): describe fallback boundaries and correct test types |
||
|
|
be29644ec9 | fix(mobile): open a worktree on the tab the desktop is showing (#31215) | ||
|
|
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 |
||
|
|
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)
|
||
|
|
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> |
||
|
|
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 |
||
|
|
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 |
||
|
|
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> |
||
|
|
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. |
||
|
|
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> |
||
|
|
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
|
||
|
|
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 |
||
|
|
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 |
||
|
|
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> |
||
|
|
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
|