Skip to content

Native chat: a failed save of an earlier session's unsent message no longer fails your new messages - #30405

Open
brennanb2025 wants to merge 5 commits into
mainfrom
brennanb2025/delivery-hold-write
Open

brennanb2025 wants to merge 5 commits into
mainfrom
brennanb2025/delivery-hold-write

Conversation

@brennanb2025

@brennanb2025 brennanb2025 commented Oct 18, 2026 •

Copy link
Copy Markdown
Contributor
Files Added Deleted Net
Test 13 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​84 $\color{#cf222e}{\Huge{\mathbf{−}}}$​29 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​55
Prod 21 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​136 $\color{#cf222e}{\Huge{\mathbf{−}}}$​66 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​70

ELI5

When Orca restarts, a chat can still hold a message the earlier Orca accepted but never sent. Orca keeps that message as a waiting card instead of sending it on its own. Saving that card is a database write. If that write failed, Orca gave up on the chat's whole queue and marked every waiting message as failed, including the ones you had just typed. Now Orca tells "the earlier Orca's message" apart from "your new messages" without needing that write to succeed: your new messages can proceed when their own required writes succeed, and the old one is never sent automatically. If keeping it as a card still fails during delivery, Orca makes one more best-effort write to mark only that earlier message as not sent, with the existing restart explanation. Failure of that write does not fail your new message.

What Changed

Before: after a restart, if keeping the earlier Orca's unsent message as a card failed both when the chat opened and again just before the next handover, the delivery run failed and every queued message in the chat was rejected with "Orca failed", including messages sent in this session that were perfectly safe to send.

After: your own queued messages can proceed when their own required writes succeed. If both attempts to settle the earlier message during delivery fail, one further best-effort write rejects only that earlier message with “Orca restarted before this message was sent.” This is a saved rejection, so later recovery cannot also create a card for it. If that write fails too, it is logged and your new messages still proceed. The earlier message is never sent automatically, does not count as current host work, and does not prevent the chat from closing. The existing successful recovery-card path is unchanged; the remaining failure-state tradeoff is described below.

Mechanism: the problem was structural. "This queued message belongs to this Orca, so it may be delivered, withdrawn or waited on" was read straight off the stored queued state, which is only right once the restart cleanup has succeeded. So a failed cleanup write had to fail the whole run to keep the earlier message from being sent.

  • The chat's journal now records, when it opens, the ids of the messages that were already queued (an earlier process accepted them). This is in memory only: no saved field, no wire change. It is keyed by message id, because replacing the provider history re-numbers carried queued messages; the old position-based check stopped recognising them after that, which is how an earlier message could have been sent.
  • One rule, structuredAgentSessionHandsOver (queued and not accepted before open), which the Working status already used, is now what every host reader that means "this chat's own deliverable work" asks: choosing and ordering the next message to send, the wake after a journal commit, the idle sweep, the failed-start error row and its command retry advice, Stop's target and its withdrawal, the start-failure rejection, and whether a chat can be released.
  • Readers that mean "never handed to the agent" (history replacement, rewind, settlement observation, and the restart cleanup itself) keep the raw queued check on purpose.
  • The cleanup before handover is bookkeeping again: after it fails, the existing rejection writer makes one best-effort attempt for messages accepted before this journal opened. New messages are excluded. This runs only in the delivery step, not during opening; its failure is logged and delivery continues. Pending commands already handed to the provider retain their command-specific failure advice.
  • Intended wording difference: a successful fallback uses the existing “Orca restarted before this message was sent” explanation rather than main’s generic “Orca ran into a problem, so this didn’t go through.” It does not add a start-failure status row because no agent start failed.

Why

Bookkeeping should never decide whether the person's own message is sent. The common pattern works out which queued messages are eligible from facts already held (saved rows plus what this process owns), instead of from a flag a recovery step must write first; a design that reads a written flag is only safe when a failed write stops the server from starting, which Orca deliberately never does.

Alternatives considered:

  • Keep the run-level failure path and only narrow its rejection batch. That would still end the delivery attempt when bookkeeping fails, and the position-based ownership check would still misread a message carried across a history replacement. The chosen design combines identity-based ownership with a best-effort rejection that lets delivery continue.
  • Patch only the delivery loop's picker (this PR's first commit). A review of every reader found the idle sweep, Stop, the failed-start row and chat release still treated the earlier message as this chat's work, so a single rule for all of them was needed.
  • Fail-fast database writes with a background retry coordinator (the approach of the closed Native chat: recover interrupted messages before opening and retry cleanup in the background #28122). Much larger, adds time-based give-up rules and a second write mode to every journal API.

Linked Issue

Tracked in #28122 (closed unmerged; this is its one remaining fix).

Visual Proof

Real Electron QA on a separate Mac (the M4Air), main at 36913fd versus this branch at 64a7055. Same inputs on both: a real signed-in Claude chat; a message sent while /compact runs (so it's accepted but not yet handed over); then Orca quit and relaunched. Both builds behaved the same and as intended: after the restart the message waits as a card and is never sent on its own; a new message runs first and the card follows it; Stop shows Paused with Resume and keeps the card; Resume sends it. Codex was also checked as a second agent (it answered in the branch build). The failed-save path this PR fixes has no way to trigger it from the app, so this QA covers the surrounding normal behavior only; the failure path is covered by the automated tests below.

Step main this branch
Sent during /compact, before quitting Orca main branch
After restart: kept as a waiting card main branch
90 s later: still not sent on its own main branch
New message: it runs first, then the card follows main branch
Stop during a turn: Paused with Resume, card kept main branch
Resume: the card is sent and answered main branch

Testing

  • structured-agent-session-unsent-send-hold.test.ts: injects failed settlement for earlier messages, including /compact. If a new ordinary message cannot start the agent, its error says to send the message again, while the earlier command stays untouched. A subsequent new message is still sent, Stop preserves the earlier messages, and a later successful settlement keeps the ordinary earlier message as a card. This tests ownership under injected failures; it does not reproduce a persistent database outage.

  • journal-store.spec.ts: an earlier handle's queued message is still recognised after a provider history replacement re-numbers it; a message accepted after it is not.

  • The existing recovery test now covers two cases: after the open’s two writes fail, the delivery step keeps the card; after both the open’s and delivery step’s writes fail, the extra rejection succeeds with restart wording and no card. The new message is delivered in both cases.

  • CI runs the full suite, lint and typecheck.

  • I manually tested these changes locally

  • Automated tests added/updated, or explained why not below

AI Disclosure

Review

Agent skill upstream boundary

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

Notes

  • SSH / remote: the execution host owns the journal and every reader changed here; no wire change. Remaining tradeoff: when every write across the hold and its extra rejection fails, main also leaves the earlier message pending at that moment. If storage then recovers and the person presses Stop before another send or reopen, this branch leaves that earlier message pending, while main’s Stop would withdraw it. An older paired client can therefore continue showing Working until a later settlement succeeds. Another send, close, reopen, or the next delivery-loop iteration after a successful handover provides an opportunity to settle it, not a guarantee. Acceptance of this remaining tradeoff is pending and blocks merge; the broader failure-state limitations are not claimed to be fully removed.
  • Cross-platform, mobile, performance: no platform-specific code; on the success path the only new work is one id set built when a chat's journal opens.

Author: Brennan Benson (X: @BrennanKB5)

Checklist

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

…nger rejects this process's queued sends

The delivery loop and its ordering now read the derived handsOver rule, so a
message an earlier Orca process left queued is never picked whether or not the
write that keeps it as a card succeeded. That write is bookkeeping again: its
failure is logged and retried at the next delivery step instead of failing the
run and rejecting every queued message.
@coderabbitai

coderabbitai Bot commented Oct 18, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration
  • Configuration used: Repository UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: b054a4db-a1e7-4684-b917-d2616f1ede5e
📥 Commits

Reviewing files that changed from the base of the PR and between 9f7512c and 578cf6f.

📒 Files selected for processing: 3
  • src/main/native-chat/agent-session-wire/structured-agent-session-command-turn.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-startup-failure-exit.test.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-unsent-send-hold.test.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.

📝 Walkthrough

Delivery-order reconciliation now uses the session journal and the handover eligibility rule. The host wakes the delivery loop only when current work hands over a submission. Failures while settling sends from an earlier process are logged and do not propagate. A regression test checks that a later send can be dispatched while an earlier send remains pending.

Priority: ➖ Normal

Merge Risk: ⚪ Minimal · up to 578cf

No actionable merge-blocking behavior was established in the reviewed handover and settlement paths.

Pre-merge checks: Passed 4 · Failed 1

Failed Failed checks: 1 warning

Check name Status Explanation Resolution
Docstring Coverage Warning Docstring coverage is 62.96% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 27 functions across 34 files. Write docstrings for the functions missing them to satisfy the coverage threshold.
Passed Passed checks: 4 passed
Check name Status Explanation
Linked Issues check Passed Issue #28122 is closed and supplies historical context only. No active directly linked coding target remains. This PR has no linked-issue coding requirements.
Out of Scope Changes check Passed The production changes address restart delivery ownership, failed settlement, message identity after history replacement, and related delivery readers. The test changes support these behaviors and upd…
Title check Passed The title clearly states the main user-facing change: failures while saving an earlier session's unsent message no longer fail new messages.
Description check Passed The description is detailed and covers ELI5, changes, rationale, alternatives, issue context, visual proof, testing, platform considerations, tradeoffs, and checklist items. Minor inconsistencies rema…
Full details: Out of Scope Changes check

Explanation

The production changes address restart delivery ownership, failed settlement, message identity after history replacement, and related delivery readers. The test changes support these behaviors and update affected fixtures. No unrelated change is demonstrated.

Full details: Description check

Explanation

The description is detailed and covers ELI5, changes, rationale, alternatives, issue context, visual proof, testing, platform considerations, tradeoffs, and checklist items. Minor inconsistencies remain: the manual-testing checkbox is unchecked despite reported Electron QA, the visual-proof checklist is unchecked despite attached evidence, and the build result is not explicitly reported.

  • Fix all pre-merge checks with AI
✨ 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.

A send an earlier Orca process left queued is now named by its id, captured
when the journal opens, so a provider history replacement can no longer make it
look like this process's send. Every host reader that means this handle's own
deliverable work asks the same rule: delivery picking and order, the idle
sweep, the failed-start row, Stop's target and withdrawal, the start-failure
rejection, and whether the handle can be released. Raw queued checks stay where
they mean never handed to the agent.
@brennanb2025

brennanb2025 commented Oct 19, 2026 •

Copy link
Copy Markdown
Contributor Author

An unsent message from an earlier Orca process could prevent new messages from running: if saving the old message as a card failed at open and again at delivery, the delivery loop failed and rejected this process’s queued messages too.

This change keeps new messages deliverable when that recovery write fails. It identifies messages already queued when the journal opened, so every host operation uses the same rule for which messages this process owns. It preserves the existing restart-card behavior and adds no saved field or wire change. The review fixed an old, never-delivered command being used to explain a new message’s failure. It also added one best-effort attempt to save the existing restart rejection for old messages when keeping them as cards fails; failure of that write cannot fail the new run.

Three review rounds, an independent architecture challenge, a bounded design ruling, and final readiness and performance reviews covered head 64a705592ee3594d7f59620557d80009555d6296. The final round found no new issue. The modified recovery suite passed 36 tests, and the full PR workflow passed on that exact head. Test changes remain below production changes: +86/−30 versus +134/−65.

One merge gate remains (QA has since passed):

  • Product decision: if every recovery and rejection write fails, storage then recovers, and Stop occurs before another recovery opportunity, the earlier message stays pending on this branch; main could withdraw it at that Stop. An older paired client may continue showing Working until settlement succeeds. Later send, reopen or delivery-loop activity offers repair opportunities, not a guarantee. This narrower tradeoff remains unaccepted; the review did not add broader recovery machinery.
  • Real Electron QA: done, passed. The review loop's own QA attempt was refused before launch by its automatic approval check; the lane owner then ran it separately on the M4Air (qa-gate ticket 571) with a Claude QA lead, the Codex driver and the real signed-in Claude CLI as the agent under test, main 36913fd versus this head. Both builds behaved the same and as intended: after a restart the earlier message waits as a card and is never sent on its own; a new message runs first and the card follows; Stop shows Paused with Resume and keeps the card; Resume sends it. Codex also works as an agent under test in the branch build. Screenshots are in the PR description. The failing-write path cannot be triggered from the UI and has focused test coverage only.

For Brennan’s review

Main-versus-branch screenshots are in the PR description's Visual Proof section. This comment does not certify completion or merge readiness while the product decision above is open.

  • Ownership rule: queued messages from before this journal opened are excluded from this handle’s deliverable work.
  • Best-effort settlement: one real rejection attempt after the hold fails; both failures are logged without failing new work.
  • Stop transaction: excludes messages accepted before open, which produces the remaining product tradeoff above.

This branch has not been deployed

No deployments
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