Repository navigation
native chat: queued-message actions and question answers accept through their own receipt - #26988
Conversation
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.
Review summaryThe 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):
Deferred (P3):
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
Not verified
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.
…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
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info
📝 Walkthrough
Priority: ➖ Normal Merge Risk: ⚪ Minimal · up to 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 |
|
…eipts-queue-actions
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.
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:
Mechanism:
One route. These plans declare a command receipt, and
admitAndRunAgentSessionMutationsends 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.Same transaction. The receipt is inserted if absent inside the transaction of the action's own write:
A failed receipt write rolls the action back.
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".
Replay. Replay answers from what the receipt recorded, in today's wire result shapes:
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.
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:
settledByOpstamping 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.
replayedwith no second send.replayedand sent nothing more.replayed), with no second tool run.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
At a7c2e9c:
orca-ci-checkspasses.At fcea398 (the newer-Orca prompt-answer refusal):
journal_read_only).orca-ci-checkspasses 22/22.At 8a82b07 (synced with main after #26672 merged):
orca-ci-checkspasses 22/22.The tests cover, for each action:
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
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
N/Awith reasonpnpm lint,pnpm typecheck,pnpm test, andpnpm buildpass (or CI will cover; local preferred)