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.

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.

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.

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:

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 and attachment reads.Use the checkpoint on every later history-page or attachment request.
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. 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.

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.

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_invalidCorrect the request. Do not retry the same malformed range or checkpoint.
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_unavailable, attachment_not_foundThe requested public artifact is not available. Refresh the session catalog before treating a session as permanently unavailable.
422history_corruptSurface the data-integrity failure. Do not silently replace the history with an empty transcript.
413attachment_too_largeRequest a smaller attachment range.
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 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.

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.

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.

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.

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.

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."
}