Files
archive-clients/docs/protocol.md

7.8 KiB

Daemon protocol

The source of truth is archive_control.v1 in the sibling archive-control-proto repository. This document defines behavior that cannot be expressed by field declarations alone. There is no separately maintained JSON schema.

Framing and encoding

  • Each WebSocket text frame contains exactly one Envelope encoded with the canonical protobuf JSON mapping.
  • Binary frames, batches, unknown top-level payloads, malformed JSON, duplicate JSON keys, non-finite numbers, invalid timestamps, and over-limit envelopes are protocol errors.
  • UUID fields use lowercase canonical UUID text. Hash fields use validated lowercase hexadecimal text.
  • Protobuf uint64 values use JSON strings according to the protobuf JSON mapping. Enum names are emitted symbolically.
  • Receivers may ignore unknown JSON fields only after a compatible minor version has been negotiated. An unknown required command or job operation is rejected explicitly.
  • Implementations apply negotiated envelope and inventory chunk limits before allocation and use bounded outbound queues.

Registration and connection ownership

RegisterRequest must be the first application message on every new connection. It carries protocol version, stable client ID/name, role, connection-instance UUID, capabilities, current active-job cursors, and the single shared token.

The server validates the entire request and constant-time token comparison before accepting other messages. A rejected token or malformed first message causes a rejection when safe to send and then immediate connection closure. The token is forbidden in all later messages and is redacted before any structured logging, error rendering, metrics label, trace, or database write.

A valid new registration for an already connected client atomically becomes the current connection generation. The old connection is closed, and messages arriving from its stale generation cannot mutate state.

Clients retry registration indefinitely until accepted or the process exits. The default delay begins at one second, doubles with jitter, and caps at 60 seconds. A connection stable for 60 seconds resets the backoff.

Version and feature negotiation

The envelope and registration both state the sender version. Major 1 accepts only major 1; a different major is rejected. The negotiated minor is the highest mutually supported minor no greater than either endpoint's advertised minor.

Capabilities, not version guesses, gate optional behavior such as route provisioning, sparse files, partfile migration, or eviction. A required missing feature fails the job preflight with UNSUPPORTED. Compatible changes add fields or enum values; released field numbers and enum values are never reused, and removals reserve them. Buf breaking checks enforce file-level compatibility.

Heartbeats and liveness

Default settings are:

Setting Default
Registration deadline 10 s
Heartbeat interval 15 s
Offline threshold 45 s
Unacknowledged command retry 15 s
Maximum command send attempts 3 total
Reconnect initial/cap 1 s / 60 s
Stable reset window 60 s

The server communicates the effective heartbeat values in RegisterResponse. Heartbeat sequence acknowledgements prove application-level responsiveness. Missing the offline threshold marks the client offline, releases connection ownership, and moves affected jobs to waiting; it does not fail them.

Reliable commands

Control generates a UUID command_id, persists the command and intended client, then sends it at least once. A client transactionally inserts an unseen ID and its immutable payload before replying ACCEPTED. A repeat with the same payload returns the stored acknowledgement as DUPLICATE; the same ID with a different payload is a protocol conflict.

CommandAck confirms durable acceptance or rejection only. It never means the step completed. Completion is represented by ordered JobEvent, InventoryChunk, RouteUpdate, or JobSnapshot messages. Control retries an unacknowledged command with its original ID after reconnect or timeout, up to the configured total attempt limit. A late acknowledgement remains valid after automatic delivery stops.

Route updates use a stable update UUID and a sequence monotonic within their route-creation command. Conflicting duplicates or gaps are reconciled with the client state snapshot before a route can become ready.

Every mutating command includes an expected job revision or immutable job definition plus the expected last global event sequence. A stale revision or sequence is rejected without side effects. The control daemon sends only one active step command for a job and waits for its persisted completion event before commanding the next participant. This single-writer lease lets whichever client owns the current step allocate the next global per-job event sequence; the following command starts from the sequence control has durably accepted.

Events, progress, and reconciliation

Each client-originated job event has a unique ID, monotonically increasing global per-job sequence, and resulting job revision. Duplicate IDs/sequences are idempotent only when their complete content matches. Control never grants concurrent event-writer leases for one job. A gap or conflicting duplicate pauses destructive orchestration and requests snapshots.

fraction_complete is current-step progress and overall_fraction_complete is the weighted five- or three-step job progress; both are clamped to [0,1]. Byte counts are authoritative when known. STALLED retains both fractions, byte counters, and current step. Progress timestamp movement alone does not prove content movement.

On registration, active-job cursors provide the client's revision, last event sequence, state, and commit flag. Reconciliation applies these rules:

  1. Equal cursors resume normal delivery.
  2. A client behind receives safe replay/snapshot commands.
  3. Control behind requests and validates the client's full job snapshot.
  4. Conflicting commit evidence reserves the resource and requires manual reconciliation; neither side performs cleanup.
  5. In-flight command IDs are compared before any resend.

Inventory protocol

Inventory is requested only for a user flow, job preflight, active-job monitoring, route discovery, or reconciliation. Registration does not upload a full torrent inventory.

Queries declare a scope, resource IDs/filter, page bounds, and optional expected revision. Responses form an atomic snapshot stream identified by query ID, snapshot ID, revision, monotonically increasing chunk index, and final marker. Control commits the result only after every chunk through last_chunk is present. A disconnect or error discards the partial snapshot. Full content trees are fetched for one chosen resource and from both relevant endpoints before staging. File entries carry index, logical/allocated/completed bytes, selection, sparse/padding flags, and the resource content revision required to validate the preview.

Stable errors

Machine behavior uses ErrorCode, retryable, and structured details; the human message explains the concrete condition without secrets. Transient unavailability/timeouts may wait or retry. Invalid input, stale fingerprints, path conflicts, permissions, unsupported formats, integrity failures, and unsafe state are permanent for the current attempt. Unknown internal errors must not be mislabeled retryable.

Manifest encoding

TransferManifest and ReadyMarker use the same canonical protobuf JSON mapping, written atomically. The manifest describes immutable source/target identity, route, requested set, target baseline, transfer delta, file paths and sizes, sparse flags, materialization provenance, fingerprints, and auxiliary artifacts. The ready marker is written last and includes a SHA-256 digest of the exact manifest bytes. A receiver ignores a job directory until both exist and the digest matches.