Skip to content

sync: absorb upstream v1.4.227 - #120

Merged
zpyoung merged 73 commits into
mainfrom
zpyoung/auto-sync-fork-with-upstream-stablyai-orca-ru-20261016T1400
Oct 19, 2026
Merged

zpyoung merged 73 commits into
mainfrom
zpyoung/auto-sync-fork-with-upstream-stablyai-orca-ru-20261016T1400

Conversation

@zpyoung

@zpyoung zpyoung commented Oct 16, 2026 •

Copy link
Copy Markdown
Owner
Files Added Deleted Net
Test 516 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​32742 $\color{#cf222e}{\Huge{\mathbf{−}}}$​18904 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​13838
Prod 793 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​20111 $\color{#cf222e}{\Huge{\mathbf{−}}}$​15768 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​4343

ELI5

This brings the fork up to upstream's newest stable release, v1.4.227. That release is v1.4.224 plus 71 picked fixes and features: per-chat permission modes, Resume in New Native Chat, browser popup fixes, per-subagent chat rows, and Claude per-account folders.

One thing about it is unusual. Upstream's tag carries main's CI workflow files, not the ones that built the release, so this PR keeps the release's own CI.

What Changed

  • Target: v1.4.227 at f51e82c4c2. Resolution: auto-ours+tree. Two delete-versus-modify paths were kept on the fork side at merge time, then retired, as described below.
  • Fork commits: all 592 are still reachable at their original SHAs.
  • Backup: origin/backup/main-20261016-140129Z at c923f8e30a.

The commit sync: resolve file ownership for upstream v1.4.227 does the ownership work:

  • The classifier lists:
    • 1,206 upstream paths reset to the tag.
    • 114 paths removed.
    • 183 exceptions restored.
    • 2,102 merge-review paths. Only 36 of those were touched by upstream in this range, and every seam residual matches its budget.
  • Exceptions upstream changed are three-way merged against v1.4.224:
    • worker-report-settlement.ts, runtime-terminal-state-records.ts, structured-agent-session-integration.test.ts, NativeChatComposer.tsx, en-runtime-required.json and pr-code-change-scope.test.mjs merged cleanly.
    • package.json keeps the fork's version line. Its only upstream change was the version.
  • CI workflows resolve to a220036892^, not the tag.
    • Upstream's a220036892 ("ci: carry main's workflow files so the release tag can be pushed") swapped every workflow on the release branch for main's. Its message says: "Workflow files don't ship in the app. The release is cut by the 1.4.224-era release workflow."
    • main has deleted the SSH relay (refactor(relay): delete the SSH relay server, its build assets and tests stablyai/orca#27215). Its workflows reference 24+ scripts and tests that v1.4.227 does not have, and they add a release step that refuses any ref still shipping the relay.
    • The tag's own pr.yml contract tests are unchanged from v1.4.224.
    • So upstream-owned workflows take a220036892^: v1.4.224's workflows plus upstream's four small CI adaptations for the picked code. The 13 main-only workflow files are dropped.
    • pr.yml, release-cut.yml and release-mac-build.yml take that release-branch delta. The fork already carried it.
    • The next stable tag cut from main resets all of this normally.
  • relay-windows-process-tree.yml stays. The fork's release cut calls it, and the fork still ships the relay. It is now a declared permanent exception.
  • The claude-host-auth-fallback-selection item is retired. It was pending-upstream. 98996ff46a (Claude per-account folders) deleted runtime-auth-sync.ts and the managed-credential fallback that could strip system auth, so the bug no longer exists. Both exceptions and the docs/fork-upstreaming.md entry are gone.
  • nativeChatPermissionMode is restored. -X ours had dropped it next to a session-handoff seam.
  • Tier-2 replay: upstream's per-chat permission picker (feat(native-chat): each Claude/Codex chat gets its own permission mode stablyai/orca#26116) now renders in the fork's AgentComposerActions and AgentComposerField copies. All 18 FORK-COPY-SHA headers now point at v1.4.227.
  • The RPC params catalog is regenerated.

Why

Taking main's workflows would run CI for a different codebase. Required jobs would call scripts the tree does not have, and the release cut would refuse every fork build because the fork still ships the relay. The release's own workflows are what upstream used to build this exact code.

Linked Issue

N/A. This is a scheduled upstream sync.

Visual Proof

N/A. This is an upstream merge. The one UI addition is upstream's own permission picker, now reachable in the fork composer, and it has not been checked in a running app.

Testing

  • The local gate passes:

    • pnpm install --frozen-lockfile.
    • --verify-seams: 1,247 lines.
    • --verify-residuals v1.4.227: 572 files.
    • The fork ownership guard, run on the committed head.
    • typecheck:node, typecheck:web and typecheck:cli, run one after another.
    • pnpm lint.
  • Tests are left to this PR's CI, per the sync skill.

  • Checklists:

    • Tier-2: 18 copies, two replayed.
    • Tier-4: 108 pending-upstream rows. Four had upstream movement, were three-way merged, and remain pending. One is retired, as described above.
    • Feature collisions:
      • Possible: automation-launch-settings, native-chat-session-options, native-chat-relay, session-handoff, terminal-dock and agent-composer. Agent-composer is carried by the tier-2 replay.
      • None: every other feature.
      • Decision needed: none.
    • Sweeps: deleted-exception, cross-version-wire and locale all came back clean.
  • I manually tested these changes locally

  • Automated tests added/updated, or explained why not below

Review

Agent skill upstream boundary

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

Notes

Checklist

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

Jinwoo-H and others added 30 commits October 15, 2026 11:38
…tablyai#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)
…ith the upgrade and proxy fixes (stablyai#26801)

* Revert "Revert Claude per-account folders (stablyai#24434, stablyai#26405) before release (stablyai#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 (stablyai#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)
…i#26821)

* 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 (stablyai#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)
…ablyai#26780)

* Let a Codex chat stop a command it left running in the background

A command Codex started and left running (a dev server, a watcher) showed in
the background-tasks strip above the composer with no Stop, because Codex
reported no stop it could honour. Codex 0.140+ has one: the app-server's
experimental thread/backgroundTerminals/terminate names the process by the
processId on the command's item, and thread/backgroundTerminals/list shows
what still runs.

The command tracker now keeps each command's processId. The first time a
running command carries one, the session probes its own app-server once with
a one-row list; a listing marks the app-server's backgrounded commands
stoppable and the session as offering per-row Stop and Stop all, through the
fields the strip already reads. Any refusal (an older Codex answers "unknown
variant"), timeout or malformed answer leaves it as today: no Stop.

A Stop terminates each named process, then confirms against the thread's own
list. A thread Codex says it unloaded counts as stopped; a dropped connection
does not, since lost contact is no proof a process ended. An app-server that
times out is not asked again. The killed process's own item/completed removes
its row, as before.

* Ask Codex again after a probe gets no answer, and say when a background Stop fails

A background-terminal probe that timed out or lost its request used to hide
Stop for the rest of the app-server's life. Only a refusal by name (unknown
variant, method not found) or a malformed listing now settles it as
unsupported; anything else is asked again on the next update.

A background Stop whose agent failed (a process still listed after its stop,
a timeout) used to answer "nothing stopped" and the button simply came back.
The host now refuses it, so both clients show their usual notice: "The
background task wasn't stopped." This covers Claude's background Stop too.

* Say a background Stop wasn't stopped only when the agent shows it still running

A timeout or lost contact with the agent leaves the outcome unknown, so it now answers
with the existing outcome-unknown refusal instead of "The background task wasn't stopped."

* Keep a background task's Stop in its stopping state until the row leaves

A confirmed Stop re-enabled its button for the moment between the host's answer
and the roster dropping the row. The button now holds while the host says it
stopped the task and the strip still lists it, on desktop and phone; a failed,
unconfirmed or nothing-stopped answer, or the row coming back, offers Stop again.

* Keep Grok's own subagent Stop refusal quiet, and a reply about another subagent unconfirmed

* Report Grok's own refusal of a subagent Stop as not stopped

Grok answers an ended or unknown subagent with a normal reply, so a JSON-RPC refusal means it did nothing and the subagent still runs.

(cherry picked from commit e2234a3)
…yai#26990)

* Catch Claude's sign-in link through BROWSER and wait 10 minutes for sign-in

A hidden claude auth login now runs with BROWSER pointed at a small script in the
account folder that hands the localhost-callback link back to Orca. Orca opens it
in the default browser as before, or hands it to Settings to copy when asked.
Windows host sign-ins keep Claude opening the browser itself.

* Add Copy sign-in link instead to Claude Add Account and Sign in again

* Only catch the Claude sign-in link when copying it

Plain Add Account and Sign in again no longer set BROWSER, so Claude opens the
browser itself exactly as before. A copy-link sign-in that cannot prepare its
BROWSER stand-in now fails instead of opening the default browser, a failed
chmod in WSL no longer stops the login, and a failed clipboard write shows the
existing copy-failed toast.

* End a copy-link Claude sign-in that gets no sign-in link, and simplify its plumbing

A missing or unexpected link now fails the sign-in instead of leaving a 10-minute
spinner. The link check moves to main and only tells the localhost link from the
pasted-code one; the poll is a plain loop, the pending link exists only for
copy-link sign-ins, and copyLink travels as a plain boolean.

* Pass the started Claude sign-in to the copy-link helper and give its copy-failed toast its own key

* Remove a partly written Claude sign-in script when setup fails

(cherry picked from commit 000cfe5)
…s credential (stablyai#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)
…#26976)

* 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)
stablyai#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 (stablyai#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)
…i#26460)

* 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 stablyai#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)
…26962)

* feat(native-chat): offer Codex Ultrafast as a speed choice

Codex lists Ultrafast as a third service tier for eligible models and
accounts (GPT-6.1 Sol). Codex chats saved Fast as on/off, which cannot
hold a third tier. The Codex speed is now a Standard/Fast/Ultrafast
choice built from the tiers the model lists; the tier id is still
resolved from the catalog at each send. Older clients keep their Fast
toggle: the host still reports Fast on/off and accepts it as a speed.

* refactor(native-chat): save the Codex tier id instead of a speed name

The chat now keeps the tier id Codex lists (serviceTier: default,
priority, ultrafast) and sends it as-is on turn/start; Codex drops a
tier the model no longer lists with its own warning. This removes the
saved per-model tier map, the send-time lookup and the reconcile code.
The unused fastModeTierByModel catalog field is deleted. Older clients
still get Fast on/off, derived from the model's tier named Fast. Also
drops the pnpm-lock.yaml churn the first commit picked up.

* test(pi): drop the removed fastModeTierByModel catalog field

* fix(native-chat): review round 1 for Codex service tiers

- Accept an older client's fastMode write through the Codex adapter; it
  was refused before reaching the tier alias.
- Bump the saved model catalog schema so catalogs from before
  serviceTiers re-list instead of offering a Fast toggle with no tier.
- Keep only the reported tier on the record, not the Fast it implies.
- Drop a reported tier, not only a picked one, when the next model does
  not list it.
- Localize Standard inside the speed choice only.
- Return the options answer directly instead of a one-field wrapper.

* fix(native-chat): review round 2 for Codex service tiers

Keep a tier Codex reports for the thread (it accepts unlisted ones such
as flex); read the legacy Fast tier beside an empty serviceTiers list.

* fix(native-chat): review round 3 for Codex service tiers

A reported tier the old model listed and the new one does not resets to
Standard on a model switch, and the reported tier itself stays pickable.

* fix(native-chat): keep a re-picked reported Codex tier

The thread's reported tier is never reset by the unlisted-tier drop, so a
re-picked Flex survives the next read, a model switch, and the turn.

* refactor(native-chat): one rule for which Codex tier a model offers

Standard, a tier the model lists, or the thread's reported tier that no
model lists. The setter and the unlisted-tier reset share it, replacing
the separate reported-tier and model-switch guards.

* test(native-chat): drop the removed fastModeTierByModel field from main's new catalog test

(cherry picked from commit c8b8997)
…ablyai#27026)

* Let a Codex chat stop a sub-agent from the background-tasks strip

A Codex sub-agent showed in the background-tasks strip with no Stop, and
the chat's own Stop interrupts only the main turn, so a helper ran on until
it finished. Codex's app-server takes `turn/interrupt` for any loaded thread,
sub-agent threads included, so a sub-agent row's Stop now interrupts the
turn Orca saw that sub-agent run, through the same background Stop route
Codex commands use.

Codex's -32600 refusal (no active turn, another turn, thread not found)
means the run the row showed is over, so the Stop is quiet; any other
refusal says the task wasn't stopped; a timeout or lost contact is
unconfirmed. A live run showed Codex holds an interrupt that lands as the
turn ends until the sub-agent's next turn ends, so a timeout on a run the
session has since seen end is quiet too. Older clients' `codex-agent:` rows
name no host record and stay unstoppable.

* Stop a Codex sub-agent's own sub-agents and commands with it, and hold Stopping per run

A sub-agent Stop interrupted only that helper's turn, so the commands it was
running (Codex keeps a turn's processes when the turn is interrupted) and the
helpers it had spawned kept going, and its row stayed in the strip. The Stop
now interrupts the helper's run and every descendant's run, then terminates
the processes those threads hold where the app-server has background
terminals. The interrupt goes through interruptCodexTurn, and a small
codex-background-task-stops module composes the sub-agent and terminal stops,
so the terminals module is terminals only again.

A Codex helper keeps one row id (its thread id) across runs, so a confirmed
Stop's held button carried over to the helper's next run. Both clients now
hold it for the run it was pressed on, read from the row's invocation.

* Test that a sub-agent Stop leaves siblings alone and skips terminates after a timeout

Adds direct tests of the per-run Stop hold (including an older host's roster),
matches the desktop hold comment to mobile's, and reuses the one 10 s bound.

(cherry picked from commit cbc565d)
…kground (stablyai#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)
…yai#26798)

* 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)
…ai#27415)

Co-authored-by: m4air <m4air@Mac.localdomain>
(cherry picked from commit 80d34c3)
…irst frame before the agent starts (stablyai#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)
…lyai#27212)

* 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)
…blyai#27228)

* 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)
…tablyai#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 (stablyai#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 (stablyai#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)
…lyai#27732)

* 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 (stablyai#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 (stablyai#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)
…27914)

* 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 (stablyai#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)
brennanb2025 and others added 23 commits October 15, 2026 11:38
…el pick can't reset it (stablyai#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.
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 (stablyai#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 (stablyai#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 stablyai#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.
…25075)

* 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 (stablyai#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)
Workflow files don't ship in the app. The release is cut by the 1.4.224-era
release workflow on brennanb2025/release-1.4.227-picks; this branch only
holds the code it tags.
…auto-sync-fork-with-upstream-stablyai-orca-ru-20261016T1400

# Conflicts:
#	src/main/claude-accounts/runtime-auth-service-materialization.test.ts
#	src/main/claude-accounts/runtime-auth/runtime-auth-sync.ts
Resets upstream-owned paths to the tag (1206), applies declared and
tag-side deletions (114), and restores fork exceptions (183), then:

- Three-way merges every exception upstream changed (base v1.4.224).
  package.json keeps the fork version line.
- Resolves upstream-owned .github/workflows to a220036^, not the tag.
  Upstream's a220036 replaced the release branch's workflows with
  main's and says so: "Workflow files don't ship in the app. The release
  is cut by the 1.4.224-era release workflow". main's workflows assume
  the SSH relay is gone (stablyai#27215) and reference 24+ scripts and tests the
  tag does not have, and the tag's own pr.yml contract tests are
  1.4.224-era. The 13 main-only workflow files are removed.
- Keeps relay-windows-process-tree.yml, which the fork's release-cut
  still calls while the fork ships the relay; declared as a permanent
  exception. pr.yml, release-cut.yml and release-mac-build.yml take
  upstream's release-branch delta, which the fork already carried.
- Retires the claude-host-auth-fallback-selection pending-upstream item:
  98996ff deleted runtime-auth-sync.ts and the managed-credential
  fallback that stripped system auth, so the bug no longer exists.
- Restores nativeChatPermissionMode, which -X ours dropped beside a seam.
- Replays upstream's per-chat permission picker into the fork's
  AgentComposerActions/AgentComposerField copies and advances all 18
  FORK-COPY-SHA headers to v1.4.227.
- Regenerates the RPC params catalog.
Copilot AI balanced review requested due to automatic review settings October 16, 2026 14:20

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@greptile-apps greptile-apps Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Your trial has ended. Reactivate Greptile to resume code reviews.

@zpyoung zpyoung mentioned this pull request Oct 19, 2026
6 of 9 tasks
@zpyoung
zpyoung merged commit 83fc809 into main Oct 19, 2026
63 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

8 participants