QMail Object Extend Retention — Group 6, Code 90
Buys more storage time for an already-committed object by moving its expires_at forward. Storage is prepaid at object_begin; this command is how a client adds time later without re-uploading the payload.
IMPLEMENTED, NOT YET DEPLOYED
This command is implemented and dispatched in the canary build on RAIDA 11. It is not on the production fleet: every deployed RAIDA still answers Group 6 Code 90 with ERROR_INVALID_COMMAND (6). Treat this page as the agreed contract for the command, and see Pending Protocol Changes for the rollout state of the whole group.
Quick reference
| Command Group | 6 |
| Command Code | 90 |
| Server function | cmd_qmail_object_extend |
| Body layout | QMail preamble (48) + payload (47) + terminator (2) = 97 bytes |
| Transport | UDP or TCP |
| Encryption | As for any other QMail command |
| Authorisation | The recorded object owner only |
| Idempotent | No — each accepted call charges and extends again |
Not part of the Object Transfer framing family
Despite operating on objects, command 90 is an ordinary QMail command. It does not use the 32-bit-length request framing that commands 76–84 use, it carries no 16-byte common prefix, and it is not restricted to TCP or to encryption type 1. Its shape is the same as Subscribe (85): preamble, payload, terminator.
Purpose
Every committed object carries an absolute expires_at. After that moment reads return ERROR_FILE_NOT_EXIST and the object becomes eligible for physical removal by the server's reaper. Storage duration is prepaid at upload, so without this command the only way to keep an object alive longer than its original retention would be to upload the whole payload again — unacceptable for a multi-gigabyte attachment.
object_extend_retention charges for the additional time only and moves expires_at forward. The payload is untouched: no bytes move, the object's generation does not change, and its hash stays the same.
Request body
Preamble (48 bytes, offsets 0–47)
Standard QMail preamble. See QMail Overview — Universal preamble. The preamble coin identifies and authenticates the caller, and its (denomination, serial number) must match the object's recorded owner.
Extend payload (47 bytes, offsets 48–94)
| Offset | Size | Field | Description |
|---|---|---|---|
| 48–63 | 16 | object_id | The committed object whose expiry is being moved. |
| 64 | 1 | file_type | Same file_type used at object_begin. 0 = meta, 1 = qmail body, 10+ = attachments. |
| 65–72 | 8 | generation | Big Endian. The generation being extended. |
| 73 | 1 | duration_code_plus1 | The ADDITIONAL time to buy, using the shared duration-code table shifted by one. 0x01 = forever, 0x02–0x4C = codes 1–75. 0x00 is invalid here (there is no legacy seconds field on this command) and returns ERROR_INVALID_PARAMETER. See the duration-code table. |
| 74–89 | 16 | locker_code | Payment locker funding the extension. All zeros means "fund from the mailbox's prepaid subscription credit". A non-zero locker is not yet supported on this command and returns ERROR_PAYMENT_REQUIRED once billing is enabled; subscription credit is the funding path for now. |
| 90–94 | 5 | reserved | Must be zero. |
| 95–96 | 2 | Terminator | Fixed clear-text terminator. |
Response body
On success the response payload is exactly 16 bytes. There is no common prefix and no object_id echo — the caller correlates by request, as with every other ordinary QMail command.
| Offset | Size | Field | Description |
|---|---|---|---|
| 0–7 | 8 | new_expires_at | Big Endian absolute Unix seconds after the extension. 0 means the object now never expires. |
| 8–15 | 8 | amount_charged_units | Big Endian. What this call actually cost, in byte-periods — the object's stored size on this server multiplied by the number of billing periods bought. 0 while duration billing is switched off, which is its state on every server today. |
Pricing
The charge covers the added time only, at this RAIDA's configured billing period. It is computed on the object's stored size for this server's stripe — not the logical file size, which the server never sees. The number of periods is ceil(added_seconds / fee_period_seconds), and "forever" is billed at the 30-year figure rather than being free.
Charging draws down the mailbox's prepaid subscription balance on this server. While duration billing is off, the billing period is zero, which means one period, and nothing is deducted.
Extending an object that has already expired
Once expires_at has passed, the object is eligible for physical removal and its lifecycle record may already be gone. If the record has been removed the request fails with ERROR_OBJECT_NOT_COMMITTED. Clients must not rely on a grace period — extend before expiry, not after.
Forever
duration_code_plus1 = 0x01 asks for perpetual storage. Operators may refuse it entirely, and it is refused by default, in which case the call returns ERROR_RETENTION_UNAVAILABLE. Where it is permitted it is billed at the 30-year rate.
An object that is already perpetual
If the object's expires_at is already 0 there is nothing to extend. The call succeeds, reports new_expires_at = 0 and amount_charged_units = 0, and changes nothing.
Status codes
| Code | Symbol | Meaning |
|---|---|---|
| 250 | SUCCESS | Expiry moved; the amount charged is in the response. |
| 16 | ERROR_INVALID_PACKET_LENGTH | The body is not exactly 97 bytes, or the terminator is wrong. |
| 40 | ERROR_INVALID_SN_OR_DENOMINATION | The preamble denomination is outside −8 to +6. |
| 8 | ERROR_COIN_NOT_FOUND | The preamble coin does not exist on this RAIDA. |
| — | ERROR_INVALID_AN | The preamble Authenticity Number does not match this RAIDA's record. |
| 198 | ERROR_INVALID_PARAMETER | duration_code_plus1 is 0x00, or a reserved byte is non-zero. |
| 240 | ERROR_RETENTION_CODE_INVALID | duration_code_plus1 decodes to a reserved code. This is a client bug, not a transient — do not retry. |
| 235 | ERROR_RETENTION_UNAVAILABLE | The code is valid but longer than this operator sells, or "forever" is disabled on this node, or the resulting expiry would overflow. Clamp and retry with a shorter code. |
| 228 | ERROR_OBJECT_NOT_COMMITTED | No committed object with this object_id, file_type and generation. |
| 232 | ERROR_NOT_OBJECT_OWNER | The preamble coin is not the object's recorded owner. |
| 169 | ERROR_PAYMENT_REQUIRED | A non-zero locker was supplied. Locker funding is not implemented on this command. |
| 161 | ERROR_DURATION_NOT_FUNDED | Prepaid credit exists and covers the object's bytes, but not this much added time. Offer the user a shorter extension — telling them to buy more credit is the wrong remedy. |
| 170 | ERROR_NO_STORAGE_CREDIT | The mailbox has no prepaid credit on this server. |
| 167 | ERROR_PAYMENT_PROCESSING | The credit ledger could not be updated. Transient; retry. |
| — | ERROR_FILESYSTEM | The new expiry could not be made durable. Nothing changed; any charge is refunded. |
Idempotency
This command is not idempotent. Every accepted call charges again and extends again, so a blind retry after an ambiguous network failure buys a second extension. A client that does not know whether its call landed should read the object's current expires_at with object_info before retrying, and compare it against what it expected.
The one exception is an object that is already perpetual, where the call is a no-op and safe to repeat.
Common mistakes
Sending an absolute expiry instead of a duration
The field is the amount of time to ADD, not the target date. Sending a code that represents "one year" extends by one year from the object's current expiry; it does not set the expiry to one year from now. If the object has already expired, the extension is measured from now instead.
Expecting the Object Transfer framing
Command 90 uses the ordinary QMail request header, not the 32-bit-length framing of commands 76–84, and it has no common prefix. A client that reuses its object-transfer encoder for this command produces a body the server rejects with ERROR_INVALID_PACKET_LENGTH.
Assuming a uniform fleet price
Each RAIDA operator sets their own billing period, so extending the same object across nine servers can produce nine different charges. Sum the per-server amount_charged_units rather than multiplying one server's answer.
Retrying on 240
ERROR_RETENTION_CODE_INVALID means the client sent a reserved code. Retrying sends the same invalid byte. Fix the caller instead.