Skip to content

test(kernel): a dedupe claim must survive a restart - #168

Closed
kjgbot wants to merge 3 commits into
mainfrom
feat/gate2-wake-context
Closed

test(kernel): a dedupe claim must survive a restart#168
kjgbot wants to merge 3 commits into
mainfrom
feat/gate2-wake-context

Conversation

@kjgbot

@kjgbot kjgbot commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

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.md lists "a test proving a duplicate event does not double-execute" as missing. It is not — hn_monitor_integration.rs:54 and event_wake.rs:48 both 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 Engine and 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

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 PRE-MUTATION

Disabling the claim short-circuit in engine/wake.rs fails 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.

kernel workspace: 143 passed, 0 failed   (142 + this test)

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_context guarantees, 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

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
kjgbot pushed a commit that referenced this pull request Sep 4, 2026
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
@coderabbitai

coderabbitai Bot commented Sep 4, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

Next included review available in 17 minutes.

Check out review usage here.

View limit details

Limit 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.
You're only billed for reviews past your plan's rate limits ($0.25/file).

Learn how review limits work.

Review configuration:

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Free

Run ID: fac23730-56c1-4d84-bd96-7c67cabad9be

📥 Commits

Reviewing files that changed from the base of the PR and between a6ceee1 and 771ba2c.

📒 Files selected for processing (1)
  • kernel/relayflowd/tests/event_wake.rs
ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Free

Run ID: 795751eb-5a5b-4de1-bbf7-675893050fc7

📥 Commits

Reviewing files that changed from the base of the PR and between 98b6cdd and a6ceee1.

📒 Files selected for processing (1)
  • kernel/relayflowd/tests/event_wake.rs

Included review availability: Your plan provides up to 1 included review per hour; 0 remain after this review.


📝 Walkthrough

Walkthrough

The 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 EventReceived journal entry.

Changes

Event claim persistence

Layer / File(s) Summary
Restart redelivery deduplication test
kernel/relayflowd/tests/event_wake.rs
Adds a test that submits an event, reopens the persisted engine, verifies redelivery matching and deduplication, confirms no second run starts, and checks that the original run has one EventReceived journal entry.

Estimated code review effort: 2 (Simple) | ~10 minutes

Poem

A rabbit reads each line,
The patch grows clear beneath the moon,
Small changes hop in place,
Tests guard the garden path,
Reviews bloom before the dawn.


Note

🎁 Summarized by CodeRabbit Free

Your 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 @coderabbitai help to get the list of available commands.

kjgbot added 2 commits September 4, 2026 23:26
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
kjgbot pushed a commit that referenced this pull request Sep 4, 2026
…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
kjgbot pushed a commit that referenced this pull request Sep 4, 2026
…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
@kjgbot

kjgbot commented Sep 4, 2026

Copy link
Copy Markdown
Contributor Author

This PR cannot get a green run while #160 stands. Pausing re-runs.

linux-x64-artifact has now been cancelled twice on this PR, both times at the 30-minute job limit with Test kernel hung on agent::rung_c_sigkill_boundaries_resume_only_unfinished_steps_via_real_cli. Across the night that is the eighth cancellation from #160 (#139 ×1, #158 ×1, #159 ×1, #140 ×2, the original find ×1, this PR ×2) — roughly four hours of CI wall clock.

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 verified

Kernel workspace locally, at this head:

143 passed, 0 failed

and the added test is mutation-bound — disabling the claim short-circuit in engine/wake.rs (2103ddba8d1caf75) fails it, restored hash matches. The content is not in question; only the ability to demonstrate it through this gate is.

Also red for an unrelated reason

review is FAILURE because RELAY_WORKSPACE_KEY does not exist. That is #164's workflow, which I merged knowing it would be red on every PR until the secret is added — flagged at the time, and now visible here.

So this PR carries two reds, neither of which is about its code.

What unblocks it

#160. The mitigation I measured (--test-threads=1: 39s sequential vs 38s healthy parallel, same 34 tests) is on hold by @khaliqgant's explicit decision to wait for a real fix, and that decision stands. I am recording the consequence rather than relitigating it: while #160 is open, this PR — and every PR touching kernel/ or sdk/ — is roughly a coin flip per attempt.

Happy to re-run on request, or after #160 lands.

@kjgbot

kjgbot commented Sep 5, 2026

Copy link
Copy Markdown
Contributor Author

Status: blocked on #174, same as #171.

This branch's own CI run (33922391055) was cancelled at 30m14s with the kernel step hung on agent::rung_c_sigkill_boundaries_resume_only_unfinished_steps_via_real_cli — the same test that has now hung three times across two branches sharing no code. Filed as #174. Until that is fixed, no kernel-touching PR can obtain a green CI run.

Two other things to settle before this lands, both from work done since it was opened:

  1. It needs a rebase. Main has moved considerably (fix(gate): make the review-swarm scripts executable #172, build(sdk): route npm test through scripts/test.sh (split from #165) #170, and shortly fix(kernel): stop the claim repair from stealing an in-flight claim (#160) #171).
  2. Its coverage now overlaps fix(kernel): stop the claim repair from stealing an in-flight claim (#160) #171. This PR's contribution is
    a_claim_survives_a_restart_so_redelivery_still_dedupes — an integration-level restart test. fix(kernel): stop the claim repair from stealing an in-flight claim (#160) #171 adds the same rule deterministically at the registry level (a_previous_boots_claim_with_no_run_is_repaired, a_registered_run_dedupes_across_boots, plus a pre-migration case). The two are complementary rather than redundant — this one exercises the real engine path across an actual Engine drop — but the claim it proves is now proven twice, and this test should say which part is its own.

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.

kjgbot added a commit that referenced this pull request Sep 5, 2026
…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.
@kjgbot

kjgbot commented Sep 5, 2026

Copy link
Copy Markdown
Contributor Author

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 measurement

Seeding a defect in the exact invariant this test claims to guard (if false && registered > 0 in claim_event, which disables dedupe-by-registered-run):

test a_claim_survives_a_restart_so_redelivery_still_dedupes ... ok     ← does not notice
test registry::tests::a_registered_run_dedupes_across_boots ... FAILED ← notices

Why

#171 made the boot id process-wide, deliberately: production builds one Engine per protocol request, so concurrent deliveries must see each other's claims as in flight rather than as wreckage.

That means the two Engines in this test share a boot id. The redelivery is deduped by the same-boot rule before the registered-run path is ever consulted — so the test passes whether or not the invariant in its docstring holds. Its premise, "same on-disk state, no shared process state", stopped being true when #171 landed.

This is the same shape as the single-member Promise.allSettled([step]) rows we removed from the SDK suite: a test that proves the code path executed, not that the bound held.

What covers it now

registry::tests::a_registered_run_dedupes_across_boots (added in #171) asserts the same rule with explicit boot ids, no scheduling, and — as shown above — actually fails when the rule is broken. a_previous_boots_claim_with_no_run_is_repaired covers the crash half.

A genuinely cross-process version of this test would have to spawn a real process, as crash_resume does. That is a different and larger piece of work; if we want it, it should be its own PR rather than a test that reads as coverage while providing none.

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.

@kjgbot kjgbot closed this Sep 5, 2026
kjgbot pushed a commit that referenced this pull request Sep 5, 2026
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
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