Repository navigation
Native chat: a message stopped before the agent got it reaches the agent with your next send - #27623
Conversation
A message a person sent and then stopped before the agent started on it was kept on screen as "Stopped manually" but never reached the agent. The delivery loop now derives the stopped messages the agent is still owed, names them on the next ordinary send's handover row, and each agent receives them in that same write as framed context (Claude and Pi: framed text and images in the prompt; ACP: leading blocks; Codex: turn/start additionalContext, or framed input items for images and steers). Codex withdrawals that may have reached the agent are marked so they are never carried.
…once outcomes A card's hand-off records no sender, so carrier eligibility also accepts a queued card whose body names no other agent. Tests cover every row of the exactly-once table against the real host, each agent's rendering, the journal fold, and that nothing a client parses changes.
…ation After a host crash, restart reconciliation matches an unsettled send to the agent's transcript by fingerprint. A carrier's transcript copy leads with its stopped history, so it could never match and would read as never delivered, which would also release its history again. Like a send with an attachment, it now stays unconfirmed.
… Codex history as input - Codex: every stopped message rides as leading framed input on turn/start and turn/steer (an older Codex silently ignores additionalContext and would read as delivered); add a Codex fit check against its 1,048,576-character input limit. - Flip the marker: a withdrawal is owed only with `neverReachedAgent`, written where Orca proves it (host-held Stop, ACP held steer, Claude queue clear, a start the Stop ended). Old journals and Codex withdrawals read as not owed. - A carrier releases its history only for Orca's own pre-write refusals or a marked withdrawal; provider refusals and not-signed-in are doubt. - If the history does not fit, try once without its images, then send alone. - Re-read the turn scope after selection; owed excludes typed slash commands; dropped-history fallbacks log through the host logger.
… actually written - Claude's queue clear (lifecycle and interrupt sweep) and a start ended by Stop write plain `cancelled` again, as on main: Claude may already hold a swept send, and the child-exit site proved nothing for leftover rows. The proof sites are the host-held Stop withdrawal and ACP held steers. - An adapter that leaves the history out after it fit calls the host's new `historyLeftOut` before writing; the host rewrites the pending row without `carries`, so those messages stay owed. - History is chosen oldest first: each message with its images if it fits, else without them, else skipped this time without holding back later ones. - Session History titles and previews skip `<stopped-message>` blocks through the existing hidden-block list.
A rewind rebuilt each kept message from the provider's copy. Codex's copy of a send that carried stopped history starts with the framed history, so that bubble showed it. Keep Orca's body for a person's own message, as the live fold already does; other senders keep the provider's body with their sender.
…ides Wrapping the content build in an async function added microtask ticks before every Claude write, even with no history, which a turn-timing test caught. Return the same promise when nothing rides.
Review summaryThis PR went through design review, real-agent measurements, three code review rounds and hidden app QA before it opened. In order: 1. Design review of the first plan (independent Codex reviewer): rejected the mechanism, kept the goal. 2. Measurements on the real
Ruling: no separate append; stopped messages ride inside the next send's single write, framed, for every agent. 3. Code review round 1 (Claude): changes required.
4. Design counter-review of the revised ruling (Codex): keep the design, with changes.
5. Code review round 2 (Claude): no P0/P1. Narrowed the mark to exactly two sites (messages Orca still held; ACP held steers), made Session History titles skip the frame, and corrected two PR-body claims. 6. Code review round 3 (Claude): clean. Two P2s, both fixed. A rewind rebuilt a carrier's bubble from Codex's copy, which shows the frame; a rewind now keeps Orca's own text for a person's own messages, as the live chat already does. The PR body also states what a failed row rewrite costs. 7. Hidden Electron QA (main vs this branch; stand-in agents that answer with exactly what they received; screenshots in the PR body): on main a stopped message never reached the agent. On this branch each one reached it exactly once, framed and in order, for Claude (one stopped message, and two Stops in a row) and for Codex after a restart. The chat itself looked the same. 8. CI. The first run failed one test: Claude's turn-timing test showed a plain send (no stopped history) now reached Claude a few microtask ticks later than on main, because the content build was wrapped in an extra async step. Fixed in 855b261 (a send with nothing riding returns the same promise as before). On the final head every check passes (18 passed, 16 skipped, none failing). Verified: 57 test files / 658 tests on the final head, |
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 Walkthrough
Priority: ➖ Normal Merge Risk: 🟡 Moderate · up to
|
There was a problem hiding this comment.
Actionable comments posted: 2
ℹ️ Review info
⚙️ Run configuration
- Configuration used: Repository UI
- Review profile: CHILL
- Plan: Advanced
- Run ID:
8ec38fa2-3605-4873-b605-3c938cfc393d
📒 Files selected for processing (45)
src/main/acp/acp-prompt-content.tssrc/main/acp/acp-structured-session-adapter-lifecycle.test.tssrc/main/acp/acp-structured-session-adapter.tssrc/main/acp/acp-structured-turns.tssrc/main/ai-vault/session-scanner-claude-title.test.tssrc/main/ai-vault/session-scanner-codex-parser.test.tssrc/main/ai-vault/session-scanner-text-normalization.tssrc/main/claude/claude-structured-dispatch-content.tssrc/main/claude/claude-structured-dispatch.tssrc/main/claude/claude-structured-queued-stop.test.tssrc/main/claude/claude-structured-session-adapter.tssrc/main/codex/codex-structured-session-adapter.tssrc/main/codex/codex-structured-turn-end-settlement.test.tssrc/main/codex/codex-structured-turn-start.tssrc/main/native-chat/agent-session-journal/journal-dispatch-reducer.tssrc/main/native-chat/agent-session-journal/journal-reducer.tssrc/main/native-chat/agent-session-journal/journal-restart-reconciliation.test.tssrc/main/native-chat/agent-session-journal/journal-restart-reconciliation.tssrc/main/native-chat/agent-session-journal/journal-rewind-submission-retirement.tssrc/main/native-chat/agent-session-journal/journal-row-builders.tssrc/main/native-chat/agent-session-journal/journal-row-schema.tssrc/main/native-chat/agent-session-journal/journal-stopped-history-carried.test.tssrc/main/native-chat/agent-session-journal/journal-store-contracts.tssrc/main/native-chat/agent-session-journal/journal-store.tssrc/main/native-chat/agent-session-wire/structured-agent-session-adapter-router.tssrc/main/native-chat/agent-session-wire/structured-agent-session-adapter.tssrc/main/native-chat/agent-session-wire/structured-agent-session-chat-stop.tssrc/main/native-chat/agent-session-wire/structured-agent-session-command-turn.tssrc/main/native-chat/agent-session-wire/structured-agent-session-delivery-loop.tssrc/main/native-chat/agent-session-wire/structured-agent-session-queued-message-rig-provider.test-fixture.tssrc/main/native-chat/agent-session-wire/structured-agent-session-rewind.test.tssrc/main/native-chat/agent-session-wire/structured-agent-session-stopped-history-frame.tssrc/main/native-chat/agent-session-wire/structured-agent-session-stopped-history-rendering.test.tssrc/main/native-chat/agent-session-wire/structured-agent-session-stopped-history-selection.test.tssrc/main/native-chat/agent-session-wire/structured-agent-session-stopped-history-selection.tssrc/main/native-chat/agent-session-wire/structured-agent-session-stopped-history.test.tssrc/main/native-chat/agent-session-wire/structured-agent-session-turns.tssrc/main/native-chat/agent-session-wire/structured-agent-session-unopened-send-withdrawal.test.tssrc/main/native-chat/agent-session-wire/structured-rewind-retained-host-rows.test.tssrc/main/native-chat/agent-session-wire/structured-rewind-retained-host-rows.tssrc/main/pi/rpc-prompt.tssrc/main/pi/rpc-session-adapter.tssrc/shared/agent-session-failure.tssrc/shared/agent-session-stopped-history.test.tssrc/shared/agent-session-stopped-history.ts
Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 2 remain after this review.
A rewind rebuilt history from the provider's items, which hold none of the host's send facts, so a stopped message the agent never received lost its bubble and its submission, and a surviving send that carried it lost its coverage. The rebuild now restates those sends with the submission and dispatch rows that first recorded them, in both the epoch and the post-clear paths. A carrier the rewind removed is simply absent, so what it carried is owed again with the next send.
Drives the real host, store and journal through a Codex-shaped rewind: before and after the carrier, after a /clear, across a host restart, for a stopped message nothing carried, and for a rewind that crashed after its history committed. Each checks what the desktop and the phone draw and what the next send carries.
…gent A Pi prompt refused for sign-in after its retries, and an ACP prompt line the encoder refused before writing a byte, now settle with neverReachedAgent. The owed derivation treats any carrier rejection carrying that proof as refused before write, so its history rides with the next send instead of being lost. Pi checks sign-in before it records a prompt from 0.84.0 (Orca's floor).
Moves the scripted connection and setup out of the adapter test, unchanged, so a stopped-history test can drive the real adapter without growing that file.
…ch frame Stopped messages now reach the agent in one frame ahead of the next send: a header saying they are earlier, stopped history, one tagged element each with its images numbered in the order sent, then a 'User message:' label. Only a '<' that would open or close the frame's own tags is escaped. Each adapter prepares the send once (prepareDispatch): images are read once, each message's cost is weighed once against that agent's real limits, and the result is the exact payload plus the ordered ids inside it. The host lists those ids on the pending row and dispatch writes that same payload, so what a row says it carried is what the agent got. A message that does not fit goes without its images, then shortened with a marker; only when not even that fits does it wait, and the frame says how many were left out. A plain send with nothing owed is prepared and written exactly as before. ACP measures every prompt's whole line, text included, and refuses a text-only message too long for one line before writing it. Titles, previews and copied first prompts hide the whole frame and its label, however long.
After a /clear the rewind rebuilds in place, so a restated accepted carrier reads as a turn after the person's Stop (or a restart's pause) and released the cards it held. Restate those pauses above it, as a new epoch does.
Owed history follows the restated rows' order, so write them in their old send order within each anchor rather than the order they were refused.
…tles and copied prompts
…the readable ones riding
… images, read from its handshake
… anchoring it at the end
… not just the first
…ent now shares Main removed the rejectedInPlace option: the desktop and the phone now draw a recorded-then-rejected send the same way, so the tests read one transcript instead of comparing two.
There was a problem hiding this comment.
Actionable comments posted: 1
ℹ️ Review info
⚙️ Run configuration
- Configuration used: Repository UI
- Review profile: CHILL
- Plan: Advanced
- Run ID:
3263b8b4-618c-4b8a-95ef-f3e350d096bd
📒 Files selected for processing (5)
src/main/claude/claude-structured-session-state.tssrc/main/native-chat/agent-session-journal/journal-store-collaborators.tssrc/main/native-chat/agent-session-wire/structured-agent-session-stopped-history-rewind.test.tssrc/main/native-chat/agent-session-wire/structured-agent-session-stopped-history.test.tssrc/main/pi/rpc-session-adapter.ts
Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 6 remain after this review.
Review summary (round 3: stopped messages go into the agent's own log)Round 3 rebuilt this PR. Stopped messages used to ride with your next message inside an Orca-made block. Now Orca writes each one into the agent's own conversation, followed by the agent's own "interrupted" record. The PR body explains the design. Measured on the real CLIs before building (Claude 2.1.280, Codex 0.159.0):
Decision review. Three independent reviews: all approve with changes. Every finding was ruled on before building. Among them:
Code review rounds. Each round had a Codex gpt-6.1-sol xhigh review and a Claude review of the full diff.
Not fixed: the rest are intended, and the PR body lists each:
QA, hidden app, real agents.
Checks:
|
Follow-up: Claude verified, main mergedThe Claude CLI login on the QA machine had expired during the earlier round, so every Claude check was blocked. With the login restored, all of them ran on the PR head after merging main (
The PR body's Testing and Visual Proof sections now include these results. |
…y other agent inline Claude no longer writes stopped messages as separate shouldQuery:false appends with their own frame-claiming, idle gate and settlement. Like ACP agents and Pi, it now gets them at the front of the person's next words-only send, followed by the "[Request interrupted by user]" record. Codex keeps thread/inject_items right before the send, idle only, at most once; a Codex without the method is refused and its sends carry them instead. One selection serves both: words only, the newest that fit a 1 MiB encoded bound, in the order sent, older ones left out and covered. A send with attachments carries nothing, so history can never push a send past an agent's limits; the per-agent prepare and fit modules, stopped-image reads and Codex version plumbing go. The injection record is renamed for what it is. Rewind restatement is split out: a rewind drops stopped messages with their bubbles, as before, and nothing is injected twice.
…its marker, and keep scanning older messages
…r transport's framing
…nd, and carry none when none fits
…h would have reached the agent unstopped
The delivered stopped messages and the send's own words never exceed one send's size, so the agent is never handed more than a send the person could make. If the newest doesn't fit, they all stay owed for a later send.
Such a message could never fit, so it held every older one back for good.
The smallest send that carries history has one letter, so the owed bound reserves that, not an empty send.
Review summary: realignment of this PR (head
|
main now accepts a Stop in one transaction (#27076). The withdrawal rows that transaction writes for host-held sends carry neverReachedAgent, so the proof lands with the Stop itself; the old unawaited withdrawal path is gone. Trims the PR's tests to the main flow, the failed-save case and the contracts.
Codex no longer gets stopped messages through thread/inject_items. Its next turn-opening send carries them at the front, the same text every other agent gets, and the carrier's own pending row records what it carries. That removes the injection module, its host-only journal row and reducer handling, the adapter and router hooks, the unsupported-method fallback flag, and the <turn_aborted> hiding in Session History that only injected items needed. A Codex rewind rebuilds kept messages from Codex's copy, which now leads with the carried words, so a person's own message keeps the words Orca recorded, as the live fold already does. The per-message size allowance is now the inline framing itself. Measured with real Codex 0.159.0 before building: inline acted on "go ahead" 8/8, never on "ignore", and 0/21 on unrelated follow-ups, matching the injected pair and a real Stop.
…n/start A request Codex answered with an error recorded nothing, so the rejection now carries the never-reached proof and any stopped messages that send carried are owed again instead of covered forever. A timeout or transport failure still proves nothing.
…plit-2 Part 1 landed on main as fd4a802. Main's #27623 (a stopped message reaches the agent with the next send) crossed this part's reads of what runs: its Stop withdrawal words are fixed on main, so the acceptance input keeps only the host's live turn; and the stopped history a handover carries is decided from the host's current-work turn scope, the same one the dispatch row is written with.
ELI5
If you send a message and press Stop before the agent starts on it, the message stays in the chat marked "Stopped manually", but until now the agent never saw it. Say "go ahead with that" next, and the agent has no idea what "that" is.
Now the stopped message reaches the agent together with your next message, marked the way the agent marks its own interrupted requests. So:
Nothing changes on screen.
What Changed
The problem. In native chat (Claude, Codex, ACP agents such as OpenCode and Grok, and Pi) you can send a message and press Stop before the agent receives it: while the agent is still starting, or in Orca's short hold windows. On main the bubble shows "Stopped manually", but the agent has no record of the message. QA with the real agents on main confirmed it: after Stop and then "go ahead with that", Claude, Codex and OpenCode each asked what "that" meant.
Before: after a Stop, the agent never learns what you sent.
After: your next message delivers it. The agent sees the stopped message, then its own "interrupted" note, then what you just typed.
Nothing changes in the chat:
One way of handing over, for every agent. What is still owed is delivered with your next message that starts a new turn. Claude, Codex, the ACP agents and Pi all get it the same way, as plain text at the front of your message:
[Request interrupted by user]is the line Claude writes for its own Stop. There are no Orca instructions in it. Only the words travel: a stopped message's images are not re-sent.What is owed: only ordinary messages Orca proves the agent never received, each at most once. It is worked out from the chat's saved journal each time, not stored separately. A message is owed when:
/clear;A message the agent may already have (for example one Claude may already hold when Stop clears its queue) carries no proof, so it is never re-sent.
Builds on "Stop saved first" (#27076, merged). Stop now saves first: the Stop's receipt, its Stop event and the withdrawal of every message Orca still held commit in one write, and only then is the agent interrupted. The "never reached the agent" proof is written in that same write. So a Stop that was saved always carries the proof for what it withdrew, and a Stop that could not be saved ("Couldn't stop … Try again") withdraws nothing and marks nothing; those messages are delivered normally.
At most once, with one record. Before anything is written, your message's own "about to send" row lists the stopped messages it carries (
carries), in the same write. From then on they are never sent again, unless a later fact proves your message was refused before anything reached the agent (the agent was gone, Pi was signed out, or the queue was full); then they are owed again and ride with your next message. An outcome Orca cannot prove (a timeout, a lost connection, an unknown error) counts as delivered, so nothing is ever duplicated in the agent's history.It never gets in the way of your message.
When Orca rebuilds a chat from the agent's own history. The agent's copy of your message leads with the stopped text it carried. Orca keeps showing your own words:
Session History lists what the agent itself recorded, so a chat whose first message carried a stopped message is titled by the stopped words, as it would be if the agent had received that message and been stopped.
What this replaces. Until this round Codex was handled separately: right before your message, Orca wrote the stopped messages into Codex's own history with
thread/inject_items, followed by Codex's<turn_aborted>record. That needed its own journal row written before the call, a check that Codex was idle, a 2-second timeout, an "unknown" outcome, a fallback for a Codex without the method, and Session History filtering for the record. All of it is gone; Codex now goes through the same path as every other agent. An earlier version also wrote Claude's stopped messages as separate append-only entries and prepared each agent's message separately; that went in an earlier round.Why
When you press Stop after the agent received a message, each agent already keeps it, with its own "interrupted" note. A message stopped a moment earlier should look the same to the agent. Delivering it with your next message, using the agent's own wording, gets there with no invented instructions and nothing new on screen.
Alternatives considered:
thread/inject_items, the previous version of this PR). It works, but it was a second way of handing over with its own record, idle gate, timeout and "unknown" outcome. Measured with real Codex, the inline form behaves the same (below), so the second mechanism did not earn its place.Differences from the common pattern:
Intended: one way of handing over for every agent. The common pattern writes into the agent's own history when the agent supports it (Codex) and falls back to inline text otherwise. Orca uses the inline form for every agent. So there is one way of handing over and one record, saved in the same write as the send. A send refused before anything reached the agent owes the messages again, and nothing ends in an unknown state. Measured with real Codex 0.159.0 (
gpt-5.6-terra), using the same prompts and counts as the earlier measurement:Intended: the content is the agent's own interrupted record, not a "historical material" header with labelled items. It avoids Orca-written instructions to the agent.
Intended: an unconfirmed delivery counts as delivered and is never retried. The common pattern instead abandons the agent's native thread when a delivery is in doubt; Orca can't replace a native thread without losing the agent's context. At most once trades a rare loss (the chat still shows the message as stopped) for never duplicating a message in the agent's history.
Intended: only messages Orca proves never reached the agent are owed, not every failed or interrupted turn. A turn that reached the agent is already in its history.
Intended: only your own ordinary message delivers. A turn opened by a command, a goal or agent mail leaves the history owed, so nothing is spliced into a command.
Intended: no budget against the agent's context window. The delivery is bounded by one message's size instead. Only your own messages are owed, ones a Stop took back moments before they reached the agent; without the Stop, each would have reached the agent anyway, as its own message. The common pattern budgets against the window because it replays arbitrary earlier conversation, which nothing bounds. Older stopped messages that don't fit are left out without telling the agent, and one too large to fit beside any message is never delivered; both need close to 256 KiB of stopped text.
Intended: history waits for a message without attachments. Orca can't size attachments without reading them before it records the delivery. Carrying beside them could push a message past an agent's line limit and fail it.
Temporary: a Codex rewind drops stopped bubbles, as on main. A stopped message carried by a message the rewind keeps stays in Codex's history while its own bubble disappears; one not yet delivered is no longer owed. Nothing is ever written twice. Follow-up: keep stopped bubbles, and their delivery record, through a Codex rewind.
The rest follows the common pattern: delivery at the next turn's start, never on a steer; the record written before the write and released only on a proven refusal; and one size-bounded selection.
Linked Issue
N/A (maintainer change; no pre-existing issue).
Visual Proof
Hidden Electron QA with the real Claude 2.1.280, Codex 0.159.0 and OpenCode 2.0.14 CLIs on their own logins, at
41341f0eeb5. That is before this round: Codex was still handed the stopped message by writing into its history, and the merge with main had not happened. It was not re-run on the new head. A separate agent drove the app; I read every screenshot and log myself. Logging wrappers recorded exactly what each agent received.In each row:
pineconepineconepineconeOKOKOK444Claude and OpenCode got one prompt,
Reply with only the word pinecone\n\n[Request interrupted by user]\n\n<follow-up>: the form Codex now gets too. Codex's behaviour with that form is measured in the table under "Differences from the common pattern", against the real Codex app server but outside the app."go ahead with that"
pinecone"actually ignore that, just reply with the word OK"
OK"what's 2+2" (Claude): answers
4, and says nothing about the stopped request.After a quit and relaunch, the chats look unchanged: one "Stopped manually" row and no new rows. Asked to list every message, resumed Claude names the stopped one as interrupted, and nothing was sent to it again.
The
~/.claude/settings.jsonand~/.codex/config.tomlhashes were unchanged across QA and across the Codex measurement.Testing
Tests (few, against the real host, store and journal where it matters):
/clear) and the size bound;This round deleted the Codex-only tests with the code they covered, plus two lower-value cases (a failed Stop save, and a mixed-version row check now folded into the flow test).
Checks at
5aa2e10703a(this PR merged with main1c274cc178e). This head includes one fix: aturn/startthat Codex answers with an error now carries the "never reached the agent" proof, so the stopped messages that send carried are owed again.pnpm tc:node;orca-ci-checks --no-typecheck: 20 of 21 pass; the one local red is the known "Script not found" fromtypecheck:cion this machine;src/main/codexandsrc/main/native-chat: 6,604 tests pass. With acp, claude, pi, ai-vault and the touched shared tests, 11,399 passed at4e5ac0be06a, before the fix.CI: all checks green at
5aa2e10703a(20 passed, the rest skipped by path filters). That includes static analysis and typecheck, all five test shards, relay integration, cross-version wire compatibility, both package jobs, persistence, runtime on macOS, Ubuntu and Windows, and "test vs non-test LoC" (test +375, production +379).Not verified:
turn/startfix: no test covers a declined send that was carrying stopped messages (three existing Codex assertions were updated for the new proof).Review
Each stage was checked against the common pattern by two independent reviewers: a Codex gpt-6.1-sol xhigh review and a Claude review.
Every finding was fixed, or ruled on with a reason; see the review-summary comment. After "Stop saved first" merged, a three-reviewer design pass moved the proof into the Stop's own save. This round, by Brennan's decision, unified Codex onto the inline form; it has not had its own review round yet.
Notes
carriesis an optional field on the host's own "about to send" row, which older hosts keep and ignore. The "never reached the agent" mark is an optional field on the rejection record, and client schemas strip it. There are no RPC or stream changes, and nothing new is published.Agent skill upstream boundary
Checklist
N/Awith reasonpnpm lint,pnpm typecheck,pnpm test, andpnpm buildpass (or CI will cover; local preferred)Author: @BrennanKB5