The /client/imessage/send-queue/pending endpoint released up to 50 queued
sends per poll, so an enqueued burst all fired at once. Add a configurable
release cap: the endpoint now returns at most (maxSends − sent-in-window)
queued items, so a burst queues and drips out at the configured rate.
- macsync.send_rate_config single-row table, default max_sends=10,
window_seconds=300 (10 per 5 min).
- entities/send-queue repo: getSendRateConfig / setSendRateConfig /
countSentWithin.
- Admin control: GET/PUT /admin/send-rate-limit (service-token auth) so the
cap is adjustable at runtime (wired to MCP via quinn.api separately).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The iMessage read cycle is driven by BaseSyncManager's 30s timer →
syncNow(), which is gated by 'guard !isSyncing'. performSync awaited
blobSyncManager.syncBlobs() inline, and that blob pass infinite-loops
when the upload backend is failing: /attachments/missing has no cursor,
so a full page of perpetually-failing uploads is re-fetched and re-failed
forever, the loop only breaking on a < pageSize page. performSync never
returned → isSyncing stuck true → every 30s read tick swallowed. Net
effect: messages only synced on app launch, drifting hours behind between
restarts (send-queue timers are independent, so they kept polling — the
tell that the timer fired but syncNow was gated).
Two fixes:
- Decouple the blob pass: fire it detached + in-flight-guarded instead of
awaiting it on the read cycle, so a slow/failing blob backend can never
hold isSyncing.
- Bound the blob loop: stop a pass after any full page that produced zero
successful uploads (the same missing set would be re-fetched), instead
of spinning forever.
Verified: read cycle now fires every ~30s on the live process without a
restart; blob pass logs 'stopping pass' and returns; store lag ~7s.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>