Skip to content

Native chat: a message stopped before the agent got it reaches the agent with your next send - #27623

Merged
brennanb2025 merged 70 commits into
mainfrom
brennanb2025/snu-stopped-history
Oct 14, 2026
Merged

brennanb2025 merged 70 commits into
mainfrom
brennanb2025/snu-stopped-history

Conversation

@brennanb2025

@brennanb2025 brennanb2025 commented Oct 11, 2026 •

Copy link
Copy Markdown
Contributor
Files Added Deleted Net
Test 12 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​375 $\color{#cf222e}{\Huge{\mathbf{−}}}$​15 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​360
Prod 20 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​379 $\color{#cf222e}{\Huge{\mathbf{−}}}$​43 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​336

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:

  • "go ahead with that" does the stopped request;
  • "actually ignore that" doesn't;
  • an unrelated question gets an unrelated answer.

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:

  • each stopped bubble still shows one "Stopped manually" row;
  • your next message's bubble shows only what you typed, also after a relaunch and after a Codex rewind;
  • no extra rows, notices or turns appear on desktop or phone.

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:

<stopped text>

[Request interrupted by user]

<your new text>

[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:

  • a Stop took it back while Orca still held it, so it provably never reached the agent;
  • it is your own ordinary message (not a slash command, a goal, orchestration mail or a task preamble) and has words;
  • it came after the latest /clear;
  • nothing has delivered it yet.

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.

  • History only rides on a message that has no attachments, so it can never push your message past an agent's size limits. With attachments, it waits for your next words-only message.
  • The stopped messages and your message together are never larger than one message Orca accepts (256 KiB), so the agent is never handed more than you could send yourself. The newest stopped message goes first. Older ones that don't fit are left out, and won't arrive later out of order. If the newest doesn't fit beside your message, they all wait for a later one. A stopped message so large it could never fit beside any message is not delivered at all, so it never holds the others back.
  • A message that steers a running turn carries nothing: the agent may never record it.

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:

  • on a relaunch, your bubble comes from Orca's journal, not the agent's copy, and restart reconciliation leaves a carrying message "unconfirmed" rather than calling it undelivered;
  • on a Codex rewind, a kept message of yours now keeps the words Orca recorded, as the live chat already does, instead of Codex's copy.

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:

  • Writing into Codex's own history (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.
  • Claude append-only entries. Claude's SDK merges a non-querying message into the next querying one anyway, so the model sees essentially the same thing, but the separate write needed its own settlement, frame filtering and idle gate.
  • Orca-written framing (a "historical material" header, labels, numbering). Rejected: it is an Orca-invented instruction the agent has to interpret.
  • Prompt, then cancel immediately (for ACP and Pi). Rejected: it actually runs the model, which could start a tool call before the cancel lands.
  • Starting the agent after Stop just to deliver it. Rejected: Stop should not start anything.

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:

    Follow-up after the stopped "Reply with only the word pinecone" Inline (this PR) Written into Codex's history Real Stop
    "go ahead with that" (fresh + warm) pinecone 8/8 pinecone 8/8 pinecone 4/4
    "actually ignore that" (fresh + warm) never acted, 0/8 never acted, 0/8 never acted, 0/4
    "what's 2+2" + "Say goodbye in one word." pinecone 0/21 pinecone 1/29 pinecone 0/18
    "What exact messages have I sent you…" 4/4 list it, not acted on 4/4 2/2
  • 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:

  1. Send "Reply with only the word pinecone".
  2. Press Stop while the agent is still starting.
  3. Send the follow-up.
Row Claude Codex (earlier history write) OpenCode
"go ahead with that" pinecone pinecone pinecone
"actually ignore that, just reply with the word OK" OK OK OK
"what's 2+2" 4 4 4
quit, relaunch, reopen, send again recalls the stopped message, nothing re-sent recalls the stopped message, nothing re-sent not run

Claude 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"

Before (main, Claude): asks what "that" means After (this PR, Claude): replies pinecone
before after: Claude replies pinecone

"actually ignore that, just reply with the word OK"

Before (main, OpenCode) After (this PR, OpenCode): replies OK
before after: OpenCode replies OK

"what's 2+2" (Claude): answers 4, and says nothing about the stopped request.

Claude answers 4

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.

Claude resumed lists the stopped message

The ~/.claude/settings.json and ~/.codex/config.toml hashes were unchanged across QA and across the Codex measurement.

Testing

  • I manually tested these changes locally: hidden Electron QA above (before this round), and the real-Codex measurement of the inline form (this round).
  • Automated tests added/updated.

Tests (few, against the real host, store and journal where it matters):

  • the main flow, on a Codex chat: a Stop while the agent starts saves the withdrawal with its proof; the next send carries the stopped messages, in order, exactly once; the chat looks the same, nothing new is published, and clients read every row with the schema they already have;
  • a carrier refused before any write owes them again; a carrier in doubt never re-sends them;
  • the owed rule (proof only, your own ordinary messages, after /clear) and the size bound;
  • a Codex rewind keeps your own words for a kept message;
  • Claude's queue-clear writes no proof; Pi's sign-in refusal carries the proof and a re-sent retry counts as unknown; restart reconciliation leaves a carrier unknown.

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 main 1c274cc178e). This head includes one fix: a turn/start that 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" from typecheck:ci on this machine;
  • vitest (Node runtime) for src/main/codex and src/main/native-chat: 6,604 tests pass. With acp, claude, pi, ai-vault and the touched shared tests, 11,399 passed at 4e5ac0be06a, 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:

  • The declined-turn/start fix: no test covers a declined send that was carrying stopped messages (three existing Codex assertions were updated for the new proof).
  • Live Electron QA on this head: Codex's inline form was measured against the real Codex app server outside the app, not in Orca.
  • Pi live, SSH, Linux, Windows and the phone.
  • More than one stopped message, and a stopped message with images, in live QA.
  • A full-size message beside stopped history in live QA.
  • A Codex rewind with a real carrier in live QA.

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.

  • Before building: both reviewed the plan mechanism by mechanism.
  • After each build or fix round: both reviewed the whole diff for alignment, simplicity and correctness. That took eight rounds, until both found nothing.

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

  • Mixed versions. carries is 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.
  • SSH and folder workspaces. The execution host works out what is owed, delivers it and records the outcome, so SSH and folder workspaces take the same path. Lost contact counts as "unknown", never as a refusal.

Agent skill upstream boundary

  • Not applicable, or this change copies or mechanically translates no upstream skill-installer source, tests, fixtures, registry entries, path tables, comments, or documentation.

Checklist

  • This PR is small and focused
  • I explained what changed and why (ELI5, the user-facing before/after, the mechanism, and why over the alternatives)
  • Before/after screenshots or videos attached for UI changes, or N/A with reason
  • Self-reviewed for correctness, security, and performance
  • Cross-platform, SSH/remote, and path/shortcut impact considered (or N/A)
  • pnpm lint, pnpm typecheck, pnpm test, and pnpm build pass (or CI will cover; local preferred)

Author: @BrennanKB5

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

Copy link
Copy Markdown
Contributor Author

Review summary

This 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.
The first plan changed a stopped message's own state and gave it the outcome of the next send, sometimes through a separate "history only" write to the agent. The review showed that a separate write has its own result, so "takes the next send's outcome" was not exactly-once, and that changing the message's state would break the Stop row, the queue pause and the Working status. Ruling: the stopped message's state stays exactly as on main; what the agent is owed is worked out from the chat's history; and the list of carried messages lives on the next send's own "about to write" row.

2. Measurements on the real claude and codex CLIs (isolated folder, no credentials copied, config hashes unchanged).

  • Codex's "add items to history" request works, but repeating it adds the item twice, and it is a second write with its own result.
  • Codex's experimental per-turn context field works on current Codex.
  • Claude's "append without a turn" message runs as its own command, which Orca would record as an empty turn.
  • An unframed stopped instruction was obeyed by Codex once in four runs; with the "context only, not a request" frame, never.

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.

  • P0: Codex's per-turn context field is silently ignored by older Codex builds (its request type accepts unknown fields), so history would be recorded as delivered that never arrived. Fixed: Codex gets leading framed input items, which every version records.
  • P1: the "never reached the agent" mark was negative (missing meant proven), so old journals would have read as owed. Fixed: a positive mark, written only where proven.
  • P1: release on a refused carrier was a deny-list. Fixed: an allow-list of Orca's own pre-write refusals; everything else is doubt.
  • P1: one oversized stopped message could block all history forever. Fixed: greedy selection, oldest first.

4. Design counter-review of the revised ruling (Codex): keep the design, with changes.
It found no correctness failure that forces a separate per-message delivery record or a read-back of the agent's history on every start. Required changes, all applied:

  • Claude's queue clear is not proof of absence (a message cancelled at the interrupt can already be attached to the stopped turn), so it is not carried: "never send anything twice" wins. Listed as temporary in the PR body.
  • An agent that leaves the history out at write time tells Orca first, so the row never claims history that was not written.
  • Dropped images are stated as content loss.
  • Rewind and new-session limits are stated.

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, pnpm tc clean, the real-Claude CLI suites, and the QA above.
Not verified: Linux, Windows and SSH at runtime, and a real model reading the framed history inside this build (it was checked against real models before building).

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

coderabbitai Bot commented Oct 12, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

Note

Reviews paused

It 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 reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

The change adds stopped-message history selection, bounded preparation, and delivery through Claude, Codex, ACP, and Pi adapters. The journal records which messages were appended or carried, and rewind and recovery rebuild related send and append records. Rejection facts now identify sends known not to have reached an agent. Session scanning suppresses Codex interruption markers from normalized text and previews.

Priority: ➖ Normal

Merge Risk: 🟡 Moderate · up to f799d

Resolve the open image-filter concern and complete the required real-Claude tests before merging. The attempted test run did not reach the tests.

Pre-merge checks: Passed 4 · Failed 1

Failed Failed checks: 1 warning

Check name Status Explanation Resolution
Docstring Coverage Warning Docstring coverage is 47.59% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 166 functions across 82 files. Write docstrings for the functions missing them to satisfy the coverage threshold.
Passed Passed checks: 4 passed
Check name Status Explanation
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.
Title check Passed The title clearly identifies the native-chat behavior change: a message stopped before agent receipt is delivered with the next send.
Description check Passed The description is complete and follows the repository template. It covers the user impact, implementation, rationale, alternatives, linked-issue exception, visual proof, testing, review notes, compat…
  • Fix all pre-merge checks with AI
✨ 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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2


ℹ️ Review info
⚙️ Run configuration
  • Configuration used: Repository UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 8ec38fa2-3605-4873-b605-3c938cfc393d
📥 Commits

Reviewing files that changed from the base of the PR and between b022406 and 855b261.

📒 Files selected for processing (45)
  • src/main/acp/acp-prompt-content.ts
  • src/main/acp/acp-structured-session-adapter-lifecycle.test.ts
  • src/main/acp/acp-structured-session-adapter.ts
  • src/main/acp/acp-structured-turns.ts
  • src/main/ai-vault/session-scanner-claude-title.test.ts
  • src/main/ai-vault/session-scanner-codex-parser.test.ts
  • src/main/ai-vault/session-scanner-text-normalization.ts
  • src/main/claude/claude-structured-dispatch-content.ts
  • src/main/claude/claude-structured-dispatch.ts
  • src/main/claude/claude-structured-queued-stop.test.ts
  • src/main/claude/claude-structured-session-adapter.ts
  • src/main/codex/codex-structured-session-adapter.ts
  • src/main/codex/codex-structured-turn-end-settlement.test.ts
  • src/main/codex/codex-structured-turn-start.ts
  • src/main/native-chat/agent-session-journal/journal-dispatch-reducer.ts
  • src/main/native-chat/agent-session-journal/journal-reducer.ts
  • src/main/native-chat/agent-session-journal/journal-restart-reconciliation.test.ts
  • src/main/native-chat/agent-session-journal/journal-restart-reconciliation.ts
  • src/main/native-chat/agent-session-journal/journal-rewind-submission-retirement.ts
  • src/main/native-chat/agent-session-journal/journal-row-builders.ts
  • src/main/native-chat/agent-session-journal/journal-row-schema.ts
  • src/main/native-chat/agent-session-journal/journal-stopped-history-carried.test.ts
  • src/main/native-chat/agent-session-journal/journal-store-contracts.ts
  • src/main/native-chat/agent-session-journal/journal-store.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-chat-stop.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-command-turn.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-delivery-loop.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-queued-message-rig-provider.test-fixture.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-rewind.test.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-stopped-history-frame.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-stopped-history-rendering.test.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-stopped-history-selection.test.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-stopped-history-selection.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-stopped-history.test.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-turns.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-unopened-send-withdrawal.test.ts
  • src/main/native-chat/agent-session-wire/structured-rewind-retained-host-rows.test.ts
  • src/main/native-chat/agent-session-wire/structured-rewind-retained-host-rows.ts
  • src/main/pi/rpc-prompt.ts
  • src/main/pi/rpc-session-adapter.ts
  • src/shared/agent-session-failure.ts
  • src/shared/agent-session-stopped-history.test.ts
  • src/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.
…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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1


ℹ️ Review info
⚙️ Run configuration
  • Configuration used: Repository UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 3263b8b4-618c-4b8a-95ef-f3e350d096bd
📥 Commits

Reviewing files that changed from the base of the PR and between 5cd9988 and f799dff.

📒 Files selected for processing (5)
  • src/main/claude/claude-structured-session-state.ts
  • src/main/native-chat/agent-session-journal/journal-store-collaborators.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-stopped-history-rewind.test.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-stopped-history.test.ts
  • src/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.

Comment thread src/main/claude/claude-structured-session-state.ts Outdated
@brennanb2025

Copy link
Copy Markdown
Contributor Author

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):

  • Codex shape. A stopped message injected with Codex's own <turn_aborted> record is stored exactly as written, survives resume, and never shows in Codex's item lists.
  • Codex follow-ups. "go ahead with that" acted 11/11, "actually ignore that" never did, and unrelated follow-ups did 0/10 (a real interrupt: 0/18; raw text with no record: 3/12).
  • Claude shape. Two shouldQuery:false entries are stored like Claude's own Stop. On resume, Claude treats them as its own interrupt. A single combined entry is not.
  • Codex image prep. Codex 0.159.0 resizes injected images: 4096 px was stored as 1600 px.

Decision review. Three independent reviews: all approve with changes. Every finding was ruled on before building. Among them:

  • inject into Codex only while it is idle;
  • never start an agent after Stop just to record history;
  • record each write before making it, and write at most once;
  • rewind handling only for Codex, the one agent with rewind.

Code review rounds. Each round had a Codex gpt-6.1-sol xhigh review and a Claude review of the full diff.

  • Round 1. Fixed:
    • rewind kept the "already written" record where Codex had dropped the stopped message;
    • Codex's idle check is now repeated just before the write;
    • image reads now have a time limit;
    • Claude settles once if its process dies mid-write;
    • Claude handling is down to a message-id check;
    • a dead older-Codex fallback branch was removed;
    • remote image links are left out;
    • Codex's <turn_aborted> is hidden in Session History.
  • Round 2. Fixed:
    • the ACP/Pi preparation time limit;
    • Codex images now use Codex's limit, not Claude's;
    • Claude is gated on its own idle state, which covers background wakes;
    • nothing is recorded while the agent is busy;
    • one shared rewind boundary;
    • trims.
  • Round 3. Fixed:
    • injected images go only to a Codex that resizes them (0.142.0+, from its tagged sources);
    • a stalled image read is left out rather than re-waited;
    • a rewind edge case;
    • one dispatch-row builder.
  • Final pass. A final Codex gpt-6.1-sol xhigh review of the whole diff found one issue: an abandoned image-read guard was released by the wrong read. It is fixed, with a regression test, and re-checked.

Not fixed: the rest are intended, and the PR body lists each:

  • Claude's pair is two adjacent writes;
  • a Stop pressed during the brief write window;
  • the Codex user-role record;
  • ACP/Pi riding with the next message;
  • no delivery if you never send again.

QA, hidden app, real agents.

  • Passed: Codex and OpenCode, stop during startup, then "go ahead with that" / "actually ignore that" / "what's 2+2".
  • Passed: Codex relaunch-and-resume. The resumed thread still lists the stopped message, then Codex's own record.
  • Blocked: Claude live rows. The Claude CLI login on the QA machine had expired.

Checks:

  • tc:node, tc:web and tc:cli;
  • oxlint;
  • the changed-lines code-quality gate;
  • orca-ci-checks;
  • all native-chat, agent and Session History test folders after the main sync: 2,151 files, 0 failed.

@brennanb2025

Copy link
Copy Markdown
Contributor Author

Follow-up: Claude verified, main merged

The 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 (82e5b3eebfa):

  • Main merged. A merge commit with no conflicts. It brings in main's translation fix, so CI now passes every check, including the unit-test phase it had skipped before.
  • Claude's model. After the stopped message is written into Claude's log, Claude behaves exactly as after its own Stop (sonnet and opus, fresh and warm sessions):
    • "go ahead with that": acted 8/8 (own Stop: 8/8);
    • "actually ignore that": 0/8 (own Stop: 0/8);
    • "what's 2+2": answered 4 only, 0/8 (own Stop: 0/8).
  • Claude hooks. Over 5 real-CLI runs, no UserPromptSubmit or Stop hook fired for the appended entries, while both fired for every real message.
  • Real-CLI Claude tests. The repo's two real-CLI Claude test files pass, 14/14.
  • Hidden-app QA with the real Claude. Stop during startup, then "go ahead with that" → pinecone, "actually ignore that…" → OK, "what's 2+2" → 4.
    • Each time Claude received the stopped text and [Request interrupted by user] as two no-turn entries, then one turn holding only the follow-up.
    • The chat showed one "Stopped manually" row and only the person's words.
    • After an app relaunch, the resumed Claude session listed the stopped message as interrupted, and Orca did not write it a second time.

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

Copy link
Copy Markdown
Contributor Author

Review summary: realignment of this PR (head 41341f0eeb5)

Two independent reviewers, a Codex gpt-6.1-sol xhigh review (read-only) and a Claude review, checked each stage against the common pattern, for simplicity and for correctness. Every finding was fixed or ruled on with a reason. Both reviewers ended clean on 41341f0eeb5.

Before building (plan review).

  • Accepted: carry only on a words-only message, with an encoded size bound. Drop the per-agent encoder checks. Treat a Codex without thread/inject_items (-32601) as a refusal and carry inline in the same message. Select newest-first and cover anything left out, so nothing arrives out of order. Split out the Codex rewind case as a temporary difference. Rename "append" to "injection".

After the build (eight rounds on the whole diff against main).

  1. Fixed: the bound now counts the interrupted marker, and keeps scanning past one older message that doesn't fit. Rejected: limiting the -32601 fallback further (the repo has one unsupported-method classification, and it fires only on a method-not-found response).
  2. Fixed: the whole delivery stays within one message's size, counting the larger transport's framing (512 bytes per message, pinned by a test). The "refused before any write" list was trimmed to the kinds that prove it. One constant for "Codex is gone".
  3. Tried a context-window budget, and fixed the owed sort.
  4. Stepped back. The budget immediately needed more guards: raw window and usage facts, an occupancy estimate for sessions without usage reports, the auto-compaction threshold, and a fix for the newest message being covered but not delivered. Returned to the one-message size bound, with the reasoning given in the PR body (labelled intended). Fixed: a send's carries record is written once and never removed.
  5. Fixed: the bound now covers the stopped messages together with your own words, so the agent is never handed more than one message you could send. If the newest doesn't fit, they all wait for a later message.
  6. Fixed: a stopped message that passes the size check but could never fit beside any message no longer holds the others back. It is simply not owed.
  7. Fixed: an off-by-one in that rule. It now reserves room for a one-letter message, the smallest one that can carry.
  8. Both clean.

Checks on 41341f0eeb5:

  • tc:node, tc:web and tc:cli pass, and the 14 touched test files pass (158 tests).
  • orca-ci-checks: 23 of 24 pass. The one failure is the bun-compiled typecheck variant, because this machine's bun 1.3.14 has no bun check (CI installs bun 1.4.x). The ordinary typecheck passes.
  • Hidden-window QA with real Claude, Codex and OpenCode passed every row. The PR body has the details.
  • PR CI: green except serialize-grid-transcript-replay.test.ts > codex-model-capacity-fullscreen. Its fixture came from main (Show Codex model capacity failures as red agent status #28231), the test fails the same way on another, unrelated PR, and this PR touches no daemon or terminal code. A one-off plugins file-watcher error in shard 3 passed on rerun.

Not verified: Pi live, SSH, Linux, Windows, the phone, and a Codex without thread/inject_items. Every Codex Orca supports has it.

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.
@brennanb2025
brennanb2025 merged commit 498818f into main Oct 14, 2026
35 checks passed
@brennanb2025
brennanb2025 deleted the brennanb2025/snu-stopped-history branch October 14, 2026 07:56
brennanb2025 added a commit that referenced this pull request Oct 14, 2026
…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.
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