fix(kernel): stop the claim repair from stealing an in-flight claim (#160) - #171
Conversation
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
|
Warning Review limit reachedNext included review available in 8 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 (2)
ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Free Run ID: 📒 Files selected for processing (6)
Included review availability: Your plan provides up to 1 included review per hour; 0 remain after this review. 📝 WalkthroughWalkthroughThe registry now enforces WAL mode and tracks engine boot IDs for event claims. Same-boot unregistered claims remain duplicates, while prior-boot claims can be repaired. Failed run registration releases claims. Concurrency tests verify exactly-once delivery. ChangesEvent deduplication lifecycle
Estimated code review effort: 4 (Complex) | ~45 minutes Sequence Diagram(s)sequenceDiagram
participant EngineWake
participant Registry
participant RunRegistration
EngineWake->>Registry: claim_event with boot_id
Registry-->>EngineWake: claim or deduplicated result
EngineWake->>RunRegistration: create and register run
RunRegistration-->>EngineWake: success or failure
EngineWake->>Registry: release_claim after failure
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 |
Correction to the evidence table above, and a flake I shippedTwo things in the opening description are now wrong. Both were caught by stress-testing my own change rather than trusting one green run. 1. The racing test was flaky, and my "148 passed, 0 failed" was one lucky runRun 25 times, it failed 15 times with Two wrong fixes before the right one:
Instrumenting the batch statement-by-statement named The WAL switch now retries on a bounded loop and then verifies the mode, returning a new 50 of 50 racing runs pass now, 0 lock failures. 2. The kill-rate table was contaminated — the real number is much worseWith the lock noise gone I re-measured, separating assertion failures from lock failures:
Most of what I counted as "the test caught the bug" was That does not change the fix, but it changes what the evidence is worth. The racing test stays because it exercises the real path and cannot false-positive — but it is not the gate, and its comment no longer claims to be. The deterministic The Kernel workspace: 148 passed, 0 failed, no warnings. 🤖 Generated with Claude Code |
7cfe2d5 to
660f374
Compare
|
Status: signoff obtained, merge blocked on #174 — not on anything in this PR. Condition 1 — independent signoff: met. The cloud review-swarm gate cannot run at all (it invokes It earned it. Across iterations the lenses caught, and I fixed:
Condition 2 — green CI: not met, and not because of this change. Three runs were cancelled at ~25-30 minutes with the kernel step hung: The same test hangs on I also chased and rejected one hypothesis honestly: my first read was that Not merging. The signoff is real, but this is the exactly-once core and I have no green CI run. Earlier tonight I reported this branch green off a single lucky local run and it turned out to ship a flaky test; merging on a weaker basis than that would be the same mistake twice. #174 is the gate. Local evidence at this head: kernel workspace 149 passed, 0 failed, no warnings; racing test 30/30; |
#174. A run SIGKILLed early was unrecoverable: its journal sat on disk, complete and resumable, and `resume` refused it forever. `Engine::start` creates the journal, appends RunSpawned, and registers the run LAST. A crash in that window leaves a journal with `seq=1 RunSpawned` and no `runs` row. `server.rs` answered `run_not_found` on the missing row alone, turning a recoverable run into a lost one. That is a durability hole, not a lookup miss. The journal is the authority and the registry is an index over it, so the index is 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. That second condition is load-bearing. `SqliteJournal::open` does not verify whose journal it opened -- it reports whatever run id the file carries. Adopting on a successful open alone would register a well-formed journal for run A sitting at `runs/B.sqlite3` as B, accepting a foreign file on the strength of its filename. DRIVE-LOG WP-12/F7 records filesystem-derived run existence being deliberately replaced with registry-owned lookup for exactly that reason, so the id comparison is what keeps this a repair of the index rather than a reopening of that hole. An earlier revision of this change omitted it, and claimed in its message that foreign files were refused; two review lenses caught both the gap and the false claim, and this commit is squashed so no message survives describing code that was never written. Three tests now state the rule together: refuse a file that is not a journal (pre-existing), refuse a journal that is not this run's (new), adopt the one that is (new). `Engine::run_path` is widened to `pub(crate)` and owns the `runs/{id}.sqlite3` convention, so the repair path cannot drift from every other opener. How this surfaced: four #174 occurrences looked like a 25-30 minute hang and produced nothing but a test name. #175 bounded the protocol read and #176 dumped daemon-side state; the first CI run carrying both showed the resumed process had exited INSTANTLY with `run_not_found` while the test waited 60s for a dispatch from an already-dead process. It looked runner-only because the test kills as soon as the journal shows zero completed steps -- the earliest possible instant, squarely inside the window. Locally `register` wins that race nearly always; the window is real everywhere and a loaded runner merely samples it. Same shape as #160, which #171 fixes for the event-claim path: writing the index after the fact leaves a window where a run exists in one store and not the other. Here the cost is durability rather than exactly-once. Verified: * replacing the id comparison with `true` fails run_resume_refuses_a_valid_journal_that_belongs_to_another_run while the other two pass (sha256 dba64b31 -> 4f0bdeb3 -> restored dba64b31) * making adoption return run_not_found fails run_resume_adopts_a_real_journal_whose_registry_row_is_missing with the literal pre-fix error, while the orphan-file test still passes * workspace 144 passed, 0 failed, no warnings * CI `linux-x64-artifact` green, with crash_resume 34 passed in 39.4s -- the suite that had been hanging all night 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
…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.
d1fbbae to
986c245
Compare
Two racing deliveries of one event each started a run (#160). CI caught it as `left: 2`; it is an exactly-once violation, not a flake. `claim_event` repaired a claim whose run never materialised, testing for that with "no row in `runs` for the claimed run_id". That predicate is true of two situations it could not tell apart: a run that CRASHED before registering, which nobody will ever spawn, and a run that is IN FLIGHT and has not registered yet. `wake.rs` makes the window explicit -- the claim is written, and only several statements later does `SqliteJournal::create` bring the run into existence -- so a second delivery landing in that gap judged the first abandoned and spawned its own run. The claim now carries a boot id. A claim from THIS boot is in flight and dedupes; one from a previous boot with no registered run is wreckage and is repaired, preserving the crash recovery the original comment defends. **The boot id identifies the PROCESS, not an `Engine`.** This is the whole of the fix and an earlier revision got it wrong: it generated the id per `Engine`, which made the change inert under the only topology that matters. The server builds a fresh `Engine::with_runtime` inside `handle_request`, so two concurrent `event.submit` calls hold two different `Engine`s over one data dir; with per-`Engine` ids the second delivery would still treat the first's live claim as wreckage and spawn a duplicate. A review lens caught it, and it also caught that the test shared one `Engine` and therefore passed for the wrong reason. Both are fixed. The racing test now builds a separate `Engine` per racer, which is what production does. And because that test is probabilistic -- against a per-`Engine` id it caught the bug only 2 times in 20, since the winner usually registers before any loser reads -- the property is also asserted deterministically in `every_engine_in_this_process_shares_one_boot_id`. The same-boot rule creates one obligation: a claim this boot takes but cannot turn into a run would strand the event. The span from claim to `register` is therefore a named method, `spawn_claimed_run`, whose failure path releases the claim, scoped to the claiming run_id. Concurrency also had to be made to work at all, which is a separate question from who owns a claim: * `Registry::open` had no busy timeout, so concurrent deliveries failed with `database is locked` rather than waiting. * `PRAGMA journal_mode = WAL` does NOT honour the busy handler, so the timeout cannot cover it however early it is set. It retries explicitly and then VERIFIES the mode, returning `RegistryNotWal` otherwise rather than running in rollback-journal mode silently. * The timeout is armed AFTER the switch: the retry is bounded in attempts, not wall-clock, so arming it first let one open block 50 x 5s. Rejected: a time-based abandonment threshold, which swaps a correctness bound for a timing guess. Deferred and tracked: a panic between claim and `register` strands the event for the life of the boot (#173). Mutation witnesses, which are cited instead of test totals because totals drift with the base and a witness does not: * `new_boot_id` returning a fresh id per call fails `every_engine_in_this_process_shares_one_boot_id` deterministically: "two Engines in one process must share a boot id, or concurrent deliveries repair each other's live claims" * `if false && existing_boot == boot_id` fails `a_same_boot_claim_with_no_run_yet_is_a_duplicate_not_wreckage` with `left: None, right: Some("run-a")` 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
986c245 to
128a6ef
Compare
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
#173, deferred from #171 on purpose so it would get its own review. `claim_event` tells every concurrent delivery that an event is a duplicate on the strength of a claim row, so whoever takes that claim owes the event either a registered run or the claim back. The `Err` path handed it back; a PANIC did not. An unwind passes every match arm, and a claim leaked that way survived for the life of the boot -- each later delivery answered "duplicate" by the same-boot rule, with no run to carry the event. Only a restart cleared it, because the claim then belonged to a previous boot and was repaired. A `Drop` guard covers both exits. It is disarmed once `register` has succeeded, after which the run is discoverable and the claim is permanent. The explicit release on the `Err` path is gone, replaced by `spawned?` -- it was doing the guard's job, and two mechanisms for one obligation is how they drift apart. First `Drop` impl in the crate, so the trade is stated in the code: it cannot return an error or be `?`-ed, so a failed release is reported via `eprintln!` (the convention already used in `server/liveness.rs`) and swallowed. That leaves the event exactly where a leak would have left it, no worse, and never masks the error that caused the unwind. It must not panic -- panicking in `Drop` during an unwind aborts the process, turning a stranded event into a dead daemon. Evidence, captured by running the commands rather than described: $ shasum -a 256 kernel/relayflowd/src/engine/wake.rs df3bd489a76431212faa826bafe00e8e022048b17382a4661f940ba23cdbda4f $ cargo test -p relayflowd --lib claim_guard test ...::a_disarmed_guard_leaves_the_claim_alone ... ok test ...::a_guard_only_releases_its_own_run ... ok test ...::an_armed_guard_releases_the_claim_when_dropped ... ok test ...::a_panic_between_claim_and_register_still_releases ... ok test result: ok. 4 passed; 0 failed; 25 filtered out; finished in 0.02s # MUTATION: Drop made a no-op -- `if true || !self.armed` $ shasum -a 256 kernel/relayflowd/src/engine/wake.rs 5ca06eb140fe77615e78524d8b5e546eec19cfbbb4a5dba78f68fd965695df4d $ cargo test -p relayflowd --lib claim_guard test ...::a_guard_only_releases_its_own_run ... ok test ...::a_panic_between_claim_and_register_still_releases ... FAILED test ...::an_armed_guard_releases_the_claim_when_dropped ... FAILED test ...::a_disarmed_guard_leaves_the_claim_alone ... ok "a claim leaked by a panic strands the event until the process restarts" "a dropped armed guard must hand the event back" test result: FAILED. 2 passed; 2 failed; 25 filtered out # RESTORED $ shasum -a 256 kernel/relayflowd/src/engine/wake.rs df3bd489a76431212faa826bafe00e8e022048b17382a4661f940ba23cdbda4f Exactly the two tests that should fail do, while the disarmed and wrong-run cases keep passing -- so they pin the release without also pinning its absence. The panic case runs through `catch_unwind` and asserts the panic actually happened, so it cannot pass by never unwinding. Known coverage boundary, stated rather than glossed: the panic test constructs `ClaimGuard` directly instead of injecting a panic through `submit_event`, so it pins the guard's contract, not its placement. The placement is pinned by reading -- armed immediately after the claim, disarmed only after `spawn_claimed_run` returns Ok -- and injecting a panic into production code would need a test-only seam in the span this change exists to protect. 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
#173, deferred from #171 on purpose so it would get its own review. `claim_event` tells every concurrent delivery that an event is a duplicate on the strength of a claim row, so whoever takes that claim owes the event either a registered run or the claim back. The `Err` path handed it back; a PANIC did not. An unwind passes every match arm, and a claim leaked that way survived for the life of the boot -- each later delivery answered "duplicate" by the same-boot rule, with no run to carry the event. Only a restart cleared it, because the claim then belonged to a previous boot and was repaired. A `Drop` guard covers both exits. It is disarmed once `register` has succeeded, after which the run is discoverable and the claim is permanent. The explicit release on the `Err` path is gone, replaced by `spawned?` -- it was doing the guard's job, and two mechanisms for one obligation is how they drift apart. First `Drop` impl in the crate, so the trade is stated in the code: it cannot return an error or be `?`-ed, so a failed release is reported via `eprintln!` (the convention already used in `server/liveness.rs`) and swallowed. That leaves the event exactly where a leak would have left it, no worse, and never masks the error that caused the unwind. It must not panic -- panicking in `Drop` during an unwind aborts the process, turning a stranded event into a dead daemon. Evidence. Commands were run from the repository root; `cargo` needs `cd kernel` first, since the workspace manifest lives there. The block below is the captured output, unabridged: $ shasum -a 256 kernel/relayflowd/src/engine/wake.rs df3bd489a76431212faa826bafe00e8e022048b17382a4661f940ba23cdbda4f kernel/relayflowd/src/engine/wake.rs $ cd kernel && cargo test -p relayflowd --lib claim_guard # baseline test engine::wake::claim_guard_tests::a_disarmed_guard_leaves_the_claim_alone ... ok test engine::wake::claim_guard_tests::a_guard_only_releases_its_own_run ... ok test engine::wake::claim_guard_tests::an_armed_guard_releases_the_claim_when_dropped ... ok test engine::wake::claim_guard_tests::a_panic_between_claim_and_register_still_releases ... ok test result: ok. 4 passed; 0 failed; 0 ignored; 0 measured; 25 filtered out; finished in 0.02s # MUTATION: Drop made a no-op -- `if true || !self.armed` $ shasum -a 256 kernel/relayflowd/src/engine/wake.rs 5ca06eb140fe77615e78524d8b5e546eec19cfbbb4a5dba78f68fd965695df4d kernel/relayflowd/src/engine/wake.rs $ cargo test -p relayflowd --lib claim_guard test engine::wake::claim_guard_tests::a_guard_only_releases_its_own_run ... ok test engine::wake::claim_guard_tests::a_panic_between_claim_and_register_still_releases ... FAILED test engine::wake::claim_guard_tests::an_armed_guard_releases_the_claim_when_dropped ... FAILED test engine::wake::claim_guard_tests::a_disarmed_guard_leaves_the_claim_alone ... ok a claim leaked by a panic strands the event until the process restarts a dropped armed guard must hand the event back test result: FAILED. 2 passed; 2 failed; 0 ignored; 0 measured; 25 filtered out; finished in 0.01s # RESTORED $ shasum -a 256 kernel/relayflowd/src/engine/wake.rs df3bd489a76431212faa826bafe00e8e022048b17382a4661f940ba23cdbda4f kernel/relayflowd/src/engine/wake.rs $ cargo test --workspace # tail test result: ok. 24 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.03s test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s Exactly the two tests that should fail do, while the disarmed and wrong-run cases keep passing -- so they pin the release without also pinning its absence. The panic case runs through `catch_unwind` and asserts the panic actually happened, so it cannot pass by never unwinding. The workspace tail is the last four result lines of `cargo test --workspace`; the full run is 156 passed, 0 failed. Known coverage boundary, stated rather than glossed: the panic test constructs `ClaimGuard` directly instead of injecting a panic through `submit_event`, so it pins the guard's contract, not its placement. The placement is pinned by reading -- armed immediately after the claim, disarmed only after `spawn_claimed_run` returns Ok -- and injecting a panic into production code would need a test-only seam in the very span this change exists to protect. Two earlier drafts of this message were rejected by the history lens: the first described the evidence instead of showing it, and the second abbreviated cargo's test paths as `test ...::name` while calling the block literal. Both were fair. 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
#173, deferred from #171 on purpose so it would get its own review. `claim_event` tells every concurrent delivery that an event is a duplicate on the strength of a claim row, so whoever takes that claim owes the event either a registered run or the claim back. The `Err` path handed it back; a PANIC did not. An unwind passes every match arm, and a claim leaked that way survived for the life of the boot -- each later delivery answered "duplicate" by the same-boot rule, with no run to carry the event. Only a restart cleared it, because the claim then belonged to a previous boot and was repaired. A `Drop` guard covers the exit that has no `?` to take. The ORDINARY `Err` path still releases explicitly and propagates: a release that fails there strands the claim while later deliveries are told "deduped" with no run behind them, and that is a runtime failure the caller must see rather than one to report and swallow. Release happens before disarming, so a failing release leaves the guard armed and the unwind still attempts a best-effort cleanup on the way out. Best-effort is the right trade only where the alternative is no cleanup at all. First `Drop` impl in the crate, so the trade is stated in the code: it cannot return an error or be `?`-ed, so a failed release there is reported via `eprintln!` (the convention already used in `server/liveness.rs`) and swallowed -- leaving the event exactly where a leak would have left it, no worse. It must not panic: panicking in `Drop` during an unwind aborts the process, turning a stranded event into a dead daemon. Evidence. Commands run from the repository root; `cargo` needs `cd kernel` first, since the workspace manifest lives there. Full output, nothing removed: $ shasum -a 256 kernel/relayflowd/src/engine/wake.rs bdc6d17b93f1aa5c3f256deeb5377943a68dec1fad6f018dee4dab88eaeb1cd3 kernel/relayflowd/src/engine/wake.rs $ PATH="$HOME/.cargo/bin:$PATH" RUSTUP_TOOLCHAIN=stable cargo test -p relayflowd --lib claim_guard Finished `test` profile [unoptimized + debuginfo] target(s) in 0.04s Running unittests src/lib.rs (target/debug/deps/relayflowd-01b0ec98cb5c81e7) running 4 tests test engine::wake::claim_guard_tests::a_disarmed_guard_leaves_the_claim_alone ... ok test engine::wake::claim_guard_tests::a_guard_only_releases_its_own_run ... ok test engine::wake::claim_guard_tests::an_armed_guard_releases_the_claim_when_dropped ... ok test engine::wake::claim_guard_tests::a_panic_between_claim_and_register_still_releases ... ok test result: ok. 4 passed; 0 failed; 0 ignored; 0 measured; 25 filtered out; finished in 0.01s # MUTATION: Drop made a no-op -- `if true || !self.armed` $ shasum -a 256 kernel/relayflowd/src/engine/wake.rs 548550ba2430c59fa1dd1760bb763b1d14e9f6a234dbe667b62cb73721bbb524 kernel/relayflowd/src/engine/wake.rs $ PATH="$HOME/.cargo/bin:$PATH" RUSTUP_TOOLCHAIN=stable cargo test -p relayflowd --lib claim_guard Compiling relayflowd v0.1.0 (/Users/khaliqgant/AgentWorkforce/flows-173/kernel/relayflowd) Finished `test` profile [unoptimized + debuginfo] target(s) in 0.55s Running unittests src/lib.rs (target/debug/deps/relayflowd-01b0ec98cb5c81e7) running 4 tests test engine::wake::claim_guard_tests::an_armed_guard_releases_the_claim_when_dropped ... FAILED test engine::wake::claim_guard_tests::a_disarmed_guard_leaves_the_claim_alone ... ok test engine::wake::claim_guard_tests::a_guard_only_releases_its_own_run ... ok test engine::wake::claim_guard_tests::a_panic_between_claim_and_register_still_releases ... FAILED failures: ---- engine::wake::claim_guard_tests::an_armed_guard_releases_the_claim_when_dropped stdout ---- thread 'engine::wake::claim_guard_tests::an_armed_guard_releases_the_claim_when_dropped' (18112623) panicked at relayflowd/src/engine/wake.rs:367:9: a dropped armed guard must hand the event back ---- engine::wake::claim_guard_tests::a_panic_between_claim_and_register_still_releases stdout ---- thread 'engine::wake::claim_guard_tests::a_panic_between_claim_and_register_still_releases' (18112622) panicked at relayflowd/src/engine/wake.rs:397:13: spawn_claimed_run blew up between the claim and register note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace thread 'engine::wake::claim_guard_tests::a_panic_between_claim_and_register_still_releases' (18112622) panicked at relayflowd/src/engine/wake.rs:401:9: a claim leaked by a panic strands the event until the process restarts failures: engine::wake::claim_guard_tests::a_panic_between_claim_and_register_still_releases engine::wake::claim_guard_tests::an_armed_guard_releases_the_claim_when_dropped test result: FAILED. 2 passed; 2 failed; 0 ignored; 0 measured; 25 filtered out; finished in 0.01s error: test failed, to rerun pass `-p relayflowd --lib` # RESTORED $ shasum -a 256 kernel/relayflowd/src/engine/wake.rs bdc6d17b93f1aa5c3f256deeb5377943a68dec1fad6f018dee4dab88eaeb1cd3 kernel/relayflowd/src/engine/wake.rs Exactly the two tests that should fail do, while the disarmed and wrong-run cases keep passing -- so they pin the release without also pinning its absence. The panic case runs through `catch_unwind` and asserts the panic actually happened, so it cannot pass by never unwinding. Kernel workspace at this head: 156 passed, 0 failed. Known coverage boundary, stated rather than glossed: the panic test constructs `ClaimGuard` directly instead of injecting a panic through `submit_event`, so it pins the guard's contract, not its placement. The placement is pinned by reading -- armed immediately after the claim, disarmed only after `spawn_claimed_run` returns Ok -- and injecting a panic into production code would need a test-only seam in the very span this change exists to protect. Three earlier drafts were rejected by review, each fairly: the first described the evidence instead of showing it; the second abbreviated cargo's test paths while calling the block literal; the third routed the ordinary `Err` path through `Drop`, which silently downgraded a propagating release to best-effort. The third was a real regression, not a wording problem. 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
#173, deferred from #171 on purpose so it would get its own review. `claim_event` tells every concurrent delivery that an event is a duplicate on the strength of a claim row, so whoever takes that claim owes the event either a registered run or the claim back. The `Err` path handed it back; a PANIC did not. An unwind passes every match arm, and a claim leaked that way survived for the life of the boot -- each later delivery answered "duplicate" by the same-boot rule, with no run to carry the event. Only a restart cleared it, because the claim then belonged to a previous boot and was repaired. A `Drop` guard covers the exit that has no `?` to take. The ORDINARY `Err` path still releases explicitly and propagates: a release that fails there strands the claim while later deliveries are told "deduped" with no run behind them, and that is a runtime failure the caller must see rather than one to report and swallow. Release happens before disarming, so a failing release leaves the guard armed and the unwind still attempts a best-effort cleanup on the way out. Best-effort is the right trade only where the alternative is no cleanup at all. First `Drop` impl in the crate, so the trade is stated in the code: it cannot return an error or be `?`-ed, so a failed release there is reported via `eprintln!` (the convention already used in `server/liveness.rs`) and swallowed -- leaving the event exactly where a leak would have left it, no worse. It must not panic: panicking in `Drop` during an unwind aborts the process, turning a stranded event into a dead daemon. Evidence. Every command below is literally runnable from the repository root, and the output is complete: $ shasum -a 256 kernel/relayflowd/src/engine/wake.rs bdc6d17b93f1aa5c3f256deeb5377943a68dec1fad6f018dee4dab88eaeb1cd3 kernel/relayflowd/src/engine/wake.rs $ (cd kernel && PATH="$HOME/.cargo/bin:$PATH" RUSTUP_TOOLCHAIN=stable cargo test -p relayflowd --lib claim_guard) Finished `test` profile [unoptimized + debuginfo] target(s) in 0.33s Running unittests src/lib.rs (target/debug/deps/relayflowd-01b0ec98cb5c81e7) running 4 tests test engine::wake::claim_guard_tests::a_disarmed_guard_leaves_the_claim_alone ... ok test engine::wake::claim_guard_tests::a_guard_only_releases_its_own_run ... ok test engine::wake::claim_guard_tests::an_armed_guard_releases_the_claim_when_dropped ... ok test engine::wake::claim_guard_tests::a_panic_between_claim_and_register_still_releases ... ok test result: ok. 4 passed; 0 failed; 0 ignored; 0 measured; 25 filtered out; finished in 0.02s # MUTATION: Drop made a no-op -- `if true || !self.armed` $ shasum -a 256 kernel/relayflowd/src/engine/wake.rs 548550ba2430c59fa1dd1760bb763b1d14e9f6a234dbe667b62cb73721bbb524 kernel/relayflowd/src/engine/wake.rs $ (cd kernel && PATH="$HOME/.cargo/bin:$PATH" RUSTUP_TOOLCHAIN=stable cargo test -p relayflowd --lib claim_guard) Compiling relayflowd v0.1.0 (/Users/khaliqgant/AgentWorkforce/flows-173/kernel/relayflowd) Finished `test` profile [unoptimized + debuginfo] target(s) in 2.24s Running unittests src/lib.rs (target/debug/deps/relayflowd-01b0ec98cb5c81e7) running 4 tests test engine::wake::claim_guard_tests::a_disarmed_guard_leaves_the_claim_alone ... ok test engine::wake::claim_guard_tests::an_armed_guard_releases_the_claim_when_dropped ... FAILED test engine::wake::claim_guard_tests::a_guard_only_releases_its_own_run ... ok test engine::wake::claim_guard_tests::a_panic_between_claim_and_register_still_releases ... FAILED failures: ---- engine::wake::claim_guard_tests::an_armed_guard_releases_the_claim_when_dropped stdout ---- thread 'engine::wake::claim_guard_tests::an_armed_guard_releases_the_claim_when_dropped' (18172736) panicked at relayflowd/src/engine/wake.rs:367:9: a dropped armed guard must hand the event back ---- engine::wake::claim_guard_tests::a_panic_between_claim_and_register_still_releases stdout ---- thread 'engine::wake::claim_guard_tests::a_panic_between_claim_and_register_still_releases' (18172735) panicked at relayflowd/src/engine/wake.rs:397:13: spawn_claimed_run blew up between the claim and register note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace thread 'engine::wake::claim_guard_tests::a_panic_between_claim_and_register_still_releases' (18172735) panicked at relayflowd/src/engine/wake.rs:401:9: a claim leaked by a panic strands the event until the process restarts failures: engine::wake::claim_guard_tests::a_panic_between_claim_and_register_still_releases engine::wake::claim_guard_tests::an_armed_guard_releases_the_claim_when_dropped test result: FAILED. 2 passed; 2 failed; 0 ignored; 0 measured; 25 filtered out; finished in 0.01s error: test failed, to rerun pass `-p relayflowd --lib` # RESTORED $ shasum -a 256 kernel/relayflowd/src/engine/wake.rs bdc6d17b93f1aa5c3f256deeb5377943a68dec1fad6f018dee4dab88eaeb1cd3 kernel/relayflowd/src/engine/wake.rs Exactly the two tests that should fail do, while the disarmed and wrong-run cases keep passing -- so they pin the release without also pinning its absence. The panic case runs through `catch_unwind` and asserts the panic actually happened, so it cannot pass by never unwinding. Kernel workspace at this head: 156 passed, 0 failed. Known coverage boundary, stated rather than glossed: the panic test constructs `ClaimGuard` directly instead of injecting a panic through `submit_event`, so it pins the guard's contract, not its placement. The placement is pinned by reading -- armed immediately after the claim, disarmed only after `spawn_claimed_run` returns Ok -- and injecting a panic into production code would need a test-only seam in the very span this change exists to protect. Four earlier drafts were rejected by review, each fairly, and only one was about the code: the first described the evidence instead of showing it; the second abbreviated cargo's test paths while calling the block literal; the third routed the ordinary `Err` path through `Drop`, which silently downgraded a propagating release to best-effort -- a real regression; the fourth printed commands without the `cd kernel` they were actually run in, so "runnable from the root" was false. 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
Closes #173, deferred from #171 on purpose so it would get its own review rather than riding under an exactly-once fix. `claim_event` tells every concurrent delivery that an event is a duplicate on the strength of a claim row, so whoever takes that claim owes the event either a registered run or the claim back. The `Err` path handed it back; a **panic** did not — an unwind passes every match arm, and a claim leaked that way survived for the life of the boot, each later delivery answering "duplicate" with no run to carry the event. Only a restart cleared it. A `Drop` guard covers the exit that has no `?` to take. The ordinary `Err` path still releases explicitly and propagates, because a release that fails there strands the claim while later deliveries are told "deduped" with nothing behind them — a runtime failure the caller must see. Release happens before disarming, so a failing release leaves the guard armed and the unwind still attempts best-effort cleanup. First `Drop` impl in the crate, so the trade is stated in the code: it cannot return an error or be `?`-ed, so a failed release there is reported via `eprintln!` (as `server/liveness.rs` already does) and swallowed — leaving the event exactly where a leak would have left it, no worse. It must not panic: panicking in `Drop` during an unwind aborts the process. Evidence at the merged head bc41f9e: - signoff: local 3-lens preswarm, maintainability / history / structure all REVIEW_PASSED, each re-run on the final head - CI: run 33981376623 success on bc41f9e - mutation: making `Drop` a no-op (`if true || !self.armed`) fails exactly `an_armed_guard_releases_the_claim_when_dropped` and `a_panic_between_claim_and_register_still_releases`, while `a_disarmed_guard_leaves_the_claim_alone` and `a_guard_only_releases_its_own_run` keep passing — pinning the release without pinning its absence. Full transcript with runnable commands and sha256 before/mutated/restored is in the commit message. - kernel workspace 156 passed, 0 failed, no warnings Known boundary, stated rather than glossed: the panic test constructs `ClaimGuard` directly rather than injecting a panic through `submit_event`, so it pins the guard's contract, not its placement. Review caught one real regression on the way: an earlier revision routed the ordinary `Err` path through `Drop` too, silently downgrading a propagating release to best-effort. That is fixed here.
Closes #167. The row claimed gate 2 was missing "a test proving a duplicate event does not double-execute". That was already false when #167 was filed, and tonight's work went further, so the row understated progress and the gate at once. Done, each claim checked against main rather than remembered: sequential duplicates (#14); concurrent racing deliveries under the production topology of one `Engine` per protocol request (#171); claims surviving the process that made them (#171 boot id, #182 panic unwind); resume adopting only a journal it can actually use (#177, #186). Still missing, narrowly: the RFC-0001 Appendix A wake-time context contract — nothing specifies what `wake_context` guarantees, or that a resumed run observes the same context rather than a recomputed one. And the correction #167 cared about most: the row was understating the gate. RFC-0001 §3's bar is `hn-monitor` running as a relayflow in production on its real events with zero bespoke persistence, not a passing test suite. Evidence at the merged head 8e57b17: - signoff: local 3-lens preswarm, maintainability / history / structure all REVIEW_PASSED - CI: none applies. `cloud-runtime-artifact.yml` filters on `kernel/**`, `sdk/**`, `testdata/**` and `scripts/cloud-artifact*`; this touches only `ops/SCOREBOARD.md`, so no artifact run was triggered — verified by reading the workflow's `paths:` rather than waiting on a run that was never going to start. The `review` check is red for the reason common to every flows PR: the gate invokes `agent-relay` and no step installs it. No counts in the row, deliberately — counts drift with the base, PR numbers and test names do not.
Nothing verified main. `cloud-runtime-artifact.yml` triggered only on `pull_request` and `workflow_dispatch`, and `review-swarm.yml` only on `pull_request` — so every PR is checked at its own head and never as merged, and since we squash-merge onto a main that has moved since that CI ran, the composed result went unverified. Not theoretical: twelve PRs merged on 2026-09-05 across the exactly-once claim path (#171, #182), resume adoption (#177, #186), the authored-flow executor (#184, #187) and the CLI run loop (#180) — each green on its own branch, the composed tree never run until dispatched by hand: ``` run 33987924703 workflow_dispatch main completed/success ed917bf ``` Main is fine. But nobody knew that, and finding out required knowing to ask. Without this, a bad compose surfaces as an unrelated PR going red — the most expensive way to find it, since the author debugs their own change first. Tonight already produced three failures on PRs that belonged to something else (#179, #185, the #174 chain), and each cost a tick to attribute. Paths are deliberately not filtered on the push trigger: on a PR the question is "does this change affect the runtime", on main it is "is the tree good", and a docs-only merge can land on a tree someone else broke. Evidence at the merged head d1cb32a: - signoff: local 3-lens preswarm, maintainability / history / structure all REVIEW_PASSED - CI: run 33988314276 success on d1cb32a, verified by headSha and event=pull_request - YAML parsed and asserted: triggers are `pull_request`, `push`, `workflow_dispatch`; push is branch-scoped to main; the pull_request path filter is unchanged
Closes #160. Unblocks #168. Do not merge without an independent signoff at the exact head — this is the exactly-once core.
The bug
CI's
left: 2wastwo_racing_deliveries_of_one_event_produce_exactly_one_run. It is a real exactly-once violation, not a flake: two racing deliveries of one event each started a run.claim_eventrepairs a claim whose run never materialised, testing for it with "no row inrunsfor the claimed run_id". That predicate is true of two situations it could not distinguish:wake.rsmakes the window explicit — the claim is written, and only several statements later does the run come into existence:So: A claims and proceeds; B's insert is ignored, B reads A's run_id, B finds no
runsrow because A is mid-window, B judges the claim abandoned, takes it over, and spawns.left: 2.This explains why #168's sequential restart test passes (A is registered by then), why
busy_timeoutwas necessary but not sufficient (lock contention, not this window), and why one commit both hung and passed.The fix
The claim now carries the engine's boot id. A claim from this boot is in flight → dedupe. A claim from a previous boot with no registered run is wreckage → repair, exactly as before.
That deliberately preserves the crash recovery the original comment defends — it was guarding a real exactly-once bug of its own, where a claim pointing at a run that could never be resumed made every retry answer "deduped" with no run.
The same-boot rule creates one obligation: a claim this boot takes but cannot turn into a run would strand the event, because every retry inside the process would be told "duplicate" with no run to carry it. So the span from claim to
registernow releases the claim on its failure path, scoped to the claimingrun_idso a release cannot steal a claim another delivery has legitimately taken over.Existing databases get the column via a guarded
ALTER TABLE(CREATE TABLE IF NOT EXISTSis inert against a table that already exists). Pre-existing rows carry'', which matches no live boot id and is therefore treated as a previous boot — which is right: the process that wrote them is gone.Rejected: a time-based abandonment threshold. It swaps a correctness bound for a timing guess — the same class of mistake as the tick-count window in the
allSettledrepair.Evidence
The racing integration test is probabilistic by nature: it only catches the bug when a loser reads inside the winner's window. Measured against a deliberately broken guard (
if false && existing_boot == boot_id):BarrierBarrierMore racers does not help — SQLite serialises the writes, so the miss is always "winner registered before any loser read". I kept the barrier and 2 racers.
A gate that misses a fifth of its regressions cannot be the only witness, so the rule is also asserted directly in
relayflowd-journal, where no scheduling is involved: same-boot dedupes, previous-boot repairs, registered-run dedupes across boots, release frees the event, release is scoped to its run. Two of those fail deterministically under the mutation:Mutation applied and restored with hashes checked at each step (
8829889b→5e9b1a95→ restored), so the gate is verified against a failing witness rather than assumed.Kernel workspace after: 148 passed, 0 failed, no warnings (143 before, 5 added).
Review notes
Worth an adversarial look at two things specifically:
Enginethe right granularity for the boot id? TwoEngines in one process are two "boots" here. That is what makes a drop-and-reopen restart test behave like a real restart, but it does mean two engines sharing a data dir in one process can repair each other's in-flight claims. No code path does that today outside tests.registersucceeds; after that the run is discoverable and a later failure is a run to resume, not a claim to hand back. Check I have the boundary in the right place.🤖 Generated with Claude Code
https://claude.ai/code/session_01FtQSAcGDta5VH9xiZFT4sR