Beacon Ack — Group 22, Code 3 (proposed)
A device tells its beacon RAIDA that it has durably stored one or more Tells. The server records the acknowledgement for that (mailbox, device) pair only. Nothing is deleted and retention is not shortened; the only effect is that a Wait (group 22, command 2) from the same device with hide_acked set no longer returns those Tells. Other devices on the same mailbox are unaffected.
Status of this proposal
Not implemented
Group 22 is not in raidax or rest_core. This command is the “negotiated per-device durable ACK” that the retained-notification amendment lists as follow-on work. This page is the design to review. Legacy Ping and Peek stay non-destructive whether or not Ack is deployed.
Why it is needed
Since Tells are retained for 30 days and fetching no longer deletes them, a client sees the same notifications again on every poll that starts at or before their timestamp. Today the client hides that by deduplicating against its local database, which works but has costs:
- Every poll on a busy mailbox carries the backlog. A fresh cursor, a cursor held back for skew, or any batch that was cut returns Tells the device already has. On a mailbox with hundreds of retained Tells that is hundreds of records per poll, against a 1 MiB cap.
- The server cannot tell delivered from undelivered. There is no signal from any device that a Tell reached it, so the server can neither report delivery to an operator nor prioritise Tells that no device has fetched.
- Multi-device mailboxes have no per-device state. A phone and a desktop polling the same mailbox each need their own view of “what is new to me”. Only the server can hold that view; a per-device cursor alone cannot express “I have this one but not that older one”.
Ack gives the server exactly that signal, scoped to a device, without touching the retention or the legacy commands. It is idempotent and batched so a client can send it after each local commit or once a minute.
Quick reference
| Command Group | 22 (Beacon), proposed |
| Command Code | 3 (0x03) |
| Server function | cmd_beacon_ack in a new cmd_beacon.c (to be written) |
| Request body | Variable: preamble(48) + N × 20-byte entries + terminator(2), N = 1–64. The count is derived from the body length (must be a whole number of 20-byte entries). Maximum 1330 bytes. |
| Device | Preamble byte 31 (Device ID), 1–255, chosen by the mailbox owner and carried inside the AN-authenticated, encrypted body. Zero is treated as 1. |
| Success response | Status 250 with an encrypted N-byte payload: one result code per entry, in request order. |
| Semantics | Records (mailbox, device, GUID, version) as delivered. Idempotent: repeating an Ack is a no-op that still returns success. Does not delete, does not shorten retention, does not affect other devices. |
| Encryption | Required — AES-128-CTR, type 1, keyed by the caller’s selected coin AN for this RAIDA |
Wire packet
QMail Ack request packet, bytes on TCP
+-------------------------------+-----------------------------------------+----------------------+
| RAIDA request header | encrypted request body | plaintext terminator |
| 32 bytes | 48 + N*20 bytes | 2 bytes |
+-------------------------------+-----------------------------------------+----------------------+
| command group=22, command=3 | challenge/CRC(16) | 3E 3E |
| encryption type=1 | QMail identity/auth block(32) | |
| body_size=50+N*20 | entry 1: email_id(16) version(4) | |
| nonce(8) | entry 2 ... entry N | |
+-------------------------------+-----------------------------------------+----------------------+
The 32-byte request header is the standard header used by every QMail command (see the Ping page) with command group 0x16, command code 0x03 and a body size of 50 + 20N.
Request body (50 + 20N bytes)
Decrypted request body layout, N = 2 shown
+-------------------+--------------------------+------------------------+------------------------+------------+
| challenge/CRC | QMail identity/auth | entry 1 | entry 2 | terminator |
| 16 bytes | 32 bytes | 20 bytes | 20 bytes | 2 bytes |
| encrypted | encrypted | encrypted | encrypted | plaintext |
+-------------------+--------------------------+------------------------+------------------------+------------+
| body[0..15] | body[16..47] | [48..67] | [68..87] | [88..89] |
+-------------------+--------------------------+------------------------+------------------------+------------+
| Body offset | Size | Encrypted? | Field | Description |
|---|---|---|---|---|
| 0–15 | 16 | Yes | Challenge / CRC32 | As for every QMail command. |
| 16–30 | 15 | Yes | Session ID, Coin Type, mailbox DN and SN | The mailbox whose Tells are being acknowledged. Same layout as Ping. |
| 31 | 1 | Yes | Device ID | The acknowledging device, 1–255. An Ack is scoped to this value; the same GUID acknowledged from device 1 is still returned to device 2. The shipped rest_core client writes 1 here already. Zero is treated as 1. |
| 32–47 | 16 | Yes | Authenticity Number | The mailbox coin AN for this RAIDA. Only the mailbox owner can acknowledge its Tells. |
| 48 + 20k | 16 | Yes | entry k: email_id | The Tell’s GUID (file header bytes 0–15 of the record received from Wait/Ping/Peek). |
| 64 + 20k | 4 | Yes | entry k: version | Big-endian. The publish_time field of the Wait cursor under which this Tell was received (bytes 2–5 of that cursor), or the Tell header timestamp (bytes 24–27) when it came from Ping/Peek. Identifies the published version: the server records an Ack against the retained version whose publish time is ≤ this value, and a later republication of the same GUID is a new version not covered by the Ack. |
| last 2 | 2 | No | Terminator | Fixed 3E 3E. |
Response packet
QMail Ack success response, bytes on TCP
+-------------------------------+-----------------------------------------+----------------------+
| RAIDA response header | encrypted payload | plaintext terminator |
| 32 bytes | N bytes | 2 bytes |
+-------------------------------+-----------------------------------------+----------------------+
| status=250, group=6 | result[0] result[1] ... result[N-1] | 3E 3E |
| body_size=N+2 | | |
+-------------------------------+-----------------------------------------+----------------------+
| Result byte | Meaning |
|---|---|
0x00 | Acknowledged (newly recorded). |
0x01 | Already acknowledged by this device (idempotent repeat). |
0x02 | Unknown GUID for this mailbox, or the retention window has expired. Nothing recorded; the client may drop its local pending marker. |
0x03 | Version mismatch: the Tell has been republished since the version acknowledged. The Ack applies to the old version only; the new version will be delivered by Wait. |
The whole request is authenticated once; individual entries never fail the batch. The status is 250 whenever the preamble validates, and per-entry outcomes are in the payload.
Semantics
- Scope. An acknowledgement is a row (mailbox, device_id, email_id, version) in the beacon’s retention index. It has no effect on any other device and no effect on the file itself.
- Retention. Unchanged. A Tell acknowledged by every known device still lives until its 30-day expiry. The amendment’s rule that fetch masks are not durable acknowledgements is kept: only an explicit Ack counts, and only for the device that sent it.
- Effect on Wait. A Wait from the same device with
flags.hide_ackedomits acknowledged versions and does not count them intotal_tells. A Wait without that flag, and any Ping or Peek, behaves as if no Ack existed. - Recovery. The acknowledgement rows are advisory. If the client loses its database it starts with a zero cursor and
hide_acked = 0and receives everything retained. - Timing. The client sends Ack only after its local database commit, never from a callback that precedes the commit. Sending it late is safe; sending it early loses mail on a crash.
- Device retirement. A device that stops polling leaves rows that expire with the Tells they reference. No separate cleanup is needed.
Status codes
| Decimal | Hex | Symbol | Meaning |
|---|---|---|---|
| 250 | 0xFA | STATUS_SUCCESS | Request accepted; per-entry results in the payload. |
| 5 | 0x05 | ERROR_INVALID_COMMAND_GROUP | Server build has no group 22. The client stops sending Ack to this beacon for the session. A server with the group but without this command answers ERROR_INVALID_COMMAND (6). |
| 8 | 0x08 | ERROR_COIN_NOT_FOUND | Mailbox (denom, SN) is not loaded on this RAIDA. |
| 16 | 0x10 | ERROR_INVALID_PACKET_LENGTH | Body is shorter than 70 bytes, longer than 1330, or not 50 + 20N. |
| 33 | 0x21 | ERROR_INVALID_EOF | Terminator was not 3E 3E. |
| 34 | 0x22 | ERROR_INVALID_ENCRYPTION | Body could not be decrypted or validated. |
| 37 | 0x25 | ERROR_INVALID_CRC | Challenge CRC mismatch. |
| 194 | 0xC2 | ERROR_FILESYSTEM | Retention index could not be read or written. No entry was recorded. |
| 200 | 0xC8 | ERROR_INVALID_AN | Body AN does not match the stored AN. |
Compatibility
- Existing clients: unaffected. They never send group 22 and never set
hide_acked, so they keep seeing every retained Tell. - Existing servers: answer 5. The client treats that as “Ack unsupported here” and relies on local deduplication, exactly as today. Beacon Status reports support in advance.
- Device identity: the Device ID is authenticated in the sense that only the mailbox owner (AN holder) can send it, but it is chosen by the client. Two installs that pick the same value share one acknowledgement view. The client must generate a stable per-install value; rest_core’s hard-coded 1 is not sufficient and must change before it uses Ack.
- Legacy Ping/Peek: never consult Ack rows. This keeps the amendment’s promise that legacy Ping stays non-destructive and complete.
- Storage: one small row per (device, Tell) in the retention index; bounded by 255 devices × retained Tells per mailbox and reaped with the Tell.
Common mistakes
Acknowledging from the receive callback
The callback runs before the database commit in rest_core. Send Ack from the code path that observes the commit, or on a timer that reads committed rows.
Expecting Ack to delete
It does not. Retention is a server policy; Ack only changes what this one device is shown by Wait with hide_acked.
Sharing one Device ID across devices
Two installs that both send device_id = 1 will hide each other’s deliveries once either acknowledges. Each install must choose a distinct, stable value; rest_core currently hard-codes 1 and must be changed before it uses Ack.