Skip to content

fix(native-chat): Stop is saved before it acts, and a late or repeated Stop does nothing - #27076

Merged
brennanb2025 merged 28 commits into
mainfrom
brennanb2025/chat-receipts-stop
Oct 13, 2026
Merged

brennanb2025 merged 28 commits into
mainfrom
brennanb2025/chat-receipts-stop

Conversation

@brennanb2025

@brennanb2025 brennanb2025 commented Oct 10, 2026 •

Copy link
Copy Markdown
Contributor
Files Added Deleted Net
Test 27 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​971 $\color{#cf222e}{\Huge{\mathbf{−}}}$​575 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​396
Prod 43 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​1018 $\color{#cf222e}{\Huge{\mathbf{−}}}$​392 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​626

ELI5

Pressing Stop in a native chat has two problems today:

  • Stop is not saved before it acts. If its bookkeeping fails, or a dropped connection makes the app send the same Stop again, it can act a second time. A late or repeated Stop can then stop work the person sent afterwards.
  • A Stop pressed while the agent is still starting, or retried, has no saved record that a retry can be answered from.

After this PR, Stop is saved before it does anything, the same way Send already is. A retry of the same Stop gets the same answer and never stops newer work. What happens to the messages you had queued does not change: that stays exactly as it is on main, and a separate change owns it.

What Changed

What you see:

  • A repeated or late Stop does nothing. A Stop that names a reply that already ended no longer interrupts the next message or withdraws the queue. Examples: a double press, or a retry after a lost connection. A Stop that names no reply still stops whatever is running, as before.
  • "Couldn't stop Codex. Try again." (or "Couldn't stop the agent. Try again.") appears only when Orca could not save the Stop. Then nothing happened and the agent keeps running. Once a Stop is saved it always goes ahead.
  • Unconfirmed stops say so. If the interrupt itself can't be confirmed, the chat shows the existing "Cancellation was not confirmed." note. That now includes cancelling a question or approval card.
  • Cancelling a card that can't be saved now says "Couldn't cancel this question or approval. Try again." and the card stays as it was.
  • Unchanged: which messages a Stop withdraws, what they say ("This message was withdrawn before the agent started it."), and the queue pausing.

Mechanism:

  1. Stop goes through its own command receipt, the path fix(native-chat): accept sends through their own receipt #26672 and native chat: queued-message actions and question answers accept through their own receipt #26988 added for Send and the queued-message actions. A same-id retry is answered from the receipt. It never picks a target again and never interrupts again.

  2. Save first, in one database transaction. The transaction writes:

    • the Stop's event, naming the turn it stops, resolved when the write runs;
    • main's own withdrawal rows for queued messages not yet handed over (the same rows, words and fact as main);
    • the receipt, pointing at the Stop event.

    Then the interrupt runs. Main wrote the withdrawal unawaited beside the interrupt.

  3. Stop is admitted in the session's queue, like every other command. A Stop that arrives while the agent is still starting aborts that start on arrival, as main does. A replay of an already-answered Stop never aborts anything. The Stop then picks its target once, in the queue.

  4. A committed receipt is never rewritten.

    • A failure after the save is logged, and the Stop still acts.
    • An interrupt that can't be confirmed gets the existing note.
    • Only a failed save answers "Couldn't stop".
  5. Late Stop rule. A Stop that names a turn that has ended is an accepted no-op with its receipt. That includes a turn a rewind removed within the same conversation history.

  6. A card's Cancel saves its receipt in the same transaction as the card's dismissal.

Why

The bug class: Stop's effects were not tied to a durable record of what it was pressed on. A Stop that wasn't saved, or a retry of one, could act on newer work.

The fix follows the common pattern:

  • The command's receipt, its effect and its captured target commit together before the interrupt.
  • A duplicate reads that receipt.
  • A Stop naming work that already ended is an accepted no-op.

Differences from the common pattern, and from main:

  • INTENDED: a late Stop is a full no-op (cancelled: false, no withdrawal). On main, a Stop naming an ended reply still withdrew the queue and could interrupt the next message. A retry must acknowledge the original command and never act on newer work. This also lets a message sent after that reply run instead of being thrown away.
  • INTENDED: the Stop and its withdrawal commit together or not at all. On main the withdrawal was written separately from the interrupt. Here, if the save fails, nothing is withdrawn and the agent keeps running.
  • INTENDED: a Stop pressed during a start aborts that start on arrival, before the save. If the save then fails, the start has already ended: the person sees "Couldn't stop . Try again.", and their message stays queued and is sent on the next start.
  • TEMPORARY: that exception exists because an agent start holds the session's command queue. In the common pattern a start runs after its command commits, so a Stop can always save first. Follow-up: agent start runs as a post-commit effect instead of holding the command queue.
  • TEMPORARY: a Stop issued before an ordinary rewind can still interrupt the next message, as on main. A rewind that replaces the conversation history keeps no record of the turns it removed, and Stop doesn't yet carry the context it was issued against. Follow-up: the rewind and /clear receipt PR, which adds the context revision Stop will carry.
  • INTENDED: a background task the agent won't stop, or a stop that can't be confirmed, shows a note in the chat saying so; the Stop itself is always recorded. The note is a chat-level row, written where the chat is when the answer comes back rather than inside a reply's details, because background tasks outlive the reply they started in. A background-task Stop is saved before Orca asks the agent, and its answer is that saved acceptance, so a retry gets the same answer and never asks again. If the agent says the task is still running, the note reads "The background task wasn't stopped." ("The background tasks weren't stopped." for Stop all). If the request times out or loses the agent, it reads "Cancellation was not confirmed." A Stop the agent carries out still answers cancelled: true as on main, which the phone uses to keep the task row showing as stopping. This adds one new failure kind, backgroundTaskNotStopped. A client that predates it can't read the fact, so it shows the host-written words, as it does for any newer failure kind. Main's newer background-Stop behaviour is kept: the stops the agent accepts still update the subagent rows, and a Stop all with work no row lists still ends the agent when no reply is running, now after the receipt is saved.
  • INTENDED: "Cancellation requested." after a Stop now sits inside the stopped reply's details. The Stop records which reply it stopped, so its note belongs to that reply. On main, a Stop pressed before any reasoning showed put the line below the reply. When reasoning is showing, both builds already put it inside the details.
  • INTENDED: the interrupt runs in process, with no durable outbox. Provider processes belong to the host, and restart recovery already settles a reply a crash interrupted.
  • TEMPORARY: the row in the old retry table written beside each receipt. It is removed in the final cutover PR, as in fix(native-chat): accept sends through their own receipt #26672 and native chat: queued-message actions and question answers accept through their own receipt #26988.

Older clients: stopFailed and cancelNotSaved are new refusal reasons. A client that doesn't know them shows the generic words for that action, for example "The agent wasn't stopped." That is pinned by tests and was checked against the notice code from v1.4.223.

Scope boundary: what happens to a stopped message (withdrawn, kept, or later passed to the agent) belongs to a separate change. That change builds on this one: this PR decides where and when the withdrawal is written, and that change decides what is written.

Linked Issue

Based on main. Follows #26672 and #26988 (queued-message actions and prompt answers through receipts), both merged.

Visual Proof

Real Claude Code in a hidden desktop build. BEFORE is main at 83056e5edb4; AFTER is this PR at b0e06c54513.

1. Stop during a reply. BEFORE: the stopped reply, with "Cancellation requested." shown below it.

stop-reply-before.png

AFTER: the same stop. The reply reads "Interrupted after 8s", and the note is now inside the reply's details.

stop-reply-after.png

AFTER, with the reply's details opened: "Cancellation requested." is there.

stop-reply-after-expanded.png

4. Cancelling a question card (AFTER). The card is dismissed, it reads "Cancelled", and no "Couldn't" message appears.

card-cancel-after.png

5. Messages sent while a reply runs, then Stop. BEFORE: both follow-ups are withdrawn and the chat says "Stopped manually".

queued-withdrawn-before.png

AFTER: the same result. The journal rows match BEFORE: the Stop event, then each message withdrawn in send order.

queued-withdrawn-after.png

Testing

Live check, real Claude Code, BEFORE (main) vs AFTER (this PR). Every verdict was read against the screenshots and the chat's own journal database.

Case BEFORE AFTER
1. Stop during a reply Pass Pass. "Cancellation requested." now sits inside the reply's details (see the INTENDED label above).
2. Stop during an agent start Pass: the turn had opened, so it ends "Interrupted after 0s" Pass: Claude hadn't started, so the Stop and the withdrawn send were saved together and the chat shows "Stopped manually". Main shows the same result when a Stop lands this early, so I read the difference as timing (one sample each). A follow-up answered on both.
3. Late Stop on a finished reply Not reachable from the desktop Not reachable from the desktop. The follow-up ran.
4. Cancel a question or approval card Pass Pass. No "Couldn't" message appeared.
5. Messages sent while a reply runs, then Stop Withdrawn (reference) Withdrawn exactly as BEFORE: same rows, reasons and order.
  • Case 3 is covered by tests only. The desktop shows no Stop on a finished reply, and its Stop never names a turn, so you can't press a Stop naming an ended turn from the UI.
  • Not verified live:
    • Codex: blocked by low disk space on the test machine.
    • "Couldn't stop Claude. Try again.": it appears only when the Stop can't be saved, which needs an injected storage fault.
    • A repeated Stop with the same id: the desktop disables Stop after the first press.
    • SSH and remote hosts, mobile, Windows and Linux.
  • Seen on both builds, not caused by this PR:
    • Cancelled cards read "Cancelled on desktop-renderer", an internal name.
    • Messages sent while a reply runs show as plain sent messages, not as "Queued messages" cards.

Automated:

  • The PR's own tests, cut to the few that pin behaviour someone depends on:
    • whole-flow Stop tests: Stop during a reply (saved before it interrupts), Stop during an agent start, a late Stop is a no-op, a same-id retry is answered from its receipt and never stops the newer turn, a failed save says "Couldn't stop", and the background-task notes ("wasn't stopped" for one task and for all, "not confirmed" on a timeout, no second note on a retry);
    • bugs found in review: a Stop during a start while an option change waits behind its save (round 2), a write that throws after the save (round 3, for a reply and for a start), and a Stop recording the wrong turn when the next reply opened while it saved (round 4);
    • old-client tests: the new failure kinds' fallback words, and a background-task note shown from the host's own words.
  • Main's existing tests that this change touches, updated to the new behaviour.
  • The whole src/main/native-chat (630 files) and src/main/acp (72 files) suites, src/main/claude, and every runtime and Codex test that sends a Stop.
  • tc:node, tc:web and tc:cli.
  • orca-ci-checks and the mobile test-typecheck ratchet.
  • CI is green on b66c8e06306.

AI Disclosure

Review

Agent skill upstream boundary

  • Not applicable, or this change copies or mechanically translates no upstream skill-installer source, tests, fixtures, registry entries, path tables, comments, or documentation.

Notes

Author: @BrennanKB5.

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, or N/A with reason
  • 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)

Adds the agent_session_command_receipts table and an insert-if-absent
receipt write that runs inside a command's own journal transaction. A
receipt records the command's identity, canonical hash, status and a
pointer to the row holding its result; it dies with its chat record.
Nothing uses it yet: the existing operation ledger stays authoritative.
Rows are keyed by caller and operation id; a send looks across callers and
other methods look up only their caller's row, matching the existing ledger.
Bad rows and newer table shapes read as unreadable instead of throwing.
…tion id

Global lookups now take rows in insertion order, so a read and an insert
agree on which receipt answers an id regardless of the host clock. The
primary key leads with the operation id, so its own index serves both
lookups.
Send (direct, queued, steer) and /compact no longer commit a separate
ledger reservation before the message is saved. Their command receipt is
inserted-if-absent in the same transaction as the submission or queued
draft; a repeated id replays from it, a changed request conflicts.
Operation ids are checked for syntax only, so age, clock skew, pruning
and the reservation's own storage can no longer refuse a send.

The ledger success row they already co-committed stays as a temporary
insert-if-absent write so families not yet switched keep seeing these
ids; it is never read on this path.
…d space with Send, wake delivery once

- After its commit, a send whose publication fails answers through the same receipt replay a retry takes, so one id gets one answer; the in-memory copy and journal reload are gone.
- /compact ids are global like Send's: a retry under a new connection replays instead of needing a second acknowledgment write.
- The receipt route calls its runner directly; the runner wakes delivery when a new message row commits.
- The compatibility ledger row expires from host time; each temporary site is labelled.
- Receipt read errors and post-commit failures are logged.
…d ledger

Restart-continuation tests now model the case Send cannot vouch for (saved but
not loaded into memory) and keep it unconfirmed; a later throw after the message
is in the chat answers continued, as a resend would. Drop /compact's unreachable
legacy replay branch and fix stale comments.
Send and /compact now route on acceptsWithCommandReceipt: main made /clear and
rewind settlesWithWrite plans with their own success receipts, and they stay on
ledger settlement. /compact keeps #25704's queued answer under receipt replay.
…rough their own receipt

Send Now, Delete, Resume and prompt answers move onto the command-receipt route
Send uses: the receipt is inserted with the command's own write. A no-op answers
only once its receipt commits (otherwise outcome unknown), and a prompt answer's
replay reads the exact revision it was accepted at.
Send Now's acknowledgement points at the submission it handed off; a question
answer's receipt keeps the accepted resolution; a no-op that cannot save its
receipt answers outcome unknown and holds nothing in memory; classified refusals
pass through.
…eued messages

Stop accepts through its own receipt: one transaction saves the Stop record
(target turn, agent generation, cutoff), returns unsent messages to the queue as
paused cards, and writes the receipt; only then does it interrupt the captured
target. A failed save leaves the agent running and says so. A late Stop is a
recorded no-op, and a retry is answered from the receipt.
…ed refuses, hold only what the Stop did not stop

- A Stop naming a turn that already ended is an accepted no-op.
- A card's Cancel saves its receipt with the dismissal row.
- A Stop saved but not carried out is refused as stopFailed ("Couldn't stop <agent>. Try again.") and its receipt is rewritten as rejected.
- A Stop during a start is saved first, then aborts that start; the abort-before-admission path is removed.
- The message the Stop stops is withdrawn as before; messages behind it are held as paused cards. Non-text messages are still withdrawn (TEMPORARY).
- Cleanups: one receipt commit path, dead fields and helpers removed, the returned-to-queue text names the agent.
…ssage fate as on main

- Stop's withdrawal of queued sends is main's own rows (rejected, cancelled), written in the same
  transaction as the Stop event and its receipt; a failed save withdraws nothing.
- A Stop arriving during a start aborts that wait on arrival, as on main (admits-now guard), then
  captures its target once in the session lane. The out-of-lane admission is removed.
- A committed receipt is never rewritten: a failure after the save answers from the receipt and
  writes the existing 'Cancellation was not confirmed.' note. 'Couldn't stop <agent>' only when
  the save fails; a card Cancel that can't be saved says the question or approval wasn't cancelled.
- A Stop naming a turn a rewind removed is late (an accepted no-op).
- Removes the held-card settlement and returnedToQueue: what happens to a stopped message stays as
  on main.
…nly for unconfirmed interrupts

- A Stop whose write throws after its receipt committed still interrupts (never 'Couldn't stop').
- An interrupt that worked answers stopped even when only its note fails to write.
- A strict card interrupt that throws after its receipt committed gets the 'Cancellation was not
  confirmed.' note, through one shared helper with the turn path.
- A target-less Stop stands while the main agent works, so a turn that opened while its save waited
  (a send made before the Stop) is interrupted, as on main.
- A card Cancel that can't be saved says 'Couldn't cancel this question or approval. Try again.'
@brennanb2025 brennanb2025 changed the title fix(native-chat): Stop keeps the messages queued behind it and is saved before it acts fix(native-chat): Stop is saved before it acts, and a late or repeated Stop does nothing Oct 10, 2026
- The Stop event's turn is resolved when its write runs (the named turn, else the live turn), so a
  turn that opened while the save waited is the one marked interrupted and shown stopping.
- The unconfirmed note is keyed by that turn; tests cover it and its own write failing.
- Whether a Stop withdrew anything comes from its capture, so a throw after the commit can't flip it.
- A Stop counts as saved anyway only when its own write committed the receipt.
@brennanb2025
brennanb2025 changed the base branch from brennanb2025/chat-receipts-queue-actions to main October 12, 2026 01:34
…eipts-stop

# Conflicts:
#	src/main/native-chat/agent-session-journal/command-receipt-schema.ts
#	src/main/native-chat/agent-session-journal/journal-queued-messages.ts
#	src/main/native-chat/agent-session-journal/journal-store.ts
#	src/main/native-chat/agent-session-wire/structured-agent-session-command-receipt.ts
#	src/main/native-chat/agent-session-wire/structured-agent-session-mutation-plans.ts
#	src/main/native-chat/agent-session-wire/structured-agent-session-prompt-answer-receipts.test.ts
#	src/main/native-chat/agent-session-wire/structured-agent-session-queued-action-receipts.test.ts
#	src/main/native-chat/agent-session-wire/structured-agent-session-queued-pause-lift.test.ts
#	src/main/native-chat/agent-session-wire/structured-agent-session-turns-cancel.ts
#	src/main/native-chat/agent-session-wire/structured-agent-session-turns.ts
#	src/main/runtime/agent-session-record-store.ts
…on receipt

Main's create path (from #26332) hands performSend the founding receipt; the
Stop changes widened the turn context's receipt to track its commit. Wrap it
with the existing observedReceipt.
…ot done

A background-task Stop now saves its receipt before it asks the agent. A
refusal after that save (task still running, or a timeout / lost connection)
was being replaced by the receipt's replay, so the person never heard the Stop
failed. That refusal is now the first answer, as on main; the receipt stays
saved and a retry of the same id is answered from it without asking again.
…w-up alone

The late-Stop rule makes a Stop naming a turn that has ended an accepted no-op
for every agent. The Claude case was rewritten with it; this is Grok's.
…Stop rule

- The "leaves the child alone" Stop now reaches the provider (handed over, a
  turn with no row yet); the at-rest refusal case was a duplicate of the late
  no-op test and is removed.
- The adapter comment and two test names say the gap rule covers a named turn
  with no journal row yet.
- The Grok late-Stop test checks no status row and that the follow-up completes.
- Name the two refusals refusalAnswers passes through.
@brennanb2025

Copy link
Copy Markdown
Contributor Author

Review summary

This PR makes a native-chat Stop save itself before it acts, so a late or repeated Stop can't stop work sent after it. It went through four review rounds, a sync with main and a review after that sync, then a live check with real Claude Code. Head: b0e06c54513, CI green.

Review rounds

Round 1 (at 9c381f2147e): one P1, several P2s, fixed in 121fe4f716a.

  • P1: a Stop naming a reply that had already ended still acted on the next message while it was being handed over. Now a named turn that the chat history shows and that isn't running is a late Stop: an accepted no-op.
  • A card's Cancel now saves its receipt in the same write as the card's dismissal.
  • A Stop that can't be saved now says "Couldn't stop . Try again." It uses a new refusal reason, and older clients show their generic line instead.
  • A Stop during an agent start no longer aborts that start before the Stop is saved.

Round 2: four P2s. Scope was then narrowed (a9abdab7404).

  • Kept in this PR: saving first, late-Stop handling, and "Couldn't stop" only when the save fails. A Stop pressed during an agent start aborts that start on arrival, as main does; this is labelled in the body.
  • Moved out: what happens to the messages a Stop withdraws now stays exactly as on main. A separate change owns that.
  • Receipts: a saved receipt is never rewritten. A failure after the save is logged, and the Stop still acts.

Round 3 (at a9abdab7404): message handling matches main except for late Stops, and that difference was ruled INTENDED. P3s fixed in 98eaba35fe9:

  • A Stop that was saved but whose write then threw still acts.
  • A note that fails to write no longer turns a successful interrupt into "not confirmed".
  • An unconfirmed card cancel says so on the card's turn.
  • A Stop that names no turn stops whatever is running.
  • Clearer words when a card's Cancel can't be saved.

A Stop pressed before an ordinary rewind was labelled TEMPORARY, with the rewind and /clear receipt PR as the follow-up.

Round 4 (at 98eaba35fe9): the Stop event now names the turn that is running when it is written, so the stopped reply reads "Interrupted". Two tests were added. Fixed in 491742c1bd3.

Sync with main (83056e5edb4)

  • The PR was stacked on an older commit of native chat: queued-message actions and question answers accept through their own receipt #26988. I re-merged every file against that commit and checked the result:
    • every file this PR doesn't touch matches main exactly;
    • every file it does touch is main plus this PR's change.
  • Design decision: main's background-task Stop (Let a Codex chat stop a command it left running in the background #26780) answers with a refusal when the agent says the task is still running, or when the request times out or loses the agent. This PR saves a background-task Stop before it asks, and was turning that refusal into a silent cancelled: false.
    • Ruled: on the first answer the refusal goes through, so people still see main's "not done" notice.
    • The receipt stays saved, and a retry of the same id answers cancelled: false without asking the agent again. On main a same-id retry got the refusal again; people don't see that, because clients make a fresh id for every press.
    • Labelled INTENDED (0d7f3f16ccf), with tests through the host for still running, timed out and lost connection, each with a retry.
  • d737f148688: a compile fix where main's create path passes a new chat's first send a plain receipt; it now reuses the existing wrapper that tracks when a receipt is saved.
  • e3ee92dc392: the Grok version of the late-Stop test, matching the Claude one this PR had already rewritten.

Review after the sync

No P1 or P2. Five P3s, fixed in b0e06c54513:

  • Two Stop tests now reach the provider as their names say. A third had become a copy of the late no-op test and was removed.
  • A stale adapter comment and two test names now say the gap rule covers only a named turn with no saved row yet.
  • The Grok late-Stop test checks that no status line is written and that the follow-up completes.
  • A comment names the two refusals that pass through.
  • The body notes the same-id retry difference.

Verified

  • Live, real Claude Code, main vs this PR: a Stop during a reply, a Stop during an agent start, cancelling question and approval cards, and messages sent while a reply runs (withdrawn with the same saved rows as main). Every verdict was checked against screenshots and the chat's saved history. Screenshots are in the PR body.
  • Automated:
    • the PR's own test files;
    • the full agent-session-wire (231 files) and agent-session-journal (59 files) suites;
    • src/main/acp and tests/e2e/cross-version-wire;
    • the node, web and cli typechecks;
    • orca-ci-checks and the mobile test-typecheck ratchet;
    • CI.

Not verified

  • A late Stop naming a finished reply, and a repeated Stop with the same id. The desktop can't send either, so they are covered by tests only.
  • Codex live: blocked by low disk space on the test machine.
  • "Couldn't stop . Try again." live: it needs an injected storage fault.
  • SSH and remote hosts, mobile, Windows and Linux.

Seen on main too, not from this PR

  • Cancelled cards read "Cancelled on desktop-renderer".
  • Messages sent while a reply runs show as plain sent messages, not as "Queued messages" cards.

@brennanb2025
brennanb2025 marked this pull request as ready for review October 12, 2026 07:56
@coderabbitai

coderabbitai Bot commented Oct 12, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration
  • Configuration used: Repository UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: fb9ca958-4e76-48a4-92f7-0b8059a00517
📥 Commits

Reviewing files that changed from the base of the PR and between 1c463fa and b30b464.

📒 Files selected for processing (6)
  • 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
🚧 Files skipped from review as they are similar to previous changes (6)
  • src/renderer/src/i18n/locales/zh.json
  • src/renderer/src/i18n/locales/es.json
  • src/renderer/src/i18n/locales/ko.json
  • src/renderer/src/i18n/locales/en.json
  • src/renderer/src/i18n/locales/fr.json
  • src/renderer/src/i18n/locales/ja.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.

📝 Walkthrough

Stop handling now records acceptance before cancellation and replays committed operation receipts. Chat Stops capture and validate their target before acting, and named Stops for ended turns are handled as no-ops. The journal can record queued-send withdrawals and Stop events together. Failure handling distinguishes save failures from unconfirmed cancellation. Shared failure types, notices, and translations cover Stop, Cancel, and background-task outcomes.

Priority: ➖ Normal

Merge Risk: 🔵 Low · up to b30b4

The Stop-acceptance test still leaves tracked journals open after teardown. This is a bounded test-cleanup issue rather than a demonstrated user-facing Stop failure.

Pre-merge checks: Passed 4 · Failed 1

Failed Failed checks: 1 warning

Check name Status Explanation Resolution
Docstring Coverage Warning Docstring coverage is 61.63% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 86 functions across 61 files. (6 skipped:… Write docstrings for the functions missing them to satisfy the coverage threshold.
Passed Passed checks: 4 passed
Check name Status Explanation
Linked Issues check Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check Passed Check skipped because no linked issues were found for this pull request.
Title check Passed The title clearly and concisely describes the primary change: Stop is persisted before acting, and late or repeated Stops become no-ops.
Description check Passed The description is complete and directly addresses the template. It explains the user impact, implementation, rationale, limitations, visual proof, testing, compatibility considerations, and checklist…
Full details: Docstring Coverage

Explanation

Docstring coverage is 61.63% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 86 functions across 61 files. (6 skipped: 6 unsupported.)

  • Fix all pre-merge checks with AI
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

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

Actionable comments posted: 1


ℹ️ Review info
⚙️ Run configuration
  • Configuration used: Repository UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 0107970c-24ba-4225-9d8d-2d3f76ded3c1
📥 Commits

Reviewing files that changed from the base of the PR and between 83056e5 and b0e06c5.

📒 Files selected for processing (68)
  • config/scripts/vitest-sqlite-runtime-files.mjs
  • src/main/acp/acp-structured-host-stop.test.ts
  • src/main/acp/acp-structured-session-adapter-lifecycle.test.ts
  • src/main/acp/acp-structured-session-adapter.ts
  • src/main/claude/claude-structured-conversation-stop.test.ts
  • src/main/native-chat/agent-session-journal/command-receipt-schema.ts
  • src/main/native-chat/agent-session-journal/journal-item-fold.ts
  • src/main/native-chat/agent-session-journal/journal-reducer.ts
  • src/main/native-chat/agent-session-journal/journal-row-writer.ts
  • src/main/native-chat/agent-session-journal/journal-stop-acceptance.test.ts
  • src/main/native-chat/agent-session-journal/journal-stop-acceptance.ts
  • src/main/native-chat/agent-session-journal/journal-store-collaborators.ts
  • src/main/native-chat/agent-session-journal/journal-store.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-background-stop-receipts.test.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-chat-stop-target.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-queued-stop.test.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-claude-stop-ends-session.test.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-claude-stop-turn-end.test.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-command-receipt.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-conversation-stop.test.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-create.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-host.test.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-journal-corruption.test.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-mutation-admission.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-mutation-plans.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-operation-settlement.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-prompt-answer-receipts.test.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-prompt-cancel.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-queued-action-receipts.test.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-queued-messages.test.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-queued-pause-lift.test.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-queued-stop.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-queued-withdrawn-draft.test.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-repeat-press.test.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-stop-aborts-start.test.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-stop-acceptance.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-stop-event-binding.test.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-stop-event.test.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-stop-failed-after-save.test.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-stop-never-waits.test.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-stop-receipts.test.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-stop-saves-first.test.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-stop-unconfirmed-note.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-targeted-cancel.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-turn-stop-notes.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-turns-cancel.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-turns.ts
  • src/renderer/src/components/native-chat/agent-session-failure-stop-words-text.ts
  • src/renderer/src/components/native-chat/agent-session-failure-words-text.ts
  • src/renderer/src/components/native-chat/agent-session-write-notice-text.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/shared/agent-session-failure-copy.ts
  • src/shared/agent-session-failure-words.ts
  • src/shared/agent-session-failure.ts
  • src/shared/agent-session-refusal-details.ts
  • src/shared/agent-session-refusal-notice-stop.test.ts
  • src/shared/agent-session-refusal-notice.test.ts
  • src/shared/agent-session-refusal-notice.ts
  • src/shared/agent-session-refusal-reason-words.ts
  • src/shared/agent-session-write-failure.ts
  • src/shared/agent-session-write-notice-copy.ts
💤 Files with no reviewable changes (1)
  • src/main/native-chat/agent-session-wire/structured-agent-session-stop-never-waits.test.ts

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

Comment thread src/main/native-chat/agent-session-journal/journal-stop-acceptance.test.ts Outdated
…otes what happened

A background-task Stop is saved before it asks the agent. The answer is
that saved acceptance: a stopped task answers cancelled:true as before
(the phone keeps the row "stopping" on it), and anything else answers
cancelled:false. A retry answers from the receipt without asking again.

What the agent then says comes back as a note keyed by the Stop's own id,
written through the existing Stop-note path, and never as a refusal:
- still running: "The background task wasn't stopped." (or "The
  background tasks weren't stopped." for Stop all), the new status-row
  failure kind backgroundTaskNotStopped;
- timed out or lost the agent: "Cancellation was not confirmed."
The note is the thread's, since tasks outlive their turn. A note that
cannot be written is logged and changes no answer.

The background-task body moves out of performCancel into the targeted
cancel, and the refusalAnswers flag and its receipt are deleted.
…2025/chat-receipts-stop

# Conflicts:
#	src/renderer/src/components/native-chat/agent-session-failure-words-text.ts
#	src/shared/agent-session-refusal-details.ts
#	src/shared/agent-session-write-failure.ts
A background-task Stop now answers its saved acceptance and says what happened in a note: "The background task wasn't stopped." when the command still runs, "Cancellation was not confirmed." when Codex was lost.
…eipts-stop

# Conflicts:
#	config/scripts/vitest-sqlite-runtime-files.mjs
#	src/main/native-chat/agent-session-wire/structured-agent-session-mutation-plans.ts
#	src/main/native-chat/agent-session-wire/structured-agent-session-turns-cancel.ts
#	src/main/native-chat/agent-session-wire/structured-agent-session-turns.test.ts
Merges the receipt, save-first, failed-after-save and background-task Stop
tests into one flow file; drops unit and per-branch tests already covered
by a flow test.
@brennanb2025
brennanb2025 merged commit 4d65146 into main Oct 13, 2026
32 checks passed
brennanb2025 added a commit that referenced this pull request Oct 14, 2026
main now accepts a Stop in one transaction (#27076). The withdrawal rows that
transaction writes for host-held sends carry neverReachedAgent, so the proof
lands with the Stop itself; the old unawaited withdrawal path is gone. Trims
the PR's tests to the main flow, the failed-save case and the contracts.
@brennanb2025
brennanb2025 deleted the brennanb2025/chat-receipts-stop branch October 14, 2026 07:57
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