Skip to content

The daemon's cadences count the time that passed, not the turns that ran - #1628

Merged
suleimansh merged 2 commits into
mainfrom
daemon-clock-counts-firings
Aug 21, 2026
Merged

The daemon's cadences count the time that passed, not the turns that ran#1628
suleimansh merged 2 commits into
mainfrom
daemon-clock-counts-firings

Conversation

@suleimansh

Copy link
Copy Markdown
Contributor

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: N sweep drifted with it, because they all read the same counter.

What changed

  • The interval 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 one is elapsed time — that distinction is why all seven existing tests pass unmodified.
  • Due-ness is asked per job — ticks since that job's own last turn — rather than 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.
  • 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.

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:

✖ a firing that lands mid-turn still counts, so one slow job cannot stretch every other cadence
  AssertionError: the firings that landed mid-turn counted towards the cadence
  1 !== 2

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 its fetch still has no destination refspec, so objects land in FETCH_HEAD and 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.

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
suleimansh merged commit 682fb2b into main Aug 21, 2026
2 checks passed
@suleimansh
suleimansh deleted the daemon-clock-counts-firings branch August 21, 2026 18:41
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>
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