Skip to content

fix(native-chat): keep queued messages in the chat's own record, so a refused one can't be skipped - #30418

Merged
brennanb2025 merged 31 commits into
mainfrom
brennanb2025/send-lifecycle-queue-derived-cards
Oct 23, 2026
Merged

brennanb2025 merged 31 commits into
mainfrom
brennanb2025/send-lifecycle-queue-derived-cards

Conversation

@brennanb2025

@brennanb2025 brennanb2025 commented Oct 18, 2026 •

Copy link
Copy Markdown
Contributor
Files Added Deleted Net
Test 43 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​927 $\color{#cf222e}{\Huge{\mathbf{−}}}$​1896 $\color{#cf222e}{\Huge{\mathbf{−}}}$​969
Prod 53 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​1396 $\color{#cf222e}{\Huge{\mathbf{−}}}$​1462 $\color{#cf222e}{\Huge{\mathbf{−}}}$​66

ELI5

You can queue messages behind a running chat turn. Two things went wrong with them. First, if the agent refused a queued message, it was supposed to come back as a card that holds the queue until you press its own Send or Delete; but when a bookkeeping save failed, the card silently vanished and the next message went out. Second, if you pressed Stop (or Orca restarted) after a queued message had already been handed to the agent, Orca put it back in the queue and sent it again later, even though the agent may already have had it. The only guard against that double send was matching the agent's echo of the text, and that guard kept producing new bugs. Now each queued message lives in the chat's own saved record, and a message that may have reached the agent stays in the conversation instead of going back to the queue.

What Changed

The problem.

  • Queued cards lived in their own database table, and a refusal had to update that table in a separate save. When that save failed (and its repair also failed), the queue thought the refused message had gone out: it sent the next card, and the refused card disappeared.
  • After a Stop or a restart, a queued or steered message that Orca had already handed to the agent went back to the queue whenever the agent's answer carried no failure, and Resume or your next send sent it again. Whether it went back was decided by the kind of rejection, not by proof the agent never got it. A Codex interrupt, for example, reports every message it never echoed as cancelled, even one that reached the model.
  • The only correction was to look for a later echo from the agent with the same text and withdraw the card. Nothing ties an echo to one particular send, so this produced bugs in both directions during review: one echo withdrew too few cards (a double send) or too many (a lost card). The same double send is tracked in STA-11036.

What you see now.

  • Press Stop after a queued or steered message was handed to the agent and before the agent confirmed it: the message stays in the conversation as the interrupted turn's message. If no turn had opened yet, it reads "Stopped manually", the same as a message you sent directly and stopped. It is never put back in the queue or sent again automatically. The cards behind it stay queued, and Resume sends them.
  • A message Orca can prove never reached the agent waits in the queue again, as before: one Orca was still holding, one stopped before or while the agent was still preparing it (for example Claude applying the message's model choice, or Codex still opening the previous turn), or one Claude reports it removed without running it.
  • A refused message still comes back as a blocking card that needs its own Send or Delete. Resume can't release it. That now holds after a failed save, a rewind, a replacement of the chat's history, and a restart.
  • Normal queue flows (queue, edit, delete, steer, send in order, refused card Send or Delete) look and behave the same as before.

The mechanism.

  • Every queued message is part of the chat's journal, the record the conversation itself is saved in. A new queue entry kind records queue, edit, move, hold and withdraw, and each action writes its entries in one transaction. Handing a card to the agent is the send entry that already names the card. A refusal writes only the rejection the delivery path already writes; the returned card and its hold on the queue are worked out from it when the journal is read. The old queue table, its settlement writer, its repair and the derived-card view are deleted.
  • One rule decides what a stopped hand-off becomes (queuedHandOffOutcome). A failure brings the card back as a blocking card. Proof the agent never got it puts the card back to waiting. Proof means the host still held it, or whoever settled it recorded that it never reached the agent (Claude's report that it removed a queued message unrun now records this). Anything else is in doubt: the card stays spent and its message shows as unconfirmed, using the existing "couldn't confirm" state that every client already draws. This is worked out each time the journal is read, from the stored rejection, so nothing new is saved.
  • The echo text matching and all its bookkeeping are deleted. The restart check against the agent's history no longer decides that a queued hand-off was not sent, and reopening a chat no longer holds the queue for a message that can't come back.

Existing queued cards (moving from the old table).

  • Cards saved by an earlier build move into the journal once, when the chat opens, in one transaction. Running it twice does nothing. Until a card is moved it still shows and every action works on it: the action moves that one card in its own transaction, and a rewind moves every remaining card first. If the move fails, a message that may already have reached the agent remains visible in the conversation and is never sent again automatically.
  • An earlier build put a card back to waiting after a Stop and cleared its link, so the card's own hand-offs in the journal decide on upgrade:
    • Unchanged, and its latest hand-off may have reached the agent: it becomes a message in the conversation (the interrupted turn's message, or "Stopped manually") and is not sent again.
    • Edited since, or sent again since: it is held as a "Paused" card that needs its own Send or Delete. The queue sends the cards behind it.
    • No hand-off on record, or only ones that provably never left Orca: an ordinary waiting card, as before.
  • Claude cards already sitting in a paused queue at upgrade become messages and aren't resent, because the earlier build didn't record Claude's report that it removed them unrun. The same Stop after the upgrade does put them back to waiting.
  • A card an earlier build put back to waiting after a restart, because the agent's history lacked its text, is also spent on upgrade: that check was a guess from absence too.
  • Refused, sent and deleted cards move as before. A card whose outcome can't be read is kept hidden and never resent.

Compatibility. No new saved field and no wire change. The upgrade's held card uses one new host-only value of the existing hold reason, published as a plain hold. The host now publishes an existing state ("couldn't confirm", or a stopped direct send) where it used to publish a returned card, and desktop, phone and older clients already draw both. Everything runs on the host that owns the chat, including over SSH.

STA-11036 (a delivered queued message sent twice). After this PR it can't happen: nothing puts a message that may have reached the agent back in the queue, so there is nothing to send twice and no echo to match. No separate card write exists any more either, so a failed save can't lose the outcome.

Fixed in review.

  • One update confirming two queued messages withdrew only the first, so the second was sent again.
  • A card from an earlier build whose hand-off was still unconfirmed was dropped by the move, so a later refusal could not bring it back.
  • A rewind after a failed move lost an earlier build's refused card, and the card behind it was sent.
  • One echo from the agent withdrew every queued card with the same text, so an identical message queued later was lost; it then withdrew only one card.
  • The echo matching that the first and last of these fixes changed is now deleted, along with the two remaining identical-text cases it still had.

Why

Both bugs came from the queue deciding something it couldn't know from what it stored.

  • The refused card vanished because the queue kept a second saved copy of a fact the chat already holds. Keeping the card in the chat's own record removes that whole class of bug instead of guarding the copy. The common pattern also keeps each queued message as one record with its attempts, changed in the same transaction as the action.
  • The double send happened because a message that may have reached the agent was put back in the queue, then "corrected" by text matching. The common pattern never returns a handed-over message to the queue: its outcome stays on that attempt, and resending is never automatic. Orca's delivery rule is the same: Orca never decides a message was not sent because something is missing, and never resends a message it can't confirm.

Differences from the common pattern and from the plan:

  • Stopped message after a failed upgrade import. Temporary: an already-open chat that reconnects or pages older history after a failed upgrade import shows the stopped message only after reopen; tracked in STA-11608. The follow-up fixes history publication and preserves older clients’ approval controls.
  • The refused card's hold is worked out from the refusal, not saved as a flag. Intended: a stored flag is a second write that can fail on its own, and nothing rebuilds it.
  • Resume does not release a refused card. Intended: Orca's product rule is that only the card's own Send or Delete releases it. The common pattern's Resume clears the hold, and it leaves a failed message in the transcript instead of returning a card.
  • The unconfirmed message has no resend action yet. Temporary: Stage B, tracked in STA-11448, adds Send, Edit and Delete on such a message. Until then a steered message that Codex most likely dropped on interrupt reads as sent, and you send it again yourself if you need to.
  • The "unconfirmed" outcome is worked out from the stored rejection each time the journal is read, not saved as its own entry. Intended: one derivation covers live chats, reopened chats and upgraded chats, with no rewrite of saved data.
  • Claude's "removed unrun" report counts as proof only for queued messages. Intended: on a direct send the same flag would change what Stop does to it, which is outside this PR.
  • After a crash while a queued message was being handed over, the message stays as an unconfirmed message and is never resent; a direct send in the same case still goes through the history check and can read "not sent" with Retry. Temporary until Native chat: a message is never marked not delivered after a crash, and reopening a chat stops reading the transcript #30580, which removes that history check for direct sends too, so both behave the same.
  • If a Stop closes Claude while a queued message is still waiting behind another one inside Orca's own hand-off queue to Claude, that card comes back as a blocking "couldn't hand to Claude" card (press its Send) instead of quietly waiting again. Temporary: tracked in STA-11660. The follow-up treats that unwritten failure during a Stop the same as the Stop itself.
  • In an OpenCode chat, a queued message left unconfirmed (a crash mid hand-over, or the agent exiting before it answers) makes each later reopen of that chat read OpenCode's own history again, up to 5 seconds, until /clear. Temporary until Native chat: a message is never marked not delivered after a crash, and reopening a chat stops reading the transcript #30580, which removes the history check that would otherwise settle it. Nothing is lost or resent.
  • An earlier build's card that was edited or sent again after a Stop is held with the plain "Paused" caption, and the cards behind it send first. Intended: the held card might already have reached the agent, so only its own Send may send it; this is how an existing held card already behaves, and reusing the plain hold needs no new UI or client change.
  • An earlier build's card whose echo already arrived before the upgrade can show as two messages. Intended: rare, limited to cards saved by an earlier build, and it is never sent again; telling them apart would bring back the text matching this PR deletes.
  • In one chat, a queued message stopped this way gets a tick on the message rail, while a direct send stopped the same way does not. Temporary (cosmetic): Stage B, tracked in STA-11448, adds the same recovery card for both kinds.
  • Unresolved cards are written again when the chat's history is replaced or rolled over. Intended: Orca deletes the journal epoch in those cases, so unresolved queue facts must move with it. The new copy, including a returned card's refusal, is written in the same transaction that deletes the original, so only one copy exists at a time.
  • Cards saved by an earlier build are still read from the old table until they are moved, and each action moves its own card. Intended: the plan allowed the old reader only for the one-time move. Keeping it means that if the move fails, the cards stay visible and usable and a refused card still holds the queue.
  • Deleted cards are no longer remembered for a short window after the move or after the chat's history is replaced. Intended: only a retried request (after a lost acknowledgement) for a card deleted before that point is affected; it is now told the message is missing or may already be in the chat, instead of "withdrawn". Current clients send each request once.
  • A moved /compact card that came back refused, but whose command already ran, now reads as done and disappears after the upgrade. Intended: the command did run; main kept showing the card.
  • Rolling the chat's history over keeps a card that was never handed over waiting. Intended: it never went out, and nothing in the shipped app rolls history over today.
  • Queue entries are saved in record format 4, so an older build opens such a chat read-only, including an older server sharing the same SSH host. Intended: users don't downgrade Orca, and the earlier format-3 change works the same way.

Not changed here: Claude can still refuse an omitted effort after an earlier override (STA-11572), or Fast when the agent reports a disabling reason other than a user preference or required opt-in. The incoming model-choice change removed the earlier missing-positive catalog Fast check; its old STA-11451 trace no longer applies to this head. The remaining checks are inherited from main and owned by the separate Claude model-switch work. This review does not claim they are aligned or waived.

Not changed here: A later send carries a stopped request’s text, and the agent may answer that earlier request. Real QA reproduced the same behavior on main using the same saved chat; this PR does not change it.

Linked Issue

Part of the native-chat send lifecycle work. Not a GitHub issue. Tracked in STA-11036 (a delivered queued message sent twice); see the STA-11036 paragraph above.

Visual Proof

The current head is ef47118d3bc3693423ab185726da758a0a350d6d, after the latest main merge. All 44 CI checks finished (27 successful, 17 skipped); the bounded source review found no new proven merge regression, and four named lifecycle files passed all 25 tests. Exact-head real-agent QA passed Claude ordinary queue/drain, Stop with actual Resume and normal close/reopen, plus Codex Stop with actual Resume and normal close/reopen. The owner's final M4Air takeover also passed Codex ordinary queue/drain, image A removal and B replacement with edited text, and three cards waiting together then draining naturally in order. Codex's own saved record confirms the edited text and byte-identical B, no A or original text, and one reply each: “Blue, with a white letter B.”, “Q2-OK”, “Q3-OK”. The app was stopped by its recorded PID, admission1488 is done, and the owner's real-config hashes match. Current-head screenshots and the full results are in the single review status comment.

Earlier failed driver/setup attempts are preserved separately and never counted as passes: ticket1381 ordinary-Codex method, ticket1439 unavailable attachment method, ticket1475 preparation parse error, and the loop's initial ticket1488 dev app exit. The owner traced that exit to the dev parent watchdog and corrected the rig for the successful takeover. Stale Settings copy about immediate image sends predates this PR and is owned by the B1 recovery follow-up; no current-head copy fix is claimed.

The upgrade and phone evidence below belongs to the earlier 293bb8254496 head. Those unchanged flows were excluded from the latest bounded rerun by the lane owner; the screenshots retain their original head labels. Grok remains covered by tests only under Brennan’s usage waiver. The queue is feature gated in shipped builds; the app tests use only the approved test capability advertisement and read-only store exposure.

Earlier completed review and QA (293bb8254496)

CI was green on earlier head 293bb82544967f097081040cb21d7d55ce17636c, including the WSL retry; GitHub reports no merge conflicts. Main is merged, 19 targeted files passed (347 tests), and the final source/readiness review found no new proven regression with the inherited and temporary differences recorded above. Grok was covered by tests only: three named shared adapter/lifecycle files passed (53 tests) under Brennan's usage waiver.

Desktop QA on this head carried genuine Claude and Codex chats from the previous e9bfdafc1ddd build through an actual restart/upgrade. Both reopened through Session History and produced two new real replies each. Both completed ordinary queue/drain with one delivery and reply. Codex also completed queued image replacement and text editing: Save delivered the exact blue replacement PNG and edited text, followed by the real BLUE_ONLY_1168 reply. These were fresh builds with the approved test-only queue capability advertisement and read-only store exposure; queue availability is feature gated in the shipped build.

The remaining desktop checks passed under M4Air admission1227 on this same head. Three cards waited together during a real Codex turn and drained in order, with one host submission, accepted dispatch, native input and real reply for each. A separate running turn was stopped with a card queued: it stayed Paused with no delivery during the bounded observation, then the actual Resume control delivered it once and produced one real reply. The earlier incomplete attempts remain recorded separately. Both desktop rigs are stopped and their profiles/evidence preserved.

One additional observation: the native sleeping command remained live after Stop while the queue correctly stayed paused. This run establishes queue pause and delivery behavior; it does not establish that Stop terminated the command, or that this observation is a regression. Final-head native iOS phone QA also passed: one actual mobile Send showed one queued card, drained normally to one real Codex reply, and stayed correct after workspace re-entry. Host journal and independent provider receipts each confirm exactly one delivery and reply. The first harness-only attempt sent no mobile message and remains incomplete; one diagnosed, approved correction produced the accepted evidence. The final review is complete on this head. The failed-import history publication repair remains a separate follow-up tracked in STA-11608.

Earlier-head desktop evidence (293bb8254496, ticket1168)

Both saved chats after upgrade, with the previous reply and two new real replies:

Claude Codex
final-upgrade-claude-two.png final-upgrade-codex-two.png

Ordinary queue/drain completed with Claude; the image flow below also shows Codex's completed ordinary queued reply:

final-normalqueue-claude-drained.png

Codex queued image/text before editing, the blue replacement in the editor, and the real reply after Save:

Before Edit Replacement ready to Save Delivered replacement and reply
final-image-original.png final-image-editing.png final-image-final.png

The running turn ended while the editor was open, so Save drained immediately; no saved card still waiting was captured. Retained journal and provider records confirm one delivery of the edited text and exact replacement PNG bytes.

Earlier-head desktop FIFO and Stop/Resume (293bb8254496, ticket1227)

Three cards waited together, then each produced one real reply in order:

Three waiting Drained in order
fifo-corrected-three-waiting.png fifo-corrected-drained.png

Stop kept the queued card paused; the actual Resume control delivered it once and produced one real reply:

Paused After Resume
stop-paused.png stop-drained.png

Pre-merge desktop evidence (e9bfdafc1ddd)

Image edit and actual delivered replacement

Editing queued card Actual send
branch-image-drop-editing.png branch-image-drop-drained.png

Stop holds the queue; Resume sends each card once

Paused After Resume
branch-stop-paused.png branch-stop-drained.png

Old chats after restart: two real replies each

Claude Codex
branch-upgrade-claude-two.png branch-upgrade-codex-two.png

Three queued messages, in order, once each

FIFO

Inherited stopped-text carry reproduced on merged main

Main comparison

Earlier-head native iOS phone evidence (293bb8254496, ticket1169)

The actual paired iOS app sent one message while a real Codex turn ran. One queued card drained normally to one real reply; reopening the workspace kept the message and reply without reviving the card.

Paired native app Queued card Normal drain and real reply After workspace re-entry
native-paired.png native-queued.png native-drained.png native-final-stable.png

The invalid older QA host remains visible beside the fresh connected host in the pairing screenshot. The test used the fresh host and a new chat throughout. This is ordinary iOS Codex coverage; Android, mobile images/editing/FIFO/Stop/Resume/restart/upgrade and the failed-import retained-history follow-up were not repeated on phone.

Pre-merge native iOS phone evidence (e9bfdafc1ddd)

Phone coverage is this ordinary Codex flow only; Android, images, multi-card FIFO, editing, Stop/Resume, restart and upgrade were not repeated on phone.

Paired native app Queued card Normal drain and real reply
Native paired Native queued Native drained

Earlier-head evidence

The evidence below is from earlier reviewed heads, ending at e9513f8. The desktop results above are from e9bfdaf; the older screenshots below do not certify this head.

Real Electron QA on a separate Mac, with the real Codex and Claude Code CLIs (both builds used the same local switch that turns on mid-turn queuing, which is hidden in shipped builds). Every flow matched main: queuing and draining, Edit, Delete, Steer, Stop and Resume, restart, /compact, a refused card, and the upgrade. The Stop-after-hand-over change could not be shown live: both agents confirm a handed-over message within milliseconds, so a Stop clicked from the UI always lands after the confirmation, on main and on this branch alike. That case is covered by the automated tests. A final smoke run on Windows at the last head (real Codex and Claude) passed all eight flows, including a Stop 46 ms after hand-over (the message stays in the conversation and is never resent) and a Stop while the agent was still starting up (the card waits again).

Upgrade: a profile made by a main build, then opened by this branch

Main build, at quit This branch, after opening the same profile
main at quit branch after upgrade
main refused at quit branch refused after upgrade

Stop right after a queued message is handed over (Codex)

Main This branch
main stop branch stop

A refused queued message (failed start), then Send

Main This branch
main refused branch refused

Testing

  • structured-agent-session-queued-stop-row.test.ts, a card handed over and stopped before the agent confirms it:
    • a steered card stopped inside a Codex turn stays as that turn's unconfirmed message, drawn the same by the shared view older clients use;
    • the next card, stopped before its turn opens, reads "Stopped manually";
    • neither card goes back to the queue; Resume sends only the card behind them;
    • a restart afterwards holds nothing.
  • structured-agent-session-stop-aborts-start.test.ts: a queued card stopped before the agent call, or while the agent prepares it, waits again under the Stop.
  • claude-structured-message-options.test.ts: a Stop while Claude applies a message's model choice is a stop, not a refusal; a real refusal still is one.
  • structured-agent-session-claude-queued-stop.test.ts: a card sent into a running Claude turn, which Claude reports cancelled unrun, comes back paused and is not sent twice.
  • journal-dispatch-settlement.test.ts: the one rule. A Stop puts a card back to waiting only with proof the agent never got it; a restart or close before hand-over puts it back to waiting; a failure returns the card.
  • structured-agent-session-queued-legacy-import.test.ts:
    • cards from an earlier build, in every saved state, read the same before and after the move;
    • an unchanged card stopped after hand-over is spent and drawn as a stopped message; an edited one is held, skipped by the queue, and sent by its own Send;
    • a failed first move leaves the cards visible and usable, and each action moves only its own card;
    • a rewind while the move has failed moves the cards first, and a refused one stays refused;
    • the next open moves the rest, and the open after that writes nothing.
  • structured-agent-session-queued-refused-card.test.ts, the refused card through the host: it holds the cards behind it through a rewind, a history rollover, a restart and a Resume; its Send is refused again and still holds; Delete releases it and the next card sends.
  • The Claude unit tests for command lifecycle, control actions and queued Stop are updated for the "removed unrun" report. The echo-matching test is deleted with the code.
  • I manually tested these changes locally
  • Automated tests added/updated, or explained why not below

Notes

Ensure no issues in: Security, Cross-platform support (Linux, Windows, Mac), Remote SSH, Mobile, general backwards compatibility, performance

  • SSH and mixed versions: host-owned, with the same published card and message shapes.
  • Mobile reads the same list and draws messages through the same shared view.

Checklist

  • This PR is small and focused
  • I explained what changed and why
  • 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 (CI will cover)

Author: @BrennanKB5

…n its card write fails

Every reader of a queued card goes through one derived view of the journal's dispatch facts,
so a card whose message was refused reads as returned even when its card-table update and heal
both failed. One decision and one writer store the derived state for every action.
@brennanb2025
brennanb2025 marked this pull request as ready for review October 18, 2026 20:51
@coderabbitai

coderabbitai Bot commented Oct 18, 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

Queued cards now derive from journal rows and reducer state rather than standalone SQLite draft storage. Queue writes, hand-offs, holds, withdrawals, and context clears use journal queue plans. The change adds import support for readable legacy rows and restates retained cards during epoch changes. Tests cover refusal behavior across rewind, rollover, restart, Resume, resend, and deletion.

Priority: ⬆️ High

Merge Risk: 🟡 Moderate · up to 88c44

A legacy queued card can be lost during import when two cards match one delivery echo. Fix echo pairing before merging.

Pre-merge checks: Passed 3 · Failed 2

Failed Failed checks: 2 warnings

Check name Status Explanation Resolution
Docstring Coverage Warning Docstring coverage is 51.04% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 96 functions across 62 files. Write docstrings for the functions missing them to satisfy the coverage threshold.
Description check Warning The description is detailed and covers the main change, rationale, testing, visual evidence, compatibility, and checklist. However, the required Linked Issue section does not link an actual issue; it … Add a direct link to the issue addressed by this PR, complete the Review section, and select or explain the Agent skill upstream boundary checkbox. Also update the lint, typecheck, test, and build checklist with the actual verification stat…
Passed Passed checks: 3 passed
Check name Status Explanation
Title check Passed The title clearly identifies the main change: storing queued messages in the chat record so refused messages cannot be skipped.
Linked Issues check Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check Passed Check skipped because no linked issues were found for this pull request.
Full details: Description check

Explanation

The description is detailed and covers the main change, rationale, testing, visual evidence, compatibility, and checklist. However, the required Linked Issue section does not link an actual issue; it explicitly says “Not a GitHub issue” and only references STA-11036. The Review section is also empty, and the Agent skill upstream boundary section is not completed.

Resolution

Add a direct link to the issue addressed by this PR, complete the Review section, and select or explain the Agent skill upstream boundary checkbox. Also update the lint, typecheck, test, and build checklist with the actual verification status or CI coverage.

✨ Finishing Touches: 💡 1
📝 Generate docstrings: 💡
  • Commit to this branch
  • Create a new PR
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@brennanb2025

Copy link
Copy Markdown
Contributor Author

Review loop status: ready for Brennan's review

The problem. In a native chat, messages can wait in a queue while the agent is busy. When the agent refuses a queued message, Orca's rule is that the message comes back as a "returned" card. The cards behind it then wait until you Send or Delete it. The card's state was saved in a separate write from the chat's record of the refusal. If that write failed, and the later repair write also failed, the card still read as "handed off". The queue then sent the next message past the refused one. The refused card also disappeared: its Send said "already sent" and its Delete did nothing.

What changes for users. When saves succeed, nothing you see changes. When those writes fail, the refused message now still comes back as a blocking card, and Send, Delete and Edit on it work. Every reader now works out a card's state from the chat's own saved record, so a failed save can no longer make the queue skip a refused message.

Review. One full round with four independent reviewers: the queue journal, the host and clients, a main-vs-branch trace of everything the user sees, and an architecture challenger. That was followed by the readiness checklist. They found no P0, P1 or P2 problems, so no code fixes were needed. The challenger kept the design: it is the same move earlier fixes made for the Stop and restart pauses, which were also changed to derive their state from the chat's record. The common transactional alternative writes its queue hold after the commit and only logs a failure, so it has this same bug.

Deferred (P3, not fixed here):

  • Each queue read rebuilds a short change key over the cached handed-off cards. There is no SQL and no parsing; the cost is microseconds at realistic sizes.
  • The refusal hook scans all of the session's cards once per refused hand-off; main did a single update.
  • While a repair keeps failing, every journal commit retries it and logs a warning, with no backoff. Main does the same. Follow-up candidate.
  • Known limit, now worded more precisely in the PR body: if the hook and every repair fail and the chat is then rewound, the returned card leaves the queue and the next one sends. Main had already sent it by then.
  • Two concerns were traced and refuted. (1) An action's first catch-up write can't fail in a case where main's own action on that row would have worked. (2) A rewind can't bring a used-up command card back as a blocking card.

Verified.

  • CI is green on this head, ba0681beacc5c008f7ad66d03441818f6ec9f192 (the PR checks passed gate passed), and the merge state is CLEAN.
  • Real Electron QA ran on main and the branch with the real signed-in Claude CLI, driven by Grok over the Chrome DevTools Protocol (CDP), on an isolated test copy. Mid-turn queueing is hidden in shipped builds, so both arms got the same local, uncommitted patch that turns it on.
  • Results matched on both arms:
    • three messages queued behind a long turn drain in order;
    • Stop pauses the queue and Resume sends in order;
    • Edit and Delete on queued cards;
    • Send now ("Steer") on the second card;
    • restarting the app with cards queued shows the same cards;
    • a /compact sent mid-turn queues and then runs (shipped path, no patch);
    • a plain send works.
  • On the branch, a queued message whose start failed came back live as a returned card ("Your message was not sent.", with Send and Delete). The card behind it read "Waiting — a message ahead needs attention".

Not verified.

  • The PR's own failure case (card writes failing) can't be produced in a running app. Automated tests cover it.
  • The failed-start returned card wasn't captured on main, because the QA lease ran out. The code trace shows main behaves the same when saves succeed.
  • After a restart, continuing the chat failed on both arms. That's a QA setup artifact: the test pointed Claude at its real home, so Orca didn't find the transcript to resume.

QA noticed these on both arms. The PR didn't cause them; they're listed for the lane:

  • After a restart, queued cards read "Queued" with no Resume on the queue.
  • A failed start says so in several places: the card, plus stacked red rows that tell you to send a message that is still sitting in the card.
  • Queued cards still offer "Steer" when nothing is running.

For Brennan's review

Flow main branch
Three queued behind a long turn
Drained in order
Stop, paused
After Resume
Edit and Delete settled
Send now (Steer) settled
Before restart
/compact queued (shipped)
Failed start: returned card not captured

Three places where a human judgment matters:

  1. effectiveQueuedCard: a stored "handed off" card whose message was refused reads as returned, waiting or spent. This is the whole behavior change, and it applies only when the stored row is out of date.
  2. Each action stores its own card before acting (also lines 148, 214, 234 and 286). Send, Delete, Edit, /clear and the failed-send hold now write the card's settled state inside their own transaction first. If that write fails, the action fails, but on main that card had no working action at all.
  3. revision(): published lists refresh when only the derived state changes, at a small per-read cost (deferred P3 above).

…, not a second table

A queued message lived in its own SQL table, and a hook that may be skipped copied
each refusal from the journal into it. When that write failed, a refused card
vanished: the cards behind it sent, and its Send and Delete did nothing.

Each queued message is now a `queue` row in the chat's journal beside the
submission that hands it off and the dispatch row that answers it. The reducer
folds the card from those rows, so a refusal returns the card and holds the
cards behind it with nothing written, and replay reads the same. Every action
writes its own rows in one transaction. Cards an earlier build stored are read
through the same classification until they move: each action moves its own
card, and a bulk import runs once per open. A new epoch restates every card
still owed an answer in the transaction that deletes the old rows.

The settlement hook and its savepoint, the open-time heal, the drain's heal and
the derived-card view are gone. The published card list is unchanged.
Cards are written through the journal, so tests queue them with their body
alone and hand them off under their real fingerprint. Tests of the removed
settlement hook, heal and SQL store are deleted; the text-update spec now
covers the edit decision alone, and a test of an earlier build's stored holds
seeds the legacy table and reads the imported cards. Rolling over an epoch now
keeps a card whose hand-off was never handed over, so that test checks the
retried id never sends rather than that nothing sends.
…ew epoch and a restart

One whole-flow test through the host: the refusal writes no card row, a rewind
that retires the refused hand-off and a rolled-over epoch keep the card
returned, a restart and Resume send nothing behind it, its own Send refused
again still holds the queue, and its Delete releases the next card in order.
…nal once, each read the same

One mixed-import test through the host. The first open's import fails and
moves nothing, yet every card reads as it will; Delete and Send each move only
their own card. The next open moves the rest in one transaction, writing a
refusal only where its hand-off is gone, nothing for answered cards, and leaving
an unreadable row on disk. A later command turn spends a moved command card, a
third open writes nothing, an old Delete replays from its receipt, and a chat
with only answered legacy rows clears them in a delete-only transaction.
@brennanb2025 brennanb2025 changed the title fix(native-chat): a refused queued message still blocks the queue when its card write fails fix(native-chat): keep queued messages in the chat's own record, so a refused one can't be skipped Oct 19, 2026

@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

Caution

Some comments are outside the diff and can’t be posted inline due to GitHub limitations.

⚠️ Outside diff range comments (2)

🟠 Major · Remove the stale third argument in clearContext. It breaks… · queued-message-pause.spec.ts:72

src/main/native-chat/agent-session-journal/queued-message-pause.spec.ts:72
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Remove the stale third argument in clearContext. It breaks the CI typecheck.

This PR changes JournalContextController.clear to take two parameters: boundary and receipt (journal-context-controller.ts Line 26). The clearContext helper still passes `caller-clear-${clock}` as a third argument. pnpm run typecheck:ci fails at Line 72 with TS2554: "Expected 2 arguments, but got 3". This failure blocks the PR checks. Delete the argument. The other callers in this PR, such as agent-session-attachment-claims.test.ts, already pass only two arguments.

🐛 Proposed fix
   function clearContext(journal: AgentSessionJournal) {
     return journal.context.clear(
       { operationId: `clear-${++clock}`, afterFence: 0, clearedAt: clock },
-      { write: () => {}, committed: () => {} },
-      `caller-clear-${clock}`
+      { write: () => {}, committed: () => {} }
     )
   }

Source: Pipeline failures

🟠 Major · Add the missing queue fixture. · journal-unknown-row-kind.spec.ts:120-135

src/main/native-chat/agent-session-journal/journal-unknown-row-kind.spec.ts:120-135
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Add the missing queue fixture.

rowOfEveryKind() requires one entry for every JournalRow['kind'], but it omits queue. TypeScript therefore reports TS2741, and pnpm run typecheck:ci fails.

Suggested fix
 import {
   AGENT_JOURNAL_THREAD_SCOPE,
+  AGENT_SESSION_JOURNAL_QUEUE_SCHEMA_VERSION,
   AGENT_SESSION_JOURNAL_SCHEMA_VERSION,
   type AgentJournalItemIdentity,
   type AgentSessionJournalIdentity
 } from '../../../shared/agent-session-journal-types'
@@
       'lifecycle-batch': {
         ...base,
         seq: 6,
         kind: 'lifecycle-batch',
         settlementId: 's',
         mutations: [{ kind: 'tombstone', itemId: 'i', revision: 3 }]
-      }
+      },
+      queue: {
+        ...base,
+        v: AGENT_SESSION_JOURNAL_QUEUE_SCHEMA_VERSION,
+        seq: 7,
+        kind: 'queue',
+        messageId: 'q',
+        change: { op: 'withdrawn' }
+      }
🧹 Nitpick comments: 1
src/main/native-chat/agent-session-wire/structured-agent-session-queued-refused-card.test.ts: 1

120-120: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Assert that Resume succeeds.

If rig.resume() returns an error, the cards can remain held and the later assertions can still pass. Check the result so this test verifies the successful Resume path.

Proposed change
-  await rig.resume()
+  expect(await rig.resume()).toMatchObject({ ok: true })

ℹ️ Review info
⚙️ Run configuration
  • Configuration used: Repository UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: d6ecd96d-09bd-4eab-8ecd-7aaebe1e5b49
📥 Commits

Reviewing files that changed from the base of the PR and between ba0681b and 4db1581.

📒 Files selected for processing: 67
  • src/main/native-chat/agent-session-attachments/agent-session-attachment-claims.test.ts
  • src/main/native-chat/agent-session-journal/command-receipt-transaction.spec.ts
  • src/main/native-chat/agent-session-journal/journal-closed-sets.test.ts
  • src/main/native-chat/agent-session-journal/journal-context-clear.ts
  • src/main/native-chat/agent-session-journal/journal-context-controller.ts
  • src/main/native-chat/agent-session-journal/journal-dispatch-reducer.ts
  • src/main/native-chat/agent-session-journal/journal-dispatch-settlement.ts
  • src/main/native-chat/agent-session-journal/journal-epoch-controller.ts
  • src/main/native-chat/agent-session-journal/journal-epoch-replacement.spec.ts
  • src/main/native-chat/agent-session-journal/journal-epoch-replacement.ts
  • src/main/native-chat/agent-session-journal/journal-epoch-rollover.ts
  • src/main/native-chat/agent-session-journal/journal-queue-events.ts
  • src/main/native-chat/agent-session-journal/journal-queue-fold.ts
  • src/main/native-chat/agent-session-journal/journal-queue-import.ts
  • src/main/native-chat/agent-session-journal/journal-queue-retention.ts
  • src/main/native-chat/agent-session-journal/journal-queued-message-contracts.test.ts
  • src/main/native-chat/agent-session-journal/journal-queued-messages.ts
  • src/main/native-chat/agent-session-journal/journal-reducer.ts
  • src/main/native-chat/agent-session-journal/journal-row-schema.ts
  • src/main/native-chat/agent-session-journal/journal-row-writer.ts
  • src/main/native-chat/agent-session-journal/journal-store-collaborators.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-journal/journal-submission-hook.ts
  • src/main/native-chat/agent-session-journal/journal-submission-queued-link.spec.ts
  • src/main/native-chat/agent-session-journal/journal-submission-writer.ts
  • src/main/native-chat/agent-session-journal/journal-unsent-send-hold.spec.ts
  • src/main/native-chat/agent-session-journal/journal-unsent-send-hold.ts
  • src/main/native-chat/agent-session-journal/queued-message-bookkeeping-failure.spec.ts
  • src/main/native-chat/agent-session-journal/queued-message-delivered-echo.spec.ts
  • src/main/native-chat/agent-session-journal/queued-message-delivered-echo.ts
  • src/main/native-chat/agent-session-journal/queued-message-edit-leases.test.ts
  • src/main/native-chat/agent-session-journal/queued-message-holds.ts
  • src/main/native-chat/agent-session-journal/queued-message-pause.spec.ts
  • src/main/native-chat/agent-session-journal/queued-message-positions.ts
  • src/main/native-chat/agent-session-journal/queued-message-retention.ts
  • src/main/native-chat/agent-session-journal/queued-message-settlement.ts
  • src/main/native-chat/agent-session-journal/queued-message-store.spec.ts
  • src/main/native-chat/agent-session-journal/queued-message-stored-row.ts
  • src/main/native-chat/agent-session-journal/queued-message-table.ts
  • src/main/native-chat/agent-session-journal/queued-message-text-update.spec.ts
  • src/main/native-chat/agent-session-journal/queued-message-text-update.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-clear-rewind.test.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-codex-version-recovery.test.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-conversation-open.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-ended-prompt-cancel.test.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-event-recovery.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-host-lifetime.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-queued-action-receipts.test.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-queued-card-holds.test.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-queued-edit.test.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-queued-legacy-import.test.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-queued-message-rig.test-fixture.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-queued-messages.test.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-queued-messages.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-queued-mutations.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-queued-pause.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-queued-refused-card.test.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-queued-send-answer.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-queued-withdrawn-draft.test.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-restart-interruption-test-harness.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-stop-event.test.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-unsent-send-hold.test.ts
  • src/main/native-chat/agent-session-wire/structured-conversation-command.ts
  • src/main/runtime/agent-session-conversation-clear-commit.test.ts
  • src/main/runtime/structured-agent-session-failure-logging.test.ts
  • src/shared/agent-session-journal-types.ts
💤 Files with no reviewable changes: 14
  • src/main/native-chat/agent-session-journal/queued-message-bookkeeping-failure.spec.ts
  • src/main/native-chat/agent-session-journal/queued-message-retention.ts
  • src/main/native-chat/agent-session-journal/journal-submission-hook.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-queued-message-rig.test-fixture.ts
  • src/main/native-chat/agent-session-journal/queued-message-holds.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-restart-interruption-test-harness.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-queued-withdrawn-draft.test.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-codex-version-recovery.test.ts
  • src/main/native-chat/agent-session-journal/journal-queued-message-contracts.test.ts
  • src/main/native-chat/agent-session-journal/queued-message-store.spec.ts
  • src/main/native-chat/agent-session-journal/queued-message-positions.ts
  • src/main/native-chat/agent-session-journal/queued-message-edit-leases.test.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-queued-pause.ts
  • src/main/native-chat/agent-session-journal/queued-message-settlement.ts

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

The row-kind table lists a queue row, the clear and legacy-import tests
match the current signatures, and the downgrade test keeps only the
leftover-send case: a queued card is now a newer-version journal row an
older build keeps read-only, and users don't downgrade.
…on hook

No caller passes one since the queue's savepoint bookkeeping left.
…uilds read

The highest version this build reads is now 4, the queue row's. A newer
build's row in these probes is written at version 3 so the older build
meets its kind, not a newer version, and the Stop-event probe hands the
older build the rows as stored, since a read raises each row's version.
The legacy-import test opens real SQLite, so it runs in the Node runtime
project. The newest version this build reads is now 4, and a read raises a
row's version to it, so the turn-lifecycle test no longer pins the
in-memory version (the written one stays pinned by journalRowSchemaVersion).
…s both

The per-row delivery check returned only the first waiting card an echo
proved delivered, so when one settlement carried echoes for two cards the
second stayed waiting and was sent again. It now returns every card the
row proves delivered, and the fold withdraws each; replay folds the same.
…urnal first

While the open's import had failed, an earlier build's card that read as
refused through its hand-off lost that reading when an in-place rewind
retired the hand-off: it disappeared and the card behind it sent. The
rewind now adopts every legacy card ahead of its tombstones and deletes
their rows in its own transaction, as an epoch replacement already does,
so the fold latches each card's reading before its hand-off is gone.
…l unconfirmed

The import dropped a legacy card handed off as a send whose delivery was
unknown, though the fold keeps such a card linked and a later refusal
returns it. The import now keeps every card whose hand-off a later row can
still answer, through the same check the reducer folds dispatch rows by.

@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: fb158b77-c26b-4396-93ac-139adc564e4f
📥 Commits

Reviewing files that changed from the base of the PR and between 4db1581 and e096d5f.

📒 Files selected for processing: 18
  • config/scripts/vitest-node-runtime-files.mjs
  • src/main/codex/codex-structured-journal-translation-turn-lifecycle.test.ts
  • src/main/native-chat/agent-session-journal/command-receipt-transaction.spec.ts
  • src/main/native-chat/agent-session-journal/journal-context-rewind.ts
  • src/main/native-chat/agent-session-journal/journal-queue-fold.ts
  • src/main/native-chat/agent-session-journal/journal-queue-import.ts
  • src/main/native-chat/agent-session-journal/journal-queued-messages.ts
  • src/main/native-chat/agent-session-journal/journal-row-writer.ts
  • src/main/native-chat/agent-session-journal/journal-store.ts
  • src/main/native-chat/agent-session-journal/journal-unknown-row-kind.spec.ts
  • src/main/native-chat/agent-session-journal/queued-message-delivered-echo.spec.ts
  • src/main/native-chat/agent-session-journal/queued-message-delivered-echo.ts
  • src/main/native-chat/agent-session-journal/queued-message-pause.spec.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-queued-legacy-import.test.ts
  • src/shared/agent-session-turn-record.test.ts
  • tests/e2e/cross-version-wire/agent-session-newer-chat-refusal.unit.test.ts
  • tests/e2e/cross-version-wire/agent-session-stop-event-downgrade.unit.test.ts
  • tests/e2e/cross-version-wire/kept-card-downgrade.unit.test.ts
💤 Files with no reviewable changes: 2
  • src/main/codex/codex-structured-journal-translation-turn-lifecycle.test.ts
  • src/main/native-chat/agent-session-journal/command-receipt-transaction.spec.ts

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

Comment thread src/main/native-chat/agent-session-journal/queued-message-delivered-echo.ts Outdated
@brennanb2025

brennanb2025 commented Oct 19, 2026 •

Copy link
Copy Markdown
Contributor Author

Final review and real-agent QA complete on the current head b99ed06eec7a584f43e47234a947b5e1fba28baf. Main was merged once more before merging (another PR touched one of this PR's files). One Codex reviewer then checked every incoming change against what this PR rewrote: queued-message storage, import of old queued messages, Stop and handover, Claude's cancellation retry, Codex history catch-up and delivery receipts. It found no regressions. CI is green on this head.

A real Claude and Codex re-run on this head (M4Air, ticket 1625; instrumented QA build that switches on the still-dark mid-turn queue, source restored after) passed both flows for both agents:

  • Queue while working: two messages queued during a running turn showed as cards in order, then each was delivered once, in order, with its own reply. The agents' own session records confirm it.
  • Stop, then Resume: a queued message stayed unsent for 10 s after Stop, then Resume delivered it once with one reply. (For Claude, Stop landed before its shell command started, so Stop during a running Claude command wasn't exercised here; the earlier head covered it.)

Claude: queued cards

Claude: still unsent 10 s after Stop

Codex: queued cards

Codex: stopped with the card kept

Codex: reply after Resume

The image-edit flow below passed on the previous head ef47118; nothing it depends on changed in the merge. Real config hashes were unchanged.

Queued messages could disappear after a refused send or be sent twice after Stop/restart because the queue kept a separate saved outcome. This PR keeps each queued message in the conversation record; a message that may have reached the agent stays in the conversation and is never automatically queued again.

Final review and real-agent QA complete on ef47118d3bc3693423ab185726da758a0a350d6d. The bounded source review found no new proven merge regression, four named lifecycle test files passed all 25 tests, and the full workflow finished with 27 successful and 17 skipped checks. The final whole-mechanism comparison and readiness review are recorded in the private ledger. No production change was needed in this final round.

Current-head desktop checks passed. The earlier exact-head run passed Claude ordinary queue/drain, Claude Stop → Paused → actual Resume, Claude normal close/reopen, Codex Stop → Paused → actual Resume, and Codex normal close/reopen. The owner's final M4Air takeover then completed Codex ordinary queue/drain, queued image replacement/text editing, and three-card FIFO on this same head using the real Codex CLI.

During an actual running sleep 420 turn, the driver dropped image A, queued the original text, opened the card's Edit message action, removed A, dropped B, changed the text and saved. Two further messages were queued, so all three cards waited together. With no Steer, individual-card Send or Stop, they drained naturally in order. Codex's own saved record contains the edited text and exact replacement B bytes, no A or original text, then the replies “Blue, with a white letter B.”, “Q2-OK” and “Q3-OK”, once each. The app was stopped by its recorded PID; admission1488 is done and the owner's before/after real-config hashes match.

Grok waived by Brennan (10-21): "We're out of grok usage right now. So skip it and test w/ unit tests"

Grok has tests-only evidence from the earlier 293bb825 head (three named adapter/lifecycle files, 53 tests), not a newly claimed current-head provider run. Required changed agents Claude and Codex were exercised on the current head. Queue availability remains feature gated in shipped builds; QA used the approved test-only capability advertisement and read-only store exposure.

Retained limitations and follow-ups. The incoming model-choice change removed the old missing-positive model-catalog Fast refusal. Inherited omitted-default restoration (STA-11572) and the separate reported-disabled-Fast restriction remain qualified, without an alignment claim or waiver. The temporary Claude unwritten Stop failure returned-card exception (STA-11660), failed-import retained/live/reconnecting/paged-history correction after reopen (STA-11608), and recovery work in STA-11448/#30580 remain as documented below. Stopped-text carry is unchanged. Stop evidence proves queue pause/resume, not native sleeping-command cancellation. Stale Settings copy saying image messages send immediately predates this PR; the owner assigned its correction to the B1 recovery follow-up. No completed fix of that copy is claimed here.

Evidence boundaries. Upgrade and native-phone evidence below is from 293bb825, clearly historical; the owner excluded those unchanged paths from this bounded current-head rerun. Earlier ticket1381 ordinary-Codex method failure, ticket1439 unavailable attachment method, ticket1475 preparation parse error, and the loop's initial ticket1488 app exit remain separate incomplete attempts. None is counted as a pass. The owner traced the dev app exit to its parent-process watchdog and corrected the rig before the accepted takeover. The takeover's preliminary sleep 300 setup is also separate from the accepted sleep 420 flow.

Current-head final Codex evidence (ef47118d3bc, owner takeover1488)

Running Codex turn

Queued original text and image A

Editing the card with A removed and B attached

Three queued cards in order

Natural drain with edited B and all three real replies

Current-head driver preparation failure (ef47118d3bc, ticket1475)

The whole-window screenshot shows onboarding over the empty Codex chat. No acceptance input or CDP file-drop call occurred; this is setup evidence only.

1475-driver-preparation-failure.png

Current-head incomplete attachment setup (ef47118d3bc, ticket1439)

These screenshots show Settings and the empty configured Codex chat before any base turn or queued card. They do not demonstrate queue delivery or image replacement.

Settings Empty configured chat at the method blocker
1439-settings.png 1439-empty-chat.png

Current-head desktop evidence (ef47118d3bc, ticket1381)

Claude: ordinary queued message and real reply:

claude-ordinary-queued.png

claude-ordinary-drained.png

Claude: Paused, reply after actual Resume, and reopened chat:

claude-stop-paused.png

claude-stop-drained.png

claude-reopened.png

Codex: queued during a real command, Paused, and reply after actual Resume:

codex-stop-queued-prehit.png

codex-stop-paused-observed.png

codex-stop-drained-settled.png

Codex: reopened same chat with no duplicate or queue:

codex-reopened.png

Historical completed review on 293bb825 follows. These older results are retained as earlier-head evidence. The owner excluded the unchanged upgrade and phone paths from the current short recheck.

Queued messages could disappear after a refused send or be sent twice after Stop/restart because the queue kept a separate saved outcome. This PR keeps each queued message in the conversation record; a message that may have reached the agent stays in the conversation and is never automatically queued again.

Final review complete on 293bb82544967f097081040cb21d7d55ce17636c. CI is green on this head, including the WSL retry, and GitHub reports no merge conflicts. Main is merged and 19 targeted test files passed (347 tests). The final source/readiness review found no new proven regression and completed the mechanism comparison with the inherited and temporary differences below. Final-head desktop QA completed the Claude and Codex upgrade/reopen, two real replies each, ordinary queue/drain, and image replacement/text-edit delivery checks. Three simultaneously queued cards in order and queued Stop/Resume also passed in the subsequent exact-head desktop run. Final-head native iOS phone QA passed the actual mobile Send, queued card, normal drain, real Codex reply and workspace re-entry checks, with one delivery and reply confirmed in host and provider records. Required agents follow the PR's changed agent folders: Claude and Codex. Supplemental provider attempts do not count as passes. Grok was covered by tests only: acp-structured-session-adapter.test.ts, acp-structured-session-adapter-lifecycle.test.ts and acp-structured-host-lifecycle.test.ts passed (53 tests) on this head.

Grok waived by Brennan (10-21): "We're out of grok usage right now. So skip it and test w/ unit tests"

Completed desktop checks on this head. Genuine Claude and Codex chats created by the previous e9bfdafc1ddd build were carried through an actual restart/upgrade, reopened through Session History, and each produced two new real replies. Both agents completed an ordinary queued message after a real running turn, with one delivery and reply confirmed in retained records. Editing a queued Codex message removed the red image, added the blue image, changed the text, and saved once; normal drain delivered the edited text and exact replacement PNG bytes, followed by the real BLUE_ONLY_1168 reply. The tested builds used the approved test-only queue capability advertisement and read-only store exposure; queue availability is feature gated in the shipped build.

Additional desktop checks passed on this head. In the corrected FIFO run, all three cards visibly waited together during a real Codex turn, then each produced one real reply in order. Records join each card to exactly one host submission, accepted dispatch, native input and reply. A separate Stop test retained its queued card as Paused with zero delivery during a bounded observation; the actual Resume control then delivered it once and produced one real reply. Earlier setup/driver failures and the incomplete two-card attempt remain separate evidence, never passes. Both rigs are stopped and profiles preserved.

Observed limitation: the native sleeping command remained live after Stop while the queue stayed paused. This establishes queue pause and delivery behavior, not termination of that command. No main comparison or PR attribution was established for this observation. The lane owner is tracking that possibly inherited command-cancellation behavior separately; no additional Stop reproduction or process-cancellation change was made here.

Temporary difference: an already-open chat that reconnects or pages older history after a failed upgrade import shows the stopped message only after reopen; tracked in STA-11608. The owner has ruled that history publication and older-client approval controls will be repaired in that separate follow-up. This PR keeps its snapshot correction. The final main merge and source comparison are complete. The final-head native phone check is complete. The ordinary small-history phone re-entry does not exercise or close the failed-import retained-history follow-up.

Not changed here: a later send can carry an earlier stopped request's text, and the agent may answer that earlier request. The same saved-chat experiment on merged main produced the same six-message provider sequence as the branch. This is inherited behavior; there is no demonstrated false cancellation proof or queue regression. Pause loss across restart also reproduced on main. Claude can still refuse an omitted effort after an earlier override (STA-11572), or Fast when a listed model lacks positive capability evidence (STA-11451). These checks are inherited from main and owned by separate work; this review does not claim they are aligned or waived.

Coverage limits: saved opus[1m]/unlisted picker choices, native OS clipboard/drop and a literal returned-card Send button were not exercised in this desktop run. Rare failed-import rollback and uncertain-delivery cases have automated/source evidence, separate from the real QA. These limits are retained in the review ledger. Final-head phone coverage is the ordinary Codex flow only; Android, images, multi-card FIFO, editing, Stop/Resume, restart and upgrade were not repeated on phone.

For Brennan's review: final-head screenshots below cover the completed desktop and native iOS checks. Separately labelled pre-merge screenshots remain historical evidence. The history publication repair remains tracked in STA-11608.

Final-head desktop evidence (293bb8254496, ticket1168)

Both saved chats after upgrade, with the previous reply and two new real replies:

Claude Codex
final-upgrade-claude-two.png final-upgrade-codex-two.png

Ordinary queue/drain completed with Claude; the image flow below also shows Codex's completed ordinary queued reply:

final-normalqueue-claude-drained.png

Codex queued image/text before editing, the blue replacement in the editor, and the real reply after Save:

Before Edit Replacement ready to Save Delivered replacement and reply
final-image-original.png final-image-editing.png final-image-final.png

The running turn ended while the editor was open, so Save drained immediately; no saved card still waiting was captured. Retained journal and provider records confirm one delivery of the edited text and exact replacement PNG bytes.

Final-head desktop FIFO and Stop/Resume (293bb8254496, ticket1227)

Three cards waited together, then each produced one real reply in order:

Three waiting Drained in order
fifo-corrected-three-waiting.png fifo-corrected-drained.png

Stop kept the queued card paused; the actual Resume control delivered it once and produced one real reply:

Paused After Resume
stop-paused.png stop-drained.png

Native phone acceptance and cleanup. One actual native Send of Reply exactly PHONE_QUEUE_1169 occurred during a real Codex turn. Its queued ID links to one host submission, one accepted dispatch, one native input and one real PHONE_QUEUE_1169 reply. One native workspace re-entry retained that result with no card. The initial harness-only attempt had zero mobile Sends and remains incomplete; the approved corrected attempt is separate. Fresh host/mobile/native builds were verified on this exact head, with the same disclosed test-only queue advertisement. All owned app/server/helper processes are stopped, the owned simulator is shut down, and the machine admission is released.

Final-head native iOS phone evidence (293bb8254496, ticket1169)

The actual paired iOS app sent one message while a real Codex turn ran. One queued card drained normally to one real reply; reopening the workspace kept the message and reply without reviving the card.

Paired native app Queued card Normal drain and real reply After workspace re-entry
native-paired.png native-queued.png native-drained.png native-final-stable.png

The invalid older QA host remains visible beside the fresh connected host in the pairing screenshot. The test used the fresh host and a new chat throughout. This is ordinary iOS Codex coverage; Android, mobile images/editing/FIFO/Stop/Resume/restart/upgrade and the failed-import retained-history follow-up were not repeated on phone.

Historical pre-merge evidence (e9bfdafc1ddd; does not certify final head)

Image edit and actual delivered replacement

Editing queued card Actual send
branch-image-drop-editing.png branch-image-drop-drained.png

Stop holds the queue; Resume sends each card once

Paused After Resume
branch-stop-paused.png branch-stop-drained.png

Old chats after restart: two real replies each

Claude Codex
branch-upgrade-claude-two.png branch-upgrade-codex-two.png

Three queued messages, in order, once each

FIFO

Inherited stopped-text carry reproduced on merged main

Main comparison

Pre-merge native iOS phone: pair, queue, drain and reply

Paired native app Queued card Normal drain and real reply
Native paired Native queued Native drained

For Brennan's review: the current-head galleries show the edited attachment, natural three-message drain, both agents' actual Resume and close/reopen results. Earlier main/branch upgrade and Stop comparisons retain their original head labels. The remaining user-facing recovery differences and follow-up ownership are stated above; no new product decision is requested by this final round.

One provider echo proves one delivery, yet it withdrew every waiting card
with the same text whose hand-off was refused, so a second identical card
was lost. The fold now pairs each echo with one card: on its own row the
earliest-positioned match no other echo of that row took, and for a card
that waits again the earliest applied echo no card has taken. Paired echoes
live in the fold's state, so live and replay pair the same; readers of
cards not yet imported work on a copy.

@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: 8348c0cf-199c-41b6-993e-2121da8b5bda
📥 Commits

Reviewing files that changed from the base of the PR and between 6b66b65 and 88c44a9.

📒 Files selected for processing: 4
  • src/main/native-chat/agent-session-journal/journal-queue-fold.ts
  • src/main/native-chat/agent-session-journal/journal-queue-import.ts
  • src/main/native-chat/agent-session-journal/queued-message-delivered-echo.spec.ts
  • src/main/native-chat/agent-session-journal/queued-message-delivered-echo.ts
🚧 Files skipped from review as they are similar to previous changes: 3
  • src/main/native-chat/agent-session-journal/queued-message-delivered-echo.spec.ts
  • src/main/native-chat/agent-session-journal/queued-message-delivered-echo.ts
  • src/main/native-chat/agent-session-journal/journal-queue-fold.ts

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

Comment thread src/main/native-chat/agent-session-journal/journal-queue-import.ts Outdated
…only on proof it never got it

Pressing Stop after Orca handed a queued or steered message to the agent,
before the agent confirmed it, put the card back in the queue, and Resume
or the next send sent it again. A check matching the agent's echo of the
text was the only guard against a double send, and it kept trading a
resend for a lost card. Whether the card waited again was decided by the
rejection's category, not by proof the agent never got the message.

A refused hand-off now waits again only where Orca proves the agent never
received it: the host still held it, or whoever settled it recorded that.
A failure still returns the card and holds the cards behind it. Anything
else may have reached the agent, so the card is spent and its message
reads as sent, derived on every fold from the stored rejection: with the
turn it was answered into, as that turn's unconfirmed message where it was
handed over; withdrawn before any turn opened, as a person's own send a
Stop took back. The reopen mark ignores a recovered doubt, and the restart
reconciler no longer judges a queued hand-off unsent from what history
lacks. The echo text matching and its pairing are deleted.

A close or quit while the agent starts settles sends with the unrun hold.
The host hands nothing to a child before its start answers
(structured-agent-session-delivery-loop.ts: a handover needs phase
ready), so such a send went to an earlier child and may have run: it now
reads as sent instead of waiting again.
…run lets the card wait again

Claude says when it removed a send without running it: the interrupt
receipt's cancelled list, or a command_lifecycle cancelled, discarded or
refused frame for a command that never started. Every one settles through
rejectClaudeDispatchWaiters, which now marks the settlement unrun. For a
queued card's hand-off the host records that as proof the agent never got
it, so the card waits again under the queue's pause. A direct send keeps
the plain refusal: there the proof would owe it as stopped history.
…lows the same rule on upgrade

Earlier builds put a card back to waiting after any refusal without a
verdict and cleared its link, so a card the agent may already have would
be sent again after the upgrade. The card's own hand-offs in the journal
decide now: unchanged and last sent as one that may have reached the
agent, it is spent on that hand-off and its message reads as sent; edited
or sent again since, it is held for its own Send under a host-only marker
that publishes as a plain hold; with no hand-off in doubt, including one
that never left Orca, it waits as before. The import writes a card spent
on a hand-off in doubt, so its message shows.
… uses it

The lookup's callback lost the narrowing of the row's change, which the
mobile test typecheck rejects.
…in the Stop tests

Three tests pinned the old rule, that a card handed into a running turn
comes back to waiting when a Stop ends it. Under the new rule nothing
proves the agent never got it, so the card is spent: with the turn named,
its message reads as that turn's unconfirmed one; without, as a send the
Stop took back. Neither is ever sent again.
…ecycle-queue-derived-cards

Conflicts, resolved so main's change stands and queued messages stay in
the chat's journal, sent again only on proof the agent never got them:
- #28046 (choices fixed per accepted message): its options type and its
  one reader reach the journal's queue row and the card fold too; the
  queued_messages writers it retyped stay deleted.
- #30403 (Codex late receipt): the late settlement keeps both main's
  accept-at-its-turn path and this branch's unrun proof for a card.
- A drain logging test main deleted stays deleted.
…he agent waits again

A Stop that aborts a hand-off after its row is written but before the agent call now records
the withdrawal as never reaching the agent, for queued hand-offs only, so plan D's rule returns
the card to waiting under the Stop's pause instead of spending it. Direct sends keep the plain
withdrawal (the flag would owe them as stopped history). The agent's own refusal while aborted
is kept rather than overwritten, and an abort thrown during the call stays in doubt.
…t waits again

The abort signal reaches only an adapter's preparation (its options, the previous turn opening),
never its write, so the signal's own reason thrown back is the adapter saying nothing was
written. The handover now reads that, for queued hand-offs only, as never reaching the agent,
so the card waits again under the Stop's pause. Claude no longer wraps that abort as an option
refusal, which had returned a stopped card as a refused one blocking the queue. A real refusal
keeps its cause and its barrier; any other throw stays in doubt and is never resent.
…ard through the journal queue

Main's #30396 wrote its backlog card with the stored fingerprint and host-instance stamp the
journal queue derives instead; the card the Continue must yield to is the same waiting card.
Adapts the journal-queue design to the queued-message image edit from main:
JournalQueuedMessages.update decides through the shared queuedMessageWithEdit
rule (queuedMessageUpdateDecision) and claims only newly added uploads in the
edit's own journal transaction. The old queue-table writer main renamed
(updateQueuedMessage) stays deleted; its claim moved onto the journal update.
@brennanb2025
brennanb2025 merged commit 47484b1 into main Oct 23, 2026
41 checks passed
@brennanb2025
brennanb2025 deleted the brennanb2025/send-lifecycle-queue-derived-cards branch October 23, 2026 07:54
nwparker added a commit that referenced this pull request Oct 24, 2026
* fix(git): preserve unmerged branches with no net file changes

* fix(workspace): show launch actions in empty tab groups (#32182)

* fix(workspace): show launch actions in empty tab groups

* fix(workspace): resolve empty-pane browser policy by owner

* Scope daemon inventories for safe worktree teardown (#32171)

* Fix scoped daemon inventories without changing terminal input routes

* Serialize daemon owner inventory refreshes safely

* Serialize daemon owner inventory refreshes safely

* test(daemon): protect baseline inventory budgets and recovery

* fix(daemon): clear route proof during synthetic exits (#32196)

* docs: condense test anti-pattern guidance (#32201)

Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>

* refactor: colocate repository draft input and tests (#32155)

* test: consolidate discovery coverage and replace copied oracles (#32211)

Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>

* test: retire copied oracles and implementation inventories (#32206)

Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>

* test: remove classification inventories and verify settled boundaries (#32216)

Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>

* test: keep explicit regressions without legacy algorithm copies (#32218)

* test: keep explicit regressions without legacy algorithm copies

* test: use Number.NaN in latency fixtures

---------

Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>

* ci: avoid slow Ubuntu mirrors during WebKit setup (#32205)

* ci: reuse bounded Ubuntu mirror setup for WebKit

* ci: preserve Ubuntu setup for dispatched older refs

* perf(renderer): scope runtime connection and parking subscriptions (#32199)

* fix(chat): allow visuals to declare open without breaking scripts (#32226)

* fix(native-chat): keep queued messages in the chat's own record, so a refused one can't be skipped (#30418)

* fix(native-chat): a refused queued message still blocks the queue when its card write fails

Every reader of a queued card goes through one derived view of the journal's dispatch facts,
so a card whose message was refused reads as returned even when its card-table update and heal
both failed. One decision and one writer store the derived state for every action.

* refactor(native-chat): keep each queued message in the chat's journal, not a second table

A queued message lived in its own SQL table, and a hook that may be skipped copied
each refusal from the journal into it. When that write failed, a refused card
vanished: the cards behind it sent, and its Send and Delete did nothing.

Each queued message is now a `queue` row in the chat's journal beside the
submission that hands it off and the dispatch row that answers it. The reducer
folds the card from those rows, so a refusal returns the card and holds the
cards behind it with nothing written, and replay reads the same. Every action
writes its own rows in one transaction. Cards an earlier build stored are read
through the same classification until they move: each action moves its own
card, and a bulk import runs once per open. A new epoch restates every card
still owed an answer in the transaction that deletes the old rows.

The settlement hook and its savepoint, the open-time heal, the drain's heal and
the derived-card view are gone. The published card list is unchanged.

* test(native-chat): adapt the queue tests to cards kept in the journal

Cards are written through the journal, so tests queue them with their body
alone and hand them off under their real fingerprint. Tests of the removed
settlement hook, heal and SQL store are deleted; the text-update spec now
covers the edit decision alone, and a test of an earlier build's stored holds
seeds the legacy table and reads the imported cards. Rolling over an epoch now
keeps a card whose hand-off was never handed over, so that test checks the
retried id never sends rather than that nothing sends.

* test(native-chat): a refused card holds the queue through rewind, a new epoch and a restart

One whole-flow test through the host: the refusal writes no card row, a rewind
that retires the refused hand-off and a rolled-over epoch keep the card
returned, a restart and Resume send nothing behind it, its own Send refused
again still holds the queue, and its Delete releases the next card in order.

* test(native-chat): an earlier build's queued cards move into the journal once, each read the same

One mixed-import test through the host. The first open's import fails and
moves nothing, yet every card reads as it will; Delete and Send each move only
their own card. The next open moves the rest in one transaction, writing a
refusal only where its hand-off is gone, nothing for answered cards, and leaving
an unreadable row on disk. A later command turn spends a moved command card, a
third open writes nothing, an old Delete replays from its receipt, and a chat
with only answered legacy rows clears them in a delete-only transaction.

* test(native-chat): adapt typechecked tests to cards kept in the journal

The row-kind table lists a queue row, the clear and legacy-import tests
match the current signatures, and the downgrade test keeps only the
leftover-send case: a queued card is now a newer-version journal row an
older build keeps read-only, and users don't downgrade.

* refactor(native-chat): drop the journal row writer's unused transaction hook

No caller passes one since the queue's savepoint bookkeeping left.

* test(native-chat): downgrade probes write rows at the version older builds read

The highest version this build reads is now 4, the queue row's. A newer
build's row in these probes is written at version 3 so the older build
meets its kind, not a newer version, and the Stop-event probe hands the
older build the rows as stored, since a read raises each row's version.

* test(native-chat): unit tests follow the queue row's version 4

The legacy-import test opens real SQLite, so it runs in the Node runtime
project. The newest version this build reads is now 4, and a read raises a
row's version to it, so the turn-lifecycle test no longer pins the
in-memory version (the written one stays pinned by journalRowSchemaVersion).

* fix(native-chat): one batch echoing two refused queued messages spends both

The per-row delivery check returned only the first waiting card an echo
proved delivered, so when one settlement carried echoes for two cards the
second stayed waiting and was sent again. It now returns every card the
row proves delivered, and the fold withdraws each; replay folds the same.

* fix(native-chat): a rewind moves an earlier build's cards into the journal first

While the open's import had failed, an earlier build's card that read as
refused through its hand-off lost that reading when an in-place rewind
retired the hand-off: it disappeared and the card behind it sent. The
rewind now adopts every legacy card ahead of its tombstones and deletes
their rows in its own transaction, as an epoch replacement already does,
so the fold latches each card's reading before its hand-off is gone.

* fix(native-chat): keep an earlier build's card whose hand-off is still unconfirmed

The import dropped a legacy card handed off as a send whose delivery was
unknown, though the fold keeps such a card linked and a later refusal
returns it. The import now keeps every card whose hand-off a later row can
still answer, through the same check the reducer folds dispatch rows by.

* fix(native-chat): one delivery echo withdraws at most one queued card

One provider echo proves one delivery, yet it withdrew every waiting card
with the same text whose hand-off was refused, so a second identical card
was lost. The fold now pairs each echo with one card: on its own row the
earliest-positioned match no other echo of that row took, and for a card
that waits again the earliest applied echo no card has taken. Paired echoes
live in the fold's state, so live and replay pair the same; readers of
cards not yet imported work on a copy.

* fix(native-chat): a queued message handed to the agent is sent again only on proof it never got it

Pressing Stop after Orca handed a queued or steered message to the agent,
before the agent confirmed it, put the card back in the queue, and Resume
or the next send sent it again. A check matching the agent's echo of the
text was the only guard against a double send, and it kept trading a
resend for a lost card. Whether the card waited again was decided by the
rejection's category, not by proof the agent never got the message.

A refused hand-off now waits again only where Orca proves the agent never
received it: the host still held it, or whoever settled it recorded that.
A failure still returns the card and holds the cards behind it. Anything
else may have reached the agent, so the card is spent and its message
reads as sent, derived on every fold from the stored rejection: with the
turn it was answered into, as that turn's unconfirmed message where it was
handed over; withdrawn before any turn opened, as a person's own send a
Stop took back. The reopen mark ignores a recovered doubt, and the restart
reconciler no longer judges a queued hand-off unsent from what history
lacks. The echo text matching and its pairing are deleted.

A close or quit while the agent starts settles sends with the unrun hold.
The host hands nothing to a child before its start answers
(structured-agent-session-delivery-loop.ts: a handover needs phase
ready), so such a send went to an earlier child and may have run: it now
reads as sent instead of waiting again.

* fix(native-chat): Claude's report that it removed a queued message unrun lets the card wait again

Claude says when it removed a send without running it: the interrupt
receipt's cancelled list, or a command_lifecycle cancelled, discarded or
refused frame for a command that never started. Every one settles through
rejectClaudeDispatchWaiters, which now marks the settlement unrun. For a
queued card's hand-off the host records that as proof the agent never got
it, so the card waits again under the queue's pause. A direct send keeps
the plain refusal: there the proof would owe it as stopped history.

* fix(native-chat): an earlier build's card stopped after hand-over follows the same rule on upgrade

Earlier builds put a card back to waiting after any refusal without a
verdict and cleared its link, so a card the agent may already have would
be sent again after the upgrade. The card's own hand-offs in the journal
decide now: unchanged and last sent as one that may have reached the
agent, it is spent on that hand-off and its message reads as sent; edited
or sent again since, it is held for its own Send under a host-only marker
that publishes as a plain hold; with no hand-off in doubt, including one
that never left Orca, it waits as before. The import writes a card spent
on a hand-off in doubt, so its message shows.

* fix(native-chat): read a queued row's hand-off before the lookup that uses it

The lookup's callback lost the narrowing of the row's change, which the
mobile test typecheck rejects.

* test(native-chat): a card steered into a turn a Stop ends stays sent in the Stop tests

Three tests pinned the old rule, that a card handed into a running turn
comes back to waiting when a Stop ends it. Under the new rule nothing
proves the agent never got it, so the card is spent: with the turn named,
its message reads as that turn's unconfirmed one; without, as a send the
Stop took back. Neither is ever sent again.

* fix(native-chat): a queued message stopped before Orca handed it to the agent waits again

A Stop that aborts a hand-off after its row is written but before the agent call now records
the withdrawal as never reaching the agent, for queued hand-offs only, so plan D's rule returns
the card to waiting under the Stop's pause instead of spending it. Direct sends keep the plain
withdrawal (the flag would owe them as stopped history). The agent's own refusal while aborted
is kept rather than overwritten, and an abort thrown during the call stays in doubt.

* fix(native-chat): a queued message stopped while the agent prepares it waits again

The abort signal reaches only an adapter's preparation (its options, the previous turn opening),
never its write, so the signal's own reason thrown back is the adapter saying nothing was
written. The handover now reads that, for queued hand-offs only, as never reaching the agent,
so the card waits again under the Stop's pause. Claude no longer wraps that abort as an option
refusal, which had returned a stopped card as a refused one blocking the queue. A real refusal
keeps its cause and its barrier; any other throw stays in doubt and is never resent.

* test(native-chat): the usage-limit Continue backlog case writes its card through the journal queue

Main's #30396 wrote its backlog card with the stored fingerprint and host-instance stamp the
journal queue derives instead; the card the Continue must yield to is the same waiting card.

* test(claude): the real-CLI queued Stop expects the unrun mark Claude's cancelled-unrun receipt carries

* fix(native-chat): retain spent legacy messages when import fails

* Bound per-write chat selector work and guard ownership scans (#32109)

* Guard per-write chat selector work with shared ownership indexes

* Use checked sender session identities in the scale fixture

* Read each indexed tab row once during snapshot rebuilds

* Fix lexical binding and cache matching in selector lint rules

* Keep behavioral selector guards and resolve imported bindings

* Require selector binding definitions to declare functions

* Key performance subscribers by saved chat scope

* Show activity status with worktree-style counts (#26443)

* Show prioritized agent counts on the activity bell

* Move activity badge into the corner and shrink its count

* Use a fixed side column for activity status and counts

* Fix activity count spacing and status alignment

* Use worktree-style superscript counts for activity

* Keep activity count positions and update only number styling

* Keep idle layout fixtures out of active agent status updates

* Show child workspace statuses in the children toggle (#32224)

* Show child workspace status counts in lineage toggle

* Give child status summaries room and wrap narrow layouts

* Use solid working dots and keep child summaries on one line

* Restore children icon and clarify worktree tooltip

* Keep child totals clear of status counts in narrow layouts

* fix: preserve workspace review references when clearing active reviews (#32263)

Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>

* Saving a file, then closing that tab mid-save, no longer leaves its other tabs looking clean (STA-11786) (#32259)

* fix(editor): settle a save's shared record when the saving tab closed mid-write

With one file open in two workspaces, closing the saving tab while its write was in
flight left the other tab's disk baseline, dirty flag, draft and loaded text on the
pre-save version: every completion step looked the closed tab up by id and found
nothing, and the saved event listed only the closed tab as the record's views. An undo
back to the old text then read as clean and closed without asking.

The write now captures its record id with no await before the write, and completion
(rebaseline, dirty/draft settle, conflict clear or refusal banner, saved event's view
list) goes through a live view of that record, preferring a writable one. The save
runner and the write-lock wait do the same, so a save queued by a tab that closes
still writes through the record's remaining view. The chat visual's saved-event
listener also falls back to the record's views.

Co-Authored-By: Claude <noreply@anthropic.com>

* fix(editor): follow a closed saver's record through re-keys, kept records and listeners

Review follow-ups to the closed-saving-tab fix:

- Completion now finds the record through the view ids captured before the write,
  not the record id: a re-key mid-write (an unpinned runtime tab resolving its
  pairing) keeps views but changes the id, which brought the original bug back.
- A fallback writer (asking view closed while queued or waiting on the file lock) is
  chosen like autosave/restart's writer: writable and on a current connection, so a
  stale-connection sibling no longer throws when a current view exists.
- With no view of the record left, a kept record (no-prompt removal) is dropped when
  the write saved its text, else rebaselined; other open views of the file (a
  detached peer) take the written baseline, keeping their edits.
- A save may mark a record clean through a read-only or CSV-preview view (markFileDirty
  `written`); such views still never become dirty.
- Markdown preview refresh and the CSV pending-cell listener match the saved event's
  record views, not only the saving view's id.

Co-Authored-By: Claude <noreply@anthropic.com>

* test(editor): name the closed-view save harness as a test fixture

The focus-read ratchet scans non-test sources; the `.test-fixture.ts` suffix marks this
harness as test-only like the repo's other fixtures.

Co-Authored-By: Claude <noreply@anthropic.com>

* test(ci): list the closed-view save rig as a unit support bridge

The rig builds on the autosave test fixture, which takes the editor slice harness, so
the routing check saw it as a product consumer of that harness. Bridges are an explicit
list, like the browser drawn-from-main fixture.

Co-Authored-By: Claude <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>

* Show task names for Codex subagents (#32243)

* fix: name Codex subagents from rollout metadata

* Hide generic default role in child agent rows

* Reject non-regular Codex identity transcripts without blocking

* [comment audit] Shorten preload comments (#32257)

* chore: audit preload root comments and track repository coverage

* docs: track repository comment audit coverage

* chore: keep comment audit tracker local

* chore: shorten preload API contract comments

---------

Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>

* fix: offload macOS deletion spawns and reduce branch cleanup (#32227)

* fix: reduce worktree deletion branch cleanup overhead

Refresh only the remote base branches needed to prove a branch merged, with automatic maintenance disabled. Skip merge simulation when no common ancestor is available and preserve the branch when its remote base cannot be refreshed.

* fix: preserve fetch mappings and live branch cleanup fallbacks

* fix: spawn macOS worktree deletion commands off the main thread

* test: remove redundant cases and start comprehensive value tracker (#32269)

Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>

* fix(web): the browser client advertises the desktop's chat capabilities, so its host opens chats (#32253)

* fix(web): the browser client advertises the desktop's chat capabilities, so its host opens chats

The browser client's handshake never named agent-session.structured.v1, so a paired
host refused every agentSession call with structured_agent_session_unsupported and
withheld chat tabs. The desktop window, the Electron remote handshake and the browser
client now build their lists from one shared array of what the renderer's chat implements.

Fixes #32145

* refactor(web): keep the browser client's capability list beside the desktop's in shared

The host-side test parses both handshakes with the host's own parser, which the node
typecheck project can only import from src/shared.

* test(web): assert close-intent support in the auth frame, not the whole capability list

* fix(terminal): a timed-out CLI split names its pending pane, and a retry can never re-run its command (#32278)

* fix(terminal): a timed-out CLI split names its pending pane and can be retried without splitting again

Fixes #32169

* fix(terminal): a blank retry id or a closed pane never re-runs a split's command

* refactor(terminal): a split retry only waits for its pane and never asks the window again

* fix(terminal): a split missing from the graph is reported pending, never closed

* fix(ssh): never block or over-read on an SSH identity file (#32274)

Every place Orca reads an SSH key or identity file now goes through one
reader that refuses directories, FIFOs and devices (including through
symlinks) with a nonblocking open, and caps the read at 1 MB. An auth
failure now names any identity file that was skipped and why.

Fixes #31965. Reported by @Huge, who also proposed the first fix in #31966.

Co-authored-by: Martin Vlach <mr.huge@seznam.cz>

* test: finish fixture work before unit-test cleanup (#32285)

* fix(mobile): keep a terminal tab's chat identity while its handle is unchanged (#32251)

* fix(mobile): keep a terminal tab's chat identity while its handle is unchanged

A status frame that omits agentType or providerSession flipped chat back to the
terminal for tabs with no launchAgent (#31977), and left a launched tab with no
session to read (#32134). Mobile now remembers the last resolved agent and
session per tab and handle, so a missing field is treated as missing evidence.

* fix(mobile): keep remembered chat read-only and drop it on relaunch

A chat resolved only from this phone's memory may sit on a bare shell, so its
writes stay locked until live status confirms the agent. A live agent frame
without a session no longer inherits the previous conversation, and the
latest remembered agent outranks an older launch hint.

* fix(mobile): gate native-chat image sends on a confirmed live agent

An image send from a chat only this phone remembers could paste into a bare
shell; it now shares the confirmed-agent gate with text sends.

* fix(mobile): type remembered chat identity structurally

Importing MobileSessionTab into the eligibility module pulled React Native
globals into the e2e program through nested-agent-idle-session-owner.spec.ts,
retyping setTimeout as number there.

* fix(terminal): never present a half-built synchronized-output frame on reveal or recovery (#32281)

Settled reveal and every other atlas recovery forced the current xterm buffer
onto the screen even while a TUI held DEC 2026 synchronized output. Codex
erases rows inside each held redraw, so a reveal mid-redraw showed a blank
pane for a frame (#32198).

Viewport presents now have one mode: they keep the last coherent canvas and
let xterm present when the TUI closes the frame, bounded by xterm's 1s
synchronized-output safety timeout. Structural replay keeps its retained frame
up until an open synchronized frame closes, bounded by its existing hold cap.

Co-authored-by: YEONGUK LEE <contact@abruption.dev>

* refactor(renderer): colocate task page pagination rules (#32297)

* perf(test): bundle Zod once per isolated Vitest invocation (#32298)

* Use incremental tsrs locally and update Bun to 1.4.4 (#32276)

* Use incremental tsrs for local typechecks

* Update pinned Bun to 1.4.4

* fix(deps): bump dompurify to 3.4.17 and docs next to 16.4.1 for security advisories

dompurify 3.4.17 patches GHSA-cg2g-xh64-v4q7, GHSA-mrv2-3pqr-j5jr and
GHSA-c6gr-qvv4-5v3f in the root and mobile lockfiles. next 16.4.1 patches
GHSA-2464-fhr4-w9xw, GHSA-673q-mjm6-r5xr and GHSA-p3m5-59mp-jv5r in the
docs site.

* Prepare mobile bundle for smoke benchmarks

* Give native compiler benchmarks a larger ARM runner

---------

Co-authored-by: Brennan Benson <79079362+brennanb2025@users.noreply.github.com>

* test: trim redundant coverage and fix vacuous assertions (#32305)

* test: remove redundant cases and repair behavioral assertions

* docs: reconcile audit records with current main

* fix(deps): apply security updates from #32101

Apply the six-file DOMPurify 3.4.17 and Next.js 16.4.1 patch from PR #32101 to resolve the dependency security gate for this test audit. Original patch by Brennan Benson.

* test: satisfy changed-line quality gates

Add required braces and replace filter indexing with find. Rerun both CI quality gates and the 27 affected relay tests; refresh audit hashes and record the corrections.

---------

Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
Co-authored-by: Brennan Benson <79079362+brennanb2025@users.noreply.github.com>

* fix(ci): retain ambiguous unit-test import consumers (#32366)

* test: exercise real boundaries and trim redundant cases (#32353)

* test: replace redundant checks with observed behavior

* docs: reconcile completed audit batches and upstream review

---------

Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>

* fix(git): retain zero-net history with current merge-base guards

---------

Co-authored-by: Jinjing <6427696+AmethystLiang@users.noreply.github.com>
Co-authored-by: OrcaWin <alpha-eng@stably.ai>
Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
Co-authored-by: Brennan Benson <79079362+brennanb2025@users.noreply.github.com>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Martin Vlach <mr.huge@seznam.cz>
Co-authored-by: YEONGUK LEE <contact@abruption.dev>
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