Skip to content

fix(native-chat): accept sends through their own receipt - #26672

Merged
brennanb2025 merged 9 commits into
mainfrom
brennanb2025/chat-receipts-send
Oct 10, 2026
Merged

brennanb2025 merged 9 commits into
mainfrom
brennanb2025/chat-receipts-send

Conversation

@brennanb2025

@brennanb2025 brennanb2025 commented Oct 8, 2026 •

Copy link
Copy Markdown
Contributor
Files Added Deleted Net
Test 20 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​897 $\color{#cf222e}{\Huge{\mathbf{−}}}$​125 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​772
Prod 20 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​513 $\color{#cf222e}{\Huge{\mathbf{−}}}$​73 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​440

ELI5

You press Send in a native chat and Orca says "Orca has received too many requests in the last day. Your message was not sent." Nothing was wrong with the message. Before Orca saves a message, it first writes a separate "I've seen this request" reservation into a shared retry table. That reservation step has its own rules:

  • how old a request may be;
  • how far ahead the client's clock may be;
  • cleanup of other callers' rows;
  • its own disk write.

Any of them can refuse the send before the message is ever saved. An earlier fix (#26543) removed the count limit, but the step itself still runs before every send.

After this PR, Send and /compact skip that step. The proof that a request was accepted (its "receipt", added in #26653) is saved in the same database transaction as the message. The reservation rules (age, clock skew, cleanup, the separate write) can no longer refuse a send.

What Changed

User-facing:

  • Before: a send could be refused with "too many requests", "this request has expired", or a clock-skew refusal. It could also fail because the separate reservation write failed, even though the chat could still have saved the message.
  • After: Send is refused only for reasons that concern the send itself:
    • the request id is malformed, or was already used for a different request;
    • the chat isn't attached or can't be opened or started, or its database is from a newer Orca (read-only);
    • an attachment is unavailable;
    • the queued-message limit is reached;
    • the message itself can't be saved;
    • the earlier outcome for this id can't be read ("outcome unknown"; the message is not sent again).
  • Pressing Send again with the same request (a retry after a dropped connection) still returns the original answer and never sends the message twice.

Which commands: agentSession.send in every delivery mode (sent immediately, queued while the agent is busy, or added to the running turn) and agentSession.conversationCommand (/compact). These are the commands that accept by writing a message row.

Mechanism:

  1. One route. admitAndRunAgentSessionMutation, the single route every mutating call takes, still recomputes and checks the request hash, then hands these commands to admitCommandReceiptMutation (structured-agent-session-command-admission.ts). The id is checked for format only. Its embedded timestamp is not compared with the clock.
  2. Receipt lookup. It reads the receipt for the id. Sends and /compact share one id space and match any caller.
    • Same hash: the existing replay answer, built by the same projections as today.
    • Different hash: today's conflict refusal.
    • Unreadable: today's "outcome unknown" refusal, never "not found". The read error is logged.
    • Not found: runs as a first send.
    • This route has no ledger admission, no pending reservation and no pruning.
  3. Commit. The message row (or the queued draft) and its receipt commit in one transaction. The receipt is written through the existing per-write receipt hook, before the bookkeeping savepoint, so a receipt failure rolls the message back too. JournalOperationReceipt.write now receives the row it commits with, so the receipt points at the sequence actually assigned. A small combinator lets one write carry several receipts. A queued send's receipt points at its draft.
  4. TEMPORARY ledger row. The same commit keeps the ledger success row that sends already co-committed, now insert-if-absent. Commands not yet switched (Stop, settings, prompt answers and the rest) still admit through the ledger, and this row lets them see a send's id. If the ledger already holds the same id for a different request, the send gets today's conflict refusal instead of overwriting that row. The row's expiry comes from the host clock, not the id's timestamp. No age, clock-skew, cleanup or capacity rule applies, and the ledger is never read to decide a replay. Every site of this co-write carries one greppable TEMPORARY comment so the final PR can remove it in one pass.
  5. Concurrency. Two calls with the same id are serialized per chat. The insert inside the transaction decides: if it finds an existing receipt, the second write rolls back and the call answers from the first.
  6. After commit. If a step after COMMIT fails (updating in-memory state or publishing), the call is answered exactly as a resend of the same id would be, through the same receipt replay, so its reply carries replayed: true. A message that was saved is never reported as "not sent". If the saved row could not be loaded into memory, Send answers accepted with a "may not have been sent" submission and /compact answers "outcome unknown". The error is logged. On the base branch this case threw an internal error after the message was saved.
  7. One delivery wake. The receipt runner wakes the delivery loop once, only when a message row committed. It does not wake for a queued draft, a replay or a rolled-back write, and it still wakes after a post-commit failure. The separate wakes in queued Send and /compact are removed.

A restart's automatic "continue" message goes through the same Send path. If its message is saved but can't be loaded into memory, it is still filed as unconfirmed with the chat warning. If a later step fails after the message is in the chat, it is reported as continued, because it was.

Why

The bug class is a bookkeeping transaction with its own refusal policy placed in front of the user's write. Removing one more rule (the age limit, say) would leave the class in place. The common pattern removes it: a command's receipt is inserted-if-absent inside the same transaction that commits the command's effect, and a duplicate reads the existing receipt. It has no separate admission and no count or age budget.

Alternatives considered:

  • Drop only the age and clock checks from the existing ledger admission. Rejected: the separate reservation transaction, the pruning of unrelated rows and the reservation's own storage failure would remain. A pruned row could still let a retry run twice.
  • Switch every chat command in one final PR. Rejected (decided in review of feat(native-chat): add a chat-owned command receipt store #26653): the Send bug would survive every PR until the last one. Meanwhile a receipt would be written but never read, and could roll back a write the ledger had admitted. Switching per family, Send first, gets the fix out now. The TEMPORARY ledger row keeps the cross-command conflict check in the meantime.
  • Keep /compact scoped per caller. Rejected: it forced a second "acknowledge another caller's message" path that threw internal errors on an already-accepted /compact. /compact messages are already keyed by the same operation id as Send, so one global scope is the simpler, correct model.

Differences from the common pattern:

  • INTENDED: a refusal before acceptance records nothing. Examples: an unavailable attachment, a full queue, a chat that isn't attached. A retry with the same id is evaluated again. The common pattern stores rejected receipts. Here no accepted effect exists to protect, and a cached refusal could block a later valid retry.
  • INTENDED: request hash and per-caller scopes are kept for the commands that have them (see feat(native-chat): add a chat-owned command receipt store #26653). Send and /compact use the global scope.
  • INTENDED: replies use today's result shapes. A replay answers with the existing projections, not with a receipt or event dump. That includes the "spent" answer when a rewind removed the message. Old clients read the same shapes.
  • INTENDED: a failure after commit is answered by receipt replay. The first reply and every resend give one answer per id. The common pattern returns the committed result directly. Here that would need a second, volatile copy of the answer captured during the transaction, and the review found that copy could disagree with a resend.
  • TEMPORARY: the ledger success row in the same commit. It is removed when the last native-chat command family is switched, in the final PR of this stack. Until then, ledger expiry still affects only the commands not yet switched.

Old ledger-only rows (development data only): a send or /compact recorded only in the old ledger, with no receipt and no message in the current history, is not treated as replay proof. A retry of that id runs. Native chat has no real users yet, so there is no backfill. This would not be safe for existing users' data and is not claimed to be.

Linked Issue

Builds on #26653 (receipt store, merged). Follow-up to #26543 (removed the count caps).

Visual Proof

Host-side change; checked in a hidden, isolated dev build of this branch (OMP agent against a local fake model), driven over the app's own RPC and composer. Screenshots from the PR build unless marked "before".

A normal send appears once and gets its reply, with no notice.
While the agent works on a slow turn, a queued send shows as one "Queued messages" card.
The slow turn finishes, then the queued message is delivered once. It was also retried with the same id while queued: still one card, one delivery.
The same request sent twice with one id shows once with one reply. The same id with a different body was refused and shows nothing.
Before: ids older than 24 h and 6 min in the future are refused (expired / invalid). Only the fresh-id control message appears.
After: the same old-id and future-id sends are accepted, and each appears once with its reply.

Testing

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

After the main sync (dca127e): node, cli and web typechecks pass, and 46 focused files / 585 tests pass, including /clear, rewind, queued /compact and operation-settlement tests. Before it, focused suites at dfe3c3f: 32 files, 384 tests passed. orca-ci-checks (lint, code quality, localization, typecheck) passed 23/23. They cover:

  • an id older than 24 hours and one 6 minutes in the future are accepted;
  • a ledger that would refuse has no effect on Send;
  • the same id sent twice in a row and concurrently gives one message and one provider delivery;
  • conflicts on a changed body or delivery mode, on an id a Stop already used, and on a Stop reusing a send's id;
  • a queued send's replay follows its draft, the hand-off, and the withdrawn case;
  • /compact replay across callers gives one message and one provider command;
  • a failed receipt write rolls back both the message and the ledger row;
  • an unreadable receipt answers "outcome unknown", and the read error is logged;
  • a failure after commit answers exactly as a resend does, and delivery is woken once (never on a replay);
  • a refusal before acceptance writes nothing;
  • after a rewind, a retry answers "spent" and is never sent again;
  • a restart continuation whose saved message can't be loaded is filed unconfirmed with the warning; one that fails after its message is in the chat is reported continued;
  • a newer-schema (read-only) database is refused as before.

Six existing tests in files this PR hadn't changed still expected the old ledger answers (CI caught them). They now expect the receipt answers. The restart tests were reworked as described above rather than flipped.

The QA screenshots above cover a normal send, queued delivery, a same-id retry while queued, a same-id/different-body conflict, and old/future ids before and after.

Not verified in the app: /compact, because the OMP agent used for QA can't compact, so this PR's /compact path wasn't reached there; it is covered by the automated tests. Also not reached in the app: a failure after commit.

The change is host-side. SSH and WSL hosts run the same code on their own database. RPC parameters, result shapes and published stream bytes are unchanged. The only value change is replayed: true on a first reply after a post-commit failure (an internal error before), and no client reads that field.

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. Stack: #26653 (receipt store, merged) → this PR (Send, /compact). Later PRs each switch one family: queued Send Now/Delete/Resume and prompt answers, Stop, settings and goals, create/rewind//clear. The final one removes the TEMPORARY ledger row and native chat's use of ledger admission, and decides the schema-version question.

Synced with main after #26653 merged. Main had made /clear and rewind commit their own success with their write (settlesWithWrite). So the receipt route now keys on its own plan flag (acceptsWithCommandReceipt, set only on Send and /compact), and /clear and rewind keep the ledger settlement they have on main. A /compact that waits in line (#25704) is answered through receipt replay from its queued card, like a queued Send. Main's own delivery wakes in queued Send and /compact were dropped in the merge, so the receipt runner stays their only owner.

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

brennanb2025 commented Oct 9, 2026 •

Copy link
Copy Markdown
Contributor Author

Review summary

The problem. A native-chat Send could be refused ("too many requests", "this request has expired", a clock-skew refusal, or a failed reservation write) before the message was ever saved. The cause: a separate retry-reservation transaction, with its own age, clock, cleanup and storage rules, ran in front of every Send and /compact.

What changes for the user. Send and /compact are refused only for reasons about the send itself: a malformed or reused id, a chat that can't be opened, an unavailable attachment, a full queue, a message that can't be saved, or an unreadable earlier outcome. A retry with the same id still gets the original answer and never sends twice. Ids that are old or slightly in the future are accepted.

How. The command's receipt (from #26653) is inserted-if-absent in the same database transaction as the message or queued draft. No separate admission, pruning or budget runs for these commands. Other command families still use the old ledger. Until they are switched, a TEMPORARY ledger row written in the same commit keeps cross-command id conflicts visible.

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

  • A failure after the commit was answered from a captured copy that could disagree with a resend. On current main it could also make a queued /compact wait about 2 minutes and then say "has not started yet". Now the first reply and every resend come from the same receipt replay.
  • /compact was scoped per caller, which needed a second acknowledge path that threw internal errors. It now shares Send's global id space.
  • Delivery had two wake owners, chosen by the reply's shape. Now one wake, only when a message row committed.
  • Receipt read errors and post-commit errors were swallowed. Both are now logged and still fail closed.
  • The TEMPORARY ledger row's expiry came from the client's id timestamp. It now uses the host clock, and every TEMPORARY site is tagged for removal.
  • CI caught 6 existing tests that still expected the ledger's answers. They were updated. The restart-continuation tests were reworked to model the case Send really can't vouch for (message saved but not loaded into memory), and that case is still filed "unconfirmed" with its warning. Three queued tests that now open the journal database were moved into the Node test project that CI requires for SQLite.
  • Unreachable /compact replay branch removed; stale comments fixed; PR body rewritten.

Deferred (P3, tracked for later PRs in this stack):

  • Receipt result pointers are not read on replay yet.
  • Reusing an id across command families is now refused inside the commit, after the chat may have been opened. This needs a buggy client.
  • Send no longer prunes the TEMPORARY ledger rows, so they accumulate until another family prunes them or Orca restarts.
  • A few dead or legacy /compact answer paths (priorRecord) and log-noise nits.

Main sync (dca127e). #26653 merged, so this PR now targets main. What the merge changed:

  • Main had made /clear and rewind commit their own success with their write. Under this PR's old routing (any plan that commits its success with its write takes the receipt route), they would have been moved onto the receipt route silently and lost their own success records. The receipt route now keys on its own plan flag, set only on Send and /compact. /clear and rewind keep the ledger settlement they have on main.
  • A /compact sent while the agent works now waits in line (fix(native-chat): a /compact sent while the agent works waits in line instead of being refused #25704). Its retry is answered through receipt replay from its queued card, like a queued Send.
  • /compact's old answer from a recorded ledger outcome stays removed: on the receipt route that outcome never carries one.
  • Main's own delivery wakes in queued Send and /compact were dropped, so the receipt runner stays the only owner.

Verified

  • At dfe3c3f: focused suites 32 files / 384 tests passed, and orca-ci-checks (lint, code quality, localization, typecheck) passed 23/23.
  • The earlier full native-chat run (312 files) failed only on the 6 tests fixed above.
  • CI at dca127e (final head, synced with main): all checks passed, including the 5 test shards, static analysis and typecheck, cross-version wire compatibility, and both packages.
  • After the main sync: node, cli and web typechecks pass, and 46 focused files / 585 tests pass, including the /clear, rewind, queued /compact and operation-settlement tests.
  • App QA in a hidden, isolated build of this branch against its base, using the OMP agent and a local fake model. Checked: a normal send; a queued send delivered once after a slow turn; a same-id retry while queued; a same-id, different-body conflict; old and future ids refused before and accepted after. Screenshots are in the PR body.

Not verified

  • /compact in the app: the QA agent can't compact. It is covered by tests only.
  • A failure after commit in the app.
  • Queued /compact and /clear in the app after the main sync. They are covered by tests only.
  • SSH, WSL and Windows hosts. They run the same host code; it was not exercised there.

Seen in QA, not caused by this PR (same on the base build): typing a second message while the agent works steers the running turn instead of queueing it, and the first turn's reply never appears.

@brennanb2025
brennanb2025 changed the base branch from brennanb2025/chat-writes-own-retry-proof to main October 9, 2026 01:48
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.
@brennanb2025
brennanb2025 marked this pull request as ready for review October 9, 2026 06:08
@coderabbitai

coderabbitai Bot commented Oct 9, 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: 659a3877-e815-4472-9f59-d2c013adaff1

📥 Commits

Reviewing files that changed from the base of the PR and between dca127e and 3bec600.


📒 Files selected for processing (3)
  • config/scripts/vitest-sqlite-runtime-files.mjs
  • src/main/native-chat/agent-session-journal/command-receipt-transaction.test.ts
  • src/main/runtime/agent-session-record-store.ts

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



📝 Walkthrough

Walkthrough

Send and /compact mutations now use command receipts to record acceptance and answer retries. Receipt transactions can use the associated journal row to record a submission or queued-draft result, and runtime transactions support conditional operation-row insertion. The changes also update delivery wake handling, operation-ID conflict and expiry behavior, and queued-send replay. Tests cover receipt failures, replay, and continuation recovery.

Priority: ➖ Normal

Estimated code review effort:


Merge Risk: 🔵 Low · up to 3bec6

The change is mergeable with owner awareness of a possible duplicate compaction when an older /compact request is retried after upgrade.

Pre-merge checks | Passed 4 | Failed 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage Warning Docstring coverage is 33.33% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 36 functions across 40 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 is concise and accurately identifies the main change: native-chat sends are accepted through their own command receipt.
Description check Passed The description is detailed and covers the required explanation, user-facing behavior, rationale, testing, visual proof, compatibility considerations, and checklist. The linked issue section reference…
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 💡 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.

@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: 64ae6660-fd6d-42b4-af28-b4157021dc42
📥 Commits

Reviewing files that changed from the base of the PR and between 37833ce and dca127e.

📒 Files selected for processing (41)
  • 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/command-receipt-table.test.ts
  • src/main/native-chat/agent-session-journal/command-receipt-transaction.test.ts
  • src/main/native-chat/agent-session-journal/command-receipt-transaction.ts
  • src/main/native-chat/agent-session-journal/journal-row-writer.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-attachment-admission.test.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-context.test.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-mutation-context.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-mutation-plans.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-mutation-refusals.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-queued-command.test.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-queued-gate.test.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-lift.test.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-queued-repeat-press.test.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-queued-send.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-refusal-retry.test.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-resend-answer.test.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-resend-ledger-refusal.test.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-restart-failure-filing.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-restart-ownership.test.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-restart-resume-progress.test.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-send-preparation.test.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-send.test.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-turns.ts
  • src/main/native-chat/agent-session-wire/structured-conversation-compaction.test.ts
  • src/main/native-chat/agent-session-wire/structured-conversation-compaction.ts
  • src/main/runtime/agent-session-operation-admission.test.ts
  • src/main/runtime/agent-session-operation-admission.ts
  • src/main/runtime/agent-session-operation-receipt.ts
  • src/main/runtime/agent-session-record-rows.ts
  • src/main/runtime/agent-session-record-store.ts
  • src/main/runtime/agent-session-store-transactions.ts
  • tests/e2e/cross-version-wire/versioned-agent-session-wire.ts
💤 Files with no reviewable changes (1)
  • src/main/native-chat/agent-session-wire/structured-agent-session-queued-mutations.ts

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

if (outcome.status === 'succeeded' && outcome.conversationCommand) {
return { recorded: outcome.conversationCommand }
}
replay: (ctx) => {

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.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🔎 Supported by static analysis

🏁 Script executed:

#!/bin/bash
rg -n -C30 "B15" src/main/native-chat/agent-session-wire/structured-conversation-compaction.test.ts
rg -n -C5 "priorRecord" src/main/native-chat/agent-session-wire

Repository: stablyai/orca

Length of output: 14394


🏁 Script executed:

set -eu
printf '%s\n' '--- current compaction implementation ---'
nl -ba src/main/native-chat/agent-session-wire/structured-conversation-compaction.ts | sed -n '45,90p'
nl -ba src/main/native-chat/agent-session-wire/structured-agent-session-mutation-plans.ts | sed -n '180,235p'
printf '%s\n' '--- relevant tests around B15 and committed records ---'
rg -n -C12 --glob 'structured-conversation-compaction.test.ts' 'phase: .committed.|conversationCommand|B15|older build' src/main/native-chat/agent-session-wire
printf '%s\n' '--- base versus head for the relevant flow ---'
git diff 37833ce9a02ccd6754dc9b4eb3266d39ddf8a2fd dca127ecdf5b7904b47413978ad2ee56cfb7b6b0 -- src/main/native-chat/agent-session-wire/structured-conversation-compaction.ts src/main/native-chat/agent-session-wire/structured-agent-session-mutation-plans.ts

Repository: stablyai/orca

Length of output: 42178


🏁 Script executed:

set -eu
printf '%s\n' '--- current compaction implementation ---'
nl -ba src/main/native-chat/agent-session-wire/structured-conversation-compaction.ts | sed -n '45,90p'
printf '%s\n' '--- current conversation command plan ---'
nl -ba src/main/native-chat/agent-session-wire/structured-agent-session-mutation-plans.ts | sed -n '180,235p'
printf '%s\n' '--- tests for prepared/committed conversation records ---'
rg -n -C12 --glob 'structured-conversation-compaction.test.ts' 'phase: .(prepared|committed).|conversationCommand|B15|older build' src/main/native-chat/agent-session-wire
printf '%s\n' '--- base versus head for the relevant files ---'
git diff 37833ce9a02ccd6754dc9b4eb3266d39ddf8a2fd dca127ecdf5b7904b47413978ad2ee56cfb7b6b0 -- src/main/native-chat/agent-session-wire/structured-conversation-compaction.ts src/main/native-chat/agent-session-wire/structured-agent-session-mutation-plans.ts

Repository: stablyai/orca

Length of output: 41787


Guard only committed legacy compactions in run.

run calls performSend without checking the session record. A committed record from an older build can therefore rerun when its receipt or submission is gone.

The proposed priorRecord() guard must not be used directly. It converts matching prepared records to compactionUnconfirmed, so that guard would break B15, which intentionally reruns ledger-only prepared operations. Add a committed-only callback for run and keep priorRecord for replay.

Suggested fix
 export function conversationCommandPlan(params: {
   envelope: AgentSessionMutationEnvelope
   delivery?: 'queue-if-active'
   priorRecord: () => AgentSessionConversationCommandResult | null
+  committedPriorRecord: () => AgentSessionConversationCommandResult | null
 }): MutationPlan<ConversationCommandAcceptance> {
@@
     recoverUnknownFromDurableState: true,
     run: async (ctx) => {
+      const prior = params.committedPriorRecord()
+      if (prior) {
+        return { ok: true, value: { recorded: prior } }
+      }
       const sent = await performSend(ctx, {
   const priorRecord = (): AgentSessionConversationCommandResult | null => {
     const prior = context.deps.store.getRecord(sessionId)?.conversationCommand
@@
         }
   }
+  const committedPriorRecord = (): AgentSessionConversationCommandResult | null => {
+    const prior = context.deps.store.getRecord(sessionId)?.conversationCommand
+    return prior?.operationId === clientOperationId &&
+        prior.callerKey === caller.callerKey &&
+        prior.phase === 'committed'
+      ? prior
+      : null
+  }
@@
     params,
     priorRecord,
+    committedPriorRecord,
     arrival

…eipts-send

# Conflicts:
#	src/main/runtime/agent-session-record-store.ts
@brennanb2025
brennanb2025 merged commit 280ef3c into main Oct 10, 2026
30 of 32 checks passed
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