QMail — Pending Protocol Changes
Review draft, 24 August 2026. Every wire-format and behaviour change queued by the large-attachment work, in one place, with a compatibility verdict for each. This page exists so the changes can be read and approved before they reach the fleet.
September 2026 implementation clarification: large-attachment durability, retry and compatibility checklist. This changes no base packet layout and makes no claim about current fleet deployment.
Historical deployment labels: verify before rollout
The LIVE ON CANARY and SWITCHED OFF labels below record the August 24 review, not a verified current fleet inventory. The September 6 source review finds streaming enabled in the inspected server source; source switches alone cannot establish the binaries or configuration running on production nodes. Verify node build hashes, effective switches and capabilities before relying on any extension. The frozen base specification and vectors remain unchanged.
This page is the review surface. It is deliberately not linked from the command index — a page describing unshipped behaviour is worse than a stale one, because integrators code against it and then fail against production.
Compatibility verdict
No wire-format change on this page breaks a correctly-written existing client. Every new request field lives in a byte that was previously required to be zero, and every new field is opt-in: a client that keeps sending zero gets byte-identical behaviour to today. Every new response field is either behind a flag the client itself set, or in a response byte range that is currently all zeros.
That is the wire. Four changes on this page can still change what an existing client experiences, and they are the ones worth the review time:
| Risk | What breaks | Who is exposed | Gate |
|---|---|---|---|
| R-1 — Legacy uploads start expiring | Files uploaded with cmd 70 / cmd 75 are now recorded in the retention index and deleted when they expire. Today they live forever because nothing ever recorded when they should leave. Default retention is 30 days. | Every existing client and every file already on disk. | Reaper switch (off). |
| R-2 — The duration byte is now enforced | Byte 81 of cmd 70 (byte 65 of cmd 75) has always been documented as a retention code and has always been ignored. It is now decoded. A client that puts anything other than 0x00–0x4B there gets ERROR_RETENTION_CODE_INVALID (240) on an upload that used to succeed. |
Any client sending a non-zero, non-table value. The shipped client sends 0x00, which maps to the server default and is safe. |
None — live as soon as the build ships. |
| R-3 — TCP connections are no longer closed after one response | A client that reads a response by waiting for end-of-stream, rather than by the length in the response header, blocks until the 60-second idle timeout instead of returning. | Any client that reads to EOF. The RAIDA core client reads by declared length and is unaffected. | Keep-alive switch (off). |
| R-4 — begin response bytes 74–79 stop being zero | A client that asserts "the last six bytes of the object_begin response are reserved and must be zero" rejects every welfare and subscription upload once credit reporting is switched on. | Any strict parser. Already documented on the object_begin page. | Subscription switch (off). |
Three of the four are behind an operator switch that is off in the shipped build, so they can be reviewed and staged separately from the deploy. R-2 is not — it goes live with the binary. It is the single item on this page that most deserves a decision before anything ships.
Status legend
| Label | Meaning |
|---|---|
| LIVE ON CANARY | Implemented and active in the canary build with no switch in front of it. Ships the moment the build reaches the fleet. |
| SWITCHED OFF | Implemented, but compiled out or inert in the shipped build. Requires a deliberate per-node flip. |
| RESERVED ONLY | A number or a code name exists, but no server returns it and no handler answers it. Do not build against it. |
Summary table
| # | Command | Change | Status | Breaks an existing client? |
|---|---|---|---|---|
| 1.1 | object_begin (76) | retention_code_plus1 at offset 125 | LIVE ON CANARY | No — zero keeps today's meaning |
| 1.2 | — | Shared duration-code table | LIVE ON CANARY | No — new vocabulary only |
| 1.3 | object_commit (79) | async_ok at offset 89; status 246 | LIVE ON CANARY | No — 246 is never sent unsolicited |
| 1.4 | object_get_range (82) | Verify flag; appended chunk-hash block; status 164 | LIVE ON CANARY | No — only when the client sets the flag |
| 1.5 | Subscribe (85) | v2 payload, 64-bit credit | LIVE ON CANARY | No — v1 payload unchanged |
| 1.6 | object_extend_retention (90) | New command | LIVE ON CANARY | No — new code point |
| 1.7 | Device links (86–89) | Four new commands | LIVE ON CANARY | No — new code points |
| 1.8 | Upload (70), Upload Large (75) | Duration byte decoded; 1 GiB per-GUID cap; retention index | LIVE ON CANARY | Possibly — see R-1 and R-2 |
| 2.1 | object_begin (76), object_put_range (77) | Streaming to disk; statuses 162 and 165 | SWITCHED OFF | No — new failure codes on paths that previously failed differently |
| 2.2 | — | Reaper deletes expired objects | SWITCHED OFF | See R-1 |
| 2.3 | object_begin (76), object_extend_retention (90) | Duration billing | SWITCHED OFF | No while off; a pricing change when on |
| 2.4 | object_begin (76) | Zero-locker funding; statuses 170, 171 | SWITCHED OFF | See R-4 |
| 2.5 | Tell (71), Ping (72), Peek (73) | Subordinate device rewrite; non-destructive Ping | SWITCHED OFF | No while off; changes Ping semantics when on |
| 3 | All TCP commands | Connection reuse | SWITCHED OFF | See R-3 |
1. Wire additions
1.1 object_begin (76) — retention_code_plus1 at offset 125
LIVE ON CANARY. The first byte of the seven reserved bytes at offsets 125–131 now carries a storage-duration code. The remaining six bytes (126–131) are still required to be zero.
| Offset | Size | Field | Description |
|---|---|---|---|
| 125 | 1 | retention_code_plus1 | 0x00 = absent; the legacy requested_retention_seconds at offsets 113–120 governs, exactly as today. 0x01 = code 0 (forever). 0x02–0x4C = codes 1–75. 0x4D–0xFF is rejected with ERROR_RETENTION_CODE_INVALID (240). |
| 126–131 | 6 | reserved | Must be zero. Unchanged requirement, one byte shorter. |
When a code is present it wins over the seconds field; the seconds field is not consulted. A code longer than the operator's configured maximum, or a request for "forever" on a node that does not sell it, returns ERROR_RETENTION_UNAVAILABLE (235) — the code is valid, the operator just does not offer it. Clamp and retry with a shorter code rather than failing the upload.
Why the value is shifted by one
Code 0 means "store forever". If the code travelled unshifted, the 0x00 that every existing client already sends in this reserved byte would read as a request for perpetual storage on every upload. The +1 shift makes a legacy zero mean "absent" and makes "forever" impossible to request by accident. A client must never send a bare zero to mean code 0.
Compatibility: safe. A client that keeps sending seven zero bytes takes the legacy path unchanged. The only behavioural difference for such a client is that byte 125 is no longer validated as zero — a request that used to be rejected with ERROR_INVALID_PARAMETER for a stray non-zero byte there is now interpreted instead. No shipped client sends a stray value.
1.2 The shared duration-code table
LIVE ON CANARY. One byte encodes a retention period. Five contiguous bands, strictly monotonic across codes 1–75, exact integer seconds, no calendar arithmetic: a month is 30 days and a year is 365 days — the same constants the server has always used for its default and maximum retention.
| Code | Meaning | Seconds |
|---|---|---|
| 0 | Retain forever | 0 (sentinel; operator-gated) |
| 1–23 | c hours | c × 3,600 |
| 24–29 | (c−23) days | (c−23) × 86,400 |
| 30–33 | (c−29) weeks | (c−29) × 604,800 |
| 34–45 | (c−33) months | (c−33) × 2,592,000 |
| 46–75 | (c−45) years, to 30 years | (c−45) × 31,536,000 |
| 76–255 | Reserved | Rejected with ERROR_RETENTION_CODE_INVALID (240) |
Monotonicity is deliberate: an operator cap can be applied with a single comparison, and the largest sellable code can be advertised as a single byte.
This table is a contract, not an implementation detail
The server and the client each hold their own copy. If the two disagree by one band boundary, a client asks for six months and is billed and stored for six weeks, and nothing detects it until someone reconciles accounts. The two copies are held to a shared set of golden test vectors, and both sides must change together or not at all.
The same table is used in three places, with one deliberate difference:
- object_begin (76) and object_extend_retention (90) use the shifted form (
code + 1). - Upload (70) and Upload Large (75) use the raw code, because that byte predates the shift — and there
0means "server default", never "forever".
1.3 object_commit (79) — async_ok at offset 89, and status 246
LIVE ON CANARY. Committing a very large object means hashing and flushing every byte. For a 100 GB object that is roughly 43 seconds of hashing plus around 90 seconds of fsync — longer than any reasonable socket timeout. The commit therefore moves to a background worker, and the client is told to poll.
| Offset | Size | Field | Description |
|---|---|---|---|
| 89 | 1 | async_ok | 0 = the client does not understand status 246. The handler blocks until the commit finishes and answers with success or an error, exactly as today. 1 = the client understands 246 and will poll. Any other value returns ERROR_INVALID_PARAMETER (198). |
| 90–95 | 6 | reserved | Must be zero. Unchanged requirement, one byte shorter. |
With async_ok = 1, a commit that cannot complete inline returns STATUS_COMMIT_IN_PROGRESS (246) with no response body. The client then polls object_transfer_status (78) until the transfer reaches its committed state. Re-issuing the commit while the worker is still running returns 246 again; re-issuing it after success returns the original commit response, so a lost 246 is harmless.
246 is not an error and not a retry signal
It means "accepted and running". A client that classifies it as a failure abandons a commit that is about to succeed; a client that treats it as "retry the commit" adds load to a server that is already busy hashing. Poll status instead, and choose the poll interval from the object size — polling a 100 GB commit every 200 ms is several hundred wasted round trips.
Compatibility: safe, and deliberately so. 246 is never sent unsolicited; a client that sends async_ok = 0 can never see it. The trade-off is explicit: such a client keeps today's behaviour, including today's failure mode, where a commit longer than the socket timeout surfaces as a network error and is resumed later.
1.4 object_get_range (82) — verification flag and chunk hashes
LIVE ON CANARY. Today a download is protected by one SHA-256 over the whole object. At 100 GB, a single flipped bit costs a 100 GB re-download and gives no way to tell which server served the bad byte. The server can now publish a per-chunk hash sidecar and hand back the hashes covering the range it just served.
Request — request_flags at offset 81, previously required to be zero:
| Bit | Value | Meaning |
|---|---|---|
| 0 | 0x01 | Verify this range against the server's chunk hashes before serving it, and append the covering hashes to the response. |
| 1–7 | — | Must be zero. Any other bit set returns ERROR_INVALID_PARAMETER (198). |
Response — response_flags gains one bit:
| Bit | Value | Meaning |
|---|---|---|
| 0 | 0x01 | This range reaches the end of the object. Unchanged. |
| 1 | 0x02 | The storage class is volatile. Unchanged. |
| 2 | 0x04 | New. A chunk-hash block follows the data. |
When bit 2 is set, the response body is the fixed 104-byte header, then data_length bytes of payload, then this block:
| Offset in block | Size | Field | Description |
|---|---|---|---|
| 0–3 | 4 | chunk_size | Big Endian. Bytes covered by each hash. |
| 4–7 | 4 | first_chunk_index | Big Endian. Index of the first hash supplied, counting from the start of the object. |
| 8–9 | 2 | count | Big Endian. Number of hashes that follow. |
| 10… | count × 32 | hashes | SHA-256 per chunk, in ascending chunk order. |
Setting the flag also makes the server check itself: it re-hashes the covering chunks before serving them and returns ERROR_OBJECT_CORRUPT (164) rather than handing over bytes it knows are wrong. A clean 164 lets the client heal that stripe from another RAIDA; silently served rot cannot be detected at all.
Hashes are only available for objects stored through the streaming path that carry a sidecar. For an older object, or one held in memory, the flag is accepted, the range is served normally, and bit 2 stays clear. A client must handle "asked for hashes, did not get them".
Read the response by its declared body length, not by data_length
The hash block makes the body longer than 104 + data_length. A client that computes the body length itself instead of using the length in the response header leaves the hash block unread in the socket buffer. On a single-use connection that is merely untidy; combined with connection reuse it desynchronises every subsequent response on that socket. This only affects clients that opt in to the flag — but those are exactly the clients most likely to also want connection reuse.
Compatibility: safe. A client sending request_flags = 0 gets a byte-identical response to today.
1.5 Subscribe (85) — v2 payload with 64-bit credit
LIVE ON CANARY. The v1 credit field is an unsigned 32-bit byte count, capped at about 4.29 GB. That cannot express the price of a single 100 GB upload, let alone one multiplied by a storage duration. A second payload shape carries a 64-bit value.
The two shapes are told apart by the first payload byte, immediately after the 48-byte preamble:
| Payload | Layout | Entry |
|---|---|---|
| v1 (unchanged) | N × 9-byte entries, N = 1–150 | den(1) sn(4 BE) credit(4 BE) |
| v2 (new) | 0x56 0x02 then N × 13-byte entries, N = 1–150 | den(1) sn(4 BE) credit(8 BE) |
The marker byte is 0x56, ASCII 'V'. Read as a signed denomination that is +86, which is outside the valid range of −8 to +6, so no well-formed v1 payload can begin with it and the two shapes can never be confused. A v2 payload whose version byte is not 0x02, or whose entry array is not a whole number of 13-byte entries, returns ERROR_INVALID_PACKET_LENGTH (16).
The credit unit follows the billing mode. While duration billing is off, one unit is one byte, exactly as in v1. When it is on, one unit is one byte-period: an upload of S bytes held for P billing periods consumes S × P units. Balances saturate rather than overflow.
Compatibility: safe. A v1 payload is parsed by exactly the code path it uses today.
1.6 object_extend_retention (90) — new command
LIVE ON CANARY. Buys more storage time for an already-committed object without re-uploading it. Fully specified on its own page: object_extend_retention (cmd 90).
Compatibility: safe. A new command code; a server that does not have it answers ERROR_INVALID_COMMAND (6), which is what clients already see for any unimplemented code.
1.7 Device links (86–89) — four new commands
LIVE ON CANARY. These let one user run QMail on several devices, each holding its own identity coin, so rotating one device's password does not lock out the others. Devices link under an "authority" mailbox — the public address — and a subordinate's coin is a device credential, never a visible identity.
| Code | Command | Caller | Payload after the preamble | Response |
|---|---|---|---|---|
| 86 | Link Invite | Authority | sub_den(1) sub_sn(4 BE) | pairing_code(4 BE) expires_at(8 BE). The code is six digits and lives for ten minutes. |
| 87 | Link Accept | Subordinate | auth_den(1) auth_sn(4 BE) code(4 BE) | Status only. Assigns the lowest free device slot. |
| 88 | Link List | Admin | None | The device roster. |
| 89 | Link Revoke | Admin | sub_den(1) sub_sn(4 BE) | Status only. |
Each device authenticates with its own coin through the ordinary QMail preamble. A maximum of eight devices share one authority, including the authority itself. New status codes: ERROR_LINK_NOT_FOUND (173), ERROR_NOT_LINK_ADMIN (174), ERROR_BAD_PAIRING_CODE (175), ERROR_DEVICE_LIMIT (176).
These four commands are intentionally live while the behaviour that uses them is switched off, so devices can pair before the mailbox rewrite is turned on.
Compatibility: safe. New command codes, and no existing command changes while the rewrite stays off.
These four commands have no reference pages yet
The table above is a summary, not a specification. Each needs its own page before the feature is announced to integrators.
1.8 Upload (70) and Upload Large (75) — the duration byte, a size cap, and expiry
LIVE ON CANARY. This is the item with real client exposure. Three changes to the two legacy upload commands, none of which alters a single byte of the wire format:
The duration byte is now decoded
Byte 81 of cmd 70, and byte 65 of cmd 75, has always been documented as "Storage retention code" and has always been ignored — the current page even says so: "the byte is logged but the server does not yet enforce retention". It is now interpreted, using the shared duration-code table in its raw form:
| Value | Meaning |
|---|---|
0x00 | Server default retention (30 days as shipped). Never "forever" — unlike the object path, because every deployed client already sends this value. |
0x01–0x4B | Codes 1–75 from the table, subject to the operator's maximum. Over the maximum returns ERROR_RETENTION_UNAVAILABLE (235). |
0x4C–0xFF | Reserved. Returns ERROR_RETENTION_CODE_INVALID (240) — an upload that succeeds today now fails. |
R-2: this is the one change that is not behind a switch
Every other risky item on this page can be staged. This one goes live with the binary. The evidence that it is safe is that the shipped client sends 0x00 — visible in the worked example on the Upload page, where the storage-duration byte is annotated as 00. That evidence covers the client we know about. It does not cover a third-party or older client that writes a version number, a flag, or uninitialised memory into a byte the server has never checked. Confirming what every uploader actually sends in that byte is worth doing before this ships, because the failure is a hard 240 on an upload path that has always accepted anything.
A one-gigabyte accumulation cap on cmd 75
Pages uploaded under a single GUID now accumulate against a 1 GiB ceiling. Exceeding it returns ERROR_QUOTA_EXCEEDED (227). The reasoning is that large files ride the object-transfer path (76–84), so a modest ceiling on the legacy paged path constrains only abuse — but any existing caller that assembles more than 1 GiB through cmd 75 stops working at the boundary.
Legacy uploads are registered for expiry
Uploads through cmd 70 and 75 are now recorded in the object lifecycle index with their retention, so the reaper can remove them on schedule. Until now nothing recorded when such a file should leave, and they stayed on disk forever.
R-1: files that never expired will start expiring
Indexing is live; the deletion step is behind the reaper switch. Once that switch is on, legacy uploads are removed at the end of their retention — 30 days by default for the 0x00 that every client sends. Any product behaviour that assumes a QMail attachment is permanent changes. This is a correctness fix for a real defect (unbounded disk growth), but it is a user-visible change in what "uploaded" means, and it applies to files already on disk. Decide the default retention deliberately before turning the reaper on.
2. Behaviour changes behind operator switches
None of these alter a request or response layout. They are listed because they change what a client observes, and because each is a separate flip.
2.1 Streaming storage — statuses 162 and 165
SWITCHED OFF. object_put_range writes into a preallocated staging file instead of holding the object in memory, which is what allows an object larger than RAM. Two new failures become reachable at object_begin:
ERROR_STORAGE_EXHAUSTED(162) — the server physically cannot reserve the space. Not retryable here; pick another server, and pick it before sending payload. Distinct fromERROR_STORAGE_FULL(230), which is a policy refusal: 162 is "cannot", 230 is "will not".ERROR_RESUME_MISMATCH(165) — a re-begin reused a transfer ID but declared a different size, hash, or file type. A client bug or a stale ID, not a transient. Do not retry it in a loop; start a new transfer.
2.2 The reaper deletes expired objects
SWITCHED OFF. The expiry index is always maintained; this switch gates the unlink. Covers expired objects and abandoned staging files. See R-1 for the exposure.
2.3 Duration billing
SWITCHED OFF. Storage is priced by size × duration rather than size alone. The number of billing periods is ceil(retention_seconds / fee_period_seconds), with "forever" billed at the 30-year figure rather than free. As shipped, the billing period is zero, which means one period — the exact pre-existing one-time fee.
Rate and period are per-node operator settings, so the nine QMail servers may legitimately price the same object differently. A client must sum the per-server charges rather than multiplying one server's answer.
The client's fee arithmetic and the server's must agree exactly
Both sides compute the charge independently. A disagreement of one rounding step is silent: no error, no mismatch, nothing to alarm on, until someone reconciles accounts. The two implementations are held to shared golden vectors, and those vectors need to be diffed automatically rather than by inspection.
2.4 Zero-locker funding — statuses 170 and 171
SWITCHED OFF. An upload with an all-zero locker code draws on prepaid credit instead of a funded locker, in a fixed precedence: a free floor for small stripes, then subscription credit, then a rolling welfare allowance, then refusal. Already documented in full on the object_begin page, including the response bytes at 74–79 and the parser change described in R-4. New statuses: ERROR_NO_STORAGE_CREDIT (170), ERROR_WELFARE_EXHAUSTED (171).
2.5 Linked-device mailbox rewrite
SWITCHED OFF. When on, a subordinate device's Ping and Peek serve the authority's inbox, its Tell is stamped as coming from the authority, and — the significant one — Ping stops deleting. Each tell carries a per-device delivery bitmap and is removed once every active device has collected it, or after a 30-day backstop.
That is a real change in Ping's contract, from "fetch and drain" to "fetch and mark". While the switch is off, Ping remains destructive exactly as documented today. The four link commands stay live either way.
3. Transport — TCP connection reuse
SWITCHED OFF. Today the server closes the TCP connection after writing each response. With this on, it keeps the connection open and serves further requests on it.
| Property | Behaviour |
|---|---|
| Idle timeout | 60 seconds from the end of the last response. The server closes silently — no error frame is written into a request the client never sent. |
| Request limit | Unlimited as configured. |
| Pipelining | Supported. A second request already in the socket buffer is served without waiting for another read. |
| Not reused | Connections carrying Ping (72), which is a long poll, and any connection whose framing failed at the header stage — an undrained body would be misread as the next request header. |
R-3: this is the change most likely to surprise a third-party client
A client that reads a response by the length declared in the response header is unaffected; the framing is unchanged and nothing about parsing differs. A client that instead reads until the peer closes the stream will block for 60 seconds on every request. The RAIDA core client reads by declared length and is safe. Any other implementation should be checked before this is turned on anywhere.
Two further conditions for a client that wants to reuse connections: a recv returning zero on a reused socket is normal, not an error — reconnect once and re-send that request; and the response must be drained completely, including the chunk-hash block if one was requested, or the next response on that socket is misframed.
The benefit is on the client side and only materialises once the client also stops opening a socket per request. A 100 GB object moved in 8 MB requests is 12,800 handshakes and 12,800 cold starts otherwise.
4. New status codes
There is no central status-code table on this site — each command page lists the codes it can return. These are the codes the changes above introduce, and where each can actually appear.
| Code | Symbol | Returned by | Client action |
|---|---|---|---|
| 161 | ERROR_DURATION_NOT_FUNDED | 76, 90 | Credit covers the bytes but not the time. Offer a shorter retention. Telling the user to buy more credit is the wrong remedy, which is exactly why this is not 170. |
| 162 | ERROR_STORAGE_EXHAUSTED | 76 | Server cannot hold it. Choose another server before sending payload. |
| 164 | ERROR_OBJECT_CORRUPT | 82 | The server detected its own bit rot. Reconstruct from the other servers and report the bad one. |
| 165 | ERROR_RESUME_MISMATCH | 76 | Client bug or stale transfer ID. Start a new transfer; never loop. |
| 170 | ERROR_NO_STORAGE_CREDIT | 76, 90 | No prepaid credit. Attach a locker or buy credit. |
| 171 | ERROR_WELFARE_EXHAUSTED | 76 | Free allowance used up for this window. Retry later or fund the upload. |
| 173–176 | Device-link errors | 86–89 | See 1.7. |
| 227 | ERROR_QUOTA_EXCEEDED | 75 | GUID over the 1 GiB accumulation cap. Use the object path for large files. |
| 235 | ERROR_RETENTION_UNAVAILABLE | 70, 75, 76, 90 | Valid code, longer than this operator sells. Clamp and re-offer — do not fail the upload. |
| 240 | ERROR_RETENTION_CODE_INVALID | 70, 75, 76, 90 | A reserved code was sent. Client bug — retrying sends the same bad byte. |
| 246 | STATUS_COMMIT_IN_PROGRESS | 79 | Not an error. Poll status(78). Only ever sent to a client that asked for it. |
5. Declared but not implemented
Do not build against anything in this section
These names appear in design notes and in the server's code as reserved constants. No server returns them and no handler answers them. They are listed so that a reader who encounters the name elsewhere knows its status.
| Item | Status |
|---|---|
STATUS_PARTIAL_CONTENT (239) | RESERVED ONLY. Never returned. A short read at end-of-object is already signalled today by data_length being less than requested, with response_flags bit 0 set. Do not write a handler that waits for 239. |
ERROR_ACL_DENIED (166) | RESERVED ONLY. Never returned by any command. |
ERROR_VOLUME_UNAVAILABLE (163) | Reachable only from an internal storage path; not yet a documented outcome of any command. |
| object_set_acl (91), object_get_acl (92) | RESERVED ONLY. The numbers are allocated; there is no dispatch entry, so a request returns ERROR_INVALID_COMMAND (6). Their pages carry a "proposed" banner. |
| DRD server registry (149, 150) | RESERVED ONLY. Same — pages exist for design review, no handler exists. |
| object_capabilities (83) v2 fields | Not implemented. The response is byte-identical to v1. Fields discussed in the design — maximum retention code, fee per period, billing period, chunk hash size, resume support — are not present. A client cannot yet discover any of the features on this page; it must assume they may be absent and degrade. |
| object_transfer_status (78) commit progress | Not implemented. The commit_state and bytes_durable fields discussed alongside status 246 do not exist. Polling works against the existing transfer state, but there is no byte-accurate progress figure to drive a progress bar. |
| Object-transfer framing v2 | Not started. The framing version stays 1; ERROR_UNSUPPORTED_PROTOCOL (219) keeps its current meaning. |
The gap that matters most
Capabilities (83) is unchanged, so there is no discovery mechanism for anything on this page. A client cannot ask a server whether it understands the retention code, the async commit, or chunk hashes — it can only try and interpret the answer. During a mixed-version rollout that is workable precisely because every new field is opt-in and a server that ignores it behaves as it does today. It is not workable for the billing and framing changes, which is why those are not on the near list.
6. Rollout order
The published spec should describe production. The order that keeps that true:
- Approve this page. Nothing below happens first.
- Settle R-2 — confirm what every uploader actually writes into the duration byte, since that change is not behind a switch.
- Settle R-1 — choose the default retention for legacy uploads, knowing it applies to files already on disk.
- Ship the binary with every switch off. At that point the fleet behaves as documented today, apart from items 1.8 and any new command codes, which are additive.
- Fold each approved change into its own command page as its switch is flipped — not before. A page describing behaviour that is not yet live is worse than a stale page.
- Write the four missing device-link pages before that feature is announced.
These pages are outside the build
This directory is not in either source repository and is not touched by any deployment, so nothing forces a page to be updated when the server changes. That is how documentation drifts from behaviour. Each change above needs a named owner for its page before its switch is flipped.