Skip to content

Native chat: /clear waits in line while the agent works - #26730

Closed
brennanb2025 wants to merge 9 commits into
mainfrom
brennanb2025/clear-waits-same-chat
Closed

brennanb2025 wants to merge 9 commits into
mainfrom
brennanb2025/clear-waits-same-chat

Conversation

@brennanb2025

@brennanb2025 brennanb2025 commented Oct 9, 2026 •

Copy link
Copy Markdown
Contributor
Files Added Deleted Net
Test 21 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​1435 $\color{#cf222e}{\Huge{\mathbf{−}}}$​18 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​1417
Prod 53 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​1087 $\color{#cf222e}{\Huge{\mathbf{−}}}$​339 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​748

ELI5

In native chat, typing /clear while 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, /clear waits 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 /clear then 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 /clear to keep the same chat (#26579), so this is a smaller rebuild on top of that.

What Changed

What you see (desktop and phone)

  • Before: /clear while the agent works → a refusal line under the composer; nothing is queued.
  • After: /clear while 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.
  • If background tasks are still running when the card's turn comes, and the background-tasks strip above the composer offers a Stop for them (Claude), the card stays in the queue and runs by itself once they end. Its caption says what to do if you don't want to wait: "Waiting for background tasks to finish. Stop them to clear now." It offers no Send meanwhile, because a Send could only be refused; Delete is the way out. The chat itself reads as idle, since nothing is running.
  • If the strip offers no Stop for that work (Codex and the other agents connected the same way, which have no background Stop yet), the card does not wait: a dev server the agent left running may never end, so the card would wait forever and hold everything after it. Instead the card comes back once with the reason ("Background tasks are still running. Wait for the background tasks to finish."), with Send and Delete. Anything you queued after the /clear stays 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.
  • Typing /clear while 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."
  • After you press Stop on the strip, the waiting /clear runs 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.
  • On the phone, the waiting /clear card'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).
  • A message you send while a /clear card 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.
  • After Orca restarts or the chat is reopened, a /clear card that was waiting is held until you act (as every queued card is after a reopen). It now shows Send, like a /compact card does in the same state, and Send runs the clear. Before this round it showed neither Send nor a caption and never ran.
  • If the clear itself fails when its turn comes (stopping the agent or saving the clear fails), the card stays, with the reason said once on the card in the shared failure words ("This command didn't run."). The cards behind it keep waiting. Send on the card tries the clear again; Delete removes it, and the cards behind it then send into the chat as it is.
  • A /clear card that is next in line and about to run shows no Send button, so no Send flashes at the end of a turn.
  • Typing /clear while 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

  • Admission. The /clear command request may now carry delivery: 'queue-if-active', as /compact already can. The host runs the queue's own accept rule first (maybeQueueStructuredAgentSessionSend): if a send would wait right now, the /clear becomes 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.
  • Wire. New capability agent-session.queued-clear.v1. Clients send delivery with /clear only when the host advertises it and queued messages. A host without it fingerprints a /clear without delivery, 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 of nextQueuedMessageId while the queue's next card is a /clear waiting on one of those. Older clients ignore it; a newer client talking to an older host reads it as absent.
  • Drain. When the queue's next card is a /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 /clear card 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.
  • Cards behind it. Keep native chat /clear in the same conversation #26579's rule for an immediate clear (pause waiting message cards, withdraw waiting command cards) applies only to cards queued before the /clear card. 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 /clear the cards ahead are only ones held on their own (a failed send), so in practice this pauses nothing.
  • Waiting vs failing: one rule in the queue's gate. "A /clear card 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's backgroundTaskStops), 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 /clear with stoppable background tasks becomes a card too, and one with unstoppable tasks gets today's immediate refusal. A queued /clear that 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 a commandRefused fact. A returned card already blocks the cards behind it (the queue's existing rule).
  • A Stop acknowledged is not a task ended. Claude answers a background Stop before the task's own "stopped" frame arrives (the SDK hands over a control answer ahead of frames already written), and the host's child record settles on that acknowledgement provisionally, so the strip stops offering Stop at once. The wait predicate now also holds while the provider still owes that task's own ending: a new optional adapter read, 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.
  • Clients read the host, they don't guess. Whether a /clear card 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.
  • Main merged once. 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.
  • Files. The serialized drain moved out of structured-agent-session-queued-messages.ts into structured-agent-session-queued-drain.ts unchanged apart from the gate call, to keep the gate file under the line limit.
  • Send on a /clear card goes to the same clear runner, never to the agent. A command card's existing "not while the agent is working" guard still applies.
  • Who ran it. The committed clear records the card's id as its operation and a fixed host caller (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, /clear keeps 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 /clear like 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 /compact card 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:

  • Hand the /clear card 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 /clear command.
  • Pause or withdraw the cards behind a drained /clear as 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.
  • Treat background tasks as a failure (return the card). Rejected where the strip offers a Stop: a returned card saying "wait" does nothing until the user presses Send, while the waiting card runs as soon as the person stops the tasks or they end. Kept where there is no Stop: there the wait may never end (a dev server), and a card waiting forever would also hold every later message.
  • Wait also when all the live work is sub-agents, which usually end by themselves. Rejected: a sub-agent waiting on an answer, or one the host has lost contact with, stays live until the chat closes, so "ends by itself" isn't something the host can know. The Stop rule is a fact the host already holds.
  • Let later messages skip a waiting /clear (it would then clear them too). Rejected: the queue never reorders, and the /clear would wipe what the person just asked.

Differences from the common pattern:

  • Intended: when the clear fails at its turn, the cards behind it wait instead of running in the uncleared context. They were written for the cleared chat.
  • Intended: no timed automatic retry of a failed /clear. Send retries it, which matches Orca's queue policy for every returned card.
  • Intended: a handoff, or background tasks the strip offers a Stop for, make the /clear wait rather than fail, and it runs by itself when they end.
  • Temporary: for agents whose background tasks have no Stop (Codex and the other agents connected the same way), a /clear with 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.
  • Intended: Send on a /clear card over a paused queue (after a Stop) treats the cards queued before it like an immediate /clear does (paused until the next turn or Resume). The queue never reorders, so the cards after it wait behind those.
  • Temporary: a host without agent-session.queued-clear.v1 keeps today's refusal. Remove the client fallback once supported hosts all advertise it.
  • Temporary: a message with an image skips ahead of a waiting /clear card 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 /clear waiting on background tasks makes the window longer. Follow-up: images in the queue.
  • Intended: after a Stop or a reopen, the cards are held, and a new message you type runs first; when its turn ends the held /clear runs 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 /clear alone 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, /clear while 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):

  • Desktop and phone at head 4f0278c6e10, with stand-in Claude and Codex programs in place of the real CLIs.
  • Desktop with the real signed-in Claude CLI (2.1.280) at head bc329fff577.

What changed after each head:

  • After 4f0278c6e10:
    • 5482fc822fe, the Stop timing fix. A waiting /clear now 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.
  • After bc329fff577: only f2827798c4d (test and comments only).

Desktop (macOS), stand-in Claude, head 4f0278c6e10

  1. /clear sent 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.

desktop-1-clear-card-midturn

  1. When the turn ends, one "Context cleared" divider appears in the same tab. Then the two queued messages run in order in the fresh context. The unsent text is still in the box.

desktop-2-context-cleared-followups

  1. A background task the strip can stop is still running. The /clear card 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-3-bg-wait-caption-stop

Desktop (macOS), stand-in Codex (no background Stop), head 4f0278c6e10

  1. /clear while 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.

desktop-4-codex-idle-refused

  1. A /clear queued mid-turn comes back once when the turn ends, with the reason in red plus Send and Delete.

desktop-5-codex-card-returned

Phone (iOS 26.5 simulator, Release build), stand-ins, head 4f0278c6e10

6. Claude: same-chat clear, then "After one" and "After two" in order 7. Claude: waiting caption, with Stop on the strip (caption cut at one line here; wraps since bc329fff577) 8. Codex: the /clear card returned with its reason, Send and Delete
phone-1-context-cleared-followups phone-2-bg-wait-caption-stop phone-3-codex-card-returned

Desktop (macOS), real Claude CLI, head bc329fff577

  1. /clear typed while idle, with a real background sleep 600 running. It becomes the waiting card, with no Send.

real-claude-1-clear-waiting

  1. After the strip's Stop, the task row reads "stopped · 23s" and there is no red line. One "Context cleared" divider appeared 0.65 s after the click. The output path is blacked out because it contained the OS username.

real-claude-after-stop-cleared

Testing

  • I manually tested these changes. These are rendered QA runs in hidden rigs; the screenshots are under Visual Proof.
    • Desktop, head 4f0278c6e10 (macOS, stand-in Claude and Codex, driven by scripted page automation): all 8 rows pass.

      • A /clear sent mid-turn becomes a card. Delete removes it. When its turn comes it clears the same chat, and the follow-ups run in order.
      • It waits on a background task that has a Stop, and runs after you press Stop.
      • Codex (no background Stop): an idle /clear is refused, and a queued one comes back once.
      • A card held after a failed send, sitting ahead of a waiting /clear, keeps its own Send while the clear runs.
      • Quit and reopen: the /clear card is held, and Send runs it.
      • Stop pauses the queue, and Resume runs the clear and then the next message.
      • Text left in the box survives the clear.
      • A message sent while /clear waits 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. 5482fc822fe fixes 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.

      • A: an idle /clear with 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.
      • B: the same from mid-turn, 0.85 s after Stop.
      • C2: Stop pressed while a turn runs. The clear runs 0.25 s after the turn ends.

      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. f2827798c4d replays that capture as a test.

    • Left as is:

      • After a reopen, the cards behind a sent /clear stay 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.
      • A "Working…" indicator shows for about 0.6 s between Stop and the divider while the clear waits out the stopped task's turn.
      • The refusal and returned-card wording don't name the agent.
    • 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 since bc329fff577.

  • Automated tests added/updated:
    • Host (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 with commandRefused, 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.
    • Record store lease listener for "handoff ended".
    • Desktop: runner (an idle /clear with 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).
    • Phone: capability read, composer command asks to wait, card caption / no Send from the host's facts, background-tasks hook.
    • The new round-1 tests fail on the previous head and pass now (host: 4 fail before; desktop: 7; phone: 3).
    • Round 2 (host, 3 new cases, all fail on 9f8ce31): with no background Stop, an idle /clear is 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 /clear that 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.
    • Existing suites around clear, compact, queue, rewind, restart and record store pass (36 host files, 335 cases; 27 renderer/shared files, 340 cases; 18 phone files, 146 cases). Typecheck node / web / CLI / phone pass. orca-ci-checks: 22/23 locally; the one red is audit:code-quality:native with 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).
    • Round 2 head 4f0278c: targeted vitest 13 host/renderer files / 234 cases and 7 phone files / 73 cases pass; typecheck node / web / CLI pass; orca-ci-checks 21/22 (typecheck run separately), the same local-only import-cycle red as before.
    • Round 3 (Stop timing): two host cases fail on 4f0278c's wait rule and pass now. After the strip's Stop is acknowledged (record already settled), the card keeps waiting, publishes the background-tasks wait and stops nothing, and runs once the task's own ending lands. When that ending opens the agent's turn, the clear waits for the turn to finish, and the turn ends 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.
    • Round 3 phone: a waiting /clear card's caption is drawn without a line limit (fails before: the caption was capped at one line).
    • Round 3 checks: targeted vitest 23 host/Claude files / 296 cases and 2 phone files / 55 cases pass; typecheck node / web / CLI / phone pass; orca-ci-checks 21/22 (typecheck run separately), the same local-only mobile import-cycle red as before.
    • CI on 9f8ce31: everything passes except one test shard and the verify gate that follows it. The one failing test is terminal-leaf-move.test.ts (terminal persistence, not touched here): its check that a span contains no 2222 matched 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 /clear waiting on never-ending background tasks gave no next step (caption now says Stop them); idle vs mid-turn /clear with 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 /clear showed 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 /clear wait on background tasks: in a Codex chat with a dev server running, /clear became 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 /clear caption cut off after one line; it now wraps.

Agent skill upstream boundary

  • Not applicable.

Notes

  • SSH / remote hosts: the clear runs on the execution host that owns the queue, and clients learn the result from the host's published cards and journal, as for /compact. Mixed versions are covered by the capability.
  • Folder workspaces: nothing here depends on git.
  • Mobile: same card rules as desktop, read from the same host facts.

Checklist

  • This PR is small and focused
  • I explained what changed and why
  • After screenshots attached for UI changes (no before screenshot of main's refusal; see Visual Proof)
  • Self-reviewed for correctness, security, and performance
  • Cross-platform, SSH/remote, and path/shortcut impact considered
  • Lint, typecheck and targeted tests pass locally; CI will cover the rest

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.
@brennanb2025

Copy link
Copy Markdown
Contributor Author

Review summary

The problem. In native chat, /clear sent while the agent is working (a turn running, a message being sent, a question waiting) is refused with "The agent is still working. Run /clear when it's done." The common pattern queues /clear like any message and runs it when its turn comes, with anything queued after it running in the fresh context, in order. This replaces #25842, whose design (moving waiting messages and the draft into a new chat after /clear) stopped applying once main's #26579 made /clear keep the same chat.

What changes for you (behind the queued-messages setting, which is still off in shipped builds):

  • /clear sent while the agent works becomes a "/clear" card in the queue (Delete works; no Edit; it never steers). When its turn comes it runs in the same chat and tab: one "Context cleared" divider, then the messages you queued after it run in the fresh context, in order.
  • If background tasks are still running and the chat offers a Stop for them (Claude today), the card waits with "Waiting for background tasks to finish. Stop them to clear now." and runs by itself once they end. Pressing Stop lets it run; the task reads "stopped" and no reply gets cut off.
  • Where nothing can stop the background work (Codex and the agents connected through the Agent Client Protocol), /clear typed while idle gets main's immediate "wait for background tasks" message, and a queued /clear that reaches the front comes back once with that reason (Send/Delete), so your next message still reaches the agent. This is temporary: once Let a Codex chat stop a command it left running in the background #26780 (Codex background Stop) lands, Codex chats offer Stop and /clear waits there too.
  • After a restart, a waiting /clear card shows Send, and Send runs it.
  • Text typed while a /clear runs now stays in the box (it used to end up under the old chat).

How. The host runs a /clear card itself through main's same-chat clear, and removes the card in the same database write, so it can't run twice or vanish across a crash. One rule decides whether the card waits; publication, the drain and both clients read it from the host, with no timer and no stored flag. A person's /clear gets the queue space kept for the person's own messages, same as /compact.

Fixed during review (four review rounds, design challenger, desktop + phone + real-Claude QA, readiness checklist): one waiting rule instead of two copies; a /clear after restart no longer stuck without Send; no transcript flash for messages sent while it waits; no endless wait in chats that can't stop background work; the /clear no longer runs before a stopped task's own ending arrives (which showed "status unavailable" and a red interrupted-reply line); the phone caption no longer cuts off.

Deferred / known limits:

  • An image sent while a /clear waits skips ahead of it (the queue only holds text today) — temporary, until images can be queued.
  • After a Stop or restart, a message you send runs before the waiting /clear (the existing queue rule; desktop's "Send message?" dialog asks about waiting cards).
  • If a hand-off between agents never finishes, a /clear card waits behind it until Delete (not traced whether every hand-off stage has its own exit).
  • Messages behind a /clear that came back stay held until Send or Delete on that card (existing returned-card rule).

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, /clear while idle and mid-turn with a background task, and Stop mid-turn, all passed (task read "stopped", no interrupted line, one divider). A recorded real Claude Stop is now a replay test. Screenshots are in the PR body. CI on the final head passed (16 passed, 16 skipped by path, none failed); the readiness checklist passed with no P0–P2.

Not verified: SSH, rendered Windows/Linux, mixed old and new client/host versions live, Android, a real Codex run, phone row 4.

@brennanb2025
brennanb2025 marked this pull request as ready for review October 9, 2026 10:53
@coderabbitai

coderabbitai Bot commented Oct 9, 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: 43c614ca-aa98-43c1-8560-6325e335aec5

📥 Commits

Reviewing files that changed from the base of the PR and between 8167dda and f282779.


📒 Files selected for processing (74)
  • mobile/src/session/MobileNativeChatQueuedMessages.test.tsx
  • mobile/src/session/MobileNativeChatQueuedMessages.tsx
  • mobile/src/session/mobile-native-chat-merged-snapshot-parity-hooks.test.tsx
  • mobile/src/session/mobile-structured-agent-session-host-support.test.ts
  • mobile/src/session/mobile-structured-agent-session-host-support.ts
  • mobile/src/session/mobile-structured-composer-command.test.ts
  • mobile/src/session/mobile-structured-queued-message-cards.test.ts
  • mobile/src/session/mobile-structured-queued-message-cards.ts
  • mobile/src/session/use-mobile-structured-agent-session-background-tasks.test.tsx
  • mobile/src/session/use-mobile-structured-agent-session-prompt-cancel.test.tsx
  • mobile/src/session/use-mobile-structured-agent-session-queued.test-fixture.ts
  • mobile/src/session/use-mobile-structured-agent-session.ts
  • mobile/src/session/use-mobile-structured-queued-message-controls.ts
  • mobile/src/session/use-mobile-structured-send-with-outcome.ts
  • mobile/src/session/use-mobile-structured-turn-indicator.test.tsx
  • src/main/claude/__fixtures__/claude-lifecycle-capture-stop-task.jsonl
  • src/main/claude/claude-background-stop-records.test.ts
  • src/main/claude/claude-child-work-decoder.test.ts
  • src/main/claude/claude-child-work-decoder.ts
  • src/main/claude/claude-stop-task-capture.test.ts
  • src/main/claude/claude-structured-event-delivery.ts
  • src/main/claude/claude-structured-session-adapter.ts
  • src/main/native-chat/agent-session-journal/journal-context-clear.ts
  • src/main/native-chat/agent-session-journal/journal-context-controller.ts
  • src/main/native-chat/agent-session-journal/journal-queued-messages.ts
  • src/main/native-chat/agent-session-journal/queued-message-return.ts
  • src/main/native-chat/agent-session-wire/agent-session-subscriber-frame-fields.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-adapter-router.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-adapter.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-background-task-channel.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-client-delivery.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-host.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-queued-card-holds.test.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-queued-clear.test.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-queued-compact.test.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-queued-drain.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-queued-messages.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-queued-mutations.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-queued-publication.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-queued-rig-turn.test-fixture.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-queued-wiring.ts
  • src/main/native-chat/agent-session-wire/structured-conversation-clear.ts
  • src/main/native-chat/agent-session-wire/structured-conversation-command-admission.ts
  • src/main/native-chat/agent-session-wire/structured-conversation-command.ts
  • src/main/runtime/agent-session-conversation-receipts.ts
  • src/main/runtime/agent-session-record-lease-events.test.ts
  • src/main/runtime/agent-session-record-lease-events.ts
  • src/main/runtime/agent-session-record-store.ts
  • src/main/runtime/structured-agent-session-failure-logging.test.ts
  • src/renderer/src/components/native-chat/NativeChatQueuedMessageCard.tsx
  • src/renderer/src/components/native-chat/NativeChatQueuedMessageList.test.tsx
  • src/renderer/src/components/native-chat/structured-agent-session-queued-cards.test.ts
  • src/renderer/src/components/native-chat/structured-agent-session-queued-cards.ts
  • src/renderer/src/components/native-chat/structured-conversation-command-send.test.ts
  • src/renderer/src/components/native-chat/structured-conversation-command-send.ts
  • src/renderer/src/components/native-chat/use-structured-agent-session-queued-messages.ts
  • src/renderer/src/components/native-chat/use-structured-agent-session-transport-state.ts
  • src/renderer/src/components/native-chat/use-structured-agent-session.queued-clear-gating.test.tsx
  • src/renderer/src/components/native-chat/use-structured-agent-session.ts
  • 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
  • src/renderer/src/runtime/structured-agent-session-host-capability.ts
  • src/shared/agent-child-work-stop-targets.ts
  • src/shared/agent-session-queued-message-wire.ts
  • src/shared/agent-session-wire.ts
  • src/shared/protocol-version.ts
  • src/shared/rpc-contract/structured-agent-session-params.ts
  • src/shared/structured-agent-session-queue-publication-field.ts
  • src/shared/structured-agent-session-reducer.ts
  • src/shared/structured-session-background-tasks-view.ts

Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 3 remain after this review.



📝 Walkthrough

Walkthrough

The change adds host capability support for queued /clear commands. Hosts can publish whether a clear card waits on a handoff or background tasks, execute eligible clear cards through the journal, and track task-ending events after Stop acknowledgements. Renderer and mobile clients use the capability and wait state to manage queueing and card presentation. Tests cover queued clear execution, task-stop lifecycle behavior, and client presentation.


Priority: ➖ Normal

Merge Risk: ⚪ Minimal · up to f2827

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)

Check name Status Explanation Resolution
Docstring Coverage Inconclusive 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… 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 and concisely describes the main change: queued /clear commands wait while the agent is working.
Description check Passed The description is complete and directly addresses the required sections. It explains the user impact, implementation, rationale, linked prior issue, visual proof, testing, review notes, compatibility…
Linked Issues check Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check Passed Check skipped because no linked issues were found for this pull request.

Full details: Docstring Coverage

Explanation

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 💡
  • Commit to this branch
  • Create a new PR

🛠️ Fix failing CI checks 💡
  • Commit to this branch
  • Create a new PR


  • Autofix · 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

Copy link
Copy Markdown
Contributor Author

Superseded by #27928, a smaller rebuild of the same feature.

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