Mail has ListFolders/ListMessages/GetMessage/MarkRead/SearchMessages/SendMessage/Sync — no Move, Delete, or Flag. The only way to move mail today is a filter rule. There is no DAV write path at all.
Contract
Mail.Move / Delete / SetFlags (wire-additive; avoid colliding with DeleteRuleRequest).
ERROR_REASON_CONFLICT = 13 — an error, not a response field, so old clients take the error path instead of reading a conflict as success. Breaks the exhaustive match in errors.rs at compile time, which is the good failure mode.
Mail.ListConflicts / ResolveConflict{retry|discard} over a durable conflicts table. Conflicted rows currently have no listing RPC, no GC, no retention.
Event gains placement-change and conflict arms (tags 10, 11 free); GetStatus gains per-collection sync state (11 free).
Subscribe needs a monotonic sequence and since_seq. It is a raw BroadcastStream that silently drops events on Lagged, so every event-driven guarantee is currently best-effort.
Semantics
Mutations are desired end-state on an identity, not imperative operations on a placement, so re-enumerate-and-retarget preserves intent. Cap re-targets at 2, then hard conflict. Events are an optimization over a queryable source of truth, not the truth.
DAV write-back
If-Match: <etag> on PUT/DELETE, 412 ⇒ conflict. RFC 4791 §5.3.4: a server that rewrites the submitted object MUST NOT return an ETag — and Google/Apple normalise aggressively — so Applied { token: None } must schedule a re-read or the next If-Match 412s in a self-inflicted loop.
⚠️RFC 6638 scheduling hazard: PUTting a VEVENT with ATTENDEEs on a scheduling-aware server sends real invitations to real people. "Just PUT with If-Match" is not a safe calendar write model.
Also fix here (current honesty violations)
Mail.MarkRead and Contacts.CreateContact are silently local-only and report success. These are semantics changes to published RPCs, not additive gaps.
Depends on #394 and #391. Serialize the proto tail — STORY-WORKFLOW.md permits one proto story in flight.
**The contract half. Gates the M7 freeze.**
`Mail` has `ListFolders`/`ListMessages`/`GetMessage`/`MarkRead`/`SearchMessages`/`SendMessage`/`Sync` — **no Move, Delete, or Flag**. The only way to move mail today is a filter rule. There is no DAV write path at all.
### Contract
- `Mail.Move` / `Delete` / `SetFlags` (wire-additive; avoid colliding with `DeleteRuleRequest`).
- **`ERROR_REASON_CONFLICT = 13`** — an error, not a response field, so old clients take the error path instead of reading a conflict as success. Breaks the exhaustive match in `errors.rs` at compile time, which is the good failure mode.
- `Mail.ListConflicts` / `ResolveConflict{retry|discard}` over a durable `conflicts` table. Conflicted rows currently have no listing RPC, no GC, no retention.
- `Event` gains placement-change and conflict arms (tags 10, 11 free); `GetStatus` gains per-collection sync state (11 free).
- **`Subscribe` needs a monotonic sequence and `since_seq`.** It is a raw `BroadcastStream` that silently drops events on `Lagged`, so every event-driven guarantee is currently best-effort.
### Semantics
Mutations are **desired end-state on an identity**, not imperative operations on a placement, so re-enumerate-and-retarget preserves intent. Cap re-targets at 2, then hard conflict. Events are an optimization over a queryable source of truth, not the truth.
### DAV write-back
`If-Match: <etag>` on PUT/DELETE, 412 ⇒ conflict. **RFC 4791 §5.3.4: a server that rewrites the submitted object MUST NOT return an ETag** — and Google/Apple normalise aggressively — so `Applied { token: None }` must schedule a re-read or the next `If-Match` 412s in a self-inflicted loop.
⚠️ **RFC 6638 scheduling hazard:** PUTting a VEVENT with ATTENDEEs on a scheduling-aware server sends real invitations to real people. "Just PUT with If-Match" is not a safe calendar write model.
### Also fix here (current honesty violations)
`Mail.MarkRead` and `Contacts.CreateContact` are silently **local-only and report success**. These are semantics changes to published RPCs, not additive gaps.
Depends on #394 and #391. **Serialize the proto tail** — `STORY-WORKFLOW.md` permits one proto story in flight.
Design: [ADR 0002](https://github.com/KofTwentyTwo/nuncio/blob/dev/docs/adr/0002-multi-engine-sync-model.md) · [review evidence](https://github.com/KofTwentyTwo/nuncio/blob/dev/docs/reviews/2026-08-10-multi-engine-design-review.md) (PR #385)
The engine could already execute verified remote mutations; nothing could ask it to. Now: MoveMessage, DeleteMessage, FlagMessage on Mail, each addressing a specific placement rather than just a message key — a message can occupy several mailboxes and a MOVE/DELETE against the wrong occupancy is destructive. Plus ListConflicts/ResolveConflict over a durable conflict store, Conflict as a typed ErrorReason (append-only) so older clients take the error path, Message carrying its full placement set, and filters_enabled on the wire so filter ownership no longer requires hand-editing SQLite.
CLI verbs ship alongside (+513 lines) — an RPC nobody can invoke would not meet this repo's definition of done. Proto change is strictly additive: nothing removed, nothing renumbered; descriptor golden regenerated per the test's own instructions.
Deferred: DAV conditional write-back. It is calendar/contacts, does not gate manual mail use, and would have doubled the PR. Worth its own issue.
Known follow-up: Message.placements costs one placements_of query per message in ListMessages — correct but N+1; a per-page batch query is the obvious fix if page latency matters.
Implemented in #417, merged to `dev`.
The engine could already execute verified remote mutations; nothing could ask it to. Now: `MoveMessage`, `DeleteMessage`, `FlagMessage` on `Mail`, each addressing a specific **placement** rather than just a message key — a message can occupy several mailboxes and a MOVE/DELETE against the wrong occupancy is destructive. Plus `ListConflicts`/`ResolveConflict` over a durable conflict store, `Conflict` as a typed `ErrorReason` (append-only) so older clients take the error path, `Message` carrying its full placement set, and `filters_enabled` on the wire so filter ownership no longer requires hand-editing SQLite.
CLI verbs ship alongside (+513 lines) — an RPC nobody can invoke would not meet this repo's definition of done. Proto change is strictly additive: nothing removed, nothing renumbered; descriptor golden regenerated per the test's own instructions.
**Deferred:** DAV conditional write-back. It is calendar/contacts, does not gate manual mail use, and would have doubled the PR. Worth its own issue.
Known follow-up: `Message.placements` costs one `placements_of` query per message in `ListMessages` — correct but N+1; a per-page batch query is the obvious fix if page latency matters.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
The contract half. Gates the M7 freeze.
MailhasListFolders/ListMessages/GetMessage/MarkRead/SearchMessages/SendMessage/Sync— no Move, Delete, or Flag. The only way to move mail today is a filter rule. There is no DAV write path at all.Contract
Mail.Move/Delete/SetFlags(wire-additive; avoid colliding withDeleteRuleRequest).ERROR_REASON_CONFLICT = 13— an error, not a response field, so old clients take the error path instead of reading a conflict as success. Breaks the exhaustive match inerrors.rsat compile time, which is the good failure mode.Mail.ListConflicts/ResolveConflict{retry|discard}over a durableconflictstable. Conflicted rows currently have no listing RPC, no GC, no retention.Eventgains placement-change and conflict arms (tags 10, 11 free);GetStatusgains per-collection sync state (11 free).Subscribeneeds a monotonic sequence andsince_seq. It is a rawBroadcastStreamthat silently drops events onLagged, so every event-driven guarantee is currently best-effort.Semantics
Mutations are desired end-state on an identity, not imperative operations on a placement, so re-enumerate-and-retarget preserves intent. Cap re-targets at 2, then hard conflict. Events are an optimization over a queryable source of truth, not the truth.
DAV write-back
If-Match: <etag>on PUT/DELETE, 412 ⇒ conflict. RFC 4791 §5.3.4: a server that rewrites the submitted object MUST NOT return an ETag — and Google/Apple normalise aggressively — soApplied { token: None }must schedule a re-read or the nextIf-Match412s in a self-inflicted loop.⚠️ RFC 6638 scheduling hazard: PUTting a VEVENT with ATTENDEEs on a scheduling-aware server sends real invitations to real people. "Just PUT with If-Match" is not a safe calendar write model.
Also fix here (current honesty violations)
Mail.MarkReadandContacts.CreateContactare silently local-only and report success. These are semantics changes to published RPCs, not additive gaps.Depends on #394 and #391. Serialize the proto tail —
STORY-WORKFLOW.mdpermits one proto story in flight.Design: ADR 0002 · review evidence (PR #385)
Implemented in #417, merged to
dev.The engine could already execute verified remote mutations; nothing could ask it to. Now:
MoveMessage,DeleteMessage,FlagMessageonMail, each addressing a specific placement rather than just a message key — a message can occupy several mailboxes and a MOVE/DELETE against the wrong occupancy is destructive. PlusListConflicts/ResolveConflictover a durable conflict store,Conflictas a typedErrorReason(append-only) so older clients take the error path,Messagecarrying its full placement set, andfilters_enabledon the wire so filter ownership no longer requires hand-editing SQLite.CLI verbs ship alongside (+513 lines) — an RPC nobody can invoke would not meet this repo's definition of done. Proto change is strictly additive: nothing removed, nothing renumbered; descriptor golden regenerated per the test's own instructions.
Deferred: DAV conditional write-back. It is calendar/contacts, does not gate manual mail use, and would have doubled the PR. Worth its own issue.
Known follow-up:
Message.placementscosts oneplacements_ofquery per message inListMessages— correct but N+1; a per-page batch query is the obvious fix if page latency matters.