Skip to content

fix(native-chat): a message kept after a quit or close waits in order and follows the chat's next turn - #25960

Merged
brennanb2025 merged 53 commits into
mainfrom
brennanb2025/chat-queue-kept-card-ordinary
Oct 7, 2026
Merged

brennanb2025 merged 53 commits into
mainfrom
brennanb2025/chat-queue-kept-card-ordinary

Conversation

@brennanb2025

@brennanb2025 brennanb2025 commented Oct 6, 2026 •

Copy link
Copy Markdown
Contributor
Files Added Deleted Net
Test 23 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​577 $\color{#cf222e}{\Huge{\mathbf{−}}}$​395 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​182
Prod 37 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​373 $\color{#cf222e}{\Huge{\mathbf{−}}}$​293 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​80

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):

  • After a quit, crash or close, a message the agent never got shows "Not sent yet — press Send to send it". It is held on its own: the queue sends the messages behind it first, and only your Send, Edit or Delete releases it.
  • After /clear, the carried messages show "Queue paused after you cleared the conversation" with Resume.
  • After a restart, Orca decides which messages to hold by asking which Orca process wrote them.

After:

  • That message is an ordinary card at the head of the queue. With every other message the chat closed with, it waits for the chat's next accepted turn (or Resume, Send now, Edit or Delete), then the queue sends in order.
  • After a restart, a crash or a close (closing the tab, worktree teardown, an orchestration stop): nothing sends by itself; no paused row, no Resume in the message box, no "Send message?" question, a Stop's row included. Your next message (or the carry-on) runs first; the queue follows.
  • After /clear: the same, with no paused row.
  • When Orca unloads an idle chat in the background and reopens it, nothing changes on screen: a Stop's "Queue paused because you interrupted" row and its Resume stay, and nothing new is held.
  • Only a person's Stop shows a paused row.

Mechanism. The hold is derived from the chat's journal, never stored on the card:

  • Every time a chat's journal is opened, after a restart, a crash or a close, the open writes one "reopen" mark if any message is waiting (or was being handed to the agent and might come back). The startup restore and every later read go through that one open.
  • A person's close (closing the tab, worktree teardown, an orchestration stop) writes the mark itself, in case the chat stays loaded.
  • The messages queued before the mark wait until a turn is accepted, or a Resume comes, after it. Nothing has to clear the mark: the next turn ends the hold by derivation.
  • The mark rides the same carrier as the existing Stop and Resume marks: a removal entry for an id no message uses, which every Orca build already reads. A new kind of journal row would make an older build discard the journal from that row on.
  • If the chat stopped before a later send was accepted, the mark records where it stopped, so that send still lifts it.
  • If the mark cannot be written, the chat still opens. The messages are held from where the mark would have gone until the next turn, and the next open writes the mark again. Bookkeeping never blocks you.
  • A rewind or history import that replaces the journal carries the mark over, as it already does for a Stop.
  • A message handed to a Claude start that never answered, when the chat is then closed or Orca quits, is kept the same way: an ordinary card, held by the close's or the next open's mark until the next turn.
  • Orca's idle sweep unloads an idle chat only when no message waits in it. So unloading never reopens a chat with messages to hold, and nothing on screen changes.

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

  • One rule. "A chat that wasn't running holds what it closed with until its next turn" covers a restart, a crash, a close and a kept message the same way. A second, per-card rule ("only your Send releases this one") broke strict order and needed its own caption.
  • Derived, not stored. The hold dies with the next accepted turn or Resume, so no stored flag can strand a message.
  • Why a written mark rather than "the card was queued before this open". A card's queue position is the journal position when it was queued. A card queued right after an open, before anything else is written, has the same position as the open, so it would be held by mistake. The mark gives the open a position of its own.
  • Why the idle unload writes nothing. Orca unloading an idle chat is something you never see, so it must not change the rows on screen or hold anything new.

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):

  • If a close's settlement keeps some messages as cards and then fails to write another row, its late mark is not written. Those cards are then held only by a mark written earlier, if there is one. This needs two separate write failures.
  • After a /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.

  • An older host never writes the new mark.
  • An older build reading this build's journal keeps every row and holds this build's cards under its own restart rule. The downgrade tests cover both.
  • Publication only drops values: no pausedReason: 'kept', no queuePause: 'cleared'. An older client reads an ordinary card and no row.
  • A newer client talking to an older host that still publishes a 'restarted' or 'cleared' pause shows the generic "Queue paused" row with Resume, which works on that host.

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 (#24586) After
before after

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 /clear with messages queued:

Before (#24586) After
before after

After a relaunch with two messages queued (no row, nothing sends by itself; the next message runs first, then A and B):

after relaunch

A Stop the person makes after the relaunch still shows its row:

stop after relaunch

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.

Stop after a relaunch, still stopping Resume pressed while stopping
stop row resume while stopping

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 on 4b769c75f83, 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) /clear with 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

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)

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.
…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.
… 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.
…2025/chat-queue-continues-after-stop

# Conflicts:
#	src/main/native-chat/agent-session-wire/structured-agent-session-host.ts
…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.
) into brennanb2025/chat-queue-kept-card-ordinary
…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.
@brennanb2025
brennanb2025 changed the base branch from brennanb2025/chat-queue-continues-after-stop to main October 7, 2026 01:12
…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 squashed #24586 (this branch's base) as 0c96550; merged against #24586's final head so its own changes are not re-resolved.
…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.
@brennanb2025

brennanb2025 commented Oct 7, 2026 •

Copy link
Copy Markdown
Contributor Author

Status: ready for review (head efd9f251958)

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 /clear, the carried messages showed "Queue paused after you cleared the conversation". After a restart, Orca chose which messages to hold by comparing which Orca process wrote each one.

What is different for you now.

  • That message is an ordinary queued card, first in line.
  • After a restart, a crash, a close or /clear, nothing sends by itself and no "paused" row appears. Your next message (or "carry on" after you resume the chat) runs first, then the queue sends in order.
  • Only a Stop you pressed shows "Queue paused because you interrupted" with Resume.
  • When Orca unloads an idle chat in the background, nothing on screen changes.

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:

  • 8 tests deleted by mistake, now restored verbatim;
  • a mark written too early after a close's re-check;
  • the idle sweep unloading a chat with a card mid-hand-off;
  • naming.

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 /clear showing no row. The reviewer's verdict: ready on the host mechanism.

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 ~/.codex and writes there, so opencode ran it.

This round. #24586 landed on main as 0c96550ee9a, so this PR now targets main. Main's #25888 had moved a message's sender off the queued row and onto the message itself. The merge keeps that and drops this branch's copy of the old row field. After the merge, the files differ from main by exactly this PR's own changes, line for line.

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 (25e0a297016). This merge brings in #26094, main's fix for its own broken typecheck (an ACP test still spied on a journal method that #26038 removed). That break had failed CI on the previous head and stopped the unit tests from running. The merge had no conflicts. The files differ from main only by this PR's own changes, and each locale file only by the two lines this PR removes.

The merge before it (acea59c6d87) brought in #26038, which removed the step that copied old per-chat journal files into the shared database and deleted its test file. This branch had edited that test, so the merge took main's deletion, and nothing #26038 removed comes back. In closing a chat, both changes to the same function are kept: main's #25225 cancels a start the agent never answers before the close is queued, and this PR marks the queue so nothing sends by itself after a close.

Verified on efd9f251958:

  • 103 desktop test files pass (1,202 tests): the queue, kept-card and close suites, every native-chat test main's new commits touched, and the repaired ACP test.
  • oxlint passes on the changed files.
  • The node and web typechecks show no errors in files this PR changes. The only errors come from packages this local checkout lacks.

CI on efd9f251958: 18 passed, 14 skipped, 0 failing. That includes typecheck, all five unit test shards and the cross-version wire check.

Previous round, verified on 16f40bf35ac:

  • 69 desktop test files pass (749 tests): the queue suites and the native-chat tests main's last merges changed.
  • The changed-code quality gate and oxlint pass.
  • The node typecheck shows only errors from packages this checkout lacks, none in changed files. Web, cli and mobile were checked the same way after the first main merge.
  • The 3 mobile queue test files (63 tests) passed after the first main merge; CI's Mobile Checks pass on this head.
  • Each locale file differs from main only by the two lines this PR removes.

CI on 16f40bf35ac (previous head): 16 passed, 16 skipped, 0 failing, all five unit test shards included, on the first run.

Not verified: a real Claude or Codex agent (QA used a stand-in), mobile UI, and SSH.

@brennanb2025
brennanb2025 marked this pull request as ready for review October 7, 2026 03:44
@coderabbitai

coderabbitai Bot commented Oct 7, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

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: 040fef58-6ac6-441e-a2dc-551e2ac170c2
📥 Commits

Reviewing files that changed from the base of the PR and between ca0fdc8 and efd9f25.

📒 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 with no reviewable changes (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/ko.json
  • src/renderer/src/i18n/locales/zh.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; 5 remain after this review.


📝 Walkthrough

Walkthrough

The 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 efd9f

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)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 60.32% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 63 functions across 50 files. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: queued messages retained after a quit or close wait in order for the chat’s next turn.
Description check ✅ Passed The description is detailed and covers the change, rationale, user-visible behavior, visual proof, testing, review, and compatibility notes. It references related issues but does not use the template’…
Linked Issues check ✅ Passed No active directly linked issue remains. Issue #25933 is closed and provides historical context only, so it adds no coding requirements for this PR.
Out of Scope Changes check ✅ Passed The reported changes support the PR’s queued-message behavior. Reopen-mark handling, queue pause derivation, close and restart handling, legacy hold removal, UI and wire updates, compatibility tests, …
  • Fix all pre-merge checks with AI
  • Autopilot · 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.

@brennanb2025
brennanb2025 merged commit 80d4509 into main Oct 7, 2026
32 checks passed
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