Commit Graph
12907 Commits
Author SHA1 Message Date
Jinwoo Hong faed899cd3 fix(ci): run three new SQLite-backed tests in the Node runtime project (#26010)
#25888 and #25766 added tests that import the orchestration database or the
structured session runtime without registering them in the Node runtime list,
so vitest-sqlite-runtime-boundary fails on main and every PR.
2026-10-06 19:48:41 -04:00
e717ea6b4c feat(native-chat): copy chat images from the right-click menu (#24161)
* feat(native-chat): copy chat images from the right-click menu

Fixes #23904

* test(native-chat): cover Copy image and the open preview in Electron

* fix(native-chat): keep the image preview open by click target, confirm copies, pass PNGs through

The image preview stayed open through a chat-menu click by reading which
element had focus, which depends on the menu's exit-animation timing.
It now ignores an outside interaction whose target is the chat menu, the
same target check other Orca surfaces use for portaled menus. Both
previews drop their color and padding overrides on the dialog surface,
which the design system reserves to the dialog primitive.

A successful Copy image now shows "Image copied"; before, nothing told
the user the copy had finished.

An image that is already a PNG is copied as-is after the size checks,
instead of being decoded and re-encoded, which cost time and could grow
a screenshot past the clipboard size limit.

* fix(native-chat): pass an image through as PNG only when its bytes are PNG

Copy image skipped re-encoding any image whose type said PNG, but a chat
image's type comes from its file extension. A WebP or GIF saved under a
.png name was then sent unconverted, the main process could not decode
it, and the user saw "Image copied" with nothing on the clipboard. The
pass-through now checks the PNG file signature instead.

* test(native-chat): format combined image-copy and session-ID mocks

* Release chat image menu when the retained pane hides

* Validate inherited style directives in their owning scan

* Copy displayed chat images and refuse hidden menu capture

Use full-size sent-image sources, read HTTP images when copying is chosen,
and rasterize browser-decodable formats through the existing converter.
Preserve the current preview surface and prevent a hidden preview from
retaining another copy action.

Co-authored-by: Kelvin Amoaba <97001695+AmoabaKelvin@users.noreply.github.com>

* Revert "Validate inherited style directives in their owning scan"

This reverts commit fa2d91a3cd.

* Use standard dialog surfaces for chat image previews

Keep image previews open while their chat menu is used, and inherit the standard dialog padding, background and border.

Co-authored-by: Kelvin Amoaba <97001695+AmoabaKelvin@users.noreply.github.com>

* Use dialog-owned spacing in image previews

Inherit the standard dialog gap together with its padding and surface.

Co-authored-by: Kelvin Amoaba <97001695+AmoabaKelvin@users.noreply.github.com>

---------

Co-authored-by: Brennan Benson <79079362+brennanb2025@users.noreply.github.com>
Co-authored-by: Neil <4138956+nwparker@users.noreply.github.com>
2026-10-06 16:48:24 -07:00
Brennan Benson f6448a1e26 fix(native-chat): say which retry a Claude retry is on and what it last failed with (#25818)
* fix(native-chat): say which attempt a Claude retry is on and what it last failed with

* fix(native-chat): count Claude's retries as retries, not attempts

Claude Code sends its first api_retry frame only after the first request
failed, so frame "attempt N of max_retries" is the Nth retry. Say
"Retry N of M." and store the bound as maxRetries.
2026-10-06 16:32:32 -07:00
Brennan Benson 35e173a5b7 fix(native-chat): alert when a structured chat asks for approval or input mid-turn (#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.
2026-10-06 16:25:53 -07:00
Jinwoo Hong 53b9ccba6c refactor(terminal): add in-place pane layout geometry apply (unused) (#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 (#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
2026-10-06 19:24:47 -04:00
Brennan Benson 31f103535f fix(orchestration): serve a /clear'd native-chat worker through the session running it (#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
2026-10-06 16:20:45 -07:00
Brennan Benson 5345ba34bf Fix ownership for terminal and chat file drops (STA-6940, PR 1/6) (#25749)
* Fix terminal and chat file drop destination ownership

* fix runtime terminal drop ownership and queued chat retries
2026-10-06 16:15:06 -07:00
Neil deb737478a Simplify reveal and test fixture cleanup (#25978)
* refactor: remove redundant reveal and watcher test wrappers

* test: keep closed restore fixtures free of deferred WAL writers
2026-10-06 16:09:40 -07:00
NeilandKyou0203 2137295bb6 fix(git): use branch-safe author names as username fallback (#25984)
Co-authored-by: Kyou0203 <kyou12138@gmail.com>
2026-10-06 15:59:38 -07:00
Brennan Benson 1eeb8f3f72 fix(test): drop the duplicate resolveLaunchArgs that two merges both added (#25997) 2026-10-06 18:53:47 -04:00
Jinwoo Hong 7bc9ff947c fix(grok): add Grok 4.7 to the fallback model catalog (#25976)
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
2026-10-06 18:47:30 -04:00
NeilandMasaki Yamamoto add534475d fix(browser): detect Comet in its Windows vendor directory (#25983)
Co-authored-by: Masaki Yamamoto <nnasakick@gmail.com>
2026-10-06 15:44:38 -07:00
Neilandmakoto-developer fa42e57659 fix(gitlab): select SSH workspace host for merge request and issue writes (#25985)
Co-authored-by: makoto-developer <72484465+makoto-developer@users.noreply.github.com>
2026-10-06 15:42:22 -07:00
Neil d8c871a1f0 Speed up unit tests with cross-runtime duration scheduling (#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
2026-10-06 15:33:40 -07:00
Neil 6352ff3ff9 Run nested workspace deletion in the background (#25975)
* Run confirmed nested worktree deletion in the background

* Reuse the translated workspace deletion failure title

* Supply launch arguments in the Codex session test fixture
2026-10-06 15:30:49 -07:00
Jinwoo HongandClaude 72b84118e0 test(e2e): terminal layout parity check against main (#25681)
* 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>
2026-10-06 18:26:49 -04:00
Brennan Benson c893048aeb feat(native-chat): show which agent a chat message is from, and open it (#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.
2026-10-06 15:14:04 -07:00
Jinwoo Hong cc6006779f fix(test): pass resolveLaunchArgs in the Codex stopped-send-order test so main typechecks (#25977) 2026-10-06 18:03:45 -04:00
Jinwoo Hong 51062e653e fix(test): pass resolveLaunchArgs in the record store slot test so main typechecks (#25970) 2026-10-06 17:47:58 -04:00
Brennan Benson 6aa12c30ef Name native Claude and Codex chats after their first message (#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
2026-10-06 14:42:59 -07:00
Brennan Benson e813fa5819 feat(native-chat): keep a message a Stop took back on screen, with one stop row after it (#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 (5ead1f6bcc) 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 58a1ade504)

* 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 93749070e4, 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 ab2826ee18, 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 e02f98add5, 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 4ab6cc431c.

* 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 NativeChatComposer within its line limit after the Stop props and
main's /context answer both landed in it.

* fix(mobile-chat): a send a Stop took back is not handed back to the draft

A send whose answer says a Stop withdrew it is drawn in the chat with
its stop row, so handing its text back to the composer as well showed it
twice. It reads as sent and spends its id; other rejections still go
back to the draft as main has them.

* refactor(native-chat): name the dictation hook for what it owns now

* fix(mobile-chat): resend a retained replay a Stop took back, under a fresh id

A retained id is keyed by the message text, so its replay cannot be told
from a new send of the same text. When the answer is a submission a Stop
withdrew, that message provably never ran, so the phone sends it once
more under a fresh id, as it already does for a withdrawn queued card.
A first answer a Stop took back still reads as sent, as on desktop.

* test(mobile-chat): count every send a withdrawn replay makes

* fix(mobile-chat): a resend a Stop took back again is not handed back to the draft

When storage fails, a resend's answer is lost and a second Stop takes
the resend back, the fall-through after a withdrawn resend returned the
text to the draft beside its stopped row. A withdrawn submission there
now reads as sent; a withdrawn queued draft still goes back. Pins the
one-resend guard and both orders of the stopped row and a resend.

* fix(native-chat): a Stop's note says it took once a joined close proves the exit

A session-ending Stop whose child's end failed revises its note to
'unconfirmed'. Since main's #24862, the next Stop joins that close rather
than stopping again, and wrote no note, so a close that then proved the
exit left 'unconfirmed' under a turn that ended. The close now carries
the note it settles, and its proven exit revises it to 'Cancellation
requested.'. A join that fails again leaves it unconfirmed.

* test(native-chat): draw a stopped send end to end in this PR's own Codex file

The shared turn-end settlement file went past its line limit once main's
Stop tests and the answered-turn tests met. This PR's two drawing checks
move to its own end-to-end file, leaving that file as #25073 has it.

* fix(native-chat): a proven turn end says a Stop's unconfirmed note took

Replaces the note carried on the child's close. Every settlement that
ends turns interrupted on a proven exit, live or after a crash, also
revises an unconfirmed Stop note on those turns to 'Cancellation
requested.', found by the note's turn scope, in the same batch. A note a
Stop wrote before its turn showed is re-keyed onto the running turn when
it becomes unconfirmed, in one batch, so that end finds it. Known limit:
with no turn open yet, the note keeps its key and no turn's end revises
it.

* test(native-chat): a Stop's unconfirmed note says it took when the agent exits on its own

* test(native-chat): restore the named Stop, turn anchor and role checks in the moved draw tests

* test(native-chat): name the Codex handle as main's opaque handle does

* test: restore the provider handle import the main merge dropped

* fix: derive Stop note wording from interrupted turns

* test: name the raw replay case for what it covers

* refactor(native-chat): move the waiting-slot split into its own hook

* test: follow main's chat font-size rename in the stopping tests

* test: follow main's single live-line value in the Stopping tests

* test: give the android live-line fixtures the stopping field

* chore: keep the session host under its line limit after the main merge

* test: follow main's card field and live-line component in the stop-withdrawn tests

* chore: keep the composer test and the phone chat view under their line limits after the main merge

The Stop control now disables itself while Stopping, so the composer passes
the flag through and its test file stays as main has it. The phone chat
header's Stop moves to its own component.

* perf(native-chat): read a Stop note's fields before parsing its key on every snapshot

Every snapshot projects each item through the Stop-note read, and each new
snapshot rebuilds the index of Stop notes by turn. Both parsed every item's
key first; they now check the row's kind and turn scope (and, for the
projection, its unconfirmed-stop failure) before the parse. Every Stop note
is a status row, so what each finds is unchanged.

* fix(native-chat): keep the Spanish 'Stopped before the agent started' in the one notices block; the phone parity test reads Stopping from the live line
2026-10-06 14:39:32 -07:00
Neil 28ed544da8 Fix send picker reveal under collapsed hosts and parents (#25950)
* fix: reveal send picker targets through host-owned ancestors

* test: type picker close fixture as a nullable target
2026-10-06 14:32:33 -07:00
Brennan Benson e1e9d1c14a feat(agent-launch): host-assigned caller identity and a launch record written when the surface exists (#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 (b9adf88da0, 0d7d9b5b30, c83f44dd72, 39d15d52c0) onto
main, without #24080/#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.
2026-10-06 14:25:24 -07:00
Brennan Benson 0a26f3ec90 fix(native-chat): honor saved agent Command and Arguments (#25721)
* 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
2026-10-06 14:17:22 -07:00
Jinwoo HongandClaude ce4c99ae07 fix(terminal): keep a pane closed just before quit from coming back on relaunch (#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>
2026-10-06 17:13:59 -04:00
Jinjing f7c542c7a3 Keep profile saving alive after a stalled main loop (#25318)
* 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
2026-10-06 14:13:39 -07:00
Neil 40f03199f8 fix(native-chat): fingerprint the consumed transcript boundary (#25965) 2026-10-06 14:11:17 -07:00
Neil d6b155daf5 Fix Claude's stuck spinner after Escape cancellation (#25846)
* 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
2026-10-06 13:59:46 -07:00
Neilandsotashimozono 7e52bed4e0 fix(editor): recover PDF previews after failed refreshes
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>
2026-10-06 13:37:10 -07:00
NeilandChen 53edf8ec4b fix: recover deleted Active Server defaults and preserve editor hosts (#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 #21489. Historical session recovery remains separate work.

Co-authored-by: Chen <zwq19980411@gmail.com>
2026-10-06 13:33:14 -07:00
Jinwoo Hong 58b4633b58 refactor(codex): remove code the ~/.codex hook rework left unused (#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
2026-10-06 16:30:26 -04:00
Neil 83cf7cf5e2 perf(ci): compile release JavaScript once for all packaging hosts (#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
2026-10-06 13:22:04 -07:00
9e8a3a5c67 fix(repos): stop reporting clones that never ran as successful (#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 #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 #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>
2026-10-06 13:14:03 -07:00
Jinwoo Hong ac8ea9f958 fix(codex): write Orca's hook into ~/.codex only when something changed (#25743)
* fix(codex): write Orca's hook in ~/.codex only when something changed

One reconcile replaces the per-launch writer and its background approval
session. It runs at app start after PATH hydration, when the setting turns on,
once per native pane spawn, and (with a bounded 3 s wait) on Codex launches and
resumes. Orca's entry lives alone in a matcherless group, appended last unless
already in place; other copies are removed and the shifted user approvals move
verbatim under both key spellings. The approval, with Codex's own hash, goes in
first. The routing gate closes only for a hooks.json with a bad shape.

* fix(codex): report hook status for the home the next pane uses

Status reads ~/.codex (both key spellings) when launches use it, or the CLI
has no selection, and the selected managed home otherwise. It explains: update
Codex, Codex not found, not asked yet, approved by Orca but not yet confirmed
by Codex, a hooks.json shape that moves panes to Orca's own home, and inline
config.toml approvals Orca cannot add to.

* test(codex): pin the ~/.codex approval against a real Codex, and run it on those files

The contract now also writes Orca's entry into a throwaway ~/.codex and checks
that Codex lists it trusted and enabled, turns it back on over a /hooks
switch-off, lists it for review after a user inserts a hook ahead until the next
check, keeps an inline config.toml loadable, and shows no review in a real TUI
start. The real-binary CI job now runs when the ~/.codex writer changes.

* test(ci): keep the scope test under its line cap; assert the ~/.codex paths beside the contract job

* test(codex): type the hoisted test holders instead of asserting

* test(codex): cover the matcher rule, an older build's event, and the spawn trigger

Adds tests that failed against mutants which survived the first pass: an
entry alone in a matcher group, a current copy's approval in an event left to
an older build, one run per spawn, a spawn riding a running reconcile, and the
native-only spawn trigger. The opt-out's Codex-hash cleanup now runs once,
after the sweep, instead of twice.

* docs(codex): name the ~/.codex reconcile in the legacy sweep's lane comment

* fix(codex): approve ~/.codex with Orca's own hash while Codex's answer is pending

The reconcile waits at most 0.5 s for Codex's hash. If the lookup is still
running, an entry already in place keeps its approval and nothing is written;
a missing entry or approval gets Orca's computed hash, approval first, inside a
launch's 3 s wait, as main's did. When the lookup lands, the reconcile runs
again and Codex's hash replaces it.

* refactor(codex): one start for the hook lookup and the ~/.codex reconcile

startCodexHooks replaces the two start functions and takes the PATH wait that
the reconcile request's after option carried. A spawn reconciles unless one is
running, without a scheduled flag, launches call one reconcileCodexHooksForLaunch
on the shared withTimeout, and the reconcile alone reads the hooks setting. The
startup test now runs the ready phase instead of matching its source text.

* refactor(codex): plan ~/.codex with the shared Orca-hook pruner and approval reader

The planner prunes with removeManagedCommands, as the opt-out does (so a hook
that runs Orca's script through its args goes too), and returns a prune or
settle union. While Codex has not answered, the kept approval comes from the
shared per-event reader. The reconcile result is its outcome alone, a
concurrent save spends a pass of the one bound, the opt-out removes Orca's
approvals once, the approval-first writer takes the hooks path, and
getRealHomeConfigTomlPath gives way to getSystemCodexConfigTomlPath.

* fix(codex): ~/.codex keeps only an approval holding a hash Orca's entry may carry

* refactor(codex): name the ~/.codex hooks-file check for what it reports

* test(codex): the ~/.codex entry tests know Codex's earlier hashes, as the app's lookup does

* refactor(codex): the ~/.codex reconcile uses the lookup's answer names

* test(codex): ~/.codex status tests keep a Codex on PATH unless one is missing

* refactor(codex): one stopgap for both homes; the ~/.codex pass finds its own home, hash and user data

Also passes every approval to the approval-first writer, which already skips the ones in place.

* refactor(codex): the reconcile start owns the warm-up; no rerun-on-answer flag

The lookup start only records the hydrated PATH; one catch, one request type, and the app config is kept as given.

* refactor(codex): the ~/.codex pass returns nothing; tests read the files

Also makes the ~/.codex opt-out take Codex's hashes, as every production caller passes them.

* refactor(codex): the ~/.codex approval cleanup takes Codex's hashes

* test(startup): check the Codex hook start waits for PATH by behavior, not identity

* fix(codex): a status-hook problem never moves the system default off ~/.codex

* chore(ci): run the real-Codex contract when the ~/.codex reconcile changes

* docs(codex): the ~/.codex reconcile's refused branch covers every definitive refusal

* test(codex): start the failure-memo lookup with the reconcile's PATH-only start

* fix(codex): ~/.codex gets nothing while Codex is missing, a pending answer uses the saved one, a conversion waits for a run that writes, and status and the reconcile read one home and one Orca-hash rule

* test(codex): ~/.codex and managed-home status tests name their home; ~/.codex rewrites the backslash key Codex on Windows reads

* test(codex): the Windows upgrade test reads status for the managed home it installs

* test(codex): the real-TUI contract trusts its workdir by its real path, and its userData exists

* fix(codex): keep Orca's approval at every slot its entry holds in ~/.codex
2026-10-06 16:12:23 -04:00
Neil 6a71732879 Keep sidebar section collapse independent across execution hosts (#25944)
* fix(sidebar): scope lane collapse and reveal by execution host

* fix: consolidate sidebar reveal group key imports

* test: publish cancellation readiness after signal setup
2026-10-06 13:11:57 -07:00
Jinwoo Hong 2f001fe880 fix(remote-browser): host client browser pages again when a paired host comes back after an outage (#25827)
* test(remote-browser): reproduce a client-hosted browser tab left unavailable after a network outage

A paired desktop that loses its host for longer than the browser-host
lease grace (25s reset cut, or a 100s silent cut the host's heartbeat
reaps) tears down its client-hosted pages and never hosts them again,
leaving 'Client-hosted browser unavailable'. Red on main.

Extends the freezable TCP proxy with a full silent or reset link cut.

* fix(remote-browser): host client browser pages again when a paired host comes back after an outage

When the network between a paired desktop and its Orca host dropped for longer
than the browser-host lease grace, the desktop tore down its client-hosted
pages and never offered to host them again, so the tab stayed on 'Client-hosted
browser unavailable' until the app was relaunched.

Regaining contact with a runtime now re-claims browser hosting the same way a
runtime restart already did: if this desktop still has client-placed pages for
that host, it starts its browser client host again and the host's existing
recovery re-places them, reloading at their last URL. The notice now says the
host is offline (and that the page reloads when it reconnects), names another
desktop only when the placement belongs to one, and otherwise says the page is
not available here right now.

* fix(remote-browser): claim offline only for a host that can reconnect, and re-host from the notice

The unavailable notice said the host was offline for any null status, which
also covers a retired or refused pairing and a host whose runtime did not
answer. It now uses the host connection state and excludes revoked verdicts.

When the control link returns while the old browser host is still inside its
lease reconnect, the contact-edge ensure is a no-op; if that reconnect then
fails, nothing re-hosts. The notice now re-claims hosting on mount while the
host is reachable, which is exactly that stranded state.

Plain regained contact no longer queues a second preparation behind one
already in flight; only a runtime restart does.

* fix(remote-browser): re-prepare browser hosting on every regained contact

A preparation still in flight when contact returns started while the host
was unreachable, so it is the one most likely to fail. Coalescing the
contact edge into it dropped the only trigger that could succeed and left
the page unavailable. Always queue one follow-up preparation instead.
2026-10-06 16:02:29 -04:00
Brennan Benson a0a7535b63 fix(native-chat): say each chat outcome once and drop internal-only surfaces (#24595)
* fix(native-chat): say each chat outcome once and drop internal-only surfaces

After a restart, resuming a chat reported the same outcome in up to four
places: a toast, the chat's own note, the status bar entry and the resume
dialog. Success also toasted, and a lost answer or unconfirmed dismissal
raised toasts of their own. The chat's note and the status bar entry (which
opens the dialog listing every affected chat) now carry it alone, and the
resume guidance no longer talks about which process "owns this session".

Provider events Orca does not recognise and that carry no words of their own
are logged instead of journaled as raw "codex · notification:foo" rows. Rows
with the provider's own sentence, failures, and records of replies Orca
already sent stay.

The approval card no longer shows the matched ask rule, its settings source
or a "Blocked path" line; it keeps the request, its detail and the reason.

* fix(native-chat): store wordless provider events, hide them only when drawn

Dropping text-less provider events on the host also dropped Codex's
turn/plan/updated row, which the task list (checklist and composer progress
panel) is built from. The host now journals these rows exactly as before,
and desktop and mobile chats leave out a row whose only text is the host's
"provider · kind" label. Failures, notices, rows with the provider's own
sentence and records of requests Orca answered still show. A hidden legacy
Codex collab-call row still counts as the agent acting for which subagent
section stays open.

Claude's local slash command output is read from `content` for that one
event kind only, so it shows as the row's text instead of disappearing.

* fix(native-chat): rows no client draws never fill the Codex cap or split a turn's work

- Codex per-turn cap: a wordless provider row (decided by the same shared rule the clients use)
  has its own silent storage bound, so it no longer crowds out rows that draw and never produces
  a "N more provider notifications not shown" line about rows nobody sees. Plan updates are
  exempt from the cap, so the task list keeps updating after a noisy turn.
- A Claude local command's row an older host stored with only its label (/usage output in the
  payload) stays drawn instead of disappearing.
- A settled turn whose only extra row draws nothing offers no expand arrow, and a wordless row no
  longer splits one tool run into two.

* fix(native-chat): a resume request that never reached the chats says so once, with Retry

When the resume request fails before the host reserved anything (the host is not ready, or its
recovery record cannot be written), the host has nothing to record, and the chats used to fall
silently back to "N chats to resume". The status bar and dialog now show them as failed, with the
existing guidance and Retry, until Retry, Dismiss, a host answer that no longer offers them, or a
relaunch. Comments that still cited the removed toast are corrected.

* fix(native-chat): keep mobile tool runs whole and a failed Dismiss's chat marked

- Mobile draws no task list, so it now drops wordless provider rows, plan updates included,
  before folding tool runs; a mid-run plan update no longer leaves two run headers back to back.
- A Dismiss the host never took keeps a lost resume request's chat shown as failed instead of
  turning it back into a plain offer. The lost request's real error now goes to the renderer log,
  since the row carries this side's own reason code.
- Comments corrected: the Codex cap no longer claims dropped wordless rows stay countable, and
  the resume comments cover a chat a lost request never reached.

* fix(native-chat): tell the user what their Resume click did, in one toast

Clicking Resume in the dialog (or Retry on a row) closes the dialog and acts on
chats that are mostly off-screen, and nothing said what happened. One toast now
summarises that click: "Resumed N chats and asked them to continue", "N chats
couldn't be resumed" with Show (opens the dialog), "Couldn't confirm N chats were
resumed", or the mixed case in the same toast. A request lost before it reached
the chats counts its chats as not resumed. An opted-in automatic resume at launch
raises no toast; dismissal and other bookkeeping stay silent.

* fix(native-chat): Show on the Resume toast re-reads before opening; fix toast translations

Show could open nothing when the host retired the failure while the toast was up,
because the dialog draws nothing without rows and the request stayed latched. Show now
goes through the status bar entry's re-read-then-open path, moved into the store so
both use it. The French, Japanese, Korean and Chinese "Resumed N chats and asked them
to continue" strings are rewritten to match their sibling strings. Tests pin Show on
the toast of a lost resume request whose chats the re-read still lists.

* test(native-chat): click the Resume toast's Show through a typed helper

The tests reached sonner's action handler with Reflect.apply, which the anti-slop lint
rejects. One test-support helper now returns the last toast's Show as a typed click.

* fix(native-chat): name the path an approval needs when the request doesn't show it

The approval card dropped Claude's blocked path along with the ask rule, so a
request like `git push` that needs access to `.git/config` no longer said what
it touches. Bring the path back as "Needs access to: <path>", only when the
description, reason or request detail doesn't already show it (verbatim or
JSON-escaped). One shared rule serves desktop and mobile; the ask rule stays out.

* fix(native-chat): count a blocked path as shown only when the text names the whole path

'/srv/data' inside '/srv/data-old/x' hid the "Needs access to" line although
the path itself was never shown. A match now needs the next character to end
the path: end of text, whitespace, a quote or a separator. Also pin that the
desktop approval mapping passes the blocked path through and drops the ask rule.

* fix(native-chat): the automatic resume at launch gets the same one outcome toast as a click

An opted-in launch resume now calls the same announce as a clicked Resume, with the same copy:
how many resumed, how many couldn't (with Show), unconfirmed, or mixed, said once. Dismiss and
bookkeeping stay silent; a launch whose chats all moved on raises nothing.

* fix(native-chat): the resume success toast says "Resumed N chats"

Each chat's own note already says Orca asked it to continue, so the toast drops "and asked
it/them to continue" in every locale; the couldn't-resume and couldn't-confirm counts and Show
are unchanged.
2026-10-06 12:59:32 -07:00
81a1968bc6 feat(native-chat): attach several files from one picker selection (#23956)
* feat(native-chat): attach several files from one picker selection

Picks over 256 files are rejected whole, matching the drop limit.

Fixes #23905

* refactor(native-chat): name the composer action for the files it now attaches

The hook still returned `pickAttachment` after it started attaching a
whole selection. Also records why the plural IPC handler sits next to the
singular one, matching its siblings in the same file.

* test(native-chat): cover the paperclip forwarding every picked path

The one line that makes the feature work — the composer handing the whole
picker result to attachExternalPaths — had no test; reverting it to
paths[0] kept every existing test green. Adds a hook test for the
multi-path, canceled, and wrong-pane cases, plus the missing canceled
case for the shell:pickAttachments handler.

* test(native-chat): exercise multi-pick through renderHook

* test(settings): preserve label fallback and verify Spanish copy

Keep latest main's missing-label fixture and restore its translation namespace after cleanup, so later tests still load real catalogs. Independently assert the shipped Spanish resource and rendered supported-agents label.

---------

Co-authored-by: Neil <neil@stably.ai>
Co-authored-by: Neil <4138956+nwparker@users.noreply.github.com>
Co-authored-by: Brennan Benson <79079362+brennanb2025@users.noreply.github.com>
2026-10-06 12:31:51 -07:00
Jinwoo Hong 4c35516150 fix(ci): restore main's typecheck and lint after #25751 and #24369 (#25949)
A test still called isStructuredAgentSessionThinking with a bare array after
#25751 changed it to take { items }, and #24369 left the session host one line
over its max-lines budget. Both fail CI on every PR.
2026-10-06 15:24:03 -04:00
Brennan Benson b922342199 feat(native-chat): the chat reads "Stopping…" from your Stop until the turn actually ends (#24369)
* test(native-chat): a Stop over a card sent now into the running turn keeps it paused

Red on main: Codex's turn end withdraws the steered hand-off and the queue
sends the card again as a new host turn, with no pause recorded.

* fix(native-chat): a Stop's queue pause holds a card whose hand-off is still unanswered

A card sent now into the running turn was still pending when Stop judged the
pause, so nothing was recorded; the interrupt then withdrew the hand-off, the
card went back to waiting unpaused, and the queue sent it again as a new turn.
The pause now counts a hand-off that may still return to waiting, judged with
the appended row applied, so a withdrawal lands under the pause and an
acceptance retires it in that same write. Codex and Claude both hit it.

* test(native-chat): the Claude re-send case fails on its diff, inside the test's budget

* test(native-chat): a pause held by an unanswered hand-off ends on every path that ends it

The provider's answer, the provider dying, the chat closing, a restart, a
withdrawal still owed at open, and a /clear (refused until the hand-off ends,
then carrying every waiting card paused 'cleared'); each ends with the queue
sending again.

* fix(native-chat): narrow the pause's settled hand-off, and assert the queued receipt's card

* 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 (5ead1f6bcc) 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.

* 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): read the outbox reconcile from where main moved it

* 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.

* 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 NativeChatComposer within its line limit after the Stop props and
main's /context answer both landed in it.

* refactor(native-chat): name the dictation hook for what it owns now

* fix(native-chat): a Stop's note says it took once a joined close proves the exit

A session-ending Stop whose child's end failed revises its note to
'unconfirmed'. Since main's #24862, the next Stop joins that close rather
than stopping again, and wrote no note, so a close that then proved the
exit left 'unconfirmed' under a turn that ended. The close now carries
the note it settles, and its proven exit revises it to 'Cancellation
requested.'. A join that fails again leaves it unconfirmed.

* fix(native-chat): a proven turn end says a Stop's unconfirmed note took

Replaces the note carried on the child's close. Every settlement that
ends turns interrupted on a proven exit, live or after a crash, also
revises an unconfirmed Stop note on those turns to 'Cancellation
requested.', found by the note's turn scope, in the same batch. A note a
Stop wrote before its turn showed is re-keyed onto the running turn when
it becomes unconfirmed, in one batch, so that end finds it. Known limit:
with no turn open yet, the note keeps its key and no turn's end revises
it.

* test(native-chat): a Stop's unconfirmed note says it took when the agent exits on its own

* test(native-chat): name the Codex handle as main's opaque handle does

* test: restore the provider handle import the main merge dropped

* fix: derive Stop note wording from interrupted turns

* test: name the raw replay case for what it covers

* refactor(native-chat): move the waiting-slot split into its own hook

* test: follow main's chat font-size rename in the stopping tests

* test: follow main's single live-line value in the Stopping tests

* test: give the android live-line fixtures the stopping field

* chore: keep the session host under its line limit after the main merge

* chore: keep the composer test and the phone chat view under their line limits after the main merge

The Stop control now disables itself while Stopping, so the composer passes
the flag through and its test file stays as main has it. The phone chat
header's Stop moves to its own component.

* perf(native-chat): read a Stop note's fields before parsing its key on every snapshot

Every snapshot projects each item through the Stop-note read, and each new
snapshot rebuilds the index of Stop notes by turn. Both parsed every item's
key first; they now check the row's kind and turn scope (and, for the
projection, its unconfirmed-stop failure) before the parse. Every Stop note
is a status row, so what each finds is unchanged.
2026-10-06 12:16:29 -07:00
Brennan Benson 19661dfca6 fix: stop Claude and agent helper processes when Orca quits or crashes (STA-9254, 1 of 2) (#25715)
* fix(supervisor): add a one-shot lifetime that runs past stdin end, and relay a provider's last output

A one-shot CLI (claude -p, codex exec -) reads its request until stdin ends, so the
supervisor's session rule (stdin end means the owner is closing) would stop it about
1 s into its answer. The one-shot lifetime passes stdin end through and leaves only
the owner-death watch and explicit signals to stop it.

The supervisor also exited as soon as its provider did, dropping output still in the
pipes when the owner reads slowly. It now waits, bounded, for the provider's output
to be relayed before exiting.

* fix(text-generation): run agent one-shots under the provider supervisor on POSIX

The Claude model-list probe, model discovery for every agent, and commit message,
pull request and branch name generation all spawned the agent CLI as a plain child
of Orca. If Orca quit, crashed or was killed while one was running, nothing stopped
it, and a CLI that hung kept running after Orca was gone.

They now run under the provider supervisor that native chat already uses, in its
one-shot lifetime, so Orca's exit stops the agent's whole process group however
Orca exits. A timeout or cancel asks the supervisor to stop (SIGTERM) and only
tears the tree down once it has had its full stop time; killing the supervisor
first would orphan the agent's group. A missing binary now reaches Orca as the
supervisor's exit 127, which is mapped back to the existing not-found message.
Windows and WSL keep spawning the agent directly.

* fix(supervisor): carry the provider argv on the supervisor's argv, map every spawn error back, and share one stop ladder

- The supervisor read the provider's command and arguments from one base64 JSON env string.
  Linux caps a single env string at 128 KiB, so an argv prompt of about 90-120 KiB, which
  passes the 120 KiB per-argument guard, failed execve with E2BIG. The provider argv now
  follows the supervisor script's '--' as real arguments; the env keeps only small fields.
- A supervisor that cannot start its provider reports Node's spawn error line and exits 127.
  That line now becomes the same error a direct spawn emits, so ENOENT still reads as
  'not found on PATH' and EACCES or any other spawn error reads as 'failed to start'.
- stopSupervisedProvider is the one ask, wait and force ladder: it gives a supervisor its full
  stop time before forcing. Agent one-shots, the Codex app-server close and the Claude child
  exit proof now share it, with the same requests, bounds and forced steps as before.

* fix(supervisor): report every provider spawn failure on one marked stderr line

Node throws most spawn failures (ENOEXEC, ENOTDIR, ELOOP, EPERM, ...) instead of emitting
them, and the supervisor had no catch, so it died with exit 1 and a stack trace that the
user saw as the agent's failure. A thrown or emitted spawn failure now exits 127 with one
marked line carrying whether it was thrown, its code and its message. Orca reads only the
last stderr line, so a runtime warning printed earlier cannot hide it, and maps it to the
message a direct spawn gave: thrown is 'could not be started', ENOENT is 'not found on
PATH', and any other emitted error is 'failed to start'.

* fix(supervisor): keep the user's Node options away from the supervisor and hand them to the provider

The supervisor runs Electron in Node mode, which honours NODE_OPTIONS and
NODE_REPL_EXTERNAL_MODULE. A user value such as a --require of a missing file stopped
the supervisor from starting, breaking even native CLIs like Codex that never load it.
The launch now takes both out of the supervisor's environment, carries them in its spec,
and restores them for the provider only, so a Node-based CLI still gets them.

* fix(text-generation): file a forced agent one-shot teardown under its own breadcrumb site

The forced tree teardown recorded every self-initiated kill as the Codex app-server's, so
a forced commit-message or model-discovery stop read as a Codex teardown in crash
breadcrumbs. The teardown now takes the caller's site; source-control stops pass the
site their Windows tree kill already uses.

* test(text-generation): cover the supervised stop under timeout and output limit; name the direct-child suites

The commit-message suites that drive fake children spawn them directly, the unsupervised
shape Windows and WSL use, so they now say so. The supervised POSIX stop gets its own
compositions: a timed-out Codex generation settles at once but holds the Codex home until
its supervisor has stopped (faithful fake, fake timers), and an agent that floods past the
output limit is stopped through its real supervisor with no process left behind.

* test(text-generation): run the direct-child suites on the Windows path and cover supervised discovery

The commit-message suites that drive fake children mocked the supervisor away on POSIX, so
they asserted a direct root SIGKILL that production no longer takes there. They now pin the
platform to Windows (with an empty PATH, so host installs cannot answer a bare agent name)
and assert the Windows kill, taskkill included. The three tests that check the host's own
discovery spawn shape run on the host and read the agent argv past the supervisor's '--'.
Model discovery gets its supervised composition: a timed-out Codex discovery settles at once
but holds the Codex home until its supervisor has stopped.

* fix(supervisor): show a supervised spawn failure in native chat as the spawn error it was

Native chat's exit errors carry the provider's stderr tail into Details. Under the supervisor
a missing CLI left the supervisor's internal spawn-failure report there instead of Node's own
'spawn <cmd> ENOENT'. The report, its parser and a display formatter now live in one module;
the Codex app-server and Claude stream-json exit errors pass the tail through the formatter,
which turns a report back into the spawn error and leaves any other stderr unchanged.

* fix(supervisor): report a spawn that failed without a pid instead of crashing on its missing pipes

When the provider spawn fails outright (EMFILE, ENFILE), Node emits 'error' later and leaves
the child with no pid and no stdio. Piping stdin into the missing pipe threw first, so the
supervisor died with exit 1 and a stack trace and never wrote its spawn-failure report. The
pipes are now wired only for a provider that started.

* refactor(supervisor): share the stop of a supervised child process

Agent one-shots stop their supervisor with SIGTERM through the shared stop ladder, watching
the child's own exit. That adapter moves into one helper beside the ladder, so other
supervised children can use it with their own stop request instead of a copy. The caller's
breadcrumb site still reaches the forced teardown. Same request, wait and force as before.

* fix(supervisor): give a session provider its stdin end and grace when its owner dies

The owner-death watch went straight to the group SIGTERM and cancelled any stdin-end grace,
so when Orca quit or crashed a session provider such as the Codex app-server never saw the
EOF that lets it finish writing its state (auth.json, the state database). A session whose
owner is gone now closes as an owner's stdin end does: the provider's stdin is ended, it
gets the stdin-end grace, and only then the SIGTERM and SIGKILL ladder. A one-shot already
had its EOF at the end of its request, so its owner's death still stops it at once.

* fix(supervisor): kill the rest of the provider group once the provider exits on a stop

A requested stop waited out the whole SIGTERM grace for the provider's group even after the
provider itself had exited, so a SIGTERM-ignoring helper it left behind held every stop for
up to 3 s. Under a stop, the rest of the group is now SIGKILLed as soon as the provider has
exited, the same rule its own exit already follows.

* test(text-generation): cover a supervised Codex discovery past its output limit

Model discovery's supervised stop was covered only under timeout. A Codex discovery that
floods past the output limit now runs through a real supervisor: it settles with the
too-much-data error, its agent is stopped through the supervisor, and the next discovery on
the same Codex home starts only after that agent is gone. The direct-child suites' headers
now list exactly the supervised cases that are covered.

* fix(supervisor): close a provider whose owner is gone the way its owner closes it

Owner death gave every session provider the stdin-end grace, so after an Orca crash a
Claude session, whose close is a stdin end plus SIGTERM, could keep working on its turn
for a second with nobody watching. The spawn spec now names the provider's close request:
'stdin-end' (the Codex app-server drains and exits on EOF, then gets its grace) or
'stdin-end-and-sigterm' (Claude; the default). A gone owner gets that same request. One
constant per provider feeds both its spawn spec and its owner-side close, through one
requestProviderClose, so the two cannot drift. One-shots still stop at once.

* fix(supervisor): keep the SIGTERM grace for a session's group after its provider exits

Killing the rest of the group the moment the provider exited under a stop also reached
native chat's closes, so an MCP server, a tool's child or a dev server still in Claude's or
Codex's group was SIGKILLed mid-cleanup instead of getting the rest of the SIGTERM grace.
The early group kill now applies only to one-shots, where the saved wait was the point;
a session's stop is back to waiting out the grace for its group.

* test(claude): pin that Claude's spawn passes its close request explicitly

The spawn-spec assertion matched the default close request, so dropping Claude's explicit
request still passed. The test now checks that the spec is built with the exit-proof ladder's
own constant.

* refactor(codex): give the Codex app-server close request its own module

Other Codex app-server spawns will name the same close request as the connection does.
Holding it in its own small module lets them import it without the connection itself.

* build(cli): list the Codex close request and the provider supervisor in the CLI project

The command-line build runs the short-lived Codex app-server session, which will name the
same close request as the Codex connection. Listing the close request, the provider
supervisor it takes its type from, and the spawn-failure report the supervisor uses lets the
CLI project typecheck that import without pulling the connection in.

* test(text-generation): check the ENOEXEC start failure only where Node reports one

On Linux, glibc's execvp hands an executable that is not a program to /bin/sh, so both a
direct and a supervised spawn run it and it exits 127; only macOS throws ENOEXEC. The
not-a-program case now runs on macOS only; the path-through-a-file case (ENOTDIR) still
covers a thrown start failure everywhere.
2026-10-06 12:15:49 -07:00
Brennan BensonandKelvin Amoaba 975e732ce3 feat(native-chat): a solid, fading Jump to latest (#25759)
"Jump to latest" was a faint, see-through pill that popped in and out. It is
now a solid button that fades in and out, can't be clicked or tabbed to while
hidden, and keeps focus in the composer when clicked.

Co-authored-by: Kelvin Amoaba <97001695+AmoabaKelvin@users.noreply.github.com>
2026-10-06 12:10:32 -07:00
Brennan Benson ab6389045e fix(native-chat): start Windows chats without reading process creation times (#25718)
* fix(native-chat): start Windows chats without reading process creation times

Native chat on Windows refused to start ("Orca can't run this agent in a chat
here") whenever the process-table addon could not report process creation
times. Chat never needed them; only the bookkeeping around stopping the agent
did.

Windows now follows the common pattern: Stop ends the agent's tree with
`taskkill /T /F` on the child Orca still holds, and reports the tree gone only
when taskkill exits 0. A saved pid is never signalled after a restart.

- Remove the Claude and Codex location gate and the Codex launch refusal.
- A start time that cannot be read is recorded as unknown instead of refusing
  the session; recovery already releases such an owner without signalling it.
- Delete Claude's Windows creation-time descendant snapshot and its verifier.
- Codex's Windows teardown reports the real taskkill outcome.
- The renderer no longer waits on the capability flag; the host keeps
  publishing it for older clients (temporary).

* fix(native-chat): treat a Windows Claude exit after stdin end as a proven close

On Windows an idle Claude leaves on its own once its stdin ends, so every Stop
and close read as unproven: live background work settled as unknown and the
trace logged a close that "did not finish cleanly". As with the Codex close,
that exit is now the close and Orca makes no claim about processes Claude
started; a forced close still rests on taskkill's own report.

Also drop the Settings clause about Windows needing process start times, and
fix comments and the tracked process-enumeration doc that still described the
removed start-time gate and descendant snapshot.

* fix(native-chat): renew a held child's lease without a PID probe

An owner recorded without a process start time could never renew: the renewer
re-proves every live owner by PID identity, and with no start time and no
spawn-token echo that probe is indeterminate. The lease's last renewal then
stayed at the spawn, so a turn cut by an Orca crash was dated to its own start,
and every tick logged a failed renewal and split the batch into one store
transaction per chat.

The runtime now renews a lease for a child it still holds at the record's
fence and whose exit it has not received, recorded as a `held-child` match.
Receipt of the exit ends that proof before the exit is settled, and a restart
holds no child, so a dead owner's lease still expires and restart adjudication
is unchanged. Records this runtime does not hold keep the PID probe.

Also correct the identity probe's comment about shipped addons and note that
recovery's stop ladder is POSIX-only.

* fix(native-chat): keep a failed Windows taskkill unproven across a retried close

A Windows close counts Claude leaving on its own after its stdin ends as the
close. That shortcut also caught a retried close whose first attempt forced a
taskkill that failed: once the root exited, the retry returned true and the
failure read as a proven close. The tree reaper now records whether a reap
ever reached the live root, here, on an earlier close or from a transport
failure, and the shortcut applies only when none did; otherwise taskkill's
verdict stands and no new taskkill runs against the exited root.

Pin the platform on the existing tests that assume the POSIX close, and say
what `exit-proven` means on Windows in the acquisition-failure docs.

* fix(native-chat): derive a held child's liveness from the adapter's own handle

Lease renewal trusted a held child until the host settled its exit, and the
host hears of an exit late: Claude runs its close ladder and a store write
first, unexpected exits wait on one delivery chain shared by every chat, and a
Claude close that cannot prove its tree publishes nothing at all. A dead root
could keep renewing through that window, so a crash in it dated the cut turn
late. Renewal now asks the adapter, which owns the process handle and sees the
exit first: a child is held only while it is on record at the lease's fence
and its adapter still runs that exact acquisition with no root exit seen. An
adapter that cannot answer falls back to the PID probe. The stored
exit-received mark is gone.

The held-child read is now required by the runtime state and the renewer, and
a host-level test drives renewal through the real host wiring.
2026-10-06 12:09:58 -07:00
Brennan Benson 3268760084 Add chat contrast and "Match terminal interface" (#25659)
* Add chat contrast and terminal interface matching

* Fix matching chat palettes and contrast controls

* Verify rebased appearance settings and preserve slider focus

* Memoize chat appearance and narrow settings subscriptions

* Wrap native chat tool-run summaries in the terminal font

* Use the matched chat foreground for inherited controls

* fix(native-chat): bound collapsed tool summaries

* Restore code foreground token for matched chat themes

* Reset all chat appearance controls through serialized writer

* Make terminal matching control chat size and contrast

* Verify terminal match controls on Chat settings page

* Share native chat appearance store selection

* Align matched chat palette test with full-strength foreground

* Keep tool-run header marks on the summary's first line

A wrapped two-line summary centered the leading icon, failure count, check,
and chevron between its lines. The header now top-aligns its items and gives
each mark a one-line-tall slot, so marks sit on line 1 and line 2 wraps under
the text.

* Explain terminal-matched chat controls on hover

While Match terminal interface is on, Contrast, Text size and Code text size
are disabled. Hovering any of them now shows a tooltip saying the chat is
matching the terminal interface and how to change it. Disabled controls drop
hover events, so an enabled wrapper carries the tooltip; with matching off the
controls render exactly as before.

* Name the tool-run alignment test case by summary length
2026-10-06 12:05:46 -07:00
Brennan Benson 61c9a3aeab test(mobile): split the turn-disclosure test under the 800-line test limit (#25933)
* test(mobile): split the turn-disclosure test under the 800-line test limit

* test(mobile): baseline the split-out journal cases, which carry the original file's existing type errors

* test(mobile): make the turn-disclosure tests typecheck (render a Result component, not a host string) and prune the baseline
2026-10-06 12:02:10 -07:00
Brennan Benson 07981bd12e fix(native-chat): chat copy that promised a retry, an update, or a coming change (#25157)
* fix(native-chat): chat copy that promised a retry, an update, or a coming change

- Subagent and background-task rows Orca can no longer settle read "status
  unavailable" ("2 agents with status unavailable"), not "no recent update",
  which implied an update was still coming.
- /compact or /clear refused behind an unconfirmed earlier message says "Wait
  for the agent to finish, then try again." There is no Retry for that message.
- The skills menu in SSH chats says skills can't be listed there; a skill typed
  by name still goes to the agent.
- A write the host refuses because this agent or location can't run as a chat
  says "Orca can't run this agent in a chat here." plus what didn't happen.
- Spanish gains the chat notice, background-task and subagent rows it lacked.

* fix(native-chat): whole count sentences for background-task states, and translation fixes

- Background-task header counts use one translated sentence per state
  (stateWorkingCount ... stateIdleCount), so Spanish and French can agree
  the state with the count; English output is unchanged and pinned by a test.
- es: subagent failed/idle/stopped counts and the waiting/blocked reasons
  read correctly for any count.
- fr: "listées" agrees with the feminine "skills" in the SSH skills line.
- ja: subagent status-unavailable count uses the 件 counter.
- Correct the refusal-table comment: hostUnsupported is also returned and
  recorded, not only thrown.

* fix(native-chat): es/fr background-task counts agree with any task kind

* revert(native-chat): leave the /compact unconfirmed-message refusal to its own PR

Restores main's settleEarlierMessage copy in every locale and drops its comment.
The refusal's behaviour, not only its words, is being fixed separately.

* test(native-chat): expect this PR's Spanish blocked reason in the tool-run language test
2026-10-06 11:52:35 -07:00
Brennan Benson defd85a9eb feat(floating-workspace): render the floating panel through the shared workspace surface (#22302)
* docs(floating-workspace): plan structured native chat in the floating panel

* feat(floating-workspace): resolve the floating workspace so a structured session can run there

* docs(floating-workspace): plan the unification onto the shared workspace surface

* chore(floating-workspace): keep the unification plan out of the branch

* test(floating-workspace): pin the structured route on capability, not workspace kind

The host half removed the 'floating-workspace' blocker; this pin still asserted it and went red
on the merge. Floating now routes like any workspace, so with capabilities un-negotiated the
resolver answers 'runtime-capability-unknown'.

* feat(floating-workspace): resolve the floating workspace's renderer row without a worktree record

The renderer had no Worktree row for the floating id: markdown creation, tab-bar path entry,
native-chat file links, attachments and model discovery all resolve a workspace through
getKnownWorktreeById and silently fail for it. The host already answers with a synthetic row
minted from the resolved floating directory; this gives the renderer the same single seam.

- The repo slice stores the host-resolved floating directory (floatingWorkspacePath); the panel's
  existing cwd resolution now writes it there instead of panel-local state, so there is one copy.
- findKnownWorktreeById mints the shared floatingWorkspaceToWorktree row from that path, local
  host only, cached while the directory is unchanged — mirroring the folder-workspace branch it
  sits beside.
- No consumer changes needed: every row-dependent path already goes through this resolver.

* feat(tabs): scope editor and browser activation to the owning workspace

Shared editor/browser activation always wrote global selection (activeFileId,
activeBrowserTabId, activeTabType). The floating panel is an overlay above the active
workspace, and its active tab must never become global selection — the invariant
documented in agent-auto-ack-targets.ts. Group-scoped activation is a prerequisite for
rendering the floating workspace through the shared tab-group surface.

- setActiveFile and setActiveBrowserTab take an optional target workspace, mirroring
  setActiveTabType: global selection moves only when the target IS the globally active
  workspace; per-workspace state always updates. Omitted keeps legacy behavior.
- The shared tab-group activation/creation commands pass their workspace, which is a
  no-op for the main surface (hidden surfaces are inert, so commands only fire on the
  active workspace) and confines floating activation to floating state.
- Terminal setActiveTab was already owner-scoped; now pinned by test.

* refactor(tab-group): separate the workspace drag scope and group tree from main-window chrome

Preparation for hosting the floating workspace on the shared surface, whose shell owns its own
titlebar tab strip and cannot mount the main chrome:

- WorkspaceTabDragLayer: the one dnd-kit scope for a workspace's strips and tree, extracted from
  TabGroupSplitLayout as a render-prop layer so a host can put shell-owned chrome (a titlebar
  strip) inside the same drag scope as the tree.
- TabGroupSplitNodeTree: the chrome-free recursive group tree, with an explicit tabStrip
  composition point on TabGroupPanel ('attached' keeps today's per-group strip; 'external' hosts
  render no per-group strip at all — a hidden one would register duplicate drag sortables).
- WorkspacePaneOverlayLayers: the retained pane hosts extracted from WorktreeSplitSurface so any
  tree host mounts the same pane lifecycle stack.
- TerminalPaneOverlayLayer gains ownsNativeChatToggleShortcut, following the existing rule that
  only one live workspace registers the chord.

TabGroupSplitLayout and WorktreeSplitSurface recompose these parts; the main surface's DOM is
unchanged.

* feat(floating-workspace): decide the panel's emptiness and layout atomically

An empty floating workspace has no tab-group layout at all (the slice starts empty and
hydration skips tab-less workspaces), while the shared group tree requires one. This model
resolves the two together: the panel shows its empty state exactly until the first tab exists,
and mounts the tree only with a layout plus a live focused group (stored focus, else the group
with an active tab, else the first group — matching the shell chrome's focused-group policy).

* feat(floating-workspace): render the floating panel through the shared workspace surface

The panel previously projected exactly one tab group and hand-rendered its panes, so any tab
that left that group — 'Move Tab to Split' creates a second group — stayed alive in state but
vanished from the panel. The body now mounts the shared group tree and retained pane overlay
stack for the floating workspace id, so every group renders, splits work, and structured chat
finally has a floating surface.

- One drag scope: WorkspaceTabDragLayer wraps the titlebar strip AND the body tree, so exactly
  one DndContext owns floating tab drag; strip drops reorder, body-edge drops split. The
  floating-only FloatingWorkspaceTabDragContext is deleted.
- The titlebar keeps the panel's single TabBar, now fed by the shared per-group workspace model
  of the focused group (tabStrip='external' keeps per-group strips out of the shell), with
  workspace-scoped activation commands so global selection never moves.
- Panes render through WorkspacePaneOverlayLayers: terminals gate on the resolved cwd and a
  settled viewport exactly as before; browser guests ride the shared retention layer; agent
  session tabs appear in the strip and render through the structured overlay.
- The empty state renders exactly until the first resolvable tab exists (stale unified entries
  still show it), per the atomic layout/emptiness model.
- Deleted: the single-group projection, the hand-written pane maps, FloatingBrowserSlot, and
  the floating pane-handle registry.

Known behavior deltas, both narrowing floating onto main-workspace semantics: Cmd/Ctrl+W with
panel (not terminal) focus closes the active tab rather than one pane of a multi-pane tab, and
a pane-owned close of the last tab no longer arms the panel focus reclaim (strip and shortcut
closes still do).

* test: re-register moved pane-ownership sites with the extraction

The retained overlay stack moved into WorkspacePaneOverlayLayers and the floating chrome model
became a guest-paint consumer; the structured-pane layering pin and the browser-guest retention
census both name files, so they follow the seam.

* fix(floating-workspace): mount the shared group tree in a flex frame so panes own their height

The tree's nodes size themselves as flex items, and the floating shell mounted
them in a block container, so every group body — and every pane anchored to
one via anchor-size() — measured 0px tall. Wrap the tree and overlay layers in
the same 'absolute inset-0 flex' frame the worktree split surface uses, record
the host contract on the tree, pin the frame in the panel's element-tree test,
and add a real-renderer e2e spec that measures group-body and pane boxes for
the single-group and split layouts (jsdom computes no layout, which is how the
0px regression passed 50k tests).

* fix(runtime): import the floating worktree minting the structured create path calls

floatingWorkspaceToResolvedWorktree called floatingWorkspaceToWorktree without
importing it. The module is @ts-nocheck (mechanically split runtime), so tsc
never saw the missing import and it only failed at call time — the structured
createSupport probe for the floating workspace threw a ReferenceError. Add the
import and a test that runs the real method, which fails on the missing import
where the typechecker cannot.

* fix(browser-pane): resolve floating and folder workspace paths in notebook navigation

navigateBrowserPageToUrl looked the workspace up in allWorktrees(), which has
no row for folder or floating workspaces, so their notebook navigations ran
with an undefined workspace path. Use getKnownWorktreeById, the same resolver
every other consumer was moved to.

* fix(tabs): keep a workspace's tab selection out of the main window's global selection

* refactor(floating-workspace): launch and close panes like every other workspace

* refactor(tab-group): let the surface that mounts tab groups describe its own chrome

* refactor(floating-workspace): render the floating panel through the shared workspace surface

* fix(floating-workspace): resolve the floating directory once on the host, and keep the controls' inset

* test(floating-workspace): pin the unified floating panel against the shared surface

* fix(floating-workspace): read the floating directory's paths through the app environment

The runtime now resolves the floating directory itself, and the headless
runtime has no Electron app, so the Electron path lookup threw there.

* fix(floating-workspace): keep the main window's frame and shortcuts out of the floating panel

The shared layout drew the main window's 4px titlebar band and sidebar seam
inside the floating panel, and the chat-view toggle fired in every workspace
on screen, so one key press flipped the main window's tab and the panel's.

* fix(floating-workspace): close floating tabs without taking over the main window

An unsaved floating note's save prompt made the floating panel the main
window's active worktree, and never appeared on a fresh launch because the
workbench that owns the prompt was not mounted. The prompt now shows a
floating note in the panel, and the workbench mounts while the panel holds
tabs.

Every close path now reads the same captured emptied-workspace reaction, so a
floating pane closing itself, its shell exiting, a group close, and an unsaved
note close all keep the panel's keyboard ownership, as a tab close already did.

* test(tab-group): pin that a dirty editor close carries its emptied reaction

* refactor(tabs): one rule for when a selection moves the main window's selection

Opening a file kept a floating-only exception while every other writer checked
whether the tab's workspace is the active one. Every writer now asks the same
question, so opening a file in a background worktree no longer points the main
window's editor selection into that worktree.

* refactor(floating-workspace): drop what the shared surface left unused

The terminal pane's imperative close handle lost its last consumer when the
floating panel stopped keeping its own pane registry, and the floating and
folder workspaces built the same lineage-free resolved worktree twice.

* test(floating-workspace): type test fixtures without casts and drop the removed pane ref

* fix(floating-workspace): keep panel focus when a close lands after the panel emptied

Saving the last floating note closes it after an awaited write, so the panel
could render empty before the close armed its keyboard reclaim, leaving the
intent armed with nothing left to consume it. The panel now re-checks whenever
the intent arms as well as when its tab count changes.

A pinned tab's confirmation took focus before the close read panel ownership;
the dispatcher now captures it before any prompt.

* refactor(floating-workspace): finish what the shared surface left behind

- The floating panel no longer accepts AI vault session drops: a session
  cannot resume into it, and its window-level drop handler fired beside the
  main window's.
- The last global-selection writers that spelled the active-workspace check
  inline now use the shared rule.
- Remove the save-dialog hook and item counter the old floating body used, and
  correct comments that said the workbench only mounts once a workspace exists.

* fix(floating-workspace): keep a floating leaf detach from changing the main window's tab type

* fix(floating-workspace): give floating chats their workspace identity and directory

* fix(floating-workspace): read one visibility fact for chat attention and auto-ack

* docs(floating-workspace): correct comments the shared surface made stale

* fix(attention): one on-screen rule for every tab's unread marker, main window and floating panel

* fix(agent-session): pin the folder a session launched in; floating chats resume there or refuse

* fix(native-chat): resolve a floating chat's file links, images and skills in its pinned folder

* fix(agent-session): open a floating chat's terminal handoff in its pinned folder

* fix(agent-session): surface refusal text on chat open, require the launch-directory rule, and keep bookkeeping from gating a launch

* fix(floating-workspace): wait for a floating chat's pinned folder and read panel visibility from the store

* test(floating-workspace): assert the toggle event without a cast

* fix(native-chat): show why a chat could not be opened instead of dropping the host's refusal

* fix(floating-workspace): close chat tabs through shared commands

* fix(i18n): remove obsolete floating editor loading key

* fix(floating-workspace): preserve tab ownership on activation and browser close

* fix(floating-workspace): keep terminal activation and focus reclaim in sync

* fix(readme): point translations at tracked media

* fix(floating-workspace): retain split surface with an empty focused group

* docs(tabs): describe workspace-owned activation accurately

* fix(floating-workspace): finish the main merge and name the pin a launch directory

Port what the merge left on main's newer shapes:
- The floating chat's "folder is gone" refusal becomes a typed reason,
  launchFolderMissing, worded like every other start refusal (copy, desktop
  words, five translations). Older clients drop the unknown reason and show
  the code's generic words.
- The PR's launch-folder tests run on main's journal-database record store;
  the attach-level case is a row in the pre-spawn refusal table.
- Tests main added since the PR's base learn the PR's close-queue settling,
  workspace-scoped notebook lookup and tab-stamped chat host.
- Move the store's visible-tab-index reads into the tab table, so the record
  store stays under the line cap with the pin writer in it.
- Drop a duplicate Worktree import the merge left in a @ts-nocheck file.

Rename the persisted field, the status summary field and the renderer slice
from workspacePath to launchDirectory: it records the folder a session first
launched in, not its workspace's path, and it has not shipped yet.

* Fix floating chat visibility, closes, and directory pinning

* fix(i18n): drop the floating save dialog's strings with the dialog

The floating panel now closes through the shared unsaved-changes prompt, so
its own dialog's six strings had no caller and failed the runtime-catalog
check.

* fix(native-chat): pin floating founders and retain pinned editors

* test: align auto-ack fixtures with unified tab visibility

* test: give the forget-status attach fixture a location

Attach now reads params.location to decide whether a new record is a floating founder, so the partial fixture needs the location real attach params always carry.

* test: type the forget-status attach fixture instead of casting it

The cast tripped the changed-code type-assertion gate.

* fix(native-chat): read a floating chat's model defaults in the folder it was created in

The model picker read the floating setting's current folder, so a chat
pinned to another folder could show that folder's project default.

* test(cross-version): give the stub host the record store the catalog read now checks

* test(claude): give main's thinking-display launch stub the store shape the resolver takes
2026-10-06 11:37:18 -07:00
github-actions[bot] e63e4f80da Update README downloads badge 2026-10-06 18:34:29 +00:00
Brennan Benson 128d154347 Fix Codex chat creation after its own model listing fails (#25754)
* fix(native-chat): keep Codex chat actions independent of catalog listing

* fix(native-chat): accept a Codex option pick before the model list arrives

- A live model, effort or Fast pick with no stored model list is kept as the
  next turn's intent instead of refused; Codex judges the model on turn/start
  and Fast without a known tier still sends Standard.
- Restoring saved Fast checks the saved model the turn will send, not the
  resumed thread's old model; the thread's effort is only reused for its own
  model.
- turn/start reads the exact Fast tier from the account's stored catalog at
  send time, so a tier any writer stored applies; the per-session copy is gone.
- Revert the delivery-loop extraction; that file matches main again.

* test(native-chat): pass the agent registry to the new acquisition attach tests

Merged main (#25159) requires agents on attach and an agent definition for catalog access; the PR's new acquisition attach tests and Fast-mode tests now pass them.
2026-10-06 11:23:38 -07:00
Jinwoo Hong 8d049b594d fix(codex): approve Orca's hook in managed Codex homes with Codex's own hash (#25742)
* feat(codex): ask Codex for its hash of Orca's hook in a throwaway home, cross-checked by position and path

* feat(codex): cache Codex's hook hashes per binary and version, asked one at a time and only by the app

* feat(codex): write a hook approval before its entry, and take back only its own on failure

* feat(codex): approve Orca's hook in managed Codex homes with Codex's own hash, written first

Managed homes (the shared mirror and per-account homes) no longer run a
background approval session. Status reads the home's files against Codex's
answer, and turning hooks off recognizes every saved version's hashes.

* feat(codex): managed homes approve Orca's hook with Codex's hash; drop their background approval

The previous commit carried only the managed resume's wait; this one holds
the managed install it relies on. Managed homes (the shared mirror and
per-account homes) write Codex's hash before the entry, fall back to their
own approvals when the answer is late, and strip Orca's entry only when
Codex itself answered with nothing to approve. Status reads the home's files
against Codex's answer, and turning hooks off recognizes every saved
version's hashes.

* feat(codex): only an Orca-launched Codex waits up to 3 s for the hook hash; warm it after PATH hydration

* feat(cli): name the file each agent hook status reports on

* test(codex): real-Codex contract for the derived hook hash in managed homes, on both pins and latest

* test(codex): type the hook-hash test fixtures and drop a duplicate import

* fix(codex): give a Codex launch its own install run instead of joining a plain terminal's

* test(codex): a user hook's approval stays put in an event Codex does not list

* test(codex): cover late answers, first-install mirroring, stale approvals and opt-out re-asking

* test(codex): a long managed home gets the daemon guard on its first install

* fix(codex): until Codex answers, approve a managed home's hook with Orca's own hash, as main did

A late, temporary or missing answer with no earlier approval in the home now
writes main's self-computed approval instead of leaving the hook out. Codex's
answer replaces it at the next install, a definitive answer (no hooks/list,
a refused cross-check, 0.128) never uses it, and status says the approval
is Orca's until Codex confirms it.

* test(codex): status flags an unapproved entry while Codex has not answered

* fix(codex): managed stopgap fills each missing event

Until Codex answers, a managed home kept only the events it had already
approved and dropped Orca's entry from the rest. Each event now keeps the
home's approval, else gets Orca's own hash, as main wrote every event. One
reader of the approval at Orca's entry serves the stopgap and status.

* refactor(codex): one Codex answer type, one in-process answer map, a disk-only memo

- One answer type with a kind (hashes, refused, pending) replaces two types
  and the three-field decoding at each caller.
- The lookup keeps one in-process answer per binary path, replacing the
  process memo, the global latest answer and the transient-failure map;
  status now reads the answer for the codex on PATH, not the last one asked.
- The memo file keeps Codex's refusals per version, like its hashes.
- Derivation takes the version it is given; one 30 s version-probe timeout.
- The launch wait reuses withTimeout, and launch prep passes launchesCodex
  down instead of a wait in milliseconds.
- Turning hooks off no longer forgets Codex's answer.
- Tests mock the derivation instead of a test-only resolver in production.

* chore(codex): list the approval reader for the CLI build; fold two identical scope checks

* fix(codex): count an approval at Orca's key only when it holds a hash Orca's entry may carry

* refactor(codex): one append for hook trust tables

* refactor(codex): the lookup keeps no entry for a missing Codex, and status checks the binary's fingerprint

Also names the lookup functions for the answer they return.

* refactor(codex): one stopgap reader for the managed home; the refused branch reads its own status

* refactor(codex): drop defaults and exports only tests relied on

* fix(codex): a failed ask of Codex stays pending instead of refusing its version

* chore(ci): run the real-Codex contract when the approval reader changes

* fix(codex): only a scratch home Codex loaded can refuse; the memo takes any hash and writes only on change

* fix(codex): hooks turned off during a launch's wait win, Off re-keys mirrored user approvals, and one rule says which hashes are Orca's

* fix(codex): an approval counts only under every key spelling Orca writes, as Codex on Windows reads only the backslash one
2026-10-06 14:15:35 -04:00