Repository navigation
1.4.227: filed cherry-picks, P0 fixes and 1.4.225's picks onto 1.4.224 - #29190
Draft
brennanb2025 wants to merge 69 commits into
Draft
brennanb2025 wants to merge 69 commits into
brennanb2025 wants to merge 69 commits into
Conversation
1 of 2 tasks
…26808) * Start AI commit messages from the login-shell environment chats use A Dock-launched Orca only takes PATH from the user's shell, so variables exported in the shell rc (proxy base URL, auth token) never reached the commit-message agent even though chats and terminals saw them. Local host generation now starts from the same cached login-shell base env as chats, with account overrides applied on top; WSL and SSH paths are unchanged. * Prefer the login shell's config directory over Orca's inherited one for commit messages (cherry picked from commit 65b2fcf)
…and proxy fixes (#26801) * Revert "Revert Claude per-account folders (#24434, #26405) before release (#26638)" This reverts commit 9ca9b4e. * Move the terminal daemon protocol to 44 for the re-landed claude function v43 daemons (the revert) lack the claude account function, so new terminals must not reuse them. v42 and v43 stay attachable. * Move the local build compatibility contract to daemon protocol 44 * Point the old-terminal test at daemon protocol 44 * Claude accounts re-land: never sign anyone out, keep proxies working, accounts mirror ~/.claude (#26769) * Run a Claude account on System default until it has its own login An account saved before per-account folders has no login in its folder. While System default (~/.claude, or the user's own CLAUDE_CONFIG_DIR) is signed in to the same email, the router sends every launch there and writes that choice into the pointer file, so nobody is logged out by the update. Once the account signs in to its own folder, the next launch uses it. The WSL router follows the same rule with the guest's ~/.claude. * Refresh each Claude account folder from ~/.claude before every launch ~/.claude (or the user's own CLAUDE_CONFIG_DIR) is now the master copy. Each account folder gets its whole settings.json, including the proxy address and its key, and every .claude.json key except the login, Claude's account caches and install ids, so first-run questions are not asked again. Every other top-level entry is linked (folders) or copied (files), except Claude's per-folder daemon, jobs, live sessions, login files and throwaways. sessions is no longer shared, and an earlier link is undone. The refresh runs before every Claude Orca starts, for each new terminal and on account switch. Orca never writes back to ~/.claude. The ledger that tracked selective sharing is gone. * Route chat auth and unselected-account usage through the Claude router Chats decided whether to drop inherited Anthropic auth from the selection alone, so an account running on System default lost the user's own key. They now ask the router, as terminals, AI commit messages, automations and usage already do. Usage for an unselected account is read where its launches would run. A test reads every entry point through the fallback and fails if a launch path outside the router reads the Claude selection or builds an account folder. The router's two launch errors move to their own file to keep it under the line limit. * Show Claude account sign-in prompts only when an account cannot run With the fallback, an account System default is signed in to already works, so: - The one-time "Finish setting up your Claude accounts" toast is gone. - Accounts System default covers no longer need a sign-in: no Sign in in the status bar menu or Settings, and they can be selected. - The old-terminal banner shows only when claude in that terminal would run a different account than the selected one, and asks again after a switch. Its wording is unchanged. The retired toast's saved flag stays in the paired-client schema so an older client's write is still accepted. The email comparison moves beside the login reader to keep the router under the line limit. * Keep the user's shell Anthropic auth on Claude account launches A signed-in account's launches used to drop ANTHROPIC_API_KEY, ANTHROPIC_AUTH_TOKEN and CLAUDE_CODE_OAUTH_TOKEN from the inherited environment while ANTHROPIC_BASE_URL passed through, so a proxy set in the shell got the subscription login and answered 401. Host launches on an account now keep the shell's auth with its address, as System default does. Chats still name the account kind in a failed sign-in, now from the router rather than from whether auth was dropped. * Share from ~/.claude only what Claude itself would seed into a new folder The deny list missed much of Claude's runtime state, so tokens, locks and live files such as .session_ingress_token, server-sessions.json and remote-control were copied or linked over each account's own every launch. The list now mirrors the one Claude uses when it seeds a fresh config folder from ~/.claude: its runtime entries, every hidden entry, daemon files and agent memory. teams, ide, downloads and scratch are on it, so they stay per folder. * Merge trusted folders and MCP servers into an account instead of replacing them Each refresh replaced the account's projects and mcpServers with ~/.claude's, so folder trust and allowed tools recorded only in the account were lost, and a refresh landing between Orca's folder-trust write and Claude's start brought the trust dialog back. Both are now merged per folder path or server name: ~/.claude wins for entries it has, the account keeps the rest, and a folder trusted in the account stays trusted. Both writers take Claude's lock on the file, so the order no longer matters. * Require the same organization before System default stands in for an account The fallback compared emails only, so one email saved for two organizations made both accounts run on whichever organization System default was signed in to. When the saved account and System default's login both name an organization, it must now match too, on the host and in WSL, and the old-terminal banner uses the same comparison. The saved-account lookup the three checks repeated is now one helper. * Treat v42 terminals as having the claude account function v42 daemons shipped the same claude function and pointer path as v44, so claude in those terminals already follows the selected account. The old-terminal banner now shows only for daemons before v42 and for v43, the revert, which dropped the function. * Fail the routing test when a new module resolves the claude binary The census only caught code that read the Claude selection, not a new launcher that runs claude without asking the router. A second check now lists every module that resolves the claude binary or loads the Agent SDK, each with why it is routed, and fails on any new one, or on a listed one that no longer matches. * Skip the account refresh on launches that run on System default A launch that falls back to System default never uses the account folder, so starting a setup for it on every launch was wasted work. Account switches still set the folder up for a later sign-in. * Give a terminal opened before the login shell's env only the account pointer At startup, a pane could be routed before the login shell reported the user's own CLAUDE_CONFIG_DIR, so the fallback compared against ~/.claude.json instead. Until that env arrives a pane gets only the pointer, and the pointer is rewritten once it does, so claude typed there reads the right folder. The recent claude --version answer moves beside the version probe to keep the router under the line limit. * Drop the chat-only Anthropic auth stripping and override refusal Chats no longer drop the shell's Anthropic auth on any account, so the chat launch's override refusal and the policy's stripAuthEnv could only be reached from test fixtures. The structured auth policy is now just which login a failed sign-in names. Terminals keep their refusal, which still guards WSL launches; the failure reason stays in the shared vocabulary for older peers. * Publish a new account state file whole The first .claude.json write went straight to the target, so a crash mid-write could leave Claude a truncated state file. It is now written to a staged file and linked into place without replacing one Claude created meanwhile. * Read an unselected WSL account's usage where the WSL router would run it Usage for an unselected WSL account read its saved folder directly, so an account the guest's ~/.claude covers showed no usage, and the read bypassed the router. It now asks the WSL router, after the same check that the distro is already running. * Keep the shell's Anthropic auth when the claude function runs an account The claude shell function (POSIX, fish and PowerShell) unset ANTHROPIC_API_KEY, ANTHROPIC_AUTH_TOKEN, CLAUDE_CODE_OAUTH_TOKEN, AWS_BEARER_TOKEN_BEDROCK and auth-like ANTHROPIC_CUSTOM_HEADERS before running Claude in an account folder, so a proxy set in the shell lost its key while keeping its address. It now only sets CLAUDE_CONFIG_DIR and Orca's marker, as on System default. Protocol 44 is unreleased, so no new bump; the shell-wrapper snapshots are regenerated. * Never drop the user's Anthropic auth from a Claude launch Orca's launches stripped ANTHROPIC_API_KEY, ANTHROPIC_AUTH_TOKEN, CLAUDE_CODE_OAUTH_TOKEN, AWS_BEARER_TOKEN_BEDROCK and auth-like ANTHROPIC_CUSTOM_HEADERS for WSL terminals, terminal splits and through the preparation's stripAuthEnv flag, and refused a launch whose agent env set them. If the user's shell overrides the login, that is their setup: every launch now keeps it, on accounts, System default and WSL alike. The flag, the strip paths and the override refusal are gone; the refusal's reason stays in the shared vocabulary so older peers still decode it. Orca's own process env and the claude --version probe still drop these variables: they are Orca's, not the user's launch. * Leave out of an account's state everything Claude resets when it signs out The .claude.json copy left out the login and keys matching cache patterns, but still carried account state such as additionalModelOptionsAnsweredAt, artifactRosterDenied, lastSeenOrgDefaultUpdatedAt and the subscription notices. The copy now leaves out the exact keys Claude resets on logout, beside the install ids, Console API key and first-token date; the cache patterns stay for account caches logout does not reset. Onboarding is still copied. * Judge an old terminal's Claude banner by the pane's own runtime The banner compared every old pane with the host selection and showed whenever only a WSL account was selected, so old host panes showed it with nothing to say and old WSL panes were judged by the host's account. The pane now names its runtime: a host pane is compared with the host selection, a WSL pane with its distro's selection through the WSL router, and a pane whose runtime is unknown, or with no account selected for it, stays hidden. * Leave an account's settings file alone when only Orca's hooks differ from the default home's * Reread a WSL account's Claude state only when the file changes * Move tests merged from main onto the account-only Claude auth policy (cherry picked from commit 8b2a5e4)
* fix(native-chat): the host owns the send queue; the window keeps no saved outbox The desktop kept each structured chat's unsent messages in localStorage and sent them in order, so one message whose fate was unknown froze every later send, Retry dropped it silently, failed sends could not be discarded, and an offline chat could send hours later. The host already records every message and owns the queue; the window now only sends. - One in-memory sender for composer, launch prompts and messages sent from outside the chat. One send in flight per chat; a transport failure resends the same id for up to 30 s; a refusal that proves nothing was recorded puts the text back in the composer with the reason; a send that went out and was never answered shows an in-doubt line with Send again and holds nothing up. - The host's "unknown" rows get the same in-doubt line and Send again, from the journal, in every window. - A message an older build left in localStorage is never sent: the host's conversation outline decides what goes back to the composer, and the copy is deleted once that is saved. * fix(native-chat): send through the structured chat RPC wrapper, with its timeouts The sender called the runtime RPC directly, skipping the per-method timeouts every other structured chat call gets. Only a remote host's request skips the compatibility check the sender already ran. * fix(native-chat): an unconfirmed send goes back to the composer, with no new row line The common pattern draws nothing extra on a message whose delivery is in doubt once its turn is over, and puts a failed send's text back in the composer with the reason. So a send nobody answered in time, or one a host answers in a way that proves nothing, goes back to the composer worded as unconfirmed, and a host "unknown" row shows nothing extra. Removes the in-doubt phase, Send again, and its strings. A host's made-up row for an id its journal lost now reads as unconfirmed, not as recorded, so that message comes back instead of vanishing. * test(native-chat): pin that nothing resends a send given back as unconfirmed * fix(native-chat): sends survive a tab close, never resend after a Stop, and keep remote images - A normal tab close lets sends on their way settle; what the host never took goes back to the conversation's draft. Only a cancelled launch or a worktree purge drops them. - After any Stop, a send already on its way is never sent again under its id: a doubtful answer, or a resend that was due, hands its text back worded as unconfirmed. - A returned image keeps the SSH connection it lives on; the connection never goes to the host. - An older build's saved message the host recorded and then rejected is left to the host's own row, never handed back. * fix(native-chat): a Stop or a tab close never resends a send already out, and a kept card is the card's A Stop that landed while a same-id resend was being readied (its timer fired, its request not out yet) let that resend go out after the Stop. The sender now tracks whether a request is awaiting its answer: a Stop hands back every send between attempts as unconfirmed, lets one whose request is out settle from its answer, and never issues a request after it. A normal tab close withdraws the same way instead of doing nothing, so nothing more goes out and nothing is dropped. A send the host rejected but kept as a card (keptAsQueuedMessageId) is the card's, from its reply or the journal, and never goes back to the composer. Adds the freeze tests: a send whose fate is unknown holds later sends no longer than its deadline, and one the host can neither confirm nor deny releases the next at once. * fix(native-chat): read a resend's turned-away call or reused id as unproven Ports the send-answer proof contract. A call the host turned away before running it (method_not_found, invalid_argument, unauthorized) proves only that this request wrote nothing, so it reads as never sent on a first attempt only; on a resend an earlier attempt may have landed, and it goes again under the same id. A resent id the host says was already used for other content (messageIdReused) proves nothing either, like an expired or conflicting id. Pins the rest of the contract: any row the host returns is its own, a thrown error is no answer whatever its code, and an older build's refused, held or outlived-Stop copy is handed back once and never sent. * refactor(native-chat): type the structured composer's send with its attachment type Keeps NativeChatStructuredSession.tsx within the file length limit. * refactor(native-chat): drop the kept-card guards the sender never needed A kept send's row is a rejection that was never a Stop's, so the sender already reads it as recorded, from its reply or the journal. The test that pins it stays; the two extra checks only covered a kept row that is also a withdrawal, which the host never writes. * test(native-chat): route launch tests' sends through the client wrapper the sender calls The sender sends through callStructuredAgentSession, but these launch tests replaced that module with a factory that answered nothing (and still named a probe that no longer exists), so every launch prompt resent until its 30 s deadline and the tests timed out. Each factory now hands sends to the runtime RPC mock the tests already answer, and expectations of a local send no longer ask for the remote-only compatibility option. * fix(native-chat): hand a message back without importing the composer's attachment hook The worktree purge reaches the launch prompt, which hands text back, and the attachment hook's imports reach the store. A test that builds the real store behind a mocked one then waited on itself and hung. Handing back now writes images to the draft store directly, as the hook's helper did, and a test pins that each returned image keeps its SSH connection. * fix(native-chat): one send per chat, with no line of sends behind it A chat with a send out took further messages into an in-memory line and sent them one by one. A message typed behind one in doubt then hit its own 30 s deadline and came back as not sent without ever going out. Now, as the common pattern does, a chat takes one send at a time: while it is out, Send is disabled and Enter leaves the text in the box. The sender refuses a second send instead of lining it up, so the 30 s deadline always runs from the send itself. A Stop or a tab close stops the one send: settled from its answer if its request is out, handed back as unconfirmed between attempts, or silently if it never went out. Notes sent from outside the chat while its send is out stay with their sender (not ready); a launch prompt that meets the person's own first message waits in the composer instead of being lost. * fix(native-chat): give a Stop-withdrawn note back to its chat once its notes were cleared Notes sent from outside a chat clear once their message is recorded. A message the host recorded as pending and a Stop then withdrew came back only to its sender, which had already let go of it, so the text was lost. The chat's draft now takes it, as an earlier build's outbox did. * test(native-chat): type the composer-actions probe without a cast * chore(native-chat): drop outbox wording left in comments and an empty locale group * fix(native-chat): pace failed checks, keep the host's reason, and hold the chat for its launch prompt - A send whose checks failed before its request went out (an unreachable or incompatible host, an unreadable history) was tried again at once, about a thousand times a second for 30 s. Attempts are now paced by the attempts made, whether or not their request went out. - A host that refuses every resend by throwing (native chat turned off, a journal that won't open) gave back only "couldn't confirm". The host's reason now comes first, still without claiming not sent. - A launch's prompt now holds the chat's one send from the click, drawn as sending, so a message typed while the chat starts can't overtake it; it goes out once the chat exists, and a cancelled launch frees it. - Notes whose send nobody could confirm say so instead of "did not accept", and notes launched into a new chat let go of their text once that chat's composer holds it. - An open chat keeps drawing a recorded send until its row arrives, so a reply that beats the history no longer makes the message flicker out. * fix(native-chat): hold a chat's sends until it has started, and send a failed chat's message with its restart A message sent to a chat still starting went out at once to a host that had no record of the chat yet, read for a fence it could not get, and came back as not sent. A message sent to a chat whose start failed restarted it but no longer went with the restart. While a chat starts, Send stays off and Enter leaves the text in the box, as with a send already out. A text message sent to a chat whose start failed restarts it and goes as the restart's first message, through the same staged-prompt path a launch prompt takes: it holds the chat's one send slot, drawn as sending, until the chat exists, and comes back to the composer if the restart fails again. Notes wait while a chat starts and ride a failed chat's restart, keeping their text until it is sent. * chore(native-chat): test the queue request, rejection words and card hand-offs; drop an unused clear - Pin which sends ask the host to queue, the moved rejection wording, and that a queue send whose card was handed off and then refused or withdrawn, or whose replay names a withdrawn card, is never handed back. - The send-at-most-once gate now says what the desktop promises after a reload: it never sends the id again, and hands the text back. - The legacy read names when it goes, and drops the notice clear nothing called. * fix(native-chat): keep a sent launch prompt's entry, and give back at once what can't go out - Cleaning up a launch prompt released its send slot even after the prompt had gone out, which deleted the entry the sender keeps once the host records it. A launch prompt recorded as pending and then withdrawn by a Stop was lost from both the chat and the box, and an open chat dropped the new chat's first message before its row arrived. The slot now gives back only a reservation that was never sent. - A send stopped before its request went out by something trying again won't clear (this client and the server can't talk, or the host refused the history read) kept Send off for 30 s and then said Orca couldn't reach the agent. It now comes back at once with its own cause: not sent, since nothing went out, or unconfirmed if an earlier attempt did. Transport errors keep the paced retries. * fix(native-chat): notes keep their own text through a new agent's launch Notes sent to a New agent whose start failed came back to the notes and also sat in the new chat's composer, so sending both delivered the text twice. The notes keep their text until it goes out (they hold it from the click), so the launch now says so and no composer gets a copy, on a failed start or a refused prompt alike. The tests that asserted the copy in the composer pinned the old double ownership and now assert the notes are its only owner. Notes that rode a failed chat's restart and ended unconfirmed now say Orca couldn't confirm them, as notes sent directly do, instead of that the agent did not accept them. * chore(native-chat): the send-once gate says what the desktop keeps across a reload The desktop keeps a send's id in memory only: a send still unsettled at a reload or crash is not resent and not handed back. The coverage notes no longer credit the renderer tests with durable identity across a remount. * fix(native-chat): say once why notes sent to a new agent did not go Since notes keep their own text through a new agent's launch, a prompt the host refused, nobody could confirm, or that found the chat's send taken came back to the notes with nothing said anywhere: the chat shows no notice for text its caller keeps. The notes menu now reports it once, with the toast a send to an existing chat already uses: not accepted, couldn't confirm, or not ready. A start that failed still says so in the new chat instead. * fix(native-chat): retry a history refusal the host says clears, and name it at the deadline A send stopped before its request went out came back at once for any refusal of the history read, including ones the host names as clearing (its journal briefly unavailable, a chat detached while the host quits), so the automatic retry was lost. Only a version mismatch and refusals that won't clear come back at once now; the rest go again on the paced schedule, and if the deadline still finds nothing sent, the words are the host's refusal rather than Orca couldn't reach the agent. * feat(native-chat): a send makes one request, and nothing ever sends it again The desktop resent a message under its own id for up to 30 s when the answer was lost. A send now makes exactly one request, as the common pattern's clients do: - An answer that is lost, dropped or proves nothing hands the text back at once with "couldn't confirm… check the chat". - A failure before the request goes out (the environment check, the history read for the fence, a refused connection) means nothing went out: the text comes back at once with its own reason, or as not sent. - The 30 s cap stays on the one request, so a host that never answers can't hold Send. Nothing ever resends, so nothing can go out after a Stop: a Stop only takes back a send still in its pre-send checks. The resend state goes with it (tries, generation, awaiting, stopped, the resend timer, the send-answers-proof probe, and the first-attempt/resend split in the evidence), and the send-once gate says the desktop never resends. * fix(native-chat): notes keep the chat's line, and a send held behind a rewind says it was not sent - Only a send whose text belongs to the chat's composer clears the chat's line. Notes sent from outside the chat no longer wipe a "couldn't confirm... Check the chat" line that explains text already back in the box. - A send the host turns away behind a rewind it could not confirm went back as "not sent" but said "couldn't confirm what happened. Check the chat". It now gives the rewind's reason and says the message was not sent. - Reliability gate names the single-request test and records a fresh evidence run; the sender test drops its leftover resend mocks and a duplicate Stop test. * fix(native-chat): a send ends even when putting its text back fails If writing the returned text into the chat's draft threw, the send never settled: it stayed "sending", the chat refused every later send until a reload. The send now always ends after a hand-back, the failure is logged, and the chat's line adds "Couldn't save your message." * fix(native-chat): a message typed during /clear stays in the box and follows the chat A /clear moves the chat to a new conversation, and its host refuses any send while it runs. A message sent then went out, came back refused into the old conversation's draft with its line, and vanished when the chat moved on: the box and the line now belong to the new conversation. - A /clear holds the chat's one send slot while it runs: Enter does nothing, Send shows busy and the text stays in the box, as for a send that is out. Notes sent from outside get "busy". - Once it moves the chat, the old conversation keeps taking no send until the view leaves it, and what is left of its draft moves into the new conversation's draft, after anything there: when the composer's /clear settles, and again when the view moves. * fix(native-chat): no "Send message?" or queue clear while the chat's send is out While a send is out (or a /clear runs) the chat takes no message, yet Enter over a held queue still opened "Send message?", and Clear queue deleted every card before its message was refused. Enter now does nothing there and the text stays; Clear queue re-checks and deletes nothing if a send went out after the question opened. * refactor(native-chat): take the /clear draft carry out of this change Moving the old conversation's draft into the one a /clear replaces it with fixes a bug main has too (text left in the box during a /clear stays with the old conversation), so it goes in its own change. Kept here: a /clear holds the chat's send slot while it runs, and the conversation it moved away from takes no send until the view leaves it. * fix(native-chat): a /clear's hold on sends always ends - A view that unmounted while a /clear that moves the chat was out left the old conversation holding its sends until a reload: the late reply kept the hold for a view that was gone. The reply now releases it. - The local call for a conversation command has no deadline of its own, so a /clear that never answers kept Send off for good. The hold now also ends at the command's deadline (195 s, the one the remote call already uses), whichever comes first. * test(native-chat): opening a chat an older build left stuck; hand back its copy in send order Pins, through the real chat hook, sends, legacy recovery and draft store, what opening such a chat does: the queued messages behind a message the host recorded in doubt come back to the composer once, in order, with the "not sent" or "couldn't confirm" wording; the chat is free to send under its read's fence; nothing happens while the chat has no fence here. The legacy reader now hands entries back in the order they were sent (queuedAt), as the older build's reader did, instead of array order. * fix(native-chat): a send this window turned away for a re-paired server comes back as not sent A managed server's update rotates its pairing, and this window's main process then answers the next call itself, before forwarding anything, with runtime_environment_changed. The send read that thrown answer as proving nothing, so the person was told Orca couldn't confirm a message that never left. It is now handed back as not sent, with the reason. Every other thrown answer still reads as unconfirmed once the request may have gone out. * Save a new chat's first message during create * Align opening-message types with the user-message wire * Keep chat publication and cleanup outside store initialization * Complete the create refusal fixture ledger * test(native-chat): prove a created first message waits for the agent's start and is handed over once * refactor(native-chat): keep the host and session hook under the line limit after the main sync * test(native-chat): a chat created with its first message and no pick still teaches its agent's default After the main sync, a created chat's agent starts from the delivery loop rather than the create call. Grok (through the real ACP adapter and model catalog) and Claude (through the real adapter) each still report the model their own config resolved to once the chat reads its options. (cherry picked from commit 573ff0b)
* refactor(native-chat): one rule for what a no-pick session says of its configured default ACP's rule (only a new session that restored no model pick and was sent none runs its config's default; an effort it picked says nothing) moves beside the catalog entry types so other agents' adapters report through the same step. An empty listing (a signed-out agent) now says nothing instead of "no listed model", so it can never retire a saved default. * feat(native-chat): check Pi's project config before naming its default model Pi reads `.pi/settings.json` in the chat's folder and loads project extensions from `.pi/extensions` (and the `extensions`/`packages` lists). A settings file that sets defaultProvider, defaultModel, defaultThinkingLevel, modelThinkingLevels or enabledModels, loads extension code, or can't be parsed counts as the project picking the model; extension code may register providers or set the model itself. Key names come from Pi 1.0.4's own settings manager and docs, read without running Pi. * feat(native-chat): a new Pi chat names the model and thinking level Pi starts on Pi's model listing marks no model as the one it runs, so a new Pi chat with no saved pick showed "Model" and then changed to the model its session reported. A new Pi session (not one resumed or forked from a saved conversation) that restored no model pick and was sent none now reports the model and, unless it picked one, the thinking level it started on, through the same host step as Grok, OpenCode and OMP. The host saves them as the account's default behind the project-config check, so the next new Pi chat names them on its first frame. A signed-out Pi, which lists no model, saves nothing. * fix(native-chat): a newer, coarser effort menu keeps the effort a chat ran The saved list takes each model's effort menu from the newer listing. Pi's `--list-models` menu stops at high, while a chat's own menu adds xhigh or max for models whose thinking map allows them. After a background re-list, a learned xhigh was no longer in the menu, so the first frame dropped it. When the configured effort is only in the other listing's menu, that menu is kept. * fix(native-chat): Pi teaches only the model and thinking level it started on A Pi chat re-derived its configured default from Pi's current state on every options read, so a model an extension switched to mid-chat (extensions can call setModel) was saved as the account default, and the next new chat named the wrong model. The choice is now computed once, after start-up restores the chat's saved picks, and every later read reports that snapshot. A pick made in the chat no longer matters to it. Tests cover an extension switch, a pick made in the chat, a forked chat, and a learned xhigh surviving a newer re-list. (cherry picked from commit 8defd66)
* Open a chat visual in its own tab instead of the sidebar * Keep chat visual tabs alive across tab switches and finish editor-tab consolidation - Render chat visual tabs from a worktree-level retained overlay so switching tabs or moving the tab to another split never reloads the page. - Reopening a visual focuses it in whichever split holds it, takes its newest title, and opening from the floating panel no longer takes the main window's editor selection (check-details opener uses the same helper). - Visual tabs return to the reopen-closed stack and show in the tab palette. - One editor-family list and one virtual-tab predicate: no Copy Path, Copy Relative Path, Reveal or git status decoration for check-details and chat visual tabs. * Give chat visuals one open path and re-read them when shown - Reopen-closed (Cmd/Ctrl+Shift+T) of a chat visual goes through the visual opener, so it focuses the visual in whichever split holds it and keeps its id when a runtime environment is active. - A visual tab re-asks its host each time it is shown or reopened; an unchanged revision keeps the page as is, a changed one shows the new page, and an unavailable tab recovers. - The tab palette no longer shows or matches a virtual tab's internal id; the new-tab search shows the chart icon for visuals. - Cmd/Ctrl +/- uses app zoom and Alt+Z leaves word wrap alone while a virtual tab (CI details, chat visual) is active. - A tab dragged over a visible visual passes through the frame, using the existing drag passthrough. * Slim the chat visual tab back to the minimum Remove the retained overlay, the visibility re-read, the zoom and word-wrap rerouting, palette inclusion, reopen-closed routing and the check-details opener change. The visual tab renders through the editor area again, stays off the reopen-closed stack, and keeps: one tab per visual across splits, no main-window selection change from the floating panel, the newest title on reopen, and no Reveal or git status letter on virtual tabs. * Re-read an open chat visual when it is opened again; no watcher for virtual tabs - "Open in new tab" on a visual whose tab is already on screen in another split now re-asks the host, so a rewritten visual shows its new page. - CI details and chat visual tabs no longer start a workspace file watcher. - Drop the unreachable chat-visual case from the restore-time transient check, use the shared virtual-tab check in the header path, and say why visuals stay off the reopen-closed stack. * Let tab drags pass over chat visuals; hide path copies on virtual tabs - A chat visual's frame joins the existing tab-drag pointer pass-through, so a tab dragged over a visual (in its tab or inline in a chat) can drop or split there instead of the frame swallowing the drag. - The tab menu no longer offers Copy Path / Copy Relative Path for CI details and chat visual tabs, whose path is an internal id; no stray separator. * Reuse the hover icon for editor tab icons, so a chat visual shows its icon in both (cherry picked from commit 2356baa)
…at mode switch in the upgrade tip (#26915) * Keep Chat UI users who opened new tabs in the terminal on the terminal Before the chat upgrade, Experimental Chat UI had a separate Default view choice, and it defaulted to Terminal. The upgrade retired that choice so Chat UI on now means new agent tabs open as chat, which moved people who had kept the terminal default into chat with no obvious way back. On load, a profile saved by a pre-upgrade build with Chat UI on and Default view not set to Chat now gets Chat UI turned off, and is left out of the one-time native chat upgrade tip. The old key is stripped on the first save, so the migration runs once and a later opt-in sticks. * Update the saved-settings test for Chat UI on with Default view Terminal * Show the upgrade tip to terminal-default users, with a switch to turn on chat mode People who had Chat UI on with new tabs opening in the terminal now keep the terminal on upgrade, but they still need the tip's directions for moving between chat and CLI. They now get the same upgrade tip, plus a note that new agent tabs still open in the terminal and a "Turn on chat mode" switch. The switch shows when Chat UI is off as the tip opens and stays put while it is flipped. The tip is taller in that case so nothing scrolls. * Decide the chat mode switch from the saved tip audience, not the live setting The upgrade tip's 'Turn on chat mode' switch showed whenever Chat UI was off when the tip opened, so a chat-view member who had turned Chat UI off also got it. Main now answers with one tip variant (none, standard, keep-terminal) from the saved audience, and only keep-terminal shows the note and switch. Also moves the feature tip panel styling into a DialogContent surface variant and drops the Label restyle, and tests that the switch stays put after flipping. * Let the upgrade tip decide the chat mode switch from its variant The modal now passes the saved tip variant to the dialog, which decides the switch itself, so the dialog tests cover that decision. (cherry picked from commit 5ffc8d7)
…6780) * Let a Codex chat stop a command it left running in the background A command Codex started and left running (a dev server, a watcher) showed in the background-tasks strip above the composer with no Stop, because Codex reported no stop it could honour. Codex 0.140+ has one: the app-server's experimental thread/backgroundTerminals/terminate names the process by the processId on the command's item, and thread/backgroundTerminals/list shows what still runs. The command tracker now keeps each command's processId. The first time a running command carries one, the session probes its own app-server once with a one-row list; a listing marks the app-server's backgrounded commands stoppable and the session as offering per-row Stop and Stop all, through the fields the strip already reads. Any refusal (an older Codex answers "unknown variant"), timeout or malformed answer leaves it as today: no Stop. A Stop terminates each named process, then confirms against the thread's own list. A thread Codex says it unloaded counts as stopped; a dropped connection does not, since lost contact is no proof a process ended. An app-server that times out is not asked again. The killed process's own item/completed removes its row, as before. * Ask Codex again after a probe gets no answer, and say when a background Stop fails A background-terminal probe that timed out or lost its request used to hide Stop for the rest of the app-server's life. Only a refusal by name (unknown variant, method not found) or a malformed listing now settles it as unsupported; anything else is asked again on the next update. A background Stop whose agent failed (a process still listed after its stop, a timeout) used to answer "nothing stopped" and the button simply came back. The host now refuses it, so both clients show their usual notice: "The background task wasn't stopped." This covers Claude's background Stop too. * Say a background Stop wasn't stopped only when the agent shows it still running A timeout or lost contact with the agent leaves the outcome unknown, so it now answers with the existing outcome-unknown refusal instead of "The background task wasn't stopped." * Keep a background task's Stop in its stopping state until the row leaves A confirmed Stop re-enabled its button for the moment between the host's answer and the roster dropping the row. The button now holds while the host says it stopped the task and the strip still lists it, on desktop and phone; a failed, unconfirmed or nothing-stopped answer, or the row coming back, offers Stop again. * Keep Grok's own subagent Stop refusal quiet, and a reply about another subagent unconfirmed * Report Grok's own refusal of a subagent Stop as not stopped Grok answers an ended or unknown subagent with a normal reply, so a JSON-RPC refusal means it did nothing and the subagent still runs. (cherry picked from commit e2234a3)
* Catch Claude's sign-in link through BROWSER and wait 10 minutes for sign-in A hidden claude auth login now runs with BROWSER pointed at a small script in the account folder that hands the localhost-callback link back to Orca. Orca opens it in the default browser as before, or hands it to Settings to copy when asked. Windows host sign-ins keep Claude opening the browser itself. * Add Copy sign-in link instead to Claude Add Account and Sign in again * Only catch the Claude sign-in link when copying it Plain Add Account and Sign in again no longer set BROWSER, so Claude opens the browser itself exactly as before. A copy-link sign-in that cannot prepare its BROWSER stand-in now fails instead of opening the default browser, a failed chmod in WSL no longer stops the login, and a failed clipboard write shows the existing copy-failed toast. * End a copy-link Claude sign-in that gets no sign-in link, and simplify its plumbing A missing or unexpected link now fails the sign-in instead of leaving a 10-minute spinner. The link check moves to main and only tells the localhost link from the pasted-code one; the poll is a plain loop, the pending link exists only for copy-link sign-ins, and copyLink travels as a plain boolean. * Pass the started Claude sign-in to the copy-link helper and give its copy-failed toast its own key * Remove a partly written Claude sign-in script when setup fails (cherry picked from commit 000cfe5)
…s credential (#26777) A paired device's socket passes its bearer credential as clientId, and native chat used it as the caller key it stores in ledger rows, receipts, chat records and prompt answers (shown to other clients). Name a paired device by the device id its socket proved instead. (cherry picked from commit 0f7c06a)
* fix(browser): route links and window.open to tabs by Chromium's own disposition window.open(url, '_blank', 'noopener,noreferrer') opened a floating popup window because the new-tab check required an empty features string. Chromium already reports Chrome's tab-vs-window decision as the disposition, so the one rule is now: a tab disposition opens an Orca tab unless the page keeps a named handle it may use (OAuth); size/position features and Shift-click stay popups. The injected click-routing script that relabeled link clicks is gone: native dispositions route target=_blank, Cmd/Ctrl-click, middle-click and iframe links identically. Real clicks still bypass the page-initiated tab budget, now via the browser-process input gesture tracker the route guests already use. Co-authored-by: Lasha Kvantaliani <lasha@sweenk.com> * refactor(browser): spend the click on every window open and drop the noopener parser Review follow-up: a click that opened a popup window stayed unspent and could fund a later scripted tab; every open now consumes it, as Chrome does. The noopener parser only changed named noopener opens, which stay working child windows, so the rule is just an unnamed open with a tab disposition. * test(browser): table-test the new-tab rule and correct stale comments --------- Co-authored-by: Lasha Kvantaliani <lasha@sweenk.com> (cherry picked from commit 807e93b)
#26982) * fix(native-chat): a chat says what its config resolved once, at its start Every options read re-reported a no-pick chat's configured default: ACP re-derived it from the session's current state, and Pi re-sent its start snapshot. A chat left open kept writing its start model back over a newer default another chat had learned since (for example after the user changed the agent's own default), so new chats named the old model and then jumped. A ready child's acquisition now carries its start listing, with the default its start resolved after restoring the chat's saved picks, and the host saves it once when the owner is proven (a starting child already hands it on its `started` event). Options reads carry the model list only. Grok, OpenCode, OMP and Pi share this path and the one no-pick rule. * fix(native-chat): only a chat's own effort menu outranks a newer one The configured-effort fallback took the older listing's menu whenever it had the effort, so an older background listing could bring back an effort the agent's own newer menu no longer offers. It now falls back only to the chat's own listing. * fix(native-chat): an empty Pi settings file or dotfile-only extensions folder picks nothing Pi reads an empty settings file as no settings, and a .pi/extensions folder holding only .DS_Store or .gitkeep loads no extension, so neither keeps the "Model" placeholder any more. The BOM strip is written as an escape. Tests cover the packages key and an extensions folder that can't be read. * fix(native-chat): Claude says what its config resolved once, with its start's settings readback Claude's configured default rode on every options read, so an open Claude chat with nothing picked kept writing back the model its settings applied when it started, over a newer default another chat had learned since. Its default is known only once the settings readback after `started` lands, so that readback's `options-reported` event now carries the start listing with the resolved default, and the host saves it once for the child that reported it. Options reads carry the model list only, as for every other agent. * fix(native-chat): a start's learned default is saved behind the attach, and never fails the start Claude's settings readback, its one report of what the config resolved, was checked against the host's child index as it arrived. A readback that beat a slow attach found no child yet and was dropped for good. It now waits in the session's serialized step behind that attach, as `started` does. Handing a ready child's start listing to the catalog ran inside the acquire's cleanup scope, so a throw there would have torn down a proven child. It is bookkeeping: a failure is logged and the start proceeds. * refactor(native-chat): name the Claude listing's configured-default option; Pi's start listing is read-only No behavior change: claudeCatalogListing takes { withConfiguredDefault } instead of a trailing boolean, and PiRpcSession exposes its start listing through a getter over a private field. (cherry picked from commit 7450f40)
… folder they started in (#26981) * fix(native-chat): resolve every registered agent's floating chat to its pinned folder Drop the unused showStructuredAgentSessionChoice and say what the built-in agent check means. * fix(native-chat): launch floating OpenCode, Grok, OMP and Pi chats in their pinned folder The renderer now resolves every floating chat's links, images and skills against the folder the chat was pinned to. The ACP (OpenCode/Grok/OMP) and Pi launchers still started the agent in the current floating folder setting, so after the setting moved and the agent relaunched, the agent ran in one folder while links resolved against another (and Pi forked its conversation into the new folder). Both launchers now use resolveAgentSessionLaunchDirectory, the helper Claude and Codex already use: launch in the pin, refuse with the existing "folder this chat ran in no longer exists" message when it is gone, and pin an unpinned floating record on first launch. Worktree and folder chats are unchanged. The tab-directory reader now calls the status bridge's own isStructuredTab instead of restating its predicate inline. (cherry picked from commit 094f589)
* fix: preserve streamed turn duration after host restart * test: run restart duration regression in Node * fix: preserve provider receipt time in journal updates * test: verify provider receipt in Claude ownership assertion * fix(native-chat): time interrupted turns from real provider output, not flushes Shared-timeline checkpoints stamped the coalescer flush or retry time as the provider's latest proof of life, so a turn whose provider died before a delayed flush settled with an inflated duration after a host restart. Carry each delta's receipt time through the coalescer and timeline lane. * fix(native-chat): derive interrupted-turn duration from saved output A reply cut off by a server restart could show "Worked for 0s" because recovery ended the turn at the last heartbeat or the reply's first timestamp. End it at the latest time this owner's output was saved to chat history instead, derived in the existing journal fold from the row save time. Drops the per-frame receipt plumbing through every provider and buffer. * fix(chat): exclude host recovery revisions from provider activity Mark journal-open and connection-cleanup revisions with the existing recovery annotation so an interrupted turn does not include host downtime. Preserve buffered provider output and advance restart fixtures to the restart clock. * fix(native-chat): interrupt turns a replaced server left running When the managed server died mid-reply and a new one started on the same host, the cut-off turn settled `unverifiable` with no end whenever the old provider's identity could not be proven gone, and nothing ever revised it: the chat showed no turn status and the sidebar said "unconfirmed". The new server opens the chat records holding the host's instance lock, so every runtime that wrote them before is gone, and every provider it held lost its pipes with it. Record that at load (the fences those runtimes granted, the loaded owner's last renewal, how its runtime ended) and let the stale settle end those runtimes' running or unverifiable turns as interrupted at the owner's last saved output or renewal, with the existing Orca-stop row. Lease adjudication and the live / unverifiable / exited owner verdicts are unchanged. * fix(native-chat): require the record store for any journal open that settles Only an acquisition's open may skip it; a reader open without the store would leave a replaced server's turns unverifiable. * fix(native-chat): say a remote host's Orca restarted, not that it was closed A reply cut off when a remote host's Orca server stopped read "Orca on <host> was closed while this response was in progress." Nobody closed anything there: the server stopped and Orca started it again before the row could show. For a chat on a remote environment, cause quit now reads "Orca on <host> restarted while this response was in progress." The desktop's own quit keeps "was closed". Renderer-only; no wire change. * fix(native-chat): say a remote host's Orca stopped, true for a shared desktop too * test(native-chat): register Grok without compaction in the recovery-output rig Main's #26502 made Grok declare compaction; the rest-test rig adapter has no compact, so the registry refused it before any recovery case ran. * chore(native-chat): say a replaced runtime also revises an unverifiable turn (cherry picked from commit b44a579)
* feat(native-chat): offer Codex Ultrafast as a speed choice Codex lists Ultrafast as a third service tier for eligible models and accounts (GPT-6.1 Sol). Codex chats saved Fast as on/off, which cannot hold a third tier. The Codex speed is now a Standard/Fast/Ultrafast choice built from the tiers the model lists; the tier id is still resolved from the catalog at each send. Older clients keep their Fast toggle: the host still reports Fast on/off and accepts it as a speed. * refactor(native-chat): save the Codex tier id instead of a speed name The chat now keeps the tier id Codex lists (serviceTier: default, priority, ultrafast) and sends it as-is on turn/start; Codex drops a tier the model no longer lists with its own warning. This removes the saved per-model tier map, the send-time lookup and the reconcile code. The unused fastModeTierByModel catalog field is deleted. Older clients still get Fast on/off, derived from the model's tier named Fast. Also drops the pnpm-lock.yaml churn the first commit picked up. * test(pi): drop the removed fastModeTierByModel catalog field * fix(native-chat): review round 1 for Codex service tiers - Accept an older client's fastMode write through the Codex adapter; it was refused before reaching the tier alias. - Bump the saved model catalog schema so catalogs from before serviceTiers re-list instead of offering a Fast toggle with no tier. - Keep only the reported tier on the record, not the Fast it implies. - Drop a reported tier, not only a picked one, when the next model does not list it. - Localize Standard inside the speed choice only. - Return the options answer directly instead of a one-field wrapper. * fix(native-chat): review round 2 for Codex service tiers Keep a tier Codex reports for the thread (it accepts unlisted ones such as flex); read the legacy Fast tier beside an empty serviceTiers list. * fix(native-chat): review round 3 for Codex service tiers A reported tier the old model listed and the new one does not resets to Standard on a model switch, and the reported tier itself stays pickable. * fix(native-chat): keep a re-picked reported Codex tier The thread's reported tier is never reset by the unlisted-tier drop, so a re-picked Flex survives the next read, a model switch, and the turn. * refactor(native-chat): one rule for which Codex tier a model offers Standard, a tier the model lists, or the thread's reported tier that no model lists. The setter and the unlisted-tier reset share it, replacing the separate reported-tier and model-switch guards. * test(native-chat): drop the removed fastModeTierByModel field from main's new catalog test (cherry picked from commit c8b8997)
…7026) * Let a Codex chat stop a sub-agent from the background-tasks strip A Codex sub-agent showed in the background-tasks strip with no Stop, and the chat's own Stop interrupts only the main turn, so a helper ran on until it finished. Codex's app-server takes `turn/interrupt` for any loaded thread, sub-agent threads included, so a sub-agent row's Stop now interrupts the turn Orca saw that sub-agent run, through the same background Stop route Codex commands use. Codex's -32600 refusal (no active turn, another turn, thread not found) means the run the row showed is over, so the Stop is quiet; any other refusal says the task wasn't stopped; a timeout or lost contact is unconfirmed. A live run showed Codex holds an interrupt that lands as the turn ends until the sub-agent's next turn ends, so a timeout on a run the session has since seen end is quiet too. Older clients' `codex-agent:` rows name no host record and stay unstoppable. * Stop a Codex sub-agent's own sub-agents and commands with it, and hold Stopping per run A sub-agent Stop interrupted only that helper's turn, so the commands it was running (Codex keeps a turn's processes when the turn is interrupted) and the helpers it had spawned kept going, and its row stayed in the strip. The Stop now interrupts the helper's run and every descendant's run, then terminates the processes those threads hold where the app-server has background terminals. The interrupt goes through interruptCodexTurn, and a small codex-background-task-stops module composes the sub-agent and terminal stops, so the terminals module is terminals only again. A Codex helper keeps one row id (its thread id) across runs, so a confirmed Stop's held button carried over to the helper's next run. Both clients now hold it for the run it was pressed on, read from the row's invocation. * Test that a sub-agent Stop leaves siblings alone and skips terminates after a timeout Adds direct tests of the per-run Stop hold (including an older host's roster), matches the desktop hold comment to mobile's, and reuses the one 10 s bound. (cherry picked from commit cbc565d)
…kground (#27022) * Let a Grok chat list and stop the commands it left running in the background A command Grok backgrounded (a dev server, a watcher, a monitor) showed only as a transcript line: it never reached the background-tasks card above the composer, so it could not be seen as running or stopped from Orca. Grok's subagents and Claude's and Codex's background commands already list and stop there. The ACP lane that publishes Grok's subagents as child records now also publishes its background-task rows: a running task as a live background command or monitor, an ended one as removed, derived from the rows the journal admitted. A Stop for one of them goes through the same adapter route to Grok's own per-task kill (x.ai/task/kill); Grok's completion notice ends the row, and a task Grok no longer holds is removed at once. A refusal reports the task as not stopped; no answer or an unreadable one leaves the outcome unknown. Each Grok process is checked once at start with a no-target kill, beside the existing subagent check. Only Grok naming the method unknown hides Stop. * Settle a Grok task's transcript row when Stop finds it already gone When Grok answered a Stop with already_exited or not_found, the card row was removed out of band while the transcript row kept saying it was running, and a later restated running notice could bring the card row back with Stop. The answer now settles the transcript row through the translator, like the subagent Stop, so the card row leaves because its row settled, the existing no-relight rule keeps it gone, and a completion Grok still sends after already_exited writes the real result. Also covers /clear being refused while a Grok background command runs. (cherry picked from commit d91921a)
* fix(codex): keep running subagent groups in long chats * fix(codex): remember superseded settled child turns * fix(native-chat): retain running Claude subagent groups (cherry picked from commit dab3f6e)
Co-authored-by: m4air <m4air@Mac.localdomain> (cherry picked from commit 80d34c3)
…irst frame before the agent starts (#27031) * fix(native-chat): a new chat's effort pill keeps its first-frame value before the agent starts A chat with no model pick runs the catalog's default model at that model's default effort. The at-rest options answer now says so for every agent, not only those whose live child reports the model's default, so the effort pill no longer blanks and then jumps when the agent starts. * fix(native-chat): read a chat's at-rest options for the folder it runs in The at-rest options read asked the model catalog with no workspace, so it always named the account's default model and effort, even in a repo whose own agent config could pick another. The new chat's first frame, read for that repo, names nothing there. The at-rest read now asks for the session's own folder, so both frames agree on the model and the effort. * fix(native-chat): scope the at-rest options read only for clients that take no model A client that needs a model in every options answer (mobile today, older desktops) refuses an answer naming none, and loses that read's commands with it. Only a client advertising the optional-model capability gets the answer for the chat's own folder; any other keeps today's account-default answer. * test: run the at-rest workspace-scope RPC test in the Node SQLite project * test: build catalog listings so the fixtures compile before and after main drops the tier map * fix(native-chat): one at-rest rule — the chat's folder decides its default for every client The folder-scoped catalog read now decides the default for every client. Where it names one, the answer names that model and its effort. Where it names none, a client that takes a model-less answer gets none, and one that needs a model gets the account default model with no effort: exactly what it got before. The reader is now required, and the first frame and the at-rest read share one rule for which locations name a local folder. * fix(native-chat): keep test callers of the host's options read untouched Only in-process callers omit the reader at the host's entry, and they read as the RPC gate treats an in-process caller. The shared read itself still requires one. This leaves main's newer test call sites unconflicted. * refactor: one native-local location check for Claude, Codex and the at-rest folder read (cherry picked from commit 7dfa4d6)
* fix(mobile): open Grok, OpenCode, OMP and Pi chats on the phone The phone advertised only the Claude chat lane, so the host retitled every other agent's chat tab 'Update to view' and the phone's resolver refused non-Claude/Codex chat tabs anyway. Mobile now reads any agent the host lists, passes the chat's real agent to its send and slash-command paths, advertises the registered-agents and Pi dialog capabilities, hides image attach for agents the host says take none (as desktop does), and draws Pi's input dialog as one line. * test(mobile): drive the agents read through the shared fake client * fix(mobile): read options reports with no model Grok, OpenCode and OMP can report no current model, and a chat at rest may have none saved. The host refuses those reports to clients that don't advertise agent-session.options.optional-model.v1, which left the phone's model picker on its seed list and dropped /clear and /compact. * fix(mobile): reserve the chat session for every launched agent The phone matches a launched chat tab only by its session id, which it minted only for Claude and Codex. A Grok, OpenCode, OMP or Pi chat started from + landed only once the host replied, and a lost reply read as "Couldn't confirm the agent started" even with the chat listed. Hosts accept an agent-named id for any agent and ignore it for a terminal launch. * fix(mobile): read the host's agents once per connection, retry after a failure Every session-screen mount asked agentSession.agents again, so the attach button popped in late for OpenCode and Pi, and a read that failed while connected was never retried. Share one read per client, drop it on disconnect, and forget a failed read so the next chat asks again. * test(mobile): pin the chat's own agent on the send path and attach gating The send-bridge test passed with the claude/codex narrowing reverted. Drive an OpenCode chat through the controller and the structured session's send, where /review is the agent's own command but Codex's host claims it, and check the overlay offers attach only when the agent takes images. * fix(mobile): offer a card's X only while a turn is running The X on a structured prompt card stops its turn. Pi opens dialogs outside a turn too, and tapping X there could only say "Stop not sent". Claude and Codex cards arrive inside a running turn, so they keep their X. * fix(runtime): let a phone read the host's registered agents The phone now asks agentSession.agents to decide whether a chat's agent takes images, but the mobile allowlist refused the method, so OpenCode and Pi never got an attach button. It returns only each agent's id and its fixed capability record. * fix(mobile): keep a host's refusal of the agents list for the connection Released hosts advertise registered agents but refuse agentSession.agents to phones (forbidden, not available to mobile clients). Treat that answer as no list until the next connection instead of re-asking on every chat; keep retrying transient failures. * fix(mobile): shrink the model pill before it pushes Send off the composer row On a narrow phone, attach + a long model label + mic left no room for Send (seen with Pi's resting label). The pill's slot is now the row's only shrinkable item, so its label truncates and the buttons keep their size. Rows that already fit look the same. * fix(mobile): keep /clear and /compact for a chat whose agent lists no models The phone dropped a structured chat's option controller when its picker had nothing to show, and the / menu reads the chat's conversation commands from that controller. OMP lists no models, so its menu lost /clear and /compact. Keep the controller for every structured chat; the picker still renders nothing without a model. * feat(runtime): say in a chat's options report whether it takes images The host's options read now carries acceptsImages for every session: the agent definition's imagePrompts, narrowed by a running ACP child to what its initialize advertised (the same rule its sends use). Absent means an older host. * refactor(mobile): read whether a chat takes images from its options report The phone no longer reads the host's agent list for the attach button. It takes acceptsImages from the agentSession.options read it already makes, keyed to the chat, and kept through a pick made during that read. Before the read answers, and from an older host, Claude and Codex keep attach and other agents don't, as before. Drops the agent-list hook with its cache and refusal latch, the host-support flag only it used, and agentSession.agents from the host's mobile allowlist. * refactor(native-chat): gate desktop chat images on the session's options report The composer's image gate takes the session's acceptsImages answer first, falling back to the host's agent list before the options read lands (a new chat's start) and for an older host that sends no answer. With the phone no longer using it, structuredAgentAcceptsImages moves back to the renderer, where it was. * test(native-chat): prove the session's image answer reaches the composer gate (cherry picked from commit 28d07a6)
) * test(native-chat): pin each agent's subagent row writes before unifying the rosters * refactor(native-chat): one shared subagent tracker owns every agent's subagent rows Claude, Codex and ACP (Grok) each kept their own subagent roster with drifted rules. They now only report what they observe; one tracker decides grouping, runs, names, state, sweeps, retention, row writes and restart inheritance. The golden row-write tests from the previous commit show each rule change. * fix(native-chat): one entry per subagent, derived lookups, ACP end stamped in its session-end write Review round 1: a resumed child keeps its one entry and reopens it where it is listed (the chat draws one section per child, anchored at the first row naming it), every lookup is read from the rows, the no-turn row is never released, and ACP's session end stamps its live children inside the session-end write instead of writing rows ahead of it. * fix(native-chat): derive the no-turn group, end a foreground run with the turn that started it Review round 2: the row no turn owns is recognised from its id (a stored flag was lost when an earlier run's row was first reached through one of its children), and a turn's end settles the foreground runs it started wherever their child is listed. * fix(native-chat): count an inherited subagent row as settled history when a lookup loads it A read-only lookup could load an earlier run's settled row that retention never saw, so it was never released and a fresh settled row was released in its place. (cherry picked from commit 5b228ea)
…27209) * fix(session-history): resume folder workspace sessions in their own folder Session History matched a session to its workspace only among git worktrees, so a conversation recorded in a folder workspace had no owner: its subagent rows could never resume, and the parent row only offered whatever workspace happened to be active. Folder workspaces now join the owner list as their usual folder: rows. * test(session-history): mock folderWorkspaces as the array the store holds * fix(session-history): jump through the shared workspace activator without a second toast Both Session History jumps and the resume activation now go through activateAndRevealWorkspace, which owns the folder routing and its path gate. A folder the gate refuses already shows why, so the generic 'no longer available' toast is kept only for a workspace that is actually gone. * fix(session-history): keep the jump toast when folder activation could not pick a folder Folder activation only explains a refusal once it resolves the folder's owner; an id present on two hosts returns before any toast. Suppress the generic toast only when that owner resolves. * test(session-history): type the folder fixture without an assertion * fix(session-history): call folder workspaces folders, not worktrees Folder rows now read 'Resume in Folder', 'Folder' (details heading), 'Current folder' / 'Folder' / 'Archived folder', 'Jump to Folder', and 'Folder workspace is no longer available.'. The kind is derived from the row's folder: workspace id; git worktree rows are unchanged. * fix(session-history): say Jump to Folder in the row menus for folder rows Also shows a plain folder icon on a folder row's details heading and aligns the Spanish and Japanese folder strings with their worktree counterparts. (cherry picked from commit bedf6f5)
…k was in the background (#27167) * fix(native-chat): offer restart resume for chats whose only work was background The quit started the chat teardown and then stopped the hook server, which emptied the status store holding each chat's subagents and background commands. The teardown reads that store to decide which chats were still working, so a chat whose turn had finished but whose background command or subagent was still running was never offered to resume after a restart or update. The quit now stops hook ingress without clearing the store. * test: preserve background-only chat resume across shutdown (cherry picked from commit 2201653)
…ent is running (#27520) The terminal tab menu found its conversation through the tab-title lookup, which also falls back to retained and sleeping records an exited agent leaves behind. Pass a running-agents-only option for the move; tab titles keep the remembered conversation. (cherry picked from commit 5428bd9)
* fix(native-chat): show a finished resume as a result, not a picker Once a resume starts and no chat is left to choose, the dialog drops the Select all row and the checkbox column, so each chat's check reads as its status instead of a ticked box. The Don't-ask-again box only shows while choosing; a run started by the launch's auto-resume says so and links to the setting. The finished description is past tense. * fix(i18n): keep Agent untranslated in the ja resume copy * fix(native-chat): keep resume results actionable and keyboard accessible * Fix hidden resume preference replay after run starts * Consume resume picker preference after applying it * fix(native-chat): keep Don't ask again after Resume while auto-resume is off The box now shows on the result view too whenever the setting is off and the user started the resume, and saves only while it is visible, so a hidden tick still never replays. Automatic launches keep their 'Resumed automatically' note instead. (cherry picked from commit c43ac3f)
…view (#27184) * feat(terminal-pane): header button resumes the CLI session in a new native chat The pane header's "Show chat view" toggle is replaced by the tab menu's "Resume in New Native Chat" move. The Session History lookup runs on click, so an always-visible header never scans in the background. A pane already in chat view keeps its "Show terminal" way back. * fix(terminal-pane): fold the resume gate into the pane's existing store subscription Keeps a mounted terminal pane inside its pinned store-listener budget. * fix(terminal-pane): resume the pane's own conversation, open the chat that already holds it The header "Resume in New Native Chat" button gated on chat-view eligibility but looked up the tab's best conversation, so a Grok pane beside a Claude pane resumed the Claude conversation, and Grok/OpenCode panes showed a button that always failed with a "try again" toast. - Map one pane to its conversation with the tab-title mapping narrowed to that pane (same sources, same Claude/Codex rule), for both the button's visibility and its click; this also keeps the button after the agent exits, as the tab menu does. - A conversation a native chat already holds now matches its history row; the move's gate still refuses it (tab menu unchanged, pinned in the parity test), and the button opens that chat. - Distinct messages: nothing saved yet, cannot be resumed in a native chat, or the existing generic failure. The click forces a fresh scan like the panel's Refresh does. - Call the plain resume launch directly; drop the shared actions module and restore the tab menu. - A tab closed during the lookup no longer opens a chat. * fix(terminal-pane): hide Resume in New Native Chat where the chat route is refused On a local workspace whose project runs in WSL (or needs runtime repair), the header button showed on Claude/Codex panes, but native chat cannot launch there, so every click said the conversation can't be resumed while the tab menu offered nothing. The button's visibility now also asks the same launch-route check the Session History row gate uses (structuredAgentSessionLaunchFeasible). * fix(terminal-pane): offer the header resume only while the pane's agent is running The tab menu now offers Resume in New Native Chat only while the tab's agent runs (#27520). The header button still found its pane's conversation from the retained or sleeping record an exited agent leaves behind, so it stayed after exit. The pane lookup now honours the same runningAgentsOnly option, and both the button's visibility and its click pass it. * refactor(terminal-pane): the header's pane lookup reads only the running agent Every caller of resolvePaneAiVaultTitleRequest asked for running agents only, so its retained and sleeping sources were never read. The pane lookup now maps just the pane's live agent, and collectAiVaultTitleRequests returns to main's shape, sharing only the live-agent mapping with it. (cherry picked from commit c345741)
* feat(chat): read one saved row per subagent Adds the singular `subagent` journal block (one row per child, revised in place, with a plain-text twin for older clients) and teaches every reader to draw it: desktop and phone fold a turn's sibling rows into the existing "Ran N subagents" row, the client and off-page rosters name children from their own rows, the reopen pass settles a row a dead host left working, and the phone and worker-transcript wires bound it. No producer writes the block yet; that switch lands in the follow-up. * fix(chat): open subagent background flag, separate nested group ids, page window skips subagent rows - background is an open boolean so a later build's value never fails a chat load - a helper's own helpers fold under a groupId of their own, so the phone opens them apart - each subagent's own row rides along with the page window instead of counting as conversation * refactor(chat): a subagent's group entry as one object (cherry picked from commit fe1f022)
…efuses a second terminal writer (#27183) * refactor(chat): answer transcript adoption and Session History from the agent registration Claude and Codex's transcript importers and Session History ownership (row id, resume-command grammar) move onto their runtime registrations as required-nullable fields; every other agent writes null. agentSession.agents publishes the derived transcriptAdoption/sessionHistory flags, and a client reads a host that omits them (or lists nothing) as the old build's answer. Strict parity: only Claude and Codex answer yes anywhere. * refactor(chat): one Session History ownership index, binaries from row agents, fork offer from fork argv Review round 1: the duplicate-adoption check and Session History ownership read one index keyed by the registration's row id (an importer now requires sessionHistory); resume-command binaries derive from rowAgents through the TUI agent config; the Resume in New CLI offer asks getAgentForkArgv instead of naming Claude and Codex. Strict parity: only Claude and Codex answer yes. * feat(chat): Session History owns OpenCode, Grok, OMP and Pi native chats An OpenCode, Grok, OMP or Pi native chat's conversation listed in Session History as a plain terminal row, and resuming it ran the agent's CLI as a second writer to the conversation the chat was still using. Each agent's registration now fills `sessionHistory`, so those rows show as the chat (its name, opening the chat) and Orca refuses a terminal resume of an owned conversation, as it already did for Claude and Codex. - OpenCode lists under `opencode` and `opencode2` (2.x) rows by its ses_ id; `-s/--session`, `-c/--continue` resume, `--fork` does not. - Grok: `-r/--resume/--load` and `-c`; a non-UUID value names a title, so it is read as target-less; `--fork-session` does not resume. - OMP: `-r/--resume/--session` take an id, any prefix of it, or the session file's path; bare `--resume` and `-c` are target-less. - Pi: rows are listed by the session file name's id while the chat holds the file's path; `--session <id|prefix|path>`, `-c`, `-r` (picker). - An optional `resumeTargetMatches` on the registration covers prefix and path targets; it defaults to exact equality. - Each agent now reads every token naming its own binary, so another agent's name earlier in a command (`claude --model pi`, `cd ~/src/codex && claude --resume X`) no longer hides a resume. For Claude and Codex this only ever refuses more than before. - A renamed OpenCode chat tab names its `opencode2` row too. * fix(chat): show owned rows only to clients that open the chat, without a resume command - The list and search projection now asks, per owning agent, whether the requesting client opens that agent's chat. Current mobile renders only Claude and Codex chats, so it no longer receives OpenCode/Grok/OMP/Pi rows owned by a chat (a tap there opened an empty terminal and showed agent_session_conflict). Claude and Codex keep their rule: any client that reads structured sessions. - An owned search hit opens its chat, so it no longer carries a terminal resume command (all agents; `orca` CLI search stopped printing one for a chat). - OpenCode/Grok/OMP/Pi flags match case-sensitively, as their CLIs read them, so `rg foo src/pi -C 3` is not `pi -c`. Claude and Codex still fold case. - A test feeds every resume command Orca builds for an owned row of each agent (Session History, live-tab argv; posix, PowerShell, cmd) into the guard. * fix(chat): keep the resume guard linear and skip chat-tab lookups with no move - Each agent binary mention in a command now reads its arguments only up to that agent's next mention, which reads the rest, so every token is parsed once per agent. A 200 KB text naming agents throughout took 9.4 s on the host's main process; it now takes about 90 ms. Every refusal before still holds: a later mention still reads its own resume. - Right-clicking a native chat tab looks up its Session History row only when the agent's CLI can fork a conversation, the one move a chat tab offers, so OpenCode, Grok, OMP and Pi chat tabs run no list or rescan for nothing. Claude and Codex are unchanged. * fix(chat): read each shell command once in the resume guard The guard tokenizes the text once and splits it into shell commands at &&, ||, ;, |, & and newlines (a newline after a line-continuation mark stays inside its command). In each command, each agent reads its arguments from its first binary token to the command's end; a later token naming it is an argument. A `claude` path inside a Claude command no longer ends that command, so `--session-id <owned>` still cancels a fork after it, and a binary-named path between a resume and its fork flag keeps the fork a fork. Binary names are looked up once per token, which keeps a 16 MB send to about 2 s. Against main's parser, a Claude/Codex command is now allowed only when main refused it for a flag in a later shell command (`claude ; echo --resume X`). * fix(chat): end a resume-guard command at a bare carriage return A PTY's Enter, and many orca terminal send texts, end lines with \r. The guard now treats \r and \r\n as line breaks like \n, including after a line continuation mark, so a later line's resume is read as its own command. * refactor(chat): drop the transcript-adoption and Session History wire flags agentSession.agents no longer publishes transcriptAdoption or sessionHistory, so there is no new field for older and newer peers to reconcile. Their only consumers were renderer gates that could answer from what they already have: "Resume in New Native Chat" eligibility goes back to main's rule (adopting a CLI conversation is Claude/Codex-only, and the host still refuses any other agent from its registration's transcriptImport). Removes the AgentSessionDeclaredCapabilities split, the older-host decoder shim and the generic host-flag reader. * refactor(chat): keep main's Claude/Codex rule for the CLI fork offer "Resume in New CLI" and the chat-tab menu read the agent name again instead of building a synthetic provider session to ask the fork builder, and the chat-tab lookup loses its host Session History flag gate. Only Claude and Codex CLIs fork, so only their chat tabs look up a row, exactly as on main: no new list requests. * refactor(chat): one registration-aware ownership index, no registration union - listStructuredProviderSessionOwnership reads each record's registration for its Session History row id; the callback parameter and its one-function wrapper module are gone (no import cycle: checked value and type imports). - StructuredAgentCliConversations is a plain type: transcriptImport is nullable and sessionHistory is required, since every registered agent now owns its rows. The "importer needs history" union and its null checks go. - A test pins that no two registrations claim the same Session History row agent, since the owner lookup takes the first match. * fix(chat): read resume commands into words as a shell does Separators glued to a word (`opencode -s id; echo`, `cd /x&&claude -r id`) now end the command, and quotes are removed inside a word, so `--session="id"` reads as the id. Applies to every agent, Claude and Codex included. The acp-and-pi allowed() helper now asserts. * fix(chat): count an agent's name only where a shell would run it terminal.send checks agent prompts too, and agentPrompt does not keep the text from a shell (it falls back to a plain PTY write), so prompt text like "Fix the Pi --resume flag" was refused. The resume check now reads each command's program: its first word past VAR=x prefixes, a path, and env/exec/command/sudo/npx/bunx/pnpm dlx. Every command Orca's builders produce, Claude and Codex included and with a user's command override, is still refused. * refactor(chat): compare resume ids in any case; Claude uses the shared option reader The default target match now ignores case (ids are UUIDs or ses_ ids), so Grok's override, which only repeated it, is gone. Claude folds its flags and reads them with resumeInvocationFromOptions instead of repeating the `=` handling. * perf(chat): read terminal text in one linear pass A repeated regex group overflowed the stack on a 16 MB word. A sticky-regex scanner reads words, quoted runs and separators in one pass; an unclosed quote is searched for once per quote kind. * refactor(chat): read a command's resume only when its program is an agent * fix(chat): read an agent run under nohup, time or wsl as the command's program * chore: restore pnpm-lock.yaml * fix(chat): keep the CLI's own title on OpenCode, Grok, OMP and Pi chat rows Session History shows these agents' chat conversations under the title their CLI saved, as it does for their terminal conversations; the chat tab's name and renames stay on the tab. Claude and Codex rows keep the chat's name, as on main. * feat(chat): Resume in New CLI for Grok and Pi chats A Grok or Pi chat's Session History row and tab menu now offer "Resume in New CLI", like Claude and Codex: the CLI opens a copy of the conversation (grok --resume <id> --fork-session, pi --fork <file>) and the chat keeps its own. OpenCode's interactive CLI and OMP have no way to open a copy, so their chats keep only Resume. * fix(chat): offer Resume in New CLI only when the copy command can be built A Pi chat found by search on a remote Orca server has no session file path, so its copy command can't be built; the row no longer offers the button there. * fix(chat): refuse pi --session-id of a conversation a chat owns `pi --session-id <id>` reopens that session when it exists, so it is a second writer like --session. (cherry picked from commit e83e40e)
(cherry picked from commit bdf0157)
* fix(mobile): keep the chat where the reader scrolled to RN Web reports no drag events, so the page never stopped following a live turn. Native lost its place when older history loaded. * fix(mobile): keep chat history the phone already shows A busy turn outran the 40-message window and dropped the row being read. * fix(mobile): keep the page's place when history loads RN Web's list draws rows by index, so rows added at the front replaced the ones on screen. * fix(mobile): hold the page's place by a row on screen Total height also grows when live output lands below the reader in the same update. --------- Co-authored-by: Brennan Benson <79079362+brennanb2025@users.noreply.github.com> (cherry picked from commit 12ba3bc) (cherry picked from commit 78cbb87)
* Add host-owned agent launch settings mutations * Edit agent launch defaults on the selected execution host * Await durable writes for host agent launch settings * Fence agent detection by execution-host pairing * Acknowledge host settings before hook reconciliation * Preserve Reset after pending agent launch saves * Simplify agent detection re-pair handling to the clear and the transport fence A re-pair already clears every detected-agent list, flag and pending probe for the environment, so the per-record pairing owner keys, the owner-keyed pending probes, the settle-time pairing checks and the hook's ownership gating were never reached. Detection now captures the pairing before the capability wait and fences its RPCs with it; the refresh fallback still refuses after a re-pair. Pins the fence across a capability wait, the needs-update clear, and the agent-launch RPC permission levels. * Open launch settings on the Active Server without the focus-routing helper Main's new owner-routing ratchet counts getActiveRuntimeTarget calls, and this pane's no-workspace default added one. It now reads the Active Server field the way the Agents pane did before host selection, so the default is unchanged. * Re-probe mounted launch surfaces after a re-pair; rely on the transport fence for the refresh fallback A re-pair clears an environment's detected-agent lists, but a mounted tab bar, quick launch or composer that was still waiting for its first list (or whose first probe had failed) never asked again, leaving "Can't reach this workspace's host" until another surface mounted. The hook now re-runs its effect when the target environment's pairing revision changes. The refresh fallback no longer re-checks the pairing in the renderer: it already carries the pairing captured before the wait, and main refuses any mismatch. Pins the Settings host form's remount key on re-pair. (cherry picked from commit 5035baa) Backport to 1.4.224: 1.4.224 has no Settings host picker, so the Agents pane edits the Active Server's launch settings (settings.activeRuntimeEnvironmentId) instead of a hostScope; detection has no workspace-scoped capability wait, so only the refresh fallback is pairing-fenced. Dropped tests for the host picker's unavailable notice, the hostScope-vs-Active-Server case, and workspace-scoped detection fencing.
…27204) * test(chat): snapshot every native-chat agent's spawned child as a parity oracle Captures the exact child each native-chat agent spawns (Claude, Codex, Grok, OpenCode, OMP, Pi), worktree and floating, fresh and resumed, plus a registered worker with visuals off, at the real spawn boundary (the process host's spawn) and through production's own host dependency wiring. The inherited environment holds every pane, hook, caller, terminal-handle and visuals key a parent Orca pane could hand down, so the snapshot records today's per-agent differences as they are. Each case also records the launch's permission posture, which for Codex (thread policy) and the ACP agents (full access) acts only after spawn. Each agent also runs fresh and resumed with every launch setting moved off its default: a Command setting naming another binary, the permission posture flipped, an environment overlay over inherited and shell keys, and only named login-shell variables inherited (the fake login shell carries its own PATH and a variable no case names). A second test fails when a registered agent has no snapshot, since the oracle lists its agents by hand. Test only: the launch-path refactor that follows must leave both tests and the snapshot file unchanged. * refactor(chat): one composed launch for every native-chat agent Every native-chat agent (Claude, Codex, Grok, OpenCode, OMP, Pi) now builds its child through one composer instead of six hand-written resolvers. Each agent contributes only its own part (command, args, agent env, keys it must strip); the shared composer admits the session record, reads the launch directory, base environment, launch overrides and visuals once each (memoized, in the agent's original order), sets cwd, and seals the child. The seal (sealStructuredSessionChild) is the one place that names a child: session id, structured marker, worker terminal handle, the Orca CLI env, the spawn token under one shared constant, and the per-launch list of inherited keys to delete, minus any key the seal itself sets. Claude's delete list now reaches its SDK spawn after buildClaudeChildProcessEnv's merge. Behaviour is unchanged: the parity snapshot from the previous commit passes byte for byte, including each agent's existing differences (Claude and Codex keep inherited pane and hook env, Pi has no visuals and always full access, the ACP history read stays unsealed). Admission refusals keep their reasons; only log-only wording moved to the shared check. Drops launch dependencies no production caller wires (resolveLaunchEnv, resolveCodexOverrides) and folds the per-agent env overlay hooks into the existing resolveAgentLaunchEnv. * refactor(chat): only the seal names a native-chat child Follow-ups from review of the composed launch: - The spawn token is required on every resolveLaunch. The one read that starts no child, ACP's restart-recovery history read, now has its own path (composeStructuredLaunchRead, deps.readLaunch) that returns the agent's own env unsealed, as before. Agent parts receive the session identity only, never the token. - structuredSessionChildIdentityEnv is gone; the seal is the only way to build a child's identity env, and tests go through it. - A launch's environment is the runtime's resolveAgentEnvironment, so the base-plus-overlay rule exists once. - Claude's connection test now covers a non-empty envToDelete applied after the inherited env is merged. No change to any spawned child: the parity snapshot is unchanged. * ci: run the login-shell contract job for the renamed child-env seal The seal moved from structured-session-child-identity-env to structured-session-child-env, but the PR job classifier still listed the old path, so a change to only the seal would skip the live login-shell contract job. The prefix now covers the seal, its test and its test support (which the login-shell harness imports). (cherry picked from commit d3182d4) Backport to 1.4.224: tests import spawnProcess/runProcess/ProcessSpec from src/shared/child-process instead of @orca/process-host (the parity test mocks run-process at the same spawn boundary).
* Require Codex 0.136.0 for native chats
* Add host-owned Codex install and update actions
* Bind Codex checks and maintenance to the selected installation
* Resolve Windows updater cleanup through system helpers
* Use one child environment for Codex checks and launch
* Expire Codex readiness and reconcile current host jobs
* Resolve Codex checks against the child command search path
* Restrict global npm repair to the selected Codex install layout
* Pin the host updater directory through Windows supervision
* Synchronize Codex maintenance fixtures on readiness
* Unify Codex notices and recover version-refused chats
* Route Codex recovery to the operation that remains
* Preserve Codex setup execution errors in the job log
* Preserve UTF-8 in Codex setup logs
* Remove unused Codex host installation detector
* Require host administration for Codex maintenance
* Show a missing Codex explanation once
* test(native-chat): give scripted Codex runtimes their installation fact; keep the registry out of the relay
Runtime tests that drive a scripted Codex app-server still ran the new
version check against whatever codex was on the machine's PATH: refused
on CI (no codex), and on a dev machine it ran the real codex --version.
A scripted transport now must state the Codex it stands for
(readCodexInstallation), asserted at install, and the tests get it from
one scriptedCodexTransport fixture.
The relay's Codex maintenance path reached @orca/windows-registry through
resolvePathEnvKey's re-export in windows-environment-path; import it from
its registry-free module instead.
Also update the catalog-probe test for the shared child environment.
* refactor(native-chat): drop the Update Codex action; keep Install for a missing Codex
A too-old Codex now gets only the plain message ("Codex X is too old for
chats. Update to 0.136.0 or newer.") with no button and no job. The
existing rechecks (window focus, the <=30 s expiry of a saved result)
retry the refused chat once the user has updated Codex themselves.
A missing Codex keeps the Install Codex button: `npm install -g
@openai/codex` with the npm on the chat's PATH, as a host-run job with
its log, then a recheck and recovery. A configured Command that is
missing gets the message only, since a global npm install would not
land at that path.
Removed: the `codex update` command and its 0.126 floor, manual
instructions, npm layout and launcher identity detection, the action
descriptor (install|update) and its titles, the legacy capability, and
the SSH relay maintenance path (structured chat never runs over SSH or
WSL), so the relay preflight handler is back to main's.
* refactor(native-chat): keep Install Codex in Settings only
When Codex isn't installed, main already hides Codex from the New tab menu,
so the in-chat Install button was almost never reachable. Settings → Agents
already shows "Codex isn't installed." with Install Codex.
- Chats and the new-workspace composer now only state "Codex isn't
installed." (no button, no install job from the chat).
- The Install action, its busy spinner and the host-busy subscription move
into the Settings Codex row, the only place that starts the job.
- The store no longer re-checks every other workspace context when a job
finishes. A refused chat recovers through the same re-check a too-old
Codex uses (window focus, or the <=30 s expiry of a saved result).
- The chat recovery key no longer includes the install job id.
- Tests: Settings drives the install; a new test proves a chat refused for
a missing Codex recovers through focus after a Settings install, not
through the job; the expiry recovery test covers missing and too old.
* refactor(codex): check chat compatibility when acquiring the child
* Restore workspace submit guard and simplify Codex maintenance
* Keep only the Codex chat minimum version
(cherry picked from commit c0e2b73)
Backport to 1.4.224: process-host imports point at src/shared/child-process; codex-minimum-version.test builds its fake piped child inline (the shared fake-spawned-child fixture came with @orca/process-host); dropped the NativeChatStructuredSession.approval-and-start-failure test edit (file absent on 1.4.224; the Codex wording is still covered by NativeChatNoticeRow.test).
… Chat page (#28050) * feat(settings): move Chat UI's chat settings from Experimental to the Chat page * chore(i18n): sync catalogs for the moved chat settings * fix(settings): hide the Behavior group when search matches only a switch the host lacks * Keep Chat behavior search aligned with translations and host support (cherry picked from commit edd8084) Backport to 1.4.224: ChatBehaviorSetting imports RuntimeClientTarget from @/runtime/runtime-client-target (1.4.224's location); NativeChatQueueFollowUpsSetting is deleted as on main (its only 1.4.224 difference was that import path).
#26116) Each native Claude and Codex chat now has its own permission mode (Ask for approval, Accept edits for Claude, Approve for me, Full access), shown and changed from a button right after "+" in the composer. A change takes effect before the next message: the agent restarts quietly and resumes the same conversation, never interrupting a reply or background work. New chats start from Settings → Chat → Permissions, which is seeded once from Agent Permissions. The mode is stored as an ordinary chat option, and the restart reuses the existing "replace the agent before the next message" step. (cherry picked from commit 2233fd3)
…Codex's own arguments (#28100) * fix(native-chat): seed the chat permission default from the Yolo/Manual toggle as it shows The one-time copy into Settings -> Chat -> Permissions gave Full access only when every agent's arguments were exactly the bypass flag, so a profile the toggle shows as Yolo (agents set differently, or an extra flag) got Ask. Follow the toggle: Manual seeds Ask, anything it shows as Yolo seeds Full access. * fix(native-chat): seed the chat permission default from Claude's and Codex's own arguments The Yolo/Manual toggle has no saved value: it summarises every agent's arguments and shows Yolo for any mix, including after Manual when another agent keeps custom arguments. Seeding from it would grant Full access to people who chose Manual. Read what Claude and Codex chats checked before they had modes instead: Full access when both launch with their bypass flag, otherwise Ask. (cherry picked from commit f0ccfcc)
…ehavior section (#28212) The new-chat permission default sat alone above Appearance with its own header. It is a behaviour choice, so it now renders as the first row of the Behavior card, laid out like the rows already there. Its search entry and section id are unchanged, so search and deep links still land on it. (cherry picked from commit f844518)
…ey were running with (#28099) * fix(native-chat): chats saved before permission modes run the new-chat default A Claude or Codex chat created before chats had their own permission mode has none saved, and it ran Ask, so every existing chat of a Yolo user started asking for approval after the update. A chat with no saved mode now runs Settings -> Chat -> Permissions, read from the host's settings on each launch, read and restart check. * fix(native-chat): an older chat keeps the new-chat mode its first start ran with A chat saved before chats had permission modes ran the new-chat default on every start, so it kept following that setting. Its first start now writes the mode it ran into the chat's saved options, through the same write that already carries the chat's other options over a start; from then on it is a chat with its own mode. * fix(native-chat): save older chats' permission mode once at host startup Replace the save-at-first-start path with one startup pass: before the chat host is exposed, every Claude and Codex record with no saved permissionMode gets the host's new-chat default, in one store transaction. Saved values, including invalid ones, and every other option are left alone; a read-only store is skipped and a failure is logged and retried next start. The runtime fallback in launch, acquisition, provider-started and attach is restored to main's behaviour. A client-supplied attach that creates a new record now gets the same new-chat seed as every other create path. * fix(native-chat): fill each older chat from its own agent's arguments An older Claude or Codex chat ran with that agent's own Arguments before permission modes existed, so the startup pass now saves full access or ask per agent instead of the combined new-chat default. A user with Claude on bypass and Codex on Manual keeps Claude chats on full access. The attach create path always passes its permission seed; the reservation already applies options only when it creates the record. * refactor(native-chat): read each chat's agent arguments directly in the permission backfill * test(native-chat): give stub runtimes the launch seed the attach path reads (cherry picked from commit c5d7628)
…el pick can't reset it (#28268) * fix(native-chat): picking a Codex model or effort keeps the chat's permission mode A model or effort pick on a running chat replaced the record's options with the provider's answer, which names only the provider's own options. The saved permission mode was dropped, and the next start read it as Ask. The pick now keeps the record's permission mode beside the provider's answer; everything the provider reset still resets. * chore: re-run CI against current main * chore: re-run CI against current main * test(native-chat): keep one permission-mode-survives-pick test * refactor(native-chat): store a chat's permission mode as its own record field A model or effort pick replaces the record's options with the provider's answer, which dropped the permission mode saved among them. The mode now lives beside the options, so no provider answer can touch it. Clients still pick it with setOption and read it from the same published fields. * fix(native-chat): keep a permission mode an earlier dev build saved among options Hourly builds before the separate field saved the mode in options.permissionMode. The startup pass now moves it into the field instead of re-deriving it from the agent's arguments. Temporary; removed after 1.4.226 ships. * chore: re-run CI against current main (cherry picked from commit b91795a)
* feat(chat): read one saved row per subagent Adds the singular `subagent` journal block (one row per child, revised in place, with a plain-text twin for older clients) and teaches every reader to draw it: desktop and phone fold a turn's sibling rows into the existing "Ran N subagents" row, the client and off-page rosters name children from their own rows, the reopen pass settles a row a dead host left working, and the phone and worker-transcript wires bound it. No producer writes the block yet; that switch lands in the follow-up. * feat(chat): host writes one saved row per subagent Claude, Codex and Grok now only report child-work evidence. The host writes each subagent's row from its child record, so the chat row, strip and sidebar read one lifecycle. Deletes the shared subagent tracker, the per-agent rosters and their caps. An accepted Stop settles only the run it was sent to, provisionally, until that run reports its own ending. Work the host store has no room to record keeps the running-tasks strip and its Stop instead of reading as idle. * fix(chat): open subagent background flag, separate nested group ids, page window skips subagent rows - background is an open boolean so a later build's value never fails a chat load - a helper's own helpers fold under a groupId of their own, so the phone opens them apart - each subagent's own row rides along with the page window instead of counting as conversation * fix(chat): subagent row writer reads the live turn once, stop acks survive a failed batch - the row writer reads the journal's live turn at most once per change and only for a subagent with no saved, queued or in-flight row; a revision keeps its first scope - a background stop names each accepted task as its answer lands, so a stop-all batch that times out or fails part way still ends the runs it was answered for (Claude and Grok) - the strip's untargeted Stop rule lives in the shared view (offersStopAll); both strips inline it again; incomplete tracking no longer holds commands or rewind - clients admit a frame whose only change is incomplete tracking - drop the Claude stop's dead publish; the real-CLI stop test asserts the run's own ending - list the writer test with the real SQLite tests * fix(chat): a late Grok report never revives an ended helper; Codex keeps announced helpers - Grok: once a helper's run ended, a later progress or repeated spawn sends no live evidence, so its record and saved row stay ended after the next send retires the record; the test rig now retires finished records on each turn open, as the host does - Codex: past the bound, only a child that ran and ended is evicted, so an announced helper that has not started keeps its record, row, Stop and parent - cut: Codex parentTurnId (written, never read), the Claude identities' turn-end forwarding (call the corrections directly), the duplicate Grok outcome map - update stale comments that described the deleted producers * test(chat): Claude frames to saved row, kept Codex path and usage rules, agent ingestion exemption - a captured background Claude helper keeps its saved row working past its spawn result and the parent turn's end, through the real adapter, host and child records, until its own end - restore focused tests for the Codex root path, label trim and placeholder, usage before announcement and parent usage - the per-session live limit test uses tasks: subagents are exempt from it * fix(chat): incomplete tracking follows the refused children, and the strip's Stop reaches them - the host remembers which children the store refused for room, by handle; each leaves when a later report of it is admitted or it reports its own end, so incomplete clears on a truthful signal; the provider session's end still clears all, and past 512 a flag holds until then - a parent row the store refused counts every live child it reports as refused - with work no record lists, an untargeted strip Stop while no turn runs ends the agent through the idle sweep's stop; under a running turn it leaves the agent alone - the background Stop moves into its own module * fix(chat): the incomplete Stop is offered for every agent and decides inside its step - the strip offers the untargeted Stop for work no row lists whenever no turn runs, whatever untargeted stop the provider has (ACP agents, Codex before it proves terminal stops), since the host reaches it by ending the agent; never under a running turn - the Stop reads whether work is unlisted and whether a turn runs inside its own step, after the listed stops land, so a wake-up turn opened meanwhile is never ended - Codex executions past the bound evict the oldest ended child, else the oldest never started - test the writer's unnamed-subagent label; qualify the writer's retry comment * refactor(chat): the subagent row writer joins the views with their records Drops the shared host-only row projection and its hook-server read: the writer's wiring reads the views every surface reads and the records they come from, and takes residency and the ending's basis from the record. * refactor(chat): Grok subagent evidence reads its ending through the one outcome map * refactor(chat): a subagent's group entry as one object * fix(chat): a background Stop with nothing listed asks the agent for no task stops So an agent with no stop route (an ACP agent the probe found none for) never refuses before the Stop ends the work no row lists. Tests: the unlisted case is untargeted and reads unlisted work inside the step; the repeat-press rig lists the task its Stops reach; reflow the writer header. * test(chat): a nested helper's saved parent; describe how incomplete tracking clears * fix(chat): a closed session claims no Stop; a helper's row follows its own running work - a session close Orca proves is loss of contact, not a Stop the CLI accepted: Claude no longer ends what still ran as stopped, so the host settles it unknown and its row reads unverifiable, as before this stack; stop-acknowledged comes only from a Stop the CLI accepted - a helper that finished while work it started still runs is written working, by the strip's own rule; once that work ends, or the session ends, it reads its own ending again - the CLI starting a completed helper again under the same spawn call is the helper waking when its work ends: the decoder reports it as a restart, so its one row revises in place and ends with its real duration; after any other ending such a start is still read as late - tests from a captured 2.1.280 frame order; item 4 Stops pin their stop-acknowledged basis * refactor(chat): drop the unused Stop basis on a child's own ending Only the acknowledged-Stop edge writes that basis now; reflow the decoder header. * fix(chat): an older history page keeps a turn's subagent ordinals and row A page now reaches back to the first sibling of each subagent group it holds part of, at most one more page of conversation, so loading the older page no longer renumbers same-named helpers or moves (and closes) their folded row. (cherry picked from commit 8be17dd) Backport to 1.4.224: readChildWorkRecords is wired where 1.4.224 builds the status sink (orcad-entry, main-process-runtime-service) since agent-hook-runtime-deps.ts doesn't exist here.
…nning 1.4.227 is built from 1.4.224 plus picks. Users who already updated to 1.4.225/1.4.226 have a v45 terminal daemon running their terminals; a 1.4.224-era client only adopts versions it lists as previous, so those terminals would be stranded. Take v47 (above every shipped or hourly daemon; main is on v46) and list 44-46 as previous.
…on Windows 1.4.226 registered Claude's hook as `orca-agent-hook.exe claude`. 1.4.224's matcher didn't recognize it, so this build would add its .cmd hook beside it and every Claude event would fire twice.
1.4.224 has no @orca/process-host package; its run-process module exports the same spawnProcess.
Drops one redundant classification case; the renamed child-env module is still covered.
brennanb2025
force-pushed
the
brennanb2025/release-1.4.227-picks
branch
from
October 15, 2026 18:43
cb4d229 to
bba2364
Compare
The 1.4.224 cut synced main's pr.yml, which runs check:process-host-imports; 1.4.224 has neither the script nor the @orca/process-host package it guards.
Kiro, the host-scoped Agents pane and non-Claude/Codex worker identities landed on main in commits this release doesn't take.
…live agents (#27795) * fix(orchestration): stop retried failed worker starts from piling up live agents A failed worker-start keeps the terminal it created (a pane the user typed into is theirs), so a coordinator retrying a start that can never succeed left one more live agent behind on every retry, with nothing to stop it. - A --retry-of start now releases the failed attempt's still-owned terminal through the normal worker-release path (output archived, user takeover kept, recovery finishes it after a restart). - A failed start now counts toward the Task's existing three-failure circuit breaker, carried across retries; a retry of a circuit-broken Dispatch is refused with a plain error. Linear: STA-7113 * test(orchestration): type the failed-start retry fixtures without casts * fix(orchestration): carry failed-start count only along --retry-of; plain breaker refusal - Only a --retry-of start inherits its predecessor's failure count, so worker exits across separate starts keep the old behaviour. - The circuit-broken refusal carries failureCount and next steps that say to stop retrying, read the last attempt, and that its pane stays open. - A repeated failed report on a circuit-broken Dispatch is a duplicate. Linear: STA-7113 * fix(orchestration): breaker refusal only points at a terminal that is still open Linear: STA-7113 * fix(orchestration): log a retry's release that could not prove the old terminal closed Linear: STA-7113 * Preserve worker diagnostics before terminal release and mask wrapped tokens (cherry picked from commit b70f13f) Backport to 1.4.224: kept both sides of the sqlite vitest file list (1.4.224 ssh tests + the new archive tests); applied the recovery-and-cleanup.md text change to 1.4.224's generated ORCHESTRATION_FULL_MARKDOWN constant instead of taking main's regenerated copy.
…ca (#27731) * perf(notifications): save mobile dismissal history off the main thread Every agent notification recorded its id in mobile-notification-dismissals.json through the secure-file writer. On Windows that writer runs about five synchronous icacls launches per write, so each notification froze every Orca window (STA-9692). The file holds notification ids, sequence numbers and session ids, not credentials, so it now goes through the existing AtomicSnapshotWriter: async temp-plus-rename, mode 0600, bursts coalesced into one write of the latest entries. Memory stays authoritative, so delivery and retirement are unchanged. The pending write is flushed in the quit barrier after chat and RPC teardown, and in the pre-relaunch hook, since renderer relaunch exits without the quit pipeline. * perf(notifications): flush dismissal history when a headless host shuts down orcad never runs the desktop quit barrier, so its cleanup list awaits the pending dismissal write after RPC stops. * fix(persistence): drain a snapshot write requested just after a drain ends AtomicSnapshotWriter cleared its pending-write promise one microtask after the drain loop saw no more requests. A write() in that gap set writeRequested, got the settling promise back, and nothing drained it, so the newest bytes stayed unwritten and waitForPendingWrite() (the dismissal store's flush) resolved without them. The drain now clears the pending promise in the same tick its loop exits, on success and failure, so a later write() always starts a new drain. * test(notifications): flush dismissal writes before deleting temp state dirs Dismissal history now saves asynchronously, and the writer recreates its directory before each write. Tests that deleted their temp dir with a write still pending left recreated dirs behind in the system temp folder and could race the delete on Windows. They now await the controller's flush first. * perf(notifications): close dismissal history at committed exits The shutdown flush only waited for writes already queued. A notification recorded after it (orcad cleanups still running, or the 150 ms before a renderer relaunch calls app.exit) started an async write the exit dropped, so the phone could keep an alert the desktop could no longer withdraw. The store's flush() becomes close(): it waits for the pending write, and every later record saves synchronously through the snapshot writer's existing writeSync (plain fs, no icacls; a failure only warns). It is called only where the exit can no longer be vetoed: the will-quit teardown, a new exit-only hook that app:relaunch runs before app.exit(0) (restart and profile switches share onBeforeRelaunch but quit through will-quit), and orcad's cleanup after RPC stops. The quit test now holds chat teardown and RPC stop and proves the close starts only after both settle, in either order. The orcad push-startup runtime mock gains the method the entry now calls. * fix(notifications): bound the dismissal close before a renderer relaunch app:relaunch waited on the dismissal store's close with no limit, so a stalled userData disk left Restart stuck on "Restarting…" with a half-quit app saving synchronously. The close now races a 2 s cap through the existing settleWithinMs helper, the same cap the restart path's auth preservation uses, and logs when it gives up. * test(notifications): keep async dismissal writes under test between reads Mid-test disk reads used closeDismissals() as a barrier, which switched every later record to the exit-only synchronous path. The store gains a non-closing flush() (close() = mark closed + flush), exposed on the controller as flushDismissals for tests; mid-test barriers use it, and close stays only where a test simulates an exit or teardown. Also notes why the writer's pendingWrite assignment is safe, renames a store test to what it covers, and groups the orcad cleanup-order test under a describe. (cherry picked from commit 3fb4b06) Backport to 1.4.224: this base has no AtomicSnapshotWriter (src/main/persistence/atomic-snapshot-writer.ts, added by #26822, is not on 1.4.224), so the dismissal store saves through the existing StatsSnapshotWriter, its predecessor with the same async temp-plus-rename and writeSync. StatsSnapshotWriter gains an optional fileMode (0o600 for the dismissal file; stats unchanged) and the PR's drain-race fix; the writer regression test moves to a new stats-snapshot-writer.test.ts. The two conflicting e2e tests keep 1.4.224's shape with the PR's async liveDeliveries()/closeDismissals() edits applied, and the same edit is applied to structured-attention-relay-settlement.unit.test.ts, which exists only on this base.
* fix(worktrees): a retried or resumed delete only removes the folder it started deleting A failed worktree delete stays listed for a retry, and an interrupted one is finished at the next start. Both later deletes force-deleted whatever was at the recorded path, proven only by "git does not list it and it has no .git", so an ordinary folder the user put at that path since was deleted with its files, with no click needed in the startup case (STA-9338). An accepted removal now records the checkout directory's identity (device, inode, creation time) in its durable record. A retry, a startup finish, and the in-slot check before the force-delete refuse any folder at the path that is not that directory. Startup no longer rebuilds a missing .git for a registered checkout unless the folder is the recorded one. Records without an identity (older builds, or no creation time) are only finished through Git's own checks and never force-deleted. * fix(worktrees): identify the accepted checkout by inode and a real creation time only The device number changes when an external volume is attached again, so a genuine leftover read as a different folder. Windows reports a missing creation time as a negative number rather than 0, which slipped past the guard. * refactor(worktrees): every background removal states the checkout identity it accepted A new acceptance site that forgot the identity used to compile and silently write records no retry or resume could ever finish. The resume tests also assert the identity was read, so their refusals can't pass as unrecorded. * fix(worktrees): a delete that leaves the user's folder in place also ends the workspace When Orca decided the folder at a failed or interrupted delete's path was not the one it started deleting, it dropped the removal record but kept the workspace's metadata. That metadata still says Orca created a checkout there, and with no record left, the next Delete of the same workspace (a runtime Delete from mobile, the CLI or a paired client, or a second desktop Delete on a row not yet refreshed) went to the cleanup that deletes an Orca-created folder Git no longer lists, and removed the user's folder after all. Now, when a removal ends without deleting while Git does not list the path (a listing ends a failed delete, or a retry, resume or first delete fails with no leftover to show), the host runs a finished delete's bookkeeping: metadata, terminal history, caches and change events. The folder and the branch are left alone, and nothing is ended while Git still lists the path. * test(worktrees): cover the in-slot folder check when Git cannot remove a registered checkout Reverting that check left every test green: only the unregistered branch's check had a test where the folder changes during the delete-slot wait. * fix(worktrees): a resumed or retried Git delete checks the folder inside the delete slot The check that the folder at the path is still the one Orca accepted ran before the watcher gate and the delete-slot wait, which can last minutes behind other large deletes. A folder restored there meanwhile with a copy of the checkout's .git file passes Git's own validation, so `git worktree remove --force` deleted it. The resume and retry now repeat the check inside the slot, right before Git deletes, as the non-Git branch already did; the refusal skips the Windows recovery that could otherwise finish the delete itself. A normal first delete is unchanged. * test(worktrees): assert the second desktop Delete refuses the user's folder The handler result is untyped, so the previous catch-all did not typecheck; the assertion now names the refusal the Delete gets once the workspace has ended. * test(worktrees): the desktop Delete test's stand-in host ends the workspace without a cast * fix(worktrees): a listing asks Git before dropping a failed delete, and ends its workspace in the same step * fix(worktrees): a failed delete Git cannot be asked about stays failed for the next listing to decide * test(worktrees): the failed-delete record test states that Git still registers the checkout * fix(worktrees): a restart keeps a failed delete whose checkout is gone, so the first listing ends its workspace * fix(worktrees): a folder Orca cannot read keeps its failed delete and is never deleted * fix(worktrees): a failed delete leaves the disk only after its workspace's end is saved * fix(worktrees): a runtime Delete of a failed delete's row is told when Orca left the folder in place * test(worktrees): the desktop Delete test's stand-in host returns nothing * fix(worktrees): a Delete of a failed delete's leftover whose .git Orca cannot read says so * Revert "fix(worktrees): a runtime Delete of a failed delete's row is told when Orca left the folder in place" This reverts commit 68d6598. The shortcut reaches no real client: the CLI and mobile list the workspace before deleting it, which ends the failed delete first, so they still answer not found (the folder is kept either way). * fix(worktrees): a failed delete Git could not be asked about is dropped once Git says it still registers the checkout * revert to main before the standard-model rework * fix(worktrees): stop deleting Orca-created folders that have no .git and that Git no longer tracks Deleting an Orca-created workspace whose folder Git no longer tracked and that had no `.git` force- deleted the whole folder once the user confirmed force. "Orca made a workspace here once" and "there is no .git" do not prove the folder still holds that checkout: an ordinary folder the user put at the path was deleted. Such a folder now gets the existing "Refusing to delete unregistered worktree path" answer, forced or not. Once the user removes the folder, Delete takes the existing missing-path branch and drops the workspace. An orphaned worktree whose `.git` file points back at this repo is still deleted after the user confirms force, as before. * fix(worktrees): delete a failed or interrupted workspace only through Git A delete that failed partway (Git dropped the registration, files left) stays listed as "Delete failed", and a delete a quit interrupted is finished at the next launch. Both later deletes removed the folder with Orca's own file removal, proven only by "Git does not list it and there is no .git". An ordinary folder the user put at that path was deleted, at startup without any click. Orca now deletes a checkout only through Git, plus the existing cleanup right after a Git delete that succeeded in the same operation: - Delete on a failed row: if Git registers a checkout at the path, the normal delete runs. If the folder is still there and Git does not register it, Delete refuses with "Git no longer tracks <path>, so Orca won't delete it. Remove the folder yourself; Orca removes this workspace once the folder is gone." and keeps the row. With the folder gone, Delete prunes Git's stale entry, deletes the branch as recorded and drops the workspace. - Startup finish of an interrupted delete: a checkout Git still registers is removed with `git worktree remove` and the user's recorded force choice, no longer always forced. If Git refuses or fails, the row shows Git's error. The step that rewrote a missing `.git` so Git could force the delete is gone. A folder Git does not register becomes a failed row and is not deleted; with the folder gone, the rest of the delete finishes. Records written by earlier daily builds still load; fields this version does not know are ignored. * test(worktrees): read Git's listing before the Delete call in the real-Git failed-delete suite * docs(worktrees): say when a failed delete's record ends under the Git-only delete * fix(worktrees): keep main's handling of a startup finish Git refuses, and say the listing drops the workspace A startup finish that Git refuses for a checkout Git still registers now clears its record and leaves Git's normal row, exactly as an in-session failure does on main; the resumed-only failed record and the listing that put Git's error on Git's own row are gone, and the listing is main's again. An unregistered folder that still exists is still a failed row, and nothing is deleted. The refusal now reads: "Git no longer tracks <path>, so Orca won't delete it. Remove the folder yourself, and Orca will drop this workspace from the list." The listing drops a failed record once its folder is gone, as on main. * fix(worktrees): finish a failed delete's branch and push-target remote when it fails A delete Git fails partway keeps a "Delete failed" row for the folder Git left. The row's record also carried the branch delete and the push-target remote cleanup for later, but the listing drops the record once the user removes the folder, so both were silently left behind. Now, once Git no longer registers the checkout, the failure runs that Git side right away (merged branch only, stale admin entry, push-target remote), in the one settle path desktop and runtime share. It deletes no files, and a failure of this bookkeeping is logged without touching the failed row. * fix(worktrees): unregister an interrupted delete's checkout whose .git Git already deleted A quit mid-delete can leave a checkout Git still registers but whose .git link Git already deleted (Git deletes it first on Windows). No `git worktree remove` can validate it, so the startup finish failed, the record was cleared, and the workspace came back as a normal row no Delete could ever remove. The startup finish now has Git prune that registration (refused while locked; every file kept) and then treats the folder as one Git no longer registers: the "Delete failed" row with the remove-it-yourself message, ending once the user removes the folder. Adds the non-forced partial-delete case: Git refuses, the files stay, and the row returns as before. * fix(worktrees): name a worktree Git registers inside a failed delete's folder before asking the user to remove it The refusal for a folder Git no longer tracks tells the user to remove the folder. When that folder holds another worktree Git still registers, doing so would delete that worktree's files too. Delete on the failed row and the startup finish now run the existing nested-worktree check first, so its message ("contains another registered worktree: <path>") wins, as it did before this branch. * fix(worktrees): don't prune other worktrees when a delete fails The Git side of a failed delete started with a repo-wide `git worktree prune`. It did nothing for the failed delete, whose admin entry Git had already dropped, but it unregistered any other worktree whose folder was missing at that moment (an unmounted drive), so Orca dropped that workspace too. The failure now runs only the branch step; the prune stays where it was on main, in the folder-gone finish and at startup. * test(worktrees): cover the push-target cleanup every delete job wires for a failed delete A failed delete removes the push-target remote Orca added through the hook each job creator passes: desktop Delete, runtime Delete, and the startup finish. Nothing tested that wiring; these tests fail when any creator drops it. * fix(worktrees): say the leftover folder was left on disk instead of what Orca won't do (cherry picked from commit 3825530) Backport to 1.4.224: kept 1.4.224's module names (orca-runtime-remove-managed-worktree.ts, orca-runtime-resolve-worktree-removal-target.ts, mixin classes without abstract declarations) and its SSH-aware signatures (isAlreadyRemovedWorktreePath(repo, ...), isRuntimeWorktreePathMissing(hostId, ...)); the failed-row retry stays local-only (!repo.connectionId) and is now awaited; dropped the removed isLocalGitRepository from the host-home test's module mock. The SSH orphan cleanup that 1.4.224 still has (fsProvider.deletePath) keeps the same .git-file-points-back-at-this-repo proof and force confirmation the PR keeps for the local orphan cleanup; the Orca-created no-.git leftover branch the PR removes was local-only on 1.4.224 and is removed in both the desktop and runtime paths. The RemovalRuntime retry-override test in worktree-removal-execution-host.spec.ts is not on this base, so its edit was dropped.
The startup permission test waits on real timers like the rest of its file. The transcript-dependency contract test hangs on 1.4.224's transcript readers under its fs spies (10 of 13 agents time out); it pins main's reader shape, so it stays on main only.
… chat (#29056) * fix(native-chat): resume conversations of any size in a new native chat Resume in New Native Chat refused any saved conversation over 16 MiB, so long Claude and Codex sessions could not move into a chat. The import now streams the file, bounds each message as it is read, and keeps the newest messages within the same 16 MiB budget; one line is still bounded at 16 MiB. The agent resumes the full conversation itself, so only the chat's earliest messages can fall outside the window. * test(native-chat): adopt preflight refuses one line past the record bound, not a large file * fix(native-chat): bound retained Pi import state and identities * fix(native-chat): keep Pi branch projection responsive * fix(native-chat): classify temporary Pi import table * fix(native-chat): keep Pi resume independent of import scratch cleanup * test(native-chat): match legacy Pi identity by shape (cherry picked from commit 7f492dc)
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
1.4.225 and 1.4.226 were rolled back after two P0s:
orca serve([Bug]: orca serve (orcad) runtime never gets account services — all accounts.* RPC fail on 1.4.225+ #28962).1.4.227 ships only the fixes that were asked for, built on the 1.4.224 base without the SSH and serve changes behind the rollback.
What's in (59 commits on
v1.4.224)Filed for cherry-pick (Slack / Linear):
P0 user fixes (Urgent tickets):
The last three came from a triage of all 238 fixes that reached 1.4.225/1.4.226 through main. The other high-priority ones are listed "on the fence" in the team doc and are not on this branch.
Already cherry-picked into 1.4.225: #28050, #26684, #27914, #27947, #27183, #27184, #28082, #28199, #28113, #28131.
Required for those to apply: 26 earlier commits, listed with the reason for each in the team doc.
Release-only commits:
@orca/process-host, which 1.4.224 doesn't have.How it was built
-x: everything except 10 commits. The 9 permission commits (Edit agent launch settings on the workspace host #26479, Native chat: every agent launches through one composed launch path #27204, feat(native-chat): require Codex 0.136.0 for chats #26684, feat(settings): move Chat UI's chat settings from Experimental to the Chat page #28050 and the 5 permission PRs) were hand-resolved against 1.4.224's launch code. feat(chat): host writes one saved row per subagent #27947 was resolved by hand too: itsreadChildWorkRecordswiring goes where 1.4.224 builds the status sink.StatsSnapshotWriter, now with an optional file mode) in place of main'sAtomicSnapshotWriter, which isn't on this base..gitproof and a confirmed force.Testing
Draft. Do not merge: this PR exists to run CI on the release candidate.