Commit21ffda50Recorded6 Jul 2026Repositorycourier

Simplify dedup: use stdlib assoc/filter, rename state file

Message

Post-review cleanup (no behavior change): replace the hand-rolled has-key?/remove-key/dedup-prune recursions with (sigil core) assoc and filter, and skip the redundant filter on check-and-record!'s hot 'proceed path (a freshly-pruned non-dup list can't contain the key). Rename the persisted file send-dedup.log -> send-dedup.state since it is rewritten in place, not appended like courier.log.

Full suite 69 pass; regression harness still shows fixed=1 vs control=4.

Changed
 repro/send_driver.py  |  2 +-
 src/courier/dedup.sgl | 42 +++++++++++++++++++-----------------------
 2 files changed, 20 insertions(+), 24 deletions(-)
Diff
repro/send_driver.pymodified
@@ -156,7 +156,7 @@ def main():
156
# Sends ENABLED against the mock (this is the whole point).
157
env.pop("COURIER_DISABLE_TELEGRAM_SEND", None)
158
env["CLAUDE_OPS_LOG_DIR"] = args.logdir
159
# Isolate persistent state (relay dir + the send-dedup.log that the
+159
# Isolate persistent state (relay dir + the send-dedup.state that the
160
# fix writes beside it) to this run's logdir, so runs don't cross-
161
# contaminate. Every restarted courier gen shares this dir -- exactly
162
# how the real leader's restarts share ~/.courier -- so the persistent
src/courier/dedup.sglmodified
@@ -63,7 +63,9 @@
63
(let ((override (getenv "COURIER_SEND_DEDUP_FILE")))
64
(if (and override (not (string=? override "")))
65
override
66
(path-join (path-dirname (default-relay-dir)) "send-dedup.log"))))
+66
;; ".state", not ".log": this is a rewritten-in-place state set,
+67
;; not an append log like courier.log.
+68
(path-join (path-dirname (default-relay-dir)) "send-dedup.state"))))
69
70
;; ---- FNV-1a 64-bit key hashing (no plaintext on disk) ----
71
(define *fnv-offset* 14695981039346656037)
@@ -118,25 +120,11 @@
120
;; Drop entries older than the window.
121
(define (dedup-prune entries now window)
122
(: list? number? number? -> list?)
121
(cond
122
((null? entries) '())
123
((< (- now (cdr (car entries))) window)
124
(cons (car entries) (dedup-prune (cdr entries) now window)))
125
(else (dedup-prune (cdr entries) now window))))
+123
(filter (lambda (e) (< (- now (cdr e)) window)) entries))
124
127
;; Remove any entry with the given key.
128
(define (remove-key entries key)
129
(cond
130
((null? entries) '())
131
((string=? (car (car entries)) key)
132
(remove-key (cdr entries) key))
133
(else (cons (car entries) (remove-key (cdr entries) key)))))
134
135
(define (has-key? entries key)
136
(cond
137
((null? entries) #f)
138
((string=? (car (car entries)) key) #t)
139
(else (has-key? (cdr entries) key))))
+125
;; All entries except the one with `key`.
+126
(define (without-key entries key)
+127
(filter (lambda (e) (not (string=? (car e) key))) entries))
128
129
(define (save-entries! path entries)
130
(ensure-directory (path-dirname path))
@@ -157,9 +145,17 @@
145
(define (dedup-check-and-record! path key now window)
146
(: string? string? number? number? -> symbol?)
147
(let* ((entries (dedup-prune (dedup-load-entries path) now window))
160
(dup (has-key? entries key)))
161
(save-entries! path (cons (cons key now) (remove-key entries key)))
162
(if dup 'suppress 'proceed)))
+148
(dup (assoc key entries)))
+149
(if dup
+150
;; Refresh the timestamp (sliding window) so an ongoing retry
+151
;; loop stays suppressed as long as it keeps firing.
+152
(begin (save-entries! path
+153
(cons (cons key now) (without-key entries key)))
+154
'suppress)
+155
;; New key: record it (a freshly-pruned non-dup list cannot
+156
;; already contain it, so no removal needed) BEFORE delivery.
+157
(begin (save-entries! path (cons (cons key now) entries))
+158
'proceed))))
159
160
;;; Remove a previously-recorded key. Called when the caller learns
161
;;; the send definitely did NOT reach Telegram, so a genuine retry is
@@ -168,4 +164,4 @@
164
(: string? string? -> void?)
165
(when (file-exists? path)
166
(guard (e (else (values)))
171
(save-entries! path (remove-key (dedup-load-entries path) key)))))))
+167
(save-entries! path (without-key (dedup-load-entries path) key)))))))