Skip to content

sync: absorb upstream v1.4.202 - #59

Merged
zpyoung merged 262 commits into
mainfrom
zpyoung/auto-sync-fork-with-upstream-stablyai-orca-ru-20260914T1400
Sep 14, 2026
Merged

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

Conversation

@zpyoung

@zpyoung zpyoung commented Sep 14, 2026 •

Copy link
Copy Markdown
Owner
Files Added Deleted Net
Test 838 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​75310 $\color{#cf222e}{\Huge{\mathbf{−}}}$​8764 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​66546
Prod 1537 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​76521 $\color{#cf222e}{\Huge{\mathbf{−}}}$​22410 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​54111

Absorbs upstream stable v1.4.202 (b1f7085315542b37f7726f3df03a72539cafa540) into the fork, resolving every
differing path against config/fork-ownership.json.

Previous absorbed tag: v1.4.200. Upstream's v1.4.201 was never synced, so this run spans
two releases.

Merge

  • Attempt 1 (plain git merge) conflicted; git merge --abort.
  • Attempt 2 git merge -X ours completed with one tree conflict:
    src/renderer/src/components/native-chat/NativeChatComposerField.tsx — deleted by us, modified
    by them
    . Honoured the fork's declared deleted: true exception (git rm -f).
  • Resolution mode: auto-ours+tree.
  • No conflict marker survives anywhere in the tree.

Commit accounting

395 fork commits, every SHA still reachable at its original value (set comparison against
git rev-list HEAD, not a loop). The --no-merges range at the merged head is 399 — the 395 plus
this run's four commits. N includes upstream commits earlier syncs absorbed from release branches;
those are not on upstream/main and so fall inside the range by construction.

Ownership resolution

Classifier over 3530 differing paths: checkout 2246, remove 78, ours 88, merge-review 1118.

checkout.txt — reset to the tag, then swept for fork edits the reset would have dropped.
49 undeclared fork edits surfaced, all from PR #55 (feat(ledger)): 956 insertions across
upstream-owned files, none declared as seams. Each was restored by three-way merge
(base v1.4.200, ours = pre-merge fork tip, theirs = the tag) and is now declared. One file
conflicted — src/main/runtime/rpc/methods/repo.ts, where v1.4.202 moved its param schemas into
src/shared/rpc-contract/repo-params.ts; resolved onto upstream's new structure with the fork's
ExpectedLedgers hunks replayed.

remove.txt — applied, then swept for deleted: true exceptions this release invalidated.
One real finding: v1.4.202's new use-native-chat-structured-composer-send.test.tsx type-imports
the attachment type from the deleted NativeChatComposerField. Repointed at
fork-agent-composer/AgentComposerField, matching the three siblings that already take that swap,
and declared as an import-swap seam.

ours.txt — audited over v1.4.200..v1.4.202. 15 exception-owned paths had upstream
movement; 13 took a real three-way merge, 2 kept the fork outright because the exception's reason is
exactly what upstream's side undoes (docs/assets/readme-downloads.svg — fork build artifact;
resources/skills/release-mapping.json — upstream's rows name tags absent from this remote).
Three conflicted:

  • package.json — version line; fork's 1.4.201-rc.0.zy03 kept, as the fork owns its version.
  • config/scripts/check-changed-code-quality.mjs — both sides added functions at one point; kept
    both (fork's filesForScan, upstream's two casting-directive checks; both already wired).
  • .github/workflows/pr.yml — kept upstream's new Check README local links step and the fork's
    fork_ownership_guard job; honoured the fork's deletion of package_windows (ci: cap sync-sized PR scans and remove Windows lanes #52).

merge-review.txt — 1118 paths took a real three-way merge. --verify-residuals flagged 19
that drifted, and every one was -X ours damage or a module split:

  • Module split (5). v1.4.202 moved every RPC param schema into src/shared/rpc-contract/*-params.ts,
    gutting client-ui-schemas.ts (241→8 lines), session-tabs-schemas.ts (229→14),
    worktree-schemas.ts (215→18) and thinning artifacts.ts. -X ours defended the fork's stale
    monoliths. Each file was restored to the tag and its seam re-homed into the new shared params
    module; fork-workspace-activity-window/fork-workspace-review-filters UI schemas moved from
    src/main/runtime/rpc/methods/ into the existing src/shared/ feature directories so the shared
    contract does not import main.
  • Half-applied restructure (2). NativeChatToolRun.tsx (upstream split ToolLine out into the
    new NativeChatToolLine.tsx) and TerminalContextMenu.tsx. Both restored to the tag with the
    fork delta replayed; the tool-name colouring seam followed ToolLine into its new module.
  • Hunk damage (13). 31 conflicts resolved by hand. Notable: sync-runtime-graph.ts combines
    upstream's new native-chat coverage bail with the fork's dock-composer focus redirect;
    native-chat-composer-state.ts takes upstream's rework wholesale (it absorbed the fork's
    mid-prompt / trigger) keeping only the plugin-namespace argument;
    native-chat-picker-items.ts threads upstream's new skillSigil alongside the fork's
    namespacePluginSkills; protocol-version.ts keeps both new runtime capabilities;
    native-chat-agent-profiles.ts keeps the fork's namespacesPluginSkills and upstream's two new
    optional fields, dropping groupedSlash which upstream retired.

Checklists

Tier-2 forked-copy replay. 16 copies. 12 unchanged upstream; 4 had deltas, all replayed:
use-agent-composer-keydown.ts (mid-prompt Enter no longer dispatches — autocomplete.dispatchable),
use-agent-composer-keydown.test.tsx (new mid-prompt case, adapted to the fork's imports),
AgentComposerField.tsx ('skill' autocomplete mode removed) and agent-composer-autogrow.test.tsx
(no change — see needs-attention). All 16 FORK-COPY-SHA headers advanced to b1f7085315542b37f7726f3df03a72539cafa540.

Tier-4 pending-upstream. 22 entries across 6 ledger anchors; every anchor still present. Upstream
moved on 4 of the declared paths; all 4 three-way merged and the fork's delta verified still present
afterwards. No entry resolved, declined or moved upstream, so no manifest/ledger row changed.

Feature collision. 19 features reviewed against upstream's 255 commits in range.
18 × none. agent-composer × possible — raised below.

Localization. Upstream added no new locale, so no fork bundle gained a gap.
verify:localization-catalog passes.

Verification (local gate)

step result
pnpm install --frozen-lockfile pass
--verify-seams pass — 895 lines across 427 files
--verify-residuals v1.4.202 pass — 408 files match
typecheck:node / :cli / :web pass (run sequentially per AGENTS.md; pnpm typecheck composes exactly these three)
pnpm lint pass — all 15 steps
check-fork-ownership.mjs pass

Tests are left to this PR's own CI, per the skill: vitest cannot run on the sync machine and the
test matrix here is the authoritative run.

Needs attention (not blocking this merge)

  1. PR feat(ledger): add typed project and group ledgers across CLI, UI, and runtimes #55 landed ~950 lines as undeclared in-place edits to 49 upstream files, including +237
    fork keys in upstream's src/renderer/src/i18n/locales/en.json
    . This run restored and declared
    them so the next sync keeps them, but the shape violates AGENTS.md — a seam is a line or two,
    and fork locale keys belong in per-feature bundles, not upstream's catalog. Restructuring is a
    human decision.
  2. agent-composer feature collision: possible. v1.4.202 continues migrating upstream's chat
    composer from a textarea to NativeChatPromptEditor (contenteditable). The fork's tier-2 copy
    keeps its textarea because its Markdown-overlay feature is built on one. Advancing the
    FORK-COPY-SHA means the next sync's replay diff starts after that delta, so the divergence is
    now permanent unless someone decides otherwise.
  3. projectGroup.delete and repo.rm are uncatalogued in v1.4.202's new params-parity gate,
    because feat(ledger): add typed project and group ledgers across CLI, UI, and runtimes #55 extends them with a main-side schema. Declared in
    src/main/runtime/rpc/fork-rpc-params/fork-uncatalogued-methods.ts; wire parity is weaker for
    those two until the schema moves shared-side.
  4. release-mac-build.yml took upstream's pnpm install --cpu=current,x64,arm64 despite the
    fork's arm64-only exception. No release workflow runs in PR CI, so this is unverified until the
    next cut.
  5. typecheck:e2e has pre-existing upstream errors (e.g. toggleWorktreeCardProperty exists
    only in upstream's e2e specs, not in src at either tag). Not in pnpm typecheck and not in
    this workflow's typecheck job; not caused by this sync.
  6. $-prefix skill-picker triggering is gone — upstream's product decision in fix(native-chat): one / picker for every agent, anywhere in the prompt stablyai/orca#19832, followed
    through into the fork copies because ComposerAutocomplete no longer models a 'skill' mode.
  7. Stale sync PRs sync: absorb upstream v1.4.191 #31/sync: absorb upstream v1.4.192 #33/sync: absorb upstream v1.4.192 #34 (v1.4.191/v1.4.192, both long since in main) left open and
    untouched.

Backup

origin/backup/main-20260914-140411Z at ab9284520831b76432006f5d94904ec8b1f3a745.

github-actions Bot and others added 30 commits September 10, 2026 18:29
…lyai#19783)

* fix(orchestration): scope @ group addresses to the sender's Run

`@all`, `@idle`, and the agent-name groups (`@claude`, `@codex`, ...) resolved
against every terminal on the host. A coordinator meaning "my three reviewers"
reached 126 agents across every open project, twice in one day, and every
unrelated agent burned a turn discarding mail that was never for it.

Every group except `@worktree:<id>` now means the live Dispatches of the
sender's own Run, each addressed as `dispatch:<id>` so delivery is durable
even when the worker terminal is not attached yet. A sender bound to no Run is
refused with `invalid_argument` naming `run:<id>` / `dispatch:<id>`; there is
no host-wide fallback and the host's terminals are never enumerated for it.
`@idle` and the agent-name groups filter within that set by the same terminal
status and host-resolved identity as before. `ask --to @group` returns the
same code and points at the owning Run mailbox.

Federated Dispatches read relayed control mail rather than a local mailbox,
so a Run-scoped fan-out skips them with a `recipient_unreachable` warning
naming the direct `dispatch:<id>` address.

Group addresses are resolved host-side, so no RPC or stream shape changes; an
older CLI sending `@all` to a new host gets the Run-scoped meaning.

Claude-Session: run-scoped-group-addresses

* fix(orchestration): revalidate legacy takeover before the recipient verdict

A legacy coordinator taken over while `listTerminals` was in flight reported
`runtime_error` instead of `legacy_read_only`: Run scoping made "no live
Dispatch in this Run" the first thing the group send could fail on, and that
threw before the takeover check ran. Takeover is a precondition, not a
commit-time detail — the sender must be told it is read-only whatever else is
wrong with its recipient set.

Revalidation moves to immediately after the only `await` in the path.
Everything below it is synchronous, so the commit-time window it used to guard
is unchanged; only the error paths now see it.

The legacy partition test gave `term_current_worker` no Dispatch, so under Run
scoping it is correctly not a recipient. It now holds a real current-contract
Dispatch in the same adopted Run, which is what the test is named for: one
`legacy_direct` and one `current_delivery` recipient in one fan-out.

Claude-Session: run-scoped-group-addresses

* fix(orchestration): address the Run a nested coordinator created, not its parent

A nested coordinator is both a worker of its parent Run and the coordinator of
the Run it created. `resolveMessageRun` answers with the parent, correctly,
because that is where its own `worker_done` belongs — but audience is a
different question. Scoping `@all` to that Run sent a nested coordinator's
"shared context" to the siblings it was started beside instead of the workers
it started, and reported success, so it never learned its sub-workers heard
nothing. Before Run scoping the host-wide fan-out reached the sub-workers by
accident; this turned an over-broad delivery into a wrong-audience one, the
exact failure class the change exists to remove.

Group audience now resolves off the Run the sender coordinates, falling back
to its Dispatch's Run. A leaf worker coordinates nothing and is unaffected.
This is a separate question from `routing.run`, not a second answer to the
same one, so `resolveMessageRun` keeps its meaning for point-to-point mail.

Also: when every live Dispatch in a Run is federated, the fan-out skipped them
all and threw a bare `Error` that discarded the warnings naming those remote
workers and how to address each one. The sender was told "no recipients" while
three remote workers existed. That throw now carries a code and the skip
explanations.

Claude-Session: run-scoped-group-addresses

* docs(orchestration): say that no group address reaches a coordinator

A coordinator is not a Dispatch, so Run-scoped groups never include one. That
follows from the rule, but nothing said it, and the old host-wide meaning did
include the coordinator — a worker sending `@all` to raise a blocker would be
heard by its siblings and by nobody who can act. The guide, the CLI note, and
the docs page now say to use `run:<id>` for that, and that a worker which
created its own Run addresses that Run's workers.

Also restores the `@cursor` case dropped when the group tests moved: a Claude
pane titled "Fix the text cursor blink" must not receive Cursor's mail. That
hazard was recorded from real titles and `@droid` alone did not cover it.

Claude-Session: run-scoped-group-addresses

* fix(orchestration): preserve group audience and mailbox identity

* fix(orchestration): validate group scope before dispatch routing

* fix(orchestration): preserve pane identity and exclude coordinator dispatches
stablyai#19705)

* fix(codex): reserve the label's share of a qualified command row

A child's label is raw provider text and was spliced into the command
row unbounded, then the pair clipped to the description cap. A label at
or past that cap clipped the command away entirely, leaving a row of
kind 'command' that named an agent and showed no command - the failure
qualification exists to remove, inverted. The same clip could also cut a
surrogate pair, which boundSubagentField already guards against on the
agent row two lines away.

Give the label a reserved share and clip it the way the agent row does.

* feat(native-chat): show live background work and name each row by kind

The strip suppressed itself in three places: the Claude tracker blanked
its roster for the whole of any turn, the Codex tracker returned nothing
while a primary turn was open, and the renderer view gated on
`turnId === null`. Between them, work in flight was never shown — and a
task backgrounded in an earlier turn vanished from the strip as soon as
the next prompt was sent. Claude additionally dropped every foreground
subagent, so a fan-out reported nothing at all.

Report work while it is live, in all three layers. Foreground Claude
work is turn-scoped, so `result` retires it — that is the provider's own
outcome for a task it marked foreground, not a roster sweep. Nothing
settles a Codex child on turn end: those keep reporting well past their
parent, so turn frames only prompt a republish.

Name each ROW by kind — Subagent, Shell command, Workflow, Monitor —
instead of a generic "Background <kind>", each drawing the glyph the
shared tool-icon table already uses for that category. A row that
carries a provider description still shows it unchanged. The collapsed
header summary is deliberately untouched; it is owned elsewhere.

The conversation-command gate is unchanged in effect: an open turn
already refuses first, and Claude foreground work never reaches the
backgrounded set the gate reads.

* fix(native-chat): withhold the row stop Claude foreground work cannot honour

The strip now publishes foreground rows, but `stoppableTaskIds` still filters
on `backgrounded`, so `stopClaudeBackgroundTasks` resolved an empty target list
and returned `{ cancelled: false }` that no renderer reads: the user clicked
"Stop Subagent" and nothing ever happened.

Carry stoppability per row instead of widening the stop to a target the SDK has
no way to reach. `AgentSessionBackgroundTask.stoppable` is absent-means-yes, so
hosts that predate it keep their working control, Claude emits `false` only on
foreground rows, and the strip hides that row's button the same way it already
hides the stop-all a provider cannot honour.

* fix(claude): scope aggregate-roster authority to the work it enumerates

`background_tasks_changed` lists BACKGROUNDED tasks, so a foreground subagent
can never appear in it. Treating it as the whole world meant any such frame
cleared every live foreground row mid-flight and then dropped every later
foreground `task_started` for the rest of the session, killing the in-turn
fan-out the strip exists to show in any session that ever backgrounds anything.

Decide `backgrounded` before the staleness guard and apply the guard only to a
backgrounded start, and retain live foreground entries across a roster replace.
Retained rows count against MAX_TRACKED_TASKS, so the map stays bounded, and a
stale backgrounded start the roster no longer lists is still dropped.

* test(native-chat): pin the strip's monitor amber to the constant that defines it

`MONITOR_GLYPH_COLOR`'s comment claimed a test held it and AgentStateDot's amber
together, but no test imported it — the assertions hardcoded 'text-yellow-500',
so the two could drift with every test still green. Read the colour from the
module, which is what the comment always said was happening. Drop the unused
`BackgroundTaskGlyph` export too: nothing outside the module names it.

* fix(native-chat): keep the task list open across a gap in live work

The strip is now mounted on live work, so a sequential fan-out unmounts it
between one subagent finishing and the next starting: local `useState` meant
the expanded list collapsed itself on every such gap, on top of the strip
flickering above the composer.

Hand the disclosure to the session, keyed by session id so it does not leak
across a session switch. The strip is now controlled and holds no state of its
own, which is what makes it survive its own mount churn.

* fix(codex): route every command-row cut through one surrogate-safe clip

`boundLabel` avoided splitting a pair, then `qualifiedDescription` re-cut the
COMPOSED string with a raw slice: label (<=96) plus separator plus description
(<=512) is up to 611 chars, so that second cut landed at an arbitrary index
inside the description and could publish a lone high surrogate — lossy through
any non-JSON UTF-8 hop. `parse` had the identical hazard on an unqualified
primary-thread command.

One `boundText` helper now owns all three cuts, so no path in the file can emit
a lone surrogate from well-formed input.

* fix(claude): keep terminal evidence for ids an aggregate roster never lists

Narrowing the admission guard to backgrounded starts left a finished FOREGROUND
id with no defence: `replaceAggregateRoster` wiped `terminalTaskIds` wholesale,
so after any `background_tasks_changed` a replayed `task_started` revived a task
whose completion had already been seen — and only a later `result` could settle
it again.

Scope the wipe the same way the guard was scoped: delete only the ids the
incoming roster actually enumerates. A roster still overrules terminal evidence
for the work it lists, which is what that behaviour was added for.

* fix(claude): keep retained rows in place and evict the stalest, not the newest

Re-adding retained foreground entries after the roster made a live row the user
is reading jump below the backgrounded rows on every `background_tasks_changed`,
and the cap `break` kept the STALEST retained rows while dropping the newest.

Merge in the tracked map's own order so a surviving row holds its position, and
count the overflow up front so eviction takes the oldest retained rows. Roster
entries are never starved and the map stays bounded either way.

* fix(claude): retire leftover foreground rows when the next turn starts

A foreground `task_started` arriving with no turn open has no `result` coming
to retire it, so it sat in the strip indefinitely — with no per-row stop, since
foreground rows are not stoppable — and refused conversation commands behind an
instruction nobody could follow.

Settle on turn start as well as on `result`. This is cleanup only: visibility
never consults `startsTurn`, so a missed one degrades to today's behaviour and
can never switch the feature off. It shortens the row's life to the next turn;
the case where no further turn is ever sent is filed separately.

* fix(agent-session): withhold unstoppable rows from readers that predate them

Rule 3 of remote-wire-compatibility: changing what the host publishes reaches
old clients with no wire change. The Claude host published no foreground rows
before this feature; it does now, and a client that cannot read `stoppable`
draws a per-row Stop on every one of them — Claude always sets
`supportsTaskStop` — which filters to the backgrounded ids, stops nothing, and
returns a result no renderer inspects. That is the dead button `stoppable` was
added to remove, reappearing across a version skew.

Negotiate it. A client can advertise the existing background-task-stop
capability and still predate `stoppable`, so this needs its own constant.
Readers that do not advertise it get unstoppable rows dropped, and a state whose
every row is dropped becomes no strip — exactly their pre-feature view.

RUNTIME_PROTOCOL_VERSION is not bumped: this adds an optional field and a new
negotiated capability, and changes no existing field's meaning, which is the
explicit do-not-bump case in protocol-version.ts.

* test(agent-session): name the projected rows so the fixture typechecks

An indexed lookup into the fixture's task list is possibly-undefined under
`pnpm tc`; the rows are more readable named anyway.

* test(web): advertise the row-stop capability in the e2ee auth expectation

The web e2ee handshake started sending
AGENT_SESSION_BACKGROUND_TASK_ROW_STOP_CAPABILITY, and this test asserts the
advertised list by deep equality, so it went red on CI while every targeted
test run stayed green. Add the capability in the position the router sends it.

* test(claude): pin why the roster empties mid-turn in a sequential fan-out

The strip unmounting between two sequential subagents is truthful, not a swept
row: A leaves on the provider's own terminal frame, B does not exist yet, and
backgrounded work spanning the same gap holds the roster open — so an empty
roster is never work the strip is hiding.

Also pins the previous-turn rule against the one the subagent roster already
applies on the same frame: a still-working FOREGROUND child becomes
`unverifiable` there and a backgrounded one is left alone, so the strip drops
the first and keeps the second rather than asserting `live` for either.

---------

Co-authored-by: Merge Sim <merge-sim@users.noreply.github.com>
Co-authored-by: Merge Sim <sim@local>
… CLI agents already do (stablyai#19807)

* feat(sidebar): indent native-chat subagents under their session row

Stacked on stablyai#19311, which adds the background-task channel this reads. The
bridge maps agent-kind background tasks into AgentStatusEntry.subagents, and
the renderer status feed confirms per connection so a reconnect cannot leave a
child asserting live from a stream that ended.

* fix(sidebar): avoid completed age for unverifiable subagents

* fix(sidebar): preserve unverifiable child verdicts

---------

Co-authored-by: Merge Sim <sim@local>
…i#19868)

* feat(native-chat): focus the message box when a chat appears

Opening a native chat left focus nowhere, so you had to click the
composer before typing. Nothing in the chat surface focused it on open;
the only existing focus calls were reactive (typing on the bridge pane
background, picker acceptance, attachments, dictation), and the
structured pane had none of those.

useNativeChatComposerRevealFocus focuses the composer on the reveal
edge, covering a new chat tab, a worktree-create landing in chat, the
chat-view toggle, and switching back to an existing chat tab. Mount is
the wrong signal: retained panes hide with display:none + inert and
never unmount on a tab switch. It reuses the existing composer handle
and shouldPreserveEditableFocus rather than adding a parallel path, and
retries across a bounded run of frames because Tiptap publishes its
adapter after mount and Radix restores a closing dialog's trigger in a
setTimeout(0).

Two supporting changes:

- isFocusedGroup, from activeGroupIdByWorktree. On worktree activation
  both columns of a split flip visible in the same commit, so without it
  two revealed chats fight over the caret. The bridge route already had
  this bit as controller.isActive; only the structured overlay needed it.

- focusRuntimeTerminalSurface bails on a chat-covered pane. Its DOM-path
  twin already declines chat view via data-terminal-chat-view, but the
  runtime path focused the covered xterm unconditionally and pulled the
  caret out of the composer. Returns true, not false: false sends the
  caller to the DOM fallback, which for a structured tab id focuses an
  unrelated tab's xterm.

* fix(native-chat): preserve reveal focus ownership

---------

Co-authored-by: Merge Sim <sim@local>
stablyai#19915)

* fix(relay): keep failed rehome polls out of the durable failure budget

The regional rehome worker polls claimRegionalRehome about once a second.
Any error thrown before an attempt was claimed - in practice a director pool
timeout on the pre-claim control read, 52-74 a day against a pool of 3 - was
charged to relay_region_rehome_worker_state.consecutive_failures, which
durably disables the control at three. That counter only ever resets on a
drain receipt, so while the control is disabled it never resets: production
sits at 1068 and still climbing. Enabling the control leaves the stale
counter in place, so the next pool timeout latches it straight back off.
That is what ended the 2026-08-28 enable after ten minutes.

- A poll that never claimed an attempt drained nothing, so it no longer feeds
  the dispatch-failure budget and logs .._poll_failed instead of
  .._dispatch_failed. recordRegionalRehomeWorkerFailure had no other caller
  and is removed.
- Enabling the control clears consecutive_failures and paused_until, so a
  budget spent under a previous enable cannot kill a fresh one. The dispatch
  interval in next_dispatch_at is deliberately left alone.
- The budget's auto-disable now emits
  orca_relay_regional_rehome_failure_budget_disabled, matching the existing
  .._safety_disabled precedent. It wrote no event before, which is why this
  went unnoticed for two weeks.

No change to region selection, the candidate query, or host eligibility.

* fix(relay): serialize rehome failure accounting with control updates
…g it (stablyai#19911)

When a PTY retires mid-delivery, every staged message was marked undelivered,
which made all of them redeliverable. That is right for a pointer whose Enter
never fired, but an Enter that was already written may have landed: redelivering
it types the same mail into the pane a second time.

The Enter timer is cleared at the top of retirement, so a RESERVED or
WRITE_ATTEMPTED pointer provably never submitted and is released. An
ENTER_ATTEMPTED pointer is ambiguous and now stays at its phase for the resume
path to revalidate, matching the policy mailbox-pointer-submit.ts already
documents for an unverifiable settlement.

Co-authored-by: Merge Sim <sim@local>
… survive (stablyai#19695)

* Make the structured turn lifecycle row durable so completed durations survive

A structured-chat turn used to end by tombstoning its running lifecycle item,
which threw away the only durable record of when the turn ended. Completed
"Worked for" labels therefore depended on the renderer having observed the
turn finish, and vanished on reopen.

The lifecycle item is now revised in place, never tombstoned:
- running, with startedAt, at the provider's turn start
- completed or interrupted, with completedAt, at the provider's terminal frame,
  a user stop, or a child exit the host observed
- unverifiable, with no end, when a cold acquire finds a running row from a
  generation whose exit nobody observed

Both timestamps are the execution host's clock at receipt, captured before the
deferred sink, so the completed value is identical on every client and needs
no client clock. Codex history restore uses the provider's own second-granular
endpoints for turns that predate this change. Desktop and mobile read settled
durations off the journal through one shared selector, and anchor the live
counter on the host start with the client's local receipt so a skewed client
clock never leaks into the label. Locally observed durations remain the
fallback for hosts that still tombstone.

Timestamps live inside the existing turnLifecycle field, which old clients
strip, and every working-state consumer keys on state === 'running', so no
capability negotiation is needed.

* native-chat: avoid stale working status on settled turns

* test: align settled turn status expectations

* Name settled lifecycle rows by their terminal state

An interrupted or unverifiable turn must not read as completed for any
consumer that renders status text raw. One shared helper builds the text for
both providers from the lifecycle state.

* test: deduplicate turn lifecycle suites

Each behavior keeps one test; duplicated harnesses and restated cases go.

* Key lifecycle rows to their user item and record the provider's measured duration

A lifecycle row now names the user item that opened the turn by its provider
key, so clients attribute timing explicitly and fall back to journal order
only for rows from older hosts. A provider-initiated turn with no prompt can
no longer claim the previous prompt's duration.

When the provider measures the turn itself (Codex turn.durationMs, Claude
result.duration_ms) the terminal row records it and clients prefer it over the
host interval, so a turn shows the same number live and after a history
restore. Host receipt times remain the live-counter anchor and the fallback.

* Record a turn as a first-class journal item

The turn record is now its own item kind rather than a status row carrying a
lifecycle field: no text to misuse, and the fold matches the durable turn
record other systems keep. Rows that carry it are stamped journal schema v3;
every other row stays v2, so an older host keeps reading them and latches
read-only at the first v3 row instead of truncating the epoch.

Clients that predate the item would paint an unknown kind as a text bubble,
so the host publishes the legacy status form to any client that does not
advertise agent-session.turn-item.v1, through the same per-client seam
background tasks use. The downgrade is transitional and goes once no
supported release lacks the capability. The shared projection now renders
unknown item kinds as nothing, so later kinds need no gate. One shared reader
handles both forms for old journals and old hosts.

* Preserve observed turn end across settlement retries

* Retain turn attribution for loaded chat history

* Preserve Codex exit receipt across close retries

* Register completed turn duration reliability gate

* Keep earlier turns through a Codex rewind and count a mid-turn attach from the real start

Findings from an independent adversarial review of the typed turn record:

- A Codex rewind adopted the provider's item list as the new epoch, and the
  provider never returns the host's own turn rows, so every duration before
  the rewind point vanished. The host's turn rows are now spliced back beside
  the item each followed, and recovery no longer expects the provider to
  prove rows it never owned.
- The epoch row was stamped with the current schema version, so an older host
  latched read-only at row 1 of every new session, defeating the mixed
  version design. It carries no body and stays at v2; a stored-row test now
  reads SQLite directly, because the reader upcasts every row on read.
- A send Codex folds into a running turn shares the opening prompt's provider
  key, and the alias map credited the duration to the later prompt. The
  earliest submission naming a key now wins.
- The live counter anchored on first sight, so a client attaching mid-turn
  counted from zero. Published frames now carry the host's clock, the reducer
  keeps the last sample with its local receipt time, and both clients anchor
  on how long the host says the turn has run.

* Correct turn duration gate assertion reference

* Respect authoritative unknown native chat duration

* Preserve unverifiable timing across older host upgrade

* Record final completed turn duration reliability evidence

* Fix the CI failures the merge left behind

- A merged import list named the same module twice, which the native code
  quality plugin fails on.
- A running turn is now reported by the host with no duration, so the settled
  map carries an explicit null for it; the hook test still expected the entry
  to be absent.
- main gave the older-page action a cursor with a head-trim guard, so the
  retention test's epoch-only action no longer typechecks; it now passes an
  unbounded sequence, which is what the old shape meant.
- The roster comparator moved into the extracted module, leaving its import
  unused in the reducer.

* Split two files back under the line cap after the merge

Merging main put both one effective line over 300, and the cap forbids a
disable or a shave. The wire module's refusal vocabulary moves to its own file
and is re-exported, so its consumers are untouched; the host's four thin
mutation delegates move next to the functions they call.

* Advertise the turn-item capability on every client transport

Local IPC and mobile advertised it; the remote and web transports did not, so a
desktop paired to a remote host, the CLI, and web silently ran on the legacy
carrier forever and the canonical row was never exercised there. The renderer
that paints it is the same build on every transport.

* Update the web auth-frame expectation for the new capability

---------

Co-authored-by: Merge Sim <sim@local>
…structured chat (stablyai#19681)

* wip(native-chat): first-pass draft routing into structured chat (to be reworked)

* refactor(native-chat): gather agent launch route inputs in one builder

Every launch entrypoint assembled the route resolver's inputs by hand and
they disagreed: only three of seven passed the project runtime blocker, so
a WSL-pinned project was refused structured chat from the tab bar but
admitted from the create dialogs. buildAgentLaunchRouteInput is now the
one place that gathers host, capabilities, workspace kind, project runtime
and TUI customization, and works for workspaces that do not exist yet.

Also deletes the dead draft-prompt blocker from the shared resolver; the
renderer stopped passing it and the main process never did.

* refactor(native-chat): share one structured launch settle loop

Five entrypoints copied the same loop around startStructuredAgentLaunch:
start, claim a refusal fallback, await, branch on refusal or unknown. The
copies drifted: direct work-item and full create reported an unexpected
launch error as success, and resume handled neither refusal nor unknown.

settleStructuredAgentLaunch now owns that loop and returns one settlement
(structured, refused-then-legacy, cancelled, visibility-unknown, failed).
Direct work-item, full create, folder workspace, both onboarding folder
paths and vault resume consume it; each keeps only its own legacy fallback.
Resume deliberately has no fallback. Unknown outcomes release the caller
uniformly so a stale fallback closure cannot fire on a later reconcile.

* refactor(native-chat): route the new-tab launcher through the shared settle loop

The new-tab launcher fired its refusal fallback and forgot it: nobody
learned whether the terminal fallback ran, and a visibility-unknown outcome
was never surfaced. Its structured branch now runs through
settleStructuredAgentLaunch with the terminal launch as the legacy fallback.
launchAgentInNewTab stays synchronous; the result gains a structuredSettlement
promise, and promptDeliveryResult keeps following the terminal fallback's
delivery on refusal as it did through the callers bridge before.

* refactor(native-chat): one legacy prompt delivery path and one trust preflight

The direct work-item flow kept its own seed-and-paste copy of the legacy
prompt delivery; it now uses deliverLaunchPromptToAgentTab with its own
timeout notice supplied as a callback. Three private copies of the trust
preflight (session continuation, worktree creation, folder workspace) fold
onto preflightAgentTrust. The direct work-item pre-launch mark keeps its own
entry because it differs in timing, not mechanism.

* refactor(native-chat): run quick create through the shared settle loop

Quick create was the last entrypoint driving the launch handle itself,
because its cancel lifecycle is real: when the creation is abandoned the
structured launch must be cancelled immediately so a staged prompt never
reaches the provider. The shared loop now takes a cancellation hook with an
eager subscription plus a post-await check; it cancels the launch once,
unsubscribes on settle, and reports cancelled without running the fallback.
Quick create keeps its two-branch legacy fallback and retire-on-late-cancel.

Also updates the surface-caller census for the onboarding launch module
that step 2 introduced.

* fix(native-chat): open editable drafts in structured chat for eligible local Codex launches

Route order asked the default-view-mode question first, and that decider
applies the terminal mirror gate (a TUI cannot clear more than forty lines
of prefilled draft), so a PR body over forty lines reached the plain
terminal before structured eligibility was checked. Structured eligibility
now comes first; the mirror gate applies only on the legacy branch.

The structured draft seed writes the launch-draft store directly with no
mirror gate, since a structured session has no terminal copy to fall back
on. Closing a settled structured tab clears an unadopted seed. The
structured session treats idle and loading as unsettled so the adoption
hook takes its baseline from the loaded transcript. Each caller passes one
delivery-mode value to both the route builder and the settle loop.

The structured session component test is split with a shared harness so
it stays under the test file line cap.

* test(native-chat): make the structured session test harness type-portable

* fix(native-chat): close review gaps in the shared launch settle loop

- Claim a refusal fallback only when the caller supplies one, so vault
  resume no longer reports a terminal fallback it never opened.
- A failed or cancelled direct work-item launch returns no tab id, so the
  caller never pastes the prompt into a setup shell.
- Terminal fork activates with providesInitialSurface for structured
  launches and gates its toast on the settlement; the draft blocker
  deletion made fork route structured too.
- A failed launch clears its draft seed. The failure toast moves to its own
  module to keep the launch-state file under the line cap.
- Ratchet for settle-loop callers; cancel-during-fallback documented.
- Restore the local agent label lookup that the pane-agent identity
  inventory expects instead of the inventoried helper.

* fix(native-chat): resolve the agent label through one module

* fix(terminal-pane): keep the fork dialog from reopening a created worktree

A failed or unknown structured settlement returned false after the fork
worktree already existed, so the dialog stayed open and a second click
created another worktree. Unknown now closes the dialog (the launch badge
already reports it); failed copies the context the way a null launch does.

* chore: restore pnpm-lock.yaml to main (local pnpm rewrite slipped into a commit)

* test(native-chat): stop asserting the deleted draft feasibility input

The routing-authority test expected the shared predicate to receive
isDraftPrompt; delivery mode is prompt metadata and never reaches
feasibility now, so assert its absence instead.

* refactor(native-chat): decide every agent launch route in one planner

The route was still resolved at seven callers, each also calling the settle
loop; two census tests only stopped an eighth. planAgentSessionLaunch is now
the one production caller of the resolver and its launch() the one caller of
the settle loop, and both censuses pin exactly that file.

The funnel is two-phase because three sites need the route before the
workspace exists and quick create persists its request for recovery: a plan
exposes route before creation and launches with the created worktree id;
a persisted quick-create request carries the verdict as data and re-enters
through adoptAgentSessionLaunchVerdict without re-resolving. Delivery mode
is fixed on the request once, so route and launch cannot disagree.

* test(native-chat): pin the two adopters of a planned launch verdict

* fix(native-chat): answer route readability from the repo when the worktree row is absent

The planner's transcript-readability input dropped the repo-level connection
fallback the direct work-item path still computes for its startup payload, so a
route planned in the window right after workspace creation saw `undefined` —
which reads as "not locally readable" — and downgraded grok/omp launches from
native chat to a raw terminal. Only `undefined` ("cannot determine the host")
now defers to the repo; a resolved `null` stays the local answer.

* refactor(native-chat): answer structured feasibility with a query, not a launch plan

Every rendered AI Vault row built a whole launch plan — execution-host lookup,
project-runtime resolution, capability read, plus a plan object and a launch
closure it threw away — to read one boolean off it. Feasibility and a launch
decision are different operations, so the planner now exports the predicate for
the first and keeps the plan for the second, and the census pins the query's
callers separately. Settings arrive by argument, which makes the AI Vault
callback's dependency on them real rather than a comment the linter contradicts.

The plan's `explicitStructured` branch had that gate as its only caller and goes
with it; the vault's launch already re-enters on an adopted verdict.

* refactor(terminal-pane): fold the fork's trust preflight onto the canonical one

`preflightForkAgentTrust` was a behavioural duplicate of `preflightAgentTrust`,
whose signature now accepts a nullable agent and workspace path and so is a
drop-in replacement. Its file is left holding only the launch-platform resolver
— which is not a duplicate, since it returns an override rather than a default —
so the file is renamed for what it now contains.

* refactor(native-chat): cancel a structured launch through an AbortSignal

The settle loop's launch cancellation re-derived the standard poll-plus-eager-
event primitive that `AbortSignal` already is, so it now takes one. The eager
semantics are unchanged: the loop still cancels on the abort event rather than
only polling after awaits, so a staged prompt is discarded before it reaches the
provider, and it drops its listener on settle instead of leaving the signal
holding the closure. Quick create owns the controller and bridges its store
subscription to it.

A cancel that lands after the refusal fallback already opened a terminal now
carries that surface on the settlement. It is the fallback's tab that exists, so
reporting the pre-launch one handed the caller a workspace with no agent in it.

* fix(native-chat): tighten quick create's structured launch settle path

Four things the launch path got wrong once the settle loop owned the flow:

- The abandoned-creation check now runs before the first-message rename flag is
  written, so a creation being torn down is no longer marked for a rename that
  will never happen (the order the pre-planner code had).
- A cancel that arrives after the refusal fallback opened its terminal reports
  that terminal rather than the pre-launch tab.
- `plan.launch` is called outside the caller's try, and nothing awaits that
  caller, so a throw there would strand the creation panel. It is now caught and
  reported the way a failed launch already is.
- The launch route is a required argument instead of defaulting to
  `terminal-tui`, which would have silently reported success with no surface
  opened. Both callers already gate on the structured route.

* fix(native-chat): give one launch identity one prompt delivery mode

A caller joining a pending launch computed its outbox text from its own delivery
mode, so an auto-submit caller landing on a draft launch enqueued text the first
caller's seed was already showing in the composer: the user saw it and it was
sent. The mode is now fixed by the caller that opened the launch, and a joiner
delivers its text that way.

Seeding also moved to where the coalesce decision is made, so a launch whose
callers already settled as refused is not given a fresh draft — the refusal path
early-returns, so nothing would ever clear it and it would outlive every tab.

* fix(work-item): report a failed structured launch as a failed direct launch

`launchWorkItemDirect` returned true unconditionally, so a structured launch
that opened no surface still read as a started workspace. Callers hang
irreversible follow-up work off that boolean — the fix-checks dialog fires
`onLaunched` on it, which is documented as the home for host writes — so a
launch with no agent tab now reports false, matching what full create does.

The settle result says so explicitly rather than leaving callers to infer it
from a null tab id, which `notLaunched` also produces.

* test(session-tabs): pin the id a first structured publication is minted under

The launch draft seed is keyed on `structuredAgentSessionTabId(sessionId)`
before the tab exists, while the mirror mints ids with collision avoidance that
can append a `:history-N` suffix. The two agree today only because a fresh
session's base id is unique. Pin that where the id is actually minted, with the
collision arm alongside it so the divergence the seed depends on staying away is
visible rather than assumed.

* test(native-chat): pin the route connection fallback on the un-mocked resolver

The suite that covers the builder stages `getConnectionIdFromState`, so it can
characterize the fallback but cannot catch a defect that lives in owner
resolution itself. This one runs the real resolution over real store rows: two
repos publishing the same worktree id on different hosts, which is the
documented case where the owner cannot be named and `undefined` is returned.
Red with both fix files at the previous head, green with them.

Reverts the two caller pins added to the route census — the feasibility
predicate is exported from the planner, which the census already permits, so it
passes unedited and needs no permit clause.

* fix(native-chat): keep the structured launch's own agent eligibility check

Quick create's structured launch narrowed its guard to a bare `agent` presence
check, so a creation carrying an agent that cannot hold a structured session
reported itself cancelled once dismissed, where it previously reported that it
had done nothing. Unreachable through both callers today, but it is the last
local eligibility check in a module that otherwise trusts its callers for the
route, so it is restored rather than left to the required-route typing — which
says nothing about the agent.

Also corrects two comments that called the quick-create request "persisted".
It lives in renderer session memory and dies with the renderer; calling it
persisted made the plan/adopt split read as restart recovery, when what it
actually buys is a route decided before the worktree exists.

* fix(native-chat): keep the structured feasibility query typecheck-clean

The query threaded its narrow settings through the store, but the route
store's settings must satisfy the full GlobalSettings that two of its
resolvers require, so the narrow copy never fit. Ride the named settings
on the built input instead: the caller still names them, so a React memo
still depends on them, and no store-shaped object is needed.

Also give the launch state its delivery mode unconditionally; the key is
required, and a conditional spread makes it optional under
exactOptionalPropertyTypes.

* docs(native-chat): name the feasibility query's one remaining settings asymmetry

The builder reads launch customization off the store while the routing gate
reads the named settings, so one answer has two settings sources. It cannot
diverge with the single caller passing the object the store already holds, but a
PR about removing split sources should not leave that unstated.

* fix(native-chat): keep a coalesced joiner's draft unsent

joinLaunchDelivery stripped the joiner's delivery mode when the launch it
joined had established none, and an absent mode reads as submit. A joiner
that asked for a draft therefore had its text sent — the send-without-
consent this PR exists to prevent. Fall back to the joiner's own mode only
when nothing was established, so the first caller still wins otherwise.

* chore: re-trigger CI

GitHub created no workflow run for e935ea5 — the pull_request
synchronize event was dropped. No content change.

---------

Co-authored-by: Merge Sim <sim@local>
…tablyai#19912)

* refactor(cloud): share PostgreSQL schema startup between services

* feat(cloud): add durable native push notification gateway

* infra(push): define dedicated gateway resources and operational checks

* fix(push): bound cross-host admission and simplify gateway configuration

* fix(push): validate deploy configuration and preserve topic-error registrations
…tablyai#19927)

* fix(browser): restore the Chrome-shaped browser identity (STA-7147)

stablyai#18749 replaced every browser partition's Chrome-shaped UA with Electron's stock
one, so since v1.4.198 the embedded browser announces itself on every non-Google
host as:

  Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like
  Gecko) Orca/1.4.198 Chrome/150.0.7871.224 Electron/43.4.1 Safari/537.36

No browser sends that. Sites that re-check the identity holding a session reject
it: users report being signed out of x.com, LinkedIn and "most websites," and at
least one was signed out of LinkedIn in their own Chrome and met LinkedIn's
"suspicious activity" SMS check -- server-side revocation, which reaches beyond
our app. The repo already documented the mechanism in browser-google-auth-ua.ts:
copied-in cookies "sent under a UA that doesn't match a real first-party browser
get flagged by anti-fraud." That is why the Google auth-host switch exists;
stablyai#18749 kept it for accounts.google.com and handed every other host an Electron
identity.

Restore the pre-stablyai#18749 session identity: strip the Electron and app tokens, and
rewrite sec-ch-ua to match. Nothing in the cookie-import write path changed --
it never did; cookies were always written correctly and servers were refusing
them.

Deliberately KEPT from stablyai#18749, all independent of the UA:
- anti-detection.ts stays deleted. Its premises were measured false on Electron
  43 and its overrides are themselves published bot signatures.
- No Runtime.enable into cross-origin iframes (the documented Cloudflare CDP tell).
- No unconditional CDP debugger attach on every browsing guest.

Known tradeoff, measured: this re-opens stablyai#13822. On the unmerged predecessor
branch brennan/sta-3905-cloudflare-ua, commit 9f0a477 recorded the stock UA
clearing dash.cloudflare.com 5/5 while every rewritten variant failed 12/12, and
noted that adding client hints does not rescue it. So Cloudflare-gated sites will
show verification failures again until a coherent-identity fix lands. That is a
bounded, in-app annoyance; session revocation damages users' real accounts. A
CDP Emulation.setUserAgentOverride with full userAgentMetadata -- which drives
navigator.userAgentData as well as the headers, and was never tested -- is the
candidate that could satisfy both, and is being measured separately.

Tests: the real-Electron wire-identity test now asserts the stripped identity on
ordinary hosts and Firefox on Google auth hosts. Ablation-verified: neutering
cleanElectronUserAgent turns it red on the Electron-token assertion. Its fixture
also gained an app name -- without one the raw UA carried no app token, so the
Orca/x.y.z half of the cleaner was never exercised.

* fix(browser): finish the identity revert in the files CI caught

browser-session-registry.persistence.test.ts still asserted stablyai#18749's behaviour
("keeps the stock UA", "keeps the engine UA"), so the shipped code and its test
disagreed. Caught by CI shard 4/8, not locally: I reverted four test files and
went to typecheck without re-running the browser suite.

Also restores the accurate wording that stablyai#18749 generalised away, now that the
behaviour it described is back:
- browser-google-auth-ua.ts: names the Electron/Chrome-shaped UA again as what
  anti-fraud flags, which is the reason the auth-host switch exists at all.
- docs/browser/profiles.mdx: documents the cleaned Chrome UA default and the
  --no-ua-spoof escape hatch, which is real again.
- tests/tools/google-signin-ua-probe.cjs: comments name the live handler.

Deliberately left at stablyai#18749's version, because those changes stay correct with
anti-detection.ts deleted:
- browser-manager-viewport.ts: its comment no longer cites the retired
  addScriptToEvaluateOnNewDocument injection.
- browser-webauthn-profile-delete.test.ts: its added webRequest mock is REQUIRED
  by the restored setupClientHintsOverride, so reverting it would break the test.

* fix(browser): keep restored UA hints browser-owned

---------

Co-authored-by: Merge Sim <sim@local>
…stablyai#19854)

* fix(native-chat): never adopt an unlisted model as the launch default

`modelIsAdoptableAsLaunchDefault` is the sole gate on whether a model id may
become the persisted `-m` launch flag for future native chats. For a catalog
that does not set `discoveredModelsAreAuthoritative` — Claude and Codex —
`!catalog.discoveredModelsAreAuthoritative` short-circuited the discovered
branch to `true` for any id at all.

So a raw launch flag (`worker-start --model claude-opus-5`) is seeded verbatim
into the session record, and the first option write or model re-pick adopted it
as the durable default. Every later native chat with that agent then launched
`-m claude-opus-5` — an id neither the host CLI's list nor the catalog seed
carries.

Both branches now sit behind one precondition: the active model list or the
catalog seed must carry the id. Ids that are carried keep their existing
behaviour, including the authoritative-retirement and tracked-model rules.

* docs(native-chat): state the launch-default precondition by id, not by vector

A typed `/model` cannot introduce an unlisted id: matchNativeChatCatalogModelId
returns only ids drawn from the list it is handed, so a never-seen id either
collapses to a catalog id (claude `/model claude-opus-5` -> `opus`) or matches
nothing (codex). It can only re-assert an id already in the record.

The origins that do enter verbatim are the launch flag and an agent report --
applyNativeChatReportedSessionOptions writes `values.model` with no catalog
matching. Say that instead, so the comment is true of the code as written and
does not lean on the reconciled row that stablyai#19852 removes.

Comment only; no behaviour change.

---------

Co-authored-by: Merge Sim <sim@local>
…rovider echo (stablyai#19863)

* fix(native-chat): settle a structured send on admission, not on the provider echo

Sending a message in structured native chat raised "Message delivery is
unconfirmed." with a Retry button on a message that had in fact been
delivered. Measured across 14 days of local journals: 44 of 173 delivered
sends (25.4%) tripped it.

The dispatch path wrote the message to the provider, then waited a fixed
10s for the provider to echo the message's uuid back. That echo is emitted
when the provider STARTS the turn, so a message queued behind a running
turn cannot be echoed until that turn ends. Echo latency is bounded by the
previous turn's duration, which is unbounded -- one send took 105 minutes.
The 10s constant sat at the p75 of real echo latency, with the slowest
clean send at 9.76s, a margin of 0.24s. No constant can work: the wait was
measuring the wrong event.

The false banner was not cosmetic. It invited a Retry, and Retry bypassed
the operation ledger to redeliver. One message reached the model five times
through that path.

Dispatch now returns as soon as the transport write completes and writes no
dispatch row; the submission stays `pending`, a neutral state, and the
provider's echo settles it `accepted` through the late-settlement channel
whenever the turn ahead of it ends. Delivery doubt is reachable only from
process facts -- a refused write, a dead child, a dead host -- never from
elapsed time.

Retry re-delivers only where the recorded reason proves the message never
reached the provider. The list is deliberately fail-closed: refusing a
legitimate retry costs the user a re-type, while allowing an illegitimate
one sends the model a second copy of their message. A refused entry now
leaves the outbox with an explicit notice instead of parking at the head,
where it would have wedged every message queued behind it.

The send-response classification moves to a pure module beside the existing
outbox reconciler, so both writers of an entry's state now live together and
the decision is unit-testable rather than reachable only through the hook.

Scope and known gaps:
- Codex carries the same 10s stopwatch. It has no late-settlement channel,
  matches waiters by queue order rather than identity, and has no waiter
  lifecycle at all, so there was no safe subset to land here. A marker
  constant records the debt and deletes itself when that lands.
- A message refused re-delivery loses its standing delivery notice and
  leaves only a transient error line. A passive "waiting to be accepted"
  affordance is the follow-up.
- The restart reconciler that would decide a dead child or a dead host on
  evidence rather than refusing them is fully written and has never had a
  production caller. Wiring it is the next change, and it removes the
  re-type cost above.

* fix(native-chat): harden structured dispatch settlement

* fix(native-chat): preserve dispatch recovery evidence

* fix(native-chat): preserve pending send compatibility

* fix(native-chat): satisfy native import audit

* fix(native-chat): bound legacy send settlement

---------

Co-authored-by: Merge Sim <sim@local>
* feat(desktop): integrate native mobile push delivery and lifecycle

* fix(desktop): preserve notification replay policy and review invariants

* fix(desktop): correct notification locale namespace and auto-ack tests
* fix(sidebar): let an agent row show the provider's own session title

The row resolver never read `aiVaultTitle`, so a terminal agent's tab showed the
title from its transcript while its sidebar row showed the scraped live title —
two names for one session. Placed at the same rank the tab strip uses.

* fix(sidebar): scope provider titles to their sessions

* fix(dashboard): refresh retained agent tab metadata

---------

Co-authored-by: Merge Sim <sim@local>
…valid (stablyai#19746)

* fix(editor): make markdown images inline so a paragraph stays schema-valid

Image was registered as a block node while paragraph is content:'inline*',
but the markdown pipeline nests an inline image as a paragraph child.
Schema.nodeFromJSON does not validate content, so the editor built a
schema-invalid document that rendered fine and threw on the first step
that reassembled the paragraph - i.e. on the user's next keystroke.

Report 0e46c048 (1.4.198, macOS): RangeError "Invalid content for node
paragraph" from checkContent via Node.replace, tearing down the
editor.rich-markdown boundary.

Register Image as inline and override paragraph's parseMarkdown so a lone
image is not hoisted out of its paragraph. Also fixes the same crash class
reachable through details/summary. Markdown output is byte-identical.

* fix(editor): keep a fenced code block intact when an image is inserted into it

Making the image node inline meant it could no longer be fitted into
codeBlock (content:'text*', marks:''), so inserting one with the cursor
inside a fence made ProseMirror close the block at the insertion point:
the remaining code escaped as plain prose and the language attribute was
lost, and autosave wrote that markdown to the user's file. The pre-fix
block image split the fence into two intact blocks instead.

Resolve the insert content against the target position: when an inline
image cannot be fitted where the caret sits, wrap it in a paragraph so
ProseMirror splits the block and both halves keep their ``` fencing and
language. Prose insertion is unchanged. Every production insert path now
shares that resolution - the toolbar picker, the slash command and the
clipboard-screenshot paste through insertRichMarkdownImageFromPath, plus
the GitHub/GitLab composer's image-URL insert - each with a regression
test.

Also guard the unchecked cast of Paragraph.config.parseMarkdown: a Tiptap
upgrade that drops the field would otherwise turn every paragraph parse
into a TypeError and take the whole editor down, instead of degrading to
parseInline.

Four of the new round-trip cases asserted only on getMarkdown(), which
walks the document without running NodeType.checkContent and so emits
byte-identical output from a schema-invalid document - they passed on the
pre-fix code. roundTripMarkdown now runs doc.check(), the list-item and
table-cell case performs a real edit, and the standalone-image case types
beside the image. All twelve cases now fail on the merge-base.

Adds an Electron e2e spec driving the real renderer: a paragraph image and
a toggle-summary image each survive a keystroke, and Bold over a selection
spanning the image keeps it.

---------

Co-authored-by: m4air <m4air@m4airs-MacBook-Air.local>
Co-authored-by: Neil <neil@stably.ai>
…tablyai#19908)

* refactor(ai-vault): move the surrogate-safe slice to shared, one implementation

`sliceAtCodeUnitLimit` lived in src/main/ai-vault, and src/shared never imports
src/main, so a shared consumer could not reach it. Rather than add a second
copy, it moves to src/shared and ai-vault imports and re-exports it, leaving
every existing importer of that module untouched.

Separated from the feature that needs it: this is the only change here to a
subsystem the rest of the branch does not touch.

* feat(native-chat): give an agent session one place to hold its name

Adds the normalized conversation name and the record field that stores it, so
Orca has a durable note that it already named a session and does not name it
again on a later acquisition. No name is generated yet, and nothing displays
this field: the AI Vault path stays the home for the name a user sees.

- `agent-session-conversation-name` bounds and flattens the text once, at 200
  characters, cutting on a character boundary.
- `AgentSessionRecord.conversationName` carries it, validated on load.
- `setAgentSessionRecordConversationName` sets or clears it, unfenced: the name
  is a durable note, not ownership, so writing it never contends with the
  writer lease.

* refactor(runtime): move reserve-owner orchestration next to its admission logic

Pure move, no behavior change. `reserveOwner`'s transaction body sequenced
decisions that all live in agent-session-reservation-admission and then applied
the winning one; it now sits there as `commitAgentSessionReservation`, and the
store keeps the transaction boundary and a one-line delegation.

Takes agent-session-record-store.ts from 300/300 to 279/300, which is what the
conversation-name field needs to land. Every existing test passes unedited.

The module header said nothing in it mutates; that is now qualified rather than
left false, since the committer writes the state it is handed.

* feat(runtime): let the store set a session's conversation name

`setConversationName` is the one writer for the record field, so a producer
never reaches the record shape itself. It normalizes at the boundary, so no
caller can persist a name the loader would then reject as unreadable.

Unfenced by design: the name is durable memory that Orca already named this
session, not ownership, so naming never contends with the writer lease.

No name generation and no provider code yet.

* refactor(runtime): reuse the shared default chat label on a replacement tab

The replacement tab open-coded `'Claude Chat' : 'Codex Chat'` while every other
publisher already calls `defaultAgentChatLabel`. One writer for the placeholder.

* fix(native-chat): reject noncanonical stored conversation names

---------

Co-authored-by: Merge Sim <sim@local>
…nheriting them (stablyai#19945)

* fix(release): prune optional natives before the linux arch floor check

The linux-arm64 release build failed packaging, and retried three times:

    [verify-linux-glibc-floor] 1 bundled native binary is built for the
    wrong architecture (target arm64):
      resources/node_modules/@parcel/watcher-linux-x64-glibc/watcher.node
      is x64, expected arm64 (from its own path)

`afterPack` ran `verifyLinuxGlibcFloor` at its top, before
`prunePackagedRuntimeNodeModules`. A cross-build intentionally installs
every optional native variant, so at that point the arm64 slice still
carries the x64 `@parcel/watcher` package that the prune exists to drop.
The check was reading a file that was never going to ship.

Move the floor check below the prune so it inspects the binaries actually
packed. Same ordering as 35f1b0e on the v1.4.197 release branch, which
shipped green and was never merged back to main.

(cherry picked from commit 6400598)
(cherry picked from commit a883e17)

* fix(release): restore the minified telemetry constant fallback

The macOS, Windows, and both Linux release builds all failed on "Verify
telemetry constants present in app.asar", blocking publish-release:

    ::error::BUILD_IDENTITY constant missing or unexpected value in
    dist/mac-arm64/Orca.app/Contents/Resources/app.asar

The verifier printed no context sample, because the string
`BUILD_IDENTITY` does not occur anywhere in the shipped bundle at all.
stablyai#17527 added `minify: 'oxc'` to the main bundle, so oxc renames the
module-local `const BUILD_IDENTITY` to a short identifier.
`BUILD_IDENTITY_RE` keys off the literal name and therefore cannot match a
production bundle. `MINIFIED_TELEMETRY_RE` matches the adjacent injected
identity/key pair instead, which survives renaming.

stablyai#11019 created these patterns without that fallback, and its own comment
predicted this exact failure if minification were ever enabled on the main
bundle. The fallback was written on the v1.4.197 release branch in
35f1b0e and never merged back to main, so main has never been able to
verify a minified bundle.

Verified against a real local bundle built with the release env vars
(ORCA_BUILD_IDENTITY=stable, ORCA_POSTHOG_WRITE_KEY=phc_...): the bundle
does not contain "BUILD_IDENTITY"; both the narrowed and the widened
BUILD_IDENTITY_RE fail; MINIFIED_TELEMETRY_RE matches and recovers
identity=stable plus the write key.

(cherry picked from commit 39c3e44)
(cherry picked from commit d0bbe44)

* fix(skills): stop asserting readdir order in the skill root walk test

The Windows skill-sharing release gate — which blocks publish-release —
failed on this test, wedging the 1.4.198 cut. The implementation is
correct; the assertion was not.

`findSkillFiles` pushes results in `readdir` order and dedupes visited
directories by realpath, so of the 32 junctions pointing at one target
exactly one survives. The assertion hardcoded both the array order and
which link won. `readdir` order is filesystem-dependent: APFS and ext4
return SKILL.md first, while NTFS enumerates its filename index
alphabetically on the uppercased name, where "LINK00" sorts before
"SKILL.MD" (L=0x4C < S=0x53). Windows therefore returned the same two
paths in the opposite order.

Assert the contract instead: the real file is present, exactly one
link-routed path survives dedup, and nothing else does.

(cherry picked from commit 9a832e9)
(cherry picked from commit 505b85c)

* test(release): pin ported release gate fixes

* style(skills): apply pinned test formatting

---------

Co-authored-by: Neil <neil@stably.ai>
Co-authored-by: Merge Sim <sim@local>
* feat(cmd-j): compact palette location layout

* chore(i18n): sync palette runtime fallbacks

* fix(cmd-j): preserve agent snippet context

* fix(cmd-j): elide all file-backed tab paths
…blyai#19966)

* fix: prompt unexpectedly signed-out Cloud users once per version

* fix: align sign-in card English catalog with runtime defaults

* fix: stack notification cards by their rendered height

* fix: wait for fresh auth before showing signout card

* fix: require verified auth before signout recovery

* fix: retry transient auth readiness failures
…nsumer (stablyai#19687)

* feat(ai-vault-search): add the session search index schema and row modules

The FTS5 index that PR 2 folds the transcript reader's message stream into:
two FTS tables behind visibility views, the identifier shadow column, the
resumable fork digest, redaction at the row-insert choke point, and the
bounded compaction, warm-up and retention lanes.

Schema version starts at 1: this branch drops the query log and the fts5vocab
table, which the query engine reintroduces with its own bump.

* feat(ai-vault-search): stage index writes and publish them atomically

The writer stages a read's rows against an unpublished session row and a batch
id, then flips the whole read visible in one transaction: both FTS tables are
written together per row, and publish nulls the batch pointer before dropping
the batch so a recycled rowid can never name a later in-flight write.

Buffering replaces the branch's streamed producer. The transcript reader pushes
messages synchronously, so a staged write cannot make it wait; rows are held to
128 of them or 256 KB and then flushed in one transaction, which bounds both the
retained bytes and one stall.

The store owns the database and the set of files the index is behind on. It
carries no query, coverage or scheduling: draining that set is the indexer's.

* feat(ai-vault-search): register the index as a transcript reader consumer

The index keeps its own per-file cursor in the `files` table and never consults
the parse cache; the branch's AsyncLocalStorage capture scope is gone.

Three refusals, each leaving the cursor where it was and recording the file for
a later whole re-read: an append whose predecessor offset is not this index's
own cursor (or whose dev/ino changed) is declined in `beginRead`, a staging
failure ends the read's rows without throwing back at the reader, and an
`incomplete` outcome never publishes. A null session drops the file's rows and
still advances the cursor, because the file was read through.

Nothing in production registers it: the indexer decides when the index is live.

* test(ai-vault-search): measure the index's disk and latency cost

The write benchmark indexes a synthetic corpus through the real reader and
reports rows/s, per-table bytes per transcript MB, write amplification and
rebuild time; the retention benchmark compares the batched purge against a
whole-file delete. The corpus generator ships with them, so the numbers are
reproducible on any host and no real transcript is ever read.

* fix(ai-vault-search): decline a source the message channel cannot reach

An OpenCode SQLite candidate decodes inside a worker, so the reader reports
every read of it as incomplete. Staging a batch first meant one staging session
and one tombstone per scan of every such session, forever. Declining in
`beginRead` also keeps it out of the re-read set, because no re-read helps: the
index cannot cover that source until its capture path lands.

* fix(ai-vault-search): redact a message before it is cut into rows

A credential is a shape, and a shape split across two chunks matches neither
half: an 8000-char boundary landing inside a PEM block or a JWT indexed the key
material in full, identifier shadow terms included. Redaction moves ahead of
chunking, and the row type is branded so only `searchMessageRows` can mint a row
an insert site accepts.

`upsertFile` stops erasing dev/ino when a candidate carries none. A host that
cannot stat identity (SSH, WSL, a degraded Windows stat) was overwriting a
proven identity with NULL, which made the next rename-replace of that path
undetectable.

* test(ai-vault-search): pin each index guard on its own

Three guards were each shadowed by another, so mutating any one of them left
every test green. Driving the store directly separates the writer's own
predecessor-offset check from the consumer's cursor check, and a stubbed store
proves `beginRead` refuses before the writer is ever asked.

`publishable` gets the case it never had: a second read of the same path
publishes first, and the stale stage is tombstoned rather than resurrected. It
covers the overlap the parse file lane normally prevents, so the invalidation
slot being per-path is not load-bearing on its own.

Two ratchets: every provider fingerprint needs a fixture that reaches past
`SECRET_ANCHOR`, and every FTS read in the index modules must subtract staged
rows through a visibility view.

* fix(ai-vault-search): rebuild an index that cannot be trusted, and index retention

Opening self-heals. A file too torn to open, or one whose recovery fails, is
unlinked with its sidecars and reopened once; a second failure throws. Before
this a torn index broke open permanently, even though the index is a cache over
the transcripts and throwing it away costs only a re-scan.

A `meta` table with no readable version row is now `stale`, not `fresh`. Seeding
the current version over it would have kept whatever rows the old schema left.
`fresh` means no `meta` table at all, and no longer triggers a rebuild: the
first open of a new profile was doing create-remove-create.

Recovery moves inside open, because retiring a batch that outlived its writer is
part of opening the index rather than of using it. Its append case gets the test
it never had: 200 rows staged onto a live session and abandoned leave the
published generation intact and nothing else.

Also an index on files(mtime_ms), so the retention pass seeks the expiring end
of the list instead of scanning and sorting it, a pragma read-back test, and a
sessions.file_path comment that says what is actually true of OpenCode.

* fix(ai-vault-search): key a session's cwd the way the sidebar already does

`cwd_key` had its own normalizer, which qualified a WSL cwd with its distro:
`/home/me/repo` was stored as `//wsl/ubuntu/home/me/repo` while the sidebar's
`folderGroupKey` stored `/home/me/repo`. Any later join between an indexed hit
and a sidebar group would have returned nothing for exactly the WSL users the
qualification was meant to help. The key is now the shared normalizer verbatim,
pinned against `folderGroupKey` for POSIX, Windows drive, /mnt/c, both WSL UNC
aliases, a Linux path, and the root.

The collision it guarded against is real: two distros both spell
`/home/me/repo`. It is also a collision every SSH host has, neither key
qualifies for SSH, and the honest fix is a column naming the execution host
rather than a path key that only some hosts spell differently.

`/` now keys as `/` instead of the empty string. An empty key cannot be told
apart from a session with no cwd, and the scope filter builds its child prefix
as `key + '/'`, which would have been `//`.

* test(ai-vault-search): widen the FTS visibility ratchet past whole literals

Concatenating or interpolating a table name hid it from the per-literal scan,
so `'SELECT rowid FROM ' + table` passed. The unit is now the file, the scan
covers src/main and src/relay rather than one directory, and both bypass shapes
are checker unit tests. Exemptions carry a reason and the census fails on one
that has stopped being needed, which is how the schema module lost its.

* fix(ai-vault-search): keep a failed rebuild's real cause, and read per statement

Closing the stale handle before the unlink left `db` dangling: if the unlink or
the reopen threw, the failure path closed it a second time and node:sqlite's
ERR_INVALID_STATE replaced the real cause. Nothing classifies that as worth
retrying, so an index a virus scanner or a second Orca was holding would never
rebuild. The handle is nulled while none is open.

The visibility ratchet moves from the file to the statement. A file is far too
coarse for what it exists to guard: the query module will name both views
somewhere, and that whitelisted every raw read in it. Statements come from
literals and their concatenation chains, split on `;`, and a table name
assembled at runtime counts as a read in any file that names an FTS table. The
roots now cover cli, shared and preload as well.

Half a recorded identity is now stated to be no identity. `remote-session-file-stat`
spreads dev and ino independently and the COALESCE preserves whichever half a
host could prove, so one number cannot tell a rename-replace from a same-file
re-read; comparing it would decline healthy resumes on a coincidence.

`discard` clearing the staging slot gets a test: without it the map grew one
entry per path for the store's life.

* fix(ai-vault-search): give a behind consumer a way to get the read it needs

`markStale` promised a whole re-read that nothing could deliver. The reader
picks append or replace from the session list's resume point, so with an empty
index and a warm parse cache every read arrives as `append`, the consumer
declines every one, and nothing is ever indexed. That is the state on first
enablement inside a running app.

`requestWholeTranscriptRead` in the reader drops that path's resume point, which
is the one lever that changes the next read's mode, and it lives with the cache
that owns it rather than with the consumer that wants it. `takeStale` documents
that its paths need it before a scan is re-dispatched.

Pausing no longer empties the re-read set. `acceptsCandidate` answers whether a
write may start now and was being used for both jobs, so reconfiguring retention
while paused dropped every entry and a declined read during a pause was never
recorded at all. Retention alone prunes; a paused decline is recorded, bounded,
with the oldest dropped and the drops counted, because a set that silently
forgets is worse than one that says it is incomplete.

A read that decodes no session now tombstones its staged rows before the store
schedules cleanup. The cleanup lane reads the tombstone table the moment it is
scheduled, so the old order left that batch on disk until some later write, and
for the last read before a shutdown that is never.

* fix(ai-vault-search): create the directory the index lives in

Nothing made `<userData>/ai-vault-search/`, and SQLite's failure for a missing
parent is `unable to open database file`, which is correctly not classified as
corruption — so the retry never fired and the feature stranded on any profile
that had never held an index.

* refactor(ai-vault-search): store transcript content as written

Decision: the index does not redact. A secret in a transcript is already
plaintext under the user's home directory and is treated as compromised, so the
index is a second copy of content the user already holds, not a new exposure.
The reference agent-session-search products do not redact either. What a snippet
may carry once it leaves this machine is a transport policy and belongs where
the wire is, not in the write path.

Removes the redaction module and its census, the branded row type that existed
to prove redaction had run before an insert, and the two chunk-boundary tests.
`insertSearchMessage` takes a plain chunk again and identifier shadow terms come
off the raw text. `src/main/observability/redactor.ts` goes back to what main
has, so this PR no longer touches it at all.

Chunking and the fork digest are unchanged; the digest always folded raw message
text, ahead of chunking, so its tests hold as written.

Cost: redaction was 14 ms per 10.5 MB of transcript, about 3% of a rebuild and
below the benchmark's run-to-run spread, so the write numbers are unchanged at
roughly 22-25k rows/s and 22-27 MB/s.

* fix(ai-vault-search): hide a tombstoned session's messages, not only its row

`visible_sessions` already subtracted session-keyed tombstones; the message
half filtered on the batch pointer alone. A published row outlives its session
row until the cleanup lane reaches it, so between a `replace` publish and that
drain both generations answered, and a removed file kept answering after its
session was gone.

Both views now subtract the same set, over a partial index so neither read
scans the tombstone table.

* fix(ai-vault-search): drain the rows a read that never published staged

Only `writePublished` and `removeFile` scheduled the cleanup lane. A read that
staged rows and then declined to publish them tombstoned its batch and told
nobody, so 256 rows of a 300-message incomplete read sat in `messages` and both
FTS tables until some unrelated write happened to schedule a pass. Open-time
recovery had the same hole: it wrote the tombstones and drained none.

The abandoned branch now schedules the drain the published branch already got,
and opening schedules one pass for what recovery just tombstoned. Recovery no
longer adds a batch tombstone when the session-keyed one already covers those
rows, which it did once more on every reopen.

* fix(ai-vault-search): continue the cursor of a file that decoded no session

A read that decodes no session — an excluded Codex worker transcript — advances
the cursor and leaves `files.session_row_id` null. Every later append was then
declined, so a file that only ever grows would be re-read whole on every pass
for the rest of its life.

An append now only has to continue this index's own cursor; it creates the
session row when there is none to resume. That also collapses the `append` flag,
which meant both "resume this session" and "do not own it", into the resumed row
id itself.

* refactor(ai-vault-search): drop the index path module nothing calls

No caller in this PR or the two that follow it: the composition root that would
capture the userData path is PR 3b's.

* refactor(ai-vault-search): read the schema version as stale or not

Only the stale answer was ever acted on; the fresh/current split named two ways
of being fine.

* fix(ai-vault-search): read the resumed session id off an optional row

* refactor(ai-vault-search): leave page warmup and the freshness check to PR 4

`warm()` and `session-search-page-warmup.ts` have their first caller in the
query engine, which is what knows which pages are worth warming; the module
itself went with the previous commit. `isSessionSearchFileCurrent` has its first
caller in the reconciler. `session-search-file-cursor.ts` stays, because
`fileIdentity` and both its types are load-bearing here.

* refactor(ai-vault-search): stop pruning the re-read set on a retention change

The prune walked the whole set to drop what the next `beginRead` would refuse
anyway: `acceptsCandidate` applies the window when the re-read is dispatched,
and `markStale` applies it again before recording anything. The bound stays.

* fix(ai-vault-search): keep a batch tombstone from outliving its batch row

Draining a session-keyed tombstone deletes the session's batch rows, but left
any batch-keyed tombstone naming them in place. `search_write_batches` empties
on every publish, so ids restart low and the next read takes the freed rowid;
the stale tombstone then deleted that live batch's staged rows and the session
published missing messages, with no re-read to fill the gap.

The session drain now clears those tombstones in the same transaction.

* refactor(ai-vault-search): commit a file's rows and its cursor in one transaction

The staged-publish model is replaced by one SQLite transaction per file in WAL
mode. A read buffers its decoded rows and writes them, its session and its
cursor together; a reader on another handle sees the last committed state, which
is the "never a torn session" guarantee the staging machinery was built to
provide. A crash rolls the whole file back and it is re-read.

Gone with it: `search_write_batches`, `search_pending_deletes`,
`messages.batch_id`, `sessions.index_ready`, both `visible_*` views, the
open-time recovery pass, the cleanup lane and `settled()`, and the ratchet test
that made every query site read through a view. Rounds 2 through 6 were all
seams between those pieces.

A file whose rows exceed `SESSION_SEARCH_COMMIT_CHARS` is cut into chunks. The
reader only hands out a byte offset when a read finishes, so a chunk records one
no append can continue from: its rows answer searches as a coherent prefix of
the session, and the next whole read replaces them.

Retention keeps no record of unfinished work. It cuts a session loose from its
file in one small transaction, which is what stops it answering, then reclaims
its rows in bounded batches and hands the freed pages back as it goes. Rows
whose session row is gone are the record of what an interrupted purge left.

Deleted for want of a caller in PR 2, 3 or 4: the WAL budget (a buffered write
has no staging window to grow one across), the compaction module (folded into
the drain), the `sessions_content_hash` index (fork folding runs in JS over rows
PR 4 already holds), `lastWriteAt`, `failures`, `SessionSearchStoreOptions`,
`openStageCount` and `chunkMessageText`.

* test(ai-vault-search): price a per-file commit and the ceiling that bounds it

The write benchmark now times every transaction, because with one per file that
is the whole stall a file costs. A second phase indexes a single synthetic
100 MB transcript, which is what the commit ceiling is chosen against.

* fix(ai-vault-search): stop a fenced write reopening a transaction per message

A chunk write that finds the file record moved under it — a `removeFile`, or an
overlapping read of the same path — can never land anything afterwards. It kept
buffering, so every remaining message re-entered `BEGIN IMMEDIATE` only to roll
back, once per message for the rest of a file that may be a hundred megabytes.
It now stops at the first refusal and drops what it holds.

* fix(ai-vault-search): never hand a live session the id of a purged one

`sessions.id` was a plain rowid alias, so SQLite reissued it as max+1. That id
names rows in `messages` for far longer than the row itself lives: retention
cuts a session loose in one transaction and reclaims its messages over many, and
a session created inside that window was handed a freed id and adopted whatever
of the purged conversation the drain had not reached. The drain then skipped
those rows for good, because their session row exists again, and a search
answered for a purged transcript under a live session's name.

Reachable with no crash at all: an append of a growing transcript the parser
decoded no session from creates its session row mid-purge. AUTOINCREMENT is the
class fix; the schema version goes to 3 so a file written by an earlier commit
of this branch rebuilds rather than keeping a plain-rowid table under a
`CREATE TABLE IF NOT EXISTS`. `messages.id` was checked and is not exposed to
the same window: a row and its two FTS entries always go in one transaction.

Two smaller ones in the same pass. `removeFile` did not fence a read of a path
this index had never written: `current()` compared a cursor, and on an unknown
path the absent row and the absent expectation are both undefined, so the write
recreated the source after its owner had proven it gone. The writer now counts
removals per path and a write compares that count, which is a positive fence
rather than an inferred one. And `drainOrphanedMessages`,
`CONTENT_HASH_MESSAGE_LIMIT` and `CONTENT_HASH_MIN_MESSAGES` are no longer
exported: nothing outside their own modules reads them.

* fix(ai-vault-search): report a half-written file as held, not as unknown

`indexedFile` returned null for a file a chunked read left partway through, the
same answer it gives for a path the index has never seen. A caller asking "do
you hold this file" therefore read a half-written one as new, asked for whatever
read the parse cache offered, and the reader picked append — which the consumer
then declined. Only the stale set healed it, a cycle later.

It now reports the record with a null `byteOffset`, and
`requiresWholeRead(indexed)` names that state. Null rather than an added flag
because the mtime and size on that record are the file's real ones: a freshness
check comparing only those would call a half-written file current, and a boolean
is easy to not read, while every site that does arithmetic on the offset has to
say what null means at compile time.

The test also found the writer accepting an append that passed the partial
sentinel back as its predecessor offset. Nothing in the reader produces a
negative offset, but the sentinel is not a byte offset and no caller should be
able to continue it; `beginWrite` now refuses it outright.

* fix(ai-vault-search): cut a chunk at whitespace, never mid-token

The 8,000-char chunker backed up only to a newline, and only when one sat in
the second half of the window. A wrapped paragraph, a CJK transcript or a
minified log has no newline there, so the cut landed inside whatever word
straddled the target: the user's term was filed as two halves and matched
neither. It now backs up to the last Unicode whitespace in the second half,
and falls through to the target when there is none, because 4,000 characters
without a space is not a word.

A phrase that straddles a chunk boundary is still not matched. Chunks are
separate FTS rows and FTS5 cannot span them; that is stated at the chunker.

* feat(ai-vault-search): cap an indexed tool row at 3,072 characters

Tool output is 80-97 % of a transcript's bytes and a single message may be a
quarter of a megabyte, so without a cap the index, the buffer a read holds and
the transaction it commits are all sized by how much a tool printed rather
than by how much is worth searching. A `tool` message now becomes one row of
at most 3,072 characters, kept from the head, where the command and the first
lines of its output are. User and assistant text is never capped.

Measured on the write benchmark, 40 sessions with tool output at 95 % of
message text (the real band), per transcript MB: index 1,334,126 -> 353,973
bytes, write amplification 1.27x -> 0.34x, rows 18,804 -> 9,600, rebuild
1,676 -> 379 ms, largest transaction 49.7 -> 12.1 ms. On the default corpus,
whose tool results are 1.4 KB and so under the cap, nothing changes at all:
2,167,887 bytes per transcript MB on both arms.

The corpus generator takes `toolResultWords` and the benchmark reads
ORCA_SEARCH_BENCH_TOOL_WORDS so both arms are the same harness.

* fix(ai-vault-search): check the commit ceiling per row, not per message

The buffered-chars check ran after a whole message had been folded into rows,
so a single message could carry a transaction as far past the ceiling as it
was large. A conversation turn is one message and can be megabytes; the
ceiling exists to bound how long one commit holds the process and how large a
WAL it produces, and neither bound survived a message that overshot it.

* style(ai-vault-search): undo a stray reformat of untouched assertions

Three assertions in the file-write test were expanded by a formatter run that
was not the repo's, and the expansion survived because a multi-line object
literal is preserved once it exists. Restored to what they were.

* feat(ai-vault-search): write a session's identity with its first commit

A chunked read created its session row with an empty session id, an empty
title, an empty resume command and null cwd and timestamps, and only filled
them in on the final commit. Those chunks answer searches the moment they
land, so until the read ended every hit they produced named a session nothing
could identify — and a crash between chunks left it that way for good, since
the re-read that heals it is the same read starting over.

The reader already knows the id, cwd and timestamps from a transcript's
opening lines; it just had nowhere to put them. `TranscriptReadStart` now
carries an optional `identity()` the consumer calls during the read, backed by
an optional `identity()` on `ResumableSessionParseState`: one line in the
shared accumulator fold (which covers cursor, copilot, droid, gemini,
antigravity and the graph parsers) and one each in the Claude and Codex folds,
which keep their own state. A chunk commit writes what it returns; the final
commit overwrites it from the decoded session, so the mid-read title stays
provisional.

Two `cwd` guards in the Codex fold collapse into the `?? ` form the `branch`
line beside one of them already used. `extractString` never returns an empty
string, so it is the same assignment in one line instead of four, and it is
what keeps the file under the 300-line cap with the identity accessor added.

* refactor(ai-vault-search): drop conversation_fts for a column filter

The second FTS table existed for query latency alone: a column-filtered
`messages_fts MATCH '{user_text assistant_text}: q'` returns the identical
rowid set, which PR 2's round 5 review proved and PR 4's benchmark re-checked
per query. PR 4 then measured the filter at 1.16-1.36x the p95 of the second
table on a 105 MB corpus across two points in the tool-output band, under the
2x bar the decision was set at, and closes the stack's open decision 3.

It cost a quarter of the index on a corpus whose tool output is half the
message text. With the tool-row cap it is a smaller saving than the decision
was framed around, but it is still a table, a write per conversational row and
a delete per reclaimed one.

Schema version 4: `CREATE TABLE IF NOT EXISTS` is a no-op over an existing
index, so only the bump makes a file that still carries the table go.

* feat(ai-vault-search): expose the index handle a composed reader queries

PR 4's engine reads the index this store owns. One getter lets it compose over
the store's handle instead of opening a second connection to the same file,
and it carries the two rules this PR measured: never hold a read transaction
across an await, and no `.iterate()` outliving its statement. Either pins a
read snapshot, and a checkpoint cannot pass one, so the WAL grows without
bound for as long as it is held (10 MB to 266 MB on the write benchmark).

* fix(ai-vault-search): cut a chunk at punctuation, not only whitespace

The 8,000-character chunker backed up to the last whitespace in the second
half of the window and fell through to the target when it found none. A
minified tool result has none to find: valid minified JSON runs past 8,000
characters without a space, so the cut landed inside whatever word straddled
the target, and a search for `pericardium` matched neither `perica` nor
`rdium`. That is the shape most likely to be chunked in the first place,
because tool output is 80-97 % of a transcript's bytes.

The backoff now takes any character that cannot be inside a token — anything
outside a letter, digit or underscore — and still falls through to the target
when the window holds none, because 4,000 characters without one is not a
word. Surrogates are excluded from the class so a cut never lands between the
halves of an astral character.

A few of the tokenizer's own `tokenchars` (`. - / +`) are cut on even though
unicode61 keeps them inside a token. That costs the joined form of a path, and
only in a window with no whitespace anywhere, which is a far smaller loss than
the torn word this exists to prevent.

* fix(ai-vault-search): never chunk a read that cannot name its session

`updateProvisionalSession` is fed by the resumable readers alone. Claude and
Codex carry an `identity()` off their fold; `readWholeTranscript` supplies
none, because a format rewritten in place has no resumable state to ask. So a
Grok, Cursor, Gemini or OpenCode file large enough to pass the commit ceiling
published its chunks under a session with an empty id, an empty title and a
null cwd — rows that answer searches at once and that an interrupted read
leaves behind for good, since the re-read that heals them is the same read
starting over.

`add` now commits a chunk only while the read can name what it is writing. A
read with no identity, or one whose parser has decoded no id yet, keeps
buffering and commits whole at `finish`. The whole-file formats are the ones
with nothing to give and they are small — the largest on this machine is 5 MB
— so buffering one to the end costs nothing, and chunking stays reserved for
the readers that can say which session a prefix belongs to.

`updateProvisionalSession` takes a non-null identity now, so the invariant is
the type rather than a guard that silently wrote nothing.

* perf(ai-vault-search): cut a replaced generation loose instead of deleting it

The first transaction of a replace deleted every row of the old session before
inserting its own bounded chunk, so the transaction was sized by the history
being replaced rather than by the rows being written. On the synthetic 100 MB
transcript the longest replace transaction was 1,255-1,283 ms against
668-672 ms for the same file read fresh, and the gap grows with the session.

A replace now mints a new session row, points the `files` row at it and
deletes the one old `sessions` row, all in the same transaction. Every
retrieval joins `sessions`, so the old generation stops answering the moment
that commits — the same thing retention's first transaction does — and its
messages are reclaimed afterwards by `drainOrphanedMessages`, the bounded
batch loop that already exists for exactly this set. `sessions.id` is
AUTOINCREMENT, so the freed id is never handed to another session while those
rows still name it.

The longest replace transaction is now 746-848 ms, the same band as a fresh
read. The trade is stated plainly: the pass does more total work, 3.7 s
against 3.1 s, because the reclaim is 360 bounded transactions with an
incremental_vacuum step each instead of one large delete. It yields between
them, so none of it holds the process, and returning the pages per batch also
takes the index from 173.6 MB to 162.0 MB.

The store schedules the drain off the committing stack: an async function runs
synchronously to its first `await`, so calling it inline would put the first
batch back inside the transaction's own call. One drain at a time, with a
repeat flag for a replace that commits while one is running.

* fix(ai-vault-search): preserve search tokens and index Codex tools

* fix(ai-vault-search): match SQLite marks and Codex file-change records

* fix(ai-vault-search): preserve stat pairs and validate benchmark results
…e agent (stablyai#19929)

* fix(native-chat): pass agent-implemented slash commands through to the agent

Claim what the host implements; pass through what the agent implements.
Claude's harness expands a slash command out of the message text, so the
host claimed catalog commands it had no way to run and answered "/init is
not available in chat sessions" for commands Claude does run. Codex's
app-server has no slash parser at all, so its catalog stays claimed —
except /goal, which the model carries out through its own goal tools.

* fix(native-chat): offer the agent-run commands in the structured picker

Codex reports no command catalog, so its structured `/` menu is the host
fallback -- which listed only the host's own commands and hid `/goal`, the
one command the model itself acts on. The picker now appends the profile's
text-driven commands, described from the curated catalog, so a command that
passes through is discoverable and not merely typable.

The menu invariant holds either way: a pick is answered by the host or run
by the agent, never refused with "not available in chat sessions".

* fix mobile structured command reconciliation

* fix(mobile): keep native chat controller within lint budget

* fix mobile controller lint budget

---------

Co-authored-by: Merge Sim <sim@local>
…ee switch (stablyai#19975)

Two allocations scale with the workspace count and are rebuilt on inputs that
cannot change their result.

`workspaceSurfaces` took `renderedActiveWorktreeId` as a memo dep, but
`projectWorkspaceSurfaces` reads that id only behind a truthy
`activeWorkspaceResolvedHostId` (the folder-collision tie-break), which is null
unless the active workspace is itself a folder workspace. Every git-worktree
switch therefore re-projected every surface and re-derived the id array to reach
an identical answer. Gate the id on the host so the memo holds.

The parked-watcher sync built a fresh empty `Set` for every workspace surface,
even though only a mounted workspace can park a tab and the sync only reads the
set. Share one empty instance for the rest.

Both keep every effect firing on exactly the inputs it fired on before.
nwparker and others added 22 commits September 12, 2026 21:19
…ai#20420)

* Reduce native dependency installs to the host platform

* Remove install policy documentation

* Guard cross-arch packaging and scope release installs to the runner

electron-builder only logs a warning for a missing extraResources source,
so a host-only install silently shipped a foreign-arch slice without its
natives — `pnpm build:mac` on Apple Silicon produced an x64 DMG with no
sherpa-onnx-darwin-x64 and no @parcel/watcher-darwin-x64. The previous
beforePack hook covered only win32.

- Add assertPackagedNativeVariantsInstalled, an arch-aware check over the
  target's sherpa-onnx, @parcel/watcher, and (on Windows) node-gyp addons.
  beforePack now runs it for every platform, with remedies split: another
  architecture comes from install:release, the os:win32 addons need a
  Windows host.
- Drop --os from the release installs. Every packaging job already runs on
  a runner whose OS matches its target, so only the macOS lanes need extra
  breadth, and only on CPU for their x64+arm64 config. Windows and Linux
  packaging return to a plain host-only install.
- Add --frozen-lockfile to install:release so a bare run cannot rewrite
  the lockfile.
- Restore the install policy reference doc and the CONTRIBUTING note, plus
  the rationale comments dropped from the runtime contract test.
- Gate the packaging-closure assertions on whether the Windows addons are
  installed rather than on the host OS, so a cross-arch install exercises
  them off Windows too.
- Make the workflow contract test read `run:` steps as well as retry-action
  commands, and enforce host-only scoping on the non-macOS packaging lanes.
- Remove the unreferenced install measurement script; its numbers live in
  the policy doc.

* Track the install policy doc and index it from AGENTS.md

docs/** is ignored behind a per-file allow-list, so the new reference doc
was only committed via git add -f and future edits would be skipped. Add
it to the allow-list and give it an AGENTS.md entry like every other
tracked reference doc, so the host-only install rule is discoverable
before someone packages a second architecture.

* Route Windows-lane removals through the retrying helper

Adding these four specs to the PR Windows lane pulled them into the
windows-lane-tree-removal-boundary ratchet, which failed on 20 raw
recursive removals. On Windows a bare rmSync races a handle the OS has
not released, throwing EPERM after the assertions already passed and
reporting a green test as a lane failure.

* Adapt the packaging guard to the vendored Windows registry addon

main vendored windows-native-registry as the workspace package
@orca/windows-registry (stablyai#20438). A workspace link resolves on every
host, so including it in the installed-Windows-addons checks proved
nothing. @vscode/windows-process-tree is the only os: win32 npm addon
left, so it alone decides whether the win32 resource plan resolves.
…tablyai#20291)

* perf: deduplicate unlimited vault scans once

* perf: release discarded vault aliases during unlimited scans

* fix: bound per-session bookkeeping in unlimited vault scans

- Drop the per-session alias-key string, wrapper object and positions array
  the accumulator retained for every parsed row; index winning positions by
  the row's own sessionId instead (~430 B -> ~45 B per session at 50k rows).
- Add a --expose-gc retention test asserting a 50k mostly-unique load-all
  corpus stays under 128 B of bookkeeping per session while matching
  dedupeCodexSessionsBySessionId exactly.

* perf(ai-vault): bound per-row bookkeeping in CodexSessionCollection

Key winners by the row's own sessionId string so an unlimited scan retains no
alias-key string per live row (301 -> ~115 B/row measured over 50k rows), and
split into a per-alias-key map only for the rare id that spans several hosts,
namespaces, or rollout names, so admission stays O(1). Fold the PR's
CodexSessionAccumulator into the collection main already routes every scan
through, and rerun its scanner-level tests against that single class.
…20212)

* perf(tooling): reuse directory entry types in source scans

* fix(source-scan): stat DT_UNKNOWN dirents so untyped directories are still walked

`readdirSync(..., { withFileTypes: true })` can hand back a Dirent whose
type the filesystem did not report. For that entry every predicate is
false, so the readdir-type fast path treated a real directory as a file
and silently dropped its subtree from every ratchet guard. Fall back to
`statSync` whenever the entry is neither conclusively a file nor a
directory, keeping the no-stat fast path for ordinary entries.

Also make the two readdir-order assertions in the walk test
order-independent; `scanSourceTree` returns raw readdir order, which
differs on tmpfs.

* test(source-scan): unit-test the stat fallback via an extracted helper

The fabricated-Dirent readdir mock could not satisfy both gates at once:
vi.mocked(readdirSync) resolves to Node's Dirent<NonSharedBuffer> overload, so
the mock needed a type assertion, and stablyai#19462's casting gate rejects new ones on
changed lines. Removing the cast then failed tsc.

Extract directoryEntryNeedsStat and test it directly with a structural probe.
No mock, no cast, no top-level await, and the DT_UNKNOWN case is pinned:
removing the fallback fails 'stats an entry whose type readdir could not report'.

---------

Co-authored-by: m4air <m4air@Mac.localdomain>
Co-authored-by: Neil <neil@stably.ai>
* fix(skills): evict removed runtime discovery cache

* fix(skills): retire removed runtime cache entries using pending scan identity

* fix: rescan mounted skill consumers when a runtime re-pairs under the same id

- Fold the pairing revision into useActiveSkillDiscoveryRuntimeTarget's
  selector so a same-id re-pair yields a new runtime target and every
  mounted useInstalledAgentSkillNames effect re-runs instead of holding
  the retired peer's installed list after the module cache is evicted.
- Reset hook-local result/loading state on runtime target identity change,
  which also bumps the refresh generation so an in-flight scan issued to
  the retired peer can no longer commit its result into React state.
- Add mounted-hook regression tests covering the re-pair rescan and the
  in-flight stale-scan fence.

* fix(skills): reset discovery state via render-adjusted state, not a ref write

React Doctor flagged the render-phase write to stateResetInputRef. React can
discard a render after the write, in which case the next render sees "already
reset" and keeps painting the previous target's skill list until a rescan.
Track the reset inputs in useState and adjust it during render instead, which
React replays safely.

Also drop the `as never` / `as GlobalSettings` casts from the tests this PR
added, since main now enforces consistent-type-assertions on changed lines.

---------

Co-authored-by: m4air <m4air@Mac.localdomain>
Co-authored-by: Neil <neil@stably.ai>
…n steps (stablyai#20152)

Two defects that change nothing about when an existing schedule fires.

stablyai#16303: evaluateDueRuns awaited each row with no catch, so one unreadable schedule
skipped every later due automation in that tick. Each row is isolated now; a poison
record writes one folded skipped_unavailable run explaining itself and the tick
continues. A renderer send that throws is closed out as dispatch_failed rather than
mislabelled as an unreadable schedule.

stablyai#15895: step validation only checked integer >= 1, so a step wider than its field
degraded silently to a single value and still passed validation. Oversized steps are
refused at input time only, bounded by the count of distinct values a field holds, so
day of week rejects */8 while */7 stays legal.

Runtime parsing stays lenient so rows saved before the gate keep running the cadence
they have. isValidAutomationSchedule now answers only 'acceptable as new input'; a new
isRunnableAutomationSchedule answers 'can Orca still run this', and the editor uses it
so a legacy row opens intact and can be renamed without re-authoring a schedule that is
still firing.

Verified: 34/34 corpus expressions fire identically to main.

Fixes stablyai#16303
Fixes stablyai#15895
…ilence (stablyai#20155)

`terminal wait --for tui-idle` returned satisfied in ~0s while an agent was
mid-turn. The shared title detector defaults a name-only agent title to `idle`
so the sidebar can clear a stale spinner, and the wait accepted that stored
value as completion.

Rank the evidence instead. An explicit idle marker in the agent's own title or
a known ready prompt settles the wait; a fresh first-party OSC 9999 status
saying working/blocked/waiting vetoes it; a name-only title is a last resort
that settles only once the stream has also gone quiet. The rank is derived at
read time from `lastOscTitle` rather than stamped onto the record, because
`syncWindowGraph` rebuilds leaves from an explicit field list and would drop a
bespoke provenance field on any renderer publish.

Two things the ranking alone gets wrong are handled here too. A quiet non-shell
foreground process no longer proves idle on a pane where Orca launched a known
agent — that is an agent still booting, and resolving on it is what let
`dispatch --inject` lose the prompt (stablyai#9976). And the idle poll re-reads the live
leaf each tick, because a record captured at registration stops advancing and
its frozen `lastOutputAt` makes the quiescence gate pass while the pane streams.

The demotion is scoped to agents that go on to announce rest explicitly. Grok,
Copilot, Aider, Mimo, agy and OpenCode emit their name and nothing more at rest:
a real idle Grok pane repaints its banner about four times a second forever, so
demanding quiescence from it left no settle signal at all and the wait ran to
timeout.

The design is Brennan Benson's, from stablyai#14642, which won a cross-review against
stablyai#6012, stablyai#6555 and this branch's earlier approach; it is ported here only because
that branch shares no git history with main and cannot be merged. Neil's stablyai#6555
first drew the explicit-vs-ambiguous line the ranking rests on, and Revofusion's
stablyai#6012 first identified that a single title sample cannot prove completion.

Fixes stablyai#6011

Co-authored-by: Brennan Benson <79079362+brennanb2025@users.noreply.github.com>
Co-authored-by: Neil <4138956+nwparker@users.noreply.github.com>
Co-authored-by: Revofusion <syed@moonai.org>
…tablyai#20149)

Escape is ambiguous at the source for Claude, OMP, Pi and Prime Agent: the same
key closes an overlay and cancels a turn, and which one it meant is focus state
only the TUI holds. Nothing downstream can recover it, so for these agents a
plain Escape is never evidence a turn ended — the provider's own hook decides.
Ctrl+C is untouched, and no other agent type changes.

The renderer skips the round-trip and main re-checks the same rule, so a stale
or direct inference request cannot route around it. A navigation Escape does not
clear a Ctrl+C already waiting to settle: Escape is not a retraction.

Fixes stablyai#13547
Fixes stablyai#9208

Co-authored-by: Brennan Benson <79079362+brennanb2025@users.noreply.github.com>
Co-authored-by: Rod Boev <rod.boev@gmail.com>
…0310)

* fix(mobile): reuse the current workspace on notification taps

* revert(mobile): restore notification setting hint
…ai#20398)

* fix(sidebar): observe agent titles before workspace activation

Reuse parked terminal watchers for eligible live tabs in never-mounted workspaces, with initial title catch-up and capability-driven admission. Preserve existing watcher cleanup and avoid allocating watcher sets for empty workspaces.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* fix: reconcile background watchers when remote coverage changes

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* test: type-check terminal watcher fixtures without casts

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
)

* Add compile-time RPC params catalog parity gate

Check each registered handler against its catalog params type in both directions, with explicit exceptions for the three uncatalogued schemas.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* fix(rpc): keep the params generator off its own output

The parity gate imports the generated catalog for types, and it lives under
RPC_DIR, which indexableModules() scans for shared imports. That re-added
OUTPUT_PATH after line 46 removed it, so the generator bundled and require()d
the committed catalog. A catalog referencing a renamed or deleted shared export
then crashed regeneration — in exactly the state that requires regenerating.

Reproduced before and after: with a dangling reference injected into the
catalog, `generate:rpc-params-catalog` threw; it now rewrites the file.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
…awns (stablyai#20285)

* Widen Windows shim ratchet to detect package bin spawns

Follow local program expressions into node_modules/.bin while preserving the existing literal check, roots, and allow-list. Document static-analysis limits and cover unsafe and resolver-based invocations.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* fix(scripts): fold dot segments before matching node_modules/.bin

The predicate joins call arguments textually, so a literal '..' segment hid a
path that resolves into node_modules/.bin at runtime. Folds '.' and '..' (and
Windows separators) first. A '..' that genuinely escapes .bin still does not
match.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* style(scripts): use .at(-1) in the dot-segment fold

oxlint's prefer-at rule; the repo-wide lint gate is an error, not a warning.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
…e meaning (stablyai#20133)

* refactor(native-chat): give each structured dispatch state exactly one meaning

`unknown` meant five different things. Only one of them was genuine
ambiguity.

A transport write that the provider's input pump never took is provably
undelivered -- which is what `rejected` already means. It was recorded as
`unknown` anyway, and a one-entry allowlist then existed solely to teach
Retry that this particular `unknown` was safe to re-deliver.

Collapsing that case into `rejected` deletes the allowlist and turns a
predicate into an invariant: Retry never re-delivers an `unknown`, with no
exception to reason about. The four states now each assert one thing --
`pending` written and awaiting, `accepted` the provider has it, `rejected`
provably did not happen, `unknown` genuinely cannot tell.

A fail-closed guard is the right default here because the asymmetry is
severe: refusing a legitimate retry costs the user a retype, while allowing
an illegitimate one sends the model a second copy of their message.

Also fixed, found while auditing every reader of `rejected`:

- The renderer printed `submission.reason` verbatim, so a broken pipe put
  the internal token `provider_write_failed: broken pipe` on screen in
  destructive red. The journal reason is unchanged -- it is the durable
  evidence and the transport-versus-content discriminator -- but the screen
  now gets copy that names the cause and says the message is safe to
  resend. Content rejections still show the provider's own words.
- The fallback copy "Message was not accepted" read as a content refusal.
  A null reason now yields "Message was not sent.", which asserts only what
  every rejection shares.
- A refused worker-start preamble threw a plain Error out of the dispatch
  path. It now throws `OrchestrationError('dispatch_preamble_undelivered')`
  so a coordinator can tell "we could not send it" from "we sent it and
  something else broke" without parsing prose. Retain/discard behaviour is
  unchanged; only the verdict's legibility improves.

Two behaviours improve as a consequence rather than by design: a provably
undelivered message no longer blocks conversation commands, and no longer
leaves the session reading as "working" in chat and in every session list.

Not addressed here, and named rather than implied: a message left `unknown`
by a dead child or a host restart still has no recourse but retyping. The
restart reconciler that would decide those on evidence is written and has
never had a production caller. Parking the refused entry instead would
reintroduce the head-of-queue wedge removed in stablyai#19863, so it is not an
option.

Note for whoever edits `journal-reducer.ts` next: it sits at 297 of its 300
counted lines. The next statement added there needs a split, not a shave.

* fix(native-chat): close two gaps review found in the rejection taxonomy

Both are narrow and both were real.

A journal written before a refused write became `rejected` still holds that
submission as `unknown` with the transport marker. The predicate this change
replaced excluded exactly that shape from provider-echo matching; the
state-only check that replaced it does not, so on replay such a row could
claim the echo of a later, genuinely delivered send of the same text and
attach the delivery to the wrong message. Fail-closed still prevented any
re-delivery, so nothing duplicated — but the wrong submission was credited.
Replay now excludes the legacy shape too.

And the content-versus-transport split had a third case neither side covers:
a local capacity refusal is neither the provider explaining itself nor a
frame that failed to leave. It fell through to the verbatim branch, so
`claude structured dispatch queue is full` reached the screen — the same
class of leak this change set out to fix, one reason short of being caught.
Internal reasons now get copy; only a provider's own words are shown as
written.

Each is pinned by a test that fails with its guard reverted and passes with
it restored.

* fix(native-chat): preserve dispatch refusal across clients

* fix(native-chat): rotate immediately rejected retries

* docs(native-chat): correct rejection taxonomy reference

* docs(native-chat): align mobile retry comment

* docs(native-chat): clarify unknown replay semantics

* fix(native-chat): keep a mobile send's operation id when delivery is unknown

Mobile released the retained operation id whenever a send came back
`unknown`, so the user's next send of the same text went out under a fresh
id. A fresh id has no ledger row, so the host treats it as a first delivery
and dispatches it -- even though `unknown` is the one answer that says the
provider may already have the message. That is the duplicate this branch
exists to remove, reintroduced on the client that has no outbox.

Which case that was matters. Mobile only ever sees `unknown` from ack-loss
(`isRpcDeliveryUnknown`: "the host may have processed it and only the ack
was lost"), because the mapper reported every `ok` result as `accepted`
without reading `dispatchState`. So the rotation fired exclusively where
delivery was ambiguous and never where it was provably refused, which is
the inverse of the rule this branch establishes.

Retaining the id is what makes a retry safe, and it costs no liveness:
`performSend` answers a second request under a recorded id from the journal
and never puts it back on the wire, so a reused id delivers when nothing
landed and replays when something did. Rotating can only ever add a second
copy. The retention stays bounded by the host's admission window, which
`retainStructuredSessionOperationId` already enforces.

`retryUnknown` goes with it: the host ignores it for delivery, and all it
does is skip the cached answer to re-read the same row.

Keeping the id exposes what the rotation was hiding, so fix that too: a
replayed `unknown` comes back `ok`, and mobile called it `accepted` and
cleared the composer as if the message had landed. `dispatchState` now
decides, in one pure function:

  accepted/pending  sent, and the id is spent
  rejected          provably did not happen and terminal in the reducer, so
                    reusing the id could only replay that rejection: spent,
                    and the next attempt is a first delivery under a new id
  unknown           keeps its id

Reading `dispatchState` at all is a pre-existing defect, fixed here because
the false "sent" cannot be removed without it, and scoped to the send path.
`mutate`'s rotation for prompt/option/cancel plans is untouched. The
rejection copy is the desktop's notice, so an internal reason
(`provider_write_failed: ...`) still never reaches a person.

Tests: the hook test that was flipped to assert a rotated id now pins the
opposite -- one id across an ack-loss and two `unknown` replays, each
reported `unknown` rather than `accepted`. The send fixture grew the durable
submission row a real host returns; without it every send test asserted
against a shape that cannot express the bug.

* fix(native-chat): enforce fail-closed structured send replay

* fix(native-chat): align retry and mobile RPC contracts

* fix(native-chat): keep transient admissions retryable

* test(tab-bar): expand nested create menu in harness

---------

Co-authored-by: Merge Sim <sim@local>
…story (stablyai#20139)

* feat(native-chat): decide a restart-stranded send against provider history

`markPendingSubmissionsUnknown` flips every surviving `pending` submission
to `unknown` on attach and stops there. The module written to finish the job
describes the intended two-step in its own header -- "Every surviving
`pending` becomes `unknown` and is then matched against provider history" --
and only the first step ever shipped. `reconcileSubmissions` has been
imported by exactly one test file and nothing else.

So a message stranded by a dead child or a host restart had no recourse but
retyping: Retry correctly refuses to redeliver something that may already be
with the model, the outbox entry drops, and a transient error line is all
that remains. This wires the second step, so those are decided on evidence
instead of refused.

Caller placement is the design decision, because where it runs determines
what a consistent history boundary can mean. It runs in `attachJournal`,
immediately after the sweep: attach happens after the record store's CAS
hands this host the lease and before a provider child starts, so nothing can
append to provider history while it is read, and the window stays valid
until the resume consumes it. The three other settlement sites can all be
overtaken by a newly started child before the read is acted on.

The history source is the Claude project JSONL for the handle chain's
provider session id -- definitionally what a resume replays, which is what
makes absence meaningful. Boundary consistency reuses
`proveClaudeTranscriptBranchFromJsonl` rather than inventing a check:
a fork, a compacted log and a truncated tail each already throw there, and
each maps onto `boundaryConsistent: false`. A null leaf uuid is also false,
because there is no anchor to prove a start from.

Two guards were needed that the reconciler cannot enforce itself, because
Claude echoes no client message id and only the fingerprint pass can fire:

- A transcript records a pasted image as base64, and the block decoder drops
  it silently for want of a url or path. Such a record would enter the
  window advertising a text-only fingerprint, where an unrelated text-only
  submission with identical text could claim it. The window now inspects raw
  content parts before decoding and excludes any record a part would be
  dropped from.
- A submission carrying an image-ref path can never match a transcript that
  keeps only base64. Without a guard it matches nothing by construction
  rather than by absence and falls straight through to `not_delivered`, and
  a Retry would then redeliver an image already sent. Only text-only bodies
  are handed to the reconciler.

Both guards fail a named test when removed.

Limits, stated rather than implied. The exact-match tier needs the provider
to echo our id, which Codex does and Claude does not, so Claude resolves by
fingerprint alone -- and two identical prompts deliberately reach
`ambiguous_match` instead of guessing. Repeated one-word prompts therefore
stay unknown by construction. This decides what it can prove and refuses the
rest, which is the intended contract, not a shortfall in the wiring.

Found while doing this and not fixed here: the block decoder silently
dropping base64 images has a blast radius beyond reconciliation and deserves
its own change.

* fix(native-chat): harden restart history reconciliation

* fix(native-chat): keep Claude adapter within lint budget

---------

Co-authored-by: Merge Sim <sim@local>
…auto-sync-fork-with-upstream-stablyai-orca-ru-20260914T1400

# Conflicts:
#	src/renderer/src/components/native-chat/NativeChatComposerField.tsx
Applies the manifest classifier's four lists, restores 49 undeclared fork
edits the reset reverted, replays v1.4.202 into the tier-2 composer copies,
and merges upstream's delta into the exception-owned files that discarded it.
Resolves 31 hunks -X ours silently took the fork side of across 13 seam files,
restores the four RPC schema modules v1.4.202 turned into re-export barrels and
moves their fork seams into src/shared/rpc-contract, re-homes the tool-name
colouring seam into the new NativeChatToolLine module, and adapts the fork to
v1.4.202's params-catalog parity gate.
…duals

Re-homes the seams v1.4.202's rpc-contract split moved, retires the mid-prompt
picker seam upstream absorbed, and declares the 49 project-ledger edits to
upstream files that PR #55 left undeclared — a reset would have dropped them.
…oser-send test

v1.4.202 added the test type-importing the attachment type from the deleted
NativeChatComposerField; it takes the same swap its three siblings already do.
Copilot AI lite review requested due to automatic review settings September 14, 2026 14:50

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.

🟡 Changes recommended

Critical CI/test failures and unresolved moderate correctness issues remain.

Get a fresh assessment by requesting another Copilot review.

Pull request overview

This pull request absorbs upstream stable v1.4.202 into the fork while replaying fork-owned seams and changes.

Changes:

  • Resolves upstream module splits and ownership residuals.
  • Updates native-chat, runtime/RPC contracts, localization, mobile, cloud, and tests.
  • Refreshes synchronization and release workflow integrations.

Open findings: two critical test/CI issues, three moderate issues, and one nit remain unresolved, with the supplied vote counts preserved.

File summaries
File Review scope
tests/e2e/linear-url-workspace-entry.spec.ts Upstream sync and fork replay
tests/e2e/helpers/source-control-ai-generation.ts Upstream sync and fork replay
src/types/psl.ts Upstream sync and fork replay
src/shared/workspace-session-schema.ts Upstream sync and fork replay
src/shared/utf8-byte-limits.ts Upstream sync and fork replay
src/shared/text-search.ts Search result finalization
src/shared/structured-native-chat-launch-route.test.ts Upstream sync and fork replay
src/shared/structured-agent-session-mutation.ts Upstream sync and fork replay
src/shared/runtime-worktree-contracts.ts Upstream sync and fork replay
src/shared/rpc-contract/workspace-ports-params.ts Upstream sync and fork replay
src/shared/rpc-contract/updater-params.ts Upstream sync and fork replay
src/shared/rpc-contract/terminal-viewport-methods-params.ts Upstream sync and fork replay
src/shared/rpc-contract/ssh-params.ts Upstream sync and fork replay
src/shared/rpc-contract/skills-params.ts Upstream sync and fork replay
src/shared/rpc-contract/session-tabs-params.ts Upstream sync and fork replay
src/shared/rpc-contract/runtime-client-capabilities-params.ts Upstream sync and fork replay
src/shared/rpc-contract/preflight-params.ts Upstream sync and fork replay
src/shared/rpc-contract/orchestration-worker-stop-params.ts Upstream sync and fork replay
src/shared/rpc-contract/orchestration-worker-release-params.ts Upstream sync and fork replay
src/shared/rpc-contract/orchestration-worker-control-params.ts Upstream sync and fork replay
src/shared/rpc-contract/orchestration-runs-mutation-request-show-params.ts Upstream sync and fork replay
src/shared/rpc-contract/github-repo-target-params.ts Upstream sync and fork replay
src/shared/rpc-contract/github-issue-update-params.ts Upstream sync and fork replay
src/shared/rpc-contract/git-admission-tier-params.ts Upstream sync and fork replay
src/shared/rpc-contract/files-target-params.ts Upstream sync and fork replay
src/shared/rpc-contract/computer-params.ts Upstream sync and fork replay
src/shared/rpc-contract/client-ui-workspace-filter-fields-params.ts Upstream sync and fork replay
src/shared/rpc-contract/client-events-params.ts Upstream sync and fork replay
src/shared/rpc-contract/browser-screencast-params.ts Upstream sync and fork replay
src/shared/rpc-contract/browser-extras-params.ts Upstream sync and fork replay
src/shared/rpc-contract/browser-core-params.ts Upstream sync and fork replay
src/shared/rpc-contract/agent-hooks-params.ts Upstream sync and fork replay
src/shared/remote-workspace-types.ts Upstream sync and fork replay
src/shared/remote-runtime-client-capabilities.ts Upstream sync and fork replay
src/shared/relay-frame-buffer.ts Upstream sync and fork replay
src/shared/project-groups.ts Upstream sync and fork replay
src/shared/pr-comment-audience.ts Upstream sync and fork replay
src/shared/persisted-ui-state-types.ts Upstream sync and fork replay
src/shared/pane-agent-identity-inventory.test.ts Upstream sync and fork replay
src/shared/native-chat-types.ts Upstream sync and fork replay
src/shared/native-chat-session-option-state.ts Upstream sync and fork replay
src/shared/hosted-review-refs.ts Upstream sync and fork replay
src/shared/fork-workspace-review-filters/workspace-review-ui-schema.ts Upstream sync and fork replay
src/shared/fork-workspace-activity-window/workspace-activity-ui-schema.ts Upstream sync and fork replay
src/shared/cross-platform-path.ts Upstream sync and fork replay
src/shared/constants.ts Upstream sync and fork replay
src/shared/commit-message-agent-specs-primary.ts Upstream sync and fork replay
src/shared/child-process/windows-command-line.ts Upstream sync and fork replay
src/shared/child-process/bounded-output-sink.ts Upstream sync and fork replay
src/shared/agent-status-observation.ts Upstream sync and fork replay
src/shared/agent-session-host-authority.ts Upstream sync and fork replay
src/shared/agent-process-recognition.ts Upstream sync and fork replay
src/renderer/src/web/web-runtime-client-timeout-budget.test.ts Upstream sync and fork replay
src/renderer/src/web/web-preload-api-composition.test.ts Upstream sync and fork replay
src/renderer/src/web/preload-api/web-notifications-api.ts Upstream sync and fork replay
src/renderer/src/store/slices/ssh-target-cleanup.ts Upstream sync and fork replay
src/renderer/src/store/slices/agent-status-live-entry-builder.ts Upstream sync and fork replay
src/renderer/src/store/slices/agent-status-contract.ts Upstream sync and fork replay
src/renderer/src/runtime/web-runtime-terminal-create-operation.ts Upstream sync and fork replay
src/renderer/src/runtime/web-runtime-session-types.ts Upstream sync and fork replay
src/renderer/src/lib/worktree-creation-flow.test.ts Upstream sync and fork replay
src/renderer/src/lib/structured-agent-session-launch-label.ts Upstream sync and fork replay
src/renderer/src/lib/structured-agent-session-launch-callers.ts Upstream sync and fork replay
src/renderer/src/lib/session-write-subscriber.ts Upstream sync and fork replay
src/renderer/src/lib/pending-worktree-creation.ts Upstream sync and fork replay
src/renderer/src/lib/palette-match/palette-query.ts Upstream sync and fork replay
src/renderer/src/lib/browser-palette-search.ts Upstream sync and fork replay
src/renderer/src/i18n/locales/zh.json Upstream sync and fork replay
src/renderer/src/i18n/locales/ko.json Upstream sync and fork replay
src/renderer/src/i18n/locales/ja.json Upstream sync and fork replay
src/renderer/src/i18n/locales/es.json Upstream sync and fork replay
src/renderer/src/hooks/settings-navigation-interface-sections.ts Upstream sync and fork replay
src/renderer/src/hooks/remote-workspace-session-merge.ts Upstream sync and fork replay
src/renderer/src/hooks/ipc-events-test-harness.ts Upstream sync and fork replay
src/renderer/src/hooks/ipc-events-agent-status-window-test-fixtures.ts Upstream sync and fork replay
src/renderer/src/hooks/ipc-events-agent-status-store-test-fixtures.ts Upstream sync and fork replay
src/renderer/src/hooks/installed-agent-skill-discovery-cache.ts Upstream sync and fork replay
src/renderer/src/hooks/composer-state/full-creation-execution.test.ts Upstream sync and fork replay
src/renderer/src/hooks/composer-state/folder-submit-orchestration.ts Upstream sync and fork replay
src/renderer/src/hooks/composer-state/composer-submit-orchestration.ts Upstream sync and fork replay
src/renderer/src/hooks/agent-hook-completion-store-sync.test.ts Upstream sync and fork replay
src/renderer/src/components/workspace-surface-projection.ts Upstream sync and fork replay
src/renderer/src/components/terminal-pane/TerminalPaneSurface.tsx Upstream sync and fork replay
src/renderer/src/components/terminal-pane/terminal-pane-lifecycle-types.ts Upstream sync and fork replay
src/renderer/src/components/terminal-pane/pty-connection/unbound-pane-spawn-recovery.ts Upstream sync and fork replay
src/renderer/src/components/terminal-pane/pty-connection/direct-ssh-retry-status.ts Upstream sync and fork replay
src/renderer/src/components/terminal-pane/pty-connection-types.ts Upstream sync and fork replay
src/renderer/src/components/terminal-pane/parked-terminal-byte-watcher.ts Upstream sync and fork replay
src/renderer/src/components/terminal-pane/pane-helpers.ts Upstream sync and fork replay
src/renderer/src/components/tab-bar/tab-bar-static-create-menu.tsx Upstream sync and fork replay
src/renderer/src/components/status-bar/resource-session-classification-parity.test.ts Upstream sync and fork replay
src/renderer/src/components/sidebar/worktree-list/viewport/virtual-row-context.ts Upstream sync and fork replay
src/renderer/src/components/sidebar/worktree-list/drag/use-native-drag.ts Upstream sync and fork replay
src/renderer/src/components/sidebar/worktree-list/drag/pointer-commit.ts Upstream sync and fork replay
src/renderer/src/components/sidebar/sidebar-host-options.test.ts Upstream sync and fork replay
src/renderer/src/components/sidebar/fork-worktree-groups/WorktreeGroupMenuItems.tsx Upstream sync and fork replay
src/renderer/src/components/settings/AppearancePane.tsx Upstream sync and fork replay
src/renderer/src/components/NotificationCardStack.tsx Upstream sync and fork replay
src/renderer/src/components/new-workspace/smart-workspace-name-input-surface.tsx Upstream sync and fork replay
src/renderer/src/components/native-chat/use-native-chat-picker-command-dispatch.test.tsx Upstream sync and fork replay
src/renderer/src/components/native-chat/use-native-chat-composer-catalog.ts Upstream sync and fork replay
src/renderer/src/components/native-chat/NativeChatView.tsx Upstream sync and fork replay
src/renderer/src/components/native-chat/native-chat-prompt-document.ts Upstream sync and fork replay
src/renderer/src/components/native-chat/fork-native-chat-relay/use-native-chat-live-session.ts Upstream sync and fork replay
src/renderer/src/components/native-chat/fork-native-chat-relay/native-chat-view-state.ts Upstream sync and fork replay
src/renderer/src/components/native-chat/fork-native-chat-relay/native-chat-session-transport.ts Upstream sync and fork replay
src/renderer/src/components/native-chat/fork-native-chat-relay/native-chat-runtime-owner.ts Upstream sync and fork replay
src/renderer/src/components/native-chat/fork-native-chat-relay/native-chat-runtime-contract.ts Upstream sync and fork replay
src/renderer/src/components/native-chat/fork-native-chat-relay/native-chat-pagination.ts Upstream sync and fork replay
src/renderer/src/components/native-chat/fork-native-chat-relay/native-chat-live-status.ts Upstream sync and fork replay
src/renderer/src/components/native-chat/fork-agent-composer/AgentComposerActions.tsx Upstream sync and fork replay
src/renderer/src/components/editor/combined-diff/browse-files/combined-diff-file-tree-filter.ts Upstream sync and fork replay
src/renderer/src/components/dashboard/use-agent-row-conversation-name.ts Upstream sync and fork replay
src/renderer/src/components/dashboard/dashboard-card-labels.ts Upstream sync and fork replay
src/renderer/src/components/dashboard/agent-finished-timestamp.ts Upstream sync and fork replay
src/renderer/src/components/dashboard/agent-finished-timestamp.test.ts Upstream sync and fork replay
src/renderer/src/components/cmd-j/palette-host-badge.ts Upstream sync and fork replay
src/renderer/src/components/automations/use-automations-page-local-state.ts Upstream sync and fork replay
src/renderer/src/components/automations/use-automation-editor-actions.ts Upstream sync and fork replay
src/renderer/src/components/automations/AutomationSchedulePicker.test.ts Upstream sync and fork replay
src/renderer/src/components/automations/AutomationSchedulePicker.render.test.tsx Upstream sync and fork replay
src/renderer/src/components/automations/AutomationEditorDialog.tsx Upstream sync and fork replay
src/renderer/src/assets/main.css Upstream sync and fork replay
src/preload/updater-package-recovery.test.ts Upstream sync and fork replay
src/preload/ssh-authority-forwarding.test.ts Upstream sync and fork replay
src/preload/pty-snapshot-capability-ipc.test.ts Upstream sync and fork replay
src/preload/index.ts Upstream sync and fork replay
src/preload/app-restart-checkpoint-routing.test.ts Upstream sync and fork replay
src/preload/api/runtime-api.ts Upstream sync and fork replay
src/preload/api/os-permission-api.ts Upstream sync and fork replay
src/preload/api/notifications-bridge.ts Upstream sync and fork replay
src/preload/api-types.ts Upstream sync and fork replay
src/main/wsl-running-path-filter.ts Upstream sync and fork replay
src/main/windows/windows-pty-job.win32.test.ts Upstream sync and fork replay
src/main/windows-native-registry.ts Upstream sync and fork replay
src/main/startup/windows-shell-path-ownership.ts Upstream sync and fork replay
src/main/startup/main-process-quit.ts Upstream sync and fork replay
src/main/ssh/ssh-relay-session.ts Upstream sync and fork replay
src/main/skills/skill-package-identity.ts Upstream sync and fork replay
src/main/runtime/worktree-pty-host-fence.ts Upstream sync and fork replay
src/main/runtime/terminal-tail-sentinel-index.test.ts Upstream sync and fork replay
src/main/runtime/structured-conversation-tab-replacement.ts Upstream sync and fork replay
src/main/runtime/runtime-worktree-agent-source.ts Upstream sync and fork replay
src/main/runtime/runtime-rpc/runtime-rpc-mobile-method-allowlist.ts Upstream sync and fork replay
src/main/runtime/runtime-rpc-mobile-terminal-streaming.test.ts Upstream sync and fork replay
src/main/runtime/runtime-rpc-mobile-method-allowlist-fixtures.ts Upstream sync and fork replay
src/main/runtime/runtime-remote-managed-worktree-create.ts Upstream sync and fork replay
src/main/runtime/runtime-local-worktree-terminal-startup.ts Upstream sync and fork replay
src/main/runtime/runtime-folder-worktree-create.ts Upstream sync and fork replay
src/main/runtime/runtime-desktop-surface.ts Upstream sync and fork replay
src/main/runtime/rpc/methods/worktree-catalog-methods.ts Upstream sync and fork replay
src/main/runtime/rpc/methods/terminal/terminal-query-methods.ts Upstream sync and fork replay
src/main/runtime/rpc/methods/terminal/terminal-output-batcher.ts Upstream sync and fork replay
src/main/runtime/rpc/methods/terminal/terminal-lifecycle-methods.ts Upstream sync and fork replay
src/main/runtime/rpc/methods/status.ts Upstream sync and fork replay
src/main/runtime/rpc/methods/stats.ts Upstream sync and fork replay
src/main/runtime/rpc/methods/session-tab-markdown-methods.ts Upstream sync and fork replay
src/main/runtime/rpc/methods/pairing.ts Upstream sync and fork replay
src/main/runtime/rpc/methods/orchestration/runs/reset-methods.ts Upstream sync and fork replay
src/main/runtime/rpc/methods/orchestration-worker-start-mode.ts Upstream sync and fork replay
src/main/runtime/rpc/methods/notification-stream-policy.ts Upstream sync and fork replay
src/main/runtime/rpc/methods/mobile-markdown-tab-methods.ts Upstream sync and fork replay
src/main/runtime/rpc/methods/ledger.ts Upstream sync and fork replay
src/main/runtime/rpc/methods/index.ts Upstream sync and fork replay
src/main/runtime/rpc/methods/host-capabilities.ts Upstream sync and fork replay
src/main/runtime/rpc/methods/github.ts Upstream sync and fork replay
src/main/runtime/rpc/methods/github-repo-target-schemas.ts Upstream sync and fork replay
src/main/runtime/rpc/methods/github-issue-update-schema.ts Upstream sync and fork replay
src/main/runtime/rpc/methods/git.ts Upstream sync and fork replay
src/main/runtime/rpc/methods/git-diff-methods.ts Upstream sync and fork replay
src/main/runtime/rpc/methods/git-commit-message-generation-methods.ts Upstream sync and fork replay
src/main/runtime/rpc/methods/git-admission-tier-schema.ts Upstream sync and fork replay
src/main/runtime/rpc/methods/fork-ask-question-tool/ask.ts Upstream sync and fork replay
src/main/runtime/rpc/methods/files-target-schemas.ts Upstream sync and fork replay
src/main/runtime/rpc/methods/diagnostics.ts Upstream sync and fork replay
src/main/runtime/rpc/methods/client-ui.test.ts Upstream sync and fork replay
src/main/runtime/rpc/methods/client-ui-workspace-filter-fields.ts Upstream sync and fork replay
src/main/runtime/rpc/methods/browser-text-rpc-methods.ts Upstream sync and fork replay
src/main/runtime/rpc/methods/browser-client-host.ts Upstream sync and fork replay
src/main/runtime/rpc/methods/accounts.test.ts Upstream sync and fork replay
src/main/runtime/rpc/errors.ts Upstream sync and fork replay
src/main/runtime/remote-runtime-close-intent.integration.test.ts Upstream sync and fork replay
src/main/runtime/relay/relay-region-probe-log.test.ts Upstream sync and fork replay
src/main/runtime/relay/relay-control-protocol.ts Upstream sync and fork replay
src/main/runtime/relay/relay-control-client.test.ts Upstream sync and fork replay
src/main/runtime/push/push-gateway-origin.ts Upstream sync and fork replay
src/main/runtime/orchestration/worker-output-archive.ts Upstream sync and fork replay
src/main/runtime/orchestration/structured-mailbox-pointer-host.test.ts Upstream sync and fork replay
src/main/runtime/orchestration/structured-mailbox-pointer-delivery.ts Upstream sync and fork replay
src/main/runtime/orchestration/structured-mailbox-pointer-delivery.test.ts Upstream sync and fork replay
src/main/runtime/orchestration/mailbox-pointer-state.ts Upstream sync and fork replay
src/main/runtime/orchestration/mailbox-pointer-stage.ts Upstream sync and fork replay
src/main/runtime/orca-runtime.test.ts Upstream sync and fork replay
src/main/runtime/orca-runtime-tests/terminal-output-and-worker-recovery-part-02.spec.ts Upstream sync and fork replay
src/main/runtime/orca-runtime-tests/headless-snapshots.spec.ts Upstream sync and fork replay
src/main/runtime/orca-runtime-tail-wait-memo.test.ts Upstream sync and fork replay
src/main/runtime/orca-runtime-get-pty-record-for-pane-key.ts Upstream sync and fork replay
src/main/runtime/orca-runtime-build-pty-terminal-summary.ts Upstream sync and fork replay
src/main/runtime/__fixtures__/antigravity-ready-api-key-gemini-model.meta.json Upstream sync and fork replay
src/main/runtime/__fixtures__/antigravity-ready-account-info-hidden.meta.json Upstream sync and fork replay
src/main/runtime/__fixtures__/antigravity-dialog-trust-workspace.meta.json Upstream sync and fork replay
src/main/runtime/__fixtures__/antigravity-dialog-model-picker.meta.json Upstream sync and fork replay
src/main/runtime/__fixtures__/antigravity-dialog-dismissed.meta.json Upstream sync and fork replay
src/main/runtime/__fixtures__/antigravity-dialog-command-palette.meta.json Upstream sync and fork replay
src/main/runtime/__fixtures__/antigravity-busy-turn-ended.meta.json Upstream sync and fork replay
src/main/runtime/__fixtures__/antigravity-busy-mid-turn.meta.json Upstream sync and fork replay
src/main/orcad/orcad-sidecar-runtime-client.ts Upstream sync and fork replay
src/main/observability/diagnostic-upload-http.ts Upstream sync and fork replay
src/main/observability/bundle.ts Upstream sync and fork replay
src/main/native-chat/transcript-record-blocks.ts Upstream sync and fork replay
src/main/native-chat/transcript-reader-codex-history-mode.test.ts Upstream sync and fork replay
src/main/native-chat/transcript-line-decoders-codex.ts Upstream sync and fork replay
src/main/native-chat/fork-native-chat-relay/transcript-watch-subscription.ts Upstream sync and fork replay
src/main/native-chat/fork-native-chat-relay/transcript-tail-reader.ts Upstream sync and fork replay
src/main/native-chat/agent-session-wire/unhandled-provider-frame.test.ts Upstream sync and fork replay
src/main/native-chat/agent-session-wire/structured-agent-session-host-handoff.ts Upstream sync and fork replay
src/main/native-chat/agent-session-wire/structured-agent-session-event-sink-queue.ts Upstream sync and fork replay
src/main/native-chat/agent-session-wire/structured-agent-session-close-retry.test.ts Upstream sync and fork replay
src/main/native-chat/agent-session-wire/structured-agent-session-attach-context.ts Upstream sync and fork replay
src/main/native-chat/agent-session-wire/agent-session-empty-batch.ts Upstream sync and fork replay
src/main/native-chat/agent-session-journal/journal-store-contracts.ts Upstream sync and fork replay
src/main/native-chat/agent-session-journal/journal-row-schema.ts Upstream sync and fork replay
src/main/native-chat/agent-session-journal/journal-item-revision.ts Upstream sync and fork replay
src/main/memory/collector.test.ts Upstream sync and fork replay
src/main/memory/collector-windows-sweep.test.ts Upstream sync and fork replay
src/main/ipc/worktree-git-common-watch.test.ts Upstream sync and fork replay
src/main/ipc/terminal-preview-output-stream.ts Upstream sync and fork replay
src/main/ipc/runtime-environment-support-routing.test.ts Upstream sync and fork replay
src/main/ipc/runtime-environment-handler-channels.ts Upstream sync and fork replay
src/main/ipc/filesystem-watcher-local-subscription.ts Upstream sync and fork replay
src/main/ipc/ai-vault.test.ts Upstream sync and fork replay
src/main/ipc/ai-vault-scan-coalescing.test.ts Upstream sync and fork replay
src/main/host/electron-runtime-desktop-surface.ts Upstream sync and fork replay
src/main/git/command-runner/git-exec-options.ts Upstream sync and fork replay
src/main/codex/config-settings-promotion.test.ts Upstream sync and fork replay
src/main/codex/config-settings-baseline-upgrade.test.ts Upstream sync and fork replay
src/main/codex/codex-structured-journal-limits.ts Upstream sync and fork replay
src/main/codex/codex-structured-journal-contracts.ts Upstream sync and fork replay
src/main/codex/codex-persistent-command-retention.test.ts Upstream sync and fork replay
src/main/codex-usage/codex-usage-rollup-projections.ts Upstream sync and fork replay
src/main/claude/claude-structured-session-recovery.test.ts Upstream sync and fork replay
src/main/claude/claude-structured-options.test.ts Upstream sync and fork replay
src/main/claude/claude-structured-item-translation.ts Upstream sync and fork replay
src/main/browser/browser-client-page-inventory.ts Upstream sync and fork replay
src/main/browser/browser-client-page-inventory.test.ts Upstream sync and fork replay
src/main/ai-vault/session-scanner-opencode-sqlite-open.ts Upstream sync and fork replay
src/main/ai-vault/session-scanner-omp-subagent-transcripts.ts Upstream sync and fork replay
src/main/ai-vault/session-parse-cache-store.ts Upstream sync and fork replay
src/main/agent-hooks/server/server-status-identity.ts Upstream sync and fork replay
src/main/agent-hooks/server/server-status-disposition.ts Upstream sync and fork replay
src/main/agent-hooks/server/server-reaping.ts Upstream sync and fork replay
src/main/agent-hooks/server/server-persistence-validation.ts Upstream sync and fork replay
src/main/agent-hooks/server.ts Upstream sync and fork replay
src/main/agent-hooks/server-relay-listener-replay.test.ts Upstream sync and fork replay
src/main/agent-awake-service-platform-assertions.test.ts Upstream sync and fork replay
resources/onboarding/feature-wall/tile-01.recorded-at.json Upstream sync and fork replay
native/windows-registry/package.json Upstream sync and fork replay
native/windows-registry/binding.gyp Upstream sync and fork replay
mobile/src/transport/settings-host-client-lifecycle.test.ts Upstream sync and fork replay
mobile/src/transport/runtime-capability-probe.ts Upstream sync and fork replay
mobile/src/transport/rpc-client-stream-registry.ts Upstream sync and fork replay
mobile/src/transport/rpc-client-capabilities.test.ts Upstream sync and fork replay
mobile/src/transport/mobile-relay-rpc-streams.ts Upstream sync and fork replay
mobile/src/transport/mobile-relay-rpc-session.ts Upstream sync and fork replay
mobile/src/transport/host-open-recovery.test.tsx Upstream sync and fork replay
mobile/src/terminal/terminal-live-text-commit.ts Upstream sync and fork replay
mobile/src/settings/voice-settings-operations.ts Upstream sync and fork replay
mobile/src/settings/notification-settings-operations.ts Upstream sync and fork replay
mobile/src/session/use-mobile-structured-send-operation-reconciliation.ts Upstream sync and fork replay
mobile/src/session/use-mobile-session-image-attachments.ts Upstream sync and fork replay
mobile/src/session/use-mobile-session-controller.ts Upstream sync and fork replay
mobile/src/session/use-mobile-native-chat-image-attachments.test.ts Upstream sync and fork replay
mobile/src/session/MobileNativeChatOverlay.tsx Upstream sync and fork replay
mobile/src/session/MobileNativeChatComposer.tsx Upstream sync and fork replay
mobile/src/session/mobile-native-chat-message-text.ts Upstream sync and fork replay
mobile/src/onboarding/mobile-onboarding-styles.ts Upstream sync and fork replay
mobile/src/notifications/notification-viewing-policy.ts Upstream sync and fork replay
mobile/src/notifications/native-push-dismissal.ts Upstream sync and fork replay
mobile/src/notifications/native-push-dismissal.ios.ts Upstream sync and fork replay
mobile/src/notifications/native-notification-data.ts Upstream sync and fork replay
mobile/src/notifications/desktop-notification-events.ts Upstream sync and fork replay
mobile/src/host-screen/use-host-repo-metadata.ts Upstream sync and fork replay
mobile/src/host-route-action-state.ts Upstream sync and fork replay
mobile/src/diagnostics/diagnostics-device-operations.ts Upstream sync and fork replay
mobile/src/components/new-worktree-modal-types.ts Upstream sync and fork replay
mobile/src/components/MobileSelectableText.tsx Upstream sync and fork replay
mobile/src/components/mobile-rich-markdown-editor-script-string.ts Upstream sync and fork replay
mobile/src/components/mobile-rich-markdown-editor-script-secondary.ts Upstream sync and fork replay
mobile/src/components/mobile-rich-markdown-editor-document-suffix.ts Upstream sync and fork replay
mobile/pnpm-workspace.yaml Upstream sync and fork replay
mobile/modules/orca-notification-dismissal/package.json Upstream sync and fork replay
mobile/modules/orca-notification-dismissal/expo-module.config.json Upstream sync and fork replay
mobile/index.js Upstream sync and fork replay
mobile/app/mobile-onboarding.tsx Upstream sync and fork replay
mobile/app.json Upstream sync and fork replay
electron.vite.config.ts Upstream sync and fork replay
docs/reference/headless-linux-server.md Upstream sync and fork replay
config/scripts/vendor-feature-wall-assets.mjs Upstream sync and fork replay
config/scripts/rebuild-native-deps-test-fixtures.mjs Upstream sync and fork replay
config/scripts/rebuild-native-deps-node-pty.test.mjs Upstream sync and fork replay
config/scripts/pr-code-change-scope.mjs Upstream sync and fork replay
config/scripts/orchestration-skill-guidance.test.mjs Upstream sync and fork replay
config/scripts/locale-translation-policy.mjs Upstream sync and fork replay
config/oxlint-code-quality-casting.json Upstream sync and fork replay
cloud/packages/relay-contract/src/index.ts Upstream sync and fork replay
cloud/packages/push-contract/tsconfig.json Upstream sync and fork replay
cloud/packages/push-contract/tsconfig.build.json Upstream sync and fork replay
cloud/packages/push-contract/src/index.ts Upstream sync and fork replay
cloud/packages/postgres-schema/tsconfig.json Upstream sync and fork replay
cloud/packages/postgres-schema/tsconfig.build.json Upstream sync and fork replay
cloud/infra/terraform/relay.tf Upstream sync and fork replay
cloud/infra/terraform/relay-github-workflow-trust.tf Upstream sync and fork replay
cloud/infra/terraform/relay-github-actions.tf Upstream sync and fork replay
cloud/infra/terraform/environments/staging.tfvars Upstream sync and fork replay
cloud/dev/scripts/validate-relay-capacity-plan.test.mjs Upstream sync and fork replay
cloud/apps/relay/tsconfig.build.json Upstream sync and fork replay
cloud/apps/relay/src/admin-token-verifier.ts Upstream sync and fork replay
cloud/apps/relay-ops/src/incident-monitor.ts Upstream sync and fork replay
cloud/apps/push/vitest.config.ts Upstream sync and fork replay
cloud/apps/push/tsconfig.json Upstream sync and fork replay
cloud/apps/push/tsconfig.build.json Upstream sync and fork replay
cloud/apps/push/src/push-provider-outcome.ts Upstream sync and fork replay
cloud/apps/push/src/push-delivery-payload.ts Push payload parsing
cloud/apps/push/src/provider-retry-delay.ts Upstream sync and fork replay
cloud/apps/push/src/canonical-base64.ts Upstream sync and fork replay
.oxlintrc.json Upstream sync and fork replay
.github/workflows/windows-signing-rehearsal.yml Upstream sync and fork replay
.github/workflows/release-cut.yml Upstream sync and fork replay
.github/workflows/mobile.yml Upstream sync and fork replay
.github/workflows/mobile-ios-release.yml Upstream sync and fork replay
.github/workflows/dev-channel-win-build.yml Upstream sync and fork replay
.github/workflows/cloud-verify.yml Upstream sync and fork replay
.github/CONTRIBUTING.md Upstream sync and fork replay
Review details

Suppressed comments (1)

cloud/apps/push/src/push-delivery-payload.ts:7

  • The persisted JSON is only checked for being a non-array object and then cast to PushNotification, so malformed or stale rows bypass the required field, size, dismissal, and strict-unknown-key checks in PushNotificationSchema. DurablePushStore feeds this value directly into dismissal and delivery code; validate it with the shared schema instead of trusting the cast.
  • Files reviewed: 188/2372 changed files
  • Comments generated: 3
  • Review effort level: Lite

💡 Configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment on lines +70 to +71
const test = steps.find((step) => step.name === 'Test Windows-specific boundaries')
expect(test.run).toContain('config/scripts/windows-process-tree-gyp-path.test.mjs')
Comment on lines +286 to +290
<NativeChatTranscriptItems
slots={slots}
context={rowContext}
window={transcriptWindow}
/>
max_attempts: 3
retry_wait_seconds: 30
command: pnpm install --frozen-lockfile
command: pnpm install --frozen-lockfile --cpu=current,x64,arm64
Reconciles the terminal-dock focus redirect with v1.4.202's chat-cover rule —
the dock composer, when a pane has one, takes focus directly so the manager no
longer focuses the xterm beneath it, and every other pane follows upstream's
new focus:!covered contract. Installs upstream's windowing test viewport in the
fork's code-coloring suite, defines the REPO_ROOT the arch-guard replay needs,
and restates the new Windows-lane contract as the absence this fork intends.
Upstream landed the mid-prompt `/` picker itself with broader semantics than the
fork shipped: commands are offered mid-draft too and every known profile enables
skills, with `dispatchable` — not `commandsEnabled` — carrying the guarantee that
a mid-draft pick completes the token instead of sending it.
@zpyoung
zpyoung merged commit 23cae70 into main Sep 14, 2026
47 checks passed
@zpyoung
zpyoung deleted the zpyoung/auto-sync-fork-with-upstream-stablyai-orca-ru-20260914T1400 branch September 14, 2026 15:54
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.