Repository navigation
fix(native-chat): a message kept after a quit or close waits in order and follows the chat's next turn - #25960
Conversation
A Stop, a restart or /clear holds the queued cards. The hold stays; only the header row naming why, and its Resume button, go. A held card shows no caption, and its own Steer, or any new message, releases the queue.
Steer vs Send now follows whether a turn is running, not the card's hold, so a card held after a Stop, a restart or /clear reads Send.
… behind them A card queued after a Stop (or written after a restart or /clear) sent only once the cards held before it were released; with no header to explain or release the hold, it sat silently. The next sendable card now skips held cards; a returned card still blocks what is behind it.
… held cards follow it After a Stop, a card queued later sent past the held cards, but the queue recorded that send as Orca's own turn. It never ended the Stop's pause, so the held cards then waited forever with nothing on the card saying why. A queued card is always something the person wrote: only the client send RPC may now create one. The queue's send of it is therefore recorded as the person's turn, which ends the Stop's pause once the agent takes it, and the held cards then drain in order.
…ue-continues-after-stop # Conflicts: # src/renderer/src/i18n/locales/en.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
…nd of it is that author's turn Main now lets Orca's own sends ask to queue (sendAgentTurn's 'queue' delivery), so "every card is a person's" no longer holds by refusing host sends. Each card records who wrote it (the submission's client/host vocabulary) in a new nullable column; the drain records that origin, so a person's card ends a Stop's pause and Orca's does not. /clear carries the author. Rows from before the column read as a person's. The userSend-only admission gate is removed.
…n idle held queue offers Resume A restart's pause held every waiting card, including one a person typed after the restart while Orca's own continuation ran, and nothing released it except a per-card Send. It now holds only cards another host process wrote, the same way a Stop holds only cards queued before it. The composer's primary button becomes Resume (Play) while nothing is typed, no turn runs and the host holds a card Resume would send, whatever held it (Stop, restart or /clear). It calls the existing agentSession.queuedMessagesResume, guarded against a second press in flight. A card nothing holds keeps the run going between a turn's end and the queue's send of it, so its Steer no longer flips to Send for the frame in between.
The host published one pause for the whole queue, so a client held every waiting card while it was set. Between a turn's end and the queue's send of a card queued after a Stop or restart, the composer could flash Resume and the cards Send, and a card queued after a Stop lost its "Waiting for your answer" caption. Each published card now carries an optional `heldBy`: the pause holding it, or null, derived from the same rule the drain reads. A client holds only those cards; against a host without the field it falls back to the queue-level pause.
… Resume returns focus After Resume, the host lifts the hold in one update and sends the first card in a later one. In between nothing was running, so the composer's button flashed a disabled Send. A card nothing holds now keeps the queue's run going for the button too: an empty composer shows Stop, disabled until the turn starts. Not when the host refuses every send (a rewind whose outcome is unknown, read from its status), where nothing is coming. The same fix removes the Stop, Send, Stop flip between queued turns. Resume disables the button, which dropped keyboard focus; focus now returns to the composer.
…e chat stays working across the gap A turn's end, or a Resume, and the queue's send of the next card commit as two host updates. In between nothing was running, so the working status, timer, pickers and composer button flipped for one update. The client guessed the drain from its own copy of the host's gates, which missed a /clear-replaced source and covered only the button. The queue publication now carries `nextQueuedMessageId`: the drain's own next card through the drain's own gate (`nextStructuredQueuedMessage`, which the drain step now calls), null whenever the host would refuse the send. The client derives one fact, the queue is about to send, and every working reader follows it; Stop stays disabled until a turn can be stopped. The client-side copy of the gates and the status-feed rewind read are removed.
…reducer and a history page
…ge asks before passing it
The queue's header row ("Queue paused because you interrupted", or Orca
restarted, or you cleared the conversation) comes back above the cards it
holds, with Resume; it names the oldest held card's pause, as the host
publishes it per card, and hides over cards held only on their own or
returned. The header's Resume and the composer's share one in-flight guard.
A held card reads Steer again whether or not a turn runs; a card held on its
own or returned keeps Send.
Sending a message while the header shows (Enter or the button) first asks
"Send message?": Clear queue deletes every card and then sends (a failed
delete sends nothing), Send message sends and keeps the cards, which follow
the new turn, and dismissing sends nothing and keeps the draft. Host
commands send as they are.
…s way to lift it After "Send message" over a held queue, the row kept saying "Queue paused…" until the agent accepted the new turn. The chat now reads that gap from the outbox: while this composer's direct send is recorded by the host and not yet accepted, the controller shows no paused row (and so no Resume or confirmation). A refusal settles the entry and the row comes back, since the hold did not lift. Orca's own sends never enter this outbox, and the queue's send of a card goes under a fresh id, so neither hides it. Nothing is stored.
…d Clear queue is one toast The closing dialog stays mounted and clickable through its exit animation, and a double-click or a held Enter lands twice before any re-render, so Send message (or Clear queue) could send the captured message twice. The pending send now lives in a ref that the first choice takes; a second one finds nothing. Clear queue deletes one card at a time and stops at the first failure, so a failed press shows one toast instead of one per card. The dialog keeps its compact width at desktop sizes and the primitive's narrow-window gutter (`max-w-sm sm:max-w-sm`, as the other compact confirmations).
…ue-continues-after-stop # Conflicts: # src/main/native-chat/agent-session-journal/journal-queued-messages.ts # src/main/native-chat/agent-session-wire/structured-agent-session-queued-messages.ts
… typed while it waits After Clear queue, the message waited in the composer while the cards were deleted one by one. A second Enter in that window sent it again, and text typed meanwhile was wiped when the chained send was accepted. From the Clear queue choice until its message has gone out, the composer's structured send does nothing. The chained send (and Send message's) now carries the composition it was taken from, and the composer is cleared on acceptance only if it still holds exactly that, as host commands already do. Also: the v1 contract comment names `nextQueuedMessageId` and its absent- means-null fallback, and the own-send check returns at once on an empty outbox.
…ue-continues-after-stop # Conflicts: # src/renderer/src/components/native-chat/NativeChatStructuredSession.tsx # src/renderer/src/i18n/locales/es.json
…a restart sends nothing by itself - Any accepted turn ends a Stop's or a /clear's pause, whoever sent it (a person, Orca's own messages, or the queue), and so does Resume. The card and submission author fields that only fed the old person-only rule are gone. - The queue sends strictly in order: a card never overtakes a held one. - After a restart nothing sends by itself and no paused row shows: the chat's next turn (the carry-on, or the person's own message) runs first, then the cards. - Resume and "Send message?" are offered only while nothing runs and no prompt waits.
…itten before the next turn waits for it too
… goes while any turn that will lift it is on its way - The queue stops handing cards off when the host tears down. A card sent during a quit was refused at close, and that refused send withdrew the chat's restart offer, so resuming after the relaunch sent nothing. - The host publishes no pause while a turn sent after it (your message, Steer, or Orca's own) waits for the agent; a refusal shows it again. This replaces the client's own-send check. - A card written after a restart is an ordinary card again: it waits while any card from before the restart still waits.
…ay check in one expression
…2025/chat-queue-continues-after-stop # Conflicts: # src/main/native-chat/agent-session-wire/structured-agent-session-host.ts
…ed transport beside the held queue
…ue-continues-after-stop # Conflicts: # src/main/native-chat/agent-session-wire/structured-agent-session-host-mutations.ts # src/main/native-chat/agent-session-wire/structured-agent-session-queued-message-rig.test-fixture.ts # src/main/native-chat/agent-session-wire/structured-agent-session-queued-send.ts # src/renderer/src/components/native-chat/NativeChatComposerActions.test.tsx # src/renderer/src/components/native-chat/NativeChatComposerActions.tsx # src/renderer/src/components/native-chat/NativeChatComposerField.tsx # src/renderer/src/components/native-chat/use-native-chat-structured-composer-send.ts # src/shared/agent-session-journal-types.ts
…sked about; tests follow main's draft props - The open dialog closes when the queue's pause lifts under it (Orca's mail, another client's Resume, any accepted turn): nothing is sent, the draft stays, and the next Enter sends as usual. The pending choice records the hold it was asked under; nothing new is stored. - The composer-field Resume test passes main's dropScopeKey/draftScopeKey. - The dialog test expects main's rule: only the sent text leaves the composer.
…2025/chat-queue-continues-after-stop # Conflicts: # src/main/native-chat/agent-session-wire/structured-agent-session-restart-interruption-test-harness.ts
…r-stop Conflict in use-structured-agent-session.ts: kept main's rewind-gated retry and the PR's isWorking (queue sends next) and queueSendsNext. Trimmed two lines in that hook and collapsed the approval options map in NativeChatStructuredSession.tsx to stay within max-lines after main's rewind additions; no behaviour change.
…r-stop Main's #24660 keeps a person's unsent message as a card after a quit, crash or close, and it reads the submission's origin (client or host) to decide which sends to keep. This branch had removed origin and the client send's userSend flag as unused, so they are restored as main has them; comments now say origin only decides what a restart or a close keeps, never what lifts a pause (any accepted turn or Resume still does). The queue drain's quit gate is kept once, with main's re-check right before the hand-off. Main's new kept-card tests are adjusted to this branch's rule that the restart's hold is never published (queuePause null; the cards still wait).
…ry other card, and follows the chat's next turn
A message Orca accepted but never handed to the agent before a quit, crash or
close came back as a card held on its own ("Not sent yet — press Send"): the
queue skipped past it, so later cards sent first, and only the person's own
Send released it. It is now an ordinary card at the head of the queue that
waits, with every card the chat closed with, for the chat's next accepted turn.
One rule for a chat that was not running, derived from the journal: when this
host first opens a chat (after a restart or crash), or a person closes it, and
cards are waiting, a reopen mark is written (a tombstone carrier key, like the
Stop and Resume marks). The cards queued before it wait until a turn is
accepted or Resume comes after it; nothing sends by itself, and no paused row
shows, a Stop's included. The idle sweep's own eviction writes nothing and
changes nothing the person sees. A mark that cannot be written leaves the open
working and holds from the open itself until the next turn; the next open marks
again. A rewind restates the mark. /clear's carried cards also wait unshown.
This replaces the host-instance comparison and its adoption write, and the
'kept' hold (stored 'kept' and legacy 'stopped' holds now read as none).
…dle chat with waiting cards open Review round 1: the startup restore opened chats past the per-host first-open mark, so their cards could send by themselves after a quit or crash; a card mid-hand-off at the open got no mark; a close that left the chat open re-marked after every new send. - Every open marks when a card waits, or is mid-hand-off with its send unanswered. - The idle sweep keeps a chat's handle while cards wait, so its eviction never reopens one and stays invisible; it drops it on the next sweep once they leave. - A person's close marks once; its delivery re-check marks only when it settled a send. - A failed mark holds from where the mark would have gone. - A Stop made after a reopen shows its row. - The rig's restart is a real quit and relaunch.
…ed-Stop tests; a late mark starts where the chat stopped - Restores eight tests the previous commit dropped by mistake. - A mark the delivery loop or a close's re-check writes after a later send starts where the chat stopped, so that send still lifts it; a later mark never narrows an earlier, wider one. - Only the idle sweep's own close keeps a chat with a card waiting (or mid-hand-off) open, and it still releases an ended child's lease first; a person's close drops it as before.
…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.
…is host publishes no restart pause Main's #24660 test published queuePause 'restarted', which this branch's wire type no longer lists, so the mobile tests typecheck ratchet failed.
…one host-lifetime step Keeps the delivery loop under its line limit after main's Stopping change; no behaviour change.
…r-stop Main's #25888 shows which agent a queued message is from: the sender moves onto the message body (`from`), and a person's send carries `userSend`. The card keeps this branch's Steer/Send rule beside main's sender line; the queue's card projection imports both. The test rig and its card test take main's `from`; a card-holds test no longer passes the removed `source` field. Locale catalogs keep both sides' new keys.
…r-stop Brings main's own fix for the duplicate resolveLaunchArgs key (#25997). No conflicts.
…r-stop Main's #24514 reveals the transcript on a send and on a Resume that lifts the pause. This branch's composer Resume goes through the same reveal; the queue's Resume answers whether it lifted the pause on both surfaces. The composer's awaitable structured send keeps main's command reveal at the press, and main's new navigation test fires that send without awaiting it. Line limits after the merge: the composer's working/Stop rule moves into the structured stop controls; the queue controller takes the transport's fields by spread; the option picker request type is named; test harness resets combined.
…ard-ordinary The sender moved off the queued row onto the message body on main (#25888): the row's source plumbing goes, this branch's derived holds stay.
…nary Main's #25152 keeps a message handed to a Claude start that never answered as a card. Here that card is ordinary: the settlement returns its newest sequence as this branch's callers expect, and the close's or the reopen's mark holds it until the next turn, so its test expects no kept hold.
…rough the held start's release Main's #25152 holds the start these tests send into; quitting without releasing it left the quit waiting.
|
Status: ready for review (head What was wrong (with the message queue switched on; it ships switched off today). A message Orca had accepted but the agent never got, because Orca quit, crashed or closed the chat first, came back as a special "Not sent yet — press Send to send it" card. The queue skipped it, so newer messages went out ahead of it, and only your own Send, Edit or Delete released it. After What is different for you now.
How. The hold is worked out from the chat's journal, never stored on a card. Opening a chat's journal (after a restart, a crash or a close) writes one "reopen" mark when a message is waiting. Messages queued before the mark wait for the next accepted turn or Resume after it, so nothing has to clear it. If the mark can't be written, the chat still opens and the messages stay held until the next turn. The PR body has the full mechanism and the cases. Review. Three review rounds on Claude, read-only. They fixed:
Two minor gaps are left and listed in the PR body: a close that fails two separate writes, and a Stop before the first turn after QA. Two rounds on a rig build with the queue switched on and a stand-in agent, after a real quit and relaunch. Screenshots are in the PR body. All scenarios pass, including Resume pressed while a Stop is still landing, and a Stop after a relaunch showing its row. Codex was blocked as the QA driver: it needs its login in the real This round. #24586 landed on main as A later main merge brought #25152, which keeps a message handed to a Claude start that never answered as a card when the chat closes or Orca quits. Here that card is ordinary: the close's mark, or the next open's, holds it until the next turn, so its test now expects no "kept" hold. Two of this PR's tests sent into that start and quit without releasing it, so they waited forever. They now quit the way main's tests do. The last merge brings in main's #26050, which fixed a line-limit failure on main that had stopped CI from running any tests. Latest main merge ( The merge before it ( Verified on
CI on Previous round, verified on
CI on Not verified: a real Claude or Codex agent (QA used a stand-in), mobile UI, and SSH. |
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configuration
📒 Files selected for processing (6)
💤 Files with no reviewable changes (6)
Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 5 remain after this review. 📝 WalkthroughWalkthroughThe queue journal now records reopen marks and derives restart pauses from those marks or a fallback cursor. Interrupted sends return to waiting without a per-card kept hold. Conversation lifecycle paths write reopen marks, and queue publication no longer exposes the cleared pause reason or kept-card reason. Mobile and renderer captions, locale strings, and related tests reflect these changes. Priority: ➖ Normal Merge Risk: ⚪ Minimal · up to The locale changes align with the updated queue behavior. No actionable merge risk was identified in these changes. 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
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. Comment |
ELI5
Sometimes Orca accepts a message you sent but quits, crashes or closes the chat before the agent gets it. Today that message comes back as a special "Not sent yet — press Send to send it" card. The queue skips it, so newer messages go first, and only your own Send releases it.
With this PR it comes back as an ordinary queued message, first in line. When a chat wasn't running (Orca restarted or crashed, or you closed the chat), nothing sends by itself, and no "paused" row appears. The next turn the agent accepts (yours, or "carry on" after you resume the chat) runs first, then the queued messages follow in order.
What Changed
Before (with the queue switched on; it ships switched off today):
/clear, the carried messages show "Queue paused after you cleared the conversation" with Resume.After:
/clear: the same, with no paused row.Mechanism. The hold is derived from the chat's journal, never stored on the card:
This replaces the "which Orca process wrote this card" comparison, the rewrite that adopted cards into the current process after a turn, and the card's own 'kept' hold. A stored 'kept' hold, and the 'stopped' hold an older build wrote, now read as no hold.
Review finding R13-1 on #24586 (the kept card is skipped and order breaks) is fixed here.
Why
Differences from the common pattern:
Intended: a restart holds the queue until the next accepted turn. In the common pattern either nothing is held after a restart (the queue sends by itself) or the hold lasts until an explicit Resume. The settled rule is "nothing sends by itself after a restart; cards follow the next accepted turn". Any accepted turn releasing the hold matches how a Stop's pause already ends here.
Intended: the hold is derived from a durable journal event rather than a flag stored on each card. This matches how the common pattern derives a person's Stop pause from a recorded interruption. A stored per-card flag would need a write to clear it, and that write can fail and strand the card.
Intended: the idle sweep keeps a chat loaded while a message waits in it (held by a Stop, held after a restart or close until a turn, a failed send, a message behind a returned one, or a blocked one), then unloads it on the next sweep once the last one leaves. Unloading and reopening such a chat would have to re-hold its messages, which is visible; keeping it loaded costs one open journal per such chat.
Known limit. The host accepts a rewind while messages are queued (only the desktop blocks it). A message queued before such a rewind is then treated as one kept earlier, so it stands ahead of a message kept after the rewind. Order among the cards stays strict.
Known gaps (minor, left as is):
/clear, a Stop made before the first turn in the new conversation still shows no row, the same as the carried messages.Mixed versions. The queue's capability is still not advertised.
pausedReason: 'kept', noqueuePause: 'cleared'. An older client reads an ordinary card and no row.Linked Issue
Follow-up to #24586 (now on main as 0c96550) and #24660.
Visual Proof
Rig build of each commit with the queue switched on locally, a stand-in agent, quit and relaunched for real (QA by an automated driver over the app's debugging port; screenshots of the rendered app).
A message sent while the agent was starting, then Orca quit — after relaunch:
Before: "Not sent yet — press Send to send it", and the queue goes past it. After: an ordinary card; nothing sends by itself; the next message ran first, then this one.
After
/clearwith messages queued:After a relaunch with two messages queued (no row, nothing sends by itself; the next message runs first, then A and B):
A Stop the person makes after the relaunch still shows its row:
Re-run on
4b769c75f83(after main's "Stopping…" state came in through #24586): Stop with queued messages shows "Queue paused because you interrupted" with Resume while the turn is still stopping; Resume pressed then sends nothing until the turn ends, then the messages in order; and a Stop after a relaunch shows its row.Testing
I manually tested these changes locally (rig QA after a real restart, see below)
Automated tests added/updated, or explained why not below
Rig QA (macOS, local host), before and after, on
e47faa90500(scenarios 5 and 6 re-run on4b769c75f83, pass): (1) quit with two queued messages → no row, nothing sent in 60 s, the next message ran first, then both in order; (2) a message sent while the agent was starting, then quit → an ordinary card that follows the next message (before: "Not sent yet", held for its own Send); (3) close the chat tab with queued messages, reopen → same as (1); (4)/clearwith queued messages → no "Queue paused after you cleared the conversation" row, the next message runs first, then the carried ones; (5) Stop → "Queue paused because you interrupted" with Resume, unchanged; (6) a Stop after a relaunch → its row shows. Notes: the rig "quit" was a SIGTERM to the app process (its own shutdown runs); (3) reopened the closed chat through the host's reveal call because the desktop has no reopen control for a closed chat; not run: SSH remote host, phone, a packaged build.334 test files / 2855 tests across the queue, journal, host lifetime, idle sweep, startup restore and e2e unit suites pass locally, plus the phone's queue suites. Typecheck is clean for every changed file (desktop, CLI, e2e, phone).
Pinned: a message queued mid-turn sends when that turn ends (no reopen, no mark); after a quit, a crash, the startup restore, a close and a reopen, nothing sends by itself, and the next accepted turn runs first; an idle-sweep unload of a stopped chat writes nothing, keeps its Stop row, and unloads once the last card leaves; an idle chat with no card writes nothing; a mark that fails to write still opens the chat and holds; a hand-off in flight at the open is held once it comes back; a close's later re-check marks only once; a late mark starts where the chat stopped; a rewind keeps the mark.
Flipped deliberately: fix(native-chat): keep a message accepted before a quit or crash as a held card #24660's kept-card tests (the card is ordinary now and follows the next accepted turn, so a retyped copy is followed by the card), the /clear tests (no published 'cleared' row), and the Stop-across-close tests (a close hides the row; the Stop still holds).
Downgrade: an older build opens a journal with the reopen mark and keeps every row; an older build with the queue holds this build's kept card first and sends it first after a turn.
Review
Three rounds of Claude review. Round 1 found the startup restore skipping the mark and a close's re-check re-marking after each send; round 2 found eight dropped tests and a late mark landing after a new send; both are fixed and covered above.
Notes
structured-agent-session-live-turn.test.tstype error and an over-length mobile test file (fix test(mobile): split the turn-disclosure test under the 800-line test limit #25933); not this PR.Checklist
N/Awith reasonpnpm lint,pnpm typecheck,pnpm test, andpnpm buildpass (or CI will cover; local preferred)