Repository navigation
fix(native-chat): a message sent while Stopping waits at the end of the queue, and a person's Stop never holds another agent's mail - #25041
Closed
brennanb2025 wants to merge 249 commits into
Closed
brennanb2025 wants to merge 249 commits into
brennanb2025 wants to merge 249 commits into
Conversation
…keeps it paused Red on main: Codex's turn end withdraws the steered hand-off and the queue sends the card again as a new host turn, with no pause recorded.
… still unanswered A card sent now into the running turn was still pending when Stop judged the pause, so nothing was recorded; the interrupt then withdrew the hand-off, the card went back to waiting unpaused, and the queue sent it again as a new turn. The pause now counts a hand-off that may still return to waiting, judged with the appended row applied, so a withdrawal lands under the pause and an acceptance retires it in that same write. Codex and Claude both hit it.
…the test's budget
…ry path that ends it The provider's answer, the provider dying, the chat closing, a restart, a withdrawal still owed at open, and a /clear (refused until the hand-off ends, then carrying every waiting card paused 'cleared'); each ends with the queue sending again.
…op-keeps-steered-card
… queued receipt's card
…op-keeps-steered-card
…journal rows Stop now appends one journal row where it takes effect, before the interrupt, whatever the queue holds; Resume appends its own. The pause is a pure function of the fold: the latest Stop with no later Resume and no later accepted turn a person asked for. A /clear's carried cards name their source, which is the replacement's 'cleared' pause. Host-origin turns never lift either. One predicate decides which cards a pause holds; by default every waiting card without a hold of its own, including one queued after the Stop. The drain's consume re-judges it inside its own transaction. The rows are tombstones of an id no item takes, carrying the mark: a released host reads an unknown row kind as corruption and truncates the journal there. Deletes the stored pause (recordPause, the retire hook on every appended row, the settle-before-record step, mayReturnToWaiting and its row overlay) and the tests that only proved it retires. The queued_message_pauses table stays in the schema, unread and unwritten, for downgrade safety.
…ead of held ones A Stop's pause now holds only the cards queued before its row, plus a steer it withdrew, which returns to its own place. Each card records the journal position it was queued at, and the one hold rule compares that with the Stop row. A card queued after the Stop is a new instruction: it sends as usual, but the drain still stops at the first held card, so it never overtakes them. /clear's pause holds the cards it carried. Holding every card again is a one-line switch in that rule.
… in its transaction The drain's pick and its consume now read one function, nextSendableQueuedCard, so a Stop row that lands between them holds a newer card behind an older held one exactly as the pick would. Notes why Stop and Resume ride a tombstone row.
…op-keeps-steered-card
The queue's pause is derived from journal rows, so nothing reads or writes queued_message_pauses. It was still created on every open "for downgrade safety", but an older build creates it itself when it opens the database, so the table only sat empty in every new database. The tests now pin that no pause table exists.
A Stop holds only the cards queued before it. The pause derivation still returned the Stop alone whenever it was in force, so the restart pause was never considered: a card queued after the Stop, written by a host process that has since exited, sent by itself after Orca restarted, with no pause header and no Resume. A /clear pause that held nothing could hide it the same way. Every pause in force is now derived. A card is held if any of them holds it, and it names the first that does. The drain's pick, the consume transaction's re-check and the published header all read that one rule; the header names the pause holding the first card Resume would send.
- The Claude and Codex Stop-withdraws-a-steer tests checked "not sent again" at one instant, before a queue ignoring the pause re-sends. They now wait for the stopped turn to end and re-check after a quiet window. - The deleted-card test read a card queued after the Stop, which sends whether or not a person's turn lifts it; it now reads the Stop's pause before and after that turn. - Unit cases pin that a Stop holds a card with no recorded position and one queued before a rewind.
…op-keeps-steered-card
…turn and caller
The Stop row that paused the queue becomes the general Stop event
{ reason, turnId?, at, caller? }, whose reason is the host's existing stop
cause. It still rides a tombstone of a host-only id (a released host deletes
the journal from the first unknown row kind), and Resume keeps its own marker
on its own id. Only a person's Stop (reason user-stop) pauses the queue.
…states the same Stop event
… the at-start stop Through the real host: the event names the turn and who asked and is in the journal when the interrupt reaches the agent; at an agent still starting it is there before the start is ended and holds a card queued before it; an idle Stop writes one only when it withdrew a send; and the queue's claim re-judges a pause that landed after its pick.
…the Stop's timing Also says precisely what the claim's in-transaction pause check defends against: the Stop and the drain share one serialized lane.
… Stop events Replays this build's rows from the released build's own journal database: every row is kept, the history after the Stop still folds, and an older client is sent only removed ids no item uses.
…op-keeps-steered-card
…op-keeps-steered-card
… a writable downgrade The Stop-event downgrade test ran in no CI lane: unit shards exclude the cross-version folder, and the cross-version lane runs a fixed file list that did not name it. It is now on that list. Its only case replayed the rows into a release's own fresh database, because that release cannot open the current host database. A second case opens the journal this build wrote with a main build that shares the database: it opens writable, keeps every row, appends, and this build then reopens it with the person's Stop still pausing the queue.
A Stop reaching a running agent wrote a Stop event on every press. Two presses before the first interrupt landed wrote two events, so a card queued between them counted as before the latest Stop and was held, though a card queued after a Stop should send normally. A Stop naming a turn that had already ended, as a phone sends late, also wrote an event for a turn it never stopped. It now writes one only when it withdrew a queued send, or stops something no event records yet: not a turn the journal no longer runs, and not the live turn a Stop still in force already names, unless a card was handed over into it since, which this Stop's interrupt sends back and must hold. The interrupt and the "already finished" note are unchanged. A Stop at a starting agent still always writes.
…r lifts a person's Stop
…-state-on-b # Conflicts: # mobile/src/session/MobileNativeChatTurnStatus.test.ts # mobile/src/session/MobileNativeChatTurnStatus.tsx # mobile/src/session/MobileNativeChatView.tsx # mobile/src/session/use-mobile-native-chat-turn-disclosure.ts # src/renderer/src/components/native-chat/NativeChatMessageList.tsx # src/renderer/src/components/native-chat/NativeChatQueuedMessageList.tsx # src/renderer/src/components/native-chat/NativeChatTurnActivityLine.tsx
…-state-on-b # Conflicts: # mobile/src/session/MobileNativeChatView.tsx # mobile/src/session/use-mobile-structured-agent-session.ts # src/renderer/src/components/native-chat/NativeChatComposer.test.tsx
…e limits after the main merge The Stop control now disables itself while Stopping, so the composer passes the flag through and its test file stays as main has it. The phone chat header's Stop moves to its own component.
…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).
…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.
…on the queue merge added
…no longer needs A person's Stop now holds every card while it is in force, so no card queued under it is let through and the per-card mark carries nothing. Desktop and phone labels go with it.
…chestration mail runs past it Before, the Stop's pause held only cards queued before its row, so a message queued while the stopped turn wound down ran as soon as the stop landed. Now the pause holds every card until Resume or a turn accepted after the Stop. Mail never waits on a person's Resume: the person's Stop holds no mail card, and the queue's one ordering exception lets mail pass cards held only by that Stop. Its accepted turn lifts the pause, so the person's cards follow in order. The drain, its consume and admission all read the same rule (nextSendableQueuedCard).
…at once, beside the Stop's lane A person's Stop holds the session's lane until its provider answers, so a send asking to be queued showed nothing for the whole Stopping window. While the host reads Stopping, a text send asking to be queued is admitted beside the lane and written as a card at the end through the journal's own ordered writer. It is always the card: a conversation closed or replaced since admission throws, leaving the operation pending for a resend to rerun, and a send-blocking record refuses as the lane does. A /clear in flight, a rewind the lane would recover, or an acceptance-time check keeps the lane. The queue decision and its card write move to structured-agent-session-queued-send.ts.
…d message runs after the stop A message queued while Stopping now waits for Resume, so "Queue a message to run after the stop" was false. The composer shows its normal prompt there, desktop and phone. "Send a message to run after the stop" stays where sends are not queued: the host holds that send until the stop lands.
…tting, with no dialog while working Queueing on asks a queueing host to queue it and draws no bubble; queueing off sends it plain, marked as sent while stopping. The "Send message?" choice is never offered while a turn runs.
…hatever else that Stop holds Said in the queue's pick itself rather than left to the hold rule, so a later change to which cards a Stop holds cannot let a person's card overtake.
brennanb2025
changed the base branch from
brennanb2025/stopping-state-on-b
to
main
October 6, 2026 19:18
brennanb2025
changed the base branch from
main
to
brennanb2025/chat-queue-continues-after-stop
October 6, 2026 19:19
…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 Stop never holds; say mail passes a Stop's held cards where the queue rule is described
…son's Stop marks only their own cards paused; another agent's mail reads as waiting
brennanb2025
changed the base branch from
brennanb2025/chat-queue-continues-after-stop
to
main
October 7, 2026 01:12
…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.
Contributor
Author
|
Closing this stale queue implementation after a replacement issue is created. Preserve immediate cards for messages sent during Stop and the rule that another agent’s mail is not held by a person’s pause. The remaining problem is tracked in #27657. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Lands with the queued-messages capability rollout (#21062). Based on
main(with #24586). Nothing here takes effect on a host that does not advertise the queued-messages capability, which no shipped host does yet; the capability bump that turns it on is a separate, later PR.ELI5
Press Stop, then type another message while the chat still reads "Stopping…". With queueing on, that message now waits in the queue, at the end, paused by your Stop like everything else you queued, until you press Resume or start a new turn. A message another agent sends this chat (orchestration mail) is never held by your Stop: it runs when the stop lands, and the queue labels it as waiting, not paused.
What Changed
The problem. With queueing advertised, a message sent while "Stopping…":
Before / after (only where the host advertises queueing):
Mechanism.
queued-message-pause.ts): a person'sstoppedpause holds every waiting card except orchestration mail; a/clear's pause holds only the cards it carried; a restart's holds all.from, from feat(native-chat): show which agent a chat message is from, and open it #25888). A client's own send cannot setfrom, so its presence is the whole test, on the host and on both clients (queuePauseHoldsCardinsrc/shared). Clients check presence rather than parsing the sender, so a newer build's sender shape still reads as mail exactly as the host reads it; parsing would bring the "paused" flash back for those.fromon each card's body.Why
heldByPausecard field). It reorders the queue against the person's own order and needed a new wire field.fromon the client to decide "mail". It disagrees with the host for any sender shape the client cannot parse.Differences from the common pattern
Mixed versions.
tests/e2e/cross-version-wire/queued-card-sender-downgrade.unit.test.tsagainst a build without this change).from(before feat(native-chat): show which agent a chat message is from, and open it #25888) reads as the person's; such a host also predates this rule and holds mail, so the label matches.delivery: 'queue-if-active'text sends go off the lane. An older host never advertises queueing, so a newer client never asks it to queue.Linked Issue
None: maintainer change, the queue half of #24369 split out to land with #21062.
Visual Proof
N/A until #21062: with queueing off, which is every shipped host today, nothing here is visible. Live QA of the queue-on build is still to run (see Testing).
Testing
I manually tested these changes locally
Automated tests added/updated, or explained why not below
End to end through the shipped host, journal and Codex adapter (
structured-agent-session-codex-interrupt-order.test.ts): while the Stop waits on Codex's answer, a send asking to be queued returns a card at once; a retry answers the same card; after the stop it waits with the held cards, and Resume sends them in order.Host: the pause rule (
queued-message-pause.test.ts); a send while Stopping (structured-agent-session-queued-send-while-stopping.test.ts: always a card at the end; mail runs first, then the person's cards; only the mail card's body names a sender); the stop-row, pause-lift and Stop-event tests; a restart still holding mail queued after a Stop.Clients: desktop and phone card labels (a person's Stop marks only their cards paused, any other pause marks all); the desktop send while Stopping follows the queueing setting with no dialog while working; the placeholder.
Cross-version:
queued-card-sender-downgrade(above), plus the kept-card and Stop-event downgrade files.Each rule was checked by breaking it and watching its tests fail (removing the mail check turns 7 tests in 3 files red).
At
2d950cad6c9: 20 desktop test files (243 tests) and 4 phone files (82 tests) pass;tc:node,tc:web, phonetscand its tests-typecheck ratchet pass; oxlint and oxfmt are clean.Still to do before ready: port the client tests to the new desktop send layer (fix(native-chat): no saved outbox on the desktop; one send at a time, and the host owns what it accepted #25959) once it lands; live QA with queueing on.
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
Ensure no issues in: Security, Cross-platoform support (Linux, Windows, Mac), Remote SSH, Mobile, general backwards compatibility, performance
Checklist
N/Awith reasonpnpm lint,pnpm typecheck,pnpm test, andpnpm buildpass (or CI will cover; local preferred)