Bug
Pulse's cron scheduler (PULSE/lib.ts, isDue()) — and the standalone Install*.ts launchd LaunchAgent templates in TOOLS/ — do not catch up missed calendar-scheduled jobs after the machine has been off or asleep through the scheduled time. On any laptop that isn't left powered on/charging overnight, every nightly/weekly job silently stops running, with no error and no log entry indicating a miss.
Root cause 1 — isDue() requires a live exact-minute match, not "most recent occurrence since last run"
// PULSE/lib.ts
export function isDue(schedule: string, now: Date, lastRun?: number): boolean {
if (!matchesCron(schedule, now)) return false
if (lastRun === undefined) return true
// Don't run more than once per minute
return Math.floor(now.getTime() / 60_000) > Math.floor(lastRun / 60_000)
}
matchesCron(schedule, now) only returns true when the current wall-clock minute matches the cron fields. There is no comparison against "when was this job last supposed to fire, and did we miss it." Consequences for any [[job]] in PULSE.toml with a calendar-style schedule (e.g. 0 3 * * *, 0 4 * * 0, 0 23 * * *):
- If Pulse (and the machine) is not running at the exact HH:MM the schedule specifies,
isDue() returns false for that tick.
- It stays
false for every subsequent tick until wall-clock time happens to land on that exact minute again — which, on a machine that's off overnight, may never happen if the user's wake time doesn't line up with the job's minute.
- Nothing surfaces this: no warning, no "N days since last successful run," no degraded-health signal.
Documentation/Pulse/PulseSystem.md states "jobs that were overdue during the downtime will run on the first tick" — that's only true for high-frequency schedules (*/5 * * * * etc.) where the next match is minutes away. For once-daily/weekly jobs it's false, and the documentation is misleading about the actual guarantee.
Root cause 2 — inconsistent RunAtLoad across the independent LaunchAgent templates
Outside Pulse's own heartbeat, several background jobs are installed as separate launchd LaunchAgents via TOOLS/Install*.ts + TOOLS/com.lifeos.*.plist.template, each with its own StartCalendarInterval. RunAtLoad (fire once when the agent loads, i.e. at every login/boot — the closest thing launchd offers to "catch up if we missed the scheduled time") is set inconsistently:
| Template |
Schedule |
RunAtLoad |
com.lifeos.usage-aggregator.plist.template |
03:30 daily |
true |
com.lifeos.blogdiscovery.plist.template |
04:00 daily |
false |
com.lifeos.commitmentsweep.plist.template |
07:00 daily |
false |
com.lifeos.codexupdate.plist.template |
04:00 daily |
absent (comment: "No RunAtLoad — avoids reinstalling the codex global on every login") |
Only usage-aggregator got the fix. blogdiscovery and commitmentsweep have no stated reason to omit it — they look like the same gap, just not patched yet.
Suggested fix
1. Make isDue() catch-up-aware (fixes every current and future PULSE.toml job in one place, no per-job config):
// PULSE/lib.ts
export function isDue(schedule: string, now: Date, lastRun?: number): boolean {
const lastScheduled = lastScheduledOccurrence(schedule, now) // most recent past minute matching the cron fields
if (lastScheduled === null) return false
if (lastRun === undefined) return true
// Due if the most recent scheduled firing happened after the last successful run
// (covers both "it's due right now" and "we missed it while the machine was off/asleep").
return lastScheduled > lastRun
}
function lastScheduledOccurrence(schedule: string, now: Date, lookbackMinutes = 7 * 24 * 60): Date | null {
const cursor = new Date(now)
cursor.setSeconds(0, 0)
for (let i = 0; i <= lookbackMinutes; i++) {
if (matchesCron(schedule, cursor)) return cursor
cursor.setMinutes(cursor.getMinutes() - 1)
}
return null // no match in the lookback window — malformed schedule, or a field combination that can't occur (e.g. Feb 30)
}
The existing "don't run more than once per minute" behavior is preserved as a side effect of comparing against lastRun directly rather than floor-dividing by 60s — a job that already ran this minute has lastRun >= lastScheduled for that same occurrence, so it won't double-fire. Recommend capping lookbackMinutes (a week is generous; even a day covers "laptop was off overnight") so a badly-formed cron expression that never matches doesn't scan forever.
2. Add RunAtLoad: true to com.lifeos.blogdiscovery.plist.template and com.lifeos.commitmentsweep.plist.template, matching the fix already shipped for usage-aggregator. For com.lifeos.codexupdate.plist.template, which intentionally opts out of RunAtLoad to avoid reinstalling the codex global on every login, the safer fix is an idempotency check inside CodexUpdate.ts itself (skip if already up to date) so RunAtLoad can be safely turned on there too — happy to send that as a follow-up if useful.
Environment
- macOS, Pulse installed via the standard
~/.claude LifeOS install path.
- Reproduced by reading
PULSE/lib.ts isDue()/matchesCron() and the four com.lifeos.*.plist.template files with StartCalendarInterval, and cross-checking against PULSE/state/state.json job lastRun timestamps and Documentation/Pulse/PulseSystem.md's stated overdue-catch-up guarantee.
Bug
Pulse's cron scheduler (
PULSE/lib.ts,isDue()) — and the standaloneInstall*.tslaunchd LaunchAgent templates inTOOLS/— do not catch up missed calendar-scheduled jobs after the machine has been off or asleep through the scheduled time. On any laptop that isn't left powered on/charging overnight, every nightly/weekly job silently stops running, with no error and no log entry indicating a miss.Root cause 1 —
isDue()requires a live exact-minute match, not "most recent occurrence since last run"matchesCron(schedule, now)only returnstruewhen the current wall-clock minute matches the cron fields. There is no comparison against "when was this job last supposed to fire, and did we miss it." Consequences for any[[job]]inPULSE.tomlwith a calendar-style schedule (e.g.0 3 * * *,0 4 * * 0,0 23 * * *):isDue()returnsfalsefor that tick.falsefor every subsequent tick until wall-clock time happens to land on that exact minute again — which, on a machine that's off overnight, may never happen if the user's wake time doesn't line up with the job's minute.Documentation/Pulse/PulseSystem.mdstates "jobs that were overdue during the downtime will run on the first tick" — that's only true for high-frequency schedules (*/5 * * * *etc.) where the next match is minutes away. For once-daily/weekly jobs it's false, and the documentation is misleading about the actual guarantee.Root cause 2 — inconsistent
RunAtLoadacross the independent LaunchAgent templatesOutside Pulse's own heartbeat, several background jobs are installed as separate launchd
LaunchAgents viaTOOLS/Install*.ts+TOOLS/com.lifeos.*.plist.template, each with its ownStartCalendarInterval.RunAtLoad(fire once when the agent loads, i.e. at every login/boot — the closest thing launchd offers to "catch up if we missed the scheduled time") is set inconsistently:RunAtLoadcom.lifeos.usage-aggregator.plist.templatetruecom.lifeos.blogdiscovery.plist.templatefalsecom.lifeos.commitmentsweep.plist.templatefalsecom.lifeos.codexupdate.plist.templateOnly
usage-aggregatorgot the fix.blogdiscoveryandcommitmentsweephave no stated reason to omit it — they look like the same gap, just not patched yet.Suggested fix
1. Make
isDue()catch-up-aware (fixes every current and futurePULSE.tomljob in one place, no per-job config):The existing "don't run more than once per minute" behavior is preserved as a side effect of comparing against
lastRundirectly rather than floor-dividing by 60s — a job that already ran this minute haslastRun >= lastScheduledfor that same occurrence, so it won't double-fire. Recommend cappinglookbackMinutes(a week is generous; even a day covers "laptop was off overnight") so a badly-formed cron expression that never matches doesn't scan forever.2. Add
RunAtLoad: truetocom.lifeos.blogdiscovery.plist.templateandcom.lifeos.commitmentsweep.plist.template, matching the fix already shipped forusage-aggregator. Forcom.lifeos.codexupdate.plist.template, which intentionally opts out ofRunAtLoadto avoid reinstalling the codex global on every login, the safer fix is an idempotency check insideCodexUpdate.tsitself (skip if already up to date) soRunAtLoadcan be safely turned on there too — happy to send that as a follow-up if useful.Environment
~/.claudeLifeOS install path.PULSE/lib.tsisDue()/matchesCron()and the fourcom.lifeos.*.plist.templatefiles withStartCalendarInterval, and cross-checking againstPULSE/state/state.jsonjoblastRuntimestamps andDocumentation/Pulse/PulseSystem.md's stated overdue-catch-up guarantee.