fix(server): persist rejection code for tx-dedup replay#30
Merged
Conversation
A rejected tx's machine-readable error code was sent on the live socket
but never recorded, so a client retrying the same txId replayed the
reason message with no code — breaking code-based handling on exactly
the retry path dedup exists for.
_sync_seen_tx gains an error_code column; initSchema adds it in place
(pragma-gated ALTER) so already-deployed DOs upgrade on wake. The
replayed rejected frame is now shaped identically to the original:
{ code, message } when a code was recorded, { message } otherwise.
Fixes #21
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
grrowl
force-pushed
the
fix/replay-rejection-code
branch
from
July 22, 2026 04:09
339ce6c to
d0cd524
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #21
Problem
#rejectTxsends{ code, message }on the live socket butrecordTxpersisted only the message. A client retrying the sametxId(dedup replay) got the reason message with no machine-readablecode, so code-based error handling broke on exactly the retry path dedup exists for.Design decision
Issue #21's option 1: persist the code alongside the message (rather than re-running authorize/validation on retry, which would change dedup semantics).
_sync_seen_txgains a nullableerror_codecolumn. The table isCREATE TABLE IF NOT EXISTS, so already-deployed DOs wake with the old shape;initSchemanow runs a pragma-gatedALTER TABLE ... ADD COLUMN(idempotent, safe to re-run on every wake). Pre-migration rows replay code-less, matching what those clients were originally sent.#replayReceiptshapes the replayedrejectedframe identically to the original:{ code, message }when a code was recorded,{ message }otherwise — a client cannot tell a replay from the first send.Tests
tests/error-paths.test.ts: new test — a coded (VALIDATION) rejection retried with the sametxIdreplays the identical code (fails on main); the existing no-code retry test now also asserts the replay stays code-less.tests/schema-evolution.test.ts: new test — a DO with the pre-error_codetable shape upgrades in place on wake, idempotently, with old rows readable and new rows carrying the code.Full suite: 195/195 passing.
Adversarial review (codex gpt-5.5)
One Medium finding: two concurrent duplicate frames can both pass
lookupTxbefore either records (asyncauthorize/commandexecutecan yield), and the loser then sends its locally computed outcome rather than the stored one. This race is pre-existing on main and orthogonal to this fix — the diff changes what the dedup record contains, not when it is written or read. Left as a follow-up candidate rather than widening a bug-fix PR into a dedup-semantics change. No migration bugs found; allrecordTxcall sites confirmed updated.🤖 Generated with Claude Code