Beacon Service Overview (Group 22)
The commands a QMail client uses to learn that mail has arrived: a status query, a long-poll with a server-ordered cursor, and a per-device acknowledgement. They replace the notification half of 72 Ping and 73 Peek for clients that opt in, and leave those two commands exactly as they are.
Design-stage — not yet implemented
Group 22 is a proposed command group with a brand-new group ID (22 / 0x16). raidax’s COMMAND_GROUP enum currently ends at DRD = 16; groups 20 and 21 are already reserved on this site for the Mobility and Authority proposals, so 22 is the next free number after them (17–19 are also unused and the protocol owner may prefer one of those). Byte layouts on the three command pages are drafted from the client and server source as of September 2026 (raidax commit bcc93e8) and the analysis in the QMail repository (docs/fable-5.1.beacon-fixes.txt) and have not been verified against an implementation.
Why a separate group instead of new QMail codes
- Role separation. QMail (6) mixes storage (upload, download, object transfer), notification (tell, ping, peek), billing (subscribe, extend) and device linking. Only the beacon RAIDA serves the notification commands. A group of its own gives that role a name, a rate-limit class and a capability answer that do not depend on which storage features a node has.
- Clean capability discovery. A server without the group answers
ERROR_INVALID_COMMAND_GROUP(5) from the dispatcher for every command in it, uniformly and before any handler runs. Adding codes 91–93 to group 6 would instead need per-command probing, would sit next to codes 86–90 that are themselves still rolling out, and would collide: 91 and 92 are already assigned on this site to Object Set ACL and Object Get ACL. - The QMail base is frozen. The pending-changes page treats the group 6 wire formats as a frozen base with opt-in extensions. A new notification contract with a different payload header is a new protocol, not an extension, and belongs outside that freeze.
- Room to grow. Numbering starts at 1, matching how the Mobility and Authority proposals are laid out, and leaves space for beacon-only additions such as device registration or push hints without touching QMail’s crowded 70–90 range.
Problems it solves
These were found by comparing the raidax source with the rest_core client and its logs; details are in the QMail repository’s docs/fable-5.1.peek-or-ping-issues.txt and docs/fable-5.1.beacon-fixes.txt.
| Problem with Ping/Peek today | How group 22 addresses it |
|---|---|
| The cursor is the sender’s clock. A future-dated sender moves the recipient’s cursor past other senders’ mail. | Wait uses a cursor assigned by the beacon from its own publication order. Sender clocks never enter the filter. |
| A batch cut at the 1 MiB buffer, or by the single-doubling buffer growth, sets no flag; the one-byte count wraps past 255; the same cursor returns the same prefix, so a large backlog cannot be paged. | Wait returns total_tells, a truncated flag and next_cursor for the last record included; records are sorted by cursor; a page boundary inside one second is representable. |
| The server holds a Ping for 15–16 minutes; the client’s socket budget tops out at 15. Every idle poll expires on the client and is logged as an error. | Wait carries max_hold_seconds; the server expires at that deadline. Beacon Status reports the server maximum and whether expiry is per-watcher or swept. |
| A Tell that lands between the empty scan and the watch registration waits for the next poll. | Wait arms the watcher before its final scan. |
| No signal distinguishes “no mail” from “beacon unreachable”; health is inferred from UDP echo. | Beacon Status is an authenticated TCP round trip with counts and server time; a refusal is unambiguous. |
| Retained Tells are replayed to every device on every poll that starts at or before their timestamp. | Ack records per-device delivery; Wait with hide_acked omits them for that device only. Retention is unchanged. |
| Whether a server supports a new command can only be learned by sending it. | Beacon Status returns a bit set of supported beacon commands. |
Commands
Shared framing
Every group 22 command uses the standard 32-byte RAIDA request header, AES-128-CTR type 1 encryption keyed by the mailbox coin’s AN for this RAIDA, and the same 48-byte qmail_preamble_t (challenge/CRC + mailbox identity + AN) that every QMail command carries, so the existing decrypt, CRC and AN checks apply unchanged. Preamble byte 31, ignored by QMail, is the polling device’s ID here.
| Code | Name | Blocking | Purpose |
|---|---|---|---|
| 1 | beacon_status | No | Supported commands, server time, retained and unacknowledged counts, newest cursor, hold and cap limits, parked polls. Sent at start-up and on the health refresh. |
| 2 | wait | Up to max_hold_seconds | Long-poll (or immediate with no_park) returning retained Tells after an opaque server cursor, sorted, with total, truncation flag and next cursor. |
| 3 | ack | No | Per-device, idempotent acknowledgement of committed Tells by GUID and version. Never deletes; only affects what Wait shows this device. |
Client flow
- Start-up, per beacon RAIDA: send beacon_status. If it is refused with status 5, use Ping/Peek for the session. Otherwise read
commands,max_hold_secondsandsweep_seconds, and compareserver_timewith the local clock. - Catch-up: send wait with the stored cursor (zeros on a fresh mailbox, or
newest_cursorfrom status to skip the backlog) andno_park = 1. Persist every record, then storenext_cursor. Repeat whiletruncatedis set. - Listen: send wait with
no_park = 0and a hold below the socket timeout. On any reply, persist records, store the cursor, and send the next wait immediately. The server does the waiting; the client never sleeps between polls. - Acknowledge: after the local database commit, batch the committed GUIDs into ack. Later wait calls set
hide_acked. - Health refresh: send beacon_status on the existing cadence; a refused connection here is the “beacon unreachable” signal.
Compatibility
- Ping and Peek are not modified. Legacy Ping stays non-destructive whether or not Ack is deployed, as the retained-notification amendment requires.
- A client that never sends group 22 sees no change. A server without group 22 answers 5 and the client falls back.
- Cursors are scoped to the RAIDA that issued them and carry its ID; a cursor from RAIDA 11 is rejected by RAIDA 14.
- Acknowledgements are advisory and expire with the Tell they reference. Retention is a server policy that no group 22 command shortens.
Invariants
Rules every implementation must keep
- A Tell is never deleted or hidden from another device because one device fetched or acknowledged it.
- A client persists records before it persists the cursor. A crash between the two replays; the reverse order loses mail.
- A reply that includes records always includes the cursor of the last one. A reply that includes none returns the request cursor unchanged.
- An expired or pre-rebuild cursor is answered from the oldest retained record with
cursor_expiredset, never rejected. - The count of records in a reply is authoritative; a client validates that the payload was consumed exactly.