Sentinel-zero storm — off-device repro + fix (2026-07-03)
The 2026-07-02 live storm: a count=0 cold-start drain (empty backlog) persisted offset=0; a later restart that trusts 0 restores it and the normal poll loop issues getUpdates offset=0 -> Telegram re-returns the whole backlog -> re-delivered -> leader acks each -> outbound storm.
Fix: never persist a non-positive offset (0 is Telegram's "send everything" sentinel, never a real updateid); an empty-backlog cold start leaves NO offset file, so the next start cold-drains instead of restoring 0. load also independently rejects <=0. NOTHING below touched real Telegram (poller pointed at repro/mocktelegram.py via COURIERTELEGRAMAPI_URL).
Harness (repro/repro-sentinel-zero.sh): CONTROL vs FIXED
=== FIXED (../build/dev/bin/courier) === empty cold start left offset file? (expect no): note file removed by restart drain restart message events: 0 (expect 0) PASS: fixed binary does not re-deliver === CONTROL (/home/daviwil/Projects/Code/sigil/courier-sentinel-control/build/dev/bin/courier) — models the deployed sentinel-0 binary === restart message events: 3 (expect >0 = storm reproduced) PASS: control reproduces the storm (3 re-delivered) === ALL CHECKS PASSED ===
CONTROL = master(6805ad1) + load accepts 0 (models the deployed binary, which live-logged 'Restored offset=0'). FIXED = this branch.
Manual verification — FIXED RELEASE (native, the deploy target)
empty cold start (native): Cold start: drained ... count=0 offset=0 persisted=no (empty backlog) -> NO offset file restart w/ 2-msg backlog: Cold start: drained ... count=2 offset=3 persisted=yes -> 0 message events planted poisoned '0' file: Cold start: drained ... count=2 -> load REJECTED 0, drained, 0 message events (native codegen of (> n 0) verified correct; dev and release behave identically)
Unit tests
test/test-poller-offset.sgl adds a 'never persists the sentinel 0' group (save 0 -> no file; save 0 clears an existing file; negative -> no file; positive still persists). Full suite: 63 passed.