Skip to content

sync: absorb upstream v1.4.237 - #131

Merged
zpyoung merged 179 commits into
mainfrom
zpyoung/auto-sync-fork-with-upstream-stablyai-orca-ru-20261021T1400
Oct 22, 2026
Merged

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

Conversation

@zpyoung

@zpyoung zpyoung commented Oct 21, 2026 •

Copy link
Copy Markdown
Owner

ELI5

This brings the fork up to upstream's newest stable release, v1.4.237 (from v1.4.233). Upstream restructured a few files the fork hooks into (its agent instructions, the runtime capability list, the CLI typecheck config) and added two new code checks, so the fork's hooks were moved onto the new shapes and three fork files were adjusted to satisfy the checks. One upstream feature, Codex skill picks carrying the skill's file path, lands in a composer the fork replaces; that decision is with the owner (see the end).

What Changed

  • Target: v1.4.237 at 071747853f.
  • Resolution: auto-ours+tree. 30 content conflicts resolved by -X ours, then 4 "deleted by us / modified by them" tree conflicts by honoring the fork's deletion (all four are declared deleted: true), then everything by the manifest.
  • Fork commits: all 726 are still reachable at their original SHAs.
  • Backup: origin/backup/main-20261021-140159Z at c9feb5cc41.

Ownership classifier: 4,460 differing paths; 2,040 reset to the tag, 57 removed, 171 exceptions restored, 2,192 merge-review paths.

  • Removals of note: upstream deleted CLAUDE.md (chore: remove CLAUDE.md stablyai/orca#31242). The fork never declared it, so it goes; the owner has been asked whether to keep it as an exception (see the end).
  • Exceptions (ours.txt): no stale exceptions, no module splits. 21 changed upstream and were three-way merged against v1.4.233; 15 merged clean. The 6 conflicts:
    • package.json: the fork's version line. Upstream's new deps (@electron/osx-sign, tsrs, zod 4.6.7) and new lint steps merged clean.
    • pr.yml: both hunks sit in regions the fork's trimmed workflow removed (upstream's ratchet consolidation and Windows boundary list), so the fork side wins; the file is byte-identical to main's. The new ratchets still run through pnpm lint.
    • ci-dependency-download-cache.test.mjs: the fork's "no Windows lane" assertion stays; upstream's now-unused WINDOWS_BOUNDARY_TESTS import is dropped.
    • NativeChatComposer.tsx: upstream disables attach while a remote chat session is pending (fix(native-chat): open a New Chat's composer at once, before its host answers stablyai/orca#29358); replayed onto the fork's AgentComposerField after the props spread.
    • use-native-chat-paste-bridge.ts: upstream's new native-paste path for a given textarea (origin/mode) is combined with the fork's focus routing and image-only fallback.
    • pane-pty-visibility-bind.ts: both imports kept (fork quarantine, upstream hook-lane completion).
  • Release workflows: no toolchain bump in the range. release-cut.yml/release-mac-build.yml take upstream's new qualified-orcad-template.yml call and blob:none checkout; electron-builder.config.cjs takes the new batched mac signer (sign-mac-app.mjs, no identity in it).
  • Upstream identity sweep: five workflows gained blacksmith-* runners, all behind fast_runners/fast_release inputs that default to false or in upstream-only adhoc/dev-channel workflows. The fork's release paths call them without opting in, so nothing to change.
  • Undeclared-edit sweep (checkout.txt): none. Deleted-exception sweep: none still imported. Locales: all fork bundles have all 6 locales. Cross-version wire: no new bare upstream tag pins. Workflow-swap commit under the tag: not present (v1.4.237^ is a release fix, not a workflow copy).

Seams (merge-review.txt). 82 seam files changed upstream and were re-merged without -X ours. 59 merged clean; 23 were resolved by hand:

  • Disjoint additions, both sides kept: .gitignore, i18n.ts, runtime-rpc-lifecycle.ts, automation-definition-operations.ts, pty-exit-lifecycle.ts, apply-final-patch.ts, rpc/methods/index.ts, launch-agent-web-host-tab.ts, launch-agent-in-new-tab-web-runtime.test.ts, web-native-chat-api.ts, vitest-node-runtime-files.mjs (upstream's import scan replaces the deleted sqlite list), and the three store files (upstream's StructuredChatSleptAtSlice beside the fork's slices).
  • main-process-observers.ts: upstream folded the stats recorder into feedAgentSessionStats; the fork's transcript observer subscribes beside it.
  • settings-page-renderer.tsx: upstream wrapped the sections in a scroll provider; the fork's handoff section moves inside it (seam line re-indented).
  • native-chat-structured-composer-dispatch.ts: upstream's new skills parameter after the fork's attachment type (seam line gains a trailing comma).
  • native-chat-picker-items.ts: upstream's path-located skill rows (feat(native-chat): Codex chats list and run the skills Codex loaded stablyai/orca#30937) and the fork's plugin rows combined; the fork's byName/pickerSkills replace upstream's discovered map. Residual unchanged at +76/-21.
  • protocol-version.ts: upstream moved every capability into per-area modules (refactor(protocol): each area owns its published runtime capability list stablyai/orca#30641). Rebuilt from the tag plus the fork's registrations (ask, loops, heimdall, launch overrides, ledger.v1); +35/-0, as before.
  • AGENTS.md: upstream rewrote it. Rebuilt from the tag plus the fork's sections (the sandbox Test bullet, Running Tests, Typechecking, the Orca ledger, Fork Feature Structure). Residual re-baselined +145/-1 to +146/-1 (one separator line).
  • bundled-skill-guides.ts: regenerated.
  • Two seams retired: config/tsconfig.cli.json (upstream's CLI typecheck now follows imports from src/cli (build(cli): typecheck the CLI by following its imports, not a hand list stablyai/orca#30632), which reaches the two session-info files through claude/hook-service.ts) and structured-agent-session-options.ts (the fork's line fed an always-undefined observedAt to a function A chat's model, effort and Fast belong to the chat, not to a model stablyai/orca#30561 replaced, so it had no effect).

Tier-2 copies: 16 checked. All were recorded at v1.4.232 and none of their sources changed through v1.4.237; headers advanced to 071747853f.

Tier-4 pending-upstream: every item's fork delta is unchanged in size against the new tag, and every ledger target exists. Upstream absorbed none.

Feature-collision review: 1 confirmed, 4 possible, 28 none. Details are in the owner question at the end.

Fixes after the merge, one commit each:

Why

Merging the stable tag keeps every fork SHA permanent. Replaying fork hooks onto upstream's new file shapes keeps the fork's footprint in upstream files small, where defending the old shapes would make the next sync conflict again.

Linked Issue

N/A. This is an upstream sync.

Visual Proof

N/A. No fork UI changes; upstream's own UI changes ship as released.

Testing

Local gate on 9c07953a00: frozen install; --verify-seams (1,327 lines across 656 files) and --verify-residuals (571 files) pass; typecheck node, web, cli and process-host tests run one at a time, clean; pnpm lint passes; clean tree; check-pr-verify-selections, the root-directory guard and the fork ownership guard pass; the preflight replay passes every step it can run. Tests are left to this PR's CI.

  • I manually tested these changes locally
  • Automated tests added/updated, or explained why not below

Fixes from the first CI run (head 9c07953a00)

Three unit shards failed deterministically; each fix is its own commit, and all four files pass on the test sandbox.

  • 2f836623fd: v1.4.237's new sign-mac-app.test.mjs asserts the hourly, daily and adhoc mac channels sign. The fork's builder config ships only ORCA_MAC_RELEASE, so the test is a permanent exception that keeps every assertion for that channel and pins the rest unsigned (the same shape as release-windows-signing-gate.test.mjs in sync: absorb upstream v1.4.233 #129).
  • 6ce5d79ff3: the fork's commit-message plan test expects upstream's new --strict-mcp-config --settings {"disableAllHooks":true} on background Claude runs (Background Claude runs no longer start your MCP servers and hooks stablyai/orca#30939).
  • 1353299e61: the fork's capability-gating test matches upstream's one-argument createWebRuntimeSessionTerminalResult. The seam budget drops to +27/-0 (703ea291a3).
  • c30eddd764: the dock test's @/i18n/i18n mock gains i18n: { language: 'en' }, which upstream's new createLocalizedCatalog reads at import.
  • persistence (macos-15-intel) is advisory. It failed on 9c07953a00 with a network connect timeout and passed on a206bd4462 with no change.
  • a206bd4462: CLAUDE.md restored and declared a permanent fork exception (see Decisions).

CI on the merged head (a206bd4462)

PR checks passed is green. Advisory checks still open at merge: WSL2 distro (debian-12) pending (WSL orcad E2E). desktop_template skipped.

Decisions

The owner's rule: keep every intentional fork feature, and check in only before dropping one. None is dropped.

  1. agent-composer, Codex skill file paths (confirmed collision): upstream's Codex skill picks (feat(native-chat): Codex chats list and run the skills Codex loaded stablyai/orca#30937) send each skill's file path from the rich editor's prompt document. The fork composer is a textarea, so a Codex skill picked there goes out as $name text only. Deferred to a follow-up PR (ledger deferred-326) so this sync can land; no fork behaviour is lost.
  2. CLAUDE.md: kept as a permanent fork exception (a206bd4462). Upstream deleted it (chore: remove CLAUDE.md stablyai/orca#31242), and without its @AGENTS.md line Claude Code stops loading the fork's agent rules.
  3. Possible collisions, kept as adapted: skill-plugin-attribution (path rows and plugin rows coexist), native-chat-session-options (retired no-op seam), workspace-activity-window (stacks with upstream's slept-workspace hiding), session-info (tsconfig seam retired).

nwparker and others added 30 commits October 20, 2026 05:38
… Server focus, and a no-row listing is this app's (stablyai#31119)

* fix(renderer): a twin repo id refuses instead of following the Active Server focus, and a listing for a repo with no row is this app's

findRepoOwner no longer lets focus pick between same-id repo rows: runtimeTargetForRepoOwner refuses a
twin, as resolveOwner does, and getRuntimeEnvironmentIdForRepo names no server for one. repoHostId
stamps and routes a repo with no row as this app (or a web client's only server), never the focused
server.

* fix(renderer): a fork names its source's host, and GitHub routing keeps its twin rule until it can refuse

* fix(renderer): a fork captures its source's server transport, and a GitHub edit on a twin refuses instead of using this computer

* fix(automations): a new-per-run workspace is created on the run's captured host

* fix(automations): pass only a captured run host, so a twin without one refuses

* test: stamp the server repo row through a store helper, prune the focus-read baseline
…dow's live group (dark) (stablyai#31123)

Behind ORCA_LAYOUT_OWNER_IS_MAIN, a CLI or phone file open is applied by main as an
openEditorTab layout command, so a host with no window can open files. The window
reports each workspace's active group as an optional view fact on syncWindowGraph,
and shows main's tab once the frame that draws it arrives. Editor language
detection moves to shared so main can name a file's language.

Co-authored-by: E2E Test <e2e@test.local>
…yai#30415)

* perf(editor): watch only active, recent and at-risk workspaces

Background workspaces no longer hold a whole-tree native file watch. The
active workspace, the 3 most recently visited ones, and any showing a
deleted/renamed tab keep theirs; every other workspace's subscription is
released (local, WSL, SSH via the relay, and runtime hosts alike).

When a dropped workspace is watched again, its tabs are verified against
disk through the existing paths: one overflow resync once the watch is in
place reloads clean tabs and refreshes git status, Files and markdown
lists; dirty tabs go through the restored-tab disk check, which shows the
changed-on-disk banner. Saves stay guarded by the host-side disk check, so
nothing is held while unwatched.

Also: watch requests are shared per directory and host; root events reach
every workspace on that root and SSH payloads carry their connection id;
late SSH and local/WSL capacity-retry installs send an overflow resync.

* fix(editor): catch up combined reviews, deletions, late installs and Files trees after a rewatch

- Overflow catch-up drops cached combined-diff bodies for the workspace and
  re-reads every loaded section of a mounted review (snapshot-backed too).
- Clean edit tabs are probed on the owning host first; a file proven gone is
  marked deleted and keeps its last content instead of reloading into an error.
- Dirty tabs are checked from the overflow itself, so the check runs after the
  watch is installed, including late capacity/SSH installs.
- Local/WSL Files trees also take the renderer-synthesized catch-up.
- Deleted/renamed-tab pins are capped at 3 beyond the active and recent set.

* fix(editor): catch-up keeps unsaved combined-diff edits and drops stale existence answers

- Cached Changes views reset only clean loaded sections; dirty sections keep
  their drafts and baselines (also for per-path invalidation).
- A mounted review re-reads loaded and in-flight sections on catch-up.
- Existence answers carry a per-tab generation; a newer probe or watch event
  for the tab supersedes an older answer.
- A tab is marked deleted only on a per-path ENOENT, never on a whole-request
  not-found or an old host's scalar fallback.

* fix(editor): re-check dirty tabs requested mid-read, prove runtime deletions, chunk existence probes

- A disk check requested while one is in flight is followed by one fresh read
  (coalesced) instead of being answered by the older reply.
- The restored-tab check probes runtime-owned files on their host; a per-path
  ENOENT marks the tab deleted. After a retry ceiling it stops, clears the
  suspension and shows the changed-on-disk banner.
- Overflow existence probes are split into host-sized chunks, in order.

* fix(editor): a reload that finds the file gone keeps its content; stale absence answers re-read

- A tab that already read its file and now gets a path-specific ENOENT on reload
  is marked deleted and keeps its last content, with or without an existence
  probe, so older paired servers are covered.
- The restored-tab check applies its request generation after the absence
  probe too, so an older answer cannot resolve a newer request.
… the next startup reconnects (dark, layout-owner build only) (stablyai#31118)

Main runs one pass at startup and each window load, with or without a window
(desktop, orca serve, orcad): every live local terminal no pane names is bound
to the pane its record names while main still holds it free, else shown in a new
tab bound to it. The window no longer asks main to adopt terminals; it only reads
that main holds them. The window's layout.apply loses its adopt field.

Main also derives activeWorktreeIdsOnShutdown, remoteSessionIdsByTabId and
activeConnectionIdsAtShutdown from its own layout, terminal records and SSH
connections, replacing the window's copies in every save and the quit checkpoint.

Co-authored-by: E2E Test <e2e@test.local>
…l rows (stablyai#31127)

stablyai#30037 made the headless row builder emit customTitle on every terminal row; the Linux baseline
recorded before it (stablyai#27688) never listed the field, so the spec fails on main with either layout
switch.

Co-authored-by: E2E Test <e2e@test.local>
…#31108)

* ci: the manual switch-on leg runs the headless serve, paired-host and serve-restart specs

With H1's switch-on server template, these exercise main as the layout owner with no window.
Spec paths are exact: the changed-spec job greps each file, so prefixes only work for ssh-orcad-.

* ci: keep the Windows serve job out of the switch-on leg; it builds switch-off

* ci: the switch-on leg also runs the server-workspace tab composition and serve-restart binding specs

---------

Co-authored-by: E2E Test <e2e@test.local>
…seded or closed (stablyai#31128)

Follow-up to stablyai#30415. A watch event that superseded a pending overflow existence
probe left the tab's entry behind forever, and the dirty-tab disk-check request
map never removed anything. Superseding now deletes the entry, and closed tabs'
disk-check requests are dropped by the scan's existing open-tabs subscription.
The disk-check map uses a global sequence so a reopened tab can't reuse an old
read's number.
… sidebar feed, installed with the rows (stablyai#31102)

* fix(sidebar): the phone reads projects and project groups from main's sidebar feed, installed with the rows

The phone built its project sections from a catalog the desktop window published through a
side channel (mobileRelay.projectSections). That copy changed on a timer after the event that told
the phone to re-read it, and a re-read asked for during a read in flight was dropped, so the phone
could keep an old grouping (a project under the wrong group, a group missing) indefinitely.

Main now publishes the catalog (repos with their group and order, project groups, projects and
host setups, every host's including paired servers') as a projectCatalog domain of the sidebar
feed, from its own store and its own reads of each paired server. The phone reads it from the same
install as the rows. mobileRelay.projectSections stays, answered from the same builder, for pages
that predate the domain; a page on a desktop without the domain still reads it, now re-reading
when an invalidation lands mid-read. The phone e2e list helper runs the page's real refresh
lifecycle instead of re-reading on every assertion, and covers a group move while a read is held.

* test(mobile): stand AppState in for the screen hooks' react-native mocks, and tolerate a world with no project rows in the parity catalog

* fix(sidebar): main files a server's copy of a grouped project under its group, and reads a server's catalog only on catalog events

- The window gives an ungrouped paired-server repo the group of an existing copy of the same project
  (applyInheritedProjectGroups). That function now lives in src/shared and main's catalog builder
  folds each server's repos through it, so the phone files the copy where the desktop does.
- A paired server's change watch now says whether the event can carry a catalog change. Only
  ready, a replay and reposChanged mark the catalog stale; worktreesChanged (pins, unread, sort
  stamps) reads rows only.
- The parity fixture now builds the server side from raw server answers, folded through the
  window's mergeFetchedReposForHost and through main's own server read and builder, with a server
  copy of a grouped project and a partial saved manual order. It fails without the inheritance.

* fix(sidebar): parse a paired server's catalog rows with a schema instead of reading keys reflectively

* fix(mobile): sync the feed ref after render and re-read queued project sections without an await loop

* fix(mobile): spell the AppState shape the refresh reads, so the e2e helper pulls in no React Native types

* test(sidebar): give the merged builder test's feed domains a project catalog

---------

Co-authored-by: E2E Test <e2e@test.local>
…(dark, layout-owner build only) (stablyai#31131)

* feat(layout-owner): a layout-owner server advertises that it takes a paired desktop's layout intents (dark)

status.get adds layout.intents.v1 only in an ORCA_LAYOUT_OWNER=main build. It versions the
layout.subscribe frame shape and the layout.apply contract a paired desktop will rely on.
Tests pin the layout methods open to a paired desktop and closed to phones, and that an older
desktop still reaches every session.tabs mutation on such a server.

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

* test(layout-owner): read the paired-desktop frames and seed the server without casts

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

* review: the capability comment says only what the code does, and tests pin each next check

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

---------

Co-authored-by: E2E Test <e2e@test.local>
Co-authored-by: Claude <noreply@anthropic.com>
…rminal ready at once in the layout-owner build (stablyai#31132)

In the layout-owner build, main's load binds the pane again from its tab row while the
terminal is alive, so the replacement host publishes the pane ready and never pending.
The serve-restart spec waited for a pending frame that this build correctly never sends.
It now expects pending only in the current build, and asserts no pending frame in the
layout-owner build. A unit test pins the precondition on a windowless host: main's own
writes leave no shutdown list, and a real store load restores the binding.

Co-authored-by: E2E Test <e2e@test.local>
…'s file open to succeed (stablyai#31138)

Co-authored-by: E2E Test <e2e@test.local>
…a reader built without the switch applies a layout-owner host's order (dark, layout-owner build only) (stablyai#31140)

Co-authored-by: E2E Test <e2e@test.local>
…ote itself (stablyai#31144)

Guards the finding behind stablyai#31132: on a host with no window, a profile written only through
main's own commands (create, split, close, sleep, and a pane never started) reloads with the
layout-owner switch on and keeps every tab row, pane, binding, tab-bar entry and sleeping
agent. Main lists every tab it creates in the tab bar, so the load sweep never needs a
shutdown list to keep them.

Co-authored-by: E2E Test <e2e@test.local>
…uilding from its whole session (dark, layout-owner build only) (stablyai#31152)

* The window shows each layout records frame record by record, without reading its session back (dark, layout-owner build only)

A records frame no longer re-serializes the window's whole session and runs the
Loader. Panes' terminal bindings and live titles apply alone, to the tabs the
frame names; any other record builds only its own workspace's slices, with the
view and facts read from the store. Only a workspace the store does not show
from frames yet (startup, a reopened stream, a first visit, one sent whole)
takes the full build.

Tests install an apply check that, after every flush, compares the store with
the full build for the same cache and pre-flush store; the shared layout-apply
transport now sends main's records frames and fails a flush that differs.

* A frame of only tab rows shows without reading the window's session back or running the Loader (test, layout-owner build only)

* The account-restart test's fake binding runtime carries per-pane binding generations, so it passes with the switch on (test only)

* The account-restart test types its store writes, so its five old e2e type errors are gone (test only)

* A workspace build reads the editor selection and groups through the save's own writers, as the full build does (dark, layout-owner build only)

* A tab released from the window's hold shows the binding main wrote meanwhile (test, layout-owner build only)

* The frame-apply check still checks a flush's other workspaces when it builds one in full, and the shared transport keeps a test's own check (test only)

* The frame-apply check is proven to catch a mismatch beside a full build, and the shared transport restores only the check it installed (test only)

* Name the pane tree the window compares by what it holds: the tree without its panes' bindings or live titles (dark, layout-owner build only)

---------

Co-authored-by: E2E Test <e2e@test.local>
… paste, drop or attach (stablyai#30815)

* feat(native-chat): a queued message's Save can replace its images (host, wire and capability)

* feat(native-chat): edit a queued message's images on desktop: remove, attach, paste or drop

* refactor(native-chat): share the queued edit's image rules and drop the rarely-shown Save reasons

* fix(native-chat): a queued edit's refusal says the edit wasn't saved, and its wire takes a full image set

An expired upload refused on Save read "The command didn't run": the queued
message update fell through to the command write kind. It now has its own
kind whose sentence is "Your edit wasn't saved." The images param caps at
MAX_BLOCKS, matching the shared edit rule that counts them with the text.

* fix(native-chat): a queued edit keeps image markers right and owns drops and pastes on its card

- Removing a kept image drops its [Image #k] marker from the edit's text and
  renumbers the later ones (shared rule, reused by the phone), so the agent
  never reads a marker that names the wrong image.
- A file dragged from Orca's own tree onto the editing card goes to the edit,
  like a Finder/Explorer drop; the chat box overlay no longer covers the card.
- Cmd/Ctrl+V with focus on the card's buttons goes to the edit, not the chat box.
- A "changed" answer says "Your edit is in the chat box" only when something
  went there; otherwise it says the edit wasn't saved.
- The empty hint counts an uploading image as content and is not shown for
  agents without image input; the list grows only while thumbnails show.
- Every hand-off releases the edit's preview URLs.

* fix(native-chat): an in-app drop on the editing card goes through the chat box's owner check

The edit no longer runs its own copy of the in-app drop check, which resolved
the target host through a path that can read the Active Server setting. The
chat box's claim checks the drop and routes it by the chat's owner as before,
then hands the files to the editing card's sink when the drop landed on it.
Also: the kept-image id list is readonly where it is read, and the marker
rule looks up the remaining images in a Set.

* fix(native-chat): only an edit that takes images keeps its card's pastes, and no drop joins a Save in flight

The pane paste bridge no longer skips every paste inside a queued editor: the
card's own paste handler already claims image and file pastes, which the bridge
respects. A menu paste is left to the card only when the edit takes images (the
card is a drop owner), so an older host or an agent without image input behaves
as before, and text pasted with focus on the card's buttons goes to the chat box.
An in-app drop handed to the card while its Save is in flight adds nothing.

* fix(native-chat): a failed capability re-check keeps the host's last answer, and an edit takes no files before its lease

A re-check after regained contact that failed wrote "unknown" over a known
answer for the same host, so features the host had reported (such as editing
a queued message's images) vanished on a connection blip, mid-edit. A failed
check now keeps the last answer; a different host still starts at unknown.
Paste, Attach and OS or in-app drops into a queued edit are now blocked while
it acquires its lease, as typing already is.

* fix(native-chat): an image change is never saved as text alone when the host stops reading as taking images

Reverts the shared capability hook to keep main's behavior: a failed re-check
reads as unknown. Instead the queued edit derives from live state that an edit
whose image set changed can't be saved while the host doesn't read as taking
images, so a connection blip never saves renumbered markers over the old
images or drops added ones. Save comes back once the host answers again.

* fix(native-chat): keep queued image markers and drop feedback in sync

* fix(native-chat): derive queued image markers without changing edit text

* test(native-chat): the pane drop test's intake append returns the paths it didn't take

* fix(native-chat): route menu paste to the focused queued edit

* fix(native-chat): honor queued edit paste ownership before focus redirection

* fix(native-chat): preserve native paste without clipboard access

* fix(native-chat): keep paste in the owning queued edit
…ead of a split service cap (stablyai#31178)

The deploy passed `--max`, which is gcloud's service-wide cap. Cloud Run divides
that cap among the revisions taking traffic, so during the 20/80 stage a
candidate under a cap of 5 got one instance while the serving floor kept four,
and the spread check could never see the full floor (deploy M, 1 of 5 for six
minutes). Candidates now pin `--max-instances` per revision, and the deploy
resets a service cap below two full revisions to Cloud Run's default before it
creates any candidate. The rollout overlap of two full directors is already in
the checked Cloud SQL connection budget.
…yout stream (switch-on builds only) (stablyai#31181)

* review: shorten a test comment; the methods file header names layout.apply

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

* wip(h5b): a lost remote layout stream reopens with the local backoff

* wip(h5b): server frame id translation (pending the binding handle)

* wip(h5b): frame syncs take a source; the mirror gates frame-drawn server workspaces and indexes handles; ids translate with session.tabs handles

* wip(h5b): one layout stream per capable paired server, handing workspaces between frames and the mirror

* wip(h5b): tests for server frames; layout.getRemoteSnapshots returns only paired servers (L18)

* wip(h5b): re-gate a server workspace when its routing arrives after the snapshot; e2e hook and spec

* wip(h5b): late-owner test; e2e spec on the switch-on leg

* wip(h5b): server streams follow every paired capable server, not the focused one; a known-kind guard without a cast

* wip(h5b): server-frames tests read real paired-server state; no casts

* fix(h5b): a handed-back workspace is the mirror's alone; frozen frames draw only gated workspaces

* fix(h5b): a workspace rejoining the gate takes the full build

---------

Co-authored-by: E2E Test <e2e@test.local>
Co-authored-by: Claude <noreply@anthropic.com>
…only the copy sent to clients (stablyai#29982)

* fix(native-chat): save every chat reply and tool output whole; bound only the copy sent to clients

Orca cut Codex replies, every agent's reasoning, plans, prompts and all tool
output to a 16 KB head when it saved them, and threw the rest away. The
limit existed so a reconnecting client is not replayed a huge row, but it
was applied at save time, so every transport limit became permanent data
loss.

The journal now keeps what the agent said. One outbound form, applied to
every history page, live batch and catch-up, sends a client the same 16 KB
head of each tool output and diff it always got, and marks the item
`shortened`. A new capability-gated `agentSession.item` RPC returns the
whole item, and the desktop and phone show "Show full output" / "Show full
message" when the copy was cut. Items past half a page are shortened and
marked instead of replaced with "too large to show".

Two bounds stay, both named: a 4 MiB ceiling per text (head and tail),
applied where every provider's rows enter the write queue so one write can
never stall a chat, and a preview on Grok/OpenCode/OMP/Pi calls while they
run, whose closing row is built from the whole value the translator held.

* test(native-chat): an over-budget history item is shortened and marked, not replaced

* fix(native-chat): format, keep the sink and projection under the line limit, update method count and tests

* fix(chat): preserve complete writes and keep detail current

* test(chat): align final tail reservation with captured content

* Prototype source-batch journal ownership for Claude

* Pin the resolved iterator promise in ownership experiment

* Revert "Pin the resolved iterator promise in ownership experiment"

This reverts commit bdb49ab.

* Revert "Prototype source-batch journal ownership for Claude"

This reverts commit e5ed387.

* Preserve large received chat content while pacing input

* Verify received chat writes wait for journal persistence

* Close test journal databases before removing temporary roots

* Exercise item detail in the cross-version chat wire manifest

* Pin complete saved output and fitted client publication

* Exercise Pi exit retention with real journal write pressure

* Settle terminal web test unmount before DOM teardown

* Reuse existing readers and contract support in session tests

* Reuse measured chat item sizes in history pages

* fix(chat): derive page form type from reader contract

* fix(chat): open phone user messages while loading full text

* test(native-chat): reuse imperative context provider construction

* test(native-chat): format full-content disclosure fixture

* ci: preserve unit shard diagnostics after a timeout

* ci: retain unit shard output when tests time out

* ci: expose unit shard watchdog and output stages

* Revert "ci: expose unit shard watchdog and output stages"

This reverts commit 0b4991b.

* Revert "ci: retain unit shard output when tests time out"

This reverts commit 87eec4c.

* Revert "ci: preserve unit shard diagnostics after a timeout"

This reverts commit 1888c65.

* test: verify complete Claude tool output without large failure diffs

* Simplify reply extraction in cross-version session test

* Restore full message recovery for grouped phone thoughts

* Target recovery button in grouped thought fixture

* test(native-chat): count agentSession.item alongside main's new structured method

* fix(chat): reuse strict session subscription schema

* test(chat): open Codex before checking received content
…hell named only on its tab still runs (stablyai#31093)

* fix(ssh): an old SSH host converts while a relay shell named only on its tab still runs

The conversion check counted legacy pane-key aliases as terminals waiting to
reconnect. An alias only routes hook events to a pane whose terminal the lease
check, the bridge or the terminal gate already accounts for, so it no longer
blocks the move.

* Tighten the alias census comment

* Register the tab-only relay spec: typed deployment lookup, relay fence baseline, PR route

---------

Co-authored-by: E2E Test <e2e@test.local>
…esktop again, not an SSH setting (stablyai#31120)

Every orcad turned on the SSH host browser bridge, so since open-url began going through it
(stablyai#30849) a user-started serve with no desktop refused with SSH-settings advice. Only a client's
managed launch enables the bridge now; a started server answers browser_host_unavailable (stablyai#30782).

Co-authored-by: E2E Test <e2e@test.local>
… it ends (stablyai#29007)

* fix(mobile): reopen the phone chat's live feed from its last row when it ends

The phone opened a chat's live transcript stream once per connection and never reopened it when the
stream ended or failed while the connection stayed up, so the chat silently froze on old rows. Move
desktop's reopen-from-cursor read into src/shared and run the phone on it too, with an app-foreground
retry.

* fix(mobile): retry the chat feed on the app-resume signal client-context already relays

Module mocking react-native in the shared vitest setup is rejected; publish the existing revival
nudge instead, the way desktop's host-contact signal feeds its read.

* test(mobile): let the phone's coalesced live read apply each frame before comparing
…ai#28983)

* fix(mobile): keep the chat composer's controls inside its row

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

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

* style: format the permission pill test

* fix(desktop): keep the permission icons a static record
…fter 30 seconds (stablyai#30395)

* fix(codex): opening or resuming a Codex chat waits for Codex instead of failing after 30 s

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

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

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

* test: accept nullable Codex request timeout in probe fake

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

* Remove obsolete Codex startup timeout scenario
…ablyai#30939)

* fix(claude): keep Orca's background Claude runs off the user's MCP servers and hooks

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

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

Tracked in STA-11095 and stablyai#30411.

* fix: centralize background Claude generation and retry policy

* fix: rebuild Claude background retries from generated arguments

* test: include branch in generation retry fixture

* fix: preserve user settings arguments in background generation

* test: define retry fixture streams across runtimes
…tablyai#30561)

* fix(native-chat): a chat's model, effort and Fast belong to the chat, not to a model

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

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

Tracked in STA-10947.

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

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

Tracked in STA-10947.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

This reverts commit e633d20.

* test(native-chat): create-path attach assertions check the params argument, not the arity
…blyai#30935)

* perf(cmd-j): preload the jump palette so its first open renders without React's reveal delay

* fix: reuse lazy retry wrapper for preloaded components
nwparker and others added 18 commits October 20, 2026 22:30
* Parallelize adhoc Mac architecture packaging

* Include parallel Mac jobs in release timing reports

* Cover parallel Mac publication and failure gates

* Explicitly scope archive targets to each Mac architecture
* fix(sidebar): a slept chat workspace reads asleep and hides

* fix(sidebar): a wake during Sleep's teardown keeps the workspace awake

* test(sidebar): give the activation-race mock store the chat tab map Sleep reads

* feat(native-chat): the host marks the chats an explicit Sleep stopped

The fact "the user's Sleep put this chat to rest" now lives on the host
that runs the chat, on its session record, instead of in the window.

- New runtime capability agent-session.workspace-sleep-mark.v1 and an
  optional markAsleep on agentSession.sleepWorkspace (strict params, so
  only hosts advertising the capability are sent it).
- The Sleep close decides each chat's receipt inside the same per-session
  step that stopped it; with markAsleep it writes sleptAt in that step's
  record write, so a chat is marked exactly when it is in the receipt.
- The next send the host hands over clears sleptAt, whichever client sent
  it; the idle sweep and plain stops never mark.
- sleptAt rides the status summary (optional field), its equality, and
  the retained summary of a closed chat, so it survives restart through
  the persisted tab index.

* refactor(sidebar): read a slept chat from its host's mark, not a window record

The window no longer keeps its own saved record of which chats Sleep
stopped (explicitlySleptChatsByWorkspace). That record had to mirror every
host event that should end it (sends, replaced conversations, renames,
profile and WSL moves, removal, wakes during teardown), and each missed
mirror was a bug. The host now owns the fact (previous commit).

- Delete the record, its schema, store slice, workspace-session field
  registrations, moves, renames, purges, migrations and hydration, and the
  renderer clearing hooks. Those files are back to main.
- Opening a workspace no longer wakes its slept chats (product ruling):
  drop the chat Sleep-attempt tokens and their set-active-worktree hook. A
  slept chat wakes only when a send reaches its host, from any client.
- The status bridge mirrors each chat tab's published sleptAt into a
  store map keyed by the chat's pane key, dropped when its projection
  unmounts and never saved; a chat whose host runs it again is not slept.
- The asleep derivation, hide filter, palette, board and context menu read
  that map. Explicit Sleep sends markAsleep only to the local host and to
  hosts last probed with the new capability; a project move never does.

* fix(native-chat): preserve host-owned Sleep across fallback and legacy tabs

* fix(native-chat): clear Sleep when a new send is admitted

* chore: restore incoming documentation after merge hook formatting

* fix(native-chat): let admitted sends supersede pending Sleep receipts

* fix(sidebar): wait for host Sleep state before showing restored chats

* fix(sidebar): retain host Sleep answers when chat projections remount

* fix(native-chat): clear Sleep when queued drafts become sends

* test(sidebar): cover unknown and known chat Sleep state on the board

* test(vault): expect both host status capability checks

* test(native-chat): consolidate Sleep behavior coverage

* fix(native-chat): commit Sleep wake with send admission

* test: pin visibility while chat host is unidentified

* test: consolidate workspace sleep regression flows

* test: keep status feed regression within renderer boundary
…ablyai#31188)

* fix(mobile): remove the chat's Tools toggle and its open-everything mode

* test(mobile): keep the scroll-ownership tests' runs drawn without the Tools default

* test(mobile): the scroll-ownership harness opens settled turns as the old Tools default did
…ts hook turn only (stablyai#30620)

* fix(notifications): tie a completion banner to the hook turn it announces, not a 10 s window

A title-only working->idle edge (Claude's /btw side question fires no hook) raised a
generic "Task complete" banner because the pane's hook row was older than 10 s. On a
hook-reporting pane the hook row now decides: a row not yet announced supplies the
banner content however old it is; an already-announced row means no turn ended, so
no banner. Hookless agents keep the generic title fallback.

* fix(notifications): a pane with hooks gets its completion banner from its hook turn only

The title lanes (mounted pane and parked watcher) announced a finished task on every
working->idle title edge, even on panes whose agent reports turns through hooks. A title
spin that ends no hook turn (Claude's /btw side question, Codex's /compact) raised an extra
banner that could only be filled with the workspace name and tab title.

On a pane the hook lane is tracking, with a settled Done row from the same agent, the title
lane now defers before it claims the turn, so the hook lane's banner (with the last message)
is the only one. A BEL in the same burst waits on the hook lane's pending Done. Hookless
agents, restored rows, hooks switched off and the post-restart re-arm keep the title banner.
This replaces the previous commit's announcement memory in the dispatcher.

* fix(notifications): hook-owned turn ends settle pane bookkeeping and announce re-entered Stops

- A title idle edge that defers to the hook lane now settles the pane's turn, so quitting the agent after a /btw no longer raises a generic "Task complete" banner.
- A BEL held while the hook lane's Done is pending is superseded by that Done at fire time instead of being held until the next turn start.
- A Stop re-entered after a blocking Stop hook carries its new final message in its identity, so the hook lane announces it instead of deduping it against the first Stop.
- Title lanes ask the hook lane whether it can announce the turn end (live, agent evidence, not re-arming) instead of copying its conditions, and fall back when the agent's own hooks are switched off.
- The hook lane's non-quiet Done forwards its own snapshot, so its banner content no longer depends on the stored row's age.
- The whole-flow test now drives the real pane wiring, the real hook lane and the real dispatcher.

* fix(notifications): the host marks each real turn end, so a repeated status can't raise a second banner

The renderer told a Stop re-entered after a blocking Stop hook from a
restatement by its final message. The message also changes when the
listener's cache resets (a session reset, then an idle reminder), so a
restated finished turn raised a second, empty "finished" banner.

The execution host now classifies each provider's own turn-end event by
name and stamps an optional `turnEndedAt` on the status row. Idle
reminders, session events, replays, child re-emits and result retries
restate it; a turn-end event on an already finished row is a new end only
when the agent says it re-entered (`stop_hook_active`). Local hosts and
SSH hosts (orcad) mint it in `attachStatusTiming`; the WSL relay mints it
at admission and sends it on its envelope. The field is persisted,
forwarded over IPC and the paired-server session projection, and the
renderer's done identity is `turnEndedAt ?? stateStartedAt` (after the
existing `turnCompletedAt` precedence). `stateStartedAt` and every other
clock are unchanged.

Paired servers that predate the stamp advertise no
`agent-status.turn-ended-at.v1` capability; their panes keep the
terminal-title completion fallback exactly as before.

* test(notifications): type the old-server pane setup in the completion flow test

* fix(notifications): a blocking Stop's final banner reaches the user; /compact and quitting an idle agent stay quiet

Real-agent QA on the hook-only completion design found three gaps:

- A Stop re-entered after a blocking Stop hook could be dropped twice
  over: inside 1 s of the first banner by the replay guard (a new host
  turn-end stamp is now a real end), and by main's 5 s per-workspace
  desktop cooldown. A banner with the same notification id and changed
  text now replaces the one already shown instead of being dropped;
  every other cooldown rule is unchanged.
- On a mounted pane, a hook-owned idle edge (Codex /compact) no longer
  claimed a completion, so the burst's BEL went out as a Bell banner. The
  deferred idle edge now absorbs that BEL like a claimed completion did.
- Quitting an idle agent after a title spin raised a generic process-exit
  banner. On a pane whose hook lane owns turn ends and whose last turn is
  done, an exit ends no turn; a crash mid-turn still gets its banner.

* test(runtime): the projection reference keys on the host turn-end stamp too

* test(relay): restore the Copilot retry test while CI isolates a runner error

* test: key the hot-path projection reference on the turn-end stamp; relay retry restates the end

* fix(notifications): host turn identity per agent session; keep later blocking-Stop banners past main's cooldown

- The execution host (local main and the WSL relay, one shared reducer) now tracks which turn an
  agent session is in: Codex by its own turn id, Command Code by its transcript prompt position,
  every other agent by its prompt-opening hook. A turn end mints a new end when its turn differs
  from the last ended one, so a Codex turn woken by agent mail and a text-only Command Code turn
  get their banner; duplicates, replays, tool reopens and result retries restate it. The record
  is saved with the status row (validated on restore) and never published.
- Main's desktop and mobile notification cooldowns are back to main's code exactly.
- A later end in the same done segment (a Stop re-entered after a blocking Stop hook) that lands
  inside main's cooldown is held, latest only, and sent once after that cooldown through main's
  unchanged gates; the renderer learns main's answer to anchor it.
- One pane check gates every changed pane behavior: the pane's bound PTY runs on a host that
  stamps turn ends and its agent's continuation signal is verified (Claude, Codex). Every other
  pane, and an unbound PTY, keeps main's behavior.
- A hook-owned idle edge absorbs its burst's BEL for main's full 1.5 s; exits and replacements
  ask the owner check with the exiting agent; a title spin on a hook-owned pane opens no turn.

* test(notifications): run the whole-pane harness through main's shared desktop cooldown, not main's service module

* fix(notifications): decide hook ownership from how the host identified each turn end

The execution host now labels every classified turn end with one optional
disposition beside its end stamp, by the same rule for every agent:
identified (an unused prompt opening, a new native turn id, or a verified
re-entered Stop) mints a new end; a same-turn repeat is a proven duplicate
only when the agent supplies both a turn id and a verified continuation
flag; anything else is ambiguous and never mints. Replays and retries
restate the decided answer, and local and relay hosts carry an end only
within a done segment.

The renderer replaces the Claude/Codex pane gate with one evidence
decision: identified ends are hook-owned, ambiguous ends keep the title
lane's role, and rows without a disposition (older hosts, unclassified
agents) keep the previous behavior exactly. A title working edge on a
hook-owned pane keeps its turn bookkeeping and only the hook receipt; an
owned idle edge settles after the existing max wait, or announces through
the title path if the end turned out ambiguous. Idle and working title
edges resolve the same owner (the title's agent, else the foreground one).
The held later banner for identified continuations is unchanged.

* test(notifications): give the whole-pane harness's host event its local connection id

* fix(notifications): a settled cancellation ends the open turn, so its late Stop folds as on main

A cancel Orca inferred from Esc or Ctrl+C writes a done row with no turn-end event. The host reducer now completes the current turn key on that settled cancellation, so the same turn's late Stop is a repeat (ambiguous, no new stamp) instead of a second identified end that requested a second banner.

* fix(notifications): a settled cancellation ends the open turn on every lane, so a relay's late cancel end restates none

The desktop restates a WSL relay's end as decided. The relay never learns a
cancel the desktop inferred for a non-Claude agent, so Grok's own late
stop_cancelled reached the desktop as an identified end and gave a second
banner. One predicate now decides both lanes: after a settled cancellation
that carried no end, a restated end keeps the shown one, and a child event
carrying the cancellation also ends the open turn. The relay's own cancel
row is the main agent's, never a child's.

* fix(notifications): a relayed done segment shown without its own end keeps showing none

The restatement fold tested the previous row's cancel verdict, so the row it left behind (a late
plain stop) no longer matched and the relay's next restatement brought its end back. Fold on the
row's turn-end fact instead, which the folded row satisfies again.
…blyai#30941)

* fix(shortcuts): app shortcuts run before the terminal, whatever order they mounted in

App shortcut owners and terminal panes both listened on window capture,
so React's effect order decided who saw a chord first. After a worktree
switch the new pane registered before the tab shortcut listener and
turned a user-bound Cmd+Right into Ctrl+E; clicking a tab flipped the
order back. A keyboard router installed before React renders now runs
every app owner, in registration order, ahead of any pane listener; a
key no owner claims still reaches the terminal's own handling. Held tab
chords are claimed on every repeat but switch once.

Tracked in STA-11205 (same bug as STA-4602).

* test(shortcuts): type the router mock with key event names

* fix(shortcuts): install the keyboard router in the web client before it renders

* test(shortcuts): floating panel specs read the routed key listeners from the window stub
…tree cache is cold (stablyai#29340)

* fix(worktree): refuse a parent loop even when the worktree cache is cold

The parent cycle walk read each workspace's current instance from the
resolved-worktree cache, which the reparent itself invalidates, so it only
knew the two endpoints and accepted A->B->C->A. Read saved worktree
metadata instead.

* fix(worktree): fall back to locator metadata and tolerate loops saved earlier

* fix(worktree): skip another host's locator metadata in the parent loop walk

* fix(worktree): read loop-check identities from a read-only, ownership-scoped view

The per-host metadata accessor migrates a hostless legacy row into the
queried host, so the loop check could adopt another host's worktree and
refuse a valid parent. Read one read-only host snapshot instead, counting
a row only when its project is registered on that host (the resolved
list's ownership rule).

* fix: import host-qualified worktree meta helpers once

* test: match main's host-targeted lineage write contract
…icker, so a 13k-host ssh config can't crash any of them (stablyai#31426)

* fix(ssh): draw every host list through one bounded, searchable host picker

A generated ~/.ssh/config can import 13k+ SSH hosts, and several surfaces drew a
row per host: Settings > General workspace directory 'Apply to', Notifications >
Machines, the sidebar and Agents Hosts submenu, the composer run-target picker,
the Add Project host selector and remote step, the skill-install machine pickers,
Settings > Servers SSH host picker, repository 'Add to another host' and the
automation host field and filter.

They now share components/host-picker: boundHostList (pinned hosts plus a ranked,
capped match list), HostCombobox for single choice (a Command in a Popover that
mounts nothing until opened, replacing Selects that mount every item), and
useBoundedHostRows with HostListSearchField and HostListOverflowNote for
checklists. A host-list-render ratchet stops new JSX from mapping straight over a
host collection.

* fix(ssh): host lists keep room for search results, menu search is keyboard-reachable

- boundHostList gives pinned hosts and other matches separate limits, and narrows
  pinned hosts by the query, so a large selection never hides what the user types.
- In a dropdown, the host search is a menu item: arrow keys reach it, focus moves
  into the input, and arrows leave it again.
- The Add Project host picker hides its setup actions while searching, so Enter
  picks the matching host.

* fix(ssh): re-picking the current host in a host picker is not a change

The Selects these pickers replaced suppressed same-value changes; the skill
install form clears its workspace and target on a machine change.

* chore(ci): let the shared ratchet runner pick up the host-list render ratchet

main now discovers every check-*-ratchet.mjs through run-ratchets.mjs, so the
separate workflow step would run it twice.

* fix(ssh): host lists rank pinned matches and keep input order; label and scroll fixes

- boundHostList ranks pinned and other hosts by the same match order before
  cutting each to its limit, then keeps input order among equal ranks, so a
  muted or checked exact match is always reachable and clicking a row never
  moves it.
- The Servers SSH host picker is named by its form label again.
- The sidebar Hosts submenu scrolls its host rows instead of running off screen.
- The composer's overflow note sits outside the listbox.
… the same moments the window did (dark) (stablyai#31430)

* feat(layout): main places a swept workspace's live terminals; the window sweep only reads (switch-on builds only)

The window's activation sweep (first visit, an empty or sleeping activation, a phone wake) now
asks main, through its existing terminal.list call, to place that workspace's unshown live
terminals before listing them. With the switch on the sweep writes nothing to the window store and
counts a pane main names as shown, so it no longer seeds a fresh shell beside a terminal main just
placed. Main's "still binding" check also covers a registration in flight.

* fix(layout): a sweep's placement pass holds back only its own workspace's first surface (switch-on builds only)

* test(layout): switch-on activation-gate and census-call tests read main's listing, not window binds
… without shutting down Claude chat workers (stablyai#30577)

* fix(native-chat): a Claude chat worker keeps running after Claude acknowledges its task

Claude's adapter dropped an echoed send from the sends it holds before
reporting the send's answer to the host, and emitting the echo's turn
can publish at once. That publish read the still-pending send as held
by nobody, so it read as "couldn't confirm": orchestration worker-start
then closed the new Claude worker, and every other answer wait and
client saw a one-frame doubt.

A frame's settled sends now stay held until their answers are reported,
when the host's handover window takes them over; a Stop's rejected
sends are released and reported one at a time, as Pi and ACP already do.

Tracked in STA-11160.

* test(native-chat): assert the settled send's dispatch state through the send result

* Reapply "fix(native-chat): Claude, Pi and ACP chats are Working only while the agent holds the send (stablyai#30134)" (stablyai#30572)

This reverts commit 3ef5a46.

* fix(native-chat): a Claude send stays held until its settlement reaches the host, on the reapplied held-sends read

* fix(native-chat): a Claude send stays Working, not in doubt, while a Stop is still ending its CLI

* test(pi): drop the removed idle-release dependency from the adapter fixture
…ablyai#31431)

* fix(release): replace mobile bundle API polling with job dependencies

* test(release): update qualification token permission expectations
…auto-sync-fork-with-upstream-stablyai-orca-ru-20261021T1400

# Conflicts:
#	config/scripts/check-node-runtime-pin.test.mjs
#	config/scripts/pr-preflight-gates.test.mjs
#	config/scripts/win32-test-lane-registration.test.mjs
#	src/renderer/src/components/native-chat/native-chat-composer-prompt-recall.test.tsx
Reset 2,040 upstream-owned paths to the tag, removed 57, restored 171
exception paths and three-way merged the 21 that upstream moved, and
re-merged 82 seam files without -X ours (23 resolved by hand).

Retired two seams the release made obsolete: config/tsconfig.cli.json
now follows imports from src/cli, and the native-chat-session-options
passthrough fed an always-undefined observedAt to a function upstream
replaced. Advanced the 16 tier-2 copy headers to v1.4.237 (no source
moved since v1.4.232). Re-baselined AGENTS.md to +146/-1 after moving
the fork's sections onto upstream's restructured file.
…orBuffers field

v1.4.237 made editorBuffers a required field of WebSessionTabsSyncState
(phase 1a editor buffer records), so the two fork fixtures built from a
literal stopped typechecking. An empty record is the state a session with
no open editor buffers carries.
…upstream's repo-host-read ratchet

v1.4.237 (stablyai#31096) added check-repo-host-read-ratchet, which counts raw
repo.connectionId host checks and allows none in new files. fork-loops'
SSH check and the project ledger's host resolution read the raw field.
Both now use getRepoRoutingConnectionId, upstream's drop-in for the same
check, which also finds a row that names its SSH host only as
executionHostId 'ssh:<target>' (the loops check already special-cased it).
… upstream's host-list ratchet

v1.4.237 (stablyai#31426) added check-host-list-render-ratchet, which flags JSX
mapped over any collection whose name contains host/target/ssh/machine,
because an SSH host list can hold 10k+ rows. The handoff dialog's
`targets` is a list of worktrees, not hosts, so the match was a false
positive on the name. Renamed it to `destinations` (the type was
already HandoffDestinationOption); no behaviour changes.
Copilot AI balanced review requested due to automatic review settings October 21, 2026 14:36

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.

…e fork's one mac channel

v1.4.237's new sign-mac-app.test.mjs asserts that the hourly, daily and
adhoc mac channels sign with sign-mac-app.mjs. The fork's builder config
(a permanent exception) ships only ORCA_MAC_RELEASE; upstream's other
channels build from workflows gated to stablyai/orca. The release channel
keeps every upstream assertion and the other channels are pinned unsigned.
The fork exception for this test is declared in the manifest commit.
… background Claude runs

v1.4.237 (stablyai#30939) adds --strict-mcp-config and --settings
{"disableAllHooks":true} to background Claude invocations, so the
commit-message plan now carries them before the recipe's own flags. The
fork behaviour under test, recipe arguments replacing generated model and
effort flags, is unchanged.
…rminal create

v1.4.237 moved web launches to createWebRuntimeSessionTerminalResult,
which takes no { notifyFailure } option, so the fork's capability-gating
test no longer expects a second argument.
v1.4.237's agent catalog builds through createLocalizedCatalog, which
reads i18n.language, so the dock test's partial mock failed the whole
suite at import. Mirrors upstream's own mocks ({ language: 'en' }).
…b-runtime launch test

sign-mac-app.test.mjs becomes a permanent exception (fork's single mac
channel). launch-agent-in-new-tab-web-runtime.test.ts drops to +27/-0:
its removed line was the { notifyFailure } argument upstream's API no
longer takes.
Upstream deleted CLAUDE.md (stablyai#31242). Without its one line (@AGENTS.md),
Claude Code sessions in fork worktrees stop loading AGENTS.md, which
carries the fork's ledger, ownership-manifest and sandbox-test rules.
@zpyoung
zpyoung merged commit c1b3d60 into main Oct 22, 2026
40 of 41 checks passed
@zpyoung
zpyoung deleted the zpyoung/auto-sync-fork-with-upstream-stablyai-orca-ru-20261021T1400 branch October 22, 2026 11:50
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

7 participants