fix(cron): resilient file watcher + periodic reconciler (closes #15)#16
Merged
Conversation
The cron file-watcher could silently die on atomic-rename writes (vim, git checkout, many editors), leaving jobs.json edits invisible to the live scheduler — even though GET /api/cron kept returning the latest jobs.json with computed nextRun via cron-parser. Newly-enabled jobs never fired and there was no visible error. Two layers of defense (per #15): 1. Resilient single-file watcher (watcher.ts): - watchSingleFile() helper watches the parent directory filtered to the target file (depth 0, ignored-filter). The directory watch persists across inode replacements, where a direct file watch's fs handle becomes invalid. - Listens to both 'change' (in-place edits) and 'add' (atomic replace lands here, not 'change'). - Registers a chokidar 'error' handler that logs the error instead of swallowing it. - Wraps the user callback in try/catch with logging so a throw can't kill the watcher. - Applied to BOTH config.yaml and cron/jobs.json watchers (org/skills already watch directories, naturally resilient). 2. Periodic reconciler (cron/reconciler.ts): - Every 5 min: loadJobs() -> signatureOfJobs(jobs) compared to getScheduledSignature() (the sig of the last scheduleJobs pass). On divergence -> reloadScheduler(jobs). - Signature hashes id+schedule+timezone+engine+model+employee+prompt of *enabled* jobs only. Prompt is included because runCronJob captures it at schedule time — a prompt edit must trigger reschedule for the live closure to pick it up. - Timer .unref()s so it doesn't keep the process alive. - Wired alongside startScheduler / stopScheduler in server.ts. Tests: 7 unit tests covering signature stability, order independence, disabled-job exclusion, schedule/prompt change detection, enable/disable transitions. Full jimmy suite: 741/741 passing. Closes #15. Co-Authored-By: Claude Opus 4.7 <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.
Closes #15
Fixes the silent file-watcher drop where
cron/jobs.jsonedits stopped being picked up by the live scheduler — leaving newly-enabled jobs with a sensiblenextRunin/api/cronbutlastRun: nullforever (because the API computesnextRunviacron-parserindependently of the live node-cron scheduler state).Root cause
The single-file chokidar watch on
cron/jobs.json:git checkout, many editors) unlink the inode and the underlying fs handle becomes invalid; chokidar silently stops emitting events.change(atomic replaces emitunlink+add, notchange).errorhandler → any chokidar error was swallowed.Fix — two layers of defense
1. Resilient single-file watcher (
gateway/watcher.ts)New
watchSingleFile()helper that watches the parent directory filtered to one file viaignored, withdepth: 0. Directory watches persist across inode replacements. Listens tochangeandadd, registers anerrorhandler, wraps the callback in try/catch with logging. Applied to bothconfig.yamlandcron/jobs.json—org/andskills/already watch directories and are naturally resilient.2. Periodic reconciler (
cron/reconciler.ts, new)Every 5 min:
loadJobs()→signatureOfJobs(jobs)compared togetScheduledSignature()(the sig of the last successfulscheduleJobspass, tracked inscheduler.ts). On divergence →reloadScheduler(jobs). Signature hashesid + schedule + timezone + engine + model + employee + promptof enabled jobs only.Timer
.unref()s so it doesn't keep the process alive. Wired alongsidestartScheduler/stopSchedulerinserver.ts.Verification
pnpm typecheckclean.cron/__tests__/reconciler.test.tscover signature stability, order independence, disabled-job exclusion, schedule/prompt change detection, enable/disable transitions.Files
packages/jimmy/src/cron/scheduler.tssignatureOfJobs(), +getScheduledSignature(), tracklastScheduledSiginscheduleJobspackages/jimmy/src/cron/reconciler.tsstartCronReconciler()/stopCronReconciler()/tickReconciler()packages/jimmy/src/cron/__tests__/reconciler.test.tspackages/jimmy/src/gateway/watcher.tswatchSingleFile()helper; applied to config + cron watcherspackages/jimmy/src/gateway/server.tsOperational note
The PR doesn't remove the existing
PUT /api/cron/:idworkaround — that's still a valid per-job lever. The reconciler is the background safety net; the watcher fix should make manual reloads rarely necessary even on macOS+atomic writes.🤖 Generated with Claude Code