Commit23afc2a7Recorded3 Apr 2026Repositoryminder

Fix schedule persistence: use read-modify-write instead of full rewrite

Message

save-config! was serializing the entire in-memory active-schedules list to minder.yaml. If active-schedules was empty or incomplete when this ran, all existing schedules in the file were clobbered.

Replace with add-schedule-to-config! that reads the current file, merges the single new/updated entry, and writes back — matching the pattern remove-schedule-from-config! already used.

Also: - One-time schedules no longer persist to config (the format can't represent the one-time flag, and they auto-remove after firing) - Extract shared YAML serialization into schedules->yaml-lines and write-schedules-config! helpers - Suppress config-watcher reload after our own writes to avoid redundant re-parsing

Changed
 src/minder/scheduler.sgl | 83 +++++++++++++++++++++++++++++++++++++++++++++--------------------------------------
 1 file changed, 45 insertions(+), 38 deletions(-)
Diff
src/minder/scheduler.sglmodified
@@ -29,6 +29,9 @@
29
;; Mutable alist of (name handle cron prompt one-time?)
30
(define active-schedules '())
31
+32
;; Flag to suppress config-watcher reload after our own writes.
+33
(define suppress-reload? #f)
+34
35
(define (get-active name)
36
(let ((pair (assoc name active-schedules)))
37
(and pair (cdr pair))))
@@ -133,9 +136,12 @@
136
(when (file-exists? "minder.yaml")
137
(let ((current-mtime (file-mtime "minder.yaml")))
138
(when (> current-mtime last-mtime)
136
(log-info "minder.yaml changed, reloading schedules")
137
(reload-schedules! server)
138
(set! last-mtime current-mtime))))
+139
(set! last-mtime current-mtime)
+140
(if suppress-reload?
+141
(set! suppress-reload? #f)
+142
(begin
+143
(log-info "minder.yaml changed, reloading schedules")
+144
(reload-schedules! server))))))
145
(loop))))
146
147
;; Reload schedules from minder.yaml, only touching changed ones.
@@ -172,43 +178,43 @@
178
;; Config File Persistence
179
;; ============================================================
180
175
;; Write the current schedule state to minder.yaml.
176
(define (save-config!)
177
(let ((lines '()))
178
(when (pair? active-schedules)
179
(set! lines (list "schedules:"))
180
(for-each
181
(lambda (entry)
182
(let ((name (car entry))
183
(cron (caddr entry))
184
(desc (cadddr entry)))
185
(set! lines (append lines
186
(list (format " ~a:" name)
187
(format " cron: \"~a\"" cron)
188
(format " prompt: ~a" desc))))))
189
active-schedules))
+181
;; Serialize schedule-config records to YAML lines.
+182
(define (schedules->yaml-lines schedules)
+183
(if (pair? schedules)
+184
(cons "schedules:"
+185
(apply append
+186
(map (lambda (sched)
+187
(list (format " ~a:" (schedule-config-name sched))
+188
(format " cron: \"~a\"" (schedule-config-cron sched))
+189
(format " prompt: ~a" (schedule-config-prompt sched))))
+190
schedules)))
+191
'()))
+192
+193
;; Write a list of schedule-config records to minder.yaml.
+194
(define (write-schedules-config! schedules)
+195
(guard (e (else
+196
(log-error (format "Failed to update config: ~a" e))))
+197
(set! suppress-reload? #t)
198
(write-file-string "minder.yaml"
191
(string-append (string-join lines "\n") "\n"))))
+199
(string-append (string-join (schedules->yaml-lines schedules) "\n") "\n"))))
+200
+201
;; Add or update a single schedule in minder.yaml via read-modify-write.
+202
(define (add-schedule-to-config! name cron desc)
+203
(let* ((current (load-minder-config))
+204
(existing (filter
+205
(lambda (s) (not (equal? (schedule-config-name s) name)))
+206
(minder-config-schedules current)))
+207
(updated (append existing
+208
(list (schedule-config name: name cron: cron prompt: desc)))))
+209
(write-schedules-config! updated)))
210
211
;; Remove a single schedule from minder.yaml without touching others.
212
(define (remove-schedule-from-config! name)
195
(guard (e (else
196
(log-error (format "Failed to update config: ~a" e))))
197
(let* ((current (load-minder-config))
198
(remaining (filter
199
(lambda (s) (not (equal? (schedule-config-name s) name)))
200
(minder-config-schedules current)))
201
(lines (if (pair? remaining)
202
(cons "schedules:"
203
(apply append
204
(map (lambda (sched)
205
(list (format " ~a:" (schedule-config-name sched))
206
(format " cron: \"~a\"" (schedule-config-cron sched))
207
(format " prompt: ~a" (schedule-config-prompt sched))))
208
remaining)))
209
'())))
210
(write-file-string "minder.yaml"
211
(string-append (string-join lines "\n") "\n")))))
+213
(let* ((current (load-minder-config))
+214
(remaining (filter
+215
(lambda (s) (not (equal? (schedule-config-name s) name)))
+216
(minder-config-schedules current))))
+217
(write-schedules-config! remaining)))
218
219
;; ============================================================
220
;; MCP Tools
@@ -258,8 +264,9 @@
264
(cancel-schedule! name))
265
;; Register the schedule
266
(register-schedule! server name cron desc one-time?: one-time?)
261
;; Persist to config
262
(save-config!)
+267
;; Persist to config (skip one-time schedules — they auto-remove)
+268
(unless one-time?
+269
(add-schedule-to-config! name cron desc))
270
(format "Schedule '~a' added~a: ~a" name
271
(if one-time? " (one-time)" "") cron))))
272