packages/protocol/README.md

Public contracts

@clankie/protocol owns the schemas and wire types shared by Clankie's service, clients, relay, and hosted-service boundary. It depends on Zod, with no other workspace package dependencies. It does not run agents, store credentials, or implement a gateway.

@clankie/protocol/composer-transcription defines bounded recording chunks, availability and draft receipts. Its WAV parser validates actual sample bytes without Node dependencies. The composer API guide describes paired-device authority, cancellation and the unchanged encrypted transport limit. Provider credentials and plan configuration remain private.

The package's exports are the public entry points. The root index defines the operator-conversation and related contracts; separate entry points cover device encryption, public gateway routes, connected accounts, work items, model keys, and hosted pairing/operation. Read the owning schema rather than copying a payload shape into documentation.

@clankie/protocol/tool-output shares tool-result decoding between the TUI and companion app. It unwraps MCP content/envelopes, native Codex text parts, and repeated JSON encoding while retaining image order and unknown content. It has no Node dependencies. Host-specific exec and receipt presentation stays in the TUI; this helper preserves their decoded JSON for app detail views.

Consumers and compatibility

Seat input capabilities (VUH-1882 service contract)

Every current roster/fleet seat publishes optional inputCapabilities:

{
  "deliveryModes": ["steer", "queue"],
  "interrupt": true,
  "nextTurnOnly": false
}

The source is FleetSeatInputCapabilitiesSchema. deliveryModes lists only explicit choices the current native connection can honor; array order has no meaning. interrupt means the existing authenticated stop_task operator operation can stop that exact task. A harness's generic interrupt method does not establish this capability. Interactive Claude's stop_task is one guarded Esc into its exact working pane with an empty input line (VUH-2680); a bare terminal Escape key elsewhere still establishes nothing. A draft refusal returns outcome: "unavailable", reason: "unsent_draft" (VUH-2687). Clients use the typed reason to explain that Stop leaves the unsent terminal text intact and direct the owner to that terminal; detail is diagnostic prose, never a discriminator. The stop_task request must opt in with includeReason: true; otherwise the service omits reason for shipped strict parsers. Older services/relays reject the new request field with HTTP 400 before dispatch; a client may retry that schema refusal once with the legacy shape, never an uncertain Stop or other failure. Failures after Stop dispatch return uncertain, not HTTP 400. nextTurnOnly means Claude has only an authenticated next-UserPromptSubmit receiver: Queue can be stored, but Steer cannot wake or modify its current turn. A live exact-session Claude poll offers both modes; unverified Claude offers neither. Codex's native queue is separate from its steer control; OpenCode declares Queue, Pi and Grok declare neither. Mailbox-only seats do not gain native choices from storage.

App composers should match the current seat by occupantId and offer only the listed modes. No modes means plain Send. Missing inputCapabilities on an older service means unknown: offer plain Send, with no inferred Steer/Queue/Stop. Show Stop only when interrupt is true and a task is running; keep ordinary authorization and operation-result handling. Capabilities are observations, never authority or a delivery guarantee. They can disappear after a receiver expires, a native control releases or the occupant changes. Re-read the roster; the service still refuses unsupported explicit modes from older clients. Explicit Steer to next-turn-only Claude is rejected without sending or storage, including when the same text already has a next-turn receipt. Plain Send keeps its existing automatic routing.

After an uncertain app send, send_receipt reads the relay's retained result using the exact original turn and the current paired-device identity. It never dispatches a send. Missing, expired or upstream-failed results stay unknown; clients keep the original blocked rather than interpreting absence as permission to resend. Direct service clients return unknown because that relay cache is not theirs. The existing revision fence and relay deduplication remain unchanged. Definitive HTTP refusals (including a read-only room's 409) are not uncertain transport outcomes. Room transcripts remain read-only.

A request with record: true also gets the relay's record (VUH-2668). received says whether the relay ever took that exact send from this device, even one whose hop then failed. completeForMs says how far back that answer holds: the relay keeps every send it takes from its start, until it drops the oldest for room, and for at most 24 hours, on its monotonic clock. Absence counts only when the sender's own age for that send is less than completeForMs. The sender measures that age when the answer arrives, not when the query left, because the relay computes the window when it receives the query.

That age must come from a monotonic clock that keeps counting while asleep: mach_continuous_time or CLOCK_MONOTONIC on Darwin, CLOCK_BOOTTIME on Linux and Android. A clock that pauses in sleep reads a send taken by an earlier relay process as younger than it is. A sender that cannot name such a clock checks its monotonic age against the wall clock, and treats any disagreement (a sleep, a clock step) as an unknown age, which never allows a resend. No clocks are compared across machines. Without the field the answer is unchanged. An older relay refuses it, and an older client never sees it.

This additive service contract supports the separate VUH-1882 composer work and VUH-1883 Send-to-Stop UI. No app UI is changed in this repository.

The private companion-app and managed-service repositories consume this package from a sibling checkout. A contract change here can affect both. Keep wire identifiers stable, validate at trust boundaries, and coordinate incompatible changes with every consumer. A legacy field such as missionId or a captain.* identifier is a compatibility name, not current product terminology.

Client JSON response boundaries use parseProtocolResponse(schema, value) or safeParseProtocolResponse from the root export. These readers discard unknown object keys at every reported level, then validate the known shape using the original schema. Optional host additions therefore remain readable by an older client of the same wire version. Known values, bounds, versions and discriminators still fail validation when invalid. Dynamic record keys and declared catchall data remain intact. Use ordinary strict schema parsing for requests, persisted state and authenticated encryption envelopes; response tolerance grants no authority and never changes those schemas.

So a response grows only by new optional keys, including new optional objects such as a seat's owner (VUH-1763). A new value in an existing enum or discriminated union is a known-field change that an older reader rejects; give it a new optional key, or a new schemaVersion, instead.

Unknown enum values are not unknown keys. A response enum a newer host may extend, such as a setting key in the Discord setup catalog, uses additiveEnum(values): strict parsing rejects a new value, and the response readers drop the nearest list entry holding one. Keep statuses, versions and discriminators a client must act on as plain enums (VUH-2530).

VUH-1635 exposed this distinction when new subagent metadata made an older app reject an entire successful fleet response. Existing installed clients with strict response parsing need new bundled JavaScript to gain tolerance. Omitting new optional fields at the host's device boundary can mitigate them, but withholds that metadata until those clients update or negotiate a view.

Architecture explains the service boundaries. OpenAPI catalogs HTTP operations. The public network reference renders the host-route allowlist from public-gateway.ts; the hosted account service has separate routes and implementation.

Managed credit schemas and route declarations live in private clankie-ops/packages/hosted-protocol, outside this package. The public service accepts exact optional route declarations from its installed runtime provider; ordinary installations add none. The private companion app keeps its consumed credits decoder locally and does not require the operations repo in CI.

pnpm --filter @clankie/protocol typecheck
pnpm --filter @clankie/protocol test

This package is Apache-2.0. The public/private boundary keeps client and managed-service implementation in their respective repositories.

@clankie/protocol/skin-drafts defines owner-scoped briefs, native key poses, revision-fenced edits and exact-artifact approval. The draft API and CLI require host-proven owner authority; no schema field grants generation, spending or publication.

@clankie/protocol/hosted-browser-session defines account-owned browser cleanup: HostedOwnerDeviceSchema is the signed body's device-membership declaration; HostedBrowserRevocationSchema and its exact-ID receipt describe account-owned revocation. The fleet keeps the idempotent tombstone even while a body sleeps. These payloads carry identifiers, never device tokens or encryption keys.