Repository navigation
fix(native-chat): queued messages carry on in order after any turn, and nothing sends by itself after a restart - #24586
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.
|
Note Reviews pausedIt 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 Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configuration
📒 Files selected for processing (46)
🚧 Files skipped from review as they are similar to previous changes (7)
Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 6 remain after this review. 📝 WalkthroughWalkthroughQueued cards now retain their client or host origin, and queue drains use that origin when submitting cards. Queue selection skips held waiting cards but still stops at returned cards. Queue publication reports per-card holds and the next sendable card. The renderer uses queue and turn state to select Send, Steer, Stop, or Resume, and no longer displays a queue-pause row. Priority: ➖ Normal Merge Risk: 🔵 Low · up to The queued-message hold and Resume changes show no confirmed production defect. One test double could mislead a future test about whether a queued card shows Send or Steer. Fix it in a small follow-up; it need not block merge. Security Architecture ReviewSecurity architecture risk: 🔵 Low · up to The inspected controls preserve holds and prevent duplicate submissions while allowing newer instructions to proceed. No introduced security issue was demonstrated, but compatibility and recovery coverage is incomplete. Retained concerns Security review detailsSecurity Blast Radius
Trust Boundaries and Controls
Resilience and Maintainability Implications
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
Full details: Docstring CoverageExplanation Docstring coverage is 47.50% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 80 functions across 59 files. (1 skipped: 1 unsupported.) ✨ Finishing Touches 💡 1📝 Generate docstrings 💡
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 |
There was a problem hiding this comment.
Actionable comments posted: 1
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Advanced
Run ID: 720fa501-70e4-4259-890d-cf90f276083a
📒 Files selected for processing (22)
src/main/native-chat/agent-session-journal/journal-queued-messages.tssrc/main/native-chat/agent-session-journal/queued-message-pause.test.tssrc/main/native-chat/agent-session-journal/queued-message-pause.tssrc/main/native-chat/agent-session-wire/structured-agent-session-queued-messages.tssrc/main/native-chat/agent-session-wire/structured-agent-session-queued-pause-lift.test.tssrc/main/native-chat/agent-session-wire/structured-agent-session-queued-stop-row.test.tssrc/renderer/src/components/native-chat/NativeChatQueuedMessageCard.tsxsrc/renderer/src/components/native-chat/NativeChatQueuedMessageList.test.tsxsrc/renderer/src/components/native-chat/NativeChatQueuedMessageList.tsxsrc/renderer/src/components/native-chat/NativeChatStructuredSession.test-harness.tsxsrc/renderer/src/components/native-chat/NativeChatStructuredSessionDelivery.test.tsxsrc/renderer/src/components/native-chat/structured-agent-session-queued-cards.tssrc/renderer/src/components/native-chat/use-structured-agent-session-queued-messages.resume.test.tsxsrc/renderer/src/components/native-chat/use-structured-agent-session-queued-messages.test.tsxsrc/renderer/src/components/native-chat/use-structured-agent-session-queued-messages.tssrc/renderer/src/components/native-chat/use-structured-agent-session.tssrc/renderer/src/i18n/locales/en.jsonsrc/renderer/src/i18n/locales/es.jsonsrc/renderer/src/i18n/locales/fr.jsonsrc/renderer/src/i18n/locales/ja.jsonsrc/renderer/src/i18n/locales/ko.jsonsrc/renderer/src/i18n/locales/zh.json
💤 Files with no reviewable changes (1)
- src/renderer/src/components/native-chat/use-structured-agent-session-queued-messages.resume.test.tsx
Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 6 remain after this review.
| stop: mocks.stop, | ||
| queuedMessages: { | ||
| cards: mocks.queuedCards, | ||
| turnRunning: mocks.turnId !== null, |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
🔎 Supported by static analysis
🏁 Script executed:
rg -n 'isWorking|turnId|turnRunning' src/renderer/src/components/native-chat/NativeChatStructuredSession.test-harness.tsx src/renderer/src/components/native-chat/use-structured-agent-session-queued-messages.ts src/renderer/src/components/native-chat/NativeChatStructuredSession*.test.tsxRepository: stablyai/orca
Length of output: 3759
🏁 Script executed:
set -eu
printf '%s\n' '--- harness structure and relevant slices ---'
ast-grep outline src/renderer/src/components/native-chat/NativeChatStructuredSession.test-harness.tsx
sed -n '40,215p' src/renderer/src/components/native-chat/NativeChatStructuredSession.test-harness.tsx
sed -n '300,340p' src/renderer/src/components/native-chat/NativeChatStructuredSession.test-harness.tsx
printf '%s\n' '--- harness setters/usages ---'
rg -n -C 4 'mocks\.(isWorking|turnId|canStop)|set.*Working|turnRunning|queuedMessages|Send|Steer' src/renderer/src/components/native-chat --glob '*test*' --glob '*.tsx'
printf '%s\n' '--- production queued controller and callers ---'
sed -n '1,190p' src/renderer/src/components/native-chat/use-structured-agent-session-queued-messages.ts
rg -n -C 8 'useStructuredAgentSessionQueuedMessages|queuedMessages|turnRunning|isWorking' src/renderer/src/components/native-chat --glob '*.tsx' --glob '*.ts'Repository: stablyai/orca
Length of output: 45665
🏁 Script executed:
set -eu
h='src/renderer/src/components/native-chat/NativeChatStructuredSession.test-harness.tsx'
printf '%s\n' '--- harness exact lines ---'
sed -n '70,210p' "$h"
sed -n '310,335p' "$h"
printf '%s\n' '--- assignments and queued assertions in structured-session tests ---'
rg -n -C 6 'mocks\.(isWorking|turnId)\s*=|queuedCards|turnRunning|Steer|Send' \
src/renderer/src/components/native-chat/NativeChatStructuredSession*.test.tsx
printf '%s\n' '--- production controller binding ---'
rg -n -C 10 'useStructuredAgentSessionQueuedMessages|queuedMessages|turnRunning|isWorking' \
src/renderer/src/components/native-chat/NativeChatStructuredSession.tsx \
src/renderer/src/components/native-chat/use-structured-agent-session-queued-messages.ts \
src/renderer/src/components/native-chat/use-structured-agent-session*.tsRepository: stablyai/orca
Length of output: 42172
🏁 Script executed:
set -eu
printf '%s\n' '--- queued pane consumers ---'
rg -n -C 10 'turnRunning|Send|Steer' src/renderer/src/components/native-chat \
--glob 'NativeChat*Queued*' --glob 'NativeChatStructuredSession.tsx' --glob '*goal-dock.test.tsx'
printf '%s\n' '--- exact queued-card files ---'
git ls-files 'src/renderer/src/components/native-chat/*Queued*' 'src/renderer/src/components/native-chat/*queued*'Repository: stablyai/orca
Length of output: 41696
Derive turnRunning from mocks.isWorking.
When mocks.isWorking is true and mocks.turnId is null, the harness currently sets turnRunning to false. The queued-message pane then shows Send, while production passes isWorking and shows Steer. Use the same state contract in the mock.
Suggested fix
- turnRunning: mocks.turnId !== null,
+ turnRunning: mocks.isWorking,📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| turnRunning: mocks.turnId !== null, | |
| turnRunning: mocks.isWorking, |
There was a problem hiding this comment.
ℹ️ No critical issues — minor suggestions inline.
Reviewed changes
- Queue-paused header and Resume removed —
NativeChatQueuedMessageListno longer renders the status row or its button; the controller dropspause/resume/resuming, and the now-unused copy leaves all six locales. - Send vs Steer follows the turn —
queuedMessageCardSendNow(card, turnRunning)shows Steer only while a turn runs;turnRunningis threaded from the session'sisWorking. - Drain sends past held cards —
nextSendableQueuedCardskips held cards instead of stopping at the first, while a returned card still blocks; the drain pick, its consume guard, and admission all read the one function. - Tests — header-removal, Send-vs-Steer, and send-past-held cases added or rewritten; the client-side Resume test removed.
The host-side change holds up under adversarial tracing: no sequence sends a card past a returned one, strands a waiting card, or lets the queue's own origin: 'host' send lift a pause.
ℹ️ Nitpicks
src/renderer/src/components/native-chat/NativeChatQueuedMessageList.tsx:37— the outer box'sdivide-y divide-borderis now a no-op (the box encloses only theul, which carries its owndivide-y); the updated test already moved that class expectation to the list. Drop it from the box.
deepseek-v4.1-flash (free via Pullfrog for OSS) | 𝕏
… 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.
There was a problem hiding this comment.
ℹ️ No code issues found in the new commits — one description note.
Reviewed changes
Reviewed the delta since pullfrog's prior review (faaefcc → f91c1ae), which reverses the drain's own send to count as the person's turn.
- The queue's own drain now sends as the person's turn — the drain's
appendSubmissionwritesorigin: 'client'(washost), so once the provider accepts a card the queue sent past held cards,notePersonTurnAcceptedadvanceslatestPersonTurnSequenceand the held cards follow it. - Only a person's send becomes a card —
maybeQueueStructuredAgentSessionSendnow requires the host-localuserSendflag (set solely by the clientagentSession.sendRPC), so a host-internal send that asks to queue runs immediately instead of writing a card. - Tests and docs updated — the drain's consume seam now identifies its claim by
settledByOp === nullrather thanorigin === 'host', and the stop/restart/pause-lift tests assert the held cards follow the drained one.
I traced the origin change through notePersonTurnAccepted, journalQueueStopHolds, restartEnded, and the resume path; the drain's submission is indistinguishable from a person's queued card, which is the commit's stated intent, and no reader outside the pause derivation consumes a submission's origin.
ℹ️ The PR description still documents the behavior this commit reverses
The body's Mechanism bullet (The queue's own send of that later card is not a person's turn, so the earlier held cards stay held.), its "Differences from the common pattern" follow-up, and its Testing bullets ("...which stay held", "...the older one stays held") all describe the pre-f91c1ae behavior. After this commit the queue's own send lifts the hold and the earlier cards follow, so the description now contradicts the code and the tests it names no longer assert what it says.
Technical details
# Update the PR description to match `f91c1ae`
## Affected sites
- PR body, "The queue sends past held cards (host)" bullet — "The queue's own send of that later card is not a person's turn, so the earlier held cards stay held."
- PR body, "Differences from the common pattern" — "Sending a later card from the queue does not release the held cards ahead of it. Temporary." (listed as not done)
- PR body, Testing — "a card queued after the Stop sends past the cards it holds, which stay held" and "the older one stays held"
## Required outcome
The description and its testing section should state that a drained card's send is now the person's turn, so the cards it passed over follow it once that turn is accepted.deepseek-v4.1-flash (free via Pullfrog for OSS) | 𝕏
…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
There was a problem hiding this comment.
ℹ️ No new issues — this run reviewed only the merge of
maininto the branch; the prior review stands.
Reviewed changes
Reviewed the delta since pullfrog's prior review (f91c1ae → 7a28cd5), a Merge remote-tracking branch 'origin/main' with conflicts in five locale files.
mainmerged in, PR changes intact —main's drain-barrier removal, journal write-queue typing, and new locale copy are all present, while the PR'suserSend/origin: 'client'host changes and itsqueuePaused*/resumelocale removals survive unchanged. Confirmed by diffing the merge against each parent.- Locale conflict resolution kept both sides — the five conflicted files (
en/fr/ja/ko/zh) keep the PR's removals and addmain'sstructuredSessionHostDeclinedcopy;es.jsonauto-merged identically.verify:localization-catalogspasses. - Focused suites are green — the host queue/stop/pause suites (95 tests), the renderer queued-list suite, and the two
main-modified stop-row suites (59 tests) all pass on the merged tree.
The prior review's one open thread on NativeChatStructuredSession.test-harness.tsx:195 (the harness derives turnRunning from turnId rather than the production isWorking) is untouched by this merge and remains open.
deepseek-v4.1-flash (free via Pullfrog for OSS) | 𝕏
…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.
There was a problem hiding this comment.
ℹ️ No new issues in this delta. One prior thread (
NativeChatStructuredSession.test-harness.tsx:195, the harness derivingturnRunningfromturnIdrather thanisWorking) is untouched by these commits and remains open, so this stays a comment-only review.
Reviewed changes
Reviewed the delta since pullfrog's prior review (7a28cd5 → 4ef532e), which records each queued card's author so the queue's send of it is that author's turn.
- A queued card records its author —
queued_messagesgains anorigincolumn;QueuedMessageRowcarries it,/clearcarry preserves it, and the schema ALTERs it in at every writable open (nouser_versionbump). Legacy rows with no value read as'client'. - The drain sends as the card's author —
appendSubmissionnow writesorigin: next.origininstead of a constant, so a person's card releases a Stop's hold once its turn is accepted, while a host-authored card's send does not.maybeQueueStructuredAgentSessionSendrecordsoriginfromuserSendand drops thef91c1aeguard that had made host sends bypass the queue. - Tests — new
structured-agent-session-queued-card-author.test.tscovers a host card not lifting a Stop's hold vs a person's card lifting it, and/clearcarrying the author; the store suite adds the legacy-default and missing-column-heal cases; the stop/restart/pause suites are updated to the per-author behavior.
I traced origin through insert, /clear carry, the drain submission, notePersonTurnAccepted, and the store row mapping, and ran the touched host suites (86 tests) on the merged tree — all green.
deepseek-v4.1-flash (free via Pullfrog for OSS) | 𝕏
|
Review summary for this PR (head The problem. In a native chat you can queue follow-up messages while the agent works. If you press Stop, those messages are held instead of sending on their own. Orca showed a header row above them, "Queue paused because you interrupted" (or "…because Orca restarted", "…after you cleared the conversation"), with a Resume button. That row is an extra status line for a state you just caused and can already act on. The held cards also offered "Steer", although nothing was running to steer into. What changes for you (once the message queue ships; it is switched off today):
What the review fixed.
Deferred or pending.
Verified.
Not verified.
|
…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
|
Follow-up review summary (head What was still wrong after the first review. After Orca restarted, held messages looked exactly like messages waiting for the current turn, but they never sent. Messages you typed after the restart were held too, and Orca's own continuation turn never released them. After Stop or /clear, held messages sat in an idle chat with nothing on screen saying how to send them. Between queued turns, and right after releasing the queue, the composer button flickered (Stop → Send → Stop) and the working timer blinked off. What changes for you (once the message queue ships; it is switched off today):
How (in short). The host now publishes which hold covers each queued card, and which card it will send next. The client reads both instead of guessing. Both are new optional fields, so older clients and hosts keep working. Deferred (P3).
Verified.
Not verified.
|
There was a problem hiding this comment.
ℹ️ No critical issues — one open harness-fidelity thread re-anchored.
Reviewed changes
Reviewed the delta since pullfrog's prior review (4ef532e → 31995e4), which adds per-card holds, narrows the restart hold, adds the composer Resume, and has the host name the card its queue sends next.
- Per-card holds —
DerivedQueuePausebecomes a union (stopped/cleared/restarted); the published cards carry a newheldBy, and the client falls back to the whole-queuequeuePauseonly when the host predates the field. - Restart hold narrowed — a restart now holds only cards another host process wrote (
card.hostInstance !== pause.hostInstance), never one queued since;queuedBeforePauseis the single rule. - Composer Resume — the primary button becomes Resume (play icon) on an empty composer with a held queue and nothing running; the controller gates it through
queuedMessagesResumableand a ref guards a double press. - Host names the next card —
nextQueuedMessageIdrides the queue publication, derived from the drain's own pick/gate; the client readsqueueSendsNextand keeps a disabled Stop across the gap between a turn's end and the next queued send. - Tests — new/updated composer, resume, per-card-hold, restart-hold and next-card suites; all focused suites pass on the head.
I traced heldBy/nextQueuedMessageId through the host derivation, publication gate, subscriber frame, coalescer, reducer and client projection, and confirmed the drain and the publication share the same fence and host instance, so the named next card matches what the drain sends. Ran the focused delta suites (composer primary-action/field/actions, host pause/card-holds/stop-row, resume/gating/session, reducer + coalescer) — all green.
deepseek-v4.1-flash (free via Pullfrog for OSS) | 𝕏
…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
|
Review summary: the queue now behaves like a plain queue (head What was wrong. With the message queue switched on (it ships switched off today):
How the queue works now
What the review changed. It found and fixed:
The design was simplified at the same time: one rule ends every pause, the queue keeps strict order, there's no per-message author field, and the "paused" row hides while a turn that will lift it is on its way. Deferred (P3):
CI on
What the main merges changed here. Main's #24369 adds a "Stopping…" state to the Stop control; the message box keeps this PR's Resume beside it (Stopping… / Stop / Resume / Send), and Stop stays live only once a turn can be stopped. Main's #24660 keeps a message you sent but the agent never received (Orca quit, crashed, or the chat closed first) as a card. To pick those messages, it reads which side asked for each send: you, or Orca itself. This PR had removed that record as unused. It is back, but it only decides which unsent messages become cards; any accepted turn or Resume still ends a pause. Main's new tests for those cards now expect the restart's hold to stay unpublished, as this PR has it. Verified:
Not verified: a real Claude or Codex agent (QA used a stand-in), mobile, and SSH. |
…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).
…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.
…s-after-stop' into brennanb2025/stop-queue-after-stop Composer: Stop reads Stopping… while this client's Stop is in flight (#24369) and gives way to Resume when the queue is held and idle (#24586). Stop is offered while the queue sends next but live only once a turn can stop. queued-stop-row test takes #24586's version.
…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.
…ueue-after-stop Rebuilt as main plus this branch's own changes against its old base: the branch carried #24586's unsquashed history, so main's squash conflicted on every shared file. The person's-Stop mail check now reads the message body's sender (`from`, from #25888) and the separate card `source` field is gone; the moved queue step takes main's fingerprint and its replay answer.
…brennanb2025/nc-host-send-queue - Queue UI (#24586) taken as main has it: paused header, Resume, Steer, clear-queue question, `queueSendsNext` working state, composer Resume and queue hold. - The chat's send answers 'queued' when the transcript draws it as a card (main's rule, read through the in-memory sends' own projection), so the composer moves the reader only for a bubble. - The failed-start send wrapper passes that answer through; the submit reveal hook drops the per-message Retry this branch removed (launch Retry still reveals). - The pending-send projection takes any send shape, so the node project's queued-card test does not reach the renderer store. - Deleted outbox files stay deleted; #24514's queued-card admission test is ported to the sends hook.

ELI5
In a native chat you can queue follow-up messages while the agent works. Each one waits as a card above the message box and sends by itself, in order, when the agent finishes. After you press Stop or run /clear, Orca pauses the queue: a row above the cards says why ("Queue paused because you interrupted"), with a Resume button.
What was wrong.
How the queue works now.
Who sees this: the message queue ships switched off today, so none of this reaches users until it is switched on.
What Changed
Before (main).
After.
Mechanism.
Any accepted turn (host). The reducer keeps
latestAcceptedTurnSequence: the submission row of the latest turn the agent accepted, from any sender. Stop's and /clear's pauses end once such a turn comes after them, or on Resume. Main counted only submissions recorded as a person's; the submission'soriginfield and the host-localuserSendflag that fed it are removed. Journals written by earlier builds keep anoriginkey that nothing reads.After a restart (host). While a card written by an earlier Orca process waits, every waiting card waits until a turn is accepted in this conversation, from any sender. Meanwhile the host publishes no pause (a Stop's or /clear's row from before the restart included) and no next card, so the cards look like ordinary waiting cards and opening, reconnecting or reopening the chat sends nothing. The
'restarted'reason is removed from the wire type, the desktop row text, the six locale catalogs and the mobile label; no user ever saw that row, because no client can queue a card without the queue's capability.Quit (host). Host teardown now stops the queue (
StructuredAgentSessionQueuedMessageDrain.dispose, called where teardown already stops conversation delivery), so nothing is handed off while the host quits. Before, a card handed off during the quit was refused, and its refused send counted as the chat having moved on past its restart offer.The paused row while a turn is on its way (host). The host publishes no pause while a turn sent after the pause began is still waiting for the agent's answer (
queuePauseLiftOnItsWay): your message, Steer on a card, or Orca's own. That turn's acceptance lifts the pause; a refusal shows the row again. A send made before a Stop never counts. The host decides this, so it covers your message, Steer and Orca's own messages on every client, the phone included.Strict order (host, unchanged from main). The queue sends the oldest waiting card, and stops at the first paused or refused card; nothing overtakes it.
The next card (host and client). The queue publication carries
nextQueuedMessageId: the card the queue will send next, from the queue's own pick through its own gate (nextStructuredQueuedMessage, which the queue step itself calls). It is empty whenever the host would not send: a pause, a returned card ahead, a waiting question, a conversation a /clear replaced, a rewind whose outcome is unknown. The desktop reads "about to send" from it, so the working status, the buttons and the cards do not flip to idle between a turn's end and the next card.Resume on the message box (client).
nativeChatComposerPrimaryActionpicks one primary button: Stop while a turn runs; Resume when the box is empty and Resume is offered; otherwise Send. It calls the existingagentSession.queuedMessagesResume, and shares one in-flight guard with the row's Resume."Send message?" (client). While the message box offers Resume, the controller also offers
queueHold(the card count and aclearthat deletes each shown card in turn, stopping at the first failure).useNativeChatHeldQueueComposerSendwraps the composer's send, so Enter and the send button openNativeChatQueueSendConfirmDialoginstead of sending. Commands Orca runs itself (/clear, /compact, /model) skip it. The waiting choice remembers the hold it was asked under: if that hold is gone (the pause ended, or the cards changed), the dialog closes and no choice sends; the next Enter sends or asks again. A choice is taken once, and only the sent text leaves the composer once the message is accepted (main's draft rule).When the message box offers Resume and the question (client). Only while the paused row shows, nothing is running, and no question or approval from the agent is waiting. The row's own Resume stays available. While a turn runs, Enter queues as usual behind the paused cards. A waiting question holds the host's queue too; the message box shows beside one only when this build cannot answer it.
Why
The goal is the normal queue behaviour: it does not matter who sent a message to the agent, and the rules stay simple. A restarted chat sends nothing until it carries on, by Orca's carry-on or your own message, and then the queued messages follow in order.
Alternatives considered:
Differences from the common pattern
Each item says what Orca does and whether it is intended or temporary.
nextQueuedMessageIdand adds the message-box Resume and the question; not yet opened.Linked Issue
N/A (internal native-chat queue behaviour)
Visual Proof
The message queue ships switched off today. It was switched on in the test build only, so these screens show what users get once it ships.
Captured on the Windows test machine in an isolated, hidden test build with a stand-in Claude agent, at
7c561ee503b. Every restart below is a real quit (the app's own Quit) followed by a relaunch of the same profile. A long reply was running, and "Second message" and "Third message" were queued.After a restart
After Stop
The build shows no other new pop-up, banner or label. The "Enjoying Orca?" prompt seen in some shots is existing Orca UI.
Testing
A real quit (new
structured-agent-session-queued-quit.test.ts): cards queued behind a running turn, then the host's own quit teardown and a relaunch as a new process. Nothing was handed off during the quit, the chat still offers to carry on, nothing sends by itself, and carrying on sends Orca's carry-on, then A, then B.After a restart (
structured-agent-session-queued-card-holds.test.ts, through the real host with a test client folding every update as the chat does):The paused row while a turn is on its way (same file): your message over a paused queue, and Steer on a paused card, each hide the row from the moment the host records them; the cards follow once the agent accepts; a refusal brings the row back over the cards still paused.
What sends paused messages again.
queued-message-pause.test.ts: any turn sent after the Stop and accepted lifts it, Orca's own mail included; one sent before the Stop does not; any accepted turn lifts /clear's pause, a launch prompt included.structured-agent-session-queued-stop-row.test.ts: Orca's mail accepted after a second Stop releases the cards; your send made before that Stop does not.structured-agent-session-queued-messages.test.ts: an Orca-sent turn after a Stop lifts it once accepted; after a restart nothing sends or shows across a reopen, and your next send releases the cards.structured-agent-session-queued-pause-lift.test.tsandstructured-agent-session-queued-stop-row.test.ts: after a restart a Stop from before it shows no row; a turn, or a Resume from an older client, releases the cards.Strict order.
queued-message-pause.test.ts(main's: the queue's own send refuses a newer card behind a paused one);structured-agent-session-queued-stop-row.test.tsandstructured-agent-session-queued-card-holds.test.ts(a card typed while the Stop lands waits behind the paused one; Resume sends both in order).Desktop client.
use-structured-agent-session-queued-messages.clear.test.tsx: the question is not offered while a turn runs, though the row shows.native-chat-queue-send-confirm.test.tsx: the open question closes when the pause lifts under it, sends nothing and keeps the draft; the next Enter sends, and a later pause does not bring the old question back (with the previous dialog code this test fails); only the sent text leaves the composer.use-structured-agent-session-queued-messages.resume.test.tsx: no message-box Resume or question while a question from the agent waits; an idle chat with cards and no published pause shows no row, Resume or question.structured-agent-session-queued-cards.test.ts,NativeChatQueuedMessageList.test.tsx: a paused queue holds every card under one row; no row without a published pause.native-chat-queue-send-confirm.test.tsx,native-chat-composer-field-resume.test.tsx,native-chat-composer-primary-action.test.ts,use-structured-agent-session.queued-gating.test.tsx: the message-box Resume, "Send message?" (each choice, taken once, Clear queue's ordering and failure) and the no-flicker working state.Ablations at
7a61051caf2, each run once and restored (tree verified clean after each).Runs (vitest, explicit files). At
7cc68c2e68d(main merged at5b8a982f8f7): 505 files, this PR's list plus every test main changed under native chat, the runtime and shared agent-session code, including main's new rewind tests and every test of the chat session hook. 5044 tests passed, none failed. 34 files could not load locally because main'sstream-jsondependency is not installed in this checkout (CI installs it). The mobile tests could not run locally (no mobile install in this checkout); CI covers them. The ablations above ran at7a61051caf2; the paused-row ablation was re-run atac624ba91db, and the quit ablation at738270b49b6.Checks. At
7cc68c2e68d: the web, node and both CLI typechecks (run without the incremental cache) report only main'sstream-json/stream-chainmodules, which this checkout lacks. oxlint, oxfmt, the changed-code quality gate (React Doctor: no new findings) and the three localization checks pass. An earlier merge commit,4b50771f30e, failed CI's typecheck on one test that still passed a composer prop main had renamed;439545e1242fixed it.CI. On
7cc68c2e68d: 18 pass, 14 skipped, none failed.AI Disclosure
Review
Agent skill upstream boundary
docs/reference/agent-skill-sharing-upstream-boundary.mdand copies or mechanically translates no upstream skill-installer source, tests, fixtures, registry entries, path tables, comments, or documentation.Notes
nextQueuedMessageId, beside the queued list on the existing stream and history replies, with no new message type; clients read it only when present. The queue's rules run wherever the conversation's host runs. Resume and Clear queue use the existing requests.origin; it is ignored.Checklist
N/Awith reasonpnpm lint,pnpm typecheck,pnpm test, andpnpm buildpass (or CI will cover; local preferred)