fix(overseer): watchdog reclaims hung tick guard to prevent missed ticks (Problem #3) - #4981
Open
rysweet wants to merge 1 commit into
Open
fix(overseer): watchdog reclaims hung tick guard to prevent missed ticks (Problem #3)#4981rysweet wants to merge 1 commit into
rysweet wants to merge 1 commit into
Conversation
Observed Problem #3: the acting-overseer tick loop was missing scheduled ticks. Cadence is every 15 min, but status showed the last tick at 2026-07-28T19:23:39Z marked (stale), with large gaps (17:57:55Z -> 19:23:39Z ~86 min). Root cause: the acting-overseer tick uses a bare AtomicBool overlap guard (overseer_tick_running) cleared by a spawned thread's ClearOnDrop. If a tick HANGS on a long gh/network call, ClearOnDrop never runs, so the guard stays true forever and every subsequent scheduled tick is dropped -> staleness and multi-tick gaps. There was no watchdog to reclaim the stuck guard. Fix (additive / non-breaking): - Add a pure, clock-injected TickWatchdog + guard_generation_matches helper next to OverseerCadence in src/overseer/wiring.rs. The watchdog bounds max in-flight time (three cadence intervals, floored at 10 min) and is fully unit-tested (budget floor, reclaim on overrun, monotonic-safe inflight, generation-guard race, end-to-end hung-tick model). - Wire it into the daemon: record arm time + generation when a tick arms; before the cadence check, if a tick has been in flight past the bound, reclaim the guard, bump the generation, and emit a tracing::warn! plus a daemon_log staleness/liveness signal so a catch-up tick can fire. - Generation-token guard on ClearOnDrop: a late-finishing hung tick only clears the guard if its captured generation still matches current, avoiding the stale-clear race against a catch-up tick. Structured tracing + OTel only (no print!/println!). Preserves PRD and OODA cadence semantics. Adds 6 unit tests. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
📊 Coverage Summary
Coverage data from CI run. Test files matching |
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.
Observed Problem #3 — Overseer missing scheduled ticks
Cadence is every 15 min, but
simard statusshowed the last acting-overseertick at
2026-07-28T19:23:39Z(next expected19:38:39Z) with the overseermarked (stale), and the tick history showed large gaps:
17:57:55Z -> 19:23:39Z(~86 min) and19:23:39Z -> ~20:46Zwith nointervening tick.
Root cause
The acting-overseer tick uses a bare
AtomicBooloverlap guard(
overseer_tick_running) that is cleared by a spawned thread'sClearOnDrop.If a tick hangs on a long
gh/network call,ClearOnDropnever runs, sothe guard stays
trueforever and every subsequent scheduled tick isdropped → staleness + multi-tick gaps. There was no watchdog to reclaim the
stuck guard.
Fix (additive / non-breaking)
TickWatchdog+guard_generation_matcheshelper next to
OverseerCadenceinsrc/overseer/wiring.rs. The watchdogbounds max in-flight time (three cadence intervals, floored at 10 min) and
is fully unit-tested.
before the cadence check, if a tick has been in flight past the bound,
reclaim the guard, bump the generation, and emit
tracing::warn!+ adaemon_logstaleness/liveness signal so a catch-up tick can fire.ClearOnDrop: a late-finishing hung tick onlyclears the guard if its captured generation still matches current, avoiding
the stale-clear race against a catch-up tick.
Tests
6 new unit tests in
overseer::wiring::tests:watchdog_reclaims_a_tick_that_exceeds_its_budgetwatchdog_does_not_reclaim_a_tick_within_its_budgetwatchdog_budget_is_floored_to_avoid_same_second_respawnwatchdog_inflight_is_monotonic_safegeneration_guard_prevents_the_stale_clear_racea_hung_tick_does_not_block_subsequent_ticks(end-to-end virtual-clock model)All 50
overseer::wiring::testspass. Structured tracing + OTel only (noprint!/println!). Preserves PRD and OODA cadence semantics.Step 13: Local Testing Results
Detected toolchains:
Cargo.toml,Cargo.lock): all seven changed files are Rust daemon/Overseer sources, and the affected runtime entry point issimard ooda run.package.json,package-lock.json,bin.js): provides the npm launcher and Playwright dashboard scripts, but neither the launcher nor dashboard changed, so this toolchain is not part of the focused validation surface.Chosen validation strategy:
NODE_OPTIONS=--max-old-space-size=32768was set on both commands as required.Scenario 1 — Watchdog expires the observed hung-tick duration
Command:
NODE_OPTIONS=--max-old-space-size=32768 cargo test --locked --lib 'overseer::wiring::tests::watchdog_expires_a_tick_that_exceeds_its_budget' -- --exact --nocaptureResult: PASS
Output:
test overseer::wiring::tests::watchdog_expires_a_tick_that_exceeds_its_budget ... ok;test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 9946 filtered outScenario 2 — Hung worker is fenced, reaped, and followed by a clean replacement
Command:
NODE_OPTIONS=--max-old-space-size=32768 cargo test --locked --lib 'overseer::wiring::tests::expired_worker_exits_before_replacement_and_cannot_publish' -- --exact --nocaptureResult: PASS
Output:
test overseer::wiring::tests::expired_worker_exits_before_replacement_and_cannot_publish ... ok;test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 9946 filtered out