Skip to content

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.

A client that connects to an Outpost-managed session normally follows this sequence:

  1. Authenticate to Outpost or to the selected daemon.
  2. Fetch a fleet snapshot and retain its fleet epoch and cursor.
  3. Subscribe to fleet events, replaying from the retained cursor after a reconnect.
  4. Select a session from the snapshot or event stream.
  5. Fetch the selected daemon’s authenticated sync snapshot and operation window.
  6. Replay daemon records from the returned cursor, then apply later records in order.
  7. Attach the operation context to commands that support correlation.
  8. On an epoch change, a reset event, or an expired token, discard the affected cursor or window and fetch a new snapshot before continuing.
  9. 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.

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.

Use the authenticated routes below to build and maintain a fleet view.

RequestPurpose
GET /api/v1/catalogReturns 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}/historyReturns 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.v1 for ordered synchronization and replay.
  • session.history.v1 for public history pages.
  • session.attachments.v1 for captured public attachments.
  • session.targeted-mutations.v1 for correlated, target-safe commands.
  • daemon.catalog.v1 for the published daemon catalog.

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. Enable each capability only when the snapshot advertises its feature ID.

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, autonomy hints, and the full reasoning capability: the reasoning kind, the discrete effort levels and the default level when the model uses effort, and whether reasoning can be turned off. Entries also carry 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 redacts credential-shaped strings and values that look like secrets from catalog metadata before publication. Catalog 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. Polytoken announces the revision with catalog_published on the ordered record stream and daemon_catalog_changed on the legacy event stream. A no-op reload with identical results keeps the existing revision. Switching the active facet, model, or permission monitor changes session state, not catalog state, and does not change the revision. Clients that cache the catalog should refetch when either stream announces a new catalog revision and match the revision in the event.

The daemon uses independent values for different consistency boundaries:

ValueMeaningClient action
Daemon epochIdentifies one daemon runtime. A daemon restart creates a new epoch.Discard daemon cursors, checkpoints, and operation contexts from the old epoch.
Sync cursorOrdered position in the daemon’s retained record stream.Store and advance only after applying records in order.
Catalog revisionRevision of the daemon’s published public snapshot.Refresh the snapshot when the revision changes.
History checkpointImmutable public-history capture for page reads and Outpost attachment reads.Use the checkpoint on later history-page requests and Outpost attachment requests.
Fleet epochIdentifies one Outpost fleet event stream.Discard fleet cursors after an epoch change.
Operation windowServer-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.

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. When a supplied epoch does not match the current runtime, the daemon returns 409 with epoch_mismatch; Outpost fleet routes return 400 with epoch_mismatch for a stale expected_epoch. For any of these errors, 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.

Session titles reach the ordered stream as title_changed records. Polytoken publishes the record when an operator sets the title through /title and when Polytoken applies an inferred or fallback title after a turn, so a replaying client reconstructs the title the same way in both cases. One asymmetry is deliberate: when the ordered stream cannot accept the record, the /title request fails with a sync error, while an inferred or fallback title stays applied and Polytoken logs a warning instead, because that title change happens after the turn with no request to fail. A client that misses the record finds the current title in the next snapshot it fetches.

Prompt submission, newest-input dequeue, turn cancellation, 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, prompt submission returns 409 turn_input_queue_full when the pending input queue is full, and an interrogative response 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.

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.

StatusCodesRecovery
400invalid_cursor, cursor_wrong_sessionDiscard the malformed or misbound cursor and fetch a new snapshot.
400invalid_checkpoint, attachment_range_invalid, invalid_artifact_idCorrect the checkpoint, byte range, or malformed daemon artifact ID before retrying.
400epoch_mismatch (Outpost fleet routes)Fetch the current fleet snapshot and restart replay from its cursor.
400invalid_queryRemove repeated parameter keys or correct values the route cannot parse, then send the request again.
409epoch_mismatchFetch the current snapshot and restart replay from its cursor.
409operation_payload_conflictDo not reuse the operation key for a different payload. Create a new operation only when the original result is not the intended request.
409history_requires_stopped, history_not_finalized, history_source_changedWait for a stopped/finalized source, or fetch a new immutable history checkpoint.
409turn_in_flightFollow the route’s busy-session behavior. A correlated operation remains replayable when the server stored it.
410cursor_too_oldFetch a new snapshot and replay from its current cursor.
410checkpoint_expired, snapshot_expiredFetch history again and use the new checkpoint.
410operation_expired, reset_requiredObtain a fresh operation window or snapshot.
404history_unavailableThe requested public artifact is not available. Refresh the session catalog before treating a session as permanently unavailable.
404attachment_not_found (shared vocabulary only)Current attachment routes return history_unavailable instead.
422history_corruptSurface the data-integrity failure. Do not silently replace the history with an empty transcript.
413attachment_too_large (shared vocabulary only)Current attachment routes cap chunks rather than emit this code.
413fleet_capacity_exceededTreat the fleet response as unavailable at its capacity boundary and retry after the service recovers.
429operation_capacity_exceededBack off and obtain a new operation window when the server accepts new operations.
503snapshot_capacity_exceededRetry the snapshot request after capacity recovers.
503fleet_capacity_exceededSome generic protocol surfaces map this capacity class to service unavailable; use the response status and code together.

The shared protocol vocabulary maps history_unavailable to 404, history_corrupt to 422, and history_requires_stopped and history_not_finalized to 409. The vocabulary also maps attachment_not_found to 404 and attachment_too_large to 413, but current attachment routes do not emit those two codes. Outpost uses its fleet-specific invalid_checkpoint, snapshot_expired, attachment_range_invalid, and fleet_capacity_exceeded codes to distinguish a bad request, an expired capture, a bad byte range, or a capacity boundary.

Polytoken validates daemon query parameters before running a request handler. If a query string repeats a parameter key or contains a value the route cannot parse, Polytoken returns HTTP 400 with content type application/json and code invalid_query. For example, offset and limit must be numeric. The message describes the parsing failure and names any duplicated parameter. Correct the query string and resend. Polytoken does not run the handler for a malformed query string.

Outpost fleet endpoints also reject malformed query strings with HTTP 400. The response body contains {"code":"invalid_query"} without a message payload.

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. Because the current credential inventory cannot prove what historical secret inventories contained, legacy conversion also marks secret coverage as historical_unknown. 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.

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 its stored title and history. History reads leave session metadata unchanged.

Use the Outpost session routes for lifecycle operations. See Outpost Service.

An Outpost attachment request names the session, artifact ID, checkpoint, offset, and length. Outpost 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. Outpost returns attachment_range_invalid for an offset beyond the artifact, a zero length, or an overflowed range, and returns history_unavailable for an artifact the checkpoint does not advertise.

To read an attachment directly from the daemon, request GET /attachments/{artifact_id} with the optional offset query parameter, which defaults to zero. Each response contains at most 8 MiB; the route accepts neither a checkpoint nor a length parameter. Polytoken returns 400 with invalid_artifact_id for a malformed artifact ID. For an unknown artifact or an offset at or beyond the artifact’s total length, Polytoken returns 404 with history_unavailable.

Outpost 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.

Polytoken scrubs credentials before storing a public payload or attachment in durable history. Rotating credentials does not change previously captured records: Polytoken does not rescan history after credential rotation. Capture-time scrubbing detects credentials split across adjacent chunks, so a stream boundary does not expose a secret. The current inventory cannot prove coverage for unknown historical secrets; legacy attribution records that uncertainty explicitly.

SurfaceLimit
Operation window10 minutes
Fleet journal8,192 records, 32 MiB, five minutes
Live sync stream256 records, 2 MiB per frame
Operation ledger4,096 records, 32 MiB
Fleet snapshotEight retained, 32 MiB total, 10-minute TTL
Fleet page200 entries, 1 MiB
Fleet heartbeat15 seconds
Stopped-history page256 items, 2 MiB
Stopped-history checkpointsEight retained, 10-minute TTL
Stopped-history spool1 GiB
Attachment response8 MiB
Outpost observerEight 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.

Use these CLI commands to export contracts for client generators:

CommandContract
polytoken print sync-event-schemaDaemon synchronization records for replay and event streams (SyncRecordV1).
polytoken print fleet-event-schemaOutpost fleet observation events (FleetEventV1).
polytoken print outpost-openapiThe complete Outpost HTTP specification.

Each command accepts --format json, --format json-pretty, or --format yaml. The same JSON Schema and OpenAPI documents appear in docs/public/schemas/ for TypeScript and other client generators. The daemon catalog and facet descriptions have contracts separate from daemon synchronization records.

{
"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",
"prompt_id": null
}
{
"guidance": "Preserve the latest operation window."
}