Reconciliation and cursor recovery
Duckbill™ treats reconciliation as a required correctness mechanism. Incremental APIs and notifications are hints. Canonical MailMessage / MessageInstance rows are the source of truth after repair.
See ADR 0017 and the governing synchronization specification §§13–14 and §17. Incremental cursors use scope incremental-delta (ADR 0016).
Levels
| Level | What it compares | Typical use |
|---|---|---|
| Lightweight | Recently changed instances plus incremental cursor health | Continuous catch-up |
| Periodic | Provider vs local identity and state over a bounded received window | Scheduled mailbox check |
| Deep | Full provider listing vs local instances | Drift, manual request, post-recovery |
Each run inserts and completes a reconciliation_runs row. Diagnostics include finding kinds. Initial sync stays awaiting_reconciliation until a successful run marks the account healthy.
Drift and repairs
- Missing provider message — upsert
MailMessage/MessageInstanceusing existing internet-message-id reuse. Do not invent a second identity scheme. - Orphan local instance — tombstone (
is_deleted = true). The row remains. - Stale read or folder — update instance flags and memberships. Folder changes do not redefine semantic classification.
- Duplicate mapping — flag and audit. No auto-merge.
Repairs emit audit_events (reconciliation.repair.*, reconciliation.flag.duplicate_mapping). A second run against the same mailbox is a no-op for already-repaired drift.
Sent mail and read-state
READ ≠ COMPLETE. Provider mark-read updates MessageInstance.is_read only.
An external Outlook (or other client) sent reply can be associated to the existing thread via providerThreadId. If conversation attention is already RESPOND, reconciliation moves it to WAITING. If attention does not exist yet, sent-mail association does not invent RESPOND.
Cursor recovery
If the incremental checkpoint is expired or invalid:
- Mark the checkpoint
degradedand account syncerror, thensyncing. - Preserve existing canonical rows.
- Bounded list/upsert from the provider.
- Run reconciliation.
- Establish a new incremental cursor and return the account to
idle.
Cursor loss is degraded sync, not a database reset.
