Skip to content

fix(run): always run on schedule; rate-limit only the empty heartbeat - #6

Merged
krayong merged 1 commit into
mainfrom
fix/scheduler-catch-up-gate
Jun 25, 2026
Merged

fix(run): always run on schedule; rate-limit only the empty heartbeat#6
krayong merged 1 commit into
mainfrom
fix/scheduler-catch-up-gate

Conversation

@krayong

@krayong krayong commented Jun 25, 2026

Copy link
Copy Markdown
Owner

Problem

On a fixed-clock-time schedule (e.g. launchd StartCalendarInterval at 09:00), the digest fired only about every other day.

run_once gated the whole run on _catch_up_due — a strict now - last_successful_run >= INTERVAL_DELTA. Each run completes a few seconds after the scheduled instant, so the next same-time fire is always a hair under 24h and the gate skipped it entirely (no detection, no digest, run not marked). The skip left an empty 0-byte log and last_successful_run untouched, which is exactly how this was diagnosed on a live install.

Fix

  • Remove the catch-up gate. Detection always runs when the native scheduler fires. Real changes were never the issue — the delivery ledger already makes them idempotent, so they always go out.
  • Rate-limit only the empty heartbeat via _empty_digest_due: the "nothing notable" digest is suppressed when the previous successful run was under half an interval ago, so two fires close together (a manual run plus the scheduled one, or a wake double-fire) don't double-send it. --force overrides; cron always sends.
  • mark_successful_run now fires on every run, so the window advances even when the heartbeat is suppressed.

Tests

TDD — the two new behavior tests were watched failing against the gated code first. Replaced the two obsolete catch-up tests with:

  • test_run_within_interval_still_delivers_changes (the every-other-day fix)
  • test_empty_digest_suppressed_when_last_run_recent
  • test_empty_digest_sent_after_interval_with_prior_run
  • test_force_sends_empty_despite_recent_run

Full suite: 472 passed; ruff clean. CLAUDE.md invariant updated.

🦇 Generated by Penny-One at Batcave

The catch-up gate skipped a whole run when `now - last_successful_run`
was under one INTERVAL_DELTA. On a fixed-clock-time schedule each run
completes a few seconds after the scheduled instant, so the next
same-time fire lands just under the delta and was dropped — the digest
fired roughly every other day.

Detection now always runs when the native scheduler fires. Real changes
are never gated; the delivery ledger keeps them idempotent. Only the
empty "nothing notable" heartbeat is rate-limited (`_empty_digest_due`):
suppressed when the previous successful run was under half an interval
ago, so two fires close together do not double-send it. `--force`
overrides; cron always sends. `mark_successful_run` now fires on every
run so the window advances even when the heartbeat is suppressed.

Co-Authored-By: Penny-One (Batcave) <noreply@anthropic.com>
@krayong
krayong merged commit 5d2b5f4 into main Jun 25, 2026
2 checks passed
@krayong
krayong deleted the fix/scheduler-catch-up-gate branch June 25, 2026 16:59
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