test(kernel): a dedupe claim must survive a restart - #168
Conversation
Gate 2 requires that a duplicate event never double-executes. The existing
coverage submits twice through ONE Engine, which proves the in-process
short-circuit and nothing about durability -- and durability is the case that
actually happens in production: a webhook source that never sees an ack retries
after the daemon has restarted.
Dropping the first Engine and opening a second over the same data directory is
that restart: same on-disk registry, no shared process state. The redelivery
must be deduped, must not spawn a second run, and the original run's journal
must still hold exactly one event.received -- the last assertion matters because
suppressing a response is not the same as keeping the journal single.
Mutation-verified rather than assumed. Disabling the claim short-circuit in
engine/wake.rs:
before 2103ddbabe52f7e4...
mutated 8d1caf759435cec2... APPLIED
a_claim_survives_a_restart_so_redelivery_still_dedupes ... FAILED
matching_event_wakes_once_with_fresh_context ... FAILED
test result: FAILED. 0 passed; 2 failed
restored 2103ddbabe52f7e4... HASH MATCHES
So the new test binds the claim, not merely the response shape.
Kernel workspace: 143 passed, 0 failed.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FtQSAcGDta5VH9xiZFT4sR
Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FtQSAcGDta5VH9xiZFT4sR Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1 Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1
|
Warning Review limit reachedNext included review available in 17 minutes. View limit detailsLimit details: You’ve used the included review currently available. Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. Review configuration: ⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Free Run ID: 📒 Files selected for processing (1)
ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Free Run ID: 📒 Files selected for processing (1)
Included review availability: Your plan provides up to 1 included review per hour; 0 remain after this review. 📝 WalkthroughWalkthroughThe change adds a restart-persistence test for event claims. The test reopens an engine over the same directory, verifies redelivery deduplication, confirms no second run starts, and checks for one ChangesEvent claim persistence
Estimated code review effort: 2 (Simple) | ~10 minutes Poem
Note 🎁 Summarized by CodeRabbit FreeYour organization is on the Free plan. CodeRabbit will generate a high-level summary and a walkthrough for each pull request. For a comprehensive line-by-line review, please upgrade your subscription to CodeRabbit Essentials by visiting https://app.coderabbit.ai/settings/billing. Comment |
Second of the three gate-2 gaps from #167. The claim is an INSERT OR IGNORE against PRIMARY KEY (flow_key, subscription_id, dedupe_key), so the exclusion belongs to the database rather than the caller. That is the right design and nothing asserted it: a later refactor to read-then-write would pass every sequential test while double-executing under concurrency, which is exactly when a webhook source fans out. One Engine, two threads released from a barrier -- the in-process topology a daemon actually has. Asserts exactly one delivery runs, the loser reports deduped rather than failing, both matched the subscription, and exactly one run object exists. Worth recording: the first version of this test used TWO Engines over one data directory and failed with called `Result::unwrap()` on an `Err` value: open run registry Caused by: SQLite journal failed: database is locked That is a genuine behaviour, but of registry opening under contention, not of the dedupe claim -- and a daemon does not run two registries over one directory. Testing it here would have asserted the wrong contract, so the note stays in the test's doc comment instead. Mutation-verified: disabling the claim short-circuit in engine/wake.rs (2103ddba -> 8d1caf75) fails all three event_wake tests; restored, hash matches. Kernel workspace: 144 passed, 0 failed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FtQSAcGDta5VH9xiZFT4sR Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1
The test I added in the previous commit FAILS on Linux, in two different ways, while passing on macOS. It is not a flaky test: it is a correct test of a property that does not hold. CI (ubuntu): left: 2, right: 1 -- BOTH racing deliveries started a run local (docker): SQLite journal failed: database is locked (SQLITE_BUSY) Both are failures of the same contract -- "a duplicate event does not double-execute" -- under concurrent delivery, with one shared Engine, which is the topology a daemon actually has. Withdrawing it from this PR rather than shipping a red gate or, worse, marking it #[ignore]: an ignored test is the silently-zero failure this repo's own RFC names. The test and its evidence move to the defect issue, and it lands with the fix that makes it pass. What remains here is the restart-durability test, which passes everywhere. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FtQSAcGDta5VH9xiZFT4sR Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1
…cellation) Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FtQSAcGDta5VH9xiZFT4sR Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1
…ning Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FtQSAcGDta5VH9xiZFT4sR Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1
This PR cannot get a green run while #160 stands. Pausing re-runs.
I am not going to keep re-running. Each attempt is a coin flip that costs 30 minutes on a loss, and re-running a known-flaky gate repeatedly is not evidence-gathering. The change itself is verifiedKernel workspace locally, at this head: and the added test is mutation-bound — disabling the claim short-circuit in Also red for an unrelated reason
So this PR carries two reds, neither of which is about its code. What unblocks it#160. The mitigation I measured ( Happy to re-run on request, or after #160 lands. |
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FtQSAcGDta5VH9xiZFT4sR Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1
|
Status: blocked on #174, same as #171. This branch's own CI run (33922391055) was cancelled at 30m14s with the kernel step hung on Two other things to settle before this lands, both from work done since it was opened:
Worth noting that this PR's original framing was right: a dedupe claim must survive a restart. What #171 found is that the same code was also treating a claim held by a live boot as if it were a crashed one, which is how two racing deliveries each started a run. This test would not have caught that, because sequential redelivery always sees a registered run. |
…ng (#177) Fixes #174. A run SIGKILLed early was unrecoverable: its journal sat on disk, complete and resumable, and `resume` refused it forever. `Engine::start` registers the run last, so a crash between the RunSpawned append and the register leaves a journal with no `runs` row. `server.rs` answered `run_not_found` on the missing row alone. The journal is the authority and the registry an index over it, so the index is now repaired from the authority — on a missing row, open the run's journal and, if it says it is this run, register it and continue. The id comparison is load-bearing: `SqliteJournal::open` reports whatever run id the file carries, so adopting on a successful open alone would register a journal for run A sitting at `runs/B.sqlite3` as B. DRIVE-LOG WP-12/F7 records filesystem-derived run existence being deliberately replaced by registry-owned lookup for that reason. Two review lenses caught an earlier revision that omitted the check and claimed otherwise in its message; the branch was squashed so no commit survives describing code that was never written. Three tests state the rule together: refuse a file that is not a journal, refuse a journal that is not this run's, adopt the one that is. Evidence at the merged head 3f84b2e: - independent signoff: local 3-lens preswarm, maintainability / history / structure all REVIEW_PASSED - CI: `linux-x64-artifact` run 33969935112 success on 3f84b2e, with crash_resume 34 passed in 39.4s — the suite that had hung and been cancelled on four previous runs - mutation: replacing the id comparison with `true` fails the foreign-journal test; making adoption return `run_not_found` fails the adoption test with the literal pre-fix error. sha256 dba64b31 -> 4f0bdeb3 -> restored dba64b31 - workspace 144 passed, 0 failed, no warnings The `review` check is red for a reason unrelated to this change and common to every flows PR: the gate invokes `agent-relay` and no step installs it (exit 127). Unblocks #171 and #168, which had no green CI run because of this.
|
Closing as superseded by #171, with the evidence for it rather than an assertion. I rebased this onto current main and ran it. The test passes — but it cannot fail, which is worse than not having it. The measurementSeeding a defect in the exact invariant this test claims to guard ( Why#171 made the boot id process-wide, deliberately: production builds one That means the two This is the same shape as the single-member What covers it now
A genuinely cross-process version of this test would have to spawn a real process, as Nothing is lost: the invariant this PR set out to protect is on main and is gated by a witness that fails when it should. For the record, the framing here was right and useful — a dedupe claim must survive the process that made it. What #171 found is that the same code also treated a claim held by a live boot as wreckage, which is how two racing deliveries each spawned a run. Sequential redelivery could never have caught that, because by then the run is registered. |
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FtQSAcGDta5VH9xiZFT4sR Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1
First increment on gate 2, closing one of the three gaps I identified in #167.
Why this test and not the one the scoreboard asks for
ops/SCOREBOARD.mdlists "a test proving a duplicate event does not double-execute" as missing. It is not —hn_monitor_integration.rs:54andevent_wake.rs:48both assert it, and I proved by mutation that they bind the claim rather than decorate it (evidence in #167).What those tests do not cover is durability. Both submit twice through one
Engine, which exercises the in-process short-circuit and says nothing about the claim surviving the process that made it. That is the case that actually occurs in production: a webhook source which never saw an ack retries, and by then the daemon may have restarted. If the claim were held in memory, that redelivery would spawn a second run for one logical event — the exact double-execution gate 2 forbids.The test
Dropping the first
Engineand opening a second over the same data directory is the restart — same on-disk registry, no shared process state.It asserts three things, and the third is the one worth arguing for: beyond "deduped" and "no second run", the original run's journal must still contain exactly one
event.received. Suppressing a response is not the same as keeping the journal single, and only the journal is the durable fact.Mutation-verified
Disabling the claim short-circuit in
engine/wake.rsfails the new test, so it binds the claim. The source file is restored byte-for-byte; the only change in this PR is the test file.Still open on gate 2
Two of the three gaps from #167 remain: concurrent racing deliveries, and the wake-time context contract itself — nothing currently specifies what
wake_contextguarantees, or that a resumed run must observe the same context rather than a recomputed one. That second one is the genuine Appendix A gap and is a spec change, not just a test.I have not touched
ops/SCOREBOARD.md. It is a gate record and I would be marking my own homework; #167 proposes the correction with evidence.🤖 Generated with Claude Code
https://claude.ai/code/session_01FtQSAcGDta5VH9xiZFT4sR