Repository navigation
Native chat: /clear waits in line while the agent works - #26730
brennanb2025 wants to merge 9 commits into
Conversation
A /clear sent while the agent is working (a turn, an unanswered message, a pending question) was refused. Behind a host advertising agent-session.queued-clear.v1, it now waits as a /clear card like a queued /compact. When its turn comes the host runs it itself through the same-chat clear transaction: the card is spent in the clear's own write, cards sent after it run in the fresh context in order and unpaused, background tasks or a handoff keep it waiting, and any other failure returns the card with its reason so Send retries and Delete takes it back.
…ext send The host published the waiting /clear as nextQueuedMessageId, so clients read the chat as busy until the tasks ended.
A /clear card waiting on background tasks or a handoff is now decided in
one place, the queue's gate (structuredQueueHead + queuedClearWait). The
drain, its cheap pre-check, the published queue and admission all read
it, so a background-task update while the card waits runs no drain step,
and a /clear typed while idle with background tasks waits as a card too
instead of being refused.
The host publishes the wait beside the queue (nextQueuedMessageWait, a
new optional field). Desktop and phone take "no Send" and the waiting
caption only from the host's next card or wait, so a /clear held after a
reopen shows Send like /compact and Send runs it. The caption names the
strip's Stop when the strip offers one ("Stop them to clear now"). A
message sent while the card waits is a sending card at once, not a
transcript bubble. The desktop runner still holds a /clear behind a
rewind on its way.
The serialized drain moves to structured-agent-session-queued-drain.ts.
…its-same-chat # Conflicts: # src/renderer/src/components/native-chat/NativeChatQueuedMessageCard.tsx
In a Codex or ACP chat the background-tasks strip has no Stop, so a /clear waiting on a dev server waited forever and held later messages behind it. The host's one wait rule now counts background tasks only when the strip offers a Stop for them; otherwise the /clear is refused at once as before, and one already queued is returned with why when it reaches the front.
After the strip's Stop, Claude acknowledges the stop before the task's own "stopped" notification, and the host's record settles on the acknowledgement. The waiting /clear ran in that gap and shut Claude down before the task's ending (row read "status unavailable") and the turn Claude starts answering it (red "stopped while this response was in progress" line). The /clear's one wait rule now also holds while the provider still owes the ending of a task it acknowledged stopping (Claude: a task its decoder ended on the acknowledgement that the CLI still lists running). That ending opens the agent's turn, which the queue already waits behind.
…owed ending Adds a scrubbed Claude CLI 2.1.280 capture of a background shell's Stop at idle and a replay through the real adapter and hook server: the CLI writes the task's own ending before it answers the Stop, so the owed-ending wait never opens, and the turn the ending opens completes at the CLI's idle. Rewords the comments that said no production code reads the background-task tracker: it now tells whether a stopped task's own ending is still owed.
Review summaryThe problem. In native chat, What changes for you (behind the queued-messages setting, which is still off in shipped builds):
How. The host runs a Fixed during review (four review rounds, design challenger, desktop + phone + real-Claude QA, readiness checklist): one waiting rule instead of two copies; a Deferred / known limits:
Verified: rendered desktop QA (hidden test app, stand-in agents) rows 1–8 passed; phone QA on the iOS simulator rows 1, 2, 3, 5, 6, 7, 8 passed; with the real signed-in Claude CLI 2.1.280, Not verified: SSH, rendered Windows/Linux, mixed old and new client/host versions live, Android, a real Codex run, phone row 4. |
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configuration
📒 Files selected for processing (74)
Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 3 remain after this review. 📝 WalkthroughWalkthroughThe change adds host capability support for queued Priority: ➖ Normal Merge Risk: ⚪ Minimal · up to No confirmed merge-blocking issue remains. A waiting clear retains its queue order across new messages, and reconnecting clients receive its wait state. 🚥 Pre-merge checks | ✅ 4 | ❓ 1❌ Failed checks (1 inconclusive)✅ Passed checks (4 passed)Full details: Docstring CoverageExplanation Docstring coverage is 44.64% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 56 functions across 50 files. (24 skipped: 7 unsupported, 17 over the file limit.) ✨ Finishing Touches 💡 2📝 Generate docstrings 💡
🛠️ Fix failing CI checks 💡
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 |
|
Superseded by #27928, a smaller rebuild of the same feature. |
ELI5
In native chat, typing
/clearwhile the agent is still busy (a reply is running, a message hasn't been answered yet, or the agent is waiting on your answer to a question) is refused today with "The agent is still working. Run /clear when it's done." You have to wait and type it again.With this change,
/clearwaits in line like any other message. It shows up as a "/clear" card in the queue above the composer. When its turn comes, Orca clears the chat's context itself, in the same chat and tab, and anything you queued after the/clearthen runs in the fresh context, in the order you sent it.Supersedes #25842, which built the same thing on the old "clear opens a replacement chat" design. Main has since moved
/clearto keep the same chat (#26579), so this is a smaller rebuild on top of that.What Changed
What you see (desktop and phone)
/clearwhile the agent works → a refusal line under the composer; nothing is queued./clearwhile the agent works → a "/clear" card in the queue. The card has Delete. It has no Edit and never steers into the running turn. When the agent finishes, the card disappears and one "Context cleared" divider appears in the same chat (the existing divider from Keep native chat /clear in the same conversation #26579). The cards behind it then send one by one into the fresh context./clearstays waiting behind it until you Send or Delete the/clear(the queue's existing rule for a returned card). Messages you type after it came back go to the agent as usual./clearwhile the agent is idle but background tasks are running follows the same rule: with a Stop on the strip it becomes the waiting card; without one it is refused at once and nothing is queued, and you can keep chatting (for example, ask the agent to stop the server). One wording change on desktop: the refusal line now shows the host's own words, as the phone already did (rendered QA read: "Background tasks are still running. The command didn't run. Wait for the background tasks to finish."), instead of main's desktop line "Wait for pending work and messages to finish before using this command."/clearruns only once the stopped task has reported its own end and the agent has finished any short reply it gives about it. Before this round the clear ran the moment Claude acknowledged the Stop, which cut both off: the stopped task's row read "status unavailable" instead of "stopped", and a red "Claude stopped while this response was in progress…" line could appear just before the divider./clearcard's caption now wraps, so "Stop them to clear now." is shown instead of being cut off after one line (other hold captions stay one line)./clearcard waits goes straight into the queue as a card behind it (for the cleared chat). It no longer appears briefly as a chat bubble first./clearcard that was waiting is held until you act (as every queued card is after a reopen). It now shows Send, like a/compactcard does in the same state, and Send runs the clear. Before this round it showed neither Send nor a caption and never ran./clearcard that is next in line and about to run shows no Send button, so no Send flashes at the end of a turn./clearwhile the agent is idle and nothing else runs clears at once, exactly as today.All of this sits behind the queued-messages feature (
agent-session.queued-messages.v1), which is still dark. So nothing changes for users until queued messages ship. A client that renders the queue can also use this.Mechanism
/clearcommand request may now carrydelivery: 'queue-if-active', as/compactalready can. The host runs the queue's own accept rule first (maybeQueueStructuredAgentSessionSend): if a send would wait right now, the/clearbecomes a card ({ command: { name: 'clear' } }, text/clear), and the request answers at once with the queued receipt. It uses the person's byte reserve, like a queued/compact. Otherwise today's immediate clear runs.agent-session.queued-clear.v1. Clients senddeliverywith/clearonly when the host advertises it and queued messages. A host without it fingerprints a/clearwithoutdelivery, so it would refuse one sent with it; older clients never send it. One new optional field rides with the published queue:nextQueuedMessageWait: { messageId, reason: 'background-tasks' | 'handoff' }, set instead ofnextQueuedMessageIdwhile the queue's next card is a/clearwaiting on one of those. Older clients ignore it; a newer client talking to an older host reads it as absent./clear, the drain never turns it into a message for the agent. It runs the clear itself, inside the same serialized step: admission check, stop the agent, check again, then commit. The commit is Keep native chat /clear in the same conversation #26579's single clear transaction with one addition: the/clearcard is withdrawn in that same write, and the write rolls back if the card is no longer there. So the clear and the card's settlement happen together or not at all, and a card can't run twice or vanish without its clear. The shared steps (check, stop, recheck, commit) are now one function (clearConversationUnderSerialize) used by the immediate/clear, the drain and the card's Send./clearcard. Cards queued after it were sent for the fresh context, so they're left alone: no pause, no withdrawal. The drain then sends them in order as usual. For a drained/clearthe cards ahead are only ones held on their own (a failed send), so in practice this pauses nothing./clearcard at the head of the queue waits while a handoff is in progress, or while background tasks run that the strip offers a Stop for" is one predicate (queuedClearWait). The Stop part is the strip's own rule (agentChildWorkStripOffersStop, with the provider'sbackgroundTaskStops), the same one the strip draws its Stop by and the host's refusal wording reads, so the card waits exactly when the person can end the wait. The predicate is read by the queue's one gate (structuredQueueHead). The drain, the drain's cheap pre-check, the published queue and admission all read that gate, so they can't disagree. The drain never runs a waiting card, so a background-task update while it waits costs a couple of map lookups, not a serialized step that renders the whole journal only to be refused. Admission uses the same predicate, which is why an idle/clearwith stoppable background tasks becomes a card too, and one with unstoppable tasks gets today's immediate refusal. A queued/clearthat reaches the front with unstoppable tasks is not a wait, so the drain runs it, the clear is refused, and the card is returned with that refusal, once. Nothing is stored for the wait: the drain is woken when child work changes and when a handoff ends (a small lease listener in the record store), and re-reads the gate. Any other refusal, or a throw before the commit, returns the card with acommandRefusedfact. A returned card already blocks the cards behind it (the queue's existing rule).stoppedTaskEndingOwed. For Claude it is a task the child-work decoder ended on the acknowledgement that the CLI's own running list still names. When the ending lands, the frame opens the agent's turn answering it (existing behavior, journaled before the child-work update), and the queue already waits behind a running turn, so the clear runs once the agent is idle again. Nothing is stored and there is no timer: it is per-session memory re-derived from the provider's frames, and it ends with the task's own ending, the CLI dropping the task from its running list (a task the CLI acknowledged with no frame owes no ending), or the provider session ending. Codex and the other agents have no background Stop, so for them it is always false./clearcard hides Send ("runs on its own") and whether it shows the background-tasks caption now come only from what the host publishes: the card it names as next (nextQueuedMessageId) or the card it says waits (nextQueuedMessageWait). Before, each client picked "the next card" itself, which went wrong when the host held the queue for a reason it doesn't publish (after a reopen). Whether the caption mentions Stop follows the background-task strip's own rule for offering Stop, now shared with the host's refusal wording (agentChildWorkStripOffersStop). Desktop counts "the host's next card waits" as "a new send joins the queue", so that send is drawn as a sending card, not a transcript bubble.origin/main(8167dda) is merged in to resolve a one-file conflict with fix(native-chat): read a queued message in full #26004, which re-wrapped the queued card's layout; the card keeps main's layout with this PR's Send condition.structured-agent-session-queued-messages.tsintostructured-agent-session-queued-drain.tsunchanged apart from the gate call, to keep the gate file under the line limit./clearcard goes to the same clear runner, never to the agent. A command card's existing "not while the agent is working" guard still applies.orca:queued-clear). No new column; the card's own withdrawal receipt answers a resent request.Text typed during a running
/clear: this used to be lost because the draft was saved under the old chat. Since #26579,/clearkeeps the same chat id, so the draft key doesn't change and the text stays. This holds by construction; I didn't exercise it in a running app.Why
The common pattern queues
/clearlike any message and runs it on the host when its turn comes, with later messages going into the fresh context in order. Building it on the queue we already have (the/compactcard from #25704, the drain, the published card list) means no new client surface and no new stored state. Using #26579's clear transaction for the drained clear means one place decides what a clear does.Alternatives considered:
/clearcard to the agent like/compact. Rejected: a clear isn't something the provider runs. Orca resets the provider context itself, and the handover path refuses a/clearcommand./clearas an immediate clear does. Rejected: those were sent after the/clear, for the fresh context. Pausing them would make the user resume messages they already asked to run./clear(it would then clear them too). Rejected: the queue never reorders, and the/clearwould wipe what the person just asked.Differences from the common pattern:
/clear. Send retries it, which matches Orca's queue policy for every returned card./clearwait rather than fail, and it runs by itself when they end./clearwith background tasks running is refused at once (idle) or returned once (queued), instead of waiting as a card. This lasts until a separate change adds a background Stop for Codex and the other agents connected the same way; then /clear waits as a card for every agent./clearcard over a paused queue (after a Stop) treats the cards queued before it like an immediate/cleardoes (paused until the next turn or Resume). The queue never reorders, so the cards after it wait behind those.agent-session.queued-clear.v1keeps today's refusal. Remove the client fallback once supported hosts all advertise it./clearcard and goes to the agent in the uncleared chat, because image messages never join the queue yet (queued messages are text-only). This gap already exists for a mid-turn/compact; a/clearwaiting on background tasks makes the window longer. Follow-up: images in the queue./clearruns and clears that exchange too. This is the same rule as every held card (a new message after Stop goes first, and desktop's "Send message? clear queue" dialog asks about the held cards). Changing it for/clearalone would make one card kind follow different queue rules.Linked Issue
N/A — maintainer follow-up (supersedes #25842).
Visual Proof
Before: no screenshot. None of the QA runs captured main's refusal. On main,
/clearwhile the agent works is refused with "The agent is still working. Run /clear when it's done.", and nothing is queued.After: captures from three QA runs, all in hidden rigs on a Mac (the windows were never on screen):
4f0278c6e10, with stand-in Claude and Codex programs in place of the real CLIs.bc329fff577.What changed after each head:
4f0278c6e10:5482fc822fe, the Stop timing fix. A waiting/clearnow also waits for the stopped task's own ending. That changes only what happens after you press Stop, so shots 3 and 7 (taken before Stop) still show current behavior. Shots 9 and 10 show it with the real CLI.bc329fff577, the phone caption wrap. Shot 7 still shows the old one-line cut.f2827798c4d, a test that replays a captured real Claude Stop, plus two comment edits. No behavior change.bc329fff577: onlyf2827798c4d(test and comments only).Desktop (macOS), stand-in Claude, head
4f0278c6e10/clearsent mid-turn becomes a card with only Delete and More (no Edit, Steer or Send). Two messages are queued behind it, and unsent text sits in the box./clearcard reads "Waiting for background tasks to finish. Stop them to clear now." and has no Send. The expanded strip shows Stop. A message sent meanwhile joined the queue as a card, not a chat bubble.Desktop (macOS), stand-in Codex (no background Stop), head
4f0278c6e10/clearwhile idle with a dev server running is refused at once, and nothing is queued. The toast at the bottom right is an unrelated existing notice./clearqueued mid-turn comes back once when the turn ends, with the reason in red plus Send and Delete.Phone (iOS 26.5 simulator, Release build), stand-ins, head
4f0278c6e10bc329fff577)/clearcard returned with its reason, Send and DeleteDesktop (macOS), real Claude CLI, head
bc329fff577/cleartyped while idle, with a real backgroundsleep 600running. It becomes the waiting card, with no Send.Testing
Desktop, head
4f0278c6e10(macOS, stand-in Claude and Codex, driven by scripted page automation): all 8 rows pass./clearsent mid-turn becomes a card. Delete removes it. When its turn comes it clears the same chat, and the follow-ups run in order./clearis refused, and a queued one comes back once./clear, keeps its own Send while the clear runs./clearcard is held, and Send runs it./clearwaits appears as a card, never as a chat bubble.This run found the Stop timing problem. The stopped task read "status unavailable", or a red "Claude stopped while this response was in progress…" line appeared before the divider.
5482fc822fefixes it.Phone, head
4f0278c6e10(iOS 26.5 simulator, Release build, stand-ins): rows 1, 2, 3, 5, 6 and 8 pass. Row 7 passes as an approximation: the text was typed while the clear waited, not while it ran. Row 4 (a held card ahead of the/clear) was not run. This run found the same Stop timing problem and the caption cut off after one line. Both are fixed (5482fc822fe,bc329fff577).Real Claude CLI 2.1.280, head
bc329fff577(macOS, signed-in CLI, Orca isolated from the real home): rows A, B and C2 pass./clearwith a real background task waits. After the strip's Stop it runs 0.65 s later, the task reads "stopped", and there is no red line. A follow-up question confirmed Claude saw no earlier messages.The raw stream frames showed the same order in all 4 Stops: the task's own "stopped" frame arrived before Claude acknowledged the Stop. So with the real CLI, the case the fix guards (acknowledged but not yet ended) never opened.
f2827798c4dreplays that capture as a test.Left as is:
/clearstay held until you act, and they show Steer on an idle chat. This is the queue's existing rule for held cards, labelled intended above.Not run: SSH remote hosts; Windows and Linux rendered; mixes of older and newer client and host versions; Android; real Codex; injected stop or save failures; a real-Claude Stop of a task that had already finished. No QA re-ran on
f2827798c4d, which changes only a test and comments sincebc329fff577.structured-agent-session-queued-clear.test.ts, 16 cases): becomes a card mid-turn with nothing stopped; runs in the same chat with one divider; the card is spent in the clear's write and never sent to the agent; cards after it run in order, unpaused, in the fresh context; a resent request answers from the card, then as withdrawn, with one clear; a restart never runs it again; Send mid-turn is refused; Delete; idle clears at once; an older client is still refused; it waits out background tasks, publishes the wait, and runs when they end; typed while idle with background tasks, it becomes a waiting card and a later message queues behind it; background-task updates while it waits run no drain step (no journal render); after a reopen it publishes no next card and no wait, and its Send runs it; a failed commit rolls back whole, returns the card withcommandRefused, holds the cards behind it, and Send retries; Delete of a returned card lets the rest run; Send over a stopped queue pauses only the cards before it./clearwith background tasks asks the host to wait; a rewind on its way still holds it; an older host still refuses), card states from the host's facts (wait caption with and without Stop, handoff wait, no Send on the card the host names, Send kept on a card held after a reopen), rendered card list, and session-hook gating (chat reads idle while the card waits; a message sent then is a sending card, not a bubble)./clearis refused at once with today's sentence and the next message goes straight to the agent; a live task the strip offers no Stop for refuses it even when the provider can stop others; a mid-turn/clearthat reaches the front is returned once with the background-tasks reason, publishes no wait, isn't returned again on later task updates, doesn't hold a new message, and its Send runs it once the tasks end. The existing waiting cases now run with a Stop-capable provider.orca-ci-checks: 22/23 locally; the one red isaudit:code-quality:nativewith 5 import-cycle warnings in mobile transport/terminal files this PR doesn't touch (the same five the previous head showed locally while its CI static analysis passed).orca-ci-checks21/22 (typecheck run separately), the same local-only import-cycle red as before.completed, not interrupted. Claude side: the decoder owes an ending only for a task stopped while live that the CLI still lists, and stops owing it on the task's own ending, a restart, a close, or Orca's own proven close. Through the real adapter and hook server, a Stop of a running task owes its ending until the task's frame lands, and a Stop the CLI acknowledges for a task it no longer runs owes none./clearcard's caption is drawn without a line limit (fails before: the caption was capped at one line).orca-ci-checks21/22 (typecheck run separately), the same local-only mobile import-cycle red as before.verifygate that follows it. The one failing test isterminal-leaf-move.test.ts(terminal persistence, not touched here): its check that a span contains no2222matched a random trace ID (...fcf222276df...). It passes 3/3 locally on the merged tree. Static analysis passed in CI, so the local import-cycle red was local-only.Review
Round 1 (three reviewers: host, clients, design challenger) found 8 items. Fixed: a
/clearwaiting on never-ending background tasks gave no next step (caption now says Stop them); idle vs mid-turn/clearwith background tasks behaved differently (both wait now); the wait rule was written twice and each background-task update ran a full drain step (one gate now, cheap pre-check); after a reopen the waiting/clearshowed no Send and never ran (clients now read the host's published facts); a message sent while it waited flashed as a bubble; the desktop runner's background-task bypass only fired where the host refused. Deferred and labelled above: image messages skipping ahead (temporary) and a new message after Stop running before a held/clear(intended).Round 2 found one item, caused by round 1 making an idle
/clearwait on background tasks: in a Codex chat with a dev server running,/clearbecame a card that could never run (Codex has no background Stop), and the next message, such as "stop the dev server", queued behind it. Fixed by waiting only when the strip offers a Stop; otherwise today's refusal (labelled temporary above).Round 3 (narrow review of round 2) found no defects, and two places where this description was inaccurate (the desktop idle refusal wording, and cards queued behind a returned
/clear); both are corrected above. Rendered desktop QA, and then phone QA, found that after the strip's Stop the clear ran before the stopped task's own end and the agent's reply to it; fixed in this round (see "A Stop acknowledged is not a task ended"). Phone QA also found the waiting/clearcaption cut off after one line; it now wraps.Agent skill upstream boundary
Notes
/compact. Mixed versions are covered by the capability.Checklist