Beacon Status — Group 22, Code 1 (proposed)
An authenticated, immediate query that tells a client what its beacon RAIDA knows about its mailbox and what the server will do with a long-poll: how many Tells are retained, when the newest was published, the server’s clock, the maximum hold it will honour, and which of the beacon commands this build supports. It never returns Tell records and never parks.
Status of this proposal
Not implemented
Group 22 is not in raidax or rest_core. This page is the design to review. It is the smallest of the three proposed commands, carries the capability bits the other two depend on, and can ship before Wait (group 22, command 2) and Ack (group 22, command 3), because its most useful field is simply “which commands do you support”.
Why it is needed
Today a client can only infer the state of its beacon from side effects, and every inference is wrong in some case:
- Health is judged by UDP echo. rest_core shows “RAIDA 25/25” from echo replies, which says nothing about whether the authenticated TCP notification path works or whether a poll is parked. A beacon that answers echo but refuses Ping looks healthy.
- “No mail” and “not reachable” look identical. A refused connection, a server-side close and a genuine empty poll all reach the client as an empty result. A client cannot show the user “your beacon has been unreachable for an hour”, because it does not know.
- Clock skew is invisible. A client has no way to read its beacon’s clock, so it cannot tell whether its own clock is off. This call measures recipient-versus-server skew only; sender-versus-server skew, which is what breaks the Ping/Peek cursor, is removed by Wait’s server-ordered cursor rather than measured here.
- Hold time and caps are compiled-in constants. The 15-minute hold, the 1 MiB response cap and the 255-record limit are not discoverable. A client that guesses wrong times out or strands mail.
- Capability discovery is by trial. The only way to learn whether a server supports a new command is to send it and see whether the dispatcher rejects it. With a new command group proposed, one call that answers for all of its commands avoids probing each one per beacon per session.
Beacon Status answers all five in one 50-byte request and a fixed 48-byte reply, cheap enough to send at start-up and on every health refresh.
Quick reference
| Command Group | 22 (Beacon), proposed |
| Command Code | 1 (0x01) |
| Server function | cmd_beacon_status in a new cmd_beacon.c (to be written) |
| Request body | 50 bytes total: encrypted challenge(16) + qmail identity/auth(32), then plaintext terminator(2). Same as a minimum Ping body. |
| Success response | Status 250 with a fixed 48-byte encrypted payload. |
| Blocking | Never. Replies immediately. |
| Side effects | None. Does not read Tell records, does not touch cursors or acknowledgements. |
| Encryption | Required — AES-128-CTR, type 1, keyed by the caller’s selected coin AN for this RAIDA |
Wire packet
QMail Beacon Status request packet, bytes on TCP
+-------------------------------+-----------------------------------------+----------------------+
| RAIDA request header | encrypted request body | plaintext terminator |
| 32 bytes | 48 bytes | 2 bytes |
+-------------------------------+-----------------------------------------+----------------------+
| command group=22, command=1 | challenge/CRC(16) | 3E 3E |
| encryption type=1 | QMail identity/auth block(32) | |
| body_size=50 | | |
| nonce(8) | | |
+-------------------------------+-----------------------------------------+----------------------+
The request header is the standard QMail header (see the Ping page) with command group 0x16, command code 0x01 and body size 00 32 (50).
Request body (50 bytes)
The body is exactly the 48-byte qmail_preamble_t that every QMail command carries, plus the terminator. Authentication is the same AN check as Ping: only the mailbox owner can query its beacon status. Byte 31 (Device ID) is echoed back in the reply so a client can confirm which device identity the server saw.
Decrypted request body layout
+-------------------+--------------------------+----------------------+
| challenge/CRC | QMail identity/auth | terminator |
| 16 bytes | 32 bytes | 2 bytes |
| encrypted | encrypted | plaintext |
+-------------------+--------------------------+----------------------+
| body[0..15] | body[16..47] | body[48..49] |
+-------------------+--------------------------+----------------------+
Response packet
QMail Beacon Status response, bytes on TCP
+-------------------------------+-----------------------------------------+----------------------+
| RAIDA response header | encrypted payload | plaintext terminator |
| 32 bytes | 48 bytes | 2 bytes |
+-------------------------------+-----------------------------------------+----------------------+
| status=250, group=6 | status fields | 3E 3E |
| body_size=50 | | |
+-------------------------------+-----------------------------------------+----------------------+
| Payload offset | Size | Field | Description |
|---|---|---|---|
| 0 | 1 | version | Payload layout version, 0x01. A client that sees a higher value reads only the fields it knows. |
| 1 | 1 | device_id_seen | Preamble byte 31 as the server read it (zero mapped to 1). |
| 2–3 | 2 | commands | Big-endian bit set of beacon commands this build serves: bit 0 = 6/72 Ping, bit 1 = 6/73 Peek, bit 2 = 22/2 Wait, bit 3 = 22/3 Ack, bit 4 = 22/1 Beacon Status (always set), bit 5 = retained Tells (fetch does not delete), bit 6 = per-watcher expiry deadlines (see sweep_seconds). Bits 7–15 reserved, zero. |
| 4–7 | 4 | server_time | Big-endian Unix seconds on the beacon. The client compares with its own clock to measure its own skew from the server. This says nothing about senders’ clocks. |
| 8–11 | 4 | retained_count | Number of Tells currently retained for this mailbox on this server. |
| 12–15 | 4 | oldest_publish | Server publish time of the oldest retained Tell, or 0 when none. |
| 16–19 | 4 | newest_publish | Server publish time of the newest retained Tell, or 0 when none. A client whose cursor is older than this has mail to fetch. |
| 20–27 | 8 | newest_cursor | The Wait cursor of the newest retained Tell, or zeros. Lets a client that only wants “new from now” start Wait without replaying the backlog. Zeros when Wait is not supported. |
| 28–31 | 4 | unacked_count | Retained Tells not acknowledged by the requesting device through Ack (group 22, command 3). Equal to retained_count when Ack is not supported. |
| 32–33 | 2 | max_hold_seconds | Longest hold the server grants a long-poll (900 in the current design). A Ping client sets its socket timeout above this plus the sweep interval; a Wait client sends a value below its own timeout. |
| 34–35 | 2 | sweep_seconds | How often expired long-polls are reaped (60 today), or 0 when the server expires each poll at its own deadline. Worst-case reply latency for an idle poll is max_hold_seconds + sweep_seconds. |
| 36–39 | 4 | max_response_bytes | Tell-list byte cap (1,048,576 today). |
| 40 | 1 | max_records | Tell-list record cap per reply once the server enforces one. Today there is no cap and the one-byte count wraps, so a current server would report 0 here. |
| 41 | 1 | parked_polls | Number of long-polls currently parked for this mailbox, all devices. A client that expects its own poll to be parked and sees 0 knows it has been dropped. |
| 42–43 | 2 | retention_days | Retention window applied to this mailbox’s Tells (30 today). |
| 44–47 | 4 | reserved | Zero. Reserved for a future version. |
How a client uses it
- At start-up, once per beacon: read
commandsto choose Wait or Ping; readmax_hold_secondsandsweep_secondsto set the socket timeout or the Wait hold; compareserver_timewith the local clock and log skew above a threshold. - On the health refresh (rest_core: every 15 minutes): a successful reply is the “beacon reachable and authenticating” signal the status bar lacks today. A refused connection here is unambiguous, unlike an empty poll.
- After a long absence: compare
newest_publishwith the stored cursor. If the backlog is large (retained_countagainstmax_records), the client knows in advance that it must page. - Diagnostics:
parked_pollsshows whether the beacon still holds this client’s connection. A log line pairing the server’s counts with the client’s makes support conversations like the one inbugs/BeaconIssues.txta matter of reading two numbers.
Status codes
| Decimal | Hex | Symbol | Meaning |
|---|---|---|---|
| 250 | 0xFA | STATUS_SUCCESS | Payload follows. |
| 5 | 0x05 | ERROR_INVALID_COMMAND_GROUP | Server build has no group 22. The client assumes Ping/Peek only, a 900 s hold and a 60 s sweep. |
| 8 | 0x08 | ERROR_COIN_NOT_FOUND | Mailbox (denom, SN) is not loaded on this RAIDA. |
| 16 | 0x10 | ERROR_INVALID_PACKET_LENGTH | Body is not 50 bytes. |
| 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 unreadable. Counts could not be computed. |
| 200 | 0xC8 | ERROR_INVALID_AN | Body AN does not match the stored AN. |
Compatibility
- Existing clients: unaffected; they never send 93.
- Existing servers: answer 5. The client falls back to compiled-in assumptions, which is what it does today.
- Rate limiting: the call is cheap (one index lookup, no file reads) but authenticated, so it should sit under the same per-coin limiter as Peek so a misbehaving client cannot use it as a probe.
- Versioning: new fields go into
reservedand bumpversion; the payload length stays 48 until a version 2 defines a longer one.
Common mistakes
Polling it instead of Wait
Beacon Status tells you whether there is mail; it does not deliver it and does not park. Use it for health and set-up, then long-poll with Wait or Ping.
Treating server_time as the cursor
Ping/Peek cursors are sender timestamps, not server time; Wait cursors are opaque. Use newest_cursor to start Wait “from now”, and use server_time only to measure skew.