Skip to content

feat(native-chat): the chat reads "Stopping…" from your Stop until the turn actually ends - #24369

Merged
brennanb2025 merged 194 commits into
mainfrom
brennanb2025/stopping-state-on-b
Oct 6, 2026
Merged

brennanb2025 merged 194 commits into
mainfrom
brennanb2025/stopping-state-on-b

Conversation

@brennanb2025

@brennanb2025 brennanb2025 commented Oct 1, 2026 •

Copy link
Copy Markdown
Contributor
Files Added Deleted Net
Test 58 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​4662 $\color{#cf222e}{\Huge{\mathbf{−}}}$​97 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​4565
Prod 104 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​1660 $\color{#cf222e}{\Huge{\mathbf{−}}}$​300 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​1360

Step 2 of 2, stacked on #24864. Step 1, #24864, makes a Stop bind only the turn it actually stopped, decided when the Stop settles; this PR reads that binding to show "Stopping…". It also builds on the Stop event (#24072), its turn-end rule (#24311), one Stop note per turn (#24301) and #24334 (a Codex Stop that Codex refuses or never answers ends the Codex process), all on main.

Scope. This PR is the "Stopping…" state, the host holding any message sent while a Stop ends the turn, and what such a message looks like while queueing is off (as it is on every shipped host). Queueing a message sent while Stopping as a card, and how such a card runs past the cards a Stop paused, are split out to the draft follow-up #25041, which lands with the queued-messages capability rollout (#21062).

ELI5

When you press Stop in a native chat, the agent does not stop instantly. Orca asks it to stop, and it can keep printing for a few seconds before it does. Until now the chat said "Working…" the whole time, so it looked as if Stop did nothing. Now every place that shows the agent says "Stopping…" from the moment Stop takes effect until the turn has actually ended. The Stop button stays usable, so if a stop gets stuck you can press it again.

What Changed

The problem. After you press Stop, Orca sends the interrupt and, for Claude, ends the agent's process once a short grace period passes. Text can keep arriving during that time. The chat, the sidebar, the phone and orca worktree ps all kept reading "Working…" until the turn's end was recorded, so the Stop looked ignored.

What you see now

Where Before After
The chat's live line (desktop and phone) "Working…", or the agent's current activity "Stopping…" with the spinner, from your click until the turn ends
The chat's Stop button (desktop and phone) Enabled Held only while this device's own Stop request is waiting for its answer, normally well under a second. Then it is enabled again, so a second press can push a stuck stop through. On the desktop the button is an icon, so "Stopping…" is its accessible label, not visible text; the phone's button shows the word.
The sidebar row, compact and non-compact, and the agents dashboard row The last tool line "Stopping…" in its place; the dot keeps the working spinner. The compact row leads with "Stopping…", so a narrow sidebar cuts the chat name, not the status.
orca worktree ps --json mainAgent: { state: "working" } mainAgent: { state: "working", stopping: true }
The phone's worktree list The agent's last message "Stopping…"
The composer's placeholder (desktop and phone) "Send a message…" While the chat reads "Stopping…": "Send a message to run after the stop", or "Queue a message to run after the stop" where the host queues sends
A queued card's Steer (desktop and phone), and the desktop's Cmd/Ctrl+Enter steer Sends the message into the turn being stopped Unavailable while the chat reads "Stopping…"; the card stays queued and runs after the stop
A message sent while the turn is stopping (desktop and phone) Steered into the turn being stopped Held by the host and run as its own turn once the stopped turn ends. On the desktop it is drawn after the "Stopping…" line, not inside the turn being stopped, until the host hands it over. A message sent just before the Stop stays where the host puts it. With the queued-messages capability on, it becomes a queued card instead (follow-up #25041, with #21062).
A message an older client sends while the turn is stopping Steered into the turn being stopped Held by the host until the turn ends, then sent as its own turn (never a steer Codex would refuse). A second Stop hands it back instead: on the desktop a send's text returns to the composer, and a card's Send-now returns as a card.

"Stopping…" shows while your Stop is still being carried out, and then for as long as the turn it stopped runs: until the agent's interrupted result, the end the host records when it ends the agent's process, or a relaunch settling the turn. A Stop the agent declines, or that Orca cannot confirm, keeps "Stopping…" until that turn ends: the Stop is still yours, the note says what happened, and Stop stays enabled so a second press can push it through. A Stop that finds nothing to stop (you pressed it before Codex had opened the turn and the turn never opened) ends "Stopping…" as soon as it is carried out, so a message you send next goes out normally and a turn that opens later never reads "Stopping…".

A Claude Stop whose process end fails (the end times out, or the process can't be proven gone) while the turn is still running now says so: the Stop's note changes to "Cancellation was not confirmed.", the same row an interrupt that fails outright writes, and "Stopping…" holds until that turn ends. Before, the note kept saying "Cancellation requested." with nothing to show it had failed. Pressing Stop again retries the end. Once that turn has ended interrupted (the work stopped, however that was proven: a later Stop, the process exiting on its own, the agent's own interrupted result, or the host proving the process gone after a restart), the note reads "Cancellation requested." again.

A Codex Stop that Codex refuses or never answers ends the Codex process (#24334), so its turn ends and "Stopping…" ends with it.

The mechanism

  • The host works it out from fix(native-chat): a Stop binds only the turn it actually stopped #24864's binding. On every journal publish, and whenever a Stop's settle opens or closes, the host's status feed calls one function, structuredAgentSessionStopping (structured-agent-session-stopping.ts). It reads the latest Stop event (fix(native-chat): Stop's pause is worked out from the chat's history, so a steered message is never re-sent #24072), which must be a person's Stop or close, and what fix(native-chat): a Stop binds only the turn it actually stopped #24864 bound to it:

    • while a Stop that named no turn is still settling (from the moment it takes effect until the provider answers, or, for a Stop that ends the agent's process, until that end is done, proven or not), "Stopping…";
    • after that, "Stopping…" only while the turn it is bound to is the turn running now: the turn the Stop named, the turn its interrupt took, or the turn a Stop that failed could not stop. That last one is a display-only mark: no rule that decides how a turn ended reads it, so if that turn later dies on its own it reads "Failed after N", not as yours. When the Stop failed before any turn showed, the mark is the first turn that opens after it, as long as no message was sent (accepted by the host) in between: what was already in flight when the Stop failed;
    • a Stop that settles having stopped nothing is bound to no turn, so "Stopping…" ends there.

    Whether a Stop is settling lives in the host's memory, as fix(native-chat): a Stop binds only the turn it actually stopped #24864 keeps it, so a settle edge writes no row. It bumps a revision the status projection keys on, and a narrow settle-edge hook republishes the session's status and wakes the steer hold's handover. Nothing else: it is not a journal commit, so it delivers no history, counts as no activity for the idle sweep, and schedules no queue drain.

  • The Stop's note. It is keyed by the turn it stopped (fix(native-chat): every chat action press is its own action (re-land #23916 on main) #24301), so every press of a Stop on that turn rewrites one row, and the newest press's answer is the row, as on main. What the Stop did to the turn is carried by the turn's own label, bound at settle (fix(native-chat): a Stop binds only the turn it actually stopped #24864). A Claude Stop whose process end fails while the turn runs on revises the note it wrote, which it carries with it, to "Cancellation was not confirmed.", and keeps "Stopping…" on that turn (display only) until it ends.

  • "Cancellation requested." again once the turn has ended interrupted, worked out on read. Nothing extra is stored: the note keeps the failure it recorded, and the turn records its own end. When the host publishes journal state, a note reading "not confirmed" whose turn has ended interrupted is published as the ordinary "Cancellation requested." note, with the same identity, revision and position (journal-stop-note-projection.ts, called from the snapshot). That reaches every client through the forms they already read:

    • Live: a batch that carries a turn's interrupted end also carries that turn's note (agent-session-journal-batch.ts), so an open chat updates in the same frame. History pages carry the note alongside its turn's end.
    • Reconnect: when a client subscribes from a cursor, the host re-sends the notes it currently publishes this way, bounded by the existing item and byte limits (agent-session-stop-note-refresh.ts). That covers a client that dropped the live update for a note older than its loaded window, then loaded an older page captured before the turn ended (STA-9612; released clients do this). It writes no row and invents no revision.
    • Kept as a write: a Stop pressed before its turn showed writes its note before any turn is known. When its process end fails while a turn is running, the note is moved onto that turn (one atomic tombstone-and-write), because nothing else durably links it to the turn.
    • Rewind and raw replay read the stored rows, so they keep the recorded failure.
  • Published on the session status. The session summary gains an optional stopping: true, present only while its status is working. The main agent's row carries it as mainAgent.stopping, so the sidebar, the dashboard, worktree ps and the phone's list read it from the row they already read.

  • Clients only show it. The desktop chat reads the host's flag from the status stream it already subscribes to. The phone's chat now reads the same stream, agentSession.subscribeStatus, which this PR makes callable from the phone:

    • one stream per connection to a host, opened by the first chat that needs it and kept for that connection's life. The phone has no way to end only this stream, so opening one per chat would leave a stream behind on the host for each chat;
    • opened only on a host that advertises the status feed. A host from before this PR advertises the feed but refuses it to phones; the phone reads that refusal as "no feed" for that connection and asks again after the next reconnect, in case the host was updated meanwhile;
    • the stream's fold and its contact-loss rule moved to src/shared/agent-session-status-snapshot-fold.ts, so the desktop and the phone handle each frame the same way.
  • What the new phone method exposes. The stream carries each structured session's summary: its status, latest prompt, last reply preview, tool line, verdict and child work. The phone already reads all of that through the worktree ps agent rows and the transcript itself (agentSession.history). The summary also carries the provider's session id, which the phone already receives on its agent rows, and the host's bookkeeping for each session: which agent process is running, whether it has started, and the model in force. None of it is a path, an environment value or a credential.

  • One display status. One shared function, agentStopDisplayStatus, turns "is the agent working", the host's flag and this device's own in-flight Stop request into the display status stopping. The chat pane (live line and composer), the sidebar and dashboard rows, and the phone's chat all read it; none combines the flags itself. The phone's worktree list repeats the same test inline, because that file is loaded on its own by the cross-version tests and cannot import a value.

  • The press flag. Each chat also treats its own in-flight Stop request as "Stopping…", so the line changes on click, before the host's Stop event arrives. The flag lives only in memory, is never sent, and clears when the request answers or fails. It is also the only thing that disables Stop.

  • Nothing steers into a turn a Stop is ending. The host owns this rule. At the moment it would hand a waiting message to the agent, it reads its own "Stopping…" (the same reading the status feed publishes) and holds the message while it holds: a card's Send-now and a send made while the turn stops both wait, and go out as their own turn once the stopped turn ends, or as soon as a Stop that stopped nothing settles. The turn's end, and the settle's close, wake the handover again. A second Stop withdraws a held message, as it withdraws anything waiting. A card queued while stopping already waited for the turn to end. Clients only follow the host: while the chat reads "Stopping…" (the host's flag or the device's own Stop request), they disable a card's Steer and the desktop's steer shortcut, and the composer's placeholder says a message runs after the stop. A message sent while the chat reads Stopping is drawn after the "Stopping…" line until the host hands it over; one sent just before the Stop stays in the turn the host steers it into.

  • A Codex turn whose interrupt Codex answered is never steered. Codex answers an interrupt as the turn aborts, before it sends that turn's turn/completed. Until that end arrives, the adapter still counted the turn as running, so a message handed over right after the Stop settled (the Stop's own end row already reads the turn ended) went out as turn/steer, which Codex refused, and only then as turn/start. The adapter now marks a turn whose interrupt Codex answered as aborted, never steers into it, and opens the message's own turn directly. A steer into a turn that is genuinely running is unchanged.

  • Losing contact never leaves it up. When the status stream to a host drops, both clients remove stopping along with the host's ownership fields. When the host lets go of a session, it removes stopping from the summary it keeps. The phone's list now notices when stopping is the only field that changed.

Mixed versions

  • Old client, new host: stopping is an optional field it doesn't know, so it shows "Working…" until the turn ends, as today. A cross-version test runs the v1.4.214 summary fold and phone row reader against this host's output.
  • New client, old host: an old host never publishes stopping, so the chat shows only the press flag, then "Working…". A new phone on a host from before this PR gets a refusal for the status stream and treats it the same way. A cross-version test checks that v1.4.214 advertises the feed but refuses it to phones.
  • The steer hold: it lives on the host, so an old client on a new host is held too; its Steer button still shows, and a plain send still goes out at once rather than as a card, but the host holds it until the turn ends and then starts it as a new turn, never as a doomed steer. A new client on an old host has no host flag, so it holds Steer only while its own Stop request is in flight, and the old host still delivers a send into a stopping turn, as today.
  • SSH: the execution host works out the flag from its own journal; clients mirror it and drop it when they lose contact.

Why

  • "Stopping…" rather than jumping straight to "Interrupted". The agent can still stream for up to the grace period plus the process close. Showing "Interrupted" while text keeps arriving would contradict what you see.
  • The host works it out, not each client. Status has one owner, the host's store (docs/reference/agent-status-store.md). If clients worked it out, the desktop, the phone and worktree ps could disagree.
  • Worked out, not written. The inputs are the journal's Stop event and turn rows, plus fix(native-chat): a Stop binds only the turn it actually stopped #24864's settle, which the host holds in memory and which ends with the Stop or the host. Nothing new is written, and nothing can get stuck: the flag is published only on a working session, so "Stopping…" can never outlast "Working…", and after a host restart the agent's process is gone and nothing reads as stopping.
  • A declined Stop stays "Stopping…"; a Stop that found nothing to stop does not. The person asked for the turn to stop, and a refusal doesn't change that: the turn either ends, or a second press pushes the Stop through. A Stop that settles having stopped nothing has nothing left in force, so "Stopping…" ends; holding it would mark a later turn that the Stop never touched, and hold a message sent meanwhile for no reason.
  • Stop stays enabled while Stopping. Orca's host does not push a stuck stop through on its own. A repeated Stop is what does it: for a Codex /compact whose interrupt Codex took but never answered, the second Stop ends the agent's process; for Claude it retries the process end. An earlier draft of this PR disabled Stop while Stopping. A test that drove a /compact Stop that Codex never answered showed the chat stuck on "Stopping…" with a disabled button, and closing the chat was the only way out.
  • Nothing steers into a stopping turn. A message steered into a turn the person asked to stop either lands in work that is about to be cut off, or keeps a turn going that the person meant to end. Holding it on the host, rather than only hiding the button, protects every client version, and sending it as its own turn after the stop keeps what the person asked for.
  • The phone reads the host's stream rather than a field on its transcript stream. It is the same stream and the same field the desktop chat reads, so the phone and the desktop cannot disagree. A copy on the transcript stream would be a second carrier of the same fact. The phone already keeps its transcript through a disconnect, so a flag stored there would also outlive lost contact unless it were gated separately.

Differences from the common pattern

Only real departures in how the stop state is owned, delivered, kept or ended are listed here.

Difference Label
Lifecycle: Stop stays enabled while the chat reads "Stopping…". Only this device's own request in flight holds it. Intended. Orca already ends a Stop the agent refuses or never answers, by ending the agent's process (#24235 for Claude, #24334 for Codex). What is left is an interrupt the agent accepted but never finished, such as a Codex /compact; a second press is what ends it. Orca-observed: with Stop disabled, a /compact Stop that Codex took but never answered left the chat on "Stopping…" with no way out but closing the chat.
Shipped behaviour while the queue is off: a message sent while stopping is sent plain and held by the host, the placeholder reads "Send a message to run after the stop", and the desktop draws that message after the "Stopping…" line until the host hands it over. The common pattern queues it as a card. Temporary. Shipped hosts do not advertise the queued-messages capability yet; #21062 turns it on, and with it the follow-up #25041 makes the message a card at once with the "Queue a message…" placeholder.
"Cancellation requested." after a failed Stop is worked out when the host publishes the journal, and the recorded failure stays in storage. Intended. Interrupted turn ends are written by independent paths (the Claude and Codex translators, the Stop settlement, the dead-generation and stale settlements, restored history), and the turn records its own end. Reading the note against its turn's current end gives one rule for all of them, writes nothing, needs no repair for chats written by older builds, and released clients show it with no change, because they print the published text.
The host re-sends these notes when a client subscribes from a cursor. Temporary. It makes up for a client reducer that can keep an older page's copy of an item over newer state (STA-9612). Once that is fixed in clients, this refresh can shrink.
Ownership: stopping is an optional flag on the working status, not a new status value. Intended. Constraint: clients and hosts update independently, and an older client drops a status value it does not know. As a flag on working, every older client, every status reader and every dot keeps reading the turn as working, which it still is; only readers that know the flag show "Stopping…".

Follows the common pattern: "Stopping…" is worked out by the host that runs the agent, from what it already holds, and is never stored on its own; it runs from the Stop until it settles; a Stop that settles having stopped nothing ends it, and a failed one holds it until the turn ends; a message sent while the turn stops runs after the stop, never steered; where the host queues sends, the composer's placeholder reads "Queue a message to run after the stop"; Steer is unavailable while stopping; the chat reads its own in-flight Stop request as "Stopping…" until the host's flag arrives; and a second Stop hands a held message back.

Behaviour notes

  • The turn header keeps its running "Working for Ns" timer while the live line reads "Stopping…", and settles to "Interrupted after Ns" when the turn ends. The turn is still running while it stops; the stop shows on its own line and in its own note.
  • The tab dot and the sidebar dot keep the working spinner. A stopping turn is still running, and a new dot state is one that every dot reader would have to learn.
  • The compact sidebar row puts "Stopping…" first, as it already does for "Monitoring", so a narrow sidebar cuts the chat name and never the status.
  • On the desktop the Stop button is an icon, so "Stopping…" is its accessible label, not visible text. The phone's button shows the word.
  • While the chat reads "Stopping…", a queued card's Delete and Edit still work.
  • Live QA (M4 Air, stub Codex in Codex's real order, real Claude):
    • A Stop pressed before the host has the message (Claude about 77 ms after the send) returns the not-yet-sent text to the composer. That is the existing "a Stop withdraws what was not sent" behaviour, not a change here.
    • Real Claude stops in about 65 ms, so "queue while stopping" with Claude could not be exercised live; it is covered by tests.
    • One QA flow used an interrupt answer arriving long before the turn's end, an order real Codex does not produce; it is not a supported case.
    • A Stop pressed before any turn opened whose process end fails keeps "Cancellation was not confirmed." even after the work stops: with no turn known, nothing links the note to the turn that later ends.
    • A client that loads an older page captured before the turn ended keeps that page's "not confirmed" text until it reconnects (STA-9612).
    • A newer client talking to an older host still sees "not confirmed": the older host publishes the stored wording.
    • A restored finished turn's header keeps the accessible name "Agent is responding" while its text reads "Interrupted after Ns". That predates this PR (the header component is untouched here) and is left for a follow-up.

Linked Issue

Part of the Stop-event plan (#24072 and #24311, both merged). Step 2 of 2; step 1 is #24864.

Visual Proof

Round 17, the current head 970c6a1, after the main merges (desktop on a separate Mac, background rig). It covers the four things those merges could have touched: a normal Stop, a Stop the agent never answers, a send while stopping, and rewind next to Stop.

  • Codex: Orca's real Codex adapter, with the stand-in Codex behind it.
  • Claude: the real Claude CLI behind Orca's real Claude adapter.
  • How a Stop was made to go unanswered: the agent's own process was frozen just before the press.
Normal Stop: "Stopping…" while the agent winds down Unanswered Stop: "Stopping…" held 20 s in Unanswered Stop: turn ended
r17-A1-stopping r17-A2-stopping-20s r17-A2-interrupted
Sent while Stopping: drawn after "Stopping…" That message runs as its own turn Rewind: the stopped turn After rewinding to before it Next send
r17-A3-sent-while-stopping r17-A3-own-turn r17-A4-interrupted r17-A4-after-rewind r17-A4-next-turn
  • Normal Stop:
    • Codex: "Stopping…" from the press and through the agent's 8 s wind-down, then "Interrupted after 11s".
    • Claude: "Interrupted after 12s"; Claude answered the Stop quickly.
    • Both stored one "Cancellation requested." note under the turn. It sits inside the folded turn, so these shots don't show it.
  • A Stop the agent never answers:
    • Codex: the agent's process was frozen, then Stop pressed. "Stopping…" held through Orca's 30 s wait, then "Interrupted after 37s". Orca ended the frozen process itself.
    • Claude: the same steps. After Orca's 5 s wait, "Cancellation requested." appeared under the still-running turn, then "Interrupted after 21s".
    • What this did not reach: in both, Orca's kill proved the process gone before the note was written. So the stored note was "Cancellation requested." from the start (journal rows, Codex seq 22–23, Claude seq 38–39), never "Cancellation was not confirmed.".
    • The read-time correction only runs when a forced kill cannot prove the process gone. No real process could be made to do that here, so that path is covered by the adapter tests that make the kill fail, and by the cross-version tests.
  • Send while stopping: the composer reads "Send a message to run after the stop", and the message is drawn after "Stopping…". The agent received it as a new turn 7 ms after the stopped turn ended, never as a steer into it.
  • Rewind next to Stop: rewinding to before the stopped message removed exactly that turn (the agent was told to drop only it) and put its text back in the composer. The next send ran normally.
  • Claude screenshots: they captured only the middle of the long reply, so they are not used. The Claude results come from the page's own text at each moment, plus the stored journal rows.

Live QA on a separate Mac against background builds of this branch, driving a native Codex chat backed by a stand-in Codex that can accept a Stop slowly, or open a turn never. Desktop: every case passed. Phone: not run (see Testing). The flows that check the never-started send are in #24864.

A normal Stop the agent takes while it keeps printing (round 9, head e1d45a1; nothing on this path changed since). After the press the chat line and the sidebar agent row read "Stopping…" while text still arrives. The turn header keeps its "Working for Ns" timer until the turn ends, then reads "Interrupted after Ns". "Cancellation requested." is recorded under the stopped turn and stays folded with it, so it is not shown here.

While streaming Right after Stop Sidebar row while stopping Turn ended
r9-01-streaming r9-01-right-after-stop r9-sidebar-one-agent r9-01-ended

A message sent while Stopping (round 10, head 4888740). The composer reads "Send a message to run after the stop". The message is drawn after the "Stopping…" line, not inside the turn being stopped, and nothing is pushed into that turn. Once it ends "Interrupted", the message starts its own new turn.

Right after Stop Sent while Stopping Stopped turn ended, new turn starts
r10-04-right-after-stop r10-04-immediately r10-04-stopped-ended

QA also saw: the "Enjoying Orca?" pop-up on a first new chat can sit over the Stop button and catch the first click (not from this PR; that pass was set aside).

Testing

  • I manually tested these changes locally (live desktop QA on a separate Mac, see Visual Proof; phone not run)

  • Automated tests added/updated

  • Host, through the real host and its status feed (structured-agent-session-stopping.test.ts, structured-agent-session-status-feed-stopping.test.ts, structured-agent-session-stopping-journal.test.ts). These cover:

    • Stopping from the Stop until the turn ends, including before the agent answers the interrupt;
    • Stopping while a Stop that named no turn settles, and Working once it settles having stopped nothing, with no row written after it; a turn that opens later stays Working;
    • Stopping kept until the turn ends after a refused or unconfirmed Stop, and through the turn a Stop pressed before the turn showed failed to stop, whose own end then reads as a failure, not the person's;
    • a settle edge republishes Stopping and wakes a held send without counting as activity;
    • a Stop pressed before the turn showed whose interrupt took ends that turn at the settle, and Stopping with it;
    • a Codex Stop that Codex refuses or never answers ends the agent, and Stopping ends with the turn;
    • "Cancellation was not confirmed." after a Claude Stop's process end fails while the turn runs, with Stopping kept and the next Stop retrying the end, including for a Stop pressed before its turn showed;
    • each settle edge tells the journal's readers though it writes no row;
    • never on a later turn, or on a message sent after the Stop, including before that message's turn opens;
    • attention (not Stopping) while a prompt waits on the person;
    • not for a host's own stop;
    • dropped when the host lets go of the session.
  • "Cancellation requested." once the turn ended interrupted (agent-session-stop-note-projection.test.ts; shipping-adapter scenarios in structured-agent-session-claude-stop-note-end.test.ts and structured-agent-session-codex-stop-note-end.test.ts: a late process exit, a second Stop, a send, and the agent's own interrupted result). The snapshot and the live stream both read "Cancellation requested.", the stored note keeps its failure, revision and position, and no row is written. Completed, running, unverifiable and missing turns, and other failures, stay unchanged. Pages in both arrangements, byte-bounded catch-up, and the reconnect refresh are covered. Rewind keeps the stored failure (structured-agent-session-rewind.test.ts). With the read rule switched off, 16 of these fail, including all eight adapter scenarios.

  • Released clients (tests/e2e/cross-version-wire/): v1.4.219 and v1.4.220, with their own reducers and transcript code, render "Cancellation requested." from a page and from a live batch through the real host paths, and recover the stale-page case on reconnect.

  • Send while stopping, queue dark: the composer reads "Send a message to run after the stop" (desktop and phone), the desktop sends plain and marks only a send made while the chat reads Stopping (use-structured-agent-session.queue-while-stopping.test.tsx, real outbox; a send made just before the Stop is not marked), and draws only that message after the "Stopping…" line until the host hands it over, while one sent before the Stop stays in the turn (NativeChatMessageList.stopping-turn-bar.test.tsx). Each was checked by breaking it.

  • No steer into an aborted turn: with Codex answering the interrupt before the turn's end, a message sent behind the Stop opens its own turn with turn/start and no turn/steer (structured-agent-session-codex-interrupt-order.test.ts).

  • Steer while stopping (structured-agent-session-steer-while-stopping.test.ts): a card's Send-now and a send made while a Stop ends the turn, including a Stop the agent declined, are not handed to the agent, and are sent as their own turn once the turn ends; the same holds for a send a Stop overtook while its handover waited on the agent's start; a send made after a Stop pressed before the turn showed is handed over once that Stop settles having stopped nothing, with no "Stopping… → Working… → Stopping…" flip when the turn then opens; and the hold adds no journal read of its own per commit.

  • The bounded read: the derivation reads nothing before the running turn, however long the chat before it.

  • One display status: agentStopDisplayStatus reads stopping from the host's flag or this device's press only while the agent works; the pane, rows and phone chat read it. The phone works it out once, in its session hook, and its view and overlay read that.

  • The repeated Stop: the /compact Stop that Codex never answers publishes Stopping after the first press, and the second press ends the command and clears it.

  • The row path: a summary with stopping reaches the worktree ps row as mainAgent.stopping and leaves it when the host drops it. The sidebar row follows it even when it is the only field that changed. The compact row leads with "Stopping…". The dashboard row, also used by the sidebar's non-compact mode, shows "Stopping…" in place of its tool line.

  • Desktop: the live line; the turn header keeps its running timer above "Stopping…"; the "run after the stop" placeholder; a card's Steer and the steer shortcut held while Stopping; Stop held only while this client's request is in flight; with the host saying Stopping and no request in flight, Stop is enabled and a press reaches the host; the press flag clears on answer and on failure; losing contact clears the host flag.

  • Phone:

    • the chat reads the host's Stopping with no press of its own, and keeps it after its own Stop answers;
    • it ignores another session's flag, reads nothing while disconnected, and never opens the stream on a host without the feed;
    • Stop is held only while its own request is in flight;
    • the "run after the stop" placeholder, and a card's Steer held while Stopping;
    • the list notices a stopping-only change;
    • the status stream:
      • one stream per connection;
      • a snapshot, then status frames, fold in order;
      • end and lost contact drop Stopping;
      • a refusal is asked again only on a new connection;
    • over the real connection object: a refused or ended stream is not replayed when the phone moves to another connection.
  • Cross-version: cross-version-stopping.unit.test.ts runs v1.4.214's summary fold and phone row reader against this host's output, and this build's readers against v1.4.214's. It also checks that v1.4.214 advertises the status feed but refuses it to phones.

  • Runs (named files only, vitest --config config/vitest.config.ts). Local runs are focused lists; CI is the full run.

    • src, at head 4ed011b: 551 files, 5897 passed, 1 skipped (this PR's files, fix(native-chat): a Stop binds only the turn it actually stopped #24864's, and every test importing a changed module). At the final head c2f54be: 176 focused files (the Stopping, steer-hold, binding, restart and Codex wait files), 1836 passed; one timing test failed under machine load and fails the same way on main. CI at c2f54be is the full run and is green.
    • mobile, at head a7a9551: 235 files, 2258 passed (every session and worktree test, plus this PR's). Mobile is unchanged since.
    • cross-version (--no-file-parallelism), at head 4ed011b: 14 files, 88 passed.
    • After merging main through Add a Chat settings page for structured native chat #25685, at head 970c6a1: the renderer Stop, session and rewind files, 24 files, 289 passed; the host Stop-note, stopping and subscriber files, 11 files, 99 passed; the cross-version wire file, 41 passed. tc:node and tc:web pass from a fresh build cache. CI at 970c6a1 is the full run and is green.
  • Typecheck: tc:node and tc:web, mobile tsc, and the mobile tests-typecheck ratchet pass.

  • Lint: oxlint, the anti-slop rules and the changed-lines quality gate (against main) pass on the changed files.

  • Ablations. Each fix was reverted alone, and each revert turned at least one test red. That covers:

    • the derivation's settling rule (3 red) and its bound-turn match;
    • the projection's settle key (2 red);
    • a failed Stop marking the live turn (1 red), and a failed Claude wind-down marking it (1 red); the mark staying display-only (1 red); a Stop that failed before any turn showed marking the turn that opens next (2 red), bounded by the next sent message (1 red);
    • the settle edge reaching the status (3 red) and the handover (1 red), and not counting as activity (1 red);
    • the shared display status (desktop and phone callers);
    • the Claude process-end revise, and the note identity it carries;
    • Stop held only on the press flag (desktop and phone);
    • the compact row's status-first order, the dashboard row, and the shared sidebar label;
    • the turn header's running timer;
    • the phone list's equality;
    • the phone's host flag, session match, connection gate and capability gate;
    • the phone stream's fold, end and contact-loss strips, one-per-connection rule, refusal rule and handle release;
    • the mobile allowlist entry;
    • the shared contact-loss strip;
    • the host's steer hold at the handover, and its read of the feed's own result;
    • the placeholder (desktop helper, composer, pane; phone helper and view) and the Steer hold (desktop card, list and pane; phone card, slot and overlay).
  • Localization: the keys components.native-chat.status.stopping and components.native-chat.status.queueAfterStop are in all 6 catalogs, translated, and the localization verifiers pass.

  • Not run:

    • the app itself for the steer hold and the placeholder;
    • phone QA: blocked until Orca is restarted on the M4Air;
    • the Claude real-CLI integration test.

AI Disclosure

Review

Agent skill upstream boundary

  • Not applicable, or this change follows docs/reference/agent-skill-sharing-upstream-boundary.md and copies or mechanically translates no upstream skill-installer source, tests, fixtures, registry entries, path tables, comments, or documentation.

Notes

  • Security: one host method is newly callable from the phone, agentSession.subscribeStatus. What it carries is described under "What the new phone method exposes".
  • Mobile: covered above.
  • SSH / remote: covered under Mixed versions.
  • Cross-platform: no platform-specific code.
  • Performance: the status feed already renders the chat once per commit; the derivation runs on that render and reads from the end back to the running turn. A settle edge re-renders once more, only when a Stop starts or settles. The steer hold reads the feed's cached result, so it adds no read of its own. The phone holds one extra stream per connected host.

Checklist

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

…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.
… 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.
…ry 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.
…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.
…ead 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.
… 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.
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.
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.
- 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.
…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.
… 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.
…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.
… 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.
… 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.
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.
…-state-on-b

# Conflicts:
#	src/renderer/src/components/native-chat/NativeChatComposer.tsx
#	src/renderer/src/components/native-chat/NativeChatComposerActions.tsx
#	src/renderer/src/components/native-chat/use-native-chat-dictation.ts
…-state-on-b

# Conflicts:
#	tests/e2e/cross-version-wire/cross-version-agent-session-wire.unit.test.ts
…-state-on-b

# Conflicts:
#	src/main/native-chat/agent-session-wire/structured-agent-session-subscribers.ts
…-state-on-b

# Conflicts:
#	src/renderer/src/components/native-chat/NativeChatStructuredSession.tsx
#	src/renderer/src/components/native-chat/StructuredAgentSessionStatusBridge.tsx
#	src/renderer/src/components/native-chat/use-structured-agent-session.ts
…-state-on-b

# Conflicts:
#	mobile/src/session/MobileNativeChatTurnStatus.test.ts
#	mobile/src/session/MobileNativeChatTurnStatus.tsx
#	mobile/src/session/MobileNativeChatView.tsx
#	mobile/src/session/use-mobile-native-chat-turn-disclosure.ts
#	src/renderer/src/components/native-chat/NativeChatMessageList.tsx
#	src/renderer/src/components/native-chat/NativeChatQueuedMessageList.tsx
#	src/renderer/src/components/native-chat/NativeChatTurnActivityLine.tsx
@brennanb2025
brennanb2025 marked this pull request as ready for review October 6, 2026 17:29

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Caution

Some comments are outside the diff and can’t be posted inline due to GitHub limitations.

⚠️ Outside diff range comments (1)

🟡 Minor · Keep failedOn when a repeat press reopens the settle. · journal-stop-turn-end.ts:145-146

src/main/native-chat/agent-session-journal/journal-stop-turn-end.ts:145-146
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Keep failedOn when a repeat press reopens the settle.

beginJournalStopSettle rebuilds stop.settle from settling and turnId only. Before this PR that was enough. Now structuredAgentSessionStopping also reads settle.failedOn to keep "Stopping…" on the turn a failed Stop could not stop.

Trigger:

  1. A Stop is pressed before the turn opens. Its interrupt fails, so the settle closes with a failedOn mark.
  2. The turn opens and reads "Stopping…".
  3. The person presses Stop again (reach === 'repeat', opensSettle). beginSettle drops failedOn.
  4. The provider answers { cancelled: false } with no refusal. Then interruptFailed is false, so cancelAndNote sets neither binding.turnId nor binding.failedOn.

After settled, the settle binds nothing. "Stopping…" clears while the turn still runs, and the steer hold lets queued sends steer into that turn. Keep the earlier mark, as the code already does for turnId.

Proposed fix
-  stop.settle = { settling: true, ...(stop.settle?.turnId ? { turnId: stop.settle.turnId } : {}) }
+  stop.settle = {
+    settling: true,
+    ...(stop.settle?.turnId ? { turnId: stop.settle.turnId } : {}),
+    ...(stop.settle?.failedOn ? { failedOn: stop.settle.failedOn } : {})
+  }

ℹ️ Review info
⚙️ Run configuration
  • Configuration used: Repository UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 0e652920-289b-4fca-ba48-b38a48263a4d
📥 Commits

Reviewing files that changed from the base of the PR and between 8d4f441 and 0516948.

📒 Files selected for processing (120)
  • mobile/src/session/MobileNativeChatLiveLine.android.test.ts
  • mobile/src/session/MobileNativeChatLiveLine.test.ts
  • mobile/src/session/MobileNativeChatLiveLine.tsx
  • mobile/src/session/MobileNativeChatOverlay.tsx
  • mobile/src/session/MobileNativeChatView.test.ts
  • mobile/src/session/MobileNativeChatView.tsx
  • mobile/src/session/MobileNativeChatView.turn-status.test.ts
  • mobile/src/session/mobile-native-chat-composer-placeholder.test.ts
  • mobile/src/session/mobile-native-chat-composer-placeholder.ts
  • mobile/src/session/use-mobile-native-chat-turn-disclosure.ts
  • mobile/src/session/use-mobile-structured-agent-session-newer-approval-send.test.tsx
  • mobile/src/session/use-mobile-structured-agent-session.ts
  • mobile/src/session/use-mobile-structured-send-with-outcome.ts
  • mobile/src/session/use-mobile-structured-turn-indicator.test.tsx
  • mobile/src/transport/rpc-subscription-inventory.ts
  • mobile/src/worktree/agent-row-display.ts
  • src/main/agent-hooks/first-work-branch-rename.test.ts
  • src/main/claude/claude-background-wake-retention.test.ts
  • src/main/codex/codex-structured-rewind.ts
  • src/main/codex/codex-structured-session-acquire.ts
  • src/main/codex/codex-structured-session-state.ts
  • src/main/codex/codex-structured-turn-cancellation.ts
  • src/main/codex/codex-structured-turn-start.ts
  • src/main/native-chat/agent-session-journal/journal-reducer.ts
  • src/main/native-chat/agent-session-journal/journal-stop-marks.ts
  • src/main/native-chat/agent-session-journal/journal-stop-note-projection.ts
  • src/main/native-chat/agent-session-journal/journal-stop-turn-end.ts
  • src/main/native-chat/agent-session-journal/queued-message-pause.ts
  • src/main/native-chat/agent-session-wire/agent-session-journal-batch.ts
  • src/main/native-chat/agent-session-wire/agent-session-stop-note-projection.test.ts
  • src/main/native-chat/agent-session-wire/agent-session-stop-note-refresh.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-background-task-channel.test.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-chat-stop.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-claude-stop-note-end.test.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-client-delivery.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-codex-stop-note-end.test.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-conversations.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-dead-generation-settlement.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-delivery-loop.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-host-delivery.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-host.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-recovered-turn-clock.test.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-rewind-stop-note.test-fixture.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-rewind.test.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-rewind.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-status-feed-stopping.test.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-status-feed.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-status-journal-projection.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-status-summary-equality.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-steer-while-stopping.test.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-stop-wind-down.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-stopping-journal.test.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-stopping.test.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-stopping.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-subscribers.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-turns-cancel.ts
  • src/main/native-chat/agent-session-wire/structured-conversation-compaction.test.ts
  • src/main/native-chat/agent-session-wire/structured-rewind-recovery.ts
  • src/main/runtime/rpc/methods/structured-agent-session-rpc.test-fixture.ts
  • src/main/runtime/structured-agent-session-codex-interrupt-order.test.ts
  • src/renderer/src/components/native-chat/NativeChatComposer.test.tsx
  • src/renderer/src/components/native-chat/NativeChatComposer.tsx
  • src/renderer/src/components/native-chat/NativeChatComposerActions.tsx
  • src/renderer/src/components/native-chat/NativeChatComposerField.tsx
  • src/renderer/src/components/native-chat/NativeChatMessageList.stopping-turn-bar.test.tsx
  • src/renderer/src/components/native-chat/NativeChatMessageList.tsx
  • src/renderer/src/components/native-chat/NativeChatQueuedMessageCard.tsx
  • src/renderer/src/components/native-chat/NativeChatQueuedMessageList.test.tsx
  • src/renderer/src/components/native-chat/NativeChatQueuedMessageList.tsx
  • src/renderer/src/components/native-chat/NativeChatStructuredSession.stopping.test.tsx
  • src/renderer/src/components/native-chat/NativeChatStructuredSession.test-harness.tsx
  • src/renderer/src/components/native-chat/NativeChatStructuredSession.tsx
  • src/renderer/src/components/native-chat/NativeChatTurnActivityLine.test.tsx
  • src/renderer/src/components/native-chat/NativeChatTurnActivityLine.tsx
  • src/renderer/src/components/native-chat/StructuredAgentSessionStatusBridge.test.tsx
  • src/renderer/src/components/native-chat/StructuredAgentSessionStatusBridge.tsx
  • src/renderer/src/components/native-chat/native-chat-composer-placeholder.test.ts
  • src/renderer/src/components/native-chat/native-chat-composer-target.ts
  • src/renderer/src/components/native-chat/native-chat-composer-types.ts
  • src/renderer/src/components/native-chat/native-chat-interactive-prompt.ts
  • src/renderer/src/components/native-chat/native-chat-structured-stop-controls.ts
  • src/renderer/src/components/native-chat/native-chat-transcript-slots.ts
  • src/renderer/src/components/native-chat/structured-agent-session-outbox-retry.test.ts
  • src/renderer/src/components/native-chat/structured-agent-session-outbox-retry.ts
  • src/renderer/src/components/native-chat/structured-agent-session-outbox-storage.ts
  • src/renderer/src/components/native-chat/structured-agent-session-status-bridge-stopping.test.tsx
  • src/renderer/src/components/native-chat/use-native-chat-transcript-slots.ts
  • src/renderer/src/components/native-chat/use-structured-agent-session-outbox.ts
  • src/renderer/src/components/native-chat/use-structured-agent-session-stop.ts
  • src/renderer/src/components/native-chat/use-structured-agent-session.queue-while-stopping.test.tsx
  • src/renderer/src/components/native-chat/use-structured-agent-session.queued-gating.test.tsx
  • src/renderer/src/components/native-chat/use-structured-agent-session.ts
  • src/renderer/src/i18n/en-runtime-required.json
  • src/renderer/src/i18n/locales/en.json
  • src/renderer/src/i18n/locales/es.json
  • src/renderer/src/i18n/locales/fr.json
  • src/renderer/src/i18n/locales/ja.json
  • src/renderer/src/i18n/locales/ko.json
  • src/renderer/src/i18n/locales/zh.json
  • src/renderer/src/lib/agent-row-stopping-label.ts
  • src/renderer/src/runtime/structured-agent-session-status-feed-lifecycle.test.ts
  • src/renderer/src/runtime/structured-agent-session-status-feed.ts
  • src/shared/agent-session-status-snapshot-fold.ts
  • src/shared/agent-session-wire.ts
  • src/shared/agent-status-types.ts
  • src/shared/agent-stop-display-status.test.ts
  • src/shared/agent-stop-display-status.ts
  • src/shared/native-chat-live-line.test.ts
  • src/shared/native-chat-live-line.ts
  • src/shared/native-chat-turn-membership.ts
  • src/shared/native-chat-turn-status.ts
  • src/shared/native-chat-types.ts
  • src/shared/structured-agent-session-message-projection.ts
  • src/shared/structured-agent-session-outbox-delivery.ts
  • src/shared/structured-agent-session-outbox.ts
  • tests/e2e/cross-version-wire/agent-session-stop-event-downgrade.unit.test.ts
  • tests/e2e/cross-version-wire/cross-version-agent-session-wire.unit.test.ts
  • tests/e2e/cross-version-wire/cross-version-stop-note-scenarios.ts
  • tests/e2e/cross-version-wire/cross-version-stop-note-test-rig.ts
  • tests/e2e/cross-version-wire/versioned-agent-session-wire.ts
🚧 Files skipped from review as they are similar to previous changes (7)
  • src/renderer/src/i18n/en-runtime-required.json
  • src/renderer/src/i18n/locales/en.json
  • src/renderer/src/i18n/locales/ko.json
  • src/renderer/src/i18n/locales/es.json
  • src/renderer/src/i18n/locales/zh.json
  • src/renderer/src/i18n/locales/ja.json
  • src/renderer/src/i18n/locales/fr.json

Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 8 remain after this review.

…-state-on-b

# Conflicts:
#	mobile/src/session/MobileNativeChatView.tsx
#	mobile/src/session/use-mobile-structured-agent-session.ts
#	src/renderer/src/components/native-chat/NativeChatComposer.test.tsx
…e 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.
…n 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.
@brennanb2025
brennanb2025 merged commit b922342 into main Oct 6, 2026
24 of 27 checks passed
Jinwoo-H added a commit that referenced this pull request Oct 6, 2026
…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.
brennanb2025 added a commit that referenced this pull request Oct 6, 2026
…5/chat-ui-duplicate-surfaces

Conflicts: main moved the approval mapping into chatApprovalFromJournal (#24369); it now omits the ask rule as this branch's card does.
brennanb2025 added a commit that referenced this pull request Oct 6, 2026
…r-stop

Main's #24369 adds a Stopping state to the composer's Stop control and its own
stop controls; the composer keeps this branch's Resume action beside it
(Stopping… / Stop / Resume / Send), and Stop stays live only once a turn can be
stopped. Main's new Stopping test passes the primary action this branch requires.
The composer's queue field props come from the queue's composer hook as one
object, to stay under the file's line limit.
brennanb2025 added a commit that referenced this pull request Oct 6, 2026
…' into brennanb2025/stop-queue-after-stop

Conflicts in #25041's own host and client files took #24369's side; the old
overtaking design there is rebuilt on top in the following commits.
brennanb2025 added a commit that referenced this pull request Oct 6, 2026
…s-after-stop' into brennanb2025/stop-queue-after-stop

Composer: Stop reads Stopping… while this client's Stop is in flight (#24369) and gives way
to Resume when the queue is held and idle (#24586). Stop is offered while the queue sends next
but live only once a turn can stop. queued-stop-row test takes #24586's version.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant