Skip to content

native chat: queued-message actions and question answers accept through their own receipt - #26988

Merged
brennanb2025 merged 15 commits into
mainfrom
brennanb2025/chat-receipts-queue-actions
Oct 12, 2026
Merged

brennanb2025 merged 15 commits into
mainfrom
brennanb2025/chat-receipts-queue-actions

Conversation

@brennanb2025

@brennanb2025 brennanb2025 commented Oct 9, 2026 •

Copy link
Copy Markdown
Contributor
Files Added Deleted Net
Test 4 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​981 $\color{#cf222e}{\Huge{\mathbf{−}}}$​14 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​967
Prod 20 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​447 $\color{#cf222e}{\Huge{\mathbf{−}}}$​166 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​281

ELI5

When you press Send Now, Delete or Resume on a queued message in a native chat, or answer an agent's question card, Orca first makes a separate "I've seen this request" reservation in a shared retry table. Only then does it act. That reservation has its own rules and its own disk write, and either can refuse the action before it happens. Its retry answers also come from a separate copy that can drift from what actually happened.

After this PR, each of these actions saves its proof in the same database write as the action itself. A retry of the same request gets the original answer and never acts on newer work. This is the same change #26672 made for Send and /compact.

What Changed

User-facing:

  • Before: Send Now, Delete, Resume and question answers could be refused by retry-table rules: "this request has expired", a clock-skew refusal, or the reservation's own disk write failing. A retried question answer could come back with a newer revision than the one that was accepted.
  • After: these actions are refused only for reasons about the action itself, such as an unknown card, a stale question revision, a chat that can't be opened, or the request id reused for something else. A retry with the same request returns the original answer.
    • A retried question answer returns exactly the answer that was accepted, even after a rewind or a later edit of the question.
    • A retried Send Now on a card that was already sent answers with the message that was sent for it, never a later one.
    • If an action that changed nothing (for example Resume when nothing is paused) can't save its proof, it answers "outcome unknown" instead of claiming success.

Mechanism:

  1. One route. These plans declare a command receipt, and admitAndRunAgentSessionMutation sends them to the receipt path fix(native-chat): accept sends through their own receipt #26672 added for Send. There, the request hash is checked, the receipt is looked up, preparation and the writer-lease check run (for prompt answers), and then the plan runs.

  2. Same transaction. The receipt is inserted if absent inside the transaction of the action's own write:

    • Send Now: the submission row, written together with the card hand-off.
    • Delete: the card withdrawal in the queued-message table.
    • Resume: the Resume row.
    • Question answer: the resolved item row, committed before the provider is told.

    A failed receipt write rolls the action back.

  3. No-op actions. An action that changes nothing commits its receipt on its own, before it answers. If that commit fails, it answers "outcome unknown".

  4. Replay. Replay answers from what the receipt recorded, in today's wire result shapes:

    • Send Now: the receipt points at the exact submission it handed off.
    • Question answer: the receipt keeps the accepted resolution.
  5. TEMPORARY ledger row. As in fix(native-chat): accept sends through their own receipt #26672, the old retry table still gets an insert-if-absent success row in the same commit, so commands not yet switched (Stop, settings, goals, rewind, /clear, create) still see these ids and refuse a reused one.

  6. A chat a newer Orca wrote. A prompt answer there is refused with the same "update Orca" words the queued actions give. Before, it threw an unclassified error, because the old retry table had been the step that refused it.

Why

The bug class is a bookkeeping transaction with its own refusal rules, placed in front of the user's action, plus a replay answer derived from a copy that can drift. The common pattern removes both: a command's receipt is inserted if absent inside the transaction that commits the command's effect, a duplicate reads that receipt, and a no-op command still records its receipt.

Differences from the common pattern:

  • INTENDED: Resume stays target-less, as the common pattern's queue resume is. If a Resume's no-op receipt can't be saved, it answers "outcome unknown". A retry after newer work arrived is then evaluated again, just as the common pattern evaluates a command whose commit failed again.
  • INTENDED: refusals before acceptance store nothing. The common pattern stores rejections. Here no accepted effect exists to protect, and a cached refusal could block a later valid retry. Question answers stay bounded by the revision the user saw.
  • INTENDED: a no-op is stored as an accepted outcome, and an answer that matches an earlier one is stored as an alias. Both are accepted-only receipts.
  • INTENDED: the request-hash conflict refusal is kept.
  • TEMPORARY: the ledger success row and caller-scoped keys for families not yet switched. Both are removed by the final cutover PR.
  • TEMPORARY: editing a queued message (Save, added on main in fix(native-chat): edit a queued message in place, even while a question is open #26886) still uses the old retry table. A resend is already safe, because Save only applies when the card's text still matches what the editor last saw. It moves to receipts in the final cutover PR with the other remaining commands.
  • TEMPORARY: write-only queue bookkeeping (settledByOp stamping and the queued receipts readers) now has no production reader. It goes in the final cutover.

Linked Issue

Follows #26672 (Send and /compact through receipts) and #26653 (the receipt store), both merged. #26777 is merged too, so the "who answered" field stores a device id, never a paired device's credential.

Visual Proof

Checked in a hidden, isolated build of this branch and of its base, with the OMP agent against a local fake model. Retries were driven over the app's own RPC from inside the app window.

While the agent works on a slow turn, a queued card shows with Steer / Delete / More actions.
Pressing Steer (Send Now) sends the card once and it gets its reply. Retrying the UI's own request id answered replayed with no second send.
One of two queued cards deleted with a fixed request id; only "q2 keep me" remains. The retry removed nothing else.
Stop while a card waits shows "Queue paused because you interrupted" with Resume.
Resume sends the paused card once. Retrying the UI's Resume id answered replayed and sent nothing more.
A permission card answered "Allow once": the tool ran once.
Retrying the same answer id returned the identical resolution (replayed), with no second tool run.
Before: Send Now with a request id about 26 h old was refused (expired), and the card stayed queued.
After: the same about-26-h-old Send Now was accepted and sent the waiting card once.

Not shown in the app: question cards (the rig can only produce permission cards), the "outcome unknown" storage-failure path, and replay after a rewind. Those are covered by tests.

Testing

  • I manually tested these changes locally
  • Automated tests added/updated, or explained why not below

At a7c2e9c:

  • 79 focused files / 1,064 tests pass, including both new receipt suites.
  • The node, web and cli typechecks pass.
  • orca-ci-checks passes.
  • CI passes all 15 checks.

At fcea398 (the newer-Orca prompt-answer refusal):

  • 57 files / 551 tests pass, covering prompt answers, the newer-Orca refusals and the journal. The new test fails without the fix (it throws journal_read_only).
  • The node, web and cli typechecks pass.
  • orca-ci-checks passes 22/22.
  • CI: all checks pass.

At 8a82b07 (synced with main after #26672 merged):

  • 168 files / 1,723 tests pass, covering the receipt, queued-message (including main's new in-place edit), prompt, Stop/Resume, restart, and journal suites.
  • The node, web and cli typechecks pass.
  • orca-ci-checks passes 22/22.
  • CI: all 15 checks pass, including cross-version wire compatibility.

The tests cover, for each action:

  • the same id sent twice, one after the other and at the same time;
  • the same id with a changed body or target (a conflict);
  • a no-op whose receipt can't be saved answers "outcome unknown", never success;
  • a Send Now retry after the card was re-queued, re-sent or pruned answers the original hand-off;
  • a question-answer retry after a rewind or a later revision returns the accepted answer;
  • an unreadable receipt;
  • a refusal before acceptance stores nothing;
  • a newer Orca's read-only database is refused with the update words, for prompt answers as well as queued actions.

Platform: macOS. The change is host-side, so SSH and WSL hosts run the same code on their own database.

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

Author: @BrennanKB5. Stored data: a question answer's receipt keeps the accepted answer (the chosen option, any typed text, and who answered) for the life of the chat, so a retry after a rewind returns it. It is deleted with the chat. The change is host-side, so SSH and WSL hosts run the same code on their own database. RPC parameters and result shapes are unchanged.

Stack: #26653 (merged) → #26672 (merged) → this PR → Stop → settings, goals, rewind and /clear → create/ensure and the final removal of the old retry table for native chat.

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)

Adds the agent_session_command_receipts table and an insert-if-absent
receipt write that runs inside a command's own journal transaction. A
receipt records the command's identity, canonical hash, status and a
pointer to the row holding its result; it dies with its chat record.
Nothing uses it yet: the existing operation ledger stays authoritative.
Rows are keyed by caller and operation id; a send looks across callers and
other methods look up only their caller's row, matching the existing ledger.
Bad rows and newer table shapes read as unreadable instead of throwing.
…tion id

Global lookups now take rows in insertion order, so a read and an insert
agree on which receipt answers an id regardless of the host clock. The
primary key leads with the operation id, so its own index serves both
lookups.
Send (direct, queued, steer) and /compact no longer commit a separate
ledger reservation before the message is saved. Their command receipt is
inserted-if-absent in the same transaction as the submission or queued
draft; a repeated id replays from it, a changed request conflicts.
Operation ids are checked for syntax only, so age, clock skew, pruning
and the reservation's own storage can no longer refuse a send.

The ledger success row they already co-committed stays as a temporary
insert-if-absent write so families not yet switched keep seeing these
ids; it is never read on this path.
…d space with Send, wake delivery once

- After its commit, a send whose publication fails answers through the same receipt replay a retry takes, so one id gets one answer; the in-memory copy and journal reload are gone.
- /compact ids are global like Send's: a retry under a new connection replays instead of needing a second acknowledgment write.
- The receipt route calls its runner directly; the runner wakes delivery when a new message row commits.
- The compatibility ledger row expires from host time; each temporary site is labelled.
- Receipt read errors and post-commit failures are logged.
…d ledger

Restart-continuation tests now model the case Send cannot vouch for (saved but
not loaded into memory) and keep it unconfirmed; a later throw after the message
is in the chat answers continued, as a resend would. Drop /compact's unreachable
legacy replay branch and fix stale comments.
Send and /compact now route on acceptsWithCommandReceipt: main made /clear and
rewind settlesWithWrite plans with their own success receipts, and they stay on
ledger settlement. /compact keeps #25704's queued answer under receipt replay.
…rough their own receipt

Send Now, Delete, Resume and prompt answers move onto the command-receipt route
Send uses: the receipt is inserted with the command's own write. A no-op answers
only once its receipt commits (otherwise outcome unknown), and a prompt answer's
replay reads the exact revision it was accepted at.
Send Now's acknowledgement points at the submission it handed off; a question
answer's receipt keeps the accepted resolution; a no-op that cannot save its
receipt answers outcome unknown and holds nothing in memory; classified refusals
pass through.
@brennanb2025

brennanb2025 commented Oct 9, 2026 •

Copy link
Copy Markdown
Contributor Author

Review summary

The problem. If a queued-message action (Send Now, Delete, Resume) or a prompt answer (permission or question card) is retried with the same id, for example after a dropped connection, it could be refused as "expired" or "too many requests", or it could act on newer work. The cause: these actions proved their retries through the old retry-reservation ledger, a separate transaction with its own age, clock and cleanup rules. Some of them recorded what they did only in memory.

What changes for the user. A retried Send Now, Delete, Resume or prompt answer gets the original answer and never acts twice. That holds after a restart, after the card was re-queued or re-sent, and after a rewind for prompt answers. Old request ids are no longer refused as expired.

How. Each action's receipt is inserted-if-absent in the same database transaction as the action's own write (the common pattern). An action that changes nothing commits its receipt before it answers. If that commit fails, the answer is "outcome unknown", never success. A refusal before acceptance stores nothing. Until the final cutover PR in this stack, a TEMPORARY row in the old ledger is written in the same commit.

Fixed during review (2 rounds plus a readiness pass):

  • A retried Send Now replayed the card's current state. If the card had been returned to the queue and sent again, a retry answered with the newer hand-off. If the sent card had been pruned, the retry was refused as spent. Now Send Now's receipt points at the exact submission it acknowledged.
  • Actions that changed nothing remembered that only in memory, and a restart forgot it. The memory cache is gone: the receipt is committed before the answer, and a failed commit answers "outcome unknown". Resume stays target-less, as in the common pattern.
  • A prompt-answer retry after a rewind was refused as spent, because its receipt pointed into history that the rewind deleted. The receipt now stores the accepted answer itself.
  • Errors other than the conflict error turned into "unknown" instead of passing through. For example, a newer Orca's read-only database is now refused cleanly.
  • Two stale comments were fixed.
  • From the readiness pass: a prompt answer on a chat a newer Orca wrote threw an unclassified error. It now gets the same "update Orca" refusal the queued actions give (fcea398), with a test that fails without the fix.

Deferred (P3):

  • Resume replay answers resumed: false, which is its documented wire contract.
  • Write-only queue bookkeeping (settled-by stamps, withdrawn retention) is TEMPORARY until the final cutover.
  • A Delete accepted by the old ledger and retried after upgrade answers deleted: false. Native chat has no users yet.
  • A pre-existing race between the delivery drain and Delete can label a delivered card "withdrawn". That label is now saved in the receipt.
  • A refused prompt answer is judged again on retry (INTENDED). Its expected revision bounds it.
  • From the readiness pass:

Main sync (8a82b07). #26672 merged, so this PR now targets main. Changes the merge needed:

Stored data. A question answer's receipt keeps the answer (option, typed text, who answered) for the life of the chat. It is deleted with the chat. This is noted in the PR body.

Verified

  • At 8a82b07 (synced): 168 files / 1,723 tests pass. The node, web and cli typechecks pass. orca-ci-checks passes 22/22. CI: all 15 checks pass, including cross-version wire compatibility and verify.
  • At fcea398 (newer-Orca fix): 57 files / 551 tests and the three typechecks pass. orca-ci-checks passes 22/22. All CI checks pass.
  • At a7c2e9c: 79 focused files / 1,064 tests pass. The node, web and cli typechecks pass. orca-ci-checks passes 22/22.
  • CI at a7c2e9c: all 15 checks pass. The package job failed once on a Docker Hub image fetch; it passed on re-run.
  • Readiness review of the full diff: no P0, P1 or P2 issues.
  • App QA in a hidden, isolated build of this branch and its base, with the OMP agent and a local fake model. Checked: Send Now once plus a same-id retry; Delete one of two cards plus a retry; Stop then Resume plus a retry; a permission answer plus a retry with no second tool run; a roughly 26 h old Send Now id refused on the base and accepted here. Screenshots are in the PR body.

Not verified

  • Question cards in the app (the rig only produces permission cards).
  • The "outcome unknown" storage-failure path in the app.
  • Replay after a rewind in the app.
  • SSH, WSL and Windows hosts. They run the same host code.

Follow-up seen in QA (not caused by this PR): an answered card says "Answered on desktop-renderer", which is an internal client id, not a name a user knows.

…th the update words

A prompt answer now runs through the same newer-Orca refusal the queued-message actions use, so a read-only journal answers agent_session_journal_unreadable instead of throwing journal_read_only.
@brennanb2025
brennanb2025 changed the base branch from brennanb2025/chat-receipts-send to main October 10, 2026 00:27
…eipts-queue-actions

# Conflicts:
#	config/scripts/vitest-sqlite-runtime-files.mjs
#	src/main/native-chat/agent-session-journal/command-receipt-schema.ts
#	src/main/native-chat/agent-session-journal/journal-row-writer.ts
#	src/main/native-chat/agent-session-wire/structured-agent-session-command-admission.ts
#	src/main/native-chat/agent-session-wire/structured-agent-session-command-receipt.ts
#	src/main/native-chat/agent-session-wire/structured-agent-session-mutation-admission.ts
#	src/main/native-chat/agent-session-wire/structured-agent-session-mutation-plans.ts
#	src/main/native-chat/agent-session-wire/structured-agent-session-operation-settlement.test.ts
#	src/main/native-chat/agent-session-wire/structured-agent-session-operation-settlement.ts
#	src/main/native-chat/agent-session-wire/structured-agent-session-turns.ts
#	src/main/runtime/agent-session-record-store.ts
…eipts-queue-actions

# Conflicts:
#	src/main/native-chat/agent-session-journal/journal-queued-messages.ts
#	src/main/native-chat/agent-session-wire/structured-agent-session-queued-mutations.ts
@coderabbitai

coderabbitai Bot commented Oct 12, 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: 351e7354-4da4-49dc-bf6e-340e5e18ddd1

📥 Commits

Reviewing files that changed from the base of the PR and between 8a82b07 and 86234e8.


📒 Files selected for processing (2)
  • config/scripts/vitest-sqlite-runtime-files.mjs
  • src/main/native-chat/agent-session-journal/journal-item-appender.ts

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



📝 Walkthrough

Walkthrough

Journal writes can now commit operation receipts with changed rows or through a serialized store transaction. Mutation plans define receipt results for prompt answers and queued actions, and replay uses those saved results. The changes add tests for receipt replay, conflicts, failures, and rewinds across prompt answers, Send now, Delete, and Resume.


Priority: ➖ Normal

Merge Risk: ⚪ Minimal · up to 86234

No actionable merge-blocking issue remains in the reviewed receipt paths; accepted actions persist their receipts before success, and receipt failures do not silently report acceptance.

Pre-merge checks | Passed 4 | Failed 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage Warning Docstring coverage is 47.17% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 53 functions across 24 files. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check Passed The title clearly identifies the main change: queued-message actions and question answers now use their own receipts.
Description check Passed The description is detailed and covers the change, user impact, rationale, testing, visual proof, compatibility considerations, and temporary limitations. The Review section is empty, and the linked i…
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.

  • Fix all pre-merge checks with AI
✨ Finishing Touches 💡 2
📝 Generate docstrings 💡
  • Commit to this branch
  • Create a new PR

🛠️ Fix failing CI checks 💡
  • Commit to this branch
  • Create a new PR
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

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

❤️ Share

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

@brennanb2025
brennanb2025 merged commit 4526d8b into main Oct 12, 2026
53 of 55 checks passed
brennanb2025 added a commit that referenced this pull request Oct 12, 2026
Main moved again during the sync (#26988: queued-message actions and
prompt answers accept through their own command receipt).

Conflicts:
- journal-queued-messages.ts: one options object carries main's receipt,
  the edit lease's `adopted`, and this branch's background write flag.
- turns-prompt.ts: the answer row's turn scope stays the host's current
  work projection; it now also commits main's operation receipt.
- agent-session-store-transactions.ts: main's commitReceipt (a person's
  write, normal wait) beside this branch's background-capable commit.

Merge-caused fixes: main's prompt-receipt test rebuilds the prompt with
the generation that raised it; record-store stays within its line cap.
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