# QMail Object Transfer v1: Client Codex Handoff

Status: stable v1, frozen June 11, 2026. Implement packet serializers from
`qmail-object-transfer-v1.json` and verify them against
`qmail-object-transfer-v1-vectors.json`.

## Required Implementation

- Represent object sizes and byte offsets with unsigned 64-bit types.
- Separate logical-file metadata from per-RAIDA stored-object metadata.
- Add transfer states for begin, upload, status/resume, commit, abort, and expiry.
- Generate a nonzero cryptographically random 16-byte Transfer ID for each
  stored-object attempt. It is an idempotency key, not a session credential.
- Persist the Transfer ID before sending `object_begin`; retry the exact same
  begin after a timeout so the server cannot reserve or charge twice.
- Use a new Transfer ID after abort or expiry. Never reuse one with changed
  object, generation, hash, size, class, retention, or payment fields.
- Encode `requested_retention_seconds` as uint64. Zero requests the server
  default; a nonzero value must be accepted exactly or rejected.
- Record `accepted_retention_seconds` from the begin response.
- Keep one stable object ID across edits and track the logical target generation as uint64.
- Replace only with an exact expected generation; treat conflicts as requiring refresh.
- Select one monotonic target generation for a logical file revision and use it
  on every selected QMail server, even when their expected generations differ.
- Pin one generation for all ranges used in a single reconstruction.
- Implement owner-authorized deletion as a separate operation.
- Delete with exact expected-generation compare-and-swap and a monotonic target
  tombstone generation shared across the selected servers.
- Make upload chunk size negotiated per transfer.
- Make download range size independent of upload chunk size.
- Support concurrent and out-of-order upload ranges.
- Track retryable ranges by offset and length, not page number.
- Treat duplicate identical ranges as success and conflicting overlaps as fatal.
- Retry an unanswered commit with the same Transfer ID, size, algorithm, and
  object hash; the server returns the original committed result.
- Query DRD for discovery, then query live command 83 before allocating or uploading.
- Handle a server lowering its advertised object, chunk, range, or concurrency limit.
- Require TCP for commands 76 through 84.
- Preserve legacy commands 70, 74, and 75 during migration.
- Use Object Transfer status codes 218 through 235 exactly as assigned.
- Announce object-stored mail with the standard manifest v1 tell (16-byte
  entries, CRC32 set); the object key is object_id = email GUID, generation 1,
  file_type per entry. No Object Transfer manifest entry exists on the wire.

## Do Not Hard-Code

- 1 MiB chunk size
- maximum attachment size
- number of ranges
- maximum parallel requests
- storage class IDs and prices
- DRD capability lifetime

The initial preferred upload chunk is expected to be 1 MiB, but the accepted
size returned by `object_begin` is authoritative.

## Integrity

- SHA-256 algorithm ID 1 is the initial required algorithm.
- Upload-range hashes cover the exact range bytes sent to one RAIDA.
- The per-RAIDA object hash covers the exact stored stripe bytes.
- The v1 tell manifest entry carries the sender-authored CRC32 for the
  reconstructed logical file; receivers verify it after reassembly.

## Required Interoperability Tests

- zero and one-byte boundary cases
- final partial chunk
- duplicate, conflicting, overlapping, and out-of-order ranges
- resume after disconnect
- lost `object_begin` response followed by an exact retry with no second charge
- lost commit response followed by an exact retry returning the original commit
- reused Transfer ID with changed begin fields returning `ERROR_TRANSFER_CONFLICT`
- abort and expiry
- object and range limits changing between DRD discovery and live query
- offsets and sizes above 4 GiB
- TCP-required response after an accidental UDP request
- hash mismatch at range acceptance and commit
- concurrent replacements producing a generation conflict
- reads pinned to an old generation while a replacement commits
- owner and non-owner replacement and deletion attempts
- a server disappearing while other QMail servers complete the transfer
- byte-for-byte agreement with every frozen request, response, and error
  vector

## Phase I Authorization

- Read and owner-neutral object-info are allow-all after valid encrypted QMail framing.
- Replace, delete, and future ACL administration are owner-only.
- Store owner identity and ACL version now even though account groups, resource
  groups, and explicit ACL permissions are deferred to Phase III.

## Storage Classes And Fault Tolerance

- DRD and live capabilities advertise each server's local storage classes.
- The client chooses the class and builds horizontal, vertical, and diagonal
  parity across servers.
- Do not expect one server to coordinate or understand the global parity layout.
- A committed server copy should remain until expiry, owner deletion, or actual
  failure of its advertised class. `ERROR_STORAGE_FULL` is preferable to silent
  eviction of an unexpired paid object.

## Phase I Payment

- Expect capability command 83 to advertise `legacy_locker_marker`.
- This preserves the current asynchronous locker-consumption and marker-reuse
  behavior.
- Payment is once per server for authenticated owner + Object ID + locker code,
  shared across all file types and replacement generations.
- Transfer ID identifies one attempt; it is not part of the payment key.
- Duplicate range retries must not charge again.
- Replacement and deletion have no automatic refund or byte-accurate settlement
  in Phase I.

## Capacity Reservation

- `object_begin` reserves exactly `total_size` stored-content bytes in the
  selected server-local storage class.
- Metadata overhead is server-owned and is not part of the client reservation.
- Treat command 83 capacity figures as advisory until command 76 succeeds.
