Operation Log & Outbox
The Operation Log is the durable record of everything a device has sent. The Outbox is a view on top of that log — the entries that haven’t been confirmed delivered to the relay yet. Together, they’re what makes guaranteed delivery possible.
The Operation Log
Every blob a device sends is appended to the Operation Log before being pushed to the relay. Each entry records:
object_id— a 32-byte identifier for the logical object this blob belongs to (a clipboard entry, a secret, a document). The caller defines it, the relay never sees it — it lives inside the encrypted payload.sequence— the device’s global send counter at the time of this entry. Bumps once per Envelope, across all objects.blob— the raw blob bytes.delivered— whether the relay has confirmed receipt.
Appending is idempotent. A second write with the same (object_id, sequence) is silently ignored — first write wins. Conflict resolution is your problem, not the log’s.
Global sequence, not per-object
sequence is a single global counter. It bumps once per Envelope sent, regardless of which object the payload is for. If a device sends a clipboard entry (sequence 5), then a manifest update (sequence 6), then another clipboard entry (sequence 7), the log records 5, 6, 7 across different object IDs.
That’s deliberate. Object IDs are caller data that has to stay inside the encrypted payload — the relay must never see them. A per-object counter would need object context in the Envelope header, which would leak structure to the relay. A global counter keeps the header lean.
The trade-off: sequences aren’t contiguous within a single object. An object’s entries might have sequences 3, 7, 11 with gaps where other objects were sent in between. Don’t assume per-object continuity.
The Outbox
The Outbox isn’t a separate data structure. It’s a view over the Operation Log: all entries where delivered = false, sorted by global sequence ascending.
When a device pushes blobs to the relay and the relay confirms receipt, mark_delivered() is called for each entry. That entry drops out of the Outbox. If the push fails — network error, process crash, relay unreachable — the entry sits in the Outbox.
On reconnect, the session replays the whole Outbox to the relay in global sequence order. That replay is the part that makes delivery reliable: blobs don’t get silently lost just because the sender was offline when it first tried.
What the relay holds vs what the device holds
Two different responsibilities, easy to confuse:
The relay holds undelivered Envelopes for offline recipients — blobs addressed to a device that isn’t currently connected. Once the recipient connects, the relay delivers and deletes them.
The device holds undelivered entries in its Outbox for itself as the sender — blobs it hasn’t confirmed pushed to the relay yet. The relay is not the sender’s outbox.
Two independent problems, both have to be solved. At any given moment a blob can be:
- In the sender’s Outbox, never pushed to the relay yet. Relay doesn’t have it.
- In the relay’s Inbox for the recipient, pushed but the recipient is offline. Relay has it; recipient doesn’t.
- Delivered. Recipient has it; relay deleted it.
Sequence counter persistence
The sequence counter must never go backwards or repeat. On process restart, the session reads max_sequence() from the Operation Log and resumes from there. If the log is empty, the counter starts at zero.
This is why session state persistence matters. If the local database is wiped, the counter resets to zero. A recipient seeing a lower sequence from a known device isn’t currently treated as an error in v0 — but it’s a strong signal that the sender’s state was wiped.
Object history
The Operation Log is queryable by object_id: entries_from(object_id, sequence) returns all entries for that object with sequence ≥ the given value, in ascending order. That’s how a caller can reconstruct the history of a specific object — for example, every clipboard entry sent in the current session.