okami.mom/albedo

git clone https://git.nkp.pet/okami.mom/albedo

dawnquota: filter history to one limit and drop recovered failures from the latest readings711d48a

main
96.0 KiB1665 linesraw

HTTP API design

Status: implemented contract for daemon protocol 3.

openapi.yaml owns the exact request, response, parameter, header, and SSE payload shapes. This document owns behavior: ordering, atomicity, retry, recovery, and resource ownership. Both documents must agree. The OpenAPI version is a document-format version; daemon protocol 3 is a separate compatibility decision.

API architecture explains state ownership and settings crash recovery.

This document defines daemon protocol 3. The daemon and CLI use this contract together. It defines no compatibility aliases or legacy response readers. Changes to this contract require an update here, a client impact review, and behavioral tests before implementation.

The API serves terminal, web, and desktop clients. All current TUI features remain available. Installation discovery, local launch, and the interactive restart offer remain application policy, outside the API connection.

The resource map lists routes. The HTTP rules define authentication, errors, and retries. Sessions and SSE define chat and recovery. Extension contracts define optional domain operations. The feature checklist and behavioral verification describe the supported workflows.

Design decisions

  • Resources own their data and operations. A screen does not get its own route.
  • A session read includes the state needed to open a chat. A collection read includes the state needed to browse sessions or draw an agent family.
  • Ordinary HTTP requests perform changes. SSE delivers live updates.
  • JSON and SSE use the same session resource URL, selected by Accept.
  • Input IDs identify user intent across retries. Stream cursors identify an actor lifetime and position. Transcript checkpoints identify durable history. These identities are separate.
  • Core operations have named, typed contracts. Extensions own separate domain routes. A command menu entry points to its canonical operation.
  • The daemon owns execution state, argument parsing, storage inspection, and progress. Clients own navigation, rendering, local drafts, and confirmations.
  • Bounds apply before subscriber mailboxes. Model execution does not wait for a client to read its socket.

SSE is the sole core streaming transport. The current UI needs server-to-client updates, and SSE retains the existing replay and bounded delivery design. This contract does not add WebSocket authentication or subscriptions. It makes no claim that changing route organization reduces CPU or memory.

Reading and changing the wire contract

Each OpenAPI operation names its request and accepted response schemas. Required properties are listed explicitly. Nullable fields accept JSON null; optional fields may be omitted. Requests reject unlisted fields. Response schemas describe what the server emits; clients may ignore new fields. Tool, provider-private, and extension-specific bodies use the explicitly named DynamicJSON schema. Core resource envelopes have typed fields.

SSE itemSchema describes a parsed event whose data string contains JSON. Decode that string and validate its contentSchema separately. Ordinary schema validation does not assert string-encoded content. The wire contract uses OpenAPI 3.2 streaming definitions. The x-max-utf8-bytes, x-max-encoded-bytes, and content-byte annotations record byte limits that also need explicit runtime checks. JSON Schema maxLength counts Unicode code points, not encoded bytes.

Agree on a shape by editing the operation and its shared schemas, adding a complete example, and reviewing the affected behavior here. Validate the document and examples before changing either implementation. Validate real daemon replies and decoded SSE batches in workflow tests; shape validation does not prove ordering, atomicity, or safe retry.

Resource map

The tables list every platform path template. Extension paths appear in their own section. An omitted method is unsupported. GET defaults to JSON.

PathMethodsResponsibility
/serverGETReadiness, protocol, capabilities, build, quota, notices
/server/shutdownPOSTExplicitly stop the identified daemon instance
/settingsGET, PATCHShared defaults, provider profiles, MCP, secrets, UI preferences
/storageGETOnline storage accounting
/sessionsGETFiltered session summaries, or collection invalidations over SSE
/sessions/{session_id}PUT, GET, PATCH, DELETECreate, attach, configure, delete, or watch a session
/sessions/{session_id}/visits/{visit_id}PUTCount one deliberate opening without counting retries
/sessions/{session_id}/inputs/{input_id}PUT, GETAdmit immutable user input and read its durable outcome
/sessions/{session_id}/inputs/{input_id}/cancelPOSTCancel that input or request interruption of its exclusive turn
/sessions/{session_id}/interruptPOSTInterrupt a captured turn and cancel captured waiting inputs
/sessions/{session_id}/historyGETBounded transcript pages or checkpoint summaries
/sessions/{session_id}/history/{entry_id}GETRead full content of a large transcript entry in bounded pieces
/sessions/{session_id}/active-output/{content_id}GETRead an unfinished output prefix fixed by a captured snapshot
/sessions/{session_id}/contextGETPrepared request inspection and provider request records
/sessions/{session_id}/catalogGETFresh discovery and separately identified loaded composition
/sessions/{session_id}/reloadPOSTReload session composition, model catalogs, or both
/sessions/{session_id}/kernel/upgradePOSTExplicitly replace this session's Python kernel
/sessions/{session_id}/compactionPOSTCompact the model-facing conversation
/modelsGETProvider model metadata, effort choices, context limits, cache policy
/workspacesGETRecent workspaces or one directory with optional repository preview
/hostsGETKnown hosts or one host's cached probe state
/hosts/{host}/probePOSTExplicitly refresh a host probe
/authGETProvider login methods and redacted account metadata
/auth/logins/{login_id}PUT, GET, PATCH, DELETEStart, inspect, answer, or cancel a provider login
/auth/accounts/{account_id}DELETERemove a provider account

There is no generic request endpoint, command dispatcher, operation lookup route, separate status route, preview route, agent tree route, or stream suffix. The underlying durable admission records remain owned by the daemon. Their public representation is the session or input whose identity they protect.

Opening a chat

  1. Use the supplied base URL and bearer token to read /server. Check protocol and required capabilities before opening a screen.
  2. Read /sessions for the picker or family graph. Each row includes its metadata and bounded live state.
  3. Read the selected session. Install its history, pending inputs, status, progress, and cursor together. Count a deliberate opening with one identified visit; background reads do not count.
  4. Watch that same session URL as SSE, passing the installed cursor pair. Apply subsequent events, or replace the daemon-derived view on reset.
  5. Send a message with an identified input PUT. Keep its local handle until the returned decision or matching stream event resolves it. Cancellation targets that input identity.
  6. Fetch older history by durable position. Open context, catalog, or extension resources when the user needs them. Use the validator already displayed with a resource when editing it.

HTTP rules

Addressing and authentication

A client receives a base URL and daemon bearer token. Native local clients may read the protected discovery record. Other clients receive both values through explicit connection setup. No API method reads client installation files, launches a daemon, or replaces one.

Every platform request, including SSE, uses Authorization: Bearer <daemon-token>. The token grants operator access to this daemon. This protocol has no user accounts, tenant isolation, or session ACLs. Provider OAuth accounts are model-provider credentials, not daemon identities. Browser OPTIONS preflights are the exception to bearer presentation. They return only allowed request metadata and cannot read or mutate resources.

The default listener remains loopback. Connections across an untrusted network require HTTPS or an encrypted tunnel. Tokens never appear in URLs, response bodies, diagnostic logs, or stream events.

Browser clients use fetch for JSON and SSE so they can send the authorization header. Native EventSource does not expose arbitrary request headers. The SSE specification defines its constructor and stream format.

Browser access uses an operator-configured list of exact allowed origins. The daemon validates Origin before routing or reading a body. Allowed preflights advertise the supported method and headers, including Authorization, Content-Type, If-Match, and If-None-Match. Responses vary by Origin and expose ETag, Location, and Retry-After. There are no authentication cookies or wildcard origins. Origin approval does not replace the bearer check. This follows the Fetch CORS protocol.

The daemon validates the requested host against its listener and configured public addresses. A supplied origin or host never grants additional permission. Optional extension intake routes declare their own authentication policy.

Encoding, limits, and pagination

JSON uses UTF-8, snake_case field names, and tagged objects for variants. IDs are opaque strings. Paths and query values use ordinary percent encoding. Core timestamps are UTC RFC 3339 strings. Unknown historical timestamps are null. Durations use explicitly named units. Counters and sequence numbers are integers from zero through 2^53 - 1.

Unknown request fields, duplicate JSON keys, invalid enum values, malformed query values, and unknown query parameters return 400 invalid_request. Requests do not silently substitute defaults for invalid values. Unknown response fields may be ignored. Missing required fields and invalid known variants fail decoding.

PATCH uses application/merge-patch+json. Omission keeps a value, null removes an override or optional value, and false remains a real value. Arrays replace the complete array. Writable fields are listed for each resource. JSON Merge Patch defines these rules.

Ordinary JSON request bodies have a 64 KiB limit. Input admission has a 72,200,000-byte limit, room for ten encoded images. JSON responses and SSE data frames have a 1 MiB limit. Resource-specific limits below apply as well. HTTP body framing accepts valid fixed-length and chunked requests, enforcing the same byte limit while reading. Ambiguous framing, unsupported content encoding, and incomplete bodies fail without executing the operation.

zstd is the one content coding. A JSON or text response of at least 1 KiB is compressed when Accept-Encoding admits zstd, and so is a session or collection event stream, flushed after every frame. Limits and ETag values describe the decoded representation, and these responses add Vary: Accept-Encoding. Image resources are never coded. The daemon needs OTP 29, whose zstd can flush a stream between frames. When GET /server lists the zstd_requests capability, a request body may use Content-Encoding: zstd. It must be one frame that declares its decoded size, which must fit the route's limit; anything else is 400. The CLI compresses bodies of 16 KiB or more.

Lists return { "items": [], "next": null }. next is an opaque continuation token tied to the query and representation, including limit. Follow-up requests retain the original query and add next. Default limit is 50; maximum is 200. Each page also respects its encoded byte limit, including JSON escaping. Catalog entries, glances, input outcomes, and reset snapshots obey that bound even when their item counts fit. Large content uses its documented paging path; summary text is shortened explicitly. A single catalog entry is at most 64 KiB. Session glances include at most 16 entries of 8 KiB each. Tokens never contain credentials and cannot authorize access. A token for another query returns 400. Missing singleton resources return 404; an empty collection returns 200.

Collection traversal uses a stable sort key and ID as its tie breaker. Sessions use most recent activity first. A traversal is not a frozen global snapshot: concurrent edits can move rows. Clients key rows by ID and refresh on invalidation. History traversals use immutable transcript positions and do not have this limitation.

Revisions and errors

An ETag validates the complete selected JSON representation. Configuration has a separate representation from volatile session state. Session reads expose the URL and validator for view=configuration; settings reads expose the URL and validator for each group. Editing those representations uses If-Match without conflicting with unrelated progress or another settings group. Extension item reads likewise expose the item's observed URL and validator.

PATCH and conditional DELETE require the observed If-Match. Missing preconditions return 428; stale ones return 412. Checks and the corresponding commit occur under the owning resource's serialization or transaction boundary. Validators are opaque; clients never derive them from an integer revision. Mutation envelopes carry new resource validators explicitly and are not cached as canonical read representations.

Errors use application/problem+json, with type: "about:blank", title, status, detail, and a stable Albedo code. An optional fields array contains JSON pointers and safe explanations. Decisions described below can add a typed decision field. Clients branch on status and code, never wording. Raw request bodies, secrets, and provider responses do not appear in errors. Problem Details defines the common envelope.

StatusMeaning
200A read or synchronous operation returned its documented result
201A resource was created; Location identifies it
202Input is durably accepted, or an explicitly identified action is underway
204A deletion or cancellation needs no response body
400Invalid syntax, fields, query, or identity timestamp
401Missing or invalid daemon token; includes WWW-Authenticate: Bearer
403Disallowed browser origin or caller permission
404Unknown current resource or disabled extension route
405Known path, unsupported method; includes Allow
406Unsupported response media type
409Busy state, identity conflict, stale catalog, or incompatible operation
410Retained deletion, expired identity, or obsolete prepared context
412The observed representation or creation precondition no longer holds
413Body exceeds its limit
415Unsupported request media type
428Required precondition is missing
429Admission or rate limit; includes Retry-After where a later new intent can succeed
503Owner, storage, or upstream preparation is unavailable

All authenticated data responses use Cache-Control: no-store. Clients may keep in-memory resource state and validators. Errors after SSE headers use the stream failure contract rather than pretending to change the HTTP status.

Retries and cancellation

Reads can retry transient connection failures with capped backoff and jitter. They remain subject to the caller's deadline. Authentication and protocol errors are actionable failures, not evidence that a daemon is absent.

Session creation, input admission, visits, and login creation retain the original ID and encoded intent during recovery. Other changes have no automatic replay after a lost response. A precondition or a resource-specific identity can make an explicit retry safe, as specified below.

Canceling a client context stops its requests and subscriptions. It does not undo an accepted input, interrupt a shared turn, stop a detached daemon, or authorize replacing one. Those are separate operations. Transport cancellation after a mutation was sent can leave its outcome uncertain.

Server, settings, and storage

Server identity and shutdown

GET /server returns instance_id, protocol, state, capabilities, build, digest, extensions, quota, and notices. state is ready or draining. instance_id changes at each daemon startup. build and digest are nullable update information. Attachment requires protocol 3; optional operations check the advertised capability before invocation.

Capabilities are a map from stable feature names to integer contract versions. The required version-1 features are durable_inputs, collection_invalidation, and tool_progress; session_replay requires version 2. Optional features include context, catalog, workspace_browsing, host_probes, provider_auth, and storage_report. Extension entries identify enabled services and their contract versions. Builds cannot substitute for protocol compatibility.

quota contains the latest recorded observations per account and limit. Each observation includes durable sequence, provider and account labels, limit ID, plan, observed percentage, window, reset time, scope, status, source, and safe failure details. Unknown percentage is null. include=quota_history adds the same records newest first, paged by limit and next. History may be filtered to one limit with quota_provider, quota_account, and quota_limit (required together), and optionally bounded with quota_since (RFC 3339 UTC). Notices have id, kind, and message; dismissal is a shared UI setting and does not consume a read.

POST /server/shutdown accepts instance_id and timeout_ms, default 5,000 and maximum 30,000. A different instance returns 409 instance_changed. The daemon marks itself draining, refuses new admissions, stops active turns, flushes durable state, and closes. Accepted waiting inputs remain durable for the replacement daemon. 202 reports instance_id and state: "draining". The client observes closure with a bounded wait. It cannot use this endpoint to start the replacement.

The interactive CLI still offers to keep or restart a discovered daemon. Keeping it is the default. Its approved instance identifies the shutdown target. Daemon attachment and local startup owns this user flow and deliberate operator overrides.

Shared settings

GET /settings returns these groups and group_resources, a map of group name to canonical URL and etag. GET /settings?group={name} returns just that group's canonical representation and its ETag header.

GroupFields and meaning
providersdefault_profile and profiles keyed by name. Profiles contain provider extension, endpoint, default model, effort, nullable image edge, nullable OAuth account, and has_key.
mcpdefinitions keyed by name: configured enablement, transport, URL or command with arguments, nonsecret environment and headers, timeouts, and secret-presence metadata.
extensionsGlobal default enablement keyed by extension name.
capabilitiesGlobal skill, instruction, and MCP preferences keyed by their stable preference key.
modelsPer-model raised context choices and the cache TTL prior table, with source and units.
uithinking, tools, and dismissed_notices. Per-session pin, archive, and visits are session fields.

PATCH /settings?group={name} changes exactly one group per request. Its body is a merge patch of that group's fields, with the observed group If-Match. The group owner checks its validator in the same commit boundary. It validates the entire patch before effects and commits that group's desired state atomically. Provider profile data and its secrets have one settings owner; MCP definitions and their secrets have another. Their persistence must recover the complete old or complete new group after a crash, including when storage uses files. A multi-group patch returns 400; there is no partial PATCH success.

Provider keys and MCP secrets are write-only patch fields. Provider api_key, MCP bearer_token, and individual secret header or environment entries accept a replacement string or null for removal. Omission retains the existing secret. Reads return presence and secret names, never values. Response-only presence fields are rejected on writes.

MCP changes validate the candidate connection before saving. A failed probe keeps the previous definition and secrets. This removes the client-side save-then-undo sequence. A request can deliberately set validate_connection to false; that control is not persisted. The response reports validation state and safe diagnostics.

Changing a profile does not implicitly select it. Changing providers.default_profile explicitly sets the default for new sessions. Changing a per-model raised-cap choice affects that model's next prepared requests across sessions. models.raised_caps maps returned cap_key values to booleans: true uses the advertised maximum, false uses the ordinary limit, and null removes a choice. Missing choices default to false. Clients do not construct cap keys. Existing requests keep their captured settings.

Global capability selection accepts catalog_session_id, catalog_revision, and candidate-ID choices; the daemon resolves the IDs to preference keys. The read representation exposes persisted preferences. These write-only selection fields prevent stale discovery from changing a different file.

The provider group uses these exact profile fields: extension, endpoint, protocol, model, effort, image_edge, account_id, project, location, and response-only has_key. Protocol is responses or chat_completions. Nullable fields keep their documented absence; model and extension are required on creation. Deleting a profile removes its key. Removing the selected default requires an explicit replacement or default_profile: null in the same atomic patch.

For example, a provider-group patch selects a profile explicitly:

{
	"default_profile": "personal",
	"profiles": {
		"personal": {
			"extension": "openai",
			"endpoint": "https://provider.example/v1",
			"protocol": "responses",
			"model": "chosen-model",
			"effort": null,
			"image_edge": null,
			"account_id": null,
			"project": null,
			"location": null,
			"api_key": "replacement-secret"
		}
	}
}

An MCP definition has enabled, transport: "stdio"|"http", command, arguments, cwd, url, environment, headers, bearer_token_env_var, enabled_tools, disabled_tools, startup_timeout_ms, and call_timeout_ms. Transport selects the applicable command or URL fields. Environment and header values are tagged { "source": "literal"|"env", "value": ... }. Environment references resolve on the daemon host. Tool filters preserve the current MCP allow and deny controls. Definition enablement defaults to true.

Writes add a secrets object containing bearer_token, environment, and headers. Secret maps accept replacement strings and per-key null removals. Reads replace that object with secret_presence, containing bearer presence and the two lists of names. Deleting a definition deletes its associated secrets. Candidate validation rejects duplicate environment or header names across public and secret maps. Header names compare case-insensitively. It also rejects more than one bearer source: bearer_token_env_var, secrets.bearer_token, or an Authorization header. Switching sources requires removing the old source in the same patch. The validated connection uses the candidate's resolved values. validate_connection is a write-only group control, outside definitions:

{
	"validate_connection": true,
	"definitions": {
		"example": {
			"enabled": true,
			"transport": "http",
			"url": "https://tools.example/mcp",
			"secrets": {
				"bearer_token": "replacement-secret",
				"headers": {"X-Old-Key": null}
			}
		}
	}
}

MCP capability preferences are separate from configured enablement. Effective selection uses the session override, then the global preference, then true. A definition with enabled: false remains unavailable regardless of selection. Global MCP preference changes use the same catalog revision guard as global skill and instruction choices. Other group writes use extensions.defaults, models.raised_caps, or ui.thinking, ui.tools, and ui.dismissed_notices. Capabilities read as preferences keyed by stable preference key; their write-only selection fields are siblings in the capabilities group.

Every successful patch returns { "group": ..., "resource": ..., "application": ... }. resource contains the updated value, canonical URL, and validator. application includes desired revision, active service revision, composition_changed, validation, and warnings. composition_changed says whether the save changed the desired composition; it is always false for providers, models, and ui, which never feed it. A patch does not observe sessions: GET /sessions?needs_reload=true pages the ones that need a reload, observing loaded sessions a few at a time. Persisted defaults do not claim that every open session reloaded. Global services use the new desired state on subsequent requests. Sessions apply composition through their explicit reload operation.

Storage accounting

GET /storage returns database, sessions, images, files, and measured_at. Session items include ID, title, workspace, creation and activity timestamps, and estimated content bytes. Images report shared count and bytes. Database fields include main-file bytes, WAL plus SHM bytes, page size, total pages, free pages, and estimated used bytes. Files contain daemon-owned state file categories, sizes, and cleanup-candidate metadata. The database keys are main_file_bytes, wal_bytes, page_size_bytes, page_count, free_page_count, and used_bytes. Sessions have estimated_content_bytes; shared images have count and bytes.

Session estimates count pinned context, transcript content, cell sources, payloads, and traces. Shared images count once outside session estimates. Estimates do not claim allocated disk space per session. Database measurements share one read boundary; filesystem measurements describe the inspection period. Session and file lists use separate next tokens within this response, with each token identifying its section. Totals always cover the complete store.

An online client needs no filesystem or Python access. Returned paths describe the daemon's files and never authorize local deletion. Offline diagnosis and approved cleanup remain explicit local operations under the ownership lock.

Sessions and durable input

Session representations

SessionSummary contains id, name, automatic_name, workspace, parent_id, root_id, address, depth, closed, created_at, activity_at, provider_profile, model, effort, status, preview, preferences, current_progress, activity, and cursor. Each summary captures its own status, progress, activity, and cursor together. Family address stays stable when display name changes.

status contains phase, nullable run_id, interrupt_requested, and a nullable safe blocking_reason. Phases are idle, preparing, generating, running, interrupting, and compacting. preview has at most 256 Unicode scalar values and 1,024 UTF-8 bytes, and includes the durable transcript row count. One transcript row can produce several history entries. Continuation markers do not add transcript rows. preferences contains pinned, nullable pin_order, archived, and opens. Pin order preserves the shared ordered pin list. Pinning assigns its order once; repeating pinned: true does not move the row or increment anything. Unpinning clears the order. Only pinned and archived are writable; opens and pin order are read-only.

activity provides the agent graph's live tails without a session subscription for every node. It contains up to 12 lines, each with kind and text. Kinds are assistant, thinking, tool, input, note, and error. The current partial line is included. Each line has at most 256 Unicode scalar values and 1,024 UTF-8 bytes; the complete encoded activity object is at most 16 KiB, shortening oldest lines first. Tool-argument previews stay in current_progress and are not copied into these lines.

Activity also includes output_scalars, output_utf8_bytes, and observed_at. The counters cover generated text and reasoning in this actor generation. Clients derive display rates from successive samples and decay them while quiet. Nullable latest_input: { "input_id": ..., "source": ..., "bytes": ... } and latest_answer: { "message_id": ..., "bytes": ... } let clients animate new human input and completed root answers by identity. Input source is chat, agent, or system; bytes count UTF-8 display text. Initial reads install these identities without replaying old animations. The actor maintains this bounded activity once, before subscriber fan-out. Full content stays in history.

Session adds:

  • creation, the immutable submitted creation intent and separately resolved defaults.
  • revision and family_revision for conditional edits and family operations.
  • configuration_resource, containing its canonical URL, value, and etag.
  • workspace_change, nullable desired versus active workspace and deferred state.
  • selection, containing session overrides and effective selections.
  • composition, containing desired and loaded revisions, needs_reload, dependencies, quarantine diagnostics, and capability availability.
  • kernel, containing state, build identity, staleness reasons, live-job count, and up to 100 running-job summaries ordered by ID. Each summary names the job, its observed PID (nullable), and its command (at most 4096 scalars). Unknown counts and lists are null; known empty lists are []. The count can exceed the bounded list. Reads observe the kernel without preparing it. No Python variables or executable process handles leave the daemon.
  • pending_inputs, at most 32 bounded input summaries with acceptance order, and input_order, the greatest order ever accepted into this session.
  • usage, including token and cache counters, elapsed time, model limits, observed cache TTL, and time-indexed cache fade observations. elapsed_ms is the span of the provider call that produced the usage, and tokens_per_second is its completion tokens over that span; both are null when no call was timed.
  • history, a bounded recent page, with its older-page token.
  • cursor, the generation and sequence captured with the live state.
  • glances, bounded extension-provided summaries and links to their resources.

input_order is zero before the first admission.

Pending summaries include input ID, kind, display preview, admission time, acceptance order, delivery state, and blocking reason. They omit image content and expanded model input. Collection summaries and detail reads do not parse tool arguments or reconstruct previews.

Collection reads

GET /sessions accepts scope=roots|all and optional parent_id, family_id, workspace, search, archived, and needs_reload. The parent and family filters are mutually exclusive. family_id selects the containing root and all descendants. search matches display names and ID prefixes. sort=activity|frequent defaults to activity; frequent sorts by opens, activity, and ID. Pinned state remains available for client grouping.

The CLI's prompt, read, send, and stop workflows resolve a full lowercase UUIDv7 with GET /sessions/{id}?view=configuration, including child sessions. Only shortened IDs search the root-session collection and require an unambiguous match.

Parent and family queries default to scope=all; other queries default to roots. An explicitly supplied scope=roots with a parent or family filter returns 400. The result is a page of SessionSummary objects. A family query also returns family: { "root_id": ..., "revision": ... }. Agent trees are drawn from parent_id, address, and depth; there is no second agent identity or graph endpoint. Clients can traverse all pages. A missing requested family returns 404, while an existing parent with no children returns an empty page.

Accept: text/event-stream selects the collection watch described below.

Creation, forks, and children

PUT /sessions/{session_id} requires a fresh client-generated UUIDv7 and If-None-Match: *. Its body is one creation variant:

kindFieldsBehavior
newworkspace; optional name, provider_profile, model, effortCreate an independent session.
forksource_session_id, checkpoint_id; optional nameCopy the durable prefix and selection overrides into an independent session.
childparent_id, address, name, initial_input_id, task; optional model, effortCreate a family member inheriting workspace and provider, with an initial durable task.

The daemon resolves defaults once at first admission. Child creation, family membership, creation decision, and initial input admission commit together. A rejected task leaves no orphan child. The child task is ordinary identified user input. Forks preserve full durable tool arguments and results, image references, captured execution traces, continuation boundaries, and selected extension overrides. They copy no Python namespace, live jobs, progress, or usage totals. The daemon closes incomplete tool exchanges at the chosen checkpoint using its existing history rules.

Success returns 201 with Session, its Location, and ETag. A retained rejected creation returns its original problem and admission status on retry. An existing session fails the create precondition with 412. Recovery reads the session and compares creation.submitted structurally with the original intent. Optional creation fields absent from the request appear as null in this descriptor. String contents stay exact. Matching intent proves creation; a different intent is an ID conflict. Resolved workspace and provider defaults appear in creation.resolved and do not change the submitted descriptor. Current model, title, or workspace cannot replace that provenance check. Sessions stored before immutable provenance was recorded return creation: null. They remain readable, editable, and forkable, but cannot prove a matching creation request. Migration never invents submitted intent from their current configuration. Every newly admitted creation has provenance. Conditional creation follows HTTP If-None-Match semantics.

Creation decisions and deletion tombstones remain queryable at the same URI. Rejected creation reads return their original problem with decision metadata; deleted session reads return 410 session_deleted with creation provenance. An unknown young ID returns 404, which cannot prove an in-flight PUT will never commit. Recovery retains the same intent and identity.

GET /sessions/{session_id} returns Session. tail, default 100 and maximum 200, selects the recent history window. The byte bound can shorten the window. Status, progress, pending ownership, and the cursor share one session-actor capture. History is read through that capture's durable high-water mark. The configuration view does not accept tail or stream cursor parameters.

Changes, visits, and deletion

GET /sessions/{session_id}?view=configuration returns only editable metadata, preferences, overrides, revision, and family revision, with a strong ETag. The full session read embeds this same value and validator for an edit. PATCH /sessions/{session_id}?view=configuration accepts name, provider_profile, model, effort, preferences, and selection. An empty or null name restores the automatic title for a root, or the original family name for a child. Pin and archive changes are shared preferences. Model and composition changes require an idle session. Selection maps accept true, false, or null to clear the override and inherit the global default. A patch enabling one compaction strategy clears competing earlier true overrides to inheritance; enabling multiple strategies in the same patch returns 400 selection_conflict, and explicit false or null choices keep their meaning. When model or profile changes without an explicit effort, keep a supported current effort, then try the profile default and model default. An explicit null clears effort; an explicit unsupported level fails validation. Selection has extensions, skills, instructions, and mcp maps keyed by their catalog IDs. MCP entries enable or disable installed server definitions; their IDs are stable definition names. Definitions and secrets belong to shared settings. Skill and instruction choices additionally include the write-only catalog_revision from the discovery read. A stale catalog returns 409 catalog_changed. The session owner prepares a valid candidate before committing the patch. Editable session data and preferences commit together through the store owner. The result includes the new configuration resource and captured session state. PATCHes set explicit values; they never toggle or increment preferences.

For example, a session selection patch clears one override and disables a skill:

{
	"selection": {
		"extensions": {"browser": null},
		"skills": {"candidate-123": false}
	},
	"catalog_revision": "observed-discovery-revision"
}

Workspace changes use the same PATCH with workspace as its only change, alongside the observed family_revision as a write-only precondition. The daemon validates the destination and captures following descendants under the family revision. A busy target session returns 409 session_busy before any move; the UI retains its deferred move intent. An idle parent can move while a descendant is busy. Destination metadata and deferred descendant intents commit in one store transaction before kernel cleanup. Busy descendants finish their captured turn in the old workspace and apply the new workspace at idle. Their workspace_change reports desired location, active location, and state deferred or applying. This intent survives client disconnect and daemon restart.

The move preserves transcript and family addresses, resets affected kernels and Python namespaces, and returns applied and deferred counts, bounded ID lists, truncated, and cleanup warnings. When truncated, clients refresh the family rather than repeating a move to discover its result. Cleanup failure cannot be reported as rollback of a committed move. Affected sessions cannot begin a new turn in an old kernel after the commit. A later explicit move supersedes the earlier desired location by revision; a delayed completion cannot restore an older choice. New descendants inherit the parent's desired workspace at their admission.

Model selection does not implicitly change the default for new sessions. The TUI's model picker explicitly updates the shared default when appropriate. A later default or cap failure leaves a confirmed session model change visible.

PUT /sessions/{session_id}/visits/{visit_id} accepts an empty object and counts one deliberate attachment. Repeating the UUIDv7 returns the same count without incrementing it again. Background refreshes do not create visits. Reads never change opens. Deleted sessions cannot be revived by a visit.

DELETE /sessions/{session_id}?view=configuration defaults to scope=leaf. It requires the configuration If-Match, an idle session, and no children. scope=subtree also requires the family_revision query parameter from the family read. The daemon captures exactly that membership, prevents new child admission during this operation, interrupts members, cancels pending inputs, and deletes deepest first.

Deletion returns 200 with state: "complete"|"partial", deleted_count, remaining_count, deleted_ids, and remaining objects containing ID and safe reason. Counts cover the captured operation; lists contain up to 200 IDs each and can shorten further to meet the byte limit, with truncated: true. Clients refresh the authoritative family or read known deletion tombstones. They never repeat deletion to page its result. Partial deletion is visible. Any successful deletion changes the family revision. A retry with the old revision cannot broaden the operation or delete new descendants. The client refreshes and confirms the remaining membership before a new deletion attempt. This operation has no automatic replay after transport loss.

Input admission and outcomes

PUT /sessions/{session_id}/inputs/{input_id} uses a client UUIDv7. The body has optional client_id for echo correlation and exactly one variant:

kindFields
messagetext; optional images, up to ten, each with mime_type and base64 data; optional pastes, up to twenty texts
continueNo content fields
skillcandidate_id, catalog_revision, and arguments as text
commandcommand_id and typed arguments; only a catalog entry with delivery: "input"

A message requires nonblank text or an image. Images accept PNG, JPEG, GIF, and WebP, subject to decoding, dimension, and selected-provider bounds. The model receives the text, then the images in upload order. The CLI marks where each was attached with [Image #N] in the text, numbered from 1 in upload order. Pastes are marked the same way, [Paste #N, …] for the Nth paste. The model receives a paste under 100 lines and 32 KiB in place of its marker. A longer one is saved to $ALBEDO_HOME/pastes/<session>/<input>-<n>.md and its marker names that file. History shows the text as sent, markers intact. The daemon preserves original upload metadata and the full durable payload. Provider-specific fitting stays daemon-owned and appears in durable history.

Command inputs prepare a user turn. They do not invoke arbitrary management handlers. Their preparation produces content, runs outside the session actor, and persists the resolved content before transcript consumption. Preparation must be repeatable without external side effects. An extension that performs effects exposes its own domain operation instead.

The first accepted PUT returns 202 with Input. An identical PUT returns the retained admission decision and current delivery state, using the original admission status. Changed intent under the same ID returns 409 input_conflict. Echo-only client_id does not change identity. Lookup happens before current workspace, skill, provider, and queue validation. Defaults and skill content resolve once. Rejected decisions are durable, including 429 queue_full.

GET /sessions/{session_id}/inputs/{input_id} returns the known decision even after session deletion. Input contains id, session_id, kind, admission, accepted_at, acceptance_order, delivery, blocking_reason, transcript_position, and nullable turn. Admission is accepted or rejected; rejected admission includes the original safe problem and HTTP status. Delivery is pending, committed, or cancelled, and is null for rejection. A continuation has a durable position without a text row.

turn identifies consumed turn membership and any durable terminal outcome: running, completed, interrupted, failed, or abandoned. A restarted daemon marks an unfinished previous run abandoned rather than claiming its external effects completed. One turn can contain several inputs. Its nullable outcome contains separately recorded detail; null means no detail was recorded. Acceptance means safe storage. Commitment means transcript consumption. Neither means that the assistant answered or a tool effect ran exactly once.

There are at most 32 waiting inputs per session. A duplicate consumes no slot. Consumption commits transcript rows, images, input delivery state, turn membership, and pending removal in one store transaction. Failure leaves the input pending. Preparation failure exposes a safe blocking reason; it cannot install a successful delivery state. Waiting accepted inputs survive restart.

Known pending identities and inputs belonging to an unfinished turn never expire. An input becomes terminal only when delivery and its associated turn are terminal. Its admission record remains for seven days after that point. Rejected decisions become terminal at rejection. Live sessions retain creation provenance for their lifetime. Deleted sessions retain tombstones for seven days. Known decisions are checked first. Unknown UUIDv7 identities older than seven days return 410 identity_expired and cannot execute; identities more than five minutes ahead return 400 identity_future. This prevents recreation after deduplication records expire. These rules also cover visit identities.

Recovery queries the same input URI and may resend the exact same PUT within the caller's deadline. 404 means unknown at the time of the read, not safe to invent another ID. 410 stops automatic recovery and keeps an unresolved UI row. A matching durable event can still reconcile that row later.

Targeted cancellation and interruption

POST /sessions/{session_id}/inputs/{input_id}/cancel accepts an empty object. It returns the current Input and one result:

  • cancelled: a waiting input was removed and its cancellation committed.
  • interrupt_requested: its current turn belongs exclusively to this input.
  • shared_running: other inputs share the active turn; the turn continues.
  • not_pending: no waiting or interruptible exclusive work remains.

Every input kind uses the same identity for admission, membership, and cancel. This operation never interrupts a later unrelated turn. Repeating it reports the current outcome of the same input, without resubmitting that input.

POST /sessions/{session_id}/interrupt requires run_id, nullable, and through_input_order, taken from the session snapshot. It cancels still-waiting inputs at or below that order and requests interruption of exactly that run. Newer queued inputs remain accepted. An already ended run is reported as ended; a later run is never interrupted by retrying this request. Acceptance of an interrupt request does not mean all external tools stopped.

The result contains run_id, state: "requested"|"already_ended", cancelled_input_ids, and warnings. Interruption prevents new inputs joining the captured run. Live status reports interrupting until the run ends.

History, context, catalogs, and runtime

Durable history

GET /sessions/{session_id}/history accepts view=entries|checkpoints, default entries, and one of before, after, or next. Positions are durable transcript positions, unrelated to the actor cursor. Pages retain ascending display order and include older, newer, and high_water. Default entry count is 100, maximum 200, with a 256 KiB content budget.

Each entry has stable id, position, kind, turn_id, input_id, created_at, content, nullable checkpoint_id, and content_complete. User entries, assistant answers, and tool calls expose checkpoints for their inclusive durable transcript row. Calls packed into one row share a checkpoint; a fork cannot split that row. Checkpoint view lists user turns for the TUI fork picker. Recorded thinking duration is exposed as thinking_duration_ms; live thinking completion can report elapsed_ms. These measure thinking, not the whole turn.

Kinds are user, assistant, thinking, tool_call, tool_result, note, continuation, compaction, and image_fit. Content is a tagged set of text, JSON tool arguments or results, image metadata, and execution trace fields. Tagged daemon notes retain their native origin as a JSON part with field: "origin". Tool names, native call IDs, normalized progress-call IDs, activity summaries, and file diffs remain available. Provider-private fields are explicit opaque data; clients do not reinterpret them as execution instructions.

The daemon pages on complete display groups when they fit. A large group uses content references rather than growing a response past its bound. Preview truncation is explicit and never changes the stored content. Checkpoint view contains position, valid checkpoint ID, title, turn type, and bounded preview. It provides the TUI tree and fork picker without a separate tree route.

GET /sessions/{session_id}/history/{entry_id} returns the full entry content as pages of parts, each with field, offset_bytes, text, and complete. Text and JSON fields use their exact stored UTF-8 encoding; offsets are bytes, and pieces end on UTF-8 boundaries. Opaque next identifies the next piece. The decoded content budget is 256 KiB per page. The daemon shortens a page further when JSON escaping would reach the common encoded response bound. Image fields are not paged. An image's metadata gives its MIME type, dimensions, byte count, and a reference to GET /sessions/{session_id}/history/{entry_id}/{field}, which returns the stored bytes with the image's own Content-Type. Clients can reconstruct full arguments, results, and images without filesystem access.

Prepared context and request records

GET /sessions/{session_id}/context defaults to view=summary. It describes the latest actual prepared request, never prepares a new one. The summary has state: "pending"|"ready", nullable snapshot_id, capture time, provider, model, protocol, context-window tokens, compaction observations, and sections. Each section contains ID, label, kind, source, item count, UTF-8 byte count, page count, and bounded preview. Pending state contains a reason.

view=section requires snapshot_id, section_id, and a zero-based page. A page has text, omitted-content explanation, page index, and page count. Text has a 32 KiB UTF-8 limit and ends on a character boundary. Only the latest snapshot is retained. If it changes during navigation, the old identity returns 410 context_changed; the client refreshes the summary. A page never silently comes from another prepared request.

view=requests returns paged provider request records. after is a durable record sequence. Records include provider profile and account labels, model, nullable run identity, timestamps, request kind, outcome, HTTP status, token observations including reasoning and cache writes by TTL, cache marks, head and projection hashes, input counts, compaction strategy, bounded safe failure details, and links to source transcript positions. Unknown historical run identity is null. Full provider request bodies and credentials are absent. Estimated and observed token counts have separate fields and sources.

GET /models?view=cache-policy reads the cache-policy table and its layer diagnostics. Entries retain matching patterns, clock, tiers, prices, survival, evidence, sources, and notes. limit and next page entries. Optional extension, host, and model selectors return the first matching entry in matched, independent of the displayed page. This view does not require a provider profile and never connects to a provider. Catalogue view remains the default.

Discovery and loaded composition

GET /sessions/{session_id}/catalog returns discovery and loaded as separate fields. Optional kind=extensions|skills|instructions|mcp|commands filters entries without changing these identities. Discovery contains revision, workspace, candidates, and diagnostics. It reflects current local files and preferences. Remote files come from the last observed mirror; preparation refreshes stale mirrors. Loaded contains its composition revision and prepared command definitions. Reading discovery does not reload the prompt or start a kernel. Before a composition exists, native_commands supplies static HTTP declarations from selected extensions. Clients use these declarations only while the loaded revision is null; observing them does not prepare an extension or start a kernel. If desired discovery fails, discovery is null and discovery_failure contains a safe reason. Retained loaded commands remain readable. Successful discovery sets discovery_failure to null; clients need a discovery revision to edit selection.

Candidates contain id, kind, title, description, lexical source, nullable resolved_source, preference_key, valid, eligible, effective_enabled, nullable global preference and session override, shadowed_by, dependencies, quarantine state, and diagnostic. Extension candidates also include typed context, tool, Python module, and plugin metadata for inspection. Other candidate kinds return extension: null. Selections use candidate IDs and the observed discovery revision. Paths are daemon metadata, not client filesystem instructions. Diagnostics survive missing, invalid, shadowed, and quarantined entries.

Command definitions contain ID, slash name, description, typed argument declarations, caller permissions, and delivery: "read"|"mutation"|"input". They identify either a canonical HTTP operation or a durable input variant. There is no public endpoint that accepts an arbitrary slash command string. The CLI parses slash syntax into the named operation; Python bindings call the same domain owner with trusted model caller context.

Optional page descriptors contain title, empty-state text, rows, glance, and actions. Rows include ID, text, badge, tone, detail, and the observed resource URL and etag for an editable item. Actions declare label, optional keyboard hint, confirmation text, HTTP method, relative path template, typed path, query, body, and header bindings, and result schema. Path, query, and header binding keys name declared parameters. Body binding keys are JSON pointers into the request object; they also support nested fields. Bindings read a literal, the displayed row, submitted form, or installed session snapshot. Form fields can declare default_binding with a row or session source and JSON pointer to prefill an edit. The resolved value replaces the literal default and must fit the declared field type. Missing optional form values omit their bindings. Result schemas are JSON Schema 2020-12 fragments, without remote schema retrieval. An operation can declare success_status as 200 or 201. Without it, reads require 200 and mutations accept 200 or 201; acknowledgment validation still applies. An edit binds If-Match to the row's observed validator. The adapter never fetches a newer validator just to overwrite data from a stale screen. Field types are text, secret, boolean, choice, integer, or hidden literal. Descriptors cannot change retry policy or authorization. The adapter rejects foreign origins and undeclared extension routes. Extension-specific response content stays an explicit dynamic JSON body, such as json.RawMessage in Go.

Reload, kernel upgrade, and compaction

POST /sessions/{session_id}/reload accepts target: "session"|"models"|"both", default both. Session reload requires idle, builds the candidate composition before swapping it, and retains the previous composition on failure. Success preserves the Python namespace and pinned prompt prefix, except where an extension's declared lifecycle requires restart. The result includes loaded revision, restart requirements, and warnings. Model reload reports success or failure per provider and keeps old catalog data where refresh failed. It also reloads cache policy, reported separately as cache_policy with state and failure; session-only reload returns null there. both returns these outcomes explicitly; one confirmed refresh is not hidden by failure of the other.

POST /sessions/{session_id}/kernel/upgrade accepts an empty object and requires idle. It stages the new kernel, reports the fate of live jobs, copies supported namespace state, and swaps only after validation. Failure keeps the previous kernel usable where the existing upgrade boundary permits it; stopped external jobs are reported as stopped and cannot be claimed restored. The response contains old and new identities, upgrade state, stopped jobs, and warnings.

POST /sessions/{session_id}/compaction accepts an optional strategy. It requires idle and compacts the model-facing projection without deleting transcript history. A supplied strategy first becomes the session override. The response reports selection_applied, the effective strategy, compaction state, before and after counts, and safe failure details. A failed compaction can retain a successfully selected strategy; the response preserves that fact. Neither runtime action automatically retries an uncertain external effect.

Models, workspaces, hosts, and provider login

GET /models requires provider_profile, or provider with optional endpoint for a not-yet-saved profile. It always returns model objects, never a mode-dependent array of plain IDs. Objects contain ID, label, effort choices, default and effective context tokens, nullable maximum context and output tokens, input modalities, image edge, raised state, and cache policy. Unknown facts are nullable. Optional model returns a single metadata row for a requested ID, including an unlisted manual ID and its cap_key. Manual IDs remain accepted when the provider cannot enumerate them. Metadata includes source and freshness.

GET /workspaces without location lists recent canonical workspaces with use counts and last activity. With location, it returns directory, child directories, canonical parent, home, host state, and pagination. include=preview adds repository facts, language shares, and a bounded two-level file tree. Git and jj facts retain their distinct typed variants. A missing optional VCS fact is null with a diagnostic, rather than loss of the entire directory. The tree has at most 12 top-level entries and four children per directory; more counts omissions. Local and SSH locations share this representation. Paths are interpreted and canonicalized on the daemon's host.

GET /hosts returns known SSH targets and cached state, optionally filtered by target. State is unknown, probing, ready, needs_auth, unreachable, or unsupported, with safe detail, observation time, OS, architecture, home, and nullable authentication instructions. POST /hosts/{host}/probe starts or joins one bounded probe and returns 202 with the host state. Repeated probe requests coalesce. GET does not start SSH sign-in.

SSH sign-in remains an explicit operator action. A local CLI can perform the existing terminal handoff using daemon-supplied target and control-path metadata after confirmation. A client on another machine displays instructions for signing in on the daemon host. This API does not promise a browser terminal. Filesystem failure codes distinguish missing workspace, needs-auth, unreachable host, unsupported host, and permission denied.

GET /auth returns login providers, their form fields and supported flows, redacted accounts, and account selection metadata. It does not duplicate credential values from settings. PUT /auth/logins/{login_id} uses UUIDv7, with provider and declared typed form values. The server records immutable login intent and returns URL, expiry, state, and manual-input instructions. First creation returns 201; an identical PUT returns the existing flow with 200. Changed intent returns 409 login_conflict. Same-ID comparison stays inside the daemon and includes the private form values. A duplicate never starts a second flow. Login secrets never appear in the read representation or its fingerprint provenance fields. Fingerprints are server-created and are not exposed for low-entropy login secrets.

GET inspects that flow. PATCH supplies response, such as a pasted callback URL or provider code, under If-Match. DELETE cancels it idempotently. States are waiting, exchanging, complete, failed, cancelled, and expired. Flow reads include bounded safe progress and globally unique opaque account IDs that also identify their provider. Completed flows return account metadata. Terminal flow metadata remains for 15 minutes, uncompleted flows expire after their advertised provider deadline, and at most eight flows are active. Duplicate creation never opens a second flow. An expired or evicted ID cannot restart a flow. Unknown IDs older than the 15-minute flow recovery window return 410.

Login admission intent and terminal decisions survive daemon restart within their retention window. A restart marks an unfinished flow failed with code: "daemon_restarted"; it does not reuse a lost listener or exchange. Repeating its PUT returns that retained failure. Starting again requires a new login ID. The daemon retains only the protected secret material needed for immutable-intent comparison and removes it when the record expires.

OAuth callbacks stay on provider-required daemon-owned loopback listeners. They validate provider state and PKCE where the provider supports it. Manual callback input preserves remote-client usability. Secrets and callback codes stay out of logs and SSE. Selecting an account updates a provider profile through /settings; DELETE /auth/accounts/{account_id} refuses removal while a saved profile still references it. It does not interrupt a running request using captured credentials.

SSE delivery and recovery

Session watch

GET /sessions/{session_id} with Accept: text/event-stream watches one session. Initial attachment omits after_generation and after_seq. Reconnect sends both from the last fully consumed batch. tail applies when a snapshot is needed. JSON reads return the same cursor pair for subsequent attachment. Vary: Accept distinguishes the representations. A successfully installed JSON session snapshot counts as the initial reset. Its cursor can seed the SSE watch after the snapshot callback succeeds. Without an installed snapshot, the client omits the cursor and requires reset.

A batch uses this envelope. snapshot appears only with a leading reset.

{
	"generation": "J6kYj6IGcXyP3eMIp-WL3A",
	"cursor": 42,
	"events": [
		{"type": "reset", "data": {"reason": "initial"}}
	],
	"snapshot": {
		"id": "019a0000-0000-7000-8000-000000000001",
		"history": {"items": [], "older": null, "newer": null, "high_water": 0},
		"current_progress": [],
		"pending_inputs": []
	}
}

The example shows the relevant snapshot fields; a real snapshot is the complete Session representation defined above. Generation is a nonempty base64url 128-bit random identity created by the session actor initializer. Sequence is a nonnegative integer in that generation. Empty keepalive batches carry both values and an empty events array. Keepalives do not advance the event sequence. The data: JSON is authoritative; this protocol does not use SSE id or automatic Last-Event-ID recovery.

The session checks generation equality before consulting numeric replay bounds. Missing or different generation, or an unavailable sequence, requires a durable snapshot reset. Malformed cursor components return 400 before headers. A well-formed future or evicted cursor resets. The replay buffer retains at most 256 events or 992 KiB, whichever bound is reached first. An oversized event is represented by bounded history references.

Registration and state capture serialize with session publication. A reset captures state, bounded current progress, pending ownership, and replay cursor together, then loads history through the captured durable high-water mark. It also captures ordered active_output segments. Their inline text or signed content references include the complete unfinished prefix at that cursor, including output evicted from the replay buffer. Text and thinking share a 64 KiB encoded inline budget. Spill references fix their byte cutoff and remain readable for 15 minutes even when the response commits during hydration. Content reads retain the original snapshot query while following next; continuations cannot extend the original snapshot's lifetime. Expiration returns 410 active_output_expired and requires a fresh session snapshot. Capture publishes any pending coalesced progress or activity before choosing the cursor. Live-state changes advance the actor sequence; one cursor cannot identify conflicting status, progress, or activity replacements. Events after that cursor remain eligible for replay. Slow snapshot loading cannot silently skip an evicted interval: the subscription resets again or closes with failure. Transcript loading failure emits failure and closes; it never installs a successful replacement cursor.

Client delivery follows these rules:

  1. Validate the complete batch, generation, sequence, and known event fields.
  2. Require a leading reset on first attachment or a generation change.
  3. On reset, replace visible recent history, pending state, and live progress. Stage full active-output hydration before installing the replacement. Restore history and the captured prefixes before delivering later deltas. Clear partial arguments and transient text from the previous stream.
  4. Deliver all events in order. Same-generation replay cannot regress the sequence without a reset.
  5. Save generation and sequence together only after every callback succeeds.

EOF and transient failures retain the last consumed pair. Explicit recovery and cancellation clear it. A generation change with reset is ordinary recovery. A change without reset is a protocol failure. The client can attempt one cursor-free recovery; a second protocol failure is surfaced to the user. Reset history is authoritative for its covered window. Previously loaded older history remains accessible by durable positions and is deduplicated by entry ID. Reset replaces daemon-derived views. It preserves local drafts and unresolved creation or input handles, reconciling them only by matching identities.

Session events and progress

Each event has type and data, its typed body. A published session event also has sequence, its position in the generation. Control events reset and failure have no sequence. Session identity is implicit in the subscribed URL. Events within a batch are ordered. There is no ordering claim between streams belonging to different sessions. A failure control appears alone, without a snapshot, and terminates the stream even before attachment has succeeded. Clients never treat it as a successful reset.

For example, an incremental batch appends text:

{
	"generation": "J6kYj6IGcXyP3eMIp-WL3A",
	"cursor": 43,
	"events": [
		{
			"type": "text",
			"sequence": 43,
			"data": {"run_id": "run-1", "message_id": "message-1", "text": "Hello"}
		}
	]
}

Incremental event sequences increase contiguously from the requested cursor. The batch cursor is the last published event represented by the batch. Clients apply a batch atomically to local state or deduplicate already delivered events by generation and sequence. This prevents duplication when an earlier callback succeeds and a later callback fails before the batch cursor is saved.

TypeBody and behavior
resetReason initial, generation_changed, replay_unavailable, or recovery; requires the complete batch snapshot.
statusCurrent phase, run ID, interrupt state, and blocking reason.
inputinput, a bounded Input outcome update, including membership and delivery changes.
textRun ID, message ID, and appended assistant text.
thinkingRun ID, message ID, and appended reasoning text.
messageCompleted display message with stable history entry identity.
tool_progressprogress, a normalized progress object, or null to clear all live progress.
toolCompleted call identity, progress identity, bounded arguments, result, trace, and full-content reference if needed.
noteEntry identity, origin, display text, and related family mail IDs where present.
retryRun, provider attempt, safe reason, and retry delay in milliseconds; clears obsolete attempt progress.
usageTyped token, timing, model-window, and cache observations.
committedDurable high-water position, replaces_live_ids, and replaces_all_live. Model commits clear all provisional output; other commits replace only the listed identities.
turn_completedRun ID, terminal outcome, and input membership.
compactedStrategy and observation, without replacing durable history.
errorSafe error code and message for the turn; not transport authentication.
invalidateResource kind and relative URL whose metadata changed.
failureStream error code; terminal, never advances the saved cursor.

Assistant text and reasoning are provisional until committed. Clients reconcile completed entries by stable identity, including every bounded chunk of a long provisional segment. Canonical text and thinking publication precedes its commit marker; clients replace provisional chunks together with that publication. replaces_all_live also clears fragments omitted from bounded backfill after a storage failure. Generation, model step, and attempt separate live identities. Model retry can end an attempt without creating another user input. Family mail events are durable notes with nullable mail: { "mail_id": ..., "sender_session_id": ..., "receiver_session_id": ..., "kind": ..., "bytes": ... }. Sender is null for external senders. Kind is task, message, result, unreviewed, or webhook; bytes count the UTF-8 message body. unreviewed is an answer forwarded after a child ended without replying. A null sender also includes a bounded sender_label, such as a hook name, at most 100 Unicode scalar values and 400 UTF-8 bytes. The agent view animates these identities without interpreting note text.

Progress contains call_id, nullable tool_call_id, name, phase, intent, and optional preview: { "offset_scalars": ..., "text": ... }. Phase is generating or running. Intent is read, search, list, run, or unknown; unknown remains valid. Intent comes from declared tool metadata or emitted execution evidence; generating code can have unknown intent. The daemon does not parse Python just to guess a label. The client does not accumulate arguments or infer Python behavior. Calls interleave by call_id, which changes across actor lifetimes, runs, provider steps, retries, and output indexes.

Preview updates replace the latest window. The window has at most 512 decoded Unicode scalar values and 2,048 UTF-8 bytes. offset_scalars counts decoded source scalars before that window, including source newlines. It does not count JSON escapes, UTF-8 bytes, grapheme clusters, or terminal columns. Names have a 100-byte UTF-8 bound; native IDs over 200 bytes are omitted. One progress event has an 8 KiB bound and an attempt tracks at most 32 calls. Excess calls disable generating previews for that attempt while execution continues.

First appearance and transition to running publish immediately. Other preview updates coalesce over 100 ms. Attachment captures updates awaiting publication. Completion, cancellation, failure, retry, and reset clear obsolete previews. Argument parsing occurs once per provider fragment, before subscriber delivery. Full tool arguments and results remain durable. Capability tool_progress: 1 is required; there is no legacy client reconstruction.

Collection watch and overload

GET /sessions with Accept: text/event-stream uses the same scope filters as the JSON collection, excluding pagination and sort. It sends invalidations, not transcripts or duplicate full argument payloads. Its first batch contains ready and reset, requiring an authoritative collection read. No cursor or replay promise applies to this stream.

{
	"events": [
		{"type": "ready", "data": {}},
		{"type": "reset", "data": {"reason": "initial"}}
	]
}

An incremental batch contains invalidate events with resource URLs and affected session IDs, plus bounded activity replacements for visible agents. An activity event has this typed body:

{
	"type": "activity",
	"data": {
		"session_id": "session-1",
		"cursor": {"generation": "J6kYj6IGcXyP3eMIp-WL3A", "sequence": 10},
		"status": {"phase": "idle", "run_id": null, "interrupt_requested": false, "blocking_reason": null},
		"current_progress": [],
		"activity": {
			"lines": [],
			"output_scalars": 0,
			"output_utf8_bytes": 0,
			"observed_at": "2026-10-02T14:00:00Z",
			"latest_input": null,
			"latest_answer": null
		}
	}
}

Status, current progress, activity, and cursor are captured together and replace the row's live state together. The cursor matches the summary cursor. Separate updates cannot share a cursor while replacing only one of those fields. Collection mail events carry the same mail metadata as session notes, without message text. Peer labels come from session summaries; unknown peer IDs trigger a metadata read. These animation events have no replay guarantee. Scope filters cover both entry and removal, including a workspace change moving a row out of the filter. The daemon invalidates the scope when it cannot resolve membership cheaply or safely.

A client subscribes, waits for ready, then reads authoritative JSON pages. Changes during loading leave the view dirty and cause another refresh. Activity replacements arriving during that read are applied afterward by their per-session generation and sequence. Equal or older sequences cannot overwrite a newer row. An actor-generation mismatch requires refreshing that row. This yields a current view without claiming an atomic snapshot across actors. Reconnect always repeats the readiness and refresh flow.

The bus queue holds at most 256 events and 1 MiB per subscriber. Mailboxes carry at most one outstanding wake, never full payloads. Publication uses bounded nonblocking admission. Session streams read the existing replay buffer rather than maintaining additional payload queues. Flushes coalesce over 100 ms. Progress and display events have independent payload bounds.

At overflow, the collection stream emits overflow and closes if the socket can still accept the signal. Closure alone also requires refresh; delivery of the final marker cannot be guaranteed to a stalled reader. The client refreshes by resubscribing, waiting for ready, and then reading the authoritative tree, status, progress, and glances. It does not append new state to an obsolete graph. Disconnect and failed writes release the subscription and its queue.

Both stream kinds send keepalives at least every five seconds when quiet. Sockets have bounded write waits. Publication never awaits subscriber socket writes. A stalled reader cannot retain an unbounded mailbox or force another subscriber's writes to wait behind its own. Reverse proxies must pass streaming responses without buffering. History, images, and large extension results remain bounded HTTP reads, not unsolicited stream data.

Extension contracts

Extension management stays under /extensions/{extension_name}. These routes are not aliases for core commands. The extension owns validation, persistence, permissions, and typed domain results. Enabled routes and action descriptors are discoverable through the server and session catalog.

HTTP bearer clients are human operators. Internal model callers use the same domain operations with trusted caller context supplied by the runtime. A JSON role, caller, or session ID cannot grant human permission. Core shutdown and global credential management cannot be delegated through a page descriptor. Disabling a service returns 404 for its HTTP routes.

Extension lists use common pagination and may include optional page and glance render hints. Each editable row includes resource, containing URL, value, and its observed etag. Work, paperclip, and schedule item reads are canonical item representations. Hook and linked-group reads include runtime observations and expose a separate view=configuration resource for edits, using the session configuration convention. Its validator covers only the complete configuration representation, not changing runtime observations.

Mutation responses contain resource and notification. Notification has state: "not_requested"|"queued"|"failed", with nullable safe code and detail. Multiple targets use notifications, one result per target. Creates return 201 with Location; edits return 200. Deletes return 200 when reporting a deleted resource or notification outcome, otherwise 204. A committed ledger edit remains successful if notifying an agent fails. Secret-generating POSTs have no automatic retry after response loss; the caller reads the resource and explicitly rotates if the secret was lost.

Work

PathMethods
/extensions/work/itemsGET, POST
/extensions/work/items/{item_id}GET, PATCH, DELETE

Every Work read and mutation requires a workspace query selecting the invoking workspace. GET collection reads its linked group. POST accepts title, optional notes, parent_id, session_id, and run_id. Fields include those values, status, revision, and timestamps. Status is open, active, blocked, done, or cancelled. A run requires a session. PATCH changes title, notes, status, session, or run under If-Match; parent and original workspace are immutable. The query supplies the creation workspace; notes default to empty, status defaults to open, and optional IDs default to null. Title has a 4,096-byte limit; notes have a 32 KiB limit. DELETE requires If-Match and rejects an item with children. Writes remain scoped to the invoking workspace's linked group.

Human mutations can name notify_session_id to queue the existing user-change note. Results distinguish ledger commit from note delivery. Models retain their workspace-scoped create, read, update, and delete operations through trusted internal bindings. Work pages preserve linked-workspace labels and pending-work glances.

The CLI accepts /work add <title>, /work edit <id> <title>, /work status <id> <status>, and /work remove <id> alongside page forms. These shortcuts invoke the page's declared HTTP actions. Removal asks for confirmation. Targeted shortcuts fetch the item even when it is outside the first list page and retain its ETag through confirmation.

Paperclips

PathMethods
/extensions/paperclips/itemsGET, POST
/extensions/paperclips/items/{item_id}GET, PATCH, DELETE

This ledger is global. Items contain ID, topic, title, message, suggestion, reply, status, origin session and workspace, creation time, resolution, and resolving session. Topic is harness, workflow, bug, user, or other. POST accepts topic, message, optional title and suggestion, and the filing session and workspace. Topic defaults to other; optional text defaults to empty. Message is nonblank and at most 32 KiB; title is at most 4 KiB; suggestion, reply, and resolution are at most 16 KiB each. Topic, title, message, suggestion, and origin are immutable after creation. Status, timestamps, and resolving session are response-only at creation.

Humans PATCH status to acknowledged, resolved, or dismissed, or supply reply. A reply acknowledges the item and attempts a note to its filing session. The reply stays saved when notification fails. DELETE removes the item. PATCH with reply cannot also set resolved or dismissed. Human resolution can include a resolution note; it never changes the original message. A model's resolving session comes from trusted runtime context. DELETE requires If-Match. Models can file, list, and resolve still-open or acknowledged items with a nonempty resolution. Human terminal decisions cannot be overwritten by a model. All edits use If-Match. Listings use newest-first cursor order and global glances.

The CLI accepts /paperclips acknowledge <id>, /paperclips reply <id> <text>, /paperclips resolve <id> [note], /paperclips dismiss <id>, and /paperclips remove <id> through the same declared actions as page forms. Removal asks for confirmation; a stale ETag returns a conflict without retrying the mutation.

Web search

PathMethods
/extensions/web-search/providersGET
/extensions/web-search/providers/{name}POST

The order in which the model's web search tries providers. It is global and lives in extensions.json. Items carry name, label, 1-based position, and whether the provider is tried. POST takes {"change": "up" | "down" | "toggle"}; a move past either end leaves the order unchanged. An unknown name is 404.

Schedule

PathMethods
/extensions/schedule/jobsGET, POST
/extensions/schedule/jobs/{job_id}GET, PATCH, DELETE

GET requires session_id. POST accepts session ID, kind, prompt, and delay_seconds, with every_seconds required for recurring or heartbeat jobs. Kinds are once, recurring, and heartbeat. Prompt has a 4,096-byte limit; intervals range from 60 to 31,536,000 seconds. Resources include next_at. PATCH can change prompt, kind, delay_seconds, and every_seconds under If-Match, preserving once-to-recurring edits. Once jobs prohibit repetition; recurring and heartbeat jobs require it. A prompt-only edit preserves next_at. A changed delay resolves from commit time. DELETE requires If-Match. Models retain these operations.

Once jobs disappear after durable delivery. Recurring prompts queue behind running work. Heartbeats skip busy sessions. Restart resumes the schedule without replaying every missed interval. Session deletion removes its jobs.

Linked workspaces

PathMethods
/extensions/links/groupsGET, POST, DELETE

GET requires workspace and returns its complete group with bounded remote presence facts, paged where necessary. Configuration view contains membership and revision without remote presence facts. POST uses that observed configuration URL and its If-Match, with other_workspace and other_etag from the other group's configuration read. It checks both groups and merges them in one owner commit. Stale membership on either side returns 412. DELETE uses the configuration URL plus member and checks that member belongs to the observed group at commit. The member becomes its own group. Neither operation deletes work or memory. Writes are human-only. Notifications to affected open sessions are reported separately from the committed group change. notification_count covers all targets; the response includes up to 200 outcomes and truncated when the list reaches its count or byte bound.

Webhooks

PathMethodsAuthentication
/extensions/webhooks/hooksGET, POSTDaemon bearer
/extensions/webhooks/hooks/{hook_id}GET, PATCH, DELETEDaemon bearer
/extensions/webhooks/hooks/{hook_id}/secretPOSTDaemon bearer
/extensions/webhooks/hooks/{hook_id}/deliveriesGETDaemon bearer
/extensions/webhooks/hooks/{hook_id}/deliveriesPOSTSignature over raw body
/extensions/webhooks/deliveries/{delivery_id}GETDaemon bearer
/extensions/webhooks/permissions/{session_id}GET, PATCHDaemon bearer

Hooks contain ID, name, target session, enabled state, signature header and prefix, revision, public delivery URL, pending delivery count, and latest deferral reason. Names use 1 to 64 ASCII letters, digits, _, or -. POST accepts session_id, name, optional enabled, signature_header, signature_prefix, and secret of 16 to 4,096 bytes. Name is unique within its target session. Enabled defaults true. Header uses 1 to 64 ASCII letters, digits, or hyphens; prefix has at most 32 bytes and no CR or LF. Absence generates a secret. PATCH configuration accepts name, enabled, header, and prefix. Target session is immutable. ID, revision, URL, pending counts, and diagnostics are read-only. Secret rotation POST accepts only an optional secret. Creation and rotation return that secret only in the operation response. Rotation and PATCH require If-Match. Stored secrets never appear in lists, descriptors, ordinary logs, or SSE.

Permission reads return false when no setting exists. PATCH accepts explicit agent_manage, with omission retaining its value. Only humans grant it. Agent management remains restricted to that agent's target session. Turning management off does not disable hooks or prevent reading that session's own accepted delivery payloads. Hook edits use the caller's observed revision, not a revision fetched just before overwriting a stale screen.

Incoming delivery POST has a 65,536-byte raw-body limit. It requires the configured HMAC-SHA256 signature and never accepts a bearer token instead. Default header is X-Albedo-Signature and prefix is sha256=. Missing or invalid signatures return 403, without a daemon bearer challenge. Optional X-Albedo-Event-Id, at most 128 bytes without newlines, deduplicates by hook, ID, and body hash. A changed body under that ID returns 409. Acceptance stores the delivery and its durable inbox entry together, then returns 202 with delivery_id. A full target inbox returns 429; unknown or disabled hooks return 404. The pending bound is 1,000 per session.

Delivery lists accept state=pending|delivered|all, default all, and common pagination. Metadata contains ID, hook ID, target session, receipt and delivery timestamps, attempts, and latest deferral reason. Direct delivery reads add payload: { "encoding": "utf8"|"base64", "data": ... }. Both direct and by-hook historical reads remain available after hook deletion. Payloads are external data and carry no instruction authority. Deleting a hook retains accepted deliveries. Deleting the target session removes its hooks and inbox. An operator exposing intake through a reverse proxy allows only the signed POST method, not management GET on the same path.

OpenAI proxy

PathMethods
/extensions/proxy/v1/modelsGET
/extensions/proxy/v1/chat/completionsPOST

This optional service keeps its OpenAI-compatible request and response format, including JSON and streaming responses. It is stateless with respect to agent sessions: the caller supplies history. It retains the 32,000,000-byte request limit and opaque provider state carried in tool IDs. The supported request subset contains model, messages, tools, token limits, temperature, top_p, stop, tool_choice, parallel_tool_calls, reasoning effort, response format, stream, and stream_options.include_usage. User images must be base64 data URLs; remote image URLs are not fetched. Unsupported fields return a proxy-format error. Provider transports apply only options they can express, as described in the proxy transport contract. Text and reasoning stream as delta.content and delta.reasoning_content. Complete tool calls publish when the turn ends. Provider failure sends an error chunk before termination, or a JSON error for a nonstreaming request. The proxy's OpenAI envelope, field names, and body limit override platform encoding conventions. It uses daemon bearer authentication. An operator may explicitly enable anonymous loopback access; that policy never grants access to core or other extension routes.

Existing feature coverage

This table is the implementation acceptance checklist. UI shortcuts and labels can stay unchanged while their adapter calls use the target contract.

Existing featureTarget operation or data
Session picker, search, workspace filter, grouping, recent previewsFiltered session summaries with names, activity, workspace, preview, and shared preferences
Pinned, archived, and frequent sessionsSession preferences, sort=frequent, and identified visits
New session, checkpoint fork, child spawnSession creation variants with retry provenance and atomic child task
Rename, stable family address, leaf or subtree deletionSession PATCH, separate name and address, conditional deletion result
Text, images, continue, selected skill, queued steeringImmutable input variants and ordered durable admission
Pending rows, uncertain sends, uncertain creation across navigationSame-resource decision lookup and retained original handles
Targeted cancel, shared turn, whole-turn interruptInput cancel and captured session interrupt
Chat, thinking, code preview, full tool output, file diffsSession SSE plus durable history content reads
Late attachment, reconnect, restart, replay evictionActor-generation cursor and durable reset snapshot
Older history and transcript treeImmutable history pages and checkpoint view
Footer status, usage, cache fade, context windowSession snapshot, status and usage events, model metadata
Model picker, manual ID, effort, default profile, raised capSession selection, models, and explicit separate settings changes
/reload, /reload session, /reload modelsTyped reload targets with independent confirmed outcomes
/compact [strategy]Compaction result preserving confirmed strategy choice
/kernel, /kernel upgradeSession kernel metadata and explicit upgrade
Extensions, dependencies, quarantine, global defaults and inheritanceFresh catalog, loaded composition, session overrides, shared settings
Skill and instruction discovery, source diagnostics, selectionCandidate revision checks and explicit resolved-source metadata
Context inspector and bounded section pagesPrepared snapshot identity and context section reads
Provider request telemetryContext request-record view
MCP forms, credentials, validation and failed-save recoveryAtomic candidate settings patch with write-only secrets
Provider login, manual callback, choose or remove accountAuth flows, provider-profile selection, account deletion
/cd, folder and repository preview, missing workspace repairWorkspace read and family-aware session move
SSH host completion, probe, sign-in handoffHosts, explicit probe, and actionable local operator instructions
Agent graph, live tails and rate, attach, spawn, rename, delete, send, mail animationSession family summaries, atomic activity replacements, normal input admission, typed mail metadata
Work, paperclips, schedule, linked-workspace pages and glancesExtension resources and declarative typed page actions
Webhook forms, copy URL, generated secret, rotation, permission toggleWebhooks domain contract and one-time operation secrets
Shared thinking and tool display preferencesSettings UI group
Online storage report and offline diagnosisStorage API and deliberate local diagnostic path
Ordinary CLI keep-or-restart offerExisting launcher policy with instance-targeted shutdown
Drafts, editor, clipboard, mouse, animations, navigation, quitClient-owned behavior; no daemon endpoint

Agent-originated mail remains an extension/runtime operation under trusted family permissions. Human graph messages use ordinary user inputs addressed to the chosen session. The API does not let clients forge agent senders.

Architecture and implementation boundaries

server.gleam owns routing, authentication, HTTP admission, negotiation, and mapping domain errors to wire responses. It does not own session state or execute extension commands inside a session actor.

The session actor owns generation, live status, normalized progress, replay capture, and configuration application. The store owner commits identified creation, pending input, consumption, and deletion outcomes on its transaction connection. Family operations serialize membership validation with mutation. The existing bus owns bounded collection notification admission and cleanup.

Settings owners validate and persist their desired state. Runtime composition prepares candidates before applying them. Extensions keep their ledgers, permissions, migrations, and handlers. Provider transport and Python RPCs stay internal; their URLs and execution rules are not public client responsibilities.

The Go API adapter owns paths, query encoding, request and result types, capability checks, byte bounds, deadlines, retry policy, and SSE validation. Application commands and UI packages receive typed results and UI messages. Local discovery and launching stay outside the connection. Other clients can implement this contract without local storage or execution knowledge.

The implementation uses direct domain operations. It adds no forwarding-only wrappers, generic command bus, generation registry, duplicate progress parser, or per-client full-argument buffers. New route aliases require a contract change, not a convenience helper in the UI.

Verification

Behavioral tests exercise these outcomes through the real daemon:

  • Lose creation and input responses, retry original IDs, edit a created session, and still recover the original intent. Reject changed intent and expired IDs.
  • Retry a rejected queue-full input after capacity clears. It stays rejected. Delete its session and retain accepted, cancelled, and rejected decisions.
  • Race child creation with interruption and deletion. No child lacks its task, and an old interrupt or deletion attempt cannot affect newer work.
  • Attach during generation; replay, evict, restart the actor, and restart the daemon. Committed history survives and old numeric cursors never authorize replay.
  • Interleave tool calls with escaped Unicode, retry, cancel, and transition to running. Progress remains bounded and full arguments remain in history.
  • Stall one reader under sustained events from many agents. Measure queue bytes and mailbox length; another reader stays responsive, overflow refreshes, and disconnect releases subscriptions.
  • Change collection membership while pages load. A removal or moved workspace reaches the client, stale activity cannot replace newer state, and reconnect refreshes graph, status, progress, tails, and glances without duplicate rows. Interleave text, tools, and mail across agents; one collection subscription preserves live tails and animation identities within the payload bounds.
  • Page older history, retrieve a large tool result and image, and fork a valid checkpoint. Full content is recoverable and history navigation survives reset.
  • Change prepared context between summary and page. The old page fails by identity rather than displaying another request's section.
  • Race preference edits and stale catalog choices. Preconditions protect changes, false differs from omission, and null restores inheritance.
  • Fail candidate MCP validation and session reload. Previous secrets and loaded composition remain usable; committed model changes remain visible when a later default or cap change fails. Reject conflicting credential sources before attempting a connection or changing saved settings.
  • Exercise local and remote workspace browsing, missing workspaces, busy moves, descendant moves, and explicit SSH authentication instructions.
  • Complete, cancel, and expire provider login flows, including manual callback. Restart during a flow; the old ID stays failed and cannot start another flow. Verify secret redaction, origin rejection, bearer failures, and browser CORS.
  • Exercise work, paperclip reply, scheduling while busy, linked-group merge, webhook stale edits, signatures, deduplication, secret loss, and permissions. Committed domain changes survive a failed notification.
  • Read storage from a client without daemon filesystem or Python access and compare known isolated totals. Attachment never starts or stops a daemon.

Controlled transport tests cover malformed wire data, callback failure before cursor commit, partial HTTP responses, and failures a healthy daemon cannot produce. Cross-cutting parser tests cover fragmented SSE lines and UTF-8 bounds. There are no compatibility fixtures, UI copy snapshots, route-constant tests, or tests that only encode and decode the same local type.

An independent consumer authenticates, creates a session, submits identified input, watches SSE, resumes after disconnect, and reads history using only this document. Its success verifies that Go-specific knowledge is not required.

Run focused E2Es and Go race tests for changed behavior, then ./test.sh for the complete gate. Review resource ownership, uncertain outcomes, bounded queues, cleanup, and test failure modes when changing the API. Performance experiments and their results belong outside the repository.

Server and clients use the same contract. Durable history and supported storage migrations remain supported; old API wire formats do not.

1# HTTP API design
2
3Status: implemented contract for daemon protocol 3.
4
5[openapi.yaml](openapi.yaml) owns the exact request, response, parameter, header,
6and SSE payload shapes. This document owns behavior: ordering, atomicity,
7retry, recovery, and resource ownership. Both documents must agree.
8The OpenAPI version is a document-format version; daemon protocol 3 is a
9separate compatibility decision.
10
11[API architecture](http-api-architecture.md) explains state ownership and settings
12crash recovery.
13
14This document defines daemon protocol 3. The daemon and CLI use this contract
15together. It defines no compatibility
16aliases or legacy response readers. Changes to this contract require an update
17here, a client impact review, and behavioral tests before implementation.
18
19The API serves terminal, web, and desktop clients. All current TUI features
20remain available. Installation discovery, local launch, and the interactive
21restart offer remain application policy, outside the API connection.
22
23The [resource map](#resource-map) lists routes. The [HTTP rules](#http-rules)
24define authentication, errors, and retries. [Sessions](#sessions-and-durable-input)
25and [SSE](#sse-delivery-and-recovery) define chat and recovery.
26[Extension contracts](#extension-contracts) define optional domain operations.
27The [feature checklist](#existing-feature-coverage) and
28[behavioral verification](#verification) describe the supported workflows.
29
30## Design decisions
31
32- Resources own their data and operations. A screen does not get its own route.
33- A session read includes the state needed to open a chat. A collection read
34  includes the state needed to browse sessions or draw an agent family.
35- Ordinary HTTP requests perform changes. SSE delivers live updates.
36- JSON and SSE use the same session resource URL, selected by `Accept`.
37- Input IDs identify user intent across retries. Stream cursors identify an
38  actor lifetime and position. Transcript checkpoints identify durable history.
39  These identities are separate.
40- Core operations have named, typed contracts. Extensions own separate domain
41  routes. A command menu entry points to its canonical operation.
42- The daemon owns execution state, argument parsing, storage inspection, and
43  progress. Clients own navigation, rendering, local drafts, and confirmations.
44- Bounds apply before subscriber mailboxes. Model execution does not wait for
45  a client to read its socket.
46
47SSE is the sole core streaming transport. The current UI needs server-to-client
48updates, and SSE retains the existing replay and bounded delivery design.
49This contract does not add WebSocket authentication or subscriptions.
50It makes no claim that changing route organization reduces CPU or memory.
51
52### Reading and changing the wire contract
53
54Each OpenAPI operation names its request and accepted response schemas.
55Required properties are listed explicitly. Nullable fields accept JSON null;
56optional fields may be omitted. Requests reject unlisted fields. Response
57schemas describe what the server emits; clients may ignore new fields.
58Tool, provider-private, and extension-specific bodies use the explicitly named
59`DynamicJSON` schema. Core resource envelopes have typed fields.
60
61SSE `itemSchema` describes a parsed event whose `data` string contains JSON.
62Decode that string and validate its `contentSchema` separately. Ordinary schema
63validation does not assert string-encoded content. The wire contract uses
64[OpenAPI 3.2 streaming definitions](https://spec.openapis.org/oas/v3.2#special-considerations-for-server-sent-events).
65The `x-max-utf8-bytes`, `x-max-encoded-bytes`, and content-byte annotations
66record byte limits that also need explicit runtime checks. JSON Schema
67`maxLength` counts Unicode code points, not encoded bytes.
68
69Agree on a shape by editing the operation and its shared schemas, adding a
70complete example, and reviewing the affected behavior here. Validate the
71document and examples before changing either implementation. Validate real daemon replies and decoded SSE batches in workflow tests;
72shape validation does not prove ordering, atomicity, or safe retry.
73
74## Resource map
75
76The tables list every platform path template. Extension paths appear in their
77own section. An omitted method is unsupported. `GET` defaults to JSON.
78
79| Path | Methods | Responsibility |
80| --- | --- | --- |
81| `/server` | GET | Readiness, protocol, capabilities, build, quota, notices |
82| `/server/shutdown` | POST | Explicitly stop the identified daemon instance |
83| `/settings` | GET, PATCH | Shared defaults, provider profiles, MCP, secrets, UI preferences |
84| `/storage` | GET | Online storage accounting |
85| `/sessions` | GET | Filtered session summaries, or collection invalidations over SSE |
86| `/sessions/{session_id}` | PUT, GET, PATCH, DELETE | Create, attach, configure, delete, or watch a session |
87| `/sessions/{session_id}/visits/{visit_id}` | PUT | Count one deliberate opening without counting retries |
88| `/sessions/{session_id}/inputs/{input_id}` | PUT, GET | Admit immutable user input and read its durable outcome |
89| `/sessions/{session_id}/inputs/{input_id}/cancel` | POST | Cancel that input or request interruption of its exclusive turn |
90| `/sessions/{session_id}/interrupt` | POST | Interrupt a captured turn and cancel captured waiting inputs |
91| `/sessions/{session_id}/history` | GET | Bounded transcript pages or checkpoint summaries |
92| `/sessions/{session_id}/history/{entry_id}` | GET | Read full content of a large transcript entry in bounded pieces |
93| `/sessions/{session_id}/active-output/{content_id}` | GET | Read an unfinished output prefix fixed by a captured snapshot |
94| `/sessions/{session_id}/context` | GET | Prepared request inspection and provider request records |
95| `/sessions/{session_id}/catalog` | GET | Fresh discovery and separately identified loaded composition |
96| `/sessions/{session_id}/reload` | POST | Reload session composition, model catalogs, or both |
97| `/sessions/{session_id}/kernel/upgrade` | POST | Explicitly replace this session's Python kernel |
98| `/sessions/{session_id}/compaction` | POST | Compact the model-facing conversation |
99| `/models` | GET | Provider model metadata, effort choices, context limits, cache policy |
100| `/workspaces` | GET | Recent workspaces or one directory with optional repository preview |
101| `/hosts` | GET | Known hosts or one host's cached probe state |
102| `/hosts/{host}/probe` | POST | Explicitly refresh a host probe |
103| `/auth` | GET | Provider login methods and redacted account metadata |
104| `/auth/logins/{login_id}` | PUT, GET, PATCH, DELETE | Start, inspect, answer, or cancel a provider login |
105| `/auth/accounts/{account_id}` | DELETE | Remove a provider account |
106
107There is no generic request endpoint, command dispatcher, operation lookup
108route, separate status route, preview route, agent tree route, or stream suffix.
109The underlying durable admission records remain owned by the daemon. Their
110public representation is the session or input whose identity they protect.
111
112### Opening a chat
113
1141. Use the supplied base URL and bearer token to read `/server`. Check protocol
115   and required capabilities before opening a screen.
1162. Read `/sessions` for the picker or family graph. Each row includes its
117   metadata and bounded live state.
1183. Read the selected session. Install its history, pending inputs, status,
119   progress, and cursor together. Count a deliberate opening with one identified
120   visit; background reads do not count.
1214. Watch that same session URL as SSE, passing the installed cursor pair.
122   Apply subsequent events, or replace the daemon-derived view on reset.
1235. Send a message with an identified input PUT. Keep its local handle until
124   the returned decision or matching stream event resolves it. Cancellation
125   targets that input identity.
1266. Fetch older history by durable position. Open context, catalog, or extension
127   resources when the user needs them. Use the validator already displayed
128   with a resource when editing it.
129
130## HTTP rules
131
132### Addressing and authentication
133
134A client receives a base URL and daemon bearer token. Native local clients may
135read the protected discovery record. Other clients receive both values through
136explicit connection setup. No API method reads client installation files,
137launches a daemon, or replaces one.
138
139Every platform request, including SSE, uses
140`Authorization: Bearer <daemon-token>`. The token grants operator access to this
141daemon. This protocol has no user accounts, tenant isolation, or session ACLs.
142Provider OAuth accounts are model-provider credentials, not daemon identities.
143Browser OPTIONS preflights are the exception to bearer presentation. They
144return only allowed request metadata and cannot read or mutate resources.
145
146The default listener remains loopback. Connections across an untrusted network
147require HTTPS or an encrypted tunnel. Tokens never appear in URLs, response
148bodies, diagnostic logs, or stream events.
149
150Browser clients use `fetch` for JSON and SSE so they can send the authorization
151header. Native `EventSource` does not expose arbitrary request headers.
152[The SSE specification](https://html.spec.whatwg.org/multipage/server-sent-events.html#the-eventsource-interface)
153defines its constructor and stream format.
154
155Browser access uses an operator-configured list of exact allowed origins.
156The daemon validates `Origin` before routing or reading a body. Allowed
157preflights advertise the supported method and headers, including `Authorization`,
158`Content-Type`, `If-Match`, and `If-None-Match`. Responses vary by `Origin` and
159expose `ETag`, `Location`, and `Retry-After`. There are no authentication cookies
160or wildcard origins. Origin approval does not replace the bearer check.
161This follows the [Fetch CORS protocol](https://fetch.spec.whatwg.org/#http-cors-protocol).
162
163The daemon validates the requested host against its listener and configured
164public addresses. A supplied origin or host never grants additional permission.
165Optional extension intake routes declare their own authentication policy.
166
167### Encoding, limits, and pagination
168
169JSON uses UTF-8, `snake_case` field names, and tagged objects for variants.
170IDs are opaque strings. Paths and query values use ordinary percent encoding.
171Core timestamps are UTC RFC 3339 strings. Unknown historical timestamps are null.
172Durations use explicitly named units.
173Counters and sequence numbers are integers from zero through `2^53 - 1`.
174
175Unknown request fields, duplicate JSON keys, invalid enum values, malformed
176query values, and unknown query parameters return `400 invalid_request`.
177Requests do not silently substitute defaults for invalid values. Unknown
178response fields may be ignored. Missing required fields and invalid known
179variants fail decoding.
180
181`PATCH` uses `application/merge-patch+json`. Omission keeps a value, `null`
182removes an override or optional value, and `false` remains a real value.
183Arrays replace the complete array. Writable fields are listed for each resource.
184[JSON Merge Patch](https://www.rfc-editor.org/rfc/rfc7396.html)
185defines these rules.
186
187Ordinary JSON request bodies have a 64 KiB limit. Input admission has a
18872,200,000-byte limit, room for ten encoded images. JSON responses and SSE data
189frames have a 1 MiB limit. Resource-specific limits below apply as well.
190HTTP body framing accepts valid fixed-length and chunked requests, enforcing
191the same byte limit while reading. Ambiguous framing, unsupported content
192encoding, and incomplete bodies fail without executing the operation.
193
194zstd is the one content coding. A JSON or text response of at least 1 KiB is
195compressed when `Accept-Encoding` admits `zstd`, and so is a session or
196collection event stream, flushed after every frame. Limits and `ETag` values
197describe the decoded representation, and these responses add
198`Vary: Accept-Encoding`. Image resources are never coded. The daemon needs
199OTP 29, whose `zstd` can flush a stream between frames.
200When `GET /server` lists the `zstd_requests` capability, a request body may use
201`Content-Encoding: zstd`. It must be one frame that declares its decoded size,
202which must fit the route's limit; anything else is `400`. The CLI compresses
203bodies of 16 KiB or more.
204
205Lists return `{ "items": [], "next": null }`. `next` is an opaque continuation
206token tied to the query and representation, including `limit`. Follow-up requests
207retain the original query and add `next`. Default `limit` is 50; maximum is
208200. Each page also respects its encoded byte limit, including JSON escaping.
209Catalog entries, glances, input outcomes, and reset snapshots obey that bound
210even when their item counts fit. Large content uses its documented paging path;
211summary text is shortened explicitly. A single catalog entry is at most 64 KiB.
212Session glances include at most 16 entries of 8 KiB each. Tokens never contain credentials
213and cannot authorize access. A token for another query returns `400`.
214Missing singleton resources return `404`; an empty collection returns `200`.
215
216Collection traversal uses a stable sort key and ID as its tie breaker.
217Sessions use most recent activity first. A traversal is not a frozen global
218snapshot: concurrent edits can move rows. Clients key rows by ID and refresh
219on invalidation. History traversals use immutable transcript positions and
220do not have this limitation.
221
222### Revisions and errors
223
224An `ETag` validates the complete selected JSON representation. Configuration
225has a separate representation from volatile session state. Session reads expose
226the URL and validator for `view=configuration`; settings reads expose the URL
227and validator for each `group`. Editing those representations uses `If-Match`
228without conflicting with unrelated progress or another settings group.
229Extension item reads likewise expose the item's observed URL and validator.
230
231`PATCH` and conditional `DELETE` require the observed `If-Match`. Missing
232preconditions return `428`; stale ones return `412`. Checks and the corresponding
233commit occur under the owning resource's serialization or transaction boundary.
234Validators are opaque; clients never derive them from an integer revision.
235Mutation envelopes carry new resource validators explicitly and are not cached
236as canonical read representations.
237
238Errors use `application/problem+json`, with `type: "about:blank"`, `title`,
239`status`, `detail`, and a stable Albedo `code`. An optional `fields` array
240contains JSON pointers and safe explanations. Decisions described below can
241add a typed `decision` field. Clients branch on status and code, never wording.
242Raw request bodies, secrets, and provider responses do not appear in errors.
243[Problem Details](https://www.rfc-editor.org/rfc/rfc9457.html)
244defines the common envelope.
245
246| Status | Meaning |
247| --- | --- |
248| 200 | A read or synchronous operation returned its documented result |
249| 201 | A resource was created; `Location` identifies it |
250| 202 | Input is durably accepted, or an explicitly identified action is underway |
251| 204 | A deletion or cancellation needs no response body |
252| 400 | Invalid syntax, fields, query, or identity timestamp |
253| 401 | Missing or invalid daemon token; includes `WWW-Authenticate: Bearer` |
254| 403 | Disallowed browser origin or caller permission |
255| 404 | Unknown current resource or disabled extension route |
256| 405 | Known path, unsupported method; includes `Allow` |
257| 406 | Unsupported response media type |
258| 409 | Busy state, identity conflict, stale catalog, or incompatible operation |
259| 410 | Retained deletion, expired identity, or obsolete prepared context |
260| 412 | The observed representation or creation precondition no longer holds |
261| 413 | Body exceeds its limit |
262| 415 | Unsupported request media type |
263| 428 | Required precondition is missing |
264| 429 | Admission or rate limit; includes `Retry-After` where a later new intent can succeed |
265| 503 | Owner, storage, or upstream preparation is unavailable |
266
267All authenticated data responses use `Cache-Control: no-store`. Clients may
268keep in-memory resource state and validators. Errors after SSE headers use the
269stream failure contract rather than pretending to change the HTTP status.
270
271### Retries and cancellation
272
273Reads can retry transient connection failures with capped backoff and jitter.
274They remain subject to the caller's deadline. Authentication and protocol
275errors are actionable failures, not evidence that a daemon is absent.
276
277Session creation, input admission, visits, and login creation retain the
278original ID and encoded intent during recovery. Other changes have no automatic
279replay after a lost response. A precondition or a resource-specific identity can
280make an explicit retry safe, as specified below.
281
282Canceling a client context stops its requests and subscriptions. It does not
283undo an accepted input, interrupt a shared turn, stop a detached daemon, or
284authorize replacing one. Those are separate operations. Transport cancellation
285after a mutation was sent can leave its outcome uncertain.
286
287## Server, settings, and storage
288
289### Server identity and shutdown
290
291`GET /server` returns `instance_id`, `protocol`, `state`, `capabilities`,
292`build`, `digest`, `extensions`, `quota`, and `notices`. `state` is `ready` or `draining`.
293`instance_id` changes at each daemon startup. `build` and `digest` are nullable update
294information. Attachment requires protocol 3; optional operations check the
295advertised capability before invocation.
296
297Capabilities are a map from stable feature names to integer contract versions.
298The required version-1 features are `durable_inputs`,
299`collection_invalidation`, and `tool_progress`; `session_replay` requires
300version 2. Optional features include
301`context`, `catalog`, `workspace_browsing`, `host_probes`, `provider_auth`, and
302`storage_report`. Extension entries identify enabled services and their
303contract versions. Builds cannot substitute for protocol compatibility.
304
305`quota` contains the latest recorded observations per account and limit. Each
306observation includes durable sequence, provider and account labels, limit ID,
307plan, observed percentage, window, reset time, scope, status, source, and safe
308failure details. Unknown percentage is null. `include=quota_history` adds the
309same records newest first, paged by `limit` and `next`. History may be filtered
310to one limit with `quota_provider`, `quota_account`, and `quota_limit` (required
311together), and optionally bounded with `quota_since` (RFC 3339 UTC).
312Notices have `id`, `kind`, and `message`; dismissal is a shared UI setting and does not consume a read.
313
314`POST /server/shutdown` accepts `instance_id` and `timeout_ms`, default 5,000
315and maximum 30,000. A different instance returns `409 instance_changed`.
316The daemon marks itself draining, refuses new admissions, stops active turns,
317flushes durable state, and closes. Accepted waiting inputs remain durable for
318the replacement daemon. `202` reports `instance_id` and `state: "draining"`.
319The client observes closure with a bounded wait. It cannot use this endpoint
320to start the replacement.
321
322The interactive CLI still offers to keep or restart a discovered daemon.
323Keeping it is the default. Its approved instance identifies the shutdown
324target. [Daemon attachment and local startup](daemon-lifecycle.md) owns this
325user flow and deliberate operator overrides.
326
327### Shared settings
328
329`GET /settings` returns these groups and `group_resources`, a map of group
330name to canonical URL and `etag`. `GET /settings?group={name}` returns just
331that group's canonical representation and its `ETag` header.
332
333| Group | Fields and meaning |
334| --- | --- |
335| `providers` | `default_profile` and `profiles` keyed by name. Profiles contain provider extension, endpoint, default model, effort, nullable image edge, nullable OAuth account, and `has_key`. |
336| `mcp` | `definitions` keyed by name: configured enablement, transport, URL or command with arguments, nonsecret environment and headers, timeouts, and secret-presence metadata. |
337| `extensions` | Global default enablement keyed by extension name. |
338| `capabilities` | Global skill, instruction, and MCP preferences keyed by their stable preference key. |
339| `models` | Per-model raised context choices and the cache TTL prior table, with source and units. |
340| `ui` | `thinking`, `tools`, and `dismissed_notices`. Per-session pin, archive, and visits are session fields. |
341
342`PATCH /settings?group={name}` changes exactly one group per request. Its body
343is a merge patch of that group's fields, with the observed group `If-Match`.
344The group owner checks its validator in the same commit boundary. It validates the
345entire patch before effects and commits that group's desired state atomically.
346Provider profile data and its secrets have one settings owner; MCP definitions
347and their secrets have another. Their persistence must recover the complete
348old or complete new group after a crash, including when storage uses files.
349A multi-group patch returns `400`; there is no partial PATCH success.
350
351Provider keys and MCP secrets are write-only patch fields. Provider
352`api_key`, MCP `bearer_token`, and individual secret header or environment
353entries accept a replacement string or `null` for removal. Omission retains
354the existing secret. Reads return presence and secret names, never values.
355Response-only presence fields are rejected on writes.
356
357MCP changes validate the candidate connection before saving. A failed probe
358keeps the previous definition and secrets. This removes the client-side
359save-then-undo sequence. A request can deliberately set `validate_connection`
360to `false`; that control is not persisted. The response reports validation
361state and safe diagnostics.
362
363Changing a profile does not implicitly select it. Changing
364`providers.default_profile` explicitly sets the default for new sessions.
365Changing a per-model raised-cap choice affects that model's next prepared
366requests across sessions. `models.raised_caps` maps returned `cap_key` values
367to booleans: true uses the advertised maximum, false uses the ordinary limit,
368and null removes a choice. Missing choices default to false. Clients do not
369construct cap keys. Existing requests keep their captured settings.
370
371Global capability selection accepts `catalog_session_id`, `catalog_revision`,
372and candidate-ID `choices`; the daemon resolves the IDs to preference keys.
373The read representation exposes persisted preferences. These write-only
374selection fields prevent stale discovery from changing a different file.
375
376The provider group uses these exact profile fields: `extension`, `endpoint`,
377`protocol`, `model`, `effort`, `image_edge`, `account_id`, `project`, `location`, and
378response-only `has_key`. Protocol is `responses` or `chat_completions`. Nullable fields keep
379their documented absence; model and extension are required on creation.
380Deleting a profile removes its key. Removing the selected default requires
381an explicit replacement or `default_profile: null` in the same atomic patch.
382
383For example, a provider-group patch selects a profile explicitly:
384
385```json
386{
387	"default_profile": "personal",
388	"profiles": {
389		"personal": {
390			"extension": "openai",
391			"endpoint": "https://provider.example/v1",
392			"protocol": "responses",
393			"model": "chosen-model",
394			"effort": null,
395			"image_edge": null,
396			"account_id": null,
397			"project": null,
398			"location": null,
399			"api_key": "replacement-secret"
400		}
401	}
402}
403```
404
405An MCP definition has `enabled`, `transport: "stdio"|"http"`, `command`,
406`arguments`, `cwd`, `url`, `environment`, `headers`, `bearer_token_env_var`,
407`enabled_tools`, `disabled_tools`, `startup_timeout_ms`, and `call_timeout_ms`.
408Transport selects the applicable command or URL fields. Environment and header
409values are tagged `{ "source": "literal"|"env", "value": ... }`. Environment
410references resolve on the daemon host. Tool filters preserve the current MCP
411allow and deny controls. Definition enablement defaults to true.
412
413Writes add a `secrets` object containing `bearer_token`, `environment`, and
414`headers`. Secret maps accept replacement strings and per-key null removals.
415Reads replace that object with `secret_presence`, containing bearer presence
416and the two lists of names. Deleting a definition deletes its associated secrets.
417Candidate validation rejects duplicate environment or header names across public
418and secret maps. Header names compare case-insensitively. It also rejects more
419than one bearer source: `bearer_token_env_var`, `secrets.bearer_token`, or an
420Authorization header. Switching sources requires removing the old source in
421the same patch. The validated connection uses the candidate's resolved values.
422`validate_connection` is a write-only group control, outside definitions:
423
424```json
425{
426	"validate_connection": true,
427	"definitions": {
428		"example": {
429			"enabled": true,
430			"transport": "http",
431			"url": "https://tools.example/mcp",
432			"secrets": {
433				"bearer_token": "replacement-secret",
434				"headers": {"X-Old-Key": null}
435			}
436		}
437	}
438}
439```
440
441MCP capability preferences are separate from configured enablement. Effective
442selection uses the session override, then the global preference, then true.
443A definition with `enabled: false` remains unavailable regardless of selection.
444Global MCP preference changes use the same catalog revision guard as global
445skill and instruction choices. Other group writes use `extensions.defaults`,
446`models.raised_caps`, or `ui.thinking`, `ui.tools`, and `ui.dismissed_notices`.
447Capabilities read as `preferences` keyed by stable preference key; their
448write-only selection fields are siblings in the capabilities group.
449
450Every successful patch returns `{ "group": ..., "resource": ..., "application": ... }`.
451`resource` contains the updated value, canonical URL, and validator.
452`application` includes desired revision, active service revision,
453`composition_changed`, validation, and warnings. `composition_changed` says
454whether the save changed the desired composition; it is always false for
455`providers`, `models`, and `ui`, which never feed it. A patch does not observe
456sessions: `GET /sessions?needs_reload=true` pages the ones that need a reload,
457observing loaded sessions a few at a time.
458Persisted defaults do not claim that every open
459session reloaded. Global services use the new desired state on subsequent
460requests. Sessions apply composition through their explicit reload operation.
461
462### Storage accounting
463
464`GET /storage` returns `database`, `sessions`, `images`, `files`, and
465`measured_at`. Session items include ID, title, workspace, creation and activity
466timestamps, and estimated content bytes. Images report shared count and bytes.
467Database fields include main-file bytes, WAL plus SHM bytes, page size, total
468pages, free pages, and estimated used bytes. Files contain daemon-owned state
469file categories, sizes, and cleanup-candidate metadata.
470The database keys are `main_file_bytes`, `wal_bytes`, `page_size_bytes`,
471`page_count`, `free_page_count`, and `used_bytes`. Sessions have
472`estimated_content_bytes`; shared images have `count` and `bytes`.
473
474Session estimates count pinned context, transcript content, cell sources,
475payloads, and traces. Shared images count once outside session estimates.
476Estimates do not claim allocated disk space per session. Database measurements
477share one read boundary; filesystem measurements describe the inspection period.
478Session and file lists use separate `next` tokens within this response, with
479each token identifying its section. Totals always cover the complete store.
480
481An online client needs no filesystem or Python access. Returned paths describe
482the daemon's files and never authorize local deletion. Offline diagnosis and
483approved cleanup remain explicit local operations under the ownership lock.
484
485## Sessions and durable input
486
487### Session representations
488
489`SessionSummary` contains `id`, `name`, `automatic_name`, `workspace`, `parent_id`,
490`root_id`, `address`, `depth`, `closed`, `created_at`, `activity_at`,
491`provider_profile`, `model`, `effort`, `status`, `preview`, `preferences`,
492`current_progress`, `activity`, and `cursor`. Each summary captures its own
493status, progress, activity, and cursor together. Family `address` stays stable
494when display name changes.
495
496`status` contains `phase`, nullable `run_id`, `interrupt_requested`, and a
497nullable safe `blocking_reason`. Phases are `idle`, `preparing`, `generating`,
498`running`, `interrupting`, and `compacting`. `preview` has at most 256 Unicode
499scalar values and 1,024 UTF-8 bytes, and includes the durable transcript row count.
500One transcript row can produce several history entries. Continuation markers
501do not add transcript rows.
502`preferences` contains `pinned`, nullable `pin_order`, `archived`, and `opens`.
503Pin order preserves the shared ordered pin list. Pinning assigns its order once;
504repeating `pinned: true` does not move the row or increment anything.
505Unpinning clears the order. Only `pinned` and `archived` are writable; opens
506and pin order are read-only.
507
508`activity` provides the agent graph's live tails without a session subscription
509for every node. It contains up to 12 `lines`, each with `kind` and `text`.
510Kinds are `assistant`, `thinking`, `tool`, `input`, `note`, and `error`.
511The current partial line is included. Each line has at most 256 Unicode scalar
512values and 1,024 UTF-8 bytes; the complete encoded activity object is at most
51316 KiB, shortening oldest lines first. Tool-argument previews stay in
514`current_progress` and are not copied into these lines.
515
516Activity also includes `output_scalars`, `output_utf8_bytes`, and `observed_at`.
517The counters cover generated text and reasoning in this actor generation.
518Clients derive display rates from successive samples and decay them while
519quiet. Nullable `latest_input: { "input_id": ..., "source": ..., "bytes": ... }`
520and `latest_answer: { "message_id": ..., "bytes": ... }` let clients animate
521new human input and completed root answers by identity. Input source is `chat`,
522`agent`, or `system`; bytes count UTF-8 display text. Initial reads install
523these identities without replaying old animations. The actor maintains this
524bounded activity once, before subscriber fan-out. Full content stays in history.
525
526`Session` adds:
527
528- `creation`, the immutable submitted creation intent and separately resolved defaults.
529- `revision` and `family_revision` for conditional edits and family operations.
530- `configuration_resource`, containing its canonical URL, value, and `etag`.
531- `workspace_change`, nullable desired versus active workspace and deferred state.
532- `selection`, containing session overrides and effective selections.
533- `composition`, containing desired and loaded revisions, `needs_reload`,
534  dependencies, quarantine diagnostics, and capability availability.
535- `kernel`, containing state, build identity, staleness reasons, live-job
536  count, and up to 100 running-job summaries ordered by ID. Each summary names
537  the job, its observed PID (nullable), and its command (at most 4096 scalars).
538  Unknown counts and lists are null; known empty lists are `[]`. The count can
539  exceed the bounded list. Reads observe the kernel without preparing it.
540  No Python variables or executable process handles leave the daemon.
541- `pending_inputs`, at most 32 bounded input summaries with acceptance order,
542  and `input_order`, the greatest order ever accepted into this session.
543- `usage`, including token and cache counters, elapsed time, model limits,
544  observed cache TTL, and time-indexed cache fade observations. `elapsed_ms` is
545  the span of the provider call that produced the usage, and `tokens_per_second`
546  is its completion tokens over that span; both are null when no call was timed.
547- `history`, a bounded recent page, with its older-page token.
548- `cursor`, the generation and sequence captured with the live state.
549- `glances`, bounded extension-provided summaries and links to their resources.
550
551`input_order` is zero before the first admission.
552
553Pending summaries include input ID, kind, display preview, admission time,
554acceptance order, delivery state, and blocking reason. They omit image content
555and expanded model input. Collection summaries and detail reads do not parse
556tool arguments or reconstruct previews.
557
558### Collection reads
559
560`GET /sessions` accepts `scope=roots|all` and optional `parent_id`, `family_id`,
561`workspace`, `search`, `archived`, and `needs_reload`. The parent and family
562filters are mutually exclusive. `family_id` selects the containing root
563and all descendants. `search` matches display names and ID prefixes.
564`sort=activity|frequent` defaults to activity; frequent sorts by opens, activity,
565and ID. Pinned state remains available for client grouping.
566
567The CLI's prompt, read, send, and stop workflows resolve a full lowercase UUIDv7
568with `GET /sessions/{id}?view=configuration`, including child sessions. Only
569shortened IDs search the root-session collection and require an unambiguous match.
570
571Parent and family queries default to `scope=all`; other queries default to roots.
572An explicitly supplied `scope=roots` with a parent or family filter returns `400`.
573The result is a page of `SessionSummary` objects. A family query also returns
574`family: { "root_id": ..., "revision": ... }`. Agent trees are drawn from
575`parent_id`, `address`, and `depth`; there is no second agent identity or graph
576endpoint. Clients can traverse all pages. A missing requested family returns
577`404`, while an existing parent with no children returns an empty page.
578
579`Accept: text/event-stream` selects the collection watch described below.
580
581### Creation, forks, and children
582
583`PUT /sessions/{session_id}` requires a fresh client-generated UUIDv7 and
584`If-None-Match: *`. Its body is one creation variant:
585
586| `kind` | Fields | Behavior |
587| --- | --- | --- |
588| `new` | `workspace`; optional `name`, `provider_profile`, `model`, `effort` | Create an independent session. |
589| `fork` | `source_session_id`, `checkpoint_id`; optional `name` | Copy the durable prefix and selection overrides into an independent session. |
590| `child` | `parent_id`, `address`, `name`, `initial_input_id`, `task`; optional `model`, `effort` | Create a family member inheriting workspace and provider, with an initial durable task. |
591
592The daemon resolves defaults once at first admission. Child creation, family
593membership, creation decision, and initial input admission commit together.
594A rejected task leaves no orphan child. The child task is ordinary identified
595user input. Forks preserve full durable tool arguments and results, image
596references, captured execution traces, continuation boundaries, and selected extension overrides.
597They copy no Python namespace, live jobs, progress, or usage totals. The daemon
598closes incomplete tool exchanges at the chosen checkpoint using its existing
599history rules.
600
601Success returns `201` with `Session`, its `Location`, and `ETag`. A retained
602rejected creation returns its original problem and admission status on retry.
603An existing session fails the create precondition with `412`. Recovery reads
604the session and compares `creation.submitted` structurally with the original
605intent. Optional creation fields absent from the request appear as null in
606this descriptor. String contents stay exact. Matching intent proves creation;
607a different intent is an ID conflict. Resolved workspace and provider defaults
608appear in `creation.resolved` and do not change the submitted descriptor.
609Current model, title, or workspace cannot replace that provenance check.
610Sessions stored before immutable provenance was recorded return
611`creation: null`. They remain readable, editable, and forkable, but cannot
612prove a matching creation request. Migration never invents submitted intent
613from their current configuration. Every newly admitted creation has provenance.
614Conditional creation follows
615[HTTP If-None-Match semantics](https://www.rfc-editor.org/rfc/rfc9110.html#section-13.1.2).
616
617Creation decisions and deletion tombstones remain queryable at the same URI.
618Rejected creation reads return their original problem with `decision` metadata;
619deleted session reads return `410 session_deleted` with creation provenance.
620An unknown young ID returns `404`, which cannot prove an in-flight PUT will
621never commit. Recovery retains the same intent and identity.
622
623`GET /sessions/{session_id}` returns `Session`. `tail`, default 100 and maximum
624200, selects the recent history window. The byte bound can shorten the window.
625Status, progress, pending ownership, and the cursor share one session-actor
626capture. History is read through that capture's durable high-water mark.
627The configuration view does not accept `tail` or stream cursor parameters.
628
629### Changes, visits, and deletion
630
631`GET /sessions/{session_id}?view=configuration` returns only editable metadata,
632preferences, overrides, revision, and family revision, with a strong `ETag`.
633The full session read embeds this same value and validator for an edit.
634`PATCH /sessions/{session_id}?view=configuration` accepts `name`, `provider_profile`,
635`model`, `effort`, `preferences`, and `selection`. An empty or null name restores
636the automatic title for a root, or the original family name for a child.
637Pin and archive changes are shared preferences. Model and
638composition changes require an idle session. Selection maps accept true,
639false, or null to clear the override and inherit the global default.
640A patch enabling one compaction strategy clears competing earlier true
641overrides to inheritance; enabling multiple strategies in the same patch
642returns `400 selection_conflict`, and explicit false or null choices keep
643their meaning.
644When model or profile changes without an explicit effort, keep a supported
645current effort, then try the profile default and model default. An explicit
646null clears effort; an explicit unsupported level fails validation.
647Selection has `extensions`, `skills`, `instructions`, and `mcp` maps keyed by
648their catalog IDs. MCP entries enable or disable installed server definitions;
649their IDs are stable definition names. Definitions and secrets belong to shared
650settings. Skill and instruction choices additionally include the write-only
651`catalog_revision` from the discovery read.
652A stale catalog returns `409 catalog_changed`.
653The session owner prepares a valid candidate before committing the patch.
654Editable session data and preferences commit together through the store owner.
655The result includes the new configuration resource and captured session state.
656PATCHes set explicit values; they never toggle or increment preferences.
657
658For example, a session selection patch clears one override and disables a skill:
659
660```json
661{
662	"selection": {
663		"extensions": {"browser": null},
664		"skills": {"candidate-123": false}
665	},
666	"catalog_revision": "observed-discovery-revision"
667}
668```
669
670Workspace changes use the same PATCH with `workspace` as its only change,
671alongside the observed `family_revision` as a write-only precondition.
672The daemon validates the destination and captures following descendants under
673the family revision. A busy target session returns `409 session_busy` before
674any move; the UI retains its deferred move intent. An idle parent can move while
675a descendant is busy. Destination metadata and deferred descendant intents
676commit in one store transaction before kernel cleanup. Busy descendants finish
677their captured turn in the old workspace and apply the new workspace at idle.
678Their `workspace_change` reports desired location, active location, and state
679`deferred` or `applying`. This intent survives client disconnect and daemon restart.
680
681The move preserves transcript and family addresses, resets affected kernels
682and Python namespaces, and returns applied and deferred counts, bounded ID
683lists, `truncated`, and cleanup warnings. When truncated, clients refresh the
684family rather than repeating a move to discover its result.
685Cleanup failure cannot be reported as rollback of a committed move.
686Affected sessions cannot begin a new turn in an old kernel after the commit.
687A later explicit move supersedes the earlier desired location by revision;
688a delayed completion cannot restore an older choice. New descendants inherit
689the parent's desired workspace at their admission.
690
691Model selection does not implicitly change the default for new sessions.
692The TUI's model picker explicitly updates the shared default when appropriate.
693A later default or cap failure leaves a confirmed session model change visible.
694
695`PUT /sessions/{session_id}/visits/{visit_id}` accepts an empty object and counts
696one deliberate attachment. Repeating the UUIDv7 returns the same count without
697incrementing it again. Background refreshes do not create visits. Reads never
698change opens. Deleted sessions cannot be revived by a visit.
699
700`DELETE /sessions/{session_id}?view=configuration` defaults to `scope=leaf`.
701It requires the configuration `If-Match`, an idle session, and no children.
702`scope=subtree` also requires
703the `family_revision` query parameter from the family read. The daemon captures exactly that
704membership, prevents new child admission during this operation, interrupts
705members, cancels pending inputs, and deletes deepest first.
706
707Deletion returns `200` with `state: "complete"|"partial"`, `deleted_count`,
708`remaining_count`, `deleted_ids`, and `remaining` objects containing ID and
709safe reason. Counts cover the captured operation; lists contain up to 200
710IDs each and can shorten further to meet the byte limit, with `truncated: true`.
711Clients refresh the authoritative family or read known deletion tombstones.
712They never repeat deletion to page its result. Partial deletion is visible.
713Any successful deletion changes the family revision. A retry with the old
714revision cannot broaden the operation or delete new descendants. The client
715refreshes and confirms the remaining membership before a new deletion attempt.
716This operation has no automatic replay after transport loss.
717
718### Input admission and outcomes
719
720`PUT /sessions/{session_id}/inputs/{input_id}` uses a client UUIDv7. The body
721has optional `client_id` for echo correlation and exactly one variant:
722
723| `kind` | Fields |
724| --- | --- |
725| `message` | `text`; optional `images`, up to ten, each with `mime_type` and base64 `data`; optional `pastes`, up to twenty texts |
726| `continue` | No content fields |
727| `skill` | `candidate_id`, `catalog_revision`, and `arguments` as text |
728| `command` | `command_id` and typed `arguments`; only a catalog entry with `delivery: "input"` |
729
730A message requires nonblank text or an image. Images accept PNG, JPEG, GIF,
731and WebP, subject to decoding, dimension, and selected-provider bounds. The
732model receives the text, then the images in upload order. The CLI marks
733where each was attached with `[Image #N]` in the text, numbered from 1 in
734upload order. Pastes are marked the same way, `[Paste #N, …]` for the Nth
735paste. The model receives a paste under 100 lines and 32 KiB in place of its
736marker. A longer one is saved to `$ALBEDO_HOME/pastes/<session>/<input>-<n>.md`
737and its marker names that file. History shows the text as sent, markers
738intact. The
739daemon preserves original upload metadata and the full durable payload.
740Provider-specific fitting stays daemon-owned and appears in durable history.
741
742Command inputs prepare a user turn. They do not invoke arbitrary management
743handlers. Their preparation produces content, runs outside the session actor,
744and persists the resolved content before transcript consumption. Preparation
745must be repeatable without external side effects. An extension that performs
746effects exposes its own domain operation instead.
747
748The first accepted PUT returns `202` with `Input`. An identical PUT returns
749the retained admission decision and current delivery state, using the original
750admission status. Changed intent under the same ID returns `409 input_conflict`.
751Echo-only `client_id` does not change identity. Lookup happens before current
752workspace, skill, provider, and queue validation. Defaults and skill content
753resolve once. Rejected decisions are durable, including `429 queue_full`.
754
755`GET /sessions/{session_id}/inputs/{input_id}` returns the known decision even
756after session deletion. `Input` contains `id`, `session_id`, `kind`,
757`admission`, `accepted_at`, `acceptance_order`, `delivery`,
758`blocking_reason`, `transcript_position`, and nullable `turn`. Admission is
759`accepted` or `rejected`; rejected admission includes the original safe problem
760and HTTP status. Delivery is `pending`, `committed`, or `cancelled`, and is
761null for rejection. A continuation has a durable position without a text row.
762
763`turn` identifies consumed turn membership and any durable terminal outcome:
764`running`, `completed`, `interrupted`, `failed`, or `abandoned`. A restarted
765daemon marks an unfinished previous run abandoned rather than claiming its
766external effects completed. One turn can contain several inputs. Its nullable
767`outcome` contains separately recorded detail; null means no detail was recorded.
768Acceptance means safe storage. Commitment means transcript consumption.
769Neither means that the assistant answered or a tool effect ran exactly once.
770
771There are at most 32 waiting inputs per session. A duplicate consumes no slot.
772Consumption commits transcript rows, images, input delivery state, turn
773membership, and pending removal in one store transaction. Failure leaves the
774input pending. Preparation failure exposes a safe blocking reason; it cannot
775install a successful delivery state. Waiting accepted inputs survive restart.
776
777Known pending identities and inputs belonging to an unfinished turn never
778expire. An input becomes terminal only when delivery and its associated turn
779are terminal. Its admission record remains for seven days after that point.
780Rejected decisions become terminal at rejection. Live sessions retain creation provenance
781for their lifetime. Deleted sessions retain tombstones for seven days.
782Known decisions are checked first. Unknown UUIDv7 identities older than seven
783days return `410 identity_expired` and cannot execute; identities more than
784five minutes ahead return `400 identity_future`. This prevents recreation after
785deduplication records expire. These rules also cover visit identities.
786
787Recovery queries the same input URI and may resend the exact same PUT within
788the caller's deadline. `404` means unknown at the time of the read, not safe
789to invent another ID. `410` stops automatic recovery and keeps an unresolved
790UI row. A matching durable event can still reconcile that row later.
791
792### Targeted cancellation and interruption
793
794`POST /sessions/{session_id}/inputs/{input_id}/cancel` accepts an empty object.
795It returns the current `Input` and one `result`:
796
797- `cancelled`: a waiting input was removed and its cancellation committed.
798- `interrupt_requested`: its current turn belongs exclusively to this input.
799- `shared_running`: other inputs share the active turn; the turn continues.
800- `not_pending`: no waiting or interruptible exclusive work remains.
801
802Every input kind uses the same identity for admission, membership, and cancel.
803This operation never interrupts a later unrelated turn. Repeating it reports
804the current outcome of the same input, without resubmitting that input.
805
806`POST /sessions/{session_id}/interrupt` requires `run_id`, nullable, and
807`through_input_order`, taken from the session snapshot. It cancels still-waiting
808inputs at or below that order and requests interruption of exactly that run.
809Newer queued inputs remain accepted. An already ended run is reported as ended;
810a later run is never interrupted by retrying this request. Acceptance of an
811interrupt request does not mean all external tools stopped.
812
813The result contains `run_id`, `state: "requested"|"already_ended"`,
814`cancelled_input_ids`, and warnings. Interruption prevents new inputs joining
815the captured run. Live status reports `interrupting` until the run ends.
816
817## History, context, catalogs, and runtime
818
819### Durable history
820
821`GET /sessions/{session_id}/history` accepts `view=entries|checkpoints`, default
822entries, and one of `before`, `after`, or `next`. Positions are durable
823transcript positions, unrelated to the actor cursor. Pages retain ascending
824display order and include `older`, `newer`, and `high_water`.
825Default entry count is 100, maximum 200, with a 256 KiB content budget.
826
827Each entry has stable `id`, `position`, `kind`, `turn_id`, `input_id`,
828`created_at`, `content`, nullable `checkpoint_id`, and `content_complete`.
829User entries, assistant answers, and tool calls expose checkpoints for their inclusive durable
830transcript row. Calls packed into one row share a checkpoint; a fork cannot
831split that row. Checkpoint view lists user turns for the TUI fork picker.
832Recorded thinking duration is exposed as `thinking_duration_ms`; live thinking
833completion can report `elapsed_ms`. These measure thinking, not the whole turn.
834
835Kinds are `user`, `assistant`, `thinking`, `tool_call`, `tool_result`, `note`,
836`continuation`, `compaction`, and `image_fit`. Content is a tagged set of text,
837JSON tool arguments or results, image metadata, and execution trace fields.
838Tagged daemon notes retain their native origin as a JSON part with `field: "origin"`.
839Tool names, native call IDs, normalized progress-call IDs, activity summaries,
840and file diffs remain available. Provider-private fields are explicit opaque
841data; clients do not reinterpret them as execution instructions.
842
843The daemon pages on complete display groups when they fit. A large group uses
844content references rather than growing a response past its bound. Preview
845truncation is explicit and never changes the stored content. Checkpoint view
846contains position, valid checkpoint ID, title, turn type, and bounded preview.
847It provides the TUI tree and fork picker without a separate tree route.
848
849`GET /sessions/{session_id}/history/{entry_id}` returns the full entry content
850as pages of `parts`, each with `field`, `offset_bytes`, `text`, and `complete`.
851Text and JSON fields use their exact stored UTF-8 encoding; offsets are bytes,
852and pieces end on UTF-8 boundaries. Opaque `next` identifies the next piece.
853The decoded content budget is 256 KiB per page. The daemon shortens a page
854further when JSON escaping would reach the common encoded response bound.
855Image fields are not paged. An image's metadata gives its MIME type,
856dimensions, byte count, and a reference to
857`GET /sessions/{session_id}/history/{entry_id}/{field}`, which returns the
858stored bytes with the image's own `Content-Type`. Clients can reconstruct full
859arguments, results, and images without filesystem access.
860
861### Prepared context and request records
862
863`GET /sessions/{session_id}/context` defaults to `view=summary`. It describes
864the latest actual prepared request, never prepares a new one. The summary has
865`state: "pending"|"ready"`, nullable `snapshot_id`, capture time, provider,
866model, protocol, context-window tokens, compaction observations, and sections.
867Each section contains ID, label, kind, source, item count, UTF-8 byte count,
868page count, and bounded preview. Pending state contains a reason.
869
870`view=section` requires `snapshot_id`, `section_id`, and a zero-based `page`.
871A page has `text`, omitted-content explanation, page index, and page count.
872Text has a 32 KiB UTF-8 limit and ends on a character boundary. Only the latest
873snapshot is retained. If it changes during navigation, the old identity returns
874`410 context_changed`; the client refreshes the summary. A page never silently
875comes from another prepared request.
876
877`view=requests` returns paged provider request records. `after` is a durable
878record sequence. Records include provider profile and account labels, model,
879nullable run identity, timestamps, request kind, outcome, HTTP status, token
880observations including reasoning and cache writes by TTL, cache marks, head and
881projection hashes, input counts, compaction strategy, bounded safe failure
882details, and links to source transcript positions. Unknown historical run
883identity is null. Full provider request bodies and credentials are absent.
884Estimated and observed token counts have separate fields and sources.
885
886`GET /models?view=cache-policy` reads the cache-policy table and its layer
887diagnostics. Entries retain matching patterns, clock, tiers, prices, survival,
888evidence, sources, and notes. `limit` and `next` page entries. Optional
889`extension`, `host`, and `model` selectors return the first matching entry in
890`matched`, independent of the displayed page. This view does not require a
891provider profile and never connects to a provider. Catalogue view remains the
892default.
893
894### Discovery and loaded composition
895
896`GET /sessions/{session_id}/catalog` returns `discovery` and `loaded` as
897separate fields. Optional `kind=extensions|skills|instructions|mcp|commands`
898filters entries without changing these identities. Discovery contains
899`revision`, workspace, candidates, and diagnostics. It reflects current local files
900and preferences. Remote files come from the last observed mirror; preparation
901refreshes stale mirrors. Loaded contains its composition revision and prepared command
902definitions. Reading discovery does not reload the prompt or start a kernel.
903Before a composition exists, `native_commands` supplies static HTTP declarations
904from selected extensions. Clients use these declarations only while the loaded
905revision is null; observing them does not prepare an extension or start a kernel.
906If desired discovery fails, `discovery` is null and `discovery_failure` contains
907a safe reason. Retained loaded commands remain readable. Successful discovery
908sets `discovery_failure` to null; clients need a discovery revision to edit selection.
909
910Candidates contain `id`, `kind`, `title`, `description`, lexical `source`,
911nullable `resolved_source`, `preference_key`, `valid`, `eligible`,
912`effective_enabled`, nullable global preference and session override,
913`shadowed_by`, dependencies, quarantine state, and diagnostic. Extension candidates
914also include typed context, tool, Python module, and plugin metadata for inspection.
915Other candidate kinds return `extension: null`.
916Selections use candidate IDs and the observed discovery revision. Paths are
917daemon metadata, not client filesystem instructions. Diagnostics survive
918missing, invalid, shadowed, and quarantined entries.
919
920Command definitions contain ID, slash name, description, typed argument
921declarations, caller permissions, and `delivery: "read"|"mutation"|"input"`.
922They identify either a canonical HTTP operation or a durable input variant.
923There is no public endpoint that accepts an arbitrary slash command string.
924The CLI parses slash syntax into the named operation; Python bindings call
925the same domain owner with trusted model caller context.
926
927Optional page descriptors contain title, empty-state text, rows, glance,
928and actions. Rows include ID, text, badge, tone, detail, and the observed
929resource URL and `etag` for an editable item. Actions declare
930label, optional keyboard hint, confirmation text, HTTP method, relative path
931template, typed path, query, body, and header bindings, and result schema.
932Path, query, and header binding keys name declared parameters. Body binding
933keys are JSON pointers into the request object; they also support nested fields.
934Bindings read a literal, the displayed row, submitted form, or installed session
935snapshot. Form fields can declare `default_binding` with a `row` or `session`
936source and JSON pointer to prefill an edit. The resolved value replaces the
937literal default and must fit the declared field type. Missing optional form values omit their bindings. Result schemas
938are JSON Schema 2020-12 fragments, without remote schema retrieval.
939An operation can declare `success_status` as 200 or 201. Without it, reads
940require 200 and mutations accept 200 or 201; acknowledgment validation still
941applies.
942An edit binds `If-Match` to the row's observed validator. The adapter never
943fetches a newer validator just to overwrite data from a stale screen.
944Field types are text, secret,
945boolean, choice, integer, or hidden literal. Descriptors cannot change retry
946policy or authorization. The adapter rejects foreign origins and undeclared
947extension routes. Extension-specific response content stays an explicit
948dynamic JSON body, such as `json.RawMessage` in Go.
949
950### Reload, kernel upgrade, and compaction
951
952`POST /sessions/{session_id}/reload` accepts
953`target: "session"|"models"|"both"`, default both. Session reload requires idle,
954builds the candidate composition before swapping it, and retains the previous
955composition on failure. Success preserves the Python namespace and pinned
956prompt prefix, except where an extension's declared lifecycle requires restart.
957The result includes loaded revision, restart requirements, and warnings.
958Model reload reports success or failure per provider and keeps old catalog
959data where refresh failed. It also reloads cache policy, reported separately as
960`cache_policy` with state and failure; session-only reload returns null there.
961`both` returns these outcomes explicitly; one
962confirmed refresh is not hidden by failure of the other.
963
964`POST /sessions/{session_id}/kernel/upgrade` accepts an empty object and requires
965idle. It stages the new kernel, reports the fate of live jobs, copies supported
966namespace state, and swaps only after validation. Failure keeps the previous
967kernel usable where the existing upgrade boundary permits it; stopped external
968jobs are reported as stopped and cannot be claimed restored. The response
969contains old and new identities, upgrade state, stopped jobs, and warnings.
970
971`POST /sessions/{session_id}/compaction` accepts an optional `strategy`. It
972requires idle and compacts the model-facing projection without deleting
973transcript history. A supplied strategy first becomes the session override.
974The response reports `selection_applied`, the effective strategy, compaction
975state, before and after counts, and safe failure details. A failed compaction
976can retain a successfully selected strategy; the response preserves that fact.
977Neither runtime action automatically retries an uncertain external effect.
978
979## Models, workspaces, hosts, and provider login
980
981`GET /models` requires `provider_profile`, or `provider` with optional
982`endpoint` for a not-yet-saved profile. It always returns model objects,
983never a mode-dependent array of plain IDs. Objects contain ID, label, effort
984choices, default and effective context tokens, nullable maximum context and
985output tokens, input modalities, image edge, raised state, and cache policy.
986Unknown facts are nullable. Optional `model` returns a single metadata row
987for a requested ID, including an unlisted manual ID and its `cap_key`.
988Manual IDs remain accepted when the provider cannot enumerate them.
989Metadata includes source and freshness.
990
991`GET /workspaces` without `location` lists recent canonical workspaces with
992use counts and last activity. With `location`, it returns `directory`, child
993directories, canonical parent, home, host state, and pagination. `include=preview`
994adds repository facts, language shares, and a bounded two-level file tree.
995Git and jj facts retain their distinct typed variants. A missing optional VCS
996fact is null with a diagnostic, rather than loss of the entire directory.
997The tree has at most 12 top-level entries and four children per directory;
998`more` counts omissions. Local and SSH locations share this representation.
999Paths are interpreted and canonicalized on the daemon's host.
1000
1001`GET /hosts` returns known SSH targets and cached state, optionally filtered by
1002`target`. State is `unknown`, `probing`, `ready`, `needs_auth`, `unreachable`,
1003or `unsupported`, with safe detail, observation time, OS, architecture, home,
1004and nullable authentication instructions. `POST /hosts/{host}/probe` starts
1005or joins one bounded probe and returns `202` with the host state. Repeated
1006probe requests coalesce. GET does not start SSH sign-in.
1007
1008SSH sign-in remains an explicit operator action. A local CLI can perform the
1009existing terminal handoff using daemon-supplied target and control-path
1010metadata after confirmation. A client on another machine displays instructions
1011for signing in on the daemon host. This API does not promise a browser terminal.
1012Filesystem failure codes distinguish missing workspace, needs-auth,
1013unreachable host, unsupported host, and permission denied.
1014
1015`GET /auth` returns login providers, their form fields and supported flows,
1016redacted accounts, and account selection metadata. It does not duplicate
1017credential values from settings. `PUT /auth/logins/{login_id}` uses UUIDv7,
1018with provider and declared typed form values.
1019The server records immutable login intent and returns URL, expiry,
1020`state`, and manual-input instructions. First creation returns `201`;
1021an identical PUT returns the existing flow with `200`. Changed intent returns
1022`409 login_conflict`. Same-ID comparison stays inside the daemon and includes
1023the private form values. A duplicate never starts a second flow.
1024Login secrets never appear in the read representation or its fingerprint
1025provenance fields. Fingerprints are server-created and are not exposed for
1026low-entropy login secrets.
1027
1028GET inspects that flow. `PATCH` supplies `response`, such as a pasted callback
1029URL or provider code, under `If-Match`. DELETE cancels it idempotently. States
1030are `waiting`, `exchanging`, `complete`, `failed`, `cancelled`, and `expired`.
1031Flow reads include bounded safe progress and globally unique opaque account
1032IDs that also identify their provider. Completed flows
1033return account metadata. Terminal flow metadata remains for 15 minutes,
1034uncompleted flows expire after their advertised provider deadline, and at
1035most eight flows are active. Duplicate creation never opens a second flow.
1036An expired or evicted ID cannot restart a flow. Unknown IDs older than the
103715-minute flow recovery window return `410`.
1038
1039Login admission intent and terminal decisions survive daemon restart within
1040their retention window. A restart marks an unfinished flow `failed` with
1041`code: "daemon_restarted"`; it does not reuse a lost listener or exchange.
1042Repeating its PUT returns that retained failure. Starting again requires a
1043new login ID. The daemon retains only the protected secret material needed
1044for immutable-intent comparison and removes it when the record expires.
1045
1046OAuth callbacks stay on provider-required daemon-owned loopback listeners.
1047They validate provider state and PKCE where the provider supports it. Manual
1048callback input preserves remote-client usability. Secrets and callback codes
1049stay out of logs and SSE. Selecting an account updates a provider profile
1050through `/settings`; `DELETE /auth/accounts/{account_id}` refuses removal
1051while a saved profile still references it. It does not interrupt a running
1052request using captured credentials.
1053
1054## SSE delivery and recovery
1055
1056### Session watch
1057
1058`GET /sessions/{session_id}` with `Accept: text/event-stream` watches one
1059session. Initial attachment omits `after_generation` and `after_seq`.
1060Reconnect sends both from the last fully consumed batch. `tail` applies when
1061a snapshot is needed. JSON reads return the same cursor pair for subsequent
1062attachment. `Vary: Accept` distinguishes the representations.
1063A successfully installed JSON session snapshot counts as the initial reset.
1064Its cursor can seed the SSE watch after the snapshot callback succeeds.
1065Without an installed snapshot, the client omits the cursor and requires reset.
1066
1067A batch uses this envelope. `snapshot` appears only with a leading reset.
1068
1069```json
1070{
1071	"generation": "J6kYj6IGcXyP3eMIp-WL3A",
1072	"cursor": 42,
1073	"events": [
1074		{"type": "reset", "data": {"reason": "initial"}}
1075	],
1076	"snapshot": {
1077		"id": "019a0000-0000-7000-8000-000000000001",
1078		"history": {"items": [], "older": null, "newer": null, "high_water": 0},
1079		"current_progress": [],
1080		"pending_inputs": []
1081	}
1082}
1083```
1084
1085The example shows the relevant snapshot fields; a real snapshot is the complete
1086`Session` representation defined above. Generation is a nonempty base64url
1087128-bit random identity created by the session actor initializer. Sequence is
1088a nonnegative integer in that generation. Empty keepalive batches carry both
1089values and an empty events array. Keepalives do not advance the event sequence.
1090The `data:` JSON is authoritative; this protocol does not use SSE `id` or
1091automatic `Last-Event-ID` recovery.
1092
1093The session checks generation equality before consulting numeric replay
1094bounds. Missing or different generation, or an unavailable sequence, requires
1095a durable snapshot reset. Malformed cursor components return `400` before
1096headers. A well-formed future or evicted cursor resets. The replay buffer
1097retains at most 256 events or 992 KiB, whichever bound is reached first.
1098An oversized event is represented by bounded history references.
1099
1100Registration and state capture serialize with session publication. A reset
1101captures state, bounded current progress, pending ownership, and replay cursor
1102together, then loads history through the captured durable high-water mark.
1103It also captures ordered `active_output` segments. Their inline text or signed
1104content references include the complete unfinished prefix at that cursor,
1105including output evicted from the replay buffer. Text and thinking share a
110664 KiB encoded inline budget. Spill references fix their byte cutoff and remain
1107readable for 15 minutes even when the response commits during hydration.
1108Content reads retain the original `snapshot` query while following `next`;
1109continuations cannot extend the original snapshot's lifetime. Expiration
1110returns `410 active_output_expired` and requires a fresh session snapshot.
1111Capture publishes any pending coalesced progress or activity before choosing
1112the cursor. Live-state changes advance the actor sequence; one cursor cannot
1113identify conflicting status, progress, or activity replacements.
1114Events after that cursor remain eligible for replay. Slow snapshot loading
1115cannot silently skip an evicted interval: the subscription resets again or
1116closes with failure. Transcript loading failure emits failure and closes;
1117it never installs a successful replacement cursor.
1118
1119Client delivery follows these rules:
1120
11211. Validate the complete batch, generation, sequence, and known event fields.
11222. Require a leading reset on first attachment or a generation change.
11233. On reset, replace visible recent history, pending state, and live progress.
1124   Stage full active-output hydration before installing the replacement.
1125   Restore history and the captured prefixes before delivering later deltas.
1126   Clear partial arguments and transient text from the previous stream.
11274. Deliver all events in order. Same-generation replay cannot regress the
1128   sequence without a reset.
11295. Save generation and sequence together only after every callback succeeds.
1130
1131EOF and transient failures retain the last consumed pair. Explicit recovery
1132and cancellation clear it. A generation change with reset is ordinary recovery.
1133A change without reset is a protocol failure. The client can attempt one
1134cursor-free recovery; a second protocol failure is surfaced to the user.
1135Reset history is authoritative for its covered window. Previously loaded older
1136history remains accessible by durable positions and is deduplicated by entry ID.
1137Reset replaces daemon-derived views. It preserves local drafts and unresolved
1138creation or input handles, reconciling them only by matching identities.
1139
1140### Session events and progress
1141
1142Each event has `type` and `data`, its typed body. A published session event
1143also has `sequence`, its position in the generation. Control events reset and
1144failure have no sequence. Session identity is implicit in the
1145subscribed URL. Events within a batch are ordered. There is no ordering claim
1146between streams belonging to different sessions. A failure control appears
1147alone, without a snapshot, and terminates the stream even before attachment
1148has succeeded. Clients never treat it as a successful reset.
1149
1150For example, an incremental batch appends text:
1151
1152```json
1153{
1154	"generation": "J6kYj6IGcXyP3eMIp-WL3A",
1155	"cursor": 43,
1156	"events": [
1157		{
1158			"type": "text",
1159			"sequence": 43,
1160			"data": {"run_id": "run-1", "message_id": "message-1", "text": "Hello"}
1161		}
1162	]
1163}
1164```
1165
1166Incremental event sequences increase contiguously from the requested cursor.
1167The batch cursor is the last published event represented by the batch. Clients
1168apply a batch atomically to local state or deduplicate already delivered events
1169by generation and sequence. This prevents duplication when an earlier callback
1170succeeds and a later callback fails before the batch cursor is saved.
1171
1172| Type | Body and behavior |
1173| --- | --- |
1174| `reset` | Reason `initial`, `generation_changed`, `replay_unavailable`, or `recovery`; requires the complete batch snapshot. |
1175| `status` | Current phase, run ID, interrupt state, and blocking reason. |
1176| `input` | `input`, a bounded `Input` outcome update, including membership and delivery changes. |
1177| `text` | Run ID, message ID, and appended assistant text. |
1178| `thinking` | Run ID, message ID, and appended reasoning text. |
1179| `message` | Completed display message with stable history entry identity. |
1180| `tool_progress` | `progress`, a normalized progress object, or null to clear all live progress. |
1181| `tool` | Completed call identity, progress identity, bounded arguments, result, trace, and full-content reference if needed. |
1182| `note` | Entry identity, origin, display text, and related family mail IDs where present. |
1183| `retry` | Run, provider attempt, safe reason, and retry delay in milliseconds; clears obsolete attempt progress. |
1184| `usage` | Typed token, timing, model-window, and cache observations. |
1185| `committed` | Durable high-water position, `replaces_live_ids`, and `replaces_all_live`. Model commits clear all provisional output; other commits replace only the listed identities. |
1186| `turn_completed` | Run ID, terminal outcome, and input membership. |
1187| `compacted` | Strategy and observation, without replacing durable history. |
1188| `error` | Safe error code and message for the turn; not transport authentication. |
1189| `invalidate` | Resource kind and relative URL whose metadata changed. |
1190| `failure` | Stream error code; terminal, never advances the saved cursor. |
1191
1192Assistant text and reasoning are provisional until committed. Clients reconcile
1193completed entries by stable identity, including every bounded chunk of a long
1194provisional segment. Canonical text and thinking publication precedes its
1195commit marker; clients replace provisional chunks together with that
1196publication. `replaces_all_live` also clears fragments omitted from bounded
1197backfill after a storage failure. Generation, model step, and attempt separate
1198live identities. Model retry can end an attempt without creating another user
1199input. Family mail events are durable notes with nullable
1200`mail: { "mail_id": ..., "sender_session_id": ..., "receiver_session_id": ...,
1201"kind": ..., "bytes": ... }`. Sender is null for external senders.
1202Kind is `task`, `message`, `result`, `unreviewed`, or `webhook`; bytes count
1203the UTF-8 message body. `unreviewed` is an answer forwarded after a child ended
1204without replying. A null sender also includes a bounded `sender_label`, such
1205as a hook name, at most 100 Unicode scalar values and 400 UTF-8 bytes.
1206The agent view animates these identities without interpreting note text.
1207
1208Progress contains `call_id`, nullable `tool_call_id`, `name`, `phase`,
1209`intent`, and optional `preview: { "offset_scalars": ..., "text": ... }`.
1210Phase is `generating` or `running`. Intent is `read`, `search`, `list`, `run`,
1211or `unknown`; unknown remains valid. Intent comes from declared tool metadata
1212or emitted execution evidence; generating code can have unknown intent.
1213The daemon does not parse Python just to guess a label.
1214The client does not accumulate arguments or infer Python
1215behavior. Calls interleave by `call_id`, which changes across actor lifetimes,
1216runs, provider steps, retries, and output indexes.
1217
1218Preview updates replace the latest window. The window has at most 512 decoded
1219Unicode scalar values and 2,048 UTF-8 bytes. `offset_scalars` counts decoded
1220source scalars before that window, including source newlines. It does not count
1221JSON escapes, UTF-8 bytes, grapheme clusters, or terminal columns. Names have
1222a 100-byte UTF-8 bound; native IDs over 200 bytes are omitted. One progress
1223event has an 8 KiB bound and an attempt tracks at most 32 calls. Excess calls
1224disable generating previews for that attempt while execution continues.
1225
1226First appearance and transition to running publish immediately. Other preview
1227updates coalesce over 100 ms. Attachment captures updates awaiting publication.
1228Completion, cancellation, failure, retry, and reset clear obsolete previews.
1229Argument parsing occurs once per provider fragment, before subscriber delivery.
1230Full tool arguments and results remain durable. Capability `tool_progress: 1`
1231is required; there is no legacy client reconstruction.
1232
1233### Collection watch and overload
1234
1235`GET /sessions` with `Accept: text/event-stream` uses the same scope filters as
1236the JSON collection, excluding pagination and sort. It sends invalidations,
1237not transcripts or duplicate full argument payloads. Its first batch contains
1238`ready` and `reset`, requiring an authoritative collection read. No cursor
1239or replay promise applies to this stream.
1240
1241```json
1242{
1243	"events": [
1244		{"type": "ready", "data": {}},
1245		{"type": "reset", "data": {"reason": "initial"}}
1246	]
1247}
1248```
1249
1250An incremental batch contains `invalidate` events with resource URLs and
1251affected session IDs, plus bounded `activity` replacements for visible agents.
1252An activity event has this typed body:
1253
1254```json
1255{
1256	"type": "activity",
1257	"data": {
1258		"session_id": "session-1",
1259		"cursor": {"generation": "J6kYj6IGcXyP3eMIp-WL3A", "sequence": 10},
1260		"status": {"phase": "idle", "run_id": null, "interrupt_requested": false, "blocking_reason": null},
1261		"current_progress": [],
1262		"activity": {
1263			"lines": [],
1264			"output_scalars": 0,
1265			"output_utf8_bytes": 0,
1266			"observed_at": "2026-10-02T14:00:00Z",
1267			"latest_input": null,
1268			"latest_answer": null
1269		}
1270	}
1271}
1272```
1273
1274Status, current progress, activity, and cursor are captured together and replace
1275the row's live state together. The cursor matches the summary cursor. Separate
1276updates cannot share a cursor while replacing only one of those fields.
1277Collection `mail` events carry the same mail metadata as session notes, without
1278message text. Peer labels come from session summaries; unknown peer IDs trigger
1279a metadata read. These animation events have no replay guarantee.
1280Scope filters cover both entry and removal, including a
1281workspace change moving a row out of the filter. The daemon invalidates the
1282scope when it cannot resolve membership cheaply or safely.
1283
1284A client subscribes, waits for ready, then reads authoritative JSON pages.
1285Changes during loading leave the view dirty and cause another refresh.
1286Activity replacements arriving during that read are applied afterward by
1287their per-session generation and sequence. Equal or older sequences cannot
1288overwrite a newer row. An actor-generation mismatch requires refreshing that row.
1289This yields a current view without claiming an atomic snapshot across actors.
1290Reconnect always repeats the readiness and refresh flow.
1291
1292The bus queue holds at most 256 events and 1 MiB per subscriber. Mailboxes carry
1293at most one outstanding wake, never full payloads. Publication uses bounded
1294nonblocking admission. Session streams read the existing replay buffer rather
1295than maintaining additional payload queues. Flushes coalesce over 100 ms.
1296Progress and display events have independent payload bounds.
1297
1298At overflow, the collection stream emits `overflow` and closes if the socket
1299can still accept the signal. Closure alone also requires refresh; delivery of
1300the final marker cannot be guaranteed to a stalled reader. The client refreshes
1301by resubscribing, waiting for ready, and then reading the authoritative tree,
1302status, progress, and glances. It does not append new state to an obsolete graph.
1303Disconnect and failed writes release the subscription and its queue.
1304
1305Both stream kinds send keepalives at least every five seconds when quiet.
1306Sockets have bounded write waits. Publication never awaits subscriber socket
1307writes. A stalled reader cannot retain an unbounded mailbox or force another
1308subscriber's writes to wait behind its own. Reverse proxies must pass streaming
1309responses without buffering. History, images, and
1310large extension results remain bounded HTTP reads, not unsolicited stream data.
1311
1312## Extension contracts
1313
1314Extension management stays under `/extensions/{extension_name}`. These routes
1315are not aliases for core commands. The extension owns validation, persistence,
1316permissions, and typed domain results. Enabled routes and action descriptors
1317are discoverable through the server and session catalog.
1318
1319HTTP bearer clients are human operators. Internal model callers use the same
1320domain operations with trusted caller context supplied by the runtime.
1321A JSON `role`, `caller`, or session ID cannot grant human permission.
1322Core shutdown and global credential management cannot be delegated through a
1323page descriptor. Disabling a service returns `404` for its HTTP routes.
1324
1325Extension lists use common pagination and may include optional `page` and
1326`glance` render hints. Each editable row includes `resource`, containing URL,
1327value, and its observed `etag`. Work, paperclip, and schedule item reads are
1328canonical item representations. Hook and linked-group reads include runtime
1329observations and expose a separate `view=configuration` resource for edits,
1330using the session configuration convention. Its validator covers only the
1331complete configuration representation, not changing runtime observations.
1332
1333Mutation responses contain `resource` and `notification`. Notification has
1334`state: "not_requested"|"queued"|"failed"`, with nullable safe `code` and
1335`detail`. Multiple targets use `notifications`, one result per target.
1336Creates return `201` with `Location`; edits return `200`. Deletes return `200`
1337when reporting a deleted resource or notification outcome, otherwise `204`.
1338A committed ledger edit remains successful if notifying an agent fails.
1339Secret-generating POSTs have no automatic retry after response loss; the caller
1340reads the resource and explicitly rotates if the secret was lost.
1341
1342### Work
1343
1344| Path | Methods |
1345| --- | --- |
1346| `/extensions/work/items` | GET, POST |
1347| `/extensions/work/items/{item_id}` | GET, PATCH, DELETE |
1348
1349Every Work read and mutation requires a `workspace` query selecting the invoking
1350workspace. GET collection reads its linked group. POST accepts
1351`title`, optional `notes`, `parent_id`, `session_id`, and `run_id`.
1352Fields include those values, `status`, `revision`, and timestamps. Status is
1353`open`, `active`, `blocked`, `done`, or `cancelled`. A run requires a session.
1354PATCH changes title, notes, status, session, or run under `If-Match`; parent
1355and original workspace are immutable. The query supplies the creation workspace;
1356notes default to empty, status defaults to open, and optional IDs default to null.
1357Title has a 4,096-byte limit; notes have a 32 KiB limit. DELETE requires
1358`If-Match` and rejects an item with children.
1359Writes remain scoped to the invoking workspace's linked group.
1360
1361Human mutations can name `notify_session_id` to queue the existing user-change
1362note. Results distinguish ledger commit from note delivery. Models retain
1363their workspace-scoped create, read, update, and delete operations through
1364trusted internal bindings. Work pages preserve linked-workspace labels and
1365pending-work glances.
1366
1367The CLI accepts `/work add <title>`, `/work edit <id> <title>`,
1368`/work status <id> <status>`, and `/work remove <id>` alongside page forms.
1369These shortcuts invoke the page's declared HTTP actions. Removal asks for
1370confirmation. Targeted shortcuts fetch the item even when it is outside the
1371first list page and retain its ETag through confirmation.
1372
1373### Paperclips
1374
1375| Path | Methods |
1376| --- | --- |
1377| `/extensions/paperclips/items` | GET, POST |
1378| `/extensions/paperclips/items/{item_id}` | GET, PATCH, DELETE |
1379
1380This ledger is global. Items contain ID, topic, title, message, suggestion,
1381reply, status, origin session and workspace, creation time, resolution, and
1382resolving session. Topic is `harness`, `workflow`, `bug`, `user`, or `other`.
1383POST accepts topic, message, optional title and suggestion, and the filing
1384session and workspace. Topic defaults to other; optional text defaults to empty.
1385Message is nonblank and at most 32 KiB; title is at most 4 KiB; suggestion,
1386reply, and resolution are at most 16 KiB each. Topic, title, message, suggestion,
1387and origin are immutable after creation. Status, timestamps, and resolving
1388session are response-only at creation.
1389
1390Humans PATCH status to `acknowledged`, `resolved`, or `dismissed`, or supply
1391`reply`. A reply acknowledges the item and attempts a note to its filing session.
1392The reply stays saved when notification fails. DELETE removes the item.
1393PATCH with reply cannot also set resolved or dismissed. Human resolution can
1394include a resolution note; it never changes the original message. A model's
1395resolving session comes from trusted runtime context. DELETE requires `If-Match`.
1396Models can file, list, and resolve still-open or acknowledged items with a
1397nonempty resolution. Human terminal decisions cannot be overwritten by a model.
1398All edits use `If-Match`. Listings use newest-first cursor order and global glances.
1399
1400The CLI accepts `/paperclips acknowledge <id>`, `/paperclips reply <id> <text>`,
1401`/paperclips resolve <id> [note]`, `/paperclips dismiss <id>`, and
1402`/paperclips remove <id>` through the same declared actions as page forms.
1403Removal asks for confirmation; a stale ETag returns a conflict without retrying
1404the mutation.
1405
1406### Web search
1407
1408| Path | Methods |
1409| --- | --- |
1410| `/extensions/web-search/providers` | GET |
1411| `/extensions/web-search/providers/{name}` | POST |
1412
1413The order in which the model's web search tries providers. It is global and
1414lives in `extensions.json`. Items carry name, label, 1-based position, and
1415whether the provider is tried. POST takes `{"change": "up" | "down" |
1416"toggle"}`; a move past either end leaves the order unchanged. An unknown
1417name is 404.
1418
1419### Schedule
1420
1421| Path | Methods |
1422| --- | --- |
1423| `/extensions/schedule/jobs` | GET, POST |
1424| `/extensions/schedule/jobs/{job_id}` | GET, PATCH, DELETE |
1425
1426GET requires `session_id`. POST accepts session ID, kind, prompt, and
1427`delay_seconds`, with `every_seconds` required for recurring or heartbeat jobs.
1428Kinds are `once`, `recurring`, and `heartbeat`. Prompt has a 4,096-byte limit;
1429intervals range from 60 to 31,536,000 seconds. Resources include `next_at`.
1430PATCH can change `prompt`, `kind`, `delay_seconds`, and `every_seconds` under
1431`If-Match`, preserving once-to-recurring edits. Once jobs prohibit repetition;
1432recurring and heartbeat jobs require it. A prompt-only edit preserves `next_at`.
1433A changed delay resolves from commit time. DELETE requires `If-Match`.
1434Models retain these operations.
1435
1436Once jobs disappear after durable delivery. Recurring prompts queue behind
1437running work. Heartbeats skip busy sessions. Restart resumes the schedule
1438without replaying every missed interval. Session deletion removes its jobs.
1439
1440### Linked workspaces
1441
1442| Path | Methods |
1443| --- | --- |
1444| `/extensions/links/groups` | GET, POST, DELETE |
1445
1446GET requires `workspace` and returns its complete group with bounded remote
1447presence facts, paged where necessary. Configuration view contains membership
1448and revision without remote presence facts. POST uses that observed configuration
1449URL and its `If-Match`, with `other_workspace` and `other_etag` from the other
1450group's configuration read. It checks both groups and merges them in one owner
1451commit. Stale membership on either side returns `412`.
1452DELETE uses the configuration URL plus `member` and checks that member belongs
1453to the observed group at commit. The member becomes its own group. Neither operation deletes work
1454or memory. Writes are human-only. Notifications to affected open sessions
1455are reported separately from the committed group change. `notification_count`
1456covers all targets; the response includes up to 200 outcomes and `truncated`
1457when the list reaches its count or byte bound.
1458
1459### Webhooks
1460
1461| Path | Methods | Authentication |
1462| --- | --- | --- |
1463| `/extensions/webhooks/hooks` | GET, POST | Daemon bearer |
1464| `/extensions/webhooks/hooks/{hook_id}` | GET, PATCH, DELETE | Daemon bearer |
1465| `/extensions/webhooks/hooks/{hook_id}/secret` | POST | Daemon bearer |
1466| `/extensions/webhooks/hooks/{hook_id}/deliveries` | GET | Daemon bearer |
1467| `/extensions/webhooks/hooks/{hook_id}/deliveries` | POST | Signature over raw body |
1468| `/extensions/webhooks/deliveries/{delivery_id}` | GET | Daemon bearer |
1469| `/extensions/webhooks/permissions/{session_id}` | GET, PATCH | Daemon bearer |
1470
1471Hooks contain ID, name, target session, enabled state, signature header and
1472prefix, revision, public delivery URL, pending delivery count, and latest
1473deferral reason. Names use 1 to 64 ASCII letters, digits, `_`, or `-`.
1474POST accepts `session_id`, `name`, optional `enabled`, `signature_header`,
1475`signature_prefix`, and `secret` of 16 to 4,096 bytes. Name is unique within its
1476target session. Enabled defaults true. Header uses 1 to 64 ASCII letters, digits,
1477or hyphens; prefix has at most 32 bytes and no CR or LF. Absence generates a secret.
1478PATCH configuration accepts name, enabled, header, and prefix. Target session
1479is immutable. ID, revision, URL, pending counts, and diagnostics are read-only.
1480Secret rotation POST accepts only an optional secret. Creation and rotation return that
1481secret only in the operation response. Rotation and PATCH require `If-Match`.
1482Stored secrets never appear in lists, descriptors, ordinary logs, or SSE.
1483
1484Permission reads return false when no setting exists. PATCH accepts explicit
1485`agent_manage`, with omission retaining its value. Only humans grant it.
1486Agent management remains restricted to that agent's target session. Turning
1487management off does not disable hooks or prevent reading that session's own
1488accepted delivery payloads. Hook edits use the caller's observed revision,
1489not a revision fetched just before overwriting a stale screen.
1490
1491Incoming delivery POST has a 65,536-byte raw-body limit. It requires the
1492configured HMAC-SHA256 signature and never accepts a bearer token instead.
1493Default header is `X-Albedo-Signature` and prefix is `sha256=`.
1494Missing or invalid signatures return `403`, without a daemon bearer challenge.
1495Optional `X-Albedo-Event-Id`, at most 128 bytes without newlines, deduplicates
1496by hook, ID, and body hash. A changed body under that ID returns `409`.
1497Acceptance stores the delivery and its durable inbox entry together, then
1498returns `202` with `delivery_id`. A full target inbox returns `429`;
1499unknown or disabled hooks return `404`. The pending bound is 1,000 per session.
1500
1501Delivery lists accept `state=pending|delivered|all`, default all, and common
1502pagination. Metadata contains ID, hook ID, target session, receipt and delivery
1503timestamps, attempts, and latest deferral reason. Direct delivery reads add
1504`payload: { "encoding": "utf8"|"base64", "data": ... }`.
1505Both direct and by-hook historical reads remain available after hook deletion.
1506Payloads are external data and carry no instruction authority. Deleting a
1507hook retains accepted deliveries. Deleting the target session removes its
1508hooks and inbox. An operator exposing intake through a reverse proxy allows
1509only the signed POST method, not management GET on the same path.
1510
1511### OpenAI proxy
1512
1513| Path | Methods |
1514| --- | --- |
1515| `/extensions/proxy/v1/models` | GET |
1516| `/extensions/proxy/v1/chat/completions` | POST |
1517
1518This optional service keeps its OpenAI-compatible request and response format,
1519including JSON and streaming responses. It is stateless with respect to agent
1520sessions: the caller supplies history. It retains the 32,000,000-byte request
1521limit and opaque provider state carried in tool IDs. The supported request
1522subset contains model, messages, tools, token limits, temperature, top_p, stop,
1523tool_choice, parallel_tool_calls, reasoning effort, response format, stream,
1524and stream_options.include_usage. User images must be base64 data URLs;
1525remote image URLs are not fetched. Unsupported fields return a proxy-format error.
1526Provider transports apply only options they can express, as described in
1527[the proxy transport contract](../robot-docs/proxy.md#translation).
1528Text and reasoning stream as delta.content and delta.reasoning_content.
1529Complete tool calls publish when the turn ends. Provider failure sends an error
1530chunk before termination, or a JSON error for a nonstreaming request.
1531The proxy's OpenAI envelope, field names, and body limit override platform
1532encoding conventions. It uses daemon bearer
1533authentication. An operator may explicitly enable anonymous loopback access;
1534that policy never grants access to core or other extension routes.
1535
1536## Existing feature coverage
1537
1538This table is the implementation acceptance checklist. UI shortcuts and labels
1539can stay unchanged while their adapter calls use the target contract.
1540
1541| Existing feature | Target operation or data |
1542| --- | --- |
1543| Session picker, search, workspace filter, grouping, recent previews | Filtered session summaries with names, activity, workspace, preview, and shared preferences |
1544| Pinned, archived, and frequent sessions | Session preferences, `sort=frequent`, and identified visits |
1545| New session, checkpoint fork, child spawn | Session creation variants with retry provenance and atomic child task |
1546| Rename, stable family address, leaf or subtree deletion | Session PATCH, separate name and address, conditional deletion result |
1547| Text, images, continue, selected skill, queued steering | Immutable input variants and ordered durable admission |
1548| Pending rows, uncertain sends, uncertain creation across navigation | Same-resource decision lookup and retained original handles |
1549| Targeted cancel, shared turn, whole-turn interrupt | Input cancel and captured session interrupt |
1550| Chat, thinking, code preview, full tool output, file diffs | Session SSE plus durable history content reads |
1551| Late attachment, reconnect, restart, replay eviction | Actor-generation cursor and durable reset snapshot |
1552| Older history and transcript tree | Immutable history pages and checkpoint view |
1553| Footer status, usage, cache fade, context window | Session snapshot, status and usage events, model metadata |
1554| Model picker, manual ID, effort, default profile, raised cap | Session selection, models, and explicit separate settings changes |
1555| `/reload`, `/reload session`, `/reload models` | Typed reload targets with independent confirmed outcomes |
1556| `/compact [strategy]` | Compaction result preserving confirmed strategy choice |
1557| `/kernel`, `/kernel upgrade` | Session kernel metadata and explicit upgrade |
1558| Extensions, dependencies, quarantine, global defaults and inheritance | Fresh catalog, loaded composition, session overrides, shared settings |
1559| Skill and instruction discovery, source diagnostics, selection | Candidate revision checks and explicit resolved-source metadata |
1560| Context inspector and bounded section pages | Prepared snapshot identity and context section reads |
1561| Provider request telemetry | Context request-record view |
1562| MCP forms, credentials, validation and failed-save recovery | Atomic candidate settings patch with write-only secrets |
1563| Provider login, manual callback, choose or remove account | Auth flows, provider-profile selection, account deletion |
1564| `/cd`, folder and repository preview, missing workspace repair | Workspace read and family-aware session move |
1565| SSH host completion, probe, sign-in handoff | Hosts, explicit probe, and actionable local operator instructions |
1566| Agent graph, live tails and rate, attach, spawn, rename, delete, send, mail animation | Session family summaries, atomic activity replacements, normal input admission, typed mail metadata |
1567| Work, paperclips, schedule, linked-workspace pages and glances | Extension resources and declarative typed page actions |
1568| Webhook forms, copy URL, generated secret, rotation, permission toggle | Webhooks domain contract and one-time operation secrets |
1569| Shared thinking and tool display preferences | Settings UI group |
1570| Online storage report and offline diagnosis | Storage API and deliberate local diagnostic path |
1571| Ordinary CLI keep-or-restart offer | Existing launcher policy with instance-targeted shutdown |
1572| Drafts, editor, clipboard, mouse, animations, navigation, quit | Client-owned behavior; no daemon endpoint |
1573
1574Agent-originated mail remains an extension/runtime operation under trusted
1575family permissions. Human graph messages use ordinary user inputs addressed
1576to the chosen session. The API does not let clients forge agent senders.
1577
1578## Architecture and implementation boundaries
1579
1580`server.gleam` owns routing, authentication, HTTP admission, negotiation, and
1581mapping domain errors to wire responses. It does not own session state or
1582execute extension commands inside a session actor.
1583
1584The session actor owns generation, live status, normalized progress, replay
1585capture, and configuration application. The store owner commits identified
1586creation, pending input, consumption, and deletion outcomes on its transaction
1587connection. Family operations serialize membership validation with mutation.
1588The existing bus owns bounded collection notification admission and cleanup.
1589
1590Settings owners validate and persist their desired state. Runtime composition
1591prepares candidates before applying them. Extensions keep their ledgers,
1592permissions, migrations, and handlers. Provider transport and Python RPCs stay
1593internal; their URLs and execution rules are not public client responsibilities.
1594
1595The Go API adapter owns paths, query encoding, request and result types,
1596capability checks, byte bounds, deadlines, retry policy, and SSE validation.
1597Application commands and UI packages receive typed results and UI messages.
1598Local discovery and launching stay outside the connection. Other clients can
1599implement this contract without local storage or execution knowledge.
1600
1601The implementation uses direct domain operations. It adds no forwarding-only
1602wrappers, generic command bus, generation registry, duplicate progress parser,
1603or per-client full-argument buffers. New route aliases require a contract
1604change, not a convenience helper in the UI.
1605
1606## Verification
1607
1608Behavioral tests exercise these outcomes through the real daemon:
1609
1610- Lose creation and input responses, retry original IDs, edit a created session,
1611  and still recover the original intent. Reject changed intent and expired IDs.
1612- Retry a rejected queue-full input after capacity clears. It stays rejected.
1613  Delete its session and retain accepted, cancelled, and rejected decisions.
1614- Race child creation with interruption and deletion. No child lacks its task,
1615  and an old interrupt or deletion attempt cannot affect newer work.
1616- Attach during generation; replay, evict, restart the actor, and restart the
1617  daemon. Committed history survives and old numeric cursors never authorize replay.
1618- Interleave tool calls with escaped Unicode, retry, cancel, and transition to
1619  running. Progress remains bounded and full arguments remain in history.
1620- Stall one reader under sustained events from many agents. Measure queue bytes
1621  and mailbox length; another reader stays responsive, overflow refreshes, and
1622  disconnect releases subscriptions.
1623- Change collection membership while pages load. A removal or moved workspace
1624  reaches the client, stale activity cannot replace newer state, and reconnect
1625  refreshes graph, status, progress, tails, and glances without duplicate rows.
1626  Interleave text, tools, and mail across agents; one collection subscription
1627  preserves live tails and animation identities within the payload bounds.
1628- Page older history, retrieve a large tool result and image, and fork a valid
1629  checkpoint. Full content is recoverable and history navigation survives reset.
1630- Change prepared context between summary and page. The old page fails by
1631  identity rather than displaying another request's section.
1632- Race preference edits and stale catalog choices. Preconditions protect
1633  changes, false differs from omission, and null restores inheritance.
1634- Fail candidate MCP validation and session reload. Previous secrets and
1635  loaded composition remain usable; committed model changes remain visible
1636  when a later default or cap change fails. Reject conflicting credential
1637  sources before attempting a connection or changing saved settings.
1638- Exercise local and remote workspace browsing, missing workspaces, busy moves,
1639  descendant moves, and explicit SSH authentication instructions.
1640- Complete, cancel, and expire provider login flows, including manual callback.
1641  Restart during a flow; the old ID stays failed and cannot start another flow.
1642  Verify secret redaction, origin rejection, bearer failures, and browser CORS.
1643- Exercise work, paperclip reply, scheduling while busy, linked-group merge,
1644  webhook stale edits, signatures, deduplication, secret loss, and permissions.
1645  Committed domain changes survive a failed notification.
1646- Read storage from a client without daemon filesystem or Python access and
1647  compare known isolated totals. Attachment never starts or stops a daemon.
1648
1649Controlled transport tests cover malformed wire data, callback failure before
1650cursor commit, partial HTTP responses, and failures a healthy daemon cannot
1651produce. Cross-cutting parser tests cover fragmented SSE lines and UTF-8 bounds.
1652There are no compatibility fixtures, UI copy snapshots, route-constant tests,
1653or tests that only encode and decode the same local type.
1654
1655An independent consumer authenticates, creates a session, submits identified
1656input, watches SSE, resumes after disconnect, and reads history using only this
1657document. Its success verifies that Go-specific knowledge is not required.
1658
1659Run focused E2Es and Go race tests for changed behavior, then `./test.sh` for
1660the complete gate. Review resource ownership, uncertain outcomes, bounded
1661queues, cleanup, and test failure modes when changing the API. Performance
1662experiments and their results belong outside the repository.
1663
1664Server and clients use the same contract. Durable history and supported storage
1665migrations remain supported; old API wire formats do not.