Skip to content

fix: resolve Orca main conflicts in Kiro usage PR #14001 - #1

Open
w-felipe360 wants to merge 1064 commits into
guanbear:codex/kiro-usage-providerfrom
w-felipe360:fix/kiro-pr-14001-main-conflicts
Open

w-felipe360 wants to merge 1064 commits into
guanbear:codex/kiro-usage-providerfrom
w-felipe360:fix/kiro-pr-14001-main-conflicts

Conversation

@w-felipe360

Copy link
Copy Markdown

ELI5

This updates the branch behind stablyai#14001 to Orca main at c61d0bc16f02, resolving its merge conflicts so the original Kiro usage contribution can move forward. The original implementation and authorship are preserved.

What Changed

  • Merged upstream main into the existing Kiro PR branch and resolved five conflicted files.
  • Kept Kiro polling independent while retaining main's direct Antigravity fetching and ZCode account-change protections.
  • Kept ZCode's Accounts settings link and Kiro's CLI-owned credential behavior.
  • Added Kiro mocks to three newly introduced service suites, retained main's removal of an obsolete type-only test, and moved reset-label tests into their own file to respect the line limit.
  • Corrected the touched refresh-orchestration test's Claude supplement expectation for Windows.

Why

The Kiro branch overlaps with newer provider changes on main. Combining both preserves the original feature and the newer account handling. A merge commit preserves the original PR history and records the incorporated main commits.

Please use a merge commit or fast-forward when incorporating this branch, rather than squash, so the main ancestry is retained for stablyai#14001. The large file list is the intervening upstream main history; the conflict resolution itself can be reviewed with git show --remerge-diff bf31aa9293.

Linked Issue

Contributes to stablyai#14001, which references stablyai#9559. This is a contribution to the original author's branch, not a replacement PR against Orca main.

Visual Proof

N/A — this reconciles the existing Kiro feature with upstream main without introducing a new visual design. No Electron app or visible test window was launched.

Testing

Windows, with ORCA_BACKGROUND_LAUNCH=1:

  • Manual Electron UI validation (not performed).
  • Relevant existing automated tests updated.
  • Node 24.21.0 typechecks passed for all three projects using node config/scripts/run-typecheck-projects-in-parallel.mjs; pnpm tc also passed.
  • 70 focused suites passed, 537 tests: Kiro parsing, provider refresh services, status-bar behavior, settings search, and persisted UI hydration.
  • Broader run: 74 suites, 622 tests passed and 17 failed. The four affected suites were excluded from the focused run: tooltip.test.ts and provider-segment-balance.test.tsx expect US currency formatting on this pt-BR host (11 failures); service-account-target-selection.test.ts and service-inactive-account-previews.test.ts expect Claude PTY/supplement support that main explicitly disables on Windows (6 failures). These expectations come from main; both currency test files are byte-identical to main.
  • ORCA_CODE_QUALITY_BASE=upstream/main pnpm run check:code-quality:changed: zero new findings across 41 source files, including type-aware checks, React Doctor, and design-system checks.
  • pnpm exec lint-staged --diff upstream/main: lint, React Doctor, and formatting passed for the 42-file contribution. These pre-commit tasks were run explicitly against main instead of rerunning them over the thousands of incoming upstream files during the merge commit.
  • git diff --check upstream/main...HEAD passed.
  • Full build, macOS/Linux execution, and live SSH validation were not run.

AI Disclosure

Conflict resolution and validation assisted by OpenAI Codex.

Review

Reviewed both sides of each conflict, the resulting diff against main, cancellation handling, independent Kiro completion, and ZCode credential-generation guards. Existing folder-workspace, host execution, and remote compatibility behavior is retained.

Agent skill upstream boundary

  • Not applicable; no agent skills changed.

Checklist

  • Contribution is limited to integrating main with the original Kiro PR.
  • Explained what changed, why, and the merge approach.
  • Visual proof marked N/A with reason.
  • Self-reviewed the conflict resolutions.
  • Cross-platform and SSH implications considered.
  • Full lint, all tests, and build pass; scoped validation and known environment failures are documented above.

AmoabaKelvin and others added 30 commits October 6, 2026 13:14
…yai#23770)

* fix(repos): reuse a saved project only when git shows it's this clone

A saved project outlived its folder, so Clone reported success without running git.

Fixes stablyai#23563

* fix(repos): let a repeat clone of an empty repository reuse its project

A clone of a repository with no commits leaves HEAD unborn, which
`rev-parse --verify HEAD` rejects exactly like the `refs/heads/.invalid`
HEAD a killed clone leaves. The saved-project check read both as "not a
finished clone", so cloning a brand-new empty repo a second time — a
double-clicked Clone, a re-run `orca project setup-clone`, or stablyai#2981's
queued retry — fell through to git and died on "destination path already
exists and is not an empty directory".

Ask the two questions apart when `--verify` fails: `symbolic-ref HEAD`
resolves an unborn branch but not a killed clone's HEAD. It is already on
the relay's read-only allowlist. Probes also short-circuit once the clone
is cancelled, so no probe after Cancel spawns git.

* fix(repos): prepare the worktree root after re-cloning a project's folder

The branch that re-clones into a saved project's folder returned the
project without the two steps every sibling branch runs: the worktree
root that vanished with the deleted folder is never re-created, and the
authorized-roots cache keeps the answers it cached while the folder was
missing. Creating a worktree or reading files in the recovered project
then fails on a root that is not there.

* fix(ssh): register the root after re-cloning a saved project's folder

`addRemoteRepoFromPath` returns an existing project before it notifies
`session.registerRoot`, which was unreachable before the saved-project
check could re-clone into a project's own path. The relay therefore never
learned the folder came back, so sessions under it stayed unregistered.
Notify from the clone, mirroring the folder-to-git upgrade beside it.

* fix(repos): say which reason refused a clone over a saved project

One message covered both "that project holds a different repository" and
"Orca has no record of what that project held", which need different
things from the user. Name the recorded URL when there is one, and say
plainly that there is no record when there isn't — the second is the case
a project predating remote-identity enrichment lands in, and the user
cannot act on it without being told why.

Also drops the `upstream` remote-name check into a named binding: `origin`
is the only stored remote that names a project's own repo, because
`Repo.upstream` and the GitHub avatar derived from it both name the repo a
fork came from.

* fix(repos): bound saved-clone probes and prove empty clone completion

* fix(repos): keep clone validation host-scoped and locale independent

---------

Co-authored-by: Neil <neil@stably.ai>
Co-authored-by: Neil <4138956+nwparker@users.noreply.github.com>
…ablyai#25828)

* perf(ci): share release JavaScript across packaging hosts

* fix(ci): verify the projected web entry in release archives

* fix(ci): use the Windows system archive tool for release bundles

* test(ci): retain stylesheet evidence in release build comparisons

* test(ci): verify release parity across native color rounding

* test(ci): normalize manifest asset references without changing import order

* test(ci): compare portable outputs across Windows text and color formatting

* test(ci): preserve module identity across dependent asset hashes

* fix(ci): keep SVG build inputs identical across release hosts

* fix(ci): stabilize compiler inputs and projected web bindings

* fix(ci): retain vendor minification in projected web output

* test(ci): normalize platform-specific pnpm manifest source paths

* fix(packaging): exclude shared build staging from application files

* test: align thinking-state fixtures with the current source shape

* test(mobile): reuse message fixtures within the line limit
…ablyai#25744)

* refactor(codex): remove trust-grant code the ~/.codex reconcile left unused

* refactor(codex): remove the native trust-grant host only tests reached

Only WSL runs a grant session, so resolveNativeCodexTrustGrantHost, its
timeout, the native host arm and buildNativeCodexBinaryStamp go. The ledger
still parses a native stamp, so files an older build wrote keep loading. The
grant suites now run on a WSL host with the guest probe stubbed.

* docs(codex): name the live caller in the envToDelete comment

* refactor(codex): the trust-grant host key and telemetry lane name the one host left

Only WSL reaches the grant, always on the managed lane; the telemetry event keeps its shape.

* refactor(codex): remove the unwired real-home lane gate

* docs(codex): the trust-grant telemetry comment names the one surface that still grants
…stablyai#25938)

Repair deleted Active Server defaults through validated registry reads and existing settings notifications. Capture editor ownership from backing files so workspace focus cannot redirect file operations.

Correct two live-turn test fixture calls to the current API.

Refs stablyai#21489. Historical session recovery remains separate work.

Co-authored-by: Chen <zwq19980411@gmail.com>
Recover PDF previews after failed refreshes, clear previous pages on changed contents, and preserve scroll and zoom while releasing superseded resources.

Co-authored-by: sotashimozono <shimozono-sota631@g.ecc.u-tokyo.ac.jp>
* Fix Claude interruption status with native terminal evidence

* Fix relay evidence test wiring and current host-turn test inputs

* Fence Claude interruption against continued work and replaced PTYs
* Keep profile saving alive after a stalled main loop

After a long main-loop stall (overnight sleep, dark wakes), the profile
writer's overdue 30s timeout could run before an acknowledgement that was
already queued, permanently retiring the writer until restart. Terminal
creation then failed because pane bindings could not be saved.

- Writer deadlines measure lateness on the monotonic clock and grant a
  bounded fresh window when the callback is overdue or the system reports
  suspended; resume re-arms without spending grace. Applies to
  initialization, every command, and the close/exit wait.
- The "Saving stopped" alert is parented to a visible main window (never a
  parentless synchronous macOS alert), deferred until shown, deduplicated,
  and says whether the latest change is unconfirmed.
- Timeouts, grace, writer faults, and alert presentation leave sanitized
  durable breadcrumbs.

* Fix profile writer timeout and shutdown races

* Run profile writer stall regression on Linux and Windows

* Keep Electron probes out of headless runtime qualification
…n relaunch (stablyai#25711)

* fix(terminal): keep a pane closed just before quit from coming back on relaunch

The daemon now lists a session whose kill it accepted as state 'exiting' (still
live), the inventory carries that through to pty:listSessions, and the
worktree activation gate never adopts an exiting PTY into a new tab.

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

* test(e2e): use the Linux close-pane chord (Ctrl+W) in the closed-pane relaunch spec

---------

Co-authored-by: Claude <noreply@anthropic.com>
…5721)

* fix(native-chat): honor saved agent launch commands and arguments

* fix(test): retain provider import after syncing main

* fix(native-chat): preserve launch arguments and explain invalid settings

* refactor(native-chat): require the current launch arguments source

* fix(native-chat): require saved arguments when installing the runtime

* fix(native-chat): show saved argument refusals beside launch retry

* fix: ship saved-argument error messages in every locale

* test: align model discovery with executable overrides

* chore: drop the local pnpm install lockfile block
… written when the surface exists (stablyai#24934)

Step 1 of the agent-launch unification, on main.

- The dispatcher stamps every request's caller from what its connection proved (runtime socket:
  the local CLI; the desktop's IPC: the desktop; a paired socket: its device). Params never set it.
- The launch record is written twice: once when the tab exists (what creation settled: on the
  launch command, a draft, or a submit still `unconfirmed`), and again once the prompt's fate is
  known. A restart in between finds the running agent instead of answering "unknown".
- A replay re-derives its terminal handle from the pane key in the running host, and shows
  `unconfirmed` only to callers that advertise agent.launch.prompt-unconfirmed.v1.
- The record store opens in its own slot, without building the chat host; the chat host is built
  on that same store.

Rebuilt from this PR's own commits (b9adf88, 0d7d9b5, c83f44d, 39d15d5) onto
main, without stablyai#24080/stablyai#24081. Conflicts: the delivery doc table (main's "line fits" row plus the
`unconfirmed` row), and main's journal-database open in install(), which now goes through the
record-store slot.
)

* fix: reveal send picker targets through host-owned ancestors

* test: type picker close fixture as a nullable target
…e stop row after it (stablyai#25051)

* refactor(native-chat): derive the queue's pause from Stop and Resume journal rows

Stop now appends one journal row where it takes effect, before the interrupt,
whatever the queue holds; Resume appends its own. The pause is a pure function
of the fold: the latest Stop with no later Resume and no later accepted turn a
person asked for. A /clear's carried cards name their source, which is the
replacement's 'cleared' pause. Host-origin turns never lift either.

One predicate decides which cards a pause holds; by default every waiting card
without a hold of its own, including one queued after the Stop. The drain's
consume re-judges it inside its own transaction.

The rows are tombstones of an id no item takes, carrying the mark: a released
host reads an unknown row kind as corruption and truncates the journal there.

Deletes the stored pause (recordPause, the retire hook on every appended row,
the settle-before-record step, mayReturnToWaiting and its row overlay) and the
tests that only proved it retires. The queued_message_pauses table stays in the
schema, unread and unwritten, for downgrade safety.

* fix(native-chat): a card queued after a Stop sends normally, never ahead of held ones

A Stop's pause now holds only the cards queued before its row, plus a steer it
withdrew, which returns to its own place. Each card records the journal
position it was queued at, and the one hold rule compares that with the Stop
row. A card queued after the Stop is a new instruction: it sends as usual, but
the drain still stops at the first held card, so it never overtakes them.
/clear's pause holds the cards it carried. Holding every card again is a
one-line switch in that rule.

* fix(native-chat): the queue's own send re-checks the no-overtake rule in its transaction

The drain's pick and its consume now read one function, nextSendableQueuedCard,
so a Stop row that lands between them holds a newer card behind an older held
one exactly as the pick would. Notes why Stop and Resume ride a tombstone row.

* fix(native-chat): stop creating the unused queue pause table

The queue's pause is derived from journal rows, so nothing reads or writes
queued_message_pauses. It was still created on every open "for downgrade
safety", but an older build creates it itself when it opens the database, so
the table only sat empty in every new database. The tests now pin that no
pause table exists.

* fix(native-chat): a Stop's pause never hides the restart pause

A Stop holds only the cards queued before it. The pause derivation still
returned the Stop alone whenever it was in force, so the restart pause was
never considered: a card queued after the Stop, written by a host process
that has since exited, sent by itself after Orca restarted, with no pause
header and no Resume. A /clear pause that held nothing could hide it the
same way.

Every pause in force is now derived. A card is held if any of them holds
it, and it names the first that does. The drain's pick, the consume
transaction's re-check and the published header all read that one rule;
the header names the pause holding the first card Resume would send.

* test(native-chat): pin the Stop's no-resend, lift and held-card rules

- The Claude and Codex Stop-withdraws-a-steer tests checked "not sent
  again" at one instant, before a queue ignoring the pause re-sends. They
  now wait for the stopped turn to end and re-check after a quiet window.
- The deleted-card test read a card queued after the Stop, which sends
  whether or not a person's turn lifts it; it now reads the Stop's pause
  before and after that turn.
- Unit cases pin that a Stop holds a card with no recorded position and one
  queued before a rewind.

* refactor(native-chat): a Stop writes one Stop event with its reason, turn and caller

The Stop row that paused the queue becomes the general Stop event
{ reason, turnId?, at, caller? }, whose reason is the host's existing stop
cause. It still rides a tombstone of a host-only id (a released host deletes
the journal from the first unknown row kind), and Resume keeps its own marker
on its own id. Only a person's Stop (reason user-stop) pauses the queue.

* test(native-chat): a rewind keeps a lifted /clear pause lifted and restates the same Stop event

* test(native-chat): pin that Stop and Resume rows never reach apps or count as history

* test(native-chat): only a person's Stop event pauses the queue

* test(native-chat): pin that a Stop's event precedes the interrupt and the at-start stop

Through the real host: the event names the turn and who asked and is in the
journal when the interrupt reaches the agent; at an agent still starting it is
there before the start is ended and holds a card queued before it; an idle Stop
writes one only when it withdrew a send; and the queue's claim re-judges a
pause that landed after its pick.

* test(native-chat): a card held at a starting agent is checked before the Stop's timing

Also says precisely what the claim's in-transaction pause check defends
against: the Stop and the drain share one serialized lane.

* test(native-chat): a released build keeps and folds a journal holding Stop events

Replays this build's rows from the released build's own journal database: every
row is kept, the history after the Stop still folds, and an older client is sent
only removed ids no item uses.

* style(native-chat): format the Stop event changes

* test(native-chat): type the released build's exports through one checked helper

* fix(native-chat): the Stop/Resume row guard narrows to those tombstones only

* test(native-chat): run the Stop-event downgrade test in CI, and cover a writable downgrade

The Stop-event downgrade test ran in no CI lane: unit shards exclude the
cross-version folder, and the cross-version lane runs a fixed file list that
did not name it. It is now on that list.

Its only case replayed the rows into a release's own fresh database, because
that release cannot open the current host database. A second case opens the
journal this build wrote with a main build that shares the database: it opens
writable, keeps every row, appends, and this build then reopens it with the
person's Stop still pausing the queue.

* fix(native-chat): a Stop that stops nothing new writes no Stop event

A Stop reaching a running agent wrote a Stop event on every press. Two
presses before the first interrupt landed wrote two events, so a card
queued between them counted as before the latest Stop and was held,
though a card queued after a Stop should send normally. A Stop naming a
turn that had already ended, as a phone sends late, also wrote an event
for a turn it never stopped.

It now writes one only when it withdrew a queued send, or stops something
no event records yet: not a turn the journal no longer runs, and not the
live turn a Stop still in force already names, unless a card was handed
over into it since, which this Stop's interrupt sends back and must hold.
The interrupt and the "already finished" note are unchanged. A Stop at a
starting agent still always writes.

* test(native-chat): pin that a later host, eviction or close Stop never lifts a person's Stop

* chore(native-chat): put each Stop-row doc on its own declaration, and say only user-stop is journaled

* fix(native-chat): any later Stop event ends a person's Stop pause

A person's Stop paused the queue until their next accepted turn or Resume,
and a later Stop of another reason (the host stopping the agent, an
eviction, a close) was ignored. Now the pause is the latest Stop event's:
a later Stop of any reason ends a person's pause, and only a person's Stop
pauses. The fold keeps the latest Stop event whatever its reason.

An eviction of a resting chat writes no Stop event (a Stop that stops
nothing writes nothing), so it cannot release held cards; a test pins that
no event means no lift.

* fix(native-chat): a second Stop press is a repeat even when the first came before the turn showed

A Stop pressed before the agent's turn shows in the journal (before
Claude's echo, or before Codex opens the turn) records no turn. A second
press once the turn showed compared that missing turn with the live one,
wrote a second Stop event, and held a card queued between the presses.

A repeat is now judged by what was sent since the Stop in force: with
nothing sent after it (a refused send aside), a Stop that named no turn,
or named the live one, is repeated and writes nothing. Anything sent since
and not refused, including a send whose fate is unknown, makes the new
press write, since its interrupt may send that card back to waiting.

Tests: the two-press case across the turn showing; a steer between the
presses settled unknown; and a Stop naming a turn that ended while the next
card is sent but shows no turn yet, which writes and holds that card. The
fold test that claimed an eviction path is renamed.

* fix(native-chat): the queue's pause ignores a Stop or Resume row holding a value no build writes

A Stop or Resume row's value is read from disk with no shape check, and
the pause fold stored whatever it found. A stored `stopEvent: null` would
then throw on every pause check for that chat: the queue's pick, its
send, and every queue update to clients. No build writes such a row, so
this is hardening.

The fold now reads a Stop only when it is an object with a string reason
and a finite time, and a Resume only when it is `true`. Anything else is
ignored: it pauses nothing and ends nothing. The row is still not treated
as malformed, which could cut the history short.

* fix(native-chat): a Stop still reads as yours after Orca restarts before the turn ends

Every stop that ends work now writes the Stop's event before it ends the child: a
person's close of the chat, an eviction (worktree teardown, orchestration stop, tab
cleanup) and the idle sweep's stop of a start that never landed. A stop that ends
nothing writes nothing, and quit writes none: its resume marker records why.

The turn-end write reads the latest Stop event where every turn row is built, so the
adapter's settle, the host's fallback and the relaunch's settle all agree: a turn a
person's Stop or close named, ending with no verdict of its own after that Stop, ends
as their cancellation. A relaunch's probe-bounded end is no earlier than a Stop that
found the turn running. When the provider refuses the interrupt and the turn runs on,
a refusal row answers the Stop, so a later crash still reads Failed; pressing Stop
again after a refusal is a new Stop.

* refactor(native-chat): a stop no longer carries its cause; the turn's end reads the Stop event

The cause of a stop was threaded in memory from each entry through the host's stop
step, the adapter router and each adapter's close onto the `ended` it settled with,
and Claude kept a per-turn copy of a Stop it sent. All of that is gone: adapters
settle a turn they cut as interrupted with no verdict, the host's fallback does the
same, and the one rule where a turn row is built (`turnEndAfterStop`) reads the
journal's latest Stop event to say whether it was a person's.

- `closeSession` / `disposeSession` take no cause; `ended` has no `stopCause`.
- Claude reads an error result after a person's Stop as their cancellation from the
  journal's Stop event (through the event sink), not from a per-turn slot, and a
  refused interrupt is the host's refusal row, not `withdrawTurnStop`.
- An owed wind-down keeps no cause: its retry's fallback reads the Stop event.
- The mutation context's Stop passes no cause: its step already wrote the event, and
  the delivery loop's child-end reason is read back from it.
- A Stop pressed before its turn showed applies to the turn that opens under it,
  unless a send a person made since was accepted.

* test(native-chat): a turn a later send opened is no Stop's that named no turn

* test(native-chat): the restart test's death proof carries its detail

* refactor(native-chat): a refused Stop leaves no record; a Stop only ever ends the turn it names

The stop-refused mark is gone: its tombstone kind, its fold, the clock-keyed match that tied it to
a Stop, and the exception that let a second press after a refusal write a new Stop. A Stop that
stops nothing writes nothing. A Codex refusal names a turn that is no longer its active one, and
the Stop names that turn, so the turn running instead never reads as the person's by its id alone.

* fix(native-chat): a Stop pressed before any turn showed stops only the turn opened next

A Stop that named no turn read as the person's cancellation for every later turn that opened
after it, until a send a person made was accepted. The queue's drain, orchestration mail and a
restart continuation send as the host, so a turn they opened long after, cut by a crash, read
"Interrupted" as if the person had stopped it. The Stop now applies only to the first turn
opened after it.

* fix(native-chat): an older Claude's error end after a Stop pressed before its echo reads Interrupted

Claude CLIs before 2.1.91 end an interrupted turn with an error result that names no reason. The
translator judged whether a person's Stop explained it by its own copy of the Stop rule, which
ignored a Stop that named no turn, so a Stop pressed before Claude echoed the send read "Failed".
The translator now writes such an end as interrupted with no verdict and no error row whenever a
person's Stop may name the turn, and the journal's one rule decides as it writes the end.

* fix(native-chat): a person's Stop and /clear each name why they end the agent

The host's mutation path ended the agent with one "recorded" ending for every caller, which read
back the reason of whatever Stop event the journal held last, however old. /clear writes no Stop
event, so its end took an unrelated earlier reason. Each caller now names its own: the chat's Stop
`user-stop`, whose event its own step wrote, and /clear `user-close`, the user replacing this chat.

* fix(native-chat): a host stop judges whether it ends work after the provider's rows land

A close, eviction or host stop decided whether it ended a running turn from the journal as it
stood, while the provider's own rows (the turn its echo opened) could still be in the session's
event sink. A close landing in that gap wrote no Stop event, so the turn it cut read as news. It
now reads after the sink drains, as a person's Stop does, through the same check; a drain that
fails or takes over a second reads working.

* fix(native-chat): a Claude Stop naming a turn that just ended still marks the follow-up it cuts

A phone names the turn it last saw. When that turn had ended and a follow-up was still unechoed,
Claude's Stop interrupted the follow-up and ended the child, but the Stop's event named the ended
turn, so the follow-up's turn the child's end cut read "Failed" under "Cancellation requested.".
A Stop that ends the provider's session ends whatever is in flight, so its event now names the
live turn or none, and a Stop that names none binds the turn opened next. Codex keeps naming only
the turn the Stop names.

The Claude Stop turn-end tests move to their own file, since the session-ending Stop suite is at
its line budget.

* fix(native-chat): the idle sweep reads working by the same rule as a stop's event

The sweep judged a chat resting while a send whose reply was lost was still unanswered, but the
stop's event writer counts that send as work. So the sweep evicted it and wrote an evict event,
which ends a person's Stop pause and let the cards behind it drain on their own. The sweep's owed
work now reads the main agent working the way every session list and the event writer do.

* test(native-chat): an aborted eviction's injected drain failure lands on the eviction's own drain

A host stop now drains the session's sink once to judge whether it ends work, so the tests that
fail the eviction's drain-published step skip that first drain.

* fix(native-chat): the idle sweep's rest writes no Stop event; it evicts a send that never echoes

The previous commit made the sweep count an unanswered send as owed work, which pins a chat whose
admitted send Codex never echoes forever, and the sweep exists to retire exactly that. That rule
returns. The sweep stops only an agent it judged resting, so its eviction now writes no Stop
event, whatever send it retires: a person's Stop pause holds through it.

* fix(native-chat): stopping a start that carries no send writes no Stop event

A host stop, eviction or close of a starting child wrote a Stop event whatever the start carried.
A start with a send already reads working, so the clause only mattered for a start with none,
which ends no turn and no send: its event only lifted a person's Stop pause and bumped the idle
clock, which is why the idle sweep had been changed to close the conversation in the same pass.
The clause goes and the sweep is #24072's again. The child's end still reads host-stop, as before.

* test(native-chat): a Stop's pause across a restart is tested with a restart that writes no event

The rig's restart closes the chat with an eviction, which now writes a Stop event when work runs
and so ends a person's Stop pause. "A Stop never hides a restart's pause" then passed with no Stop
pause left to hide anything. Those tests, and the pause-lift test whose dropped assertion returns,
restart as a process that dies with no close, which like a quit writes no Stop event, and assert
that both the Stop's and the restart's pauses are in force first.

* fix(native-chat): a host stop of a turn a person's Stop is still ending keeps that Stop's reason

An eviction or host stop that landed while a person's Stop or close was already ending the same
turn wrote a newer Stop event, and the turn's end reads only the latest, so the person's Stop of
that turn read as news. A host reason now writes nothing while a person's Stop still decides what
runs: the live turn it names or bound, or, with none, the turn a send opens next. The person's
own close still writes. The E2 tests now open and end the stopped send's own turn, as Codex does,
so the mail turn after it is not the turnless Stop's.

* fix(native-chat): an older Claude's error on a later turn keeps its error text after a Stop

The translator left an error result that names no reason to the journal's Stop rule whenever a
person's Stop named the turn or none, but the rule binds a Stop naming no turn only to the turn
opened next. So a real error on a later turn read "Failed" with its error text dropped. The
translator now asks the journal's rule itself (`personStopDecidesTurn`, the one core
`turnEndAfterStop` and a host stop's in-force check share), so the two cannot disagree.

* fix(native-chat): a Stop of a start that never landed binds no later turn, whatever sent it

A person's Stop pressed while the agent starts names no turn, and the send it stopped is
cancelled before it opens one. The Stop then bound the next turn anything opened (orchestration
mail, a restart continuation, the queue's drain, all of which send as the host), so a host
eviction of that turn wrote nothing and its crash or close read as the person's cancellation. A
Stop that named no turn now binds only a turn no send journaled after it opened: any send since,
of any origin and not refused, opens its own. The E2 test's mail send is accepted as Codex
accepts it, instead of opening the stopped send's own turn first.

* test(native-chat): a rewind's restated turnless Stop binds no turn opened after the rewind

A Codex rewind restates a person's Stop still in force after the turns it keeps, at a new
sequence, so by sequence alone it would bind the next turn opened after the rewind. A send
journaled after the restated row voids that binding (the previous commit), which this pins.

* fix(native-chat): a relaunch settles a person's stopped turn with no "stopped while in progress" row

After a restart, a turn a person's Stop ended reads "Interrupted after N" with the muted mark, but
the relaunch still added the error row saying the provider stopped mid-response, which a live Stop
never writes. The settle now skips that row when every turn it interrupts is the person's Stop's
by the journal's one rule; a crash nobody stopped keeps it.

* test(native-chat): the unexpected-exit settle's journal fake answers whether a person's Stop decides a turn

* fix(native-chat): a host stop whose sink drain fails reads the journal as it stands

A host stop drains the session's sink before judging whether it ends work, and a failed or slow
drain read as working. So an eviction of an agent at rest wrote a Stop event that ended nothing,
which lifts a person's Stop pause, and a close wrote a person's event naming no turn. The drain is
now best effort: the stop goes ahead either way and only its record is at stake, so a failed or
slow drain leaves the journal's read as it stands. A person's Stop keeps its own rule.

* fix(native-chat): a Stop that named no turn applies only to a turn a send it stopped opened

A person's Stop pressed before any turn showed names no turn. It bound the first turn opened
after it, then (5ead1f6bccd) any turn opened by no later send, so a turn the host started for
a card the Stop held, or for orchestration mail, read as the person's cancellation, and a host
eviction of it wrote no Stop event when its send had been abandoned by the close first.

The rule is now the concept itself: a Stop naming no turn applies to a turn opened by a send it
stopped, one already handed to the agent at the Stop's position. Nothing new is stored. The turn's
row names the send that opened it (Codex: the submission's key; Claude: the echo, which the journal
aliases to the submission), and a handed-over send's item sits at its handover, so the target set
is derived from the journal. A card the Stop held is handed over after it, so it is no target; a
Stop of a start whose send never opens a turn binds nothing; a rewind keeps no submissions, so a
restated Stop binds no turn opened after it. With no turn running, a host stop defers to the
person's Stop only while every unanswered send is one it stopped. Claude's translator, which asks
before its echo row lands, passes the send its echo acknowledged.

* fix(native-chat): a host stop whose sink drain runs long reads the agent working; a failed one reads the journal

A drain past its bound may still hold the turn's row, while the echo's acceptance has already
landed, so the journal as it stands read nothing running: a person's close of that turn wrote no
Stop event and the turn read as news. The two drain outcomes now differ: one that failed has
nothing more to deliver, so the journal's read holds (as before); one still running reads working.

* test(native-chat): a host stop with no turn running defers only while every unanswered send is the Stop's

The branch had no test. An eviction with only the stopped send unanswered writes nothing; one
with a send made after the Stop still unanswered writes its event.

* fix(native-chat): a slow sink drain reads working only while an accepted send's turn row is due

The previous commit read every drain past its bound as working, so a host eviction or stop of an
agent at rest during a sink backlog wrote a Stop event that ended nothing and lifted a person's
Stop pause. A slow drain now reads working only when the latest send the agent accepted has opened
no turn the journal holds, the race it was for; otherwise the journal's read holds.

* test(native-chat): the host-stop control keeps the stopped send unanswered beside the later one

With both unanswered, the host writes only because not every unanswered send is the Stop's; a rule
that deferred when any one was would pass the old control.

* fix(native-chat): a steer is no send owed a turn when a slow drain judges a host stop

A slow drain reads working when the latest accepted send has opened no turn yet. A Codex steer or
a Claude fold is accepted into the running turn and never opens one, so a chat at rest whose last
send was a steer still read working, and an eviction lifted a person's Stop pause. Sends delivered
into a running turn, whose item carries that turn's scope, are skipped.

* feat(native-chat): a chat reads Stopping from the person's Stop until the work it stopped ends

The host derives it on each journal publish from the Stop's event, the live turn and the Stop's
own answer, and publishes it as an optional field on the session status and the main agent's row.
Clients present it: the chat's tail line and Stop control, the sidebar row, worktree ps and the
phone's row. The chat and the phone also read their own Stop press until its request answers.

* test(native-chat): pin Stopping on the phone and across mixed versions

* test: give touched fake journals and mocks their SAFETY notes

* test(native-chat): a turn waiting on the person reads attention, never Stopping

* test(native-chat): the sidebar row follows Stopping when it is the only field that moved

* test(mobile): the phone reads Stopping from its own Stop until the request answers

* test(mobile): type the held Stop request instead of casting it

* chore: keep the base lockfile (a local pnpm run rewrote it)

* fix(native-chat): narrow the Stop note's optional failure; type the phone test's reply

* test(native-chat): type the Stop test envelope's fields narrowly

* fix(native-chat): a Stop the agent declined, or whose child end failed, says so while the turn runs on

A Stop naming the turn that still runs, refused by the agent, now writes the
Stop's refused fact instead of 'already finished'. A session-ending Stop whose
child end fails while the work runs on revises its note to unconfirmed.

* fix(native-chat): Stopping holds while any press of the Stop took

A repeat press refused after an earlier press took no longer clears Stopping.
Exit early when the Stop named a turn that is not the live one.

* fix(native-chat): keep Stop enabled while the host says Stopping

Only this client's own Stop request in flight disables Stop and Esc. A repeat
Stop is how a stop the provider took but never answered escalates.

* fix(sidebar): every agent row says Stopping in place of its tool line

The dashboard row, which the sidebar's non-compact mode also draws, read the
last tool line while a person's Stop ended the turn. It now shares the compact
row's rule.

* fix(mobile): the worktree list sees Stopping change on its own

A snapshot whose only change was the host dropping Stopping compared equal and
was thrown away, leaving the row on Stopping.

* refactor(native-chat): fold the status feed in src/shared for both clients

The snapshot merge and the contact-loss strip move out of the renderer feed so
the phone folds the same stream the same way.

* feat(mobile): let phones read the structured session status stream

agentSession.subscribeStatus joins the mobile allowlist. The agent-session
methods move to their own file, which the at-cap allowlist spreads in, and the
allowlist test reads the Set instead of parsing the source.

* feat(mobile): the phone chat reads Stopping from the host, like the desktop

One status stream per client, opened on a host that advertises the status feed;
a refusal to phones reads as no feed. The chat reads Stopping from the host or
its own press, and holds Stop only while its own request is in flight.

* test: give the new fakes checked types or a SAFETY reason

* revert(native-chat): drop the refused-named-turn rewrite of a Stop's note

Codex can send its refusal before the turn's end frames, so reading the turn as
still live after a flush races; the Codex Stop that ends nothing is handled by
ending the process instead. The base's 'already finished' note and its test
expectation return. The Claude wind-down failure revise stays.

* fix(mobile): release the status stream when the host ends it; refusals last one connection

The feed now drops the handle of a stream the host ended or refused, so the
logical client never replays it on a later session. A refusal to phones holds
for one connection, so a host updated while the phone stays paired is asked
again.

* perf(native-chat): read the live turn's opener from its record when deriving Stopping

After a Stop that named no turn, every later commit walked and copied the whole
journal to find the live turn's record. The derivation now reads that record
off the rendered snapshot's tail and decides with the same rule.

* refactor(mobile): move the method-unavailable check into transport

The status feed imported it from the Files tab's fallback. No behaviour change.

* test(native-chat): a send after a Stop reads Working before its turn opens

Pins the derivation's running-only read of the newest turn: the stopped turn,
already ended, must not keep the next send on Stopping.

* fix(sidebar): the compact row leads with Stopping so a narrow sidebar keeps it whole

At the default width the row read 'Codex Chat - Stoppin…': the model and time
keep their room and the line truncates from the end. Stopping now leads the line
the way monitoring already does, so the chat name is what gets cut. Also pins
that the turn bar keeps its running clock while the tail line says Stopping.

* test(native-chat): the retry of a close whose exit was unproven writes no second Stop event

The idle sweep finishes a stop left owed with that stop's own cause. It is the same stop, so its
event stands alone and the child's end keeps the cause, for a person's close and an eviction.

* fix(native-chat): read and write a Stop's answer by the turn its event records

Stop notes are now one row per turn, keyed by the turn the Stop's event
records. Performing a Stop and deriving Stopping share that key. A refused or
unconfirmed answer never overwrites one that took at the same key; only a
session-ending Stop's failed wind-down downgrades it, and that step now revises
the note the Stop actually wrote, carried on the wind-down. Stopping reads the
turn's note whenever it was first written, plus newer notes no other turn owns.
Test fixtures gain the host logger and the phone's quietRepeatedStop.

* refactor(native-chat): read a Stop's target once for its event and its note

The chat's Stop now reads what it is aimed at (the named turn and whether the
Stop ends the provider session) once, and both its event and its note's key
derive their turn from that one value through the same rule. Adds the host test
for a Stop naming an ended turn on a provider whose Stop ends the session.

* fix(native-chat): the host never steers a message into a turn a Stop is ending

A queued card's Send-now, or a send made while a person's Stop ends the turn,
went to the agent as a steer into that turn. The delivery loop now holds any
waiting message while the host's own Stopping reading holds, and sends it as
its own turn once the turn ends. The Stopping reader also stops at the Stop's
position and looks the turn's note up by key, instead of walking the whole
journal.

* feat(native-chat): while Stopping, the composer says a message runs after the stop

Desktop and phone: the composer placeholder reads "Queue a message to run
after the stop" while the chat reads Stopping, and a queued card's Steer (and
the desktop's steer shortcut) is held. New key translated in all 6 catalogs.

* refactor(native-chat): the chat pane's Stop controls live in their own module

The pane went over its line limit once merged with main. Its Stopping reading,
the press that holds Stop, and the steer and placeholder it hands the composer
move to native-chat-structured-stop-controls.ts.

* fix(native-chat): hold a send at its handover, reading the feed's own Stopping

The hold was checked when the delivery step started, but the handover runs in a
later step after waiting on the agent's start, so a Stop landing in between let
a new send steer into the stopping turn. The check now runs at the handover.
It reads the status feed's projection for the commit instead of rendering the
journal again, so holding a send adds no journal read of its own.

* refactor(native-chat): one display status decides Stopping on every surface

agentStopDisplayStatus combines whether the agent works, the host's flag and
this client's own press. The chat pane, sidebar and dashboard rows, and the
phone's chat all read it, instead of each combining the flags.

* fix(native-chat): Stopping holds until the stopped turn ends, whatever the Stop's answer

A Stop the agent declined, or whose end went unconfirmed, used to drop the chat
back to Working. It now stays on Stopping until the turn ends, and Stop stays
enabled so a repeat press escalates. The Stop's answer is no longer read for
Stopping, so its note goes back to the key the base gives it (the restore of
queued-stop.ts and the removed key test landed in the previous commit). The
note still keeps a press that took over a later refusal, and a failed process
end still says the Stop went unconfirmed.

* fix(native-chat): a Stop binds only the turn it actually stopped

A Stop pressed before any turn showed used to claim, at end-write time,
whatever turn the stopped send later opened, even when the Stop stopped
nothing. A turn that then died on its own read as "Interrupted" (your
cancellation) instead of "Failed".

Now a person's Stop that named no turn binds, in memory only, every turn
that ends while the Stop settles, and afterwards only the turn its
interrupt took. The settle ends a still-running stopped turn once. A
relaunch finds nothing in memory, so an unsettled turnless Stop binds no
turn. A Codex Stop whose answered turn does not open within its wait, or
whose send's answer was lost, now answers refused, so the host ends the
child and the turn can never run.

* fix(native-chat): keep the person's queue pause and close binding after a Stop settles

A host stop or eviction with no turn running now defers to a person's
Stop while its queue pause still holds with nothing sent since, read from
rows, so a held card is not handed off on reopen after a Stop that did
nothing or whose kill failed.

A person's close that named no turn opens a settle around its child's
end, so a turn that end cuts reads as theirs.

A press opens its settle only when the latest Stop event is its own or
the one in force it repeats: a late Stop, a card's interrupt or a lost
event row reopens no earlier Stop.

A Codex Stop that cannot reach a turn still able to open says the Stop is
unconfirmed rather than that no turn ran, and a second Stop still reaches
a turn an earlier wait left unopened.

Also drops the unused openedBy plumbing and the unreachable "a written
cancellation stays one" rule, and pins a relaunch after a named Stop.

* fix(native-chat): keep a failed Stop's turn display-only, and settle edges off the commit path

A Stop that failed marks the turn it could not stop for "Stopping…" only
(JournalStopSettle.failedOn): no turn-end rule reads it, so that turn's
own end with no verdict reads as a failure, not the person's.

A settle edge writes no row, so it no longer goes through the journal's
commit listener, which also delivers history, counts as activity for the
idle sweep and schedules the queue drain. A narrow settle-edge hook
republishes the status row and wakes the steer hold's handover, and
nothing else.

* fix(native-chat): a Codex Stop agrees on both presses when a turn is still owed, and pin the close's settle

A Codex Stop that waited for a turn Codex answered a send into now answers
"may still open" whenever that turn neither opened nor ended and its send
is still owed, however the wait ended (it ran out, or the thread went
idle). Before, a first press after an idle thread said no turn was
running and kept Codex, while an identical second press ended it.

Adds a test that a person's close the conversation outlives (as /clear
does) closes its settle, so a later turn that ends on its own reads as a
failure.

* fix(native-chat): a Stop that failed before its turn showed still reads Stopping through that turn

A Stop that failed with no turn open marked nothing, so the chat dropped
to Working and the turn that then opened never read "Stopping…". The
display-only mark now also covers that case: the first turn that opens
after the Stop failed, provided no message was handed to the agent in
between. No turn-end rule reads the mark, so that turn's own end with no
verdict still reads as a failure.

* test(native-chat): a Stop whose event row failed binds no turn to an earlier Stop

With one ordered journal writer the Stop's event is in the fold when its
write returns, so the press reads whether it owns the latest Stop from the
fold instead of awaiting the write. Pins the case the read must refuse.

* test(native-chat): name the settle, not a stream drain, in the Stop's own-end test

* test(native-chat): a Codex Stop answered before Codex ends the turn reads interrupted throughout

Codex answers an interrupt it took before it sends turn/completed (interrupted):
on TurnAborted the app-server answers pending interrupts, then ends the turn, on
one channel. The test fake did the reverse. It now answers first and ends the
turn on a later read, and the tests that read the turn's end right after a Stop
wait for it.

New end-to-end test through the shipped host, journal and Codex adapter: with
the real order, every end row of the stopped turn reads interrupted by the Stop
(named, unnamed, and a Stop pressed while turn/start was in flight). Breaking the
settle window turns the in-flight case red: the Stop's own end row then has no
verdict, which reads as failed until Codex's end lands.

* test(native-chat): Stopping ends with a Codex turn whose interrupt is answered before its end

With Codex's real order (the interrupt's answer, then turn/completed interrupted),
the status shows Stopping while the Stop settles, drops it once the turn ends, and
never carries a verdict other than the person's cancellation.

* fix(native-chat): a Codex Stop interrupts a turn Codex answered but has not opened at once

A Stop that named no turn, made after Codex answered a send but before the turn
opened, used to wait up to 5 s for the turn to open before interrupting, and
ended the Codex process when it didn't. The stated reason, that Codex refuses an
interrupt until it opens the turn, holds only part of the time: with no turn
active, Codex takes an interrupt once its thread runs (turn_interrupt_inner),
and refuses it with -32600 "no active turn to interrupt" before that or once
the turn has ended.

The Stop now sends the interrupt at once. Only on that refusal, while the turn
has neither opened nor ended, does it wait for the turn to open (bounded at
5 s) and send it once more. A turn that ended meanwhile was nothing to stop. One
that never opens, or that an earlier wait already gave up on, fails the Stop,
and the host ends the child as before. Sends still wait for the turn to open
before steering into it.

The test fake models Codex taking an interrupt once the thread runs (run()).

* fix(mobile): name how the phone's status stream is released in the subscription inventory

Main made each inventory entry state its release; the status feed's stream is
released from its subscribe params, as the session event stream is.

* fix(native-chat): every Codex Stop waits for an answered turn to start, as the first did

A Stop whose interrupt Codex refused as finding no active turn skipped the wait
when an earlier wait, a Stop's or a send's, had already given up on that turn.
Every press now waits its own bound and retries once if the turn starts, so a
turn that opens during a later press is still stopped. Both presses still reach
the same verdict when it never starts.

* fix(codex): never steer a turn whose interrupt Codex answered

Codex answers an interrupt as the turn aborts, before it sends that turn's
turn/completed. In that gap the adapter still counted the turn as running, so a
message handed over right after a Stop settled (the Stop's own end row already
reads the turn ended) went out as turn/steer, which Codex refused with -32600
"no active turn to steer", and only then as turn/start. The adapter now marks a
turn whose interrupt Codex answered as aborted until its turn/completed, never
steers into it, and starts the message's own turn directly. Steering a turn
that is genuinely running is unchanged.

* fix(native-chat): a message sent while Stopping is queued as a card, whatever the setting

While the chat reads Stopping (the host's flag or this client's own Stop in
flight) there is no turn left to steer into, so the desktop asks the host to
queue the send even with the queueing setting off, and it is never drawn as a
bubble inside the turn being stopped. The phone already queued every send on a
capable host; a test now pins that it does so while Stopping.

* fix(native-chat): a message queued while Stopping is a card at once, not after the stop

A person's Stop holds the session's lane until Codex answers its interrupt, and
a send was admitted only behind it. By then the turn read ended, so a send that
asked to be queued went out plain: no card for the whole of Stopping, then a
bubble and a new turn.

While the host reads that a person's Stop is ending the work, a text send that
asks to be queued is admitted without waiting for the lane: the same ledger and
lease admission, and a plan that only writes the card through the journal's
ordered writer. The card runs when the stop lands; the Stop's pause holds only
cards queued before it. Anything else, including a Stop that settled by the
time the send runs, takes the lane as before.

The Codex test fake now drops the active turn when it takes an interrupt, as
Codex does before it answers, so a turn/start after the answer opens a new turn.

* fix(native-chat): a Codex Stop that ends the child before any turn opened withdraws its send

A Stop on a Codex turn that was answered but never opened ends the Codex
process. That end settled the send as in doubt (unknown, recovered), and the
client's outbox holds every later send behind a send in doubt until the person
presses Retry, which re-sends the very message they stopped. The chat looked
stuck.

Codex records a prompt only once its turn has started, so a send whose turn
never opened never ran. When the Stop's refusal says so (turnMayOpen), the child
end now settles the unanswered sends as withdrawn, the verdict Codex's own
interrupted-turn end already gives an unechoed send. The flag rides on the owed
wind-down, so a retry after a failed child end withdraws them too. Claude's
child end still leaves its unanswered send in doubt.

* fix(native-chat): derive the withdrawal of a Codex send whose turn never opened

Replaces the flag the Stop carried to the child's end, and its copy on the owed
wind-down, with a reading of the journal at the settlement that lands. A Codex
child's unanswered send is withdrawn when a person's Stop is in force since it
was sent and no turn row ran, or was written, after it; any other end (a turn
that opened, a host's close, a crash, Claude) still leaves it in doubt. A
retried wind-down reads the same rows, so it withdraws the same sends.

* fix(native-chat): a card queued while Stopping runs past the cards the Stop holds

A card queued before a person's Stop waits under its pause until Resume. One
queued after it, as a message sent while Stopping now is, was stuck behind them
too, since the queue never reorders. Such a card was asked for after the Stop,
so it runs when the stop lands, past the cards held only by that Stop's pause;
a returned card and the restart and /clear pauses still hold everything behind
them.

Also: the host's own Stopping reading is gated on working, as the published
flag is, so a failed Stop's mark never reads Stopping on an idle session; a send
that falls back to the lane re-reads the conversation's journal there; and the
end-to-end test asserts the queue's pause rather than a per-card field.

* fix(native-chat): a paused queue labels only the cards it holds

Since a card queued after a person's Stop runs past the cards the Stop holds,
labelling every card "paused" while the queue's pause is published misreads that
card. The host now marks each card its pause holds (heldByPause, a new optional
field), and the desktop and phone label only those. An older host marks none,
so a client keeps today's labels; an older client ignores the field.

Adds a test of the desktop's own send through the real outbox: while the chat
reads Stopping, the request asks the host to queue it and no bubble is drawn,
on a host that advertises the queue.

* revert(native-chat): defer the per-card queue pause label to the queue's rollout

The heldByPause field and its labels are visible only where the host
advertises the queued-messages capability, which shipped hosts do not yet do.
Deferred to that rollout; the real-outbox send test stays.

* fix(native-chat): while Stopping, say and show what a send does where the queue is dark

Shipped hosts do not advertise the queued-messages capability, so a message
sent while Stopping goes out plain: the host holds it until the stopped turn
ends and then runs it as its own turn. The composer still said "Queue a message
to run after the stop", and the message was drawn inside the turn being
stopped.

Now the placeholder reads "Send a message to run after the stop" where the host
does not queue sends, and "Queue a message…" only where it does (desktop and
phone, all six catalogs). A send this client made that the host has not
recorded yet is drawn after the Stopping line while the chat reads Stopping, as
a message held behind a running command already is; once the host hands it
over it opens its own turn. Client presentation only.

* fix(native-chat): keep a send in doubt when a turn was open for it

The derived withdrawal read a turn as open for a send only if it still ran or
was written after the send. A send steered into a running Codex turn whose
interrupt failed met neither once the adapter's end settled that turn ahead of
the host's settle, so it read withdrawn, though Codex drains a steer into the
running turn and may hold it. A turn that ended after the send was handed over
was open for it too: such a send stays in doubt, as before.

Pins that case, and that a send made after the Stop, to a child that then dies
before its turn opens, stays in doubt.

* fix(native-chat): restore the per-card queue pause label

Kept after all: a paused queue labels only the cards it holds (heldByPause),
which is visible only where the host advertises the queued-messages capability.

* fix(native-chat): draw only a send made while Stopping after the Stopping line

Every send the host had not recorded yet was drawn after the Stopping line,
including one made just before the Stop, which the host steers into the turn;
it then jumped up into that turn once recorded. The outbox now marks a send
made while the chat reads Stopping, and only those wait after the line.

* fix(native-chat): withdraw a Codex send by whether it started its own turn, not by timing

Whether a turn was open for a send was read from end times: a turn that ended
after the send's handover counted. A send made while a Stop ended the turn is
handed over once that turn reads ended, yet Codex's own end for it can arrive
later, so such a send whose own turn never opened read in doubt again, and the
chat's queue held behind it.

The handover already records where the send went: its message joins the turn
running then (a steer) or belongs to no turn (it starts its own). Only a send
that started its own turn, with none opened since, is withdrawn; one that
joined a running turn, or has no recorded place, stays in doubt.

The Codex test fake takes an answered interrupt as Codex does, dropping the
turn before its end arrives.

* refactor(native-chat): move queue-while-stopping to its own follow-up

The queued-messages capability is off on every shipped host (#21062), so the
parts of this PR that act only when it is on move to a follow-up stacked on
this one: admitting a queued card while a Stop holds the session's lane, a card
queued after a Stop running past the cards it holds, the per-card pause mark,
and asking the host to queue a send made while Stopping. This PR keeps the
Stopping state, the host's steer hold, the rule that never steers a turn whose
interrupt Codex answered, and what a send while Stopping looks like where the
queue is off.

* fix(native-chat): leave no Stop row when the Stop took back a send that never ran

A Stop on a Codex send whose turn never opened ends the child, and the child's end
takes the send back into the composer. The Stop still wrote "Cancellation
requested." at the conversation level, so with the send gone it sat under the
previous finished turn and read as if that turn had been stopped. A Stop that found
no turn running and whose child end took back every send it found now writes no
row; a Stop of a running turn, or one that leaves a send in doubt, still does.

* feat(native-chat): keep a message a Stop took back on screen, with one stop row after it

A Stop that took back a message before the agent started it used to remove the
message from the transcript and paste its text back into the sender's composer.
Other devices just lost it, and with the message gone there was nowhere for a
stop row to sit, so it either landed under the previous finished turn or was
left out.

The message now stays where it was sent, on every client, followed by one row,
"Stopped before the agent started" (one row after several taken back together).
A message whose turn had opened stays in that turn, whose "Interrupted" header
already says it was stopped. A queued card's hand-off is still held by the card.
Nothing the host recorded refills the composer any more. Text this client's own
Stop took before the host ever had it still comes back, and only into an empty
composer.

Derived from facts the journal already holds (the rejected submission's
cancelled verdict and its user item); nothing new is stored or sent.

* fix(native-chat): one stop row per stopped send, drawn where the Stop took it, and nothing lost

- A message this window's Stop took before the host had it, which the composer
  cannot take (it holds text or images, or no composer shows the chat), now stays
  in the outbox as not sent, on its Retry, instead of vanishing.
- A Codex send whose turn opened but was never echoed belongs to that turn: its
  interrupted end is the stop, so no second row is drawn. A send taken back after
  a later turn started still gets its own row.
- A steer the Stop took back stays in the turn it joined, with no row of its own.
- A queued send taken back before its hand-over is drawn after the turn it waited
  behind, not inside it.
- The conversation outline the host serves still leaves out sends a Stop took
  back, and the rail gives them no tick, so what the host publishes is unchanged.
- The row is drawn as the Stop's own status rows are, and never folds behind a
  settled turn on a host that states no turn scopes.

* fix(native-chat): draw a stopped follow-up after the turn that opened while it waited

A follow-up sent just before the agent opened its turn, then stopped, drew its
"Stopped before the agent started" row above that turn's own "Cancellation
requested." whenever the turn was expanded. A send a Stop took back is now drawn
after the rest of every turn it waited on: the one it was accepted behind, and
any that opened after it and before the Stop took it back.

Also corrects the stoppedBeforeStart doc to what sets it.

* fix(native-chat): place a stopped send by journal order, not by turn start time

The turn a stopped send waited on was picked by comparing the turn's start time
with when the Stop took the send back. A Codex resume rewrites every finished
turn's start time to Codex's whole seconds, so after a restart (or a child
respawn) a turn opened for a later send could read as started first, and the
stopped send dropped below that whole later exchange. A turn is now waited on
when something sent before the stopped send opened it, read from the journal's
own order only. Covered end to end through a restart whose resume reports
whole-second start times.

* test(native-chat): pin a stopped send held behind the first send's answer

A second send made while Codex still held the first send's turn/start answer,
then a Stop: the first turn opens after the second was accepted and streams after
the Stop. The stopped second send and its row are drawn after that turn's output,
never between the turn's header and its output. Fails on the two earlier
placement rules. The restart case moves into the same file.

* docs(native-chat): say why the pre-echo anchor still compares times

* docs(native-chat): mark the pre-echo anchor's time compare temporary (STA-9337)

* docs(native-chat): say the placement rule reads journal position, not send time

* fix(native-chat): keep sends a Stop took back in the order they were sent

Two sends queued during /compact, the first handed over and the second not,
then a Stop that took both back: the handed-over one sat at its handover row and
the other at its acceptance, so the second drew above the first. Among sends a
Stop took back, a later-sent one is now drawn no earlier than just after an
earlier one. Sent order is submittedAt, the host's accept time and the order
every client already keeps submissions in, because a client never sees a
handed-over send's acceptance position (STA-9337).

* fix(native-chat): draw a stopped send below everything sent before it

A send a Stop took back that was never handed over could still draw above an
earlier send's whole exchange when that earlier send was not itself taken back:
three queued during /compact, the first ran and finished, the second was left in
doubt, and the Stop took back only the third. A stopped send is now drawn no
earlier than the latest loaded row of anything sent before it, and the turns it
waited on are read from that point, so the earlier exchange stays above it.

* fix(native-chat): count a turn opened by the floor row as one the stopped send waited on

When the latest row sent before a stopped send was itself a turn's opener, that
turn compared equal to the floor and was not counted, so the stopped send landed
inside the earlier send's exchange: two queued during /compact, the first handed
over and streaming, then a Stop took back the second. A turn whose opener sits at
or before that point now counts, and the send draws after the whole exchange.

* feat(native-chat): publish each submission's journal positions

A client never learned where in the journal a send was accepted or taken back,
so drawing a stopped send had to guess from its own row and from clocks, and the
guesses misfired after restarts. Each projected submission now carries
`submittedSequence` (its submission row) and `resolvedSequence` (the dispatch row
that resolved it; absent while pending): two optional fields, computed on every
fold from rows already stored, never stored themselves. Older clients ignore
them. The failure-fact schema moves to its own module to keep the journal schema
file within its size limit.

* fix(native-chat): place a stopped send by the journal row that took it back

Hosts now publish where each submission and its resolving row sit in the journal (cherry-picked from #25073). The projection places a send withdrawn before it started at that row, keeps stopped sends in their published submission order, and falls back to the send's own row on hosts that publish neither.

* test(native-chat): pin a submission's resolved position on the provider-echo accept path

Also say which rows can resolve a submission, and that the submission schema, not the shared type, omits acceptedSequence.

(cherry picked from commit 58a1ade5048669b1aadbac65799a6913152a9981)

* fix(native-chat): keep the previous placement rules for a host that publishes no positions

On a host from before the published journal positions, a stopped send is again drawn after the latest row of anything sent before it, as before the switch, so it no longer draws above the earlier exchange. Temporary until a host version floor. Also pins the published row against tied accept times.

* perf(native-chat): place stopped sends in one pass, and skip it in chats with none

The message projection runs on every streaming commit. It now builds turn anchors, the item lookup and the older-host floor only when the chat has a send a Stop took back, and places each such send with a binary search over the turns' furthest rows instead of rescanning every item per send. Draws are unchanged.

* fix(native-chat): count a send whose answer was lost when a Stop takes it back

The no-row rule counted only pending sends, but the child's end also takes back a send this process left in doubt when Codex's turn/start answer was lost. That case still wrote "Cancellation requested." under the previous turn. Both now read one predicate, so they cannot drift apart.

* test(native-chat): pin which sends a Stop's child end can take back

A send an earlier process left in doubt is never withdrawn and never holds the row back, and a queued card's send is never counted.

* fix(native-chat): read the host's Stopping beside main's startup phase

Main now reads only the startup phase from the status feed and no longer publishes which
child is starting. The chat reads the host's Stopping from its own hook beside it, and the
Stopping bridge test mocks the execution-host lookup main's owner resolution now calls.

* test(native-chat): phone draws the same from merged frames as from a fresh snapshot

A long chat whose last send never opened and was taken back at the child's
exit, right after a send made while a Stop was ending the turn before it.
From every subscribe point, at every later frame and with frames that carry
one to three rows, the phone's list built from merged frames must equal the
list built from a fresh snapshot of the same journal.

* test(native-chat): widen the merged-vs-snapshot guard to content, long chats and the phone hooks

The guard compared row ids and turn bars only, never left the phone's first
window, put each row's writer fence on every batch, and stopped at the fold.
It now:
- compares each row's content, so a phone that kept an earlier revision of an
  answer or a status fails it;
- adds a chat longer than the phone first loads, compared from the fresh
  page's first user row on (a turn whose user row the page cut off is keyed
  differently there);
- sends batches at the subscriber's fence and a fresh snapshot when the fence
  moves, as the host's attach does;
- writes the send made while stopping while the turn still runs, adds the
  provider's echo of each send, and a status row the provider drops;
- adds a hooks test: a phone that sent the never-opened message itself, holding
  its echo and reading the host's Stopping from the status feed, shows the same
  list after every frame as a phone that opened the chat fresh then.

* test(native-chat): pin the stop row to every frame from the one that took the send back

Both guards compared a merged phone with a fresh one, so a list bug they
shared passed, including one that held the stop row back until the next send.
Both now check, at every frame from the take-back on, that the list itself
shows the stop row: the data test through the list's own turn disclosure, the
hooks test on the live and the fresh phone.

Also:
- the send made while stopping is written after the stopped turn's end, as
  the QA journal shows, and while the turn still runs, as a second case;
- the long chat revises and drops a status near its end, inside the window;
- each phone reduces its own copy of every frame;
- Stopping is checked on the never-opened send's own Stop;
- the headers say what neither guard covers;
- the mobile production typecheck leaves *.test-fixture.ts to the tests'
  program, as it does tests, since these fixtures import desktop host code.

* test(native-chat): check the stop row in the chat view's FlatList, through the real overlay

The hooks guard rebuilt the overlay and chat view by hand, so a memo in the
real view that held its list back until the next send passed it. The guard
now renders the real overlay and chat view from the phone's own hooks, and
checks the stop row in the FlatList's data at every frame from the one that
took the send back. It also subscribes once before the history's last
dropped status, so removals run through the real hooks too.

* test(native-chat): compare the live and fresh phone on the chat view's own FlatList

The hooks guard compared a hand-built copy of the overlay and view, so a memo
in the real ones keyed on list lengths kept stale rows past it. It now
compares, after every frame, what the real chat view hands its FlatList:
each row as its renderItem draws it, with its bar, and the footer. The
hand-built copy is gone. It also checks that the FlatList's keys never
collide and that a changed list never arrives in the same array, and the
controller stub's SAFETY note now states what the stub really provides.

* test(native-chat): read the outbox reconcile from where main moved it

* test(native-chat): pass the projection's options and mock the phone row's new imports after main's rejected-in-place rows

* refactor(native-chat): place a taken-back send by where the host now puts its row

#24710 moves a rejected send's row, a Stop's take-back included, to the row
that rejected it, in no turn. Draw it at that row, past the end of every turn
opened before it, and keep the send-order rule. A send whose turn opened before
the provider echoed it is claimed by that turn's record when it was sent before
the record and taken back after it, by submittedSequence, and is drawn as the
turn's opener. The steer branch goes: the host no longer says which turn a
taken-back steer joined, so it is drawn after that turn with its own row.
The older-host fallback (floor, clock compare, submittedAt order) is now gated
on submittedSequence alone and marked Temporary. resolvedSequence is gone, as
in #25073.

* test(native-chat): a turn recorded before a send was made is not the turn it opened

* test(native-chat): a message a Stop withdrew is drawn with its stop row, not hidden

#24710's tests pinned a withdrawn send as hidden, the rule this PR replaces.
Each keeps what it is about: a withdrawn copy does not hide a failed one, no
delivery notice is raised, and a message the host failed to deliver stays
hidden on the phone.

* fix(native-chat): a retried message no longer waits behind a later Stop

Retry dropped the Stop it had outlived but kept the mark that it was sent while
a Stop was ending a turn, so a retried message waited behind whatever later,
unrelated turn a Stop was ending. Retry is a new send: drop that mark too.

* feat(native-chat): publish the turn a withdrawn Codex send was answered into

Same change as #25073 at 93749070e48b, so this stack builds on it.

* feat(native-chat): say how a withdrawn Codex send joined the turn it names

Same change as #25073 at ab2826ee1854, so this stack builds on it.

* fix(native-chat): open a turn for a stopped send only when the host says it started it

A send a Stop took back after its turn opened but before Codex echoed it
is drawn as that turn's opener when the host names the turn and says the
send started it. A steer into that turn keeps its own row after the turn.
The guess from journal order and times is kept only for hosts that
publish neither field, and never hands a turn the provider resumed on its
own to a send. A page that starts inside such a turn draws the send
before the turn's first loaded row.

* test(native-chat): an echoed opener keeps its turn from a send the host says started it

* feat(native-chat): state on every rejection whether it names a turn

Same change as #25073 at e02f98add56b, so this stack builds on it.

* fix(native-chat): place a stopped send from older history by journal order

A rejection written before the host named turns carries no answered turn,
though a newer host still publishes where it was sent and has moved its
row to the take-back. Such a send is matched to its turn by journal order
again, as before the host named turns; a rejection that states no turn
opens none.

* docs(native-chat): say an unreadable answered turn reads as none

Same change as #25073 at 4ab6cc431c01.

* refactor(native-chat): order stopped sends without non-null assertions

One helper sorts by the published send position when every entry has it,
else by accept time, and both stopped-send sorts use it. The rule kept for
rejections written before the host named turns is labelled as kept for
good: it draws those rows exactly as before.

* docs(native-chat): say which half of the older-history turn rule is kept for good

* fix(native-chat): word a send after a Stop by whether this send will queue

The 'queue a message to run after the stop' placeholder read the host's
queue capability alone. A send queues only when the host queues and this
send asks it to: the queue setting is on and no pending prompt blocks the
queue. Desktop and phone now word the placeholder from that same decision
their send uses.

* test(native-chat): one test per case for the words of a send after a Stop

* refactor(native-chat): the dictation hook owns the composer's dictation state

Keeps…
…i#25724)

* feat: generate chat names through configured text agents

* Project structured chat names across stored tabs and session rows

* Name native chats from their first live message

* Restore the journal test provider handle import

* Fix first-message naming and live Vault title updates

* Preserve Unicode characters in bounded chat naming prompts

* Read chat naming settings only when a turn needs them

* fix(chat): store only generated conversation names

* fix(chat): keep ordinary labels across unnamed chat surfaces

* fix(chat): preserve naming after fast first turns

* Preserve native chat names across command-first sends and Vault lifecycle

* Route native chat SQLite contracts through the existing Node test pool

* feat(settings): add chat naming controls to Chat page

* Preserve chat naming drafts and configure Custom commands through host settings

* Scope synthetic command output to its journal thread in naming test

* Keep naming test fixtures within their typed project boundaries

* Resolve the real Vault hook directly in its integration test

* Publish chat naming save refs after render commits

* fix: keep Japanese chat naming copy stable during repair

* Update test contracts for the naming integration

* Keep journal fixture reads on the host clock

* Give Chat names a separate settings section
…it (stablyai#25888)

* feat(native-chat): show which agent a chat message is from

A message another agent sends into a native chat (today the orchestration
mail notice) looked exactly like the person's own: a right-hand bubble and a
queued card identical to theirs. The sender was recorded only on the queue
row, so a message sent to an idle chat, or a card once it was sent, had no
sender at all.

The sender now lives on the message itself (`from` on the message body):
- The send path carries it on both deliveries; an idle chat records it too,
  and it survives the queue, /clear's carry, restart and history.
- No fingerprint covers it: one shared payload projection leaves it out at
  every fingerprint site, so stored digests are unchanged and the provider's
  echo still folds into the same bubble. The provider never receives it.
- A client cannot set it: its send params stay strict.
- One reader normalizes it where it is read (journal load, queued row,
  client projection); a newer message kind keeps the sender an agent's.
- Each sender keeps a name snapshot from names Orca controls: a chat's
  conversation name or agent label, a terminal's own tab title or agent,
  a federated sender's task name. Never an agent-set terminal title.
- The separate queued-row sender column and its plumbing are removed; it
  never shipped.

Desktop shows such a message as a left-aligned "Message from <name>" row;
the name opens the sender: its chat through the existing open-chat flow
(after a host lookup that follows /clear and dispatch addresses), or its
terminal through the terminal-link path, with the existing feedback when
it is gone. A queued card shows a plain "From <name>" line. Mobile shows
the same attribution, not yet clickable.

* fix(native-chat): name and open an agent's message by what Orca shows for it

Review fixes for showing who an agent message is from.

- Names: a sender is named by its dispatch's task (local workers too, not
  only federated ones), else a chat by the label its tab shows, else a
  terminal by its tab's stored title, else its agent. The conversation
  name the snapshot read before has no writer, so every chat read
  "<Agent> Chat".
- One sender builder (agentMessageSender) and a public runtime naming
  object (orchestrationSenderNames) any send site can use; the terminal
  naming moves out of the unrelated runtime layer into its own module.
- Opening a terminal sender always asks the chat's host: a terminal handle
  is issued per run, so the host finds an earlier run's terminal again
  through the pane its mail was sent from. The pane key never leaves the
  host.
- Feedback follows what the host found: a gone chat or a gone pane by the
  kind it lost, "pane no longer available" only when the terminal answer
  proves it, and the existing "couldn't reach" / "needs a newer Orca"
  words otherwise.
- Another agent's message is not offered rewind (it would put the
  agent's text in the person's composer), and a Codex rewind keeps the
  sender on the messages it retains.
- The queued card and mobile name every sender, two that share a name
  included, and count in "+N" only those left out; the transcript row
  separates names with ", ".
- The host no longer normalizes the sender when loading the journal or
  the queue: nothing on the host reads it, and clients read it through
  the one reader at their projections.

* fix(native-chat): name a worker's final report by its task; clearer open-sender failures

- A worker's own worker_done settles its dispatch before the mail lane
  names who the report is from, so a terminal worker's completion read
  "Codex". A local worker is now named by the dispatch it holds, else the
  one it last held (handles are per run, so that is this run's). The
  coordinator mail rig now attests the worker's process, so its report is
  accepted and settles as it does in production.
- Opening a sender: a version block or an unknown answer says "Update Orca
  to open it.", since either side may be the older one; "This needs a
  newer Orca on the computer running this chat" stays only for a host
  that predates the lookup. "Orca couldn't reach the agent." is followed
  by "Try again.", each sentence translated on its own. No new copy.

* fix(native-chat): name a worker only by work it holds or just reported

A worker with no active dispatch was named by the newest dispatch ever
assigned to its terminal, at any status and any age: one that failed before
it ran, or a task finished long ago that its later, unrelated mail kept
showing. Now a worker is named by its active dispatch, else by the dispatch
its own worker_done in the announced mail names (only if that dispatch is
its own), else by its tab or agent as any terminal is.

* fix(native-chat): restore a rewound message's sender through its typed body; mobile card fixtures name their attribution

- The rewind's held-sender restore lives with the retained rows and reads both
  bodies through the journal's message-body check, instead of a shared helper
  taking any object (anti-slop: no-object-parameters).
- Mobile queued-card test fixtures carry the attribution field the card gained.

* fix(native-chat): never call a sender gone without proof; the sender builder moves out of the mail lane

- A federated sender (dispatch:<id>) runs on another host, which the chat's
  host cannot open: its name is plain text, and the host's lookup answers
  'not found' rather than 'gone' for a dispatch it does not run. Not seeing a
  worker is no evidence it stopped.
- The lookup calls a chat gone only when the identity is proven lost; any
  other failure reaches the client as 'could not reach'.
- A sender open that throws shows 'couldn't reach the agent' instead of an
  unhandled rejection.
- agentMessageSender, the one sender builder, lives in its own module, so the
  runtime's naming no longer imports from the mail lane.
* test(e2e): add terminal layout parity check for topology refactor PRs

Runs fixed terminal-layout journeys in the real app on two builds, captures
the renderer topology and the saved workspace session, normalizes volatile
values and fails on any difference not declared for a named bug.

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

* test(e2e): record quit exit status and report paths main does not reproduce

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

* test(e2e): invoke pnpm correctly under corepack and silence the typeless-module warning

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

* test(e2e): accept pnpm's forwarded -- in the layout parity runner

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

* test(e2e): close parity panes through the user's chord and treat absent maps as empty

Driving PaneManager.closePane directly left main to learn of the close from the
PTY exit, which raced quit; the keyboard path commits the close in main first.

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

* test(e2e): build parity checkouts before running, forward -g, and add a drag-out scenario

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

---------

Co-authored-by: Claude <noreply@anthropic.com>
* Run confirmed nested worktree deletion in the background

* Reuse the translated workspace deletion failure title

* Supply launch arguments in the Codex session test fixture
…25967)

* Schedule unit tests across runtimes by measured duration

* Keep new SQLite fixture suites on Node after updating main

* Inject scheduling timings instead of mocking the module

* Keep agent-session database lifecycle contracts on Node
…ites (stablyai#25985)

Co-authored-by: makoto-developer <72484465+makoto-developer@users.noreply.github.com>
…25983)

Co-authored-by: Masaki Yamamoto <nnasakick@gmail.com>
grok 1.0.41's `grok models` now reports `Default model: grok-4.7` and its
model cache advertises low/medium/high/xhigh effort for grok-4.7. Seed it as
the CLI default with an xhigh ceiling, keep grok-4.6 and grok-4.5, and move
the probe spec default in step with the seed.

Fixes STA-9652
* refactor: remove redundant reveal and watcher test wrappers

* test: keep closed restore fixtures free of deferred WAL writers
…ablyai#25749)

* Fix terminal and chat file drop destination ownership

* fix runtime terminal drop ownership and queued chat retries
…ession running it (stablyai#25875)

* fix(orchestration): serve a /clear'd structured worker through the session running it

A structured (native chat) worker the user /clear'd keeps working in a
successor session, but orchestration kept acting on the session it was
started with, which the clear had closed. Terminal read and @idle status
lost the handle, worker-read and the release archive served the pre-clear
transcript, worker-show reported the worker exited, worker-stop closed the
already-closed session and left the successor running, mail and Dispatch
nudges were refused, and the idle sweep could put the successor to rest
while its Dispatch was open.

One forward walk over the durable session records now answers which
session runs a worker's conversation. Worker authority and custody are
judged on that session, so every caller holding a worker handle gets it by
default; reads, archive, observation, status, group addressing and the
incarnation liveness probe use it too. Stop closes the running session and
re-resolves, so a clear that commits while the close waits is followed to
its successor. The reverse direction (a successor's child env, user
takeover, the idle sweep) maps a session to its worker through the
lineage root. A successor's idle edge re-derives the worker's Dispatch
mailbox, and settlement forgets parked mail on every session of the
lineage, derived rather than stored.

When the running session cannot be found (host not installed, a
successor with no record, a looping chain) observation answers
unverifiable, never exited; readers refuse with session_caller_not_live
and stop closes nothing. worker-read and the archive warn that earlier
conversation from before a /clear is not included.

* fix(orchestration): close the review gaps in serving a /clear'd structured worker

Review of the first change found places where a /clear'd worker still
went wrong, plus a type gap that let the original bug shape compile:

- worker-stop refused a structured worker whose old session read
  exited mid-clear (stopped, but the successor not yet committed); it
  now goes to the clear-aware stop once identity and ownership are
  proven. The terminal (PTY) gate is unchanged.
- A worker stopped while its /clear successor had never run could never
  be released: no close writes death evidence for an agent that never
  started, so the probe read unverifiable forever. A released session
  that never started anything and whose chat is gone now reads exited;
  it stays unverifiable while the chat is listed.
- worker-read, terminal read and the release archive now read the
  worker's whole lineage, oldest session first, under the same page
  limit and byte bound, instead of only the running session. This
  replaces the "earlier conversation is not included" warning. Cursors
  key later sessions' items by session, so a cursor taken before a
  clear stays valid and continues into the successor.
- Pointer delivery re-derives a mailbox's target when an attempt ends;
  if a /clear moved it meanwhile, it delivers once to the new session.
- Custody and status reads take the resolved running session, and one
  typed hold (held, not held, unverifiable) replaces the rebuilt
  authority in terminal read, worker-show and group addressing.
- Smaller fixes: stop keeps an earlier close on its receipt; abandoning
  a side task no longer forgets the worker's parked mail; a release
  whose lineage cannot be verified ends release_unknown instead of
  staying requested forever; mail reach reports an unverifiable lineage
  as unverifiable, not ended; party resolution scans records once.

A /clear typed into a worker's chat is reported as a user takeover, the
same as any other message the user types there; a test pins it.

* fix(orchestration): tighten the never-ran rule and the follow-ups to /clear handling

Round-2 review of the /clear'd structured worker fixes found that the new
rule for "a session no agent ever ran is exited" was too loose, and two
of the new follow-ups missed a case:

- The rule now requires the founding fence, which every reservation
  moves. A reservation that a restart released without proof, or a
  failed first start whose exit was never proven, stays unverifiable
  instead of reading exited. It no longer asks whether the
  conversation is open (any history read opens it, which stranded a
  stop, read, release sequence), and it treats only an authoritative
  tab index without the chat as retired.
- Pointer delivery follows a mailbox a /clear moved on a thrown attempt
  too, keeping the attempt's own failure, and tries each session once.
- Releasing a retired worker whose newest session cannot be read keeps
  the readable earlier sessions' output and says the latest one was
  lost, instead of freezing an empty archive.
- worker-show judges status and addressability on one lineage walk.
- The worker-stop comment says plainly that any structured worker that
  reads exited (a crashed agent too) is closed and its chat hidden;
  tests cover that and a takeover reported by the successor session.

* fix(orchestration): keep a retired worker's earliest readable output across several lost sessions

Releasing a worker whose chat was retired and whose newest session
could not be read kept the earlier sessions' output only when exactly
one later session was lost; after two clears with both later journals
gone, release still froze an empty archive although the first session
held the worker's answer. The archive now walks back past every
unreadable later session, under the same page limit, and its warning
says how many later sessions could not be preserved.

The rule that reads a never-started session as exited also requires
that the record carries no fence floor: a copy restored from backup
may hide a reservation that the backup lost, so it stays unverifiable.

* test(orchestration): build the takeover test's session record from the shared fixture

The changed-code quality gate rejected the test's `as unknown as
AgentSessionRecord` stub, which this branch had touched. It now builds a
real record with agentSessionRecordFixture and a typed lease, so no
type assertion is needed.

* test(orchestration): give the cleared-worker pointer harness the sender-name dependency main added
…tablyai#25671)

* refactor(terminal): add in-place pane layout geometry apply (unused)

PaneManager.applyLayoutGeometry applies a layout's split orientation and
ratios to the mounted pane tree without remounting panes, and never fires
onLayoutChanged. Nothing calls it yet; the live-layout reconciler adopts it
in a later change.

* fix(lint): merge duplicate removal import in delete-worktree failure toast

Main (stablyai#25668) introduced a duplicate import that fails the focused code-quality lint.

* refactor(terminal): share split ratio read; refuse zoomed or dividerless trees in geometry apply
…nput mid-turn (stablyai#25766)

* fix(native-chat): alert when a structured chat asks for approval or input mid-turn

A structured Claude or Codex chat that stopped mid-turn to ask for approval or a
question raised no OS notification, no phone push and no unread dot: the host's
attention feed only fired when a turn settled. The host now announces each newly
pending prompt once, from the same feed, and owns the phone push for its own
structured sessions; desktops keep presentation only.

- host feed: a `prompt` edge per pending approval/question, opt-in on the
  existing stream; a clean settle that only restates an announced prompt is not
  re-announced to prompt-aware clients; failures always are
- host push: an in-process subscriber pushes the host's paired phones with
  labels from in-memory metadata, and withdraws a prompt's alert once answered
- one attention identity for desktop banner, phone push, dedupe and retirement;
  delivery dedupes on it instead of the per-workspace burst window
- agentSession.acknowledgeAttention routes reading a chat to its owning host,
  which retires what it pushed for that session via its dismissal record

* fix(native-chat): keep prompt alerts under their retirement identity

* Fix structured attention read and delivery lifecycle

* Allow later reads to retry failed attention retirement

* Prevent delayed prompt alerts after accepted reads

* Keep attention integration tests across process boundaries

* Check structured capability before cursorless attention reads

* Keep host retirement fixture in the Node typecheck boundary

* Keep attention test seams in the mock boundary

* Keep host notification formatting usable without Electron

* Declare agents in restored-host attention fixture

* Group session attention capability declarations

* Retire structured alerts on every read and when a remote prompt ends

- Mark read and Mark all read on a chat that was never opened, or is hidden,
  now cover what lit its row: the read boundary is the later of the transcript
  cursor and the newest edge the tab was handed, captured at the click. The
  one boundary closes the banners, retires relayed phone alerts and drives the
  host acknowledgement; an edge after the click stays live.
- An alert from a host older than journal cursors has no position, so a pane
  read retires it again, banner and relayed phone push, as before this change.
- A desktop-relayed prompt alert for a remote chat is withdrawn once that
  host's live status stops reporting attention (answered elsewhere, cancelled,
  host restart); cached status after loss of contact never settles it.
- The attention acknowledgement is agent-session.attention-ack.v1 with a
  required journal cursor and strict params; the cursorless no-op is gone.
- The dismissal store keeps a record whose origin a newer build wrote,
  dropping only that origin.
- Host reconciliation runs on restore and when a prompt leaves the pending
  set, not on every journal commit.
- Remove unused session-wide retirement (retire, retireMatching,
  retireMobileNotificationsMatching) and test-only store lookups.

* Settle relayed prompts only from host-dated status; reads default to view

- A remote chat's status and its prompt edges arrive on separate sockets, so a
  status row queued before the prompt could land after it and withdraw a
  relayed alert that was still pending. Settlement now needs a live,
  non-attention row whose host updatedAt (the journal's latest row time, never
  decreasing) is later than the prompt's host raisedAt. The owed prompt is
  cleared only once the settle call succeeds, so a failed call retries on the
  next qualifying row.
- An acknowledgement without captured reads now reads as a view. Mark read,
  Mark all read, the hover-card jump and both dashboard acks pass 'explicit';
  Activity's automatic per-turn read stays a view.

* Re-judge relayed prompt settlement after each call; dashboard watching is a view

- Settlement is one check of the current mirror, run on each status row, after
  each settle call returns, and when a prompt edge arrives. A row that lands
  while a call is out is no longer dropped, and an edge that arrives after its
  own resolution settles at once. A rejected call is retried only once the
  mirror holds a different row, so a failing call cannot loop.
- The Agent Dashboard's card click acknowledges as explicit; watching the open
  dialog acknowledges as a view, through both the in-window drawer and the
  pop-out relay, and no longer re-acks the state the click just read.

* Keep a reused read owner findable after its holders release it

A chat pane memoizes its read owner. When every holder let go in one commit and
the same owner was picked up again (React StrictMode's effect re-run in
development, or a pane remounted in one commit, as on a tab move), the release
dropped it from the registry and nothing put it back. The attention bridge then
found no owner, so a view read carried no cursor: the unread dot cleared but
the banner and phone alerts stayed. An owner now names its key again whenever
it gains an activation or a subscriber and the key is free, never over a
different live owner.

* Type the ack test's execution host ids

* Keep the turn-completion types in main's turn-completion wire module

Main moved these types into agent-session-turn-completion-wire.ts while this branch moved them into agent-session-attention-wire.ts. They now live at main's path with this branch's additions (journal cursor, prompt attention, prompt arm); the unused completion key is dropped.
nwparker and others added 30 commits October 7, 2026 23:34
…i#26425)

* Give Windows Electron writer probes a larger test budget

* Drain queued writer replies across two poll turns before timing out

* Check writer shutdown after both reply-drain turns

* Add inline always-force-delete preference to workspace recovery toast

* Use split-button menu for saved force-delete preference

* Await durable force-delete preference writes before deleting

* Restore absent settings keys after failed durable writes

* Place deletion translations beside worktree copy
…blyai#26358)

* Native chat: let users turn off inline visuals for new chats

* Native chat: keep inline visuals guidance light

* Move inline visuals setting to Chat settings
* Allow another phone send after delivery is unconfirmed

* Update phone image recordings after journal removal
* perf: stop retrying relay bulk frames after admission

* fix: prevent relay bulk retries during admission

* test: prove relay bulk admission races remain exercised

* test: limit teardown watchdog to rejected admissions
…d, without disabling Send (stablyai#25666)

* fix(native-chat): disable Send for unavailable agents

* test: update sign-in copy expectations

* fix: show automatic chat availability only on Send

* fix: keep signed-out evidence when a live session lists models

* refactor: split Send availability presentation and tests

* refactor(native-chat): keep the sign-in verdict on the catalog's failure record

The probe's "signed out" / "CLI missing" verdict now lives on the account's
existing model-catalog failure record instead of two extra maps, with the
failure TTL as its only lifetime:

- Only the probe sets or clears the verdict; a chat's own model listing
  (which works while signed out) never clears or renews it. An aged-out
  verdict makes the next read re-probe, however fresh the catalog.
- A healthy fresh catalog is no longer re-probed every 30 s; it keeps the
  normal 10 min fresh window. A Claude start refused as signed out ages the
  catalog so the next read re-probes.
- A waiting picker again answers from whichever catalog lands first and never
  waits on a probe. A read that still owes the probe's verdict says so, and the
  composer waits for it once with the new optional waitForAvailability param.
- The open-time start-failure line and its transcript row are hidden only
  while Send is disabled for the same reason; otherwise they show as before.
- A host verdict lifetime longer than the client's hold is clamped, not
  dropped. The catalog read watches only the account settings and the paired
  host's contact instead of every store write.

* fix(native-chat): one send-gate rule, and a missing CLI as its own failure

- The sign-in/CLI verdict is gated once where the session hands it to the
  composer: while a turn runs it neither shows nor refuses, so a follow-up
  queues as before; goal mode is held by the same gate. The start-failure line
  and row hide only while that same value is up.
- A start whose CLI was never found (a direct spawn's ENOENT, or the one a
  provider supervisor reports) is now a `cliMissing` failure with the disabled
  Send's sentence, instead of a generic "couldn't start" beside it. An older
  client shows the host's sentence for the new kind.
- Signed-out words for a slash command keep "Run /x again".
- The Claude catalog reads its models only from the reply to its own
  list_models request, never from the initialize reply.
- The Codex probe lists models while its account check runs; a signed-out
  verdict still wins.
- The missing-executable check moves to provider-process, out of native chat.

* test(native-chat): signed-out command failures name the command to run again

* fix(native-chat): probe the Codex home a launch would read

- Before the catalog probe's read-only app-server reads Orca's shared Codex
  home, it runs the same sync a launch and the usage poll run, so a
  `codex login` made in ~/.codex is seen at once instead of leaving
  "Run codex login" up until something else re-syncs (forever on a headless
  host). Per-account homes are left alone.
- A "no account" answer counts as signed out only when the home's auth.json is
  absent or holds no credential; one caught mid-write by a token refresh, or
  holding a credential, is unknown.

* test(codex-accounts): type the node:os mock with a type import

* fix(native-chat): keep the sign-in verdict until a newer answer replaces it

- No gap while signed out: the host serves an aged verdict while the probe that
  re-derives it runs, and the composer never clears its last answer before a
  read; only an answer without a verdict, an error, lost contact or an account
  change does. Send stays disabled with its reason through every re-check.
- After sign-in the stale "isn't signed in" start-failure line stays hidden:
  a probe that found the account fine now says so (`accountVerified`), and the
  line shows only when the host has no answer at all.
- A Claude start refused as signed out, or any start whose CLI was never
  found, is recorded as that home's verdict, and the chat reads it once when
  that start failure arrives.
- The re-probe mark is read only where the catalog read starts its probe, so a
  Codex chat's own picker no longer re-lists for it on every read.
- Choosing an account or signing in again in Accounts settings re-derives the
  verdict at the next read; the account key now compares settings by value.
- A re-read that brings an identical catalog keeps the option state, so focus
  re-reads re-render nothing.

* fix(native-chat): Codex's signed-out words name only the step that unblocks this chat

A chat is pinned to the Codex home it started in, so choosing another account
in Codex Accounts settings never unblocks it. The system-account sentence now
says only to run `codex login`, in every locale.

* refactor(native-chat): give an account's sign-in status its own owner

The sign-in verdict lived inside the model catalog's failure records, so
the store needed guards for who wrote a failure, which listing may clear
it, and whether a verdict was served while re-probing. Each guard fixed one
way a chat's own listing or an older probe overwrote the account's answer.

The answer now lives in AgentAccountStatuses: one in-memory, capped status
per account home (ready, not signed in, or CLI missing). Only the
session-less probe, a refused start and an account change write it. A
blocked answer older than 30 s, or one an account change marked, makes the
next read start the probe whatever else is listing, and it is served until
that probe answers. Older evidence never replaces newer. The catalog's
failure code is main's again, and agent-model-catalog-failures.ts is gone.

The wire carries one optional `availability` field in place of
`unavailable` and `accountVerified`. The renderer holds its answer per
host, agent and chat, re-reads only on that agent's account inputs, and
re-reads on a timer only while blocked. After an account change, Send
stays disabled with the previous words until the re-probe answers, then
changes once. Account switches and re-sign-ins mark the status before the
settings change a chat's read follows.

* fix(native-chat): re-derive the sign-in answer after an account change mid-probe, and back off while blocked

Round-4 review findings on the account status owner:

- An account switch or re-sign-in made while a probe was running left the
  old answer up for up to 30 s: the next read joined that probe, whose
  answer was then dropped as older than the change. A read now chains
  exactly one fresh probe after a probe that began before the change.
- Removing the selected Claude account swapped another sign-in into the
  shared config dir without marking the answer; it now marks it first.
- Evidence was ordered by wall-clock time, so a clock that stepped back
  froze a blocked answer. It is now ordered by a sequence and aged on a
  monotonic clock; a negative age reads as due.
- A chat left open while signed out started a CLI probe every 30 s.
  Probes that keep finding the same block now space out (30 s, 60 s,
  2 min, 4 min, then every 5 min), carried to the client as recheckInMs.
  An account change or a different refusal starts the hold over.
- The renderer dropped no settled wait from its leave set; it now does.
- A failed Claude probe logged its whole stdout, which now includes the
  initialize reply naming the account. Control replies are dropped from
  the log and the failure message.
- The account services take the status re-check as an injected
  onSignInChanged hook instead of importing the native-chat store.
- Imports moved below file headers; comments no longer say "ready"
  means re-checked after the failure.

* test(claude-accounts): say why the runtime-auth fakes are cast where the hook was added

* fix(native-chat): type the held-answer handle as the account status

* fix(native-chat): re-check a block when someone returns, and catch an account change during a home's first probe

Round-5 review findings:

- After a chat sat blocked for a few minutes, the backoff made every
  read wait out a hold of up to 5 minutes, so someone who ran `/login`
  or `codex login` and came back still saw Send disabled for minutes.
  The backoff now applies only to the client's own timer read, which
  it marks with an optional `scheduledRecheck` param. A read from
  someone looking (window focus, the pane becoming visible, a new chat,
  a start failure) re-probes once the block is older than 30 s. The
  param is sent only on that timer, which only a blocked `availability`
  answer arms, so a host that predates the param never receives it.
- An account change made during a home's first probe marked nothing,
  because only existing statuses were marked, so the old account's
  answer was written as current. Each agent's last account change is
  now one sequence number that every status and running probe is
  compared against; statuses are no longer mutated in place.
- Removing an account that nothing had selected no longer re-checks.

* refactor(native-chat): keep the sign-in and missing-CLI words in their own module

Main's retry wording pushed the failure-words table past its line budget.

* refactor(native-chat): build the structured chat's view session in the view-state module

Main's send-reveal work brought NativeChatStructuredSession.tsx and the
session controller to their line budgets.

* fix(native-chat): keep a stated start line hidden through a running turn, and re-probe an account changed mid first probe

- A chat whose open-time start failed signed out had that line hidden
  while the host said the same. When a new signed-out answer landed
  during a running turn, the line came back for the whole turn: the
  hide read Send's gate, which a running turn suspends. The hide now
  reads the host's answer itself (blocked for the same reason, or
  ready); Send's gate still steps aside while a turn runs. Both are
  views of the one answer the session holds.
- An account change during a home's first probe dropped that probe's
  answer but left nothing to re-check the new account, and the picker's
  reply said no probe was pending, so the chat stayed unknown until the
  catalog aged out. A read now chains one fresh probe whenever the
  running probe began before the change, and its reply says that probe
  is still owed so the client asks once more for its verdict.
- The client's timer read no longer carries `scheduledRecheck`. The
  timer already fires only when the host's backed-off `recheckInMs`
  says, which is past the 30 s after which any read re-probes, and
  every answer re-arms it. One fewer strict-schema param.

* build(cli): typecheck the missing-executable check the provider process now imports

Main's stablyai#25753 added managed-provider-process.ts to the CLI project, which
imports this PR's provider-executable-missing.ts.

* fix(native-chat): disable Send with the fix when Claude or Codex is signed out or missing

What you see: in a native Claude or Codex chat whose CLI is signed out, or not installed on the
computer running the chat, Send is disabled and its tooltip (hover or keyboard focus) says how to
fix it, in words that match the account ("Codex isn't signed in. Run `codex login`.", "This
Claude account isn't signed in. Sign in again in Claude Accounts settings.", "Claude isn't
installed on the computer running this chat."). A message refused after sending gets the same
words. On desktop the open-time "Chat could not be started" line, its Retry and its transcript row
no longer repeat these two reasons, unless a message of yours is still waiting on that start.
An unknown answer never disables Send, and while a turn runs Send stays Stop and Enter queues.

How: the host's existing session-less model-catalog probe also says why no chat can start.
Claude's probe child sends initialize before list_models and applies the rule chat start already
uses (token source none with no API key); Codex's probe asks account/read beside model/list; a
spawn that finds no executable is "CLI missing". The probe's failure record in the catalog store
carries that reason, and every catalog reply carries it as an optional `unavailable` field. Only
a probe writes or clears it: a live chat's own listing (which a signed-out Codex still answers)
never does. It lasts the store's 30 s failure lifetime; after that the next read starts or joins
the probe, serving the held reason meanwhile, and only a read that already asks to wait gets the
probe's answer. A change to the managed-account settings expires it. Before probing the shared
Codex mirror home, the probe runs the same sync a launch does. The renderer keeps the latest
answer until the next one replaces it, and only while blocked re-reads on window focus and when
the chat gains focus. No status store, timers or new request params.

* fix(native-chat): no picker loading while Send re-checks a sign-in, and plain keyboard focus on the disabled Send

A chat whose account is signed out and has no model list no longer shows the model picker as
loading each time it re-checks the sign-in after 30 s: the re-check is for Send, so a reply that
already carries the verdict ends the picker's wait. The disabled Send's wrapper takes keyboard
focus (so its tooltip opens) without becoming a second button; the inner button keeps its label.
Trims tests that repeated rules other tests already prove.

* fix(native-chat): lift the Send gate whenever Stop shows, including while a queued card is about to send

The composer shows Stop while the host's queue is about to send its next card, but the sign-in
gate stayed on in that window, so Enter was refused with no message.

* fix(native-chat): never disable Send for a sign-in or missing-CLI verdict; show a dismissible notice instead

The host's pre-send check can be wrong while the chat would work (saved
Arguments it never sees, project settings it can't read, gateways that
accept a signed-out Codex), and a disabled Send left no way past it.

- Delete the Send gate: the composer, Enter and goal mode no longer read
  the verdict, and the Send tooltip reason and focusable wrapper go back
  to main.
- Show Codex not-signed-in and Claude/Codex not-installed as one entry in
  the chat's existing notice card. Dismissed per verdict; shows again
  when the verdict changes or clears and returns; steps aside when the
  failed start (launch line or latest failed-start row) says the same.
- Claude: drop the probe's signed-out verdict (initialize-on-probe stdin,
  request-id filtering, control-reply log scrubbing). The chat start's
  refusal, with its account kind, is the only Claude sign-in signal.
  Missing-CLI from the probe stays.
- Restore the failed-start line and transcript row for these causes
  (no more hiding on desktop) and drop the chat-root focus re-check; a
  window-focus re-read stays while a notice is showing.

* fix(native-chat): re-check the notice's reason after a chat starts, keep a signed-out Codex's model list

Review round 1 of the notice rework.

- A chat that proves its start (at acquire, or on its later "started"
  event) marks the catalog's held reason for that account due, so the
  next read re-probes instead of trusting it until the TTL; the chat
  re-reads when a turn starts or ends while a notice shows.
- The Codex probe keeps a model list it got beside a signed-out reason,
  so a gateway user with a wrong verdict keeps the picker list.
- Failure records move to their own module; an expired failure with no
  reason is dropped, and expiry drops reason-less failures.
- Notice: any verdict change resets the dismissal; signed-out is
  Codex-only; the say-once rule ignores a failed start a turn has run
  past.
- Missing-CLI copy: "{{agent}} wasn't found on the computer running this
  chat. Install it, or check its Command in Settings → Agents." (6
  locales).

* fix(native-chat): only an acquiring start re-checks the catalog reason; drop the unreachable Claude filter

Review round 2 of the notice rework.

- A re-attach to a live child proved nothing new, so only an attach that
  acquired a child which proved its start marks the reason due.
- Reword: a start proves the program exists; for a Codex sign-in it only
  prompts an earlier re-check.
- Drop the notice's Claude signed-out filter: no host sends that verdict.
- Tests: a re-attach does not notify; a turn ending re-reads once more.

* fix(native-chat): never newly show a catalog reason the host is still re-checking

After signing in to Codex, a new chat flashed the old "isn't signed in"
notice for about half a second: past its 30 s lifetime the host answers a
plain read with the held reason while its probe re-checks. A chat now
adopts such a reason only when it already shows it; otherwise the joined
read's answer decides.

* test(native-chat): follow main's harness and the clearer Claude sign-in words after the merge

* test(native-chat): run the start-proof catalog test in the Node runtime project with the other SQLite host tests
…rministic (stablyai#26532)

Co-authored-by: m4air <m4air@Mac.localdomain>
…sation into the terminal (stablyai#26336)

* feat(session-history): resume a native chat's conversation in a new CLI session

Agent Session History offered "Resume in New Chat" to move a terminal session
into native chat, but nothing went the other way: once a native chat owned a
conversation, its row only reopened the chat, and Orca refused any terminal
resume of it so two programs never write one conversation.

Rows a native chat owns (Claude, Codex) now offer "Resume in New CLI". It forks:
the terminal runs `claude --resume <id> --fork-session` or `codex fork <id>`,
so the CLI gets the full history under a new id and the chat is unchanged. The
host's resume guard lets a Claude fork through, and the fork tab records no
provider session, so a restart never re-enters the chat's conversation.
"Resume in New Chat" is renamed "Resume in New Native Chat".

* fix(session-history): size the Resume in New CLI tooltip to its text

The tooltip balanced one wrapped string inside a box sized before balancing,
leaving a gap beside its lines. It is now two block lines with no fixed width.

* fix(session-history): run a CLI fork under the selected Codex account and tighten the fork guard

- Resume in New CLI now asks the host for the same Codex account home plain
  Resume uses, so a fork no longer starts under whichever account's copy of the
  rollout sorted first. The preparation call carries an optional `fork` flag
  that skips only the owned-conversation refusal; plain resume still refuses.
- An older paired host that refuses the fork now shows "Update Orca on the host
  that runs this chat to resume it in a new CLI." instead of a raw error code.
- The host guard admits `--fork-session` only before a `--` terminator and
  never alongside `--session-id`, since either shape lets Claude write the
  owned conversation.
- Tests for the click path, the menu item's eligibility, the fork home
  preparation, and the tightened guard.

* fix(session-history): read a Codex rollout's identity from its first session record

A fork of an older (non-paginated) Codex thread writes its own session record
first and then copies the parent's history, the parent's session record
included. The history scanner kept the last one it saw, so the fork's row took
the parent's id, title, folder and branch: on a native chat's fork it looked
like the chat itself, and Resume on it opened the chat instead of the fork.
Codex itself treats the first session record as the thread's own and ignores
later ones; the scanner now does the same, as Orca's duplicate-rollout check
already did.

* fix(session-history): set up status hooks in the Codex home a CLI fork keeps

When Resume in New CLI can't move a Codex fork to the selected account (that
account has no copy of the conversation yet, or the system default is
selected), the fork runs in the account home the history row came from. A new
Codex tab only gets Orca's status hooks in the selected account's home, and the
fork carries no provider session, so nothing set up the hooks in that row home.
In a home only native chat had used, the fork tab could show no working or
idle status.

The fork's home preparation now readies that home the same way a resumed
session's home is readied (hook install or refresh, plus the daemon-socket
guard). That code is moved into prepareCodexPinnedLaunchHome and shared by both
paths. The fork tab still carries no provider session, so it never reopens the
chat's conversation on restart.

* fix(session-history): ready only a host-verified Codex home for a CLI fork

The fork's hook setup ran in whatever Codex home the request named, so a paired
client could have Orca write hook and config files into any directory. Plain
Resume only readies a home it has verified: one this host lists for Codex
sessions that actually holds the rollout. The fork now uses that same check,
through a shared trustedCodexResumeHomes list, and readies the home only when
the verified home is the one the fork will run in. With no Codex runtime home
service it does nothing, as plain Resume does. A failed check or setup is
logged and never stops the fork from launching.

Tests cover the untrusted home, a missing runtime home service, a rollout on
another host, a WSL home, a fork moved into the real home, and a failed setup.

* fix(session-history): show the host-update message when the desktop wraps the fork refusal

When the local preparation step for Resume in New CLI is refused, Electron
hands the renderer the error as "Error invoking remote method '…': Error:
agent_session_conflict". The fork's failure text matched only the bare code,
so the toast showed that raw line instead of asking the user to update Orca on
the host. It now unwraps the message with the shared IPC error helper before
matching.
…26382)

* fix(hooks): run safe Unicode Windows hook paths directly

* fix(hooks): preserve guarded Unicode launch behavior

* fix(cli): include extracted Windows hook command builders

---------

Co-authored-by: m4air <m4air@m4airs-MacBook-Air.local>
Co-authored-by: Neil <neil@stably.ai>
* test: remove copied logic and literal-only checks

* test: remove duplicated SSH cases and source-text inventories

* test: retire removed SSH literal check from reliability gate

* ci: skip Ruby release checks for reliability metadata
…ed (stablyai#26566)

* test(e2e): retry Hangul-digit attempts the IME itself never delivered

The nested Wayland lane failed ~1 in 6 repetitions on main because mutter+IBus
occasionally discarded the preedit or the digit keydown before Chromium saw it.
Each attempt now checks what the page actually received; only those upstream
drops are retried, and any line Orca loses after the page received it still fails.

* test(e2e): keep the engagement receipt call the gate contract pins

---------

Co-authored-by: m4air <m4air@Mac.localdomain>
…26483)

* fix(remote-runtime): stop arm64 Linux servers dropping clients every heartbeat (empty WebSocket write EFAULT) (stablyai#26200)

* fix(remote-runtime): never write an empty WebSocket payload on its own (arm64 Electron EFAULT)

Patch ws@8.22.0 Sender.sendFrame so a frame with an empty payload is written
as its header alone. Under Electron on arm64 kernels with 39-bit virtual
addresses, socket.write() of a zero-length buffer fails with EFAULT and kills
the connection, so every empty heartbeat ping, auto-pong and close dropped
the remote runtime with 1006. Wire bytes are unchanged.

* test(remote-runtime): run the ws empty-payload guard on real Node

Bun resolves 'ws' to its built-in client, so under the Bun test project the
guard exercised Bun's implementation instead of the patched package.

---------

Co-authored-by: m4air <m4air@Mac.localdomain>

* fix(orcad): ship the session scanner child so agent session history lists on orcad hosts (stablyai#26309)

orcad built every forked child except session-scanner-service-entry.js, so
aiVault.listSessions always failed on an orcad host. The resolver also skipped
the adjacent entry whenever isPackaged was true, so shipping the file alone
would not have been enough.

- build every ORCAD_CHILD_ENTRY_POINTS entry from one loop and add the scanner
- add it to ORCAD_ARTIFACTS so a deploy missing it fails integrity checks
- share one forked-child path resolver (asar layout vs flat app root) between
  the watcher and the scanner
- smoke-fork the built scanner during build-orcad under both runtimes

Fixes stablyai#24105

Co-authored-by: m4air <m4air@Mac.localdomain>

* fix(remote-runtime): one owner for a remote pane's binding and recovery (stablyai#26308)

A remote pane no longer latches in connecting on terminal_not_found, holds
keystrokes typed during same-terminal recovery instead of dropping them,
reports transport-owned retries so the pane is not remounted mid-retry, and
no longer infers absence from an inventory that predates the leaf.

Co-authored-by: m4air <m4air@Mac.localdomain>

* ci(e2e): move apt off the Azure mirror on runner images that use a mirror list (stablyai#26335)

Ubuntu 24.04 runner images resolve apt sources through /etc/apt/apt-mirrors.txt, so rewriting only the source lists left package downloads on azure.archive.ubuntu.com, which intermittently fails ('Ign:' on every package). Rewrite the mirror list too, and retry the install once with --fix-missing.

Co-authored-by: m4air <m4air@m4airs-MacBook-Air.local>

* fix(terminal): remote snapshot replay keeps scrollback it cannot replace (stablyai#26319)

Desktop subscribers of a remote terminal received screen-only snapshots,
and every pushed snapshot replay cleared the pane's scrollback with ESC[3J.
A reconnect, a remount, or a recovery after a dropped frame therefore wiped
history the host never resent.

- Host: desktop subscribe snapshots use the same scrollback-row ladder as
  mobile, and SnapshotStart now carries an optional scrollbackRows field.
- Client: a pushed snapshot clears scrollback only when the host says it
  carries history; screen-only images (scrollbackRows 0, or older hosts
  that omit the field) repaint the screen and keep the pane's history.

Co-authored-by: m4air <m4air@Mac.localdomain>
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix(daemon): clean launch env, validate client frames, fit the socket path (stablyai#26306)

* fix(daemon): clean launch env, validate client frames, fit the socket path

- Drop Chromium's DBUS_SESSION_BUS_ADDRESS=disabled: marker from the daemon,
  its systemd-run scope launch and every PTY env (stablyai#25580, stablyai#21119).
- Use the durable user-manager scope only when that manager outlives the
  caller: lingering on, or the caller already runs inside it (stablyai#24201).
- Shape-check hello and request frames, and catch request rejections, so a
  malformed frame closes one connection instead of the daemon (stablyai#17841).
- Move the daemon socket to a short per-uid private dir when the data root
  would overflow sun_path (stablyai#17840).

* fix(daemon): validate a relocated socket dir before any connect, not just before bind

---------

Co-authored-by: m4air <m4air@Mac.localdomain>
Co-authored-by: m4air <m4air@m4airs-MacBook-Air.local>

* fix(ssh): keep crediting a PTY after its recovery delivery is canceled (stablyai#26341)

A canceled recovery delivery released its source spans while the renderer
and model could still settle them; the stale projection then threw on every
later ACK, so the next delivery was never credited and the relay paused the
PTY for good. Settling or transferring a span of a closed delivery is now a
no-op.

Co-authored-by: m4air <m4air@Mac.localdomain>

* fix(rpc): deny-by-default caller permissions and a scoped SSH bridge credential (stablyai#26337)

* fix(rpc): deny-by-default caller permissions and a scoped SSH bridge credential

Every RPC method now declares a required permission, and RpcDispatcher checks it
against the caller's scope on every transport (owner, mobile, paired runtime,
SSH bridge). The SSH relay bridge no longer spawns the client CLI with owner
authority: each invocation gets a short-lived credential bound to its SSH target,
revoked when the child exits. Without the new per-host opt-in, that credential
reaches only its own host's terminals; with it, workspace methods but never
desktop control, accounts, settings writes, skill install/share, pairing or host
admin. Paired runtime clients need a pairing-time grant (orca serve
--grant-desktop-control) for computer.* actions.

Fixes stablyai#23835
Fixes stablyai#25259
Refs stablyai#18269

* fix(rpc): let a bridged SSH worker report to its own coordinator; allowlist paired-runtime permissions

Without the per-host opt-in, orchestration send/ask/reply/check/inbox now pass when the caller is a
terminal on the bridged host, binding pane key, payload Dispatch, run and recipient to that caller's
own orchestration (recipient off-host only as the creator of its active Dispatch).

Paired runtime clients get an explicit standard permission set; pairing-admin and desktop-control
are grant-only.

* fix(rpc): resolve a bridged SSH caller's own Run and Dispatch mailboxes before admission

Mail routing stores a worker's follow-ups under dispatch:<id> and worker
questions under run:<id>, so comparing the original recipient to the caller's
terminal handle refused an SSH worker replying to its coordinator, an SSH
coordinator answering its own Run's question, and explicit sends to the
caller's own run:/dispatch: address. The bridge now resolves the exact
mailboxes the caller's live pane and process read (as check does) and admits
replies and recipients against those.

* refactor(rpc): parse SSH bridge selectors with a typed schema instead of Reflect.get

---------

Co-authored-by: m4air <m4air@m4airs-MacBook-Air.local>

* fix(projects): remove a project only on the host the user picked (stablyai#26340)

* fix(projects): remove a project only on the host the user picked

removeProject now requires a hostId and matches that host's row exactly.
The bare-id fallbacks (unique candidate, focused host) could resolve to
another host's live project and delete it. Orphaned SSH purge and group
deletion now pass each row's own host.

Fixes stablyai#13071

* fix(projects): group delete removes only the group's member rows

Same-id rows on other hosts outside the group were removed because membership
was rebuilt from bare ids after the cascade cleared it. Capture (id, host)
member pairs before deletion and remove only those.

* fix(projects): never let a stale catalog widen a project removal to every host

The renderer chose id-only `repos:remove` when its own catalog showed no
same-id row on another host. A stale catalog (another window, a paired
client, or a pending refresh) hid that row, and main then deleted every
host's registration and saved workspace metadata.

- The local catalog route now always calls `repos:removeForHost` with the
  owner row's host, for local, direct SSH, git and folder projects.
- `Store.removeProject` refuses an id that rows on more than one host share,
  so no id-only caller can delete another host's row.
- Setup rollbacks remove the just-created row by its host.

---------

Co-authored-by: m4air <m4air@m4airs-MacBook-Air.local>

* fix(serve): load SSH targets on a headless serve; a user Quit only closes serve-hosted windows (stablyai#26348)

* fix(serve): load SSH targets on a headless serve; a user Quit only closes serve-hosted windows

- launchServeMode registers the SSH handler layer before RPC binds, so a windowless
  'orca serve' lists and connects the profile's SSH targets (stablyai#25886, stablyai#8489).
- Under serve, the app-menu Quit/Exit, tray Quit and renderer-recovery Quit close the desktop
  windows instead of quitting the process (stablyai#15537). Signals, supervisor stops, relaunches and
  update installs still quit normally; on macOS a signal reaches before-quit the same way
  Cmd-Q does, so the guard lives on the user commands, not in before-quit.

* fix(serve): SSH profiles serve on Electron; native macOS quits and frozen windows no longer trap or stop serve

- orca serve: a profile with saved SSH targets stays on the Electron host by default, since
  orcad has no SSH stack yet (stablyai#25886, stablyai#8489). ORCA_SERVE_RUNTIME=orcad still forces orcad.
- macOS serve: Dock > Quit, app-switcher Cmd-Q and other native terminates only close the
  desktop windows (stablyai#15537). Signals, supervisor stops, relaunches, update installs and
  logout/shutdown still quit.
- The serve signal listeners now reclaim SIGINT/SIGTERM/SIGHUP from Electron's native handler.
  Any signal that had a Node listener before app ready was left with Electron's handler, so
  these listeners never ran.
- A serve user Quit closing a frozen renderer's window arms the same 10s ack deadline as a
  desktop quit, and skips minimize-to-tray.

* fix(serve): stay on Electron when the profile's SSH targets cannot be read yet

A valid older-schema profile is rejected by the read-only probe, and the
selector took that as "no SSH", so a default serve before the first
migration started orcad with an empty SSH registry. Fail closed instead.

* refactor(window): type user-Quit close marks as a named window slice, not object

* test(serve): pass the desktop-control grant default in the headless SSH launch fixture

* test(serve): run the serve-selection SQLite fixtures in the Node runtime project

---------

Co-authored-by: m4air <m4air@m4airs-MacBook-Air.local>

* fix(terminal): keep the main-process terminal tail linear under TUI redraw floods (stablyai#26316)

* fix(terminal): keep the main-process terminal tail linear under TUI redraw floods

Full-width in-place row rewrites rebuilt the row string per character
(O(width^2)), a long chunk past the line cap spliced the row array once
per newline (O(lines x cap)), and multi-frame chunks summed every
cursor-up so they fell back to the whole-tail path. Rows now hold
mutable cells joined once, capped rows are dropped in batches, and the
redraw window uses the net cursor reach.

Fixes stablyai#11315

* fix(terminal): do not rebuild a redraw row the cursor only revisits

Rows stayed cell arrays once touched and re-joined on every newline, so a
cursor revisiting one wide row thousands of times cost O(width) per
newline. Runs now splice into the row string, a row switches to cells
only after 32 edits between snapshots, and cells are joined once and
dropped.

* fix(terminal): keep one cached row per chunk so unchanged redraw revisits cost O(1)

A row now keeps its cells, right-trimmed snapshot and content end across
newlines and only invalidates them on a real content change; identical
writes leave it clean. Adds a work-accounting matrix over restore, the
append path and live onPtyData, plus a per-character row oracle fuzz.

* fix(terminal): return redraw rows to flat strings with cached snapshots

Cells made every changed snapshot a full-width join, which was far slower
than the old flat-string splice. Rows are flat strings again, written one
run at a time; each keeps a cached right-trimmed snapshot and content end
that only a real change invalidates, so identical rewrites and cursor-only
revisits stay O(1). A wall-time test now times every review probe against
a vendored copy of the old row model on restore, append and onPtyData.

* test(terminal): read runtime leaf and PTY tail through typed members

---------

Co-authored-by: m4air <m4air@Mac.localdomain>
Co-authored-by: m4air <m4air@m4airs-MacBook-Air.local>

* fix(ssh): relay fallback runs Orca's pinned Node by default, never a host npm install (stablyai#26303)

* fix(ssh): relay fallback runs the pinned Node ladder by default, never a host npm install

A host whose connect falls back to the relay route now gets the A/B/C ladder
(Orca's uploaded Node and prebuilt addons) unless it opts into Host Node. A
refused rung C steps to rung D with an actionable reason instead of compiling
native deps on the host.

* fix(ssh): mark the opt-in Host Node relay runtime as unsupported on the SSH host card

The relay fallback now defaults to Orca's pinned Node ladder; the host-npm path is
reachable only by an explicit per-host Runtime or ORCA_SSH_REMOTE_RUNTIME override.
When a relay actually launches on it, main publishes hostNodeRuntime on the
connection state (optional, admitted field-by-field for paired clients) and the
SSH host card shows an unsupported-configuration warning.

* fix(ssh): keep the relay runtime ladder honest about what it cannot verify

- NixOS's stub loader refusing Orca's generic Linux Node (exit 127, 'Could not
  start dynamically linked executable') is classified wrong_libc, so the ladder
  steps on to the host Node instead of failing the connect at rung A.
- In plain SSH mode the shell provider answers absent only for shells it saw
  exit; a PTY id it never minted (one an earlier relay may still run) is
  unverifiable, never exited.
- The rung D message offers Host Node only when the host's own Node refused the
  prebuilt addons, and calls it an unsupported configuration; a download,
  unresolved-host or Windows refusal gets no Host Node advice.

* fix(ssh): keep the host-Node relay where the ladder can't judge the host

A client without Orca's runtime artifacts (a dev, e2e or source build with no
orcad template, or an offline client) refused rungs A and C with
artifacts_unavailable, and the connect settled on rung D. That reason describes
the client, so it now lands on the host-Node relay it had before the ladder,
marked unsupported, and persists no host decision. A proved noexec still lands
on D.

Windows hosts have no rung B or C yet, so any rung A refusal there keeps the
host-Node relay too. The rung D message is worded from the host OS: Windows
gets no noexec-mount wording and is offered Host Node.

* fix(ssh): a host-Node fallback with no Node settles rung D, and mutable refusals expire

The host-Node relay the ladder chooses in place of D (a client artifact gap,
or a Windows rung A refusal) hard-failed the connect when the host had no
Node: the resolver threw a plain Error, which plain SSH mode does not admit.
The fallback now proves host Node with a strict probe first; an answered
"no Node" settles D with a classified, host-OS worded reason, and an
unanswered probe stays a retryable failure. A gap no longer rewrites D to
host Node once rung C proved host_node_missing.

Mutable pinned-runtime refusals (missing_lib, security_software, and noexec
on Windows) are no longer persisted and replay from memory for ten minutes
only, so a repaired host gets rung A back. The in-memory refusal cache moves
to ssh-relay-pinned-refusal-cache.ts.

* fix(ssh): no host does worse than the pre-ladder host-Node default

Restructure the relay runtime ladder around one invariant: A, B and C are tried
first, and any refusal past them falls back to exactly the pre-ladder default,
the host-Node relay marked unsupported. That covers an unidentified libc, a
client artifact gap, Windows, mutable refusals and a C N-API floor the npm build
would not have hit. Rung D is reached only when that fallback proves the host
has no usable Node, or a proved noexec home would defeat it too.

Strict host-Node resolution now treats only a command that ran and exited as an
answer, so a refused channel (MaxSessions), session limit or timeout stays a
retryable failure instead of a 'no Node' verdict. The launch reuses the Node the
fallback probe found.

Mutable refusals (missing_lib, security_software, Windows noexec) persist again
with their proof time and replay for 24 hours, so a host that still blocks
Orca's runtime is not re-uploaded to on every connect, while a repaired one gets
rung A back. A table-driven test asserts the invariant per host class.

* fix(ssh): every answered rung failure reaches the host-Node default, and D only past it

Two shortcuts skipped the pre-ladder default. An answered but unclassified
install failure (tar ENOSPC, an extract or hash marker, an unverified Windows
promote, a self-test that failed outside every refusal class) aborted the
connect on every attempt; it now steps down as a non-persisted install_failed.
Only unanswered failures (abort, timeout, channel-open, session limit, an
unconfirmed termination, SFTP connection loss, ssh exit 255) stay retryable,
and nothing steps down once a relay launch was issued.

A proved noexec no longer jumps to D: exec denial can be per binary (AppArmor
allowing /usr/bin/node), so it skips B and C, which load from the same tree,
and reaches the host-Node fallback. D now follows only that fallback answering
with a failure, so its outcome is a superset of the default's by construction.

The never-worse test is now the cross-product of every A, B, C and host-Node
answer on Linux and Windows, plus named review cases.

* fix(ssh): system-ssh upload failures and a broken cached runtime step down before launch

System-ssh directory upload and system sftp threw plain Errors that dropped the
host's exit status, so a remote tar ENOSPC or a Windows sftp permission refusal
aborted the connect instead of stepping down. The remote side now throws
SystemSshCommandExitError (ssh exit 255, signals and lost-connection
diagnostics stay retryable), and a local tar failure is still blamed on the
client rather than the remote tar that saw its truncated stream.

The warm runtime check only tested that the verified executable existed, so a
cached runtime that the host's exec policy or a removed library had since broken
was launched and hung on every reconnect. The same command now also runs it and
reports a refusal the way promotion does, so it steps down before any launch,
with no extra round trip.

* fix(ssh): keep the remote answer when local tar only failed writing to it

When the host's tar exits early (ENOSPC, a refusal), BSD tar on macOS exits 1
with 'Write error' and no signal, so the upload rethrew the local error and
dropped the typed remote exit, aborting the connect. The remote answer now wins
whenever local tar finished, was signalled, failed after ssh closed, or reported
a write or pipe error; a local tar that failed on its own input first is still
the client's fault.

* test(ssh): hostile-host cells allow read-only version probes, never installs; probe each Node once

The noexec-home cell failed on 'npm --version': under the never-worse ladder a
proved noexec reaches the strict host-Node fallback, whose probe asks npm its
version before settling D. The cells now judge install and compile calls (npm
install/ci, node-gyp, cc, make) and allow '<tool> --version' / '-v'.

The probe ran twice because the login shell named the Node the path probe had
already rejected. Candidate checks are now memoized per resolution, for the
host-Node fallback and rung C alike.

---------

Co-authored-by: m4air <m4air@Mac.localdomain>
Co-authored-by: m4air <m4air@m4airs-MacBook-Air.local>

* chore(i18n): regenerate the runtime-required English catalog after the main merge

* fix(serve): serve on the profile the CLI selected, and refuse any other before the instance lock (stablyai#26424)

Co-authored-by: m4air <m4air@m4airs-MacBook-Air.local>

* fix(remote-runtime): open terminal tabs reattach after a managed server update re-pairs the host (stablyai#26433)

* fix(remote-runtime): a remote pane rebinds after a server update re-pairs its host

A pane captured the host's pairing revision once, when it mounted. A forced
managed-server update re-pairs the host under the same environment id, so every
later request from that pane was refused as stale, and the refusal counted as
fatal: the pane stopped retrying and Reconnect replayed the same stale revision.
Each request now carries the current revision, a pairing refusal re-reads the
catalog before the retry, and the pane treats it like lost contact.

* fix(remote-runtime): close a stream left on the old pairing and ignore a 0x0 grid hold

A stream that reopened before main re-paired stayed open while sendFrame refused
its stale revision, so typed keys vanished. A revision change now closes it as
recoverable so the pane rebinds on the new pairing. A just-restarted server also
published a 0x0 hold that shrank the visible pane to one cell; holds with no grid
are ignored.

* fix(remote-runtime): a pane follows only its own machine's re-pair

A re-pair under the same environment id to a different host key published its
revision before the catalog retired the old tabs, and the pane recovered at
the new revision, so its next call was authenticated as the replacement peer.
The catalog now classifies each re-pair before publishing revisions: a
replaced or removed machine's terminal streams end for good first, and a pane
only advances its pairing along a proven same-host rotation (same registration
and host key). An unresolved identity holds the pane until it is proven.

---------

Co-authored-by: m4air <m4air@m4airs-MacBook-Air.local>

* refactor(rpc-permissions): Phase 3.5 elegance pass (stablyai#26471)

* refactor(rpc-permissions): drop never-read incarnation and invocation id from bridge credentials

* refactor(rpc-permissions): define runtime device grants once and reuse bridge scope fixtures

* refactor(rpc-permissions): decide terminal host membership in one predicate and share the opt-in hint

* refactor(rpc-permissions): parse bridge selectors once per call

* refactor(rpc-permissions): admit and bind unopted bridge methods from one table

---------

Co-authored-by: m4air <m4air@m4airs-MacBook-Air.local>

* refactor(remote-pane-binding): Phase 3.5 elegance pass (stablyai#26470)

* refactor(remote-pane-binding): keep the pane's pairing revision in one slot

* refactor(remote-pane-binding): share one hold decision across the input entry points

* refactor(remote-pane-binding): define the persisted-handle adopt once in attachPane

* refactor(remote-pane-binding): build the pushed-snapshot meta once

* refactor(remote-pane-binding): drop the test-only peerReplacedEnvironmentIds wrapper

* test(remote-pane-binding): share the input and subscribe frame readers via stream fixtures

---------

Co-authored-by: m4air <m4air@m4airs-MacBook-Air.local>

* refactor(relay-ladder): Phase 3.5 elegance pass (stablyai#26474)

* refactor(relay-ladder): put the host-Node fallback in the ladder before D

The Auto ladder now lists legacy as the rung before D, so the step function
is one rule plus the next rung. Deletes nextRelayRuntimeStep, the hostOs
step context and the mutable hostNodeFallback flag (now a constructor-time
laddered field). Windows still ends at legacy: B and C refuse there before
any host I/O.

* refactor(relay-ladder): one rung-D reason and message over reachable states

D is reached only from the host-Node fallback, so its refusal is
host_node_missing or install_failed. Merges the reason and message
functions, drops three message branches that could not run, and folds
noexecRemembered/noexecProved into one noexec field.

* test(relay-ladder): one never-worse cross-product with a remembered axis

Drops the named cases (each matches a cross-product outcome), the transport
case that only checked the model against itself, and the third case the rank
check already implies. Adds rung B's noexec answer and per-rung remembered
flags for A and B, the only rungs that replay a cached refusal.

* refactor(ssh): one answered-miss rethrow for the node resolution probes

Four catch blocks repeated the same session-limit, abort and strict checks.
They now call rethrowUnlessAnsweredMiss and keep their own fallthrough.
Also drops a stale 'Caches nothing' comment.

* refactor(ssh): share the host-answered system-ssh exit rule; flatten the upload throw cascade

isHostAnsweredSystemSshExit owns the 255/null transport rule for both the
upload pipeline and isAnsweredHostFailure. settleUploadPipeline drops a
branch that could not run: a failed tar is judged once, then ssh, then pipe.

---------

Co-authored-by: m4air <m4air@m4airs-MacBook-Air.local>

* refactor(serve-daemon-misc): Phase 3.5 elegance pass (stablyai#26478)

* refactor(ssh-pty): build obligation transitions behind one closed-delivery guard

* refactor(build-orcad): table-drive the shipped worker smokes

* refactor(daemon): pass scope probe seams as one deps object and share cgroup line parsing

* refactor(repos): route every project removal through removeProjectForHost

* refactor(build-orcad): type the worker smoke table for the type-aware gate

---------

Co-authored-by: m4air <m4air@m4airs-MacBook-Air.local>
Co-authored-by: m4air <m4air@Mac.localdomain>

* refactor(terminal-tail): Phase 3.5 elegance pass (stablyai#26465)

* refactor(terminal-tail): share mulberry32 and compare whole results in redraw equivalence tests

* refactor(terminal-tail): keep one splice ceiling at the onPtyData entry point

---------

Co-authored-by: m4air <m4air@m4airs-MacBook-Air.local>

* fix(terminal): bound the backing a redraw row can pin across chained erases (stablyai#26547)

Each erase that keeps at least half the row skipped the copy, so a chain
of them left nested slices pinning the original wide row. Rows now track
an upper bound on the string they slice and copy once the cumulative
shrink passes half.

Co-authored-by: m4air <m4air@Mac.localdomain>

---------

Co-authored-by: m4air <m4air@Mac.localdomain>
Co-authored-by: m4air <m4air@m4airs-MacBook-Air.local>
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…-pr-14001-main-conflicts

# Conflicts:
#	src/renderer/src/i18n/locales/en.json
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

7 participants