Client Protocol
Polytoken exposes a versioned client protocol for interfaces that need to follow a daemon or an Outpost fleet without treating a live screen as the source of truth. The protocol uses authenticated snapshots, opaque cursors, independent epochs, and replayable operation results. A client can recover from restarts, reconnects, expired cursors, and an interrupted command without duplicating a user action.
Use the protocol in this order
Section titled “Use the protocol in this order”A client that connects to an Outpost-managed session normally follows this sequence:
- Authenticate to Outpost or to the selected daemon.
- Fetch a fleet snapshot and retain its fleet epoch and cursor.
- Subscribe to fleet events, replaying from the retained cursor after a reconnect.
- Select a session from the snapshot or event stream.
- Fetch the selected daemon’s authenticated sync snapshot and operation window.
- Replay daemon records from the returned cursor, then apply later records in order.
- Attach the operation context to commands that support correlation.
- On an epoch change, a reset event, or an expired token, discard the affected cursor or window and fetch a new snapshot before continuing.
- When a session stops, read its immutable public history through Outpost.
The protocol never treats a cursor as a durable identity. A cursor belongs to one epoch and one stream, so a client must replace it after the server asks for a reset.
Authenticate before reading state
Section titled “Authenticate before reading state”A direct daemon request sends the daemon bearer token in
Authorization: Bearer <token>. The daemon rejects a missing or incorrect
token with 401 Unauthorized. See Daemon Authentication
for credential-file behavior.
Outpost requests use an Outpost bearer token. OAuth endpoints and authorization
server metadata have their own rules; the fleet and stopped-history routes
require an authenticated caller with the machine scope. Outpost replaces the
caller token with the daemon credential only when a request uses the session
proxy. See Outpost Service for the complete route and scope
matrix.
Discover an Outpost fleet
Section titled “Discover an Outpost fleet”Use the authenticated routes below to build and maintain a fleet view.
| Request | Purpose |
|---|---|
GET /api/v1/catalog | Returns a paged fleet snapshot. |
GET /api/v1/events?after=<cursor>&expected_epoch=<epoch> | Returns fleet changes after a cursor and the 15-second heartbeat interval. |
GET /api/v1/sessions/{id}/history | Returns paged public history for a stopped session. |
GET /api/v1/sessions/{id}/attachments/{artifact_id} | Returns a bounded chunk of an advertised public attachment. |
The catalog advertises these implemented feature IDs: fleet_catalog.v1,
fleet_events.v1, and session_history.v1. The shared protocol vocabulary
also defines outpost.fleet.v1 and outpost.stopped-history.v1; clients must
accept unknown feature IDs and must use the advertised route surface as the
capability check.
A catalog response contains a fleet epoch, an opaque cursor, projects, sessions, page information, and a snapshot token. A snapshot token binds later catalog pages to the same catalog. Catalog pages contain at most 200 project or session entries and at most 1 MiB of JSON. The in-memory fleet snapshot store retains at most eight snapshots and 32 MiB of serialized snapshots. Each snapshot expires after 10 minutes.
The fleet journal retains at most 8,192 records, 32 MiB, or five minutes of
history, whichever boundary removes records first. If after names a cursor
older than the retained journal, the server returns 410 with
cursor_too_old. Fetch a new catalog and begin event consumption at its cursor.
The event response advertises a 15-second heartbeat interval. A client should
use the heartbeat to distinguish a quiet fleet from a stale connection.
The Outpost observer refreshes running-session summaries through the daemon’s
GET /sync/summary route. The observer polls every five seconds, permits eight
summary requests at once, gives each request three seconds, and marks an
observation stale after 15 seconds. A fleet row with an old observation is not
a fresh assertion about the daemon.
Select a session and establish a daemon checkpoint
Section titled “Select a session and establish a daemon checkpoint”After selecting a session, use the session’s proxy or its direct daemon endpoint to request the daemon’s synchronization snapshot. The snapshot gives the daemon epoch, current cursor, catalog revision, advertised features, current turn state, action availability, history checkpoint, and the current operation window when one exists.
The daemon currently advertises these protocol features:
session.sync.v1for ordered synchronization and replay.session.history.v1for public history pages.session.attachments.v1for captured public attachments.session.targeted-mutations.v1for correlated, target-safe commands.
The core vocabulary also reserves model.request-options.v1,
outpost.fleet.v1, and
outpost.stopped-history.v1. A client must not infer an unadvertised feature
from a type name. Model request-options, fleet observation, and stopped
history have separate contracts and should be enabled only when their feature
IDs appear in the snapshot.
Machine-readable protocol contracts
Section titled “Machine-readable protocol contracts”The CLI exports the synchronization record schema with
polytoken print sync-event-schema. The export describes the SyncRecordV1
union used by daemon replay and event streams. The CLI exports the Outpost
fleet-event schema with polytoken print fleet-event-schema; the export
covers the FleetEventV1 union used by fleet observation. Use
polytoken print outpost-openapi for the Outpost HTTP specification. Each
command accepts --format json, --format json-pretty, or --format yaml.
Human review is required for this protocol reference and the generated schema exports before publication.
Read the daemon catalog
Section titled “Read the daemon catalog”When the snapshot advertises daemon.catalog.v1, request GET /daemon-state
on the daemon (or the session proxy path) for a sanitized catalog of what the
daemon has loaded. The catalog is one immutable publication with a revision
number and deterministic, sorted collections:
- Facets with authored descriptions, configured model pins, declared policies, and effective light and dark colors as the registry resolves them.
- Models with labels, reasoning and autonomy hints, safe provider and API identity, known capabilities (context window, output limit, vision, edit format, tool loading), and each model’s request-option declarations and defaults.
- Subagents, skills, and tools as loaded metadata: descriptions, tags, and coarse origin. Skill entries include whether a user can invoke them and whether the model may; tools appear with their input schema and strictness.
- MCP servers by name, transport kind, connection state, tool count, and synthetic tool identities only. Commands, arguments, URLs, environment, and credentials never appear.
- Loaded extensions with their approved metadata and capabilities. The catalog lists what actually loaded; it never promises future availability.
Polytoken scrubs credential-shaped strings from catalog metadata before publication and rejects values that look like secrets. Building the catalog has no side effects: reads never select a model, start a server, or execute a tool.
The catalog changes only when its inputs change. Reloads, MCP connections and
disconnections, and extension load events publish a new revision and a
daemon_catalog_changed event on both the ordered record stream and the
legacy event stream; a no-op reload with identical results keeps the existing
revision. Switching the active facet, model, or permission monitor is session
state, not catalog state, and does not change the revision. Clients that
cache the catalog should refetch when they observe daemon_catalog_changed,
matching the new revision in the event.
The daemon uses independent values for different consistency boundaries:
| Value | Meaning | Client action |
|---|---|---|
| Daemon epoch | Identifies one daemon runtime. A daemon restart creates a new epoch. | Discard daemon cursors, checkpoints, and operation contexts from the old epoch. |
| Sync cursor | Ordered position in the daemon’s retained record stream. | Store and advance only after applying records in order. |
| Catalog revision | Revision of the daemon’s published public snapshot. | Refresh the snapshot when the revision changes. |
| History checkpoint | Immutable public-history capture for page and attachment reads. | Use the checkpoint on every later history-page or attachment request. |
| Fleet epoch | Identifies one Outpost fleet event stream. | Discard fleet cursors after an epoch change. |
| Operation window | Server-issued authorization and retention window for correlated commands. | Reuse the window and operation ID for retries until the window expires. |
Counter revisions serialize as opaque strings. Do not parse them into a numeric type that can lose precision.
Replay records and reset safely
Section titled “Replay records and reset safely”Fetch the initial record page from the daemon’s public-history or sync surface, then apply records in revision order. Keep the last successfully applied cursor separate from the cursor received in an uncommitted response. A reconnect starts from the last applied cursor.
The server can return reset_required with 410 when the live stream can no
longer provide a safe continuation. The server can also return
epoch_mismatch with 409 when a supplied epoch does not match the current
runtime. In either case, stop applying the old stream, fetch a new snapshot,
and replay from the new snapshot’s cursor. Do not attempt to manufacture a
cursor by incrementing a stale value.
A history checkpoint is separate from a sync cursor. It captures public history
and its attachment references. Checkpoints expire after 10 minutes and the
server retains at most eight checkpoints, subject to a shared stopped-history
spool limit of 1 GiB. An expired or evicted checkpoint returns 410 with
checkpoint_expired or the Outpost equivalent snapshot_expired. Fetch the
history endpoint again to obtain a new checkpoint.
Correlate commands and replay outcomes
Section titled “Correlate commands and replay outcomes”Prompt submission, newest-input dequeue, and interrogative responses accept an optional operation context. The context contains:
daemon_epoch, copied from the latest daemon snapshot.window, the server-issued operation-window token.operation, a client-generated UUID.
The operation key is the (window, operation) pair. A retry must send the same
pair and the same request payload. The daemon records the payload digest and
returns the stored ClientOperationResultV1 instead of running the command a
second time. A changed payload with the same pair returns 409
operation_payload_conflict.
An operation result identifies the daemon epoch, operation window, operation
UUID, lifecycle status, optional committed cursor, optional outcome, and
optional typed error. The statuses are admitted, in_progress, completed,
and failed. A completed result can be replayed after a client timeout. A
failed result is also durable for the operation window, so the client should
surface the typed error rather than submit a new operation automatically.
The operation window lasts 10 minutes. Reusing an expired window returns 410
operation_expired. The operation ledger rejects new entries with 429
operation_capacity_exceeded when it reaches capacity. A client should wait
and retry with a fresh operation window rather than retrying indefinitely with
the same expired key.
A command can also fail because the session changes while the command waits.
For example, a prompt can return 409 turn_in_flight, and an interrogative
can return a typed withdrawal or pending-state error. The operation result,
not the transport status alone, identifies whether Polytoken admitted, completed,
or failed the command.
Error handling
Section titled “Error handling”Protocol errors use a stable code field and the HTTP status shown below.
Clients should branch on the code and status, preserve unknown fields, and keep
unknown future codes visible rather than treating them as success.
| Status | Codes | Recovery |
|---|---|---|
400 | invalid_cursor, cursor_wrong_session | Discard the malformed or misbound cursor and fetch a new snapshot. |
400 | invalid_checkpoint, attachment_range_invalid | Correct the request. Do not retry the same malformed range or checkpoint. |
409 | epoch_mismatch | Fetch the current snapshot and restart replay from its cursor. |
409 | operation_payload_conflict | Do not reuse the operation key for a different payload. Create a new operation only when the original result is not the intended request. |
409 | history_requires_stopped, history_not_finalized, history_source_changed | Wait for a stopped/finalized source, or fetch a new immutable history checkpoint. |
409 | turn_in_flight | Follow the route’s busy-session behavior. A correlated operation remains replayable when the server stored it. |
410 | cursor_too_old | Fetch a new snapshot and replay from its current cursor. |
410 | checkpoint_expired, snapshot_expired | Fetch history again and use the new checkpoint. |
410 | operation_expired, reset_required | Obtain a fresh operation window or snapshot. |
404 | history_unavailable, attachment_not_found | The requested public artifact is not available. Refresh the session catalog before treating a session as permanently unavailable. |
422 | history_corrupt | Surface the data-integrity failure. Do not silently replace the history with an empty transcript. |
413 | attachment_too_large | Request a smaller attachment range. |
413 | fleet_capacity_exceeded | Treat the fleet response as unavailable at its capacity boundary and retry after the service recovers. |
429 | operation_capacity_exceeded | Back off and obtain a new operation window when the server accepts new operations. |
503 | snapshot_capacity_exceeded | Retry the snapshot request after capacity recovers. |
503 | fleet_capacity_exceeded | Some generic protocol surfaces map this capacity class to service unavailable; use the response status and code together. |
The shared daemon protocol maps history_unavailable to 404,
history_corrupt to 422, history_requires_stopped and
history_not_finalized to 409, attachment_not_found to 404, and
attachment_too_large to 413. Outpost uses its fleet-specific
invalid_checkpoint, snapshot_expired, attachment_range_invalid, and
fleet_capacity_exceeded codes where the route needs to distinguish a bad
request, an expired capture, a bad byte range, or a capacity boundary.
Read stopped-session history
Section titled “Read stopped-session history”Outpost serves stopped-session history only after the session reaches a stopped
and finalized state. The first request to
GET /api/v1/sessions/{id}/history creates an immutable checkpoint. Pass the
checkpoint on subsequent requests. Use the returned signed page token as
next_page; do not construct page tokens locally.
A stopped-history page contains at most 256 public items and 2 MiB of JSON.
The shared spool quota is 1 GiB. History snapshots and attachment data consume
that quota, and the service evicts old captures when the quota or the eight
snapshot limit requires it. A page or attachment request returns
history_source_changed when the session’s source no longer matches the
checkpoint.
History records contain public payloads and capture-time attribution. A legacy
record that cannot establish its original facet or model uses
historical_unknown attribution. Legacy conversion marks secret coverage as
historical_secret_coverage_unknown, because the current credential inventory
cannot prove what historical secret inventories contained. Public projections
still scrub values found by the current inventory at capture time. Operators
must review this migration and assurance boundary before relying on legacy
history for compliance evidence.
Legacy stopped-history reads are read-only. They parse and project the old log in memory and do not create a v1 tree or rewrite the source. Resume uses a hard, forward-only migration: Polytoken stages and validates a complete v1 tree, retains the original tree in a private backup, publishes the v1 tree, and keeps the backup until cleanup. The migration journal lets a later resume complete a publish or restore the original tree for a retry. Polytoken does not discover a migrated session through the old-release layout, and it does not support a downgrade that copies the v1 tree back into the legacy layout. Human review is required for this migration and downgrade boundary.
Legacy image messages can produce an unavailable public image entry when the old record does not provide a retrievable attachment. The history item remains present with the unavailable image metadata. A client must render that state as unavailable and must not treat it as a missing history record.
A stopped session keeps the stored title and history. The stopped-history API does not rename a session. Use the session-management route for lifecycle operations; do not expect history reads to mutate session metadata.
Retrieve attachments in bounded chunks
Section titled “Retrieve attachments in bounded chunks”An attachment request names the session, artifact ID, checkpoint, offset, and
length. The server caps one response at 8 MiB and returns base64 data with the
total length and offset. Request adjacent ranges until the client has all bytes.
The server rejects an offset beyond the artifact, a zero length, an overflowed
range, or an artifact not advertised by the checkpoint. Those failures use
attachment_range_invalid, attachment_not_found, or
history_unavailable, depending on the failed boundary.
Attachment bytes and public payloads inherit the checkpoint’s source identity. If the source changes, discard every attachment token associated with that checkpoint and start a new history read.
Credential scrubbing happens at capture time, before a public payload or attachment enters durable history. The captured public projection does not rescan internal history after credential rotation, so later token rotation does not change an already captured record. The capture path handles a credential split across adjacent chunks, so a secret does not become visible merely because a stream boundary divides its bytes. The current inventory cannot prove coverage for unknown historical secrets; that condition remains explicit in legacy attribution.
Protocol limits at a glance
Section titled “Protocol limits at a glance”| Surface | Limit |
|---|---|
| Operation window | 10 minutes |
| Fleet journal | 8,192 records, 32 MiB, five minutes |
| Live sync stream | 256 records, 2 MiB per frame |
| Operation ledger | 4,096 records, 32 MiB |
| Fleet snapshot | Eight retained, 32 MiB total, 10-minute TTL |
| Fleet page | 200 entries, 1 MiB |
| Fleet heartbeat | 15 seconds |
| Stopped-history page | 256 items, 2 MiB |
| Stopped-history checkpoints | Eight retained, 10-minute TTL |
| Stopped-history spool | 1 GiB |
| Attachment response | 8 MiB |
| Outpost observer | Eight concurrent requests, five-second cadence, three-second request timeout, 15-second freshness |
These limits are protocol behavior, not client hints. A client should request smaller ranges or restart from the appropriate snapshot when a limit error occurs.
Machine-readable schemas
Section titled “Machine-readable schemas”The CLI exports the daemon synchronization record union with
polytoken print sync-event-schema. Outpost clients can use
polytoken print fleet-event-schema for fleet observation events and
polytoken print outpost-openapi for the complete Outpost HTTP contract. The
same JSON Schema and OpenAPI documents appear in docs/public/schemas/ for
TypeScript and other client generators.
The daemon catalog DTO and facet descriptions remain separate from the synchronization contract described on this page.
Executable request and response recipes
Section titled “Executable request and response recipes”The following JSON bodies are executable contract fixtures. The docs test parses each labeled fence and checks the body against the exported OpenAPI component.
{ "version": 1, "daemon_epoch": "epoch-1", "catalog_revision": "42"}{ "version": 1, "daemon_epoch": "epoch-1", "cursor": "42", "busy": false, "attention": false, "history_checkpoint": "checkpoint-1", "history_total_items": 0}{ "daemon_epoch": "epoch-1", "window": "window-token", "operation": "operation-1"}{ "daemon_epoch": "epoch-1", "window_id": "window-1", "operation": "operation-1", "status": "completed"}{ "artifact_id": "artifact-1", "media_type": "text/plain", "total_length": 5, "offset": 0, "data_base64": "aGVsbG8="}{ "generation": 1, "operation_context": { "daemon_epoch": "epoch-1", "window": "window-token", "operation": "operation-1" }, "prompt_id": "prompt-1"}{ "status": "accepted"}{ "guidance": "Preserve the latest operation window."}