Skip to content

Catalog (L1)

1. Mutation pipeline and catalog

A UI mutation follows this shape:

UI runMutation(operation, clientMutationId)
 -> runtime validates idempotency and authorization/context
 -> runtime persists idempotency mapping and mutation record
 -> mutation coordinator applies local effect when allowed
 -> state assertion and pending mutation state are emitted
 -> provider command or sync work is queued/executed when required
 -> provider result settles, confirms, or rejects the pending mutation
 -> runtime emits settlement state

operation is the typed MailOperation, defined once in posthaste-contract-core and parsed once per wire crossing; dispatch is an exhaustive match, not a string lookup. The catalog below is that enum's variant list — the per-action names (message.setReadState, …) are the variants' wire serialization, not stringly dispatch keys.

Accepted mutations are durable runtime state. The runtime records the client id, runtime mutation id, the operation (and its args hash for idempotency comparison), submitter link, affected resources, provider work state, and terminal outcome. Settlement progresses through the states defined by the adapter (adapter L1 §5); the pipeline internals and reconciliation are mutations L2.

2. Mutation argument rules

Mutation args carry explicit account/source context and resource identifiers. A message mutation may target one or more messages, but every target is account-scoped. Args express user intent, not transport commands: archive and trash may target roles and the runtime resolves the current account mailbox for that role, so the renderer is not required to look up role mailboxes before submitting ordinary actions.

A repeated clientMutationId with the same operation payload returns the existing runtime mutation record or terminal result. The same id with a different operation payload is rejected as invalid_mutation.

3. Local effect policy

A local effect is the pure predictor for a named mutation: a deterministic function over replicated mail state that produces the optimistic effect. It is applied as a pending mutation folded over the authoritative projection (an overlay), not an in-place write. Local effect does not mean provider success.

The local effect is defined once and shared by every node that runs it (replication L1 §4.3, single-local-effect): in-process in the embedded authority server, and the same definition compiled to a portable form (e.g. WASM) in a client-layer replica node. A named mutation must not carry two hand-written predictors.

Folding produces pending markers and recomputed views immediately. The optimistic effect is retired only when the confirmation watermark passes the mutation; because state-assertion effects are idempotent, the base-updated-and-still-folded interval is invisible. Provider rejection or timeout settles through the same mutation record and reverts the view to authoritative state.

4. Message keyword mutations

Mutation Args Local effect Provider work Failure/settlement
message.setKeywords targets, keyword command {add, remove} Apply the add/remove keyword sets to local state (set semantics: adding a present or removing an absent keyword is a no-op). The keyword primitive — setReadState/setFlaggedState reduce to it via $seen/$flagged toggles. Set provider keyword state. Retry transient; on permanent rejection refresh affected messages and settle failed.
message.setReadState targets, read: boolean Add/remove the provider seen keyword in local state. Set provider keyword state. Retry transient; on permanent rejection refresh affected messages and settle failed.
message.setFlaggedState targets, flagged: boolean Add/remove the provider flagged keyword. Set provider keyword state. Retry transient; on permanent rejection refresh and settle failed.
message.setUserTags targets, normalized tag list or add/remove delta Update non-system user keywords in local state. Set provider labels/keywords per provider policy. Invalid tags reject before local effect; provider mismatch settles failed.

The runtime owns keyword normalization for system keywords and user tags; renderer-side validation/completion is allowed but the runtime is the authority.

5. Message mailbox mutations

Mutation Args Local effect Provider work Failure/settlement
message.moveToMailbox targets, mailbox id Replace/update mailbox membership per the account mailbox model. Apply provider mailbox/label move. Missing/inaccessible mailbox rejects before local effect; provider rejection refreshes and settles failed.
message.moveToRole targets, role: inbox/archive/trash/junk Resolve role mailbox and update local membership. Apply provider move. Missing role mailbox rejects before local effect.
message.replaceMailboxes targets, full mailbox id list Replace the message's mailbox membership with the given list (full desired-state replace) — the effect that role moves and snooze/unsnooze resolve to. Apply provider mailbox/label state. Provider rejection refreshes runtime state and settles failed.
message.destroy targets Mark pending destruction and remove from active ordinary list windows. Permanently delete through provider when supported. Not invertible by default; provider rejection restores/refreshes runtime state and settles failed.
message.applyDiff sourceId, messageId, diff {keywords,mailboxes}{added,removed} Apply an invertible keyword/mailbox diff as a pending mutation — the undo/redo vehicle. Undo/redo history is client-owned (synced-history design): the client captures the diff + navigates a local cursor, dispatching applyDiff(inverse(diff)) (undo) or applyDiff(diff) (redo). The runtime applies it through the normal mutation pipeline without navigating or recording any history. Apply the equivalent keyword/mailbox changes on the provider. Same as the underlying keyword/mailbox mutations it represents.
message.snooze targets, until (unix seconds, UTC) Resolve the account's snooze-role mailbox and replace local membership with it (no optimistic fold when no mailbox carries the snooze role); record the return time durably. Apply the provider move; the timed return is authority-server scheduler work, not provider work. Rejects when no mailbox has the snooze role (the user must designate one); provider rejection refreshes and settles failed.
message.unsnooze targets Resolve the account's inbox-role mailbox and replace local membership with it; the snooze row clears when the message leaves the snoozed mailbox. Apply the provider move. Rejects when no mailbox has the inbox role; provider rejection refreshes and settles failed.

Mailbox mutations are invertible (a membership replace); the client captures the before/after diff for undo. Permanent destroy/discard are non-invertible and out of scope, same as before.

Snoozed messages return automatically: the authority server's supervisor runs a periodic snooze-scheduler tick that moves every due message back to the inbox as an ordinary authoritative role move. The auto-return is not user-initiated and records no undo step.

6. Draft, compose, and send mutations

Mutation Args Local effect Provider work Failure/settlement
draft.save draft id/intent, headers, recipients, body, attachments Persist draft locally, update body/attachment tokens and draft views. Save/update provider draft when supported; else queue sync work. Local validation failures reject before effect; provider failures leave the draft durable with failed settlement and queued reconciliation work.
draft.discard draft id Mark discarded locally and remove from compose/draft views. Delete provider draft when one exists. Provider failure queues reconciliation or settles failed while preserving recoverability.
message.send draft id or complete send request, optional expected draft revision Persist pending send/outbox state. Does not assert sent state until provider confirms. Submit through provider send path, then reconcile sent/draft state. Duplicate prevention relies on durable idempotency; provider rejection keeps/restores editable draft with failed settlement.

message.send is the strictest idempotency case: the runtime persists the accepted mutation record before provider submission, so retrying the same clientMutationId cannot send a second message.

7. Settings, account, and support mutations

Mutation Args Local effect Provider work Failure/settlement
settings.patch sparse application settings patch Persist settings and update settings views. None unless the setting schedules runtime work. Invalid patch rejects before effect; runtime-work failures become status/settlement.
account.create account config and secret instruction Persist config/secret, create account status, update views. Start/verify provider runtime per policy. Secret/config failures reject before effect; provider failures become account status.
account.patch account id, sparse config/secret patch Persist changes and converge account runtime. Restart/reconnect/verify when affected fields require it. Failed reconnect becomes account status; invalid config rejects before effect.
account.enable / account.disable account id Toggle enabled state and update status. Start/stop provider runtime. Stop/start failures settle through runtime status.
account.delete account id, retention choice Remove config, stop runtime, update views, apply local-data retention. Provider work only when the contract requires it. Local delete leaves recoverable cleanup status if background cleanup fails.
sync.trigger account id, mode Record sync request/status. Account runtime performs sync. Trigger rejection/sync failure is runtime status, not renderer-owned retry.

Settings/account mutations may be local-only, but they still use named mutation records when invoked through the renderer adapter so settlement and multi-window updates are consistent. Retrying a failed durable outbox operation is not a named mutation: it is retry_operation, an operation-lifecycle link op (POST …/operations/{id}/retry) specced with the outbox lifecycle.

8. Assertions

ID Sev. Assertion
message-keyword-catalog MUST Read, flag, and user-tag actions are named keyword mutations with runtime-owned keyword normalization.
message-mailbox-catalog MUST Archive/trash/restore intents (as role moves), move, and destroy actions are named mailbox/destruction mutations with runtime-resolved role moves.
draft-compose-catalog MUST Draft save/discard and send are named mutations with durable local draft/outbox state.
send-idempotency-before-provider MUST Send mutations persist their accepted mutation record before provider submission.
settings-account-catalog MUST Renderer-initiated settings, account, and sync actions use named runtime mutation records.
undo-runtime-backed MUST Undo/redo are ordinary message.applyDiff mutations applied through the same pipeline as any user action. Undo/redo history is client-owned (the client captures invertible diffs + navigates a local cursor; see synced-history design); the runtime applies the diff without owning or navigating a history stack.
mutation-local-phase MUST Mutations with local effects update runtime state before provider reconciliation completes.
local-effect-is-shared-predictor MUST A named mutation's local effect is one pure predictor folded over the projection, defined once and shared (in-process or WASM), not an in-place projection write or a per-language duplicate.
mutation-argument-account-scoped MUST Mutation args carry explicit account/source context; a repeated client mutation id with the same operation payload is idempotent and with a different payload is rejected.