Repository navigation
fix(native-chat): a message stopped before the agent got it comes back as a paused card - #26827
brennanb2025 wants to merge 4 commits into
Conversation
…eued card A person's Stop pressed before the agent had a message rejected it and left the bubble in the conversation with "Stopped manually", even though the agent never received it, so the screen and the agent disagreed about what was sent. A restart and a chat close already settle such a host-held send by keeping a person's text as a card at the head of the queue. Stop now does the same: holdUnsentSends gains the `cancelled` cause and the Stop's withdrawal uses it, issuing every row at once so the fold shows nothing queued when it returns. The kept card waits under the existing Stop pause; Resume or the person's next accepted turn releases it. Images, commands, mail and a card's own hand-off are rejected as before. A child that dies in its start after a Stop keeps its unrun person's send the same way, so unrunRejection and the dead rejectPendingSubmissions go. The projection no longer draws a kept send as a stopped bubble; the card is its only surface.
On a ready, idle agent the Stop awaited its withdrawal before issuing its event, so clients got one frame with the kept card waiting, unpaused and next to send. The event is now issued in the hold's tick, so one frame carries both. Also: a turn-membership test for the kept-send exclusion, a test pinning that a failed Stop record leaves the kept card unpaused, the dead rejectQueuedSubmissions helpers removed (their tests ported to holdUnsentSends or resolveDispatch), and the queued-messages capability comment corrected.
Review summaryThe problem. In native chat, if you send a message and press Stop before the agent picked it up, the message stayed in the conversation as a bubble marked "Stopped manually", even though the agent never received it. Ask "what was the last message I sent?" and the agent answers as if that message never existed, so the screen and the agent disagree. When Orca restarts, or the chat closes, before a message reaches the agent, Orca already keeps it as a queued card. Stop was the only case that rejected the message and left a bubble. What changes for you. A message stopped before the agent got it now comes back as a queued card above the composer, under the existing "Queue paused because you interrupted" header. Your next typed message goes first, then the card is sent exactly once. You can also Resume, edit or delete the card. A message the agent may already have received keeps today's bubble and "Stopped manually" row. Image sends and commands are rejected as before. Review findings (one independent review of the first commit):
Verified:
Not verified: real agent CLIs; SSH, WSL, Linux and Windows hosts; the phone on a device; and a real older client against this host. |
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configuration
📒 Files selected for processing (6)
🚧 Files skipped from review as they are similar to previous changes (1)
Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 8 remain after this review. 📝 WalkthroughWalkthroughStop handling now settles unsent sends as rejected submissions that can retain queued-message cards. Eligible cards remain waiting under the stopped pause and are not dispatched by Stop. Stop orchestration uses the hold operation, and the journal removes its direct pending and queued rejection methods. Message projection excludes kept-card sends from stopped-before-start rows and turn anchors. Added tests cover journal settlement, Stop lifecycle cases, projection, and mobile snapshot parity. Priority: ➖ Normal Merge Risk: ⚪ Minimal · up to This change keeps a text message stopped before the agent received it as a paused queued card. The supplied incremental changes only add test coverage and update a documentation comment, and no concrete merge-blocking issue was identified. Pre-merge checks |
|
…ps-unsent-as-cards
…r is kept as a card Main now carries a new chat's first message in its create request. A Stop before the agent is handed that message keeps it as a card at the head of the queue under the Stop's pause, like any person's held send: no provider write, no bubble, sent once on Resume or after the next message. The create flow puts nothing back in the composer for it, so the card is the one surface.
|
CI on the final head
All other checks pass. Locally: every test file under |
ELI5
In native chat, if you send a message and press Stop before the agent has picked it up, the agent never gets it. Until now the chat still showed that message in the conversation, marked "Stopped manually", so the screen said you had sent something the agent never saw. Now the message comes back as a queued card above the composer, paused, and you choose what happens to it: send it, edit it, or delete it.
What Changed
The problem. Send "test" and press Stop while the agent is still starting. The "test" bubble stays in the conversation with a grey "Stopped manually" row. Then ask "what was the last message I sent?" and the agent answers as if this were your first message, because "test" never reached it. The screen and the agent disagree.
The cause is that undelivered text was drawn as conversation. Orca already handles the same never-delivered message differently in two other cases. If Orca restarts, or the chat closes, before the message reaches the agent, the message is kept as a queued card at the head of the queue. Stop was the odd one out: it rejected the message and left the bubble.
Before / after (hidden dev app with stand-in agents; BEFORE is
mainat cbad641, AFTER is this branch):Before: the "test" bubble shows "Stopped manually". The stand-in agent's input log shows only the question. "test" was never sent, yet it sits in the conversation.
After: no bubble. "test" is a card under the existing "Queue paused because you interrupted" header, with Resume and the card's usual Steer / Delete / More actions. Your next typed message goes first. Once the agent accepts it, the pause lifts and the card is sent next, exactly once. The agent's input log shows the question, then "test".
A new chat's first message works the same way. Start a chat with a message and press Stop while its agent is still starting: the message comes back as a card under the pause, not as a "Stopped manually" bubble, and nothing is put back in the composer.
Resume sends the card once:
Delete drops it, and it is never sent:
Reload: the card and its pause survive:
Unchanged: a message the agent may already have received keeps today's bubble and "Stopped manually" row exactly. That covers a message handed to Claude before its interrupt, a Codex send withdrawn before its turn opened, and a steer already written. Image sends,
/compactand other commands, orchestration mail, dispatch preambles, restart continuations and a card's own hand-off are rejected exactly as before. Nothing new appears on screen: the card, the header, Resume and the card actions already exist on desktop and phone.Mechanism.
holdUnsentSends(journal-unsent-send-hold.ts), the routine that restart and close already use, gains acancelledcause. That cause selects every send the host still holds and has provably not written to the agent. The existing rule decides what is kept: a person's text-only, non-command message becomes a card at the head of the queue. The card is written in the same database transaction as the send's rejection, and the rejection names the card (keptAsQueuedMessageId). If inserting the card fails, the send falls back to the plain rejection, so the Stop still completes.structured-agent-session-chat-stop.ts) uses that hold in place of its old reject-everything call. The interrupt never waits on it. The Stop event, which the queue's pause is derived from, is issued in the same tick as the hold. So clients never see a frame where the card is waiting, unpaused and next to send (that was review finding P2-1). A side effect: if the withdrawal throws after it landed, the card it returned now waits under the Stop's pause until Resume. On main it would resend on its own.structured-agent-session-child-exit.ts).unrunRejection,rejectPendingSubmissionsandrejectQueuedSubmissions(their tests now go throughholdUnsentSendsorresolveDispatch).No new persisted field, no wire change, no new capability, no agent-adapter change, and no new copy or notice.
Why
The fix models the concept rather than patching the symptom. Text the agent never received is not conversation, so it is drawn where every other unsent message already lives: the queue. Stop now goes through the same path restart and close already use, so all three cases that leave a message unsent behave the same way. The alternative was to keep the bubble and carry the stopped text to the agent inside the next message. That builds a second delivery mechanism, it sends the agent text the person may no longer want, and its framing has to be hidden from the person's own bubble. This change instead reuses the card, the pause and Resume that already exist, and leaves the choice with the person.
Differences from the common pattern:
Known edge (rare storage failure): if the Stop's own record fails to save, the kept card is not paused, so it sends like any other card, once. A test pins this. The alternative was writing the Stop event in the same transaction as the first card. That needs a new row-writer path and a rework of how all four Stop branches record their event, which is too large for this change.
Linked Issue
None (maintainer change, no tracking issue).
Visual Proof
See the before/after table above. Hidden Orca dev app (
ORCA_BACKGROUND_LAUNCH=1, fresh profile, mock keychain), driven through Playwright CDP, with stand-in Claude and Codex agents that log every write. Each screenshot was read and checked against the stand-in input log.Testing
Hidden QA (macOS), on main and on this branch:
Automated:
/clearand rewind;/compactand images rejected as before;stoppedpause;stoppedpause, no bubble), sent exactly once on Resume or after the next message. A renderer test pins that the composer stays empty, so the card is the only place the text appears.src/main/native-chat/(370 files) passes, and so do the shared, renderer native-chat, launch and RPC agent-session tests (552 files) and the mobile queue/projection/parity tests (87 files).tc:node,tc:web,tc:cliand the mobile typecheck pass.orca-ci-checkspasses 22/22.Not verified: real agent CLIs, SSH and WSL hosts (this is host-side bookkeeping with no wire change), Linux and Windows, the phone app on a device, and an actual older client against this host.
AI Disclosure
Review
An independent review found one P2 and four P3s. Every one is fixed or labelled above:
Agent skill upstream boundary
Notes
Author: @BrennanKB5
Checklist
N/Awith reasonpnpm lint,pnpm typecheck,pnpm test, andpnpm buildpass (or CI will cover; local preferred)