proto: mutation RPCs, durable conflicts, and DAV conditional write-back #395

Closed
opened 2026-08-10 19:28:50 +00:00 by KofTwentyTwo · 1 comment
KofTwentyTwo commented 2026-08-10 19:28:50 +00:00 (Migrated from github.com)

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 · review evidence (PR #385)

**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)
KofTwentyTwo commented 2026-08-14 23:58:09 +00:00 (Migrated from github.com)

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.

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.
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: KofTwentyTwo/nuncio#395