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 Group6
Command Code90
Server functioncmd_qmail_object_extend
Body layoutQMail preamble (48) + payload (47) + terminator (2) = 97 bytes
TransportUDP or TCP
EncryptionAs for any other QMail command
AuthorisationThe recorded object owner only
IdempotentNo — 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)

OffsetSizeFieldDescription
48–6316object_idThe committed object whose expiry is being moved.
641file_typeSame file_type used at object_begin. 0 = meta, 1 = qmail body, 10+ = attachments.
65–728generationBig Endian. The generation being extended.
731duration_code_plus1The 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–8916locker_codePayment 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–945reservedMust be zero.
95–962TerminatorFixed 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.

OffsetSizeFieldDescription
0–78new_expires_atBig Endian absolute Unix seconds after the extension. 0 means the object now never expires.
8–158amount_charged_unitsBig 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

CodeSymbolMeaning
250SUCCESSExpiry moved; the amount charged is in the response.
16ERROR_INVALID_PACKET_LENGTHThe body is not exactly 97 bytes, or the terminator is wrong.
40ERROR_INVALID_SN_OR_DENOMINATIONThe preamble denomination is outside −8 to +6.
8ERROR_COIN_NOT_FOUNDThe preamble coin does not exist on this RAIDA.
—ERROR_INVALID_ANThe preamble Authenticity Number does not match this RAIDA's record.
198ERROR_INVALID_PARAMETERduration_code_plus1 is 0x00, or a reserved byte is non-zero.
240ERROR_RETENTION_CODE_INVALIDduration_code_plus1 decodes to a reserved code. This is a client bug, not a transient — do not retry.
235ERROR_RETENTION_UNAVAILABLEThe 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.
228ERROR_OBJECT_NOT_COMMITTEDNo committed object with this object_id, file_type and generation.
232ERROR_NOT_OBJECT_OWNERThe preamble coin is not the object's recorded owner.
169ERROR_PAYMENT_REQUIREDA non-zero locker was supplied. Locker funding is not implemented on this command.
161ERROR_DURATION_NOT_FUNDEDPrepaid 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.
170ERROR_NO_STORAGE_CREDITThe mailbox has no prepaid credit on this server.
167ERROR_PAYMENT_PROCESSINGThe credit ledger could not be updated. Transient; retry.
—ERROR_FILESYSTEMThe 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.