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>
The contact-summary sweep generated a 3-field digest (mostRecently /
overallSummary / recap) per iMessage contact via the model-boss chat
endpoint. It's redundant with the prospector, which already classifies
1271 prospects with tier + archetype + score + status — strictly richer
per-person intel for the contacts that matter. It was also the path that
wedged the server against the decommissioned model-boss host (2026-06-23).
Remove the generation path entirely: the per-sync sweep in
ingestContacts, the contact-summary feature module + its test, and the
now-orphaned chatJson client in shared/model-boss.ts (contact-summary was
its only caller). The connection circuit breaker stays — the
embedding-worker still calls the same coordinator and needs the same
wedge protection.
Kept the read-side data layer (summary_data column, summaryData field,
updateContactSummary, the /my/contacts surface field) dormant as the
landing spot if summaries are ever repopulated offline via batch.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The contact-summary sweep and embedding-worker call model-boss (GPU
coordinator) sequentially. When its host is offline every call paid the
full TCP-connect timeout (~3s) before failing; a sweep over ~1700
contacts serialised thousands of slow failures and stalled the whole
server — message ingest froze for hours while the listener stayed up
(observed 2026-06-23, coordinator host decommissioned).
Add a connectivity circuit breaker in shared/model-boss.ts: after 3
consecutive connection failures it opens for a 60s cooldown and fails
fast (no fetch), auto-probing once afterwards to recover. The
contact-summary sweep now bails the moment the breaker is open instead
of queueing doomed per-contact work. HTTP error responses still count as
reachable — the breaker tracks connectivity, not request success.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>