The daemon's cadences count the time that passed, not the turns that ran - #1628
Merged
Conversation
One interval fires and each job says how many ticks it wants between turns, so a cadence is a small integer rather than a duration. But a tick was only ever counted when a turn actually ran, and an interval that came round while a turn was still in flight joined it and was counted nowhere. So one slow job stretched every cadence in the daemon by its own duration. Seen live: while one project's data sync failed slowly against a remote it could not reach, the ten-minute cloud-work pass came round every twenty-six minutes — and every other `every: N` sweep drifted with it, because they all read the same counter. The interval now counts a firing that lands mid-turn instead of dropping it, and the clock takes those firings into account when the next turn starts. A manual `tick()` still joins without moving the clock: shutdown and the tests drive it, and neither is elapsed time. Due-ness is asked per job — ticks since that job's own last turn — rather than `n % every`. With the clock free to jump over the tick a modulo would have landed on, a job would otherwise have waited a whole further cadence for it to come round again. Missed turns are still skipped rather than queued: a job that was passed over comes back to the next turn, not to a backlog of them. Refs #1607. Its other half — the adoption pass spawning git per run per `claude/*` head, and the fetch with no destination refspec — is untouched, so the issue stays open. Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
suleimansh
added a commit
that referenced
this pull request
Aug 22, 2026
…ran (#1628) One interval fires and each job says how many ticks it wants between turns, so a cadence is a small integer rather than a duration. But a tick was only ever counted when a turn actually ran, and an interval that came round while a turn was still in flight joined it and was counted nowhere. So one slow job stretched every cadence in the daemon by its own duration. Seen live: while one project's data sync failed slowly against a remote it could not reach, the ten-minute cloud-work pass came round every twenty-six minutes — and every other `every: N` sweep drifted with it, because they all read the same counter. The interval now counts a firing that lands mid-turn instead of dropping it, and the clock takes those firings into account when the next turn starts. A manual `tick()` still joins without moving the clock: shutdown and the tests drive it, and neither is elapsed time. Due-ness is asked per job — ticks since that job's own last turn — rather than `n % every`. With the clock free to jump over the tick a modulo would have landed on, a job would otherwise have waited a whole further cadence for it to come round again. Missed turns are still skipped rather than queued: a job that was passed over comes back to the next turn, not to a backlog of them. Refs #1607. Its other half — the adoption pass spawning git per run per `claude/*` head, and the fetch with no destination refspec — is untouched, so the issue stays open. Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
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.
The daemon runs one interval and each job says how many ticks it wants between turns, so a cadence is a small integer rather than a duration. A tick was only counted when a turn actually ran, though — an interval that came round while a turn was still in flight joined it and was counted nowhere.
So one slow job stretched every cadence in the daemon by its own duration. Seen live and reported in #1607: while one project's data sync was failing slowly against a remote it could not reach, the ten-minute cloud-work pass came round every twenty-six minutes (10:50:46, then 11:16:57) — and every other
every: Nsweep drifted with it, because they all read the same counter.What changed
tick()still joins without moving the clock. Shutdown and the tests drive it, and neither one is elapsed time — that distinction is why all seven existing tests pass unmodified.n % every. With the clock now free to jump over the tick a modulo would have landed on, a job would otherwise have waited a whole further cadence for it to come round again.Verification
The new test is wired to the real mechanism, not to nothing — it was run against the old clock to confirm it fails there:
Against this branch it passes, with the other seven unchanged, and the full suite is green at 1525 passed / 0 failed.
Not in scope
#1607's other half is untouched, so the issue stays open (no closing keyword here): the adoption pass still spawns git per waiting run per
claude/*head, and itsfetchstill has no destination refspec, so objects land inFETCH_HEADand are re-fetched after gc.One further thing this does not fix, worth naming: jobs still run one at a time within a turn, so a job that blocks for twenty-six minutes still holds up its tickmates for twenty-six minutes. What is fixed is that the delay no longer compounds into every future cadence. Time-boxing a job's turn — the other option #1607 floated — remains open.