okami.mom/albedo
git clone https://git.nkp.pet/okami.mom/albedo
711d48a
main
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.
| Path | Methods | Responsibility |
|---|---|---|
/server | GET | Readiness, protocol, capabilities, build, quota, notices |
/server/shutdown | POST | Explicitly stop the identified daemon instance |
/settings | GET, PATCH | Shared defaults, provider profiles, MCP, secrets, UI preferences |
/storage | GET | Online storage accounting |
/sessions | GET | Filtered session summaries, or collection invalidations over SSE |
/sessions/{session_id} | PUT, GET, PATCH, DELETE | Create, attach, configure, delete, or watch a session |
/sessions/{session_id}/visits/{visit_id} | PUT | Count one deliberate opening without counting retries |
/sessions/{session_id}/inputs/{input_id} | PUT, GET | Admit immutable user input and read its durable outcome |
/sessions/{session_id}/inputs/{input_id}/cancel | POST | Cancel that input or request interruption of its exclusive turn |
/sessions/{session_id}/interrupt | POST | Interrupt a captured turn and cancel captured waiting inputs |
/sessions/{session_id}/history | GET | Bounded transcript pages or checkpoint summaries |
/sessions/{session_id}/history/{entry_id} | GET | Read full content of a large transcript entry in bounded pieces |
/sessions/{session_id}/active-output/{content_id} | GET | Read an unfinished output prefix fixed by a captured snapshot |
/sessions/{session_id}/context | GET | Prepared request inspection and provider request records |
/sessions/{session_id}/catalog | GET | Fresh discovery and separately identified loaded composition |
/sessions/{session_id}/reload | POST | Reload session composition, model catalogs, or both |
/sessions/{session_id}/kernel/upgrade | POST | Explicitly replace this session's Python kernel |
/sessions/{session_id}/compaction | POST | Compact the model-facing conversation |
/models | GET | Provider model metadata, effort choices, context limits, cache policy |
/workspaces | GET | Recent workspaces or one directory with optional repository preview |
/hosts | GET | Known hosts or one host's cached probe state |
/hosts/{host}/probe | POST | Explicitly refresh a host probe |
/auth | GET | Provider login methods and redacted account metadata |
/auth/logins/{login_id} | PUT, GET, PATCH, DELETE | Start, inspect, answer, or cancel a provider login |
/auth/accounts/{account_id} | DELETE | Remove 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
- Use the supplied base URL and bearer token to read
/server. Check protocol and required capabilities before opening a screen. - Read
/sessionsfor the picker or family graph. Each row includes its metadata and bounded live state. - 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.
- Watch that same session URL as SSE, passing the installed cursor pair. Apply subsequent events, or replace the daemon-derived view on reset.
- 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.
- 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.
| Status | Meaning |
|---|---|
| 200 | A read or synchronous operation returned its documented result |
| 201 | A resource was created; Location identifies it |
| 202 | Input is durably accepted, or an explicitly identified action is underway |
| 204 | A deletion or cancellation needs no response body |
| 400 | Invalid syntax, fields, query, or identity timestamp |
| 401 | Missing or invalid daemon token; includes WWW-Authenticate: Bearer |
| 403 | Disallowed browser origin or caller permission |
| 404 | Unknown current resource or disabled extension route |
| 405 | Known path, unsupported method; includes Allow |
| 406 | Unsupported response media type |
| 409 | Busy state, identity conflict, stale catalog, or incompatible operation |
| 410 | Retained deletion, expired identity, or obsolete prepared context |
| 412 | The observed representation or creation precondition no longer holds |
| 413 | Body exceeds its limit |
| 415 | Unsupported request media type |
| 428 | Required precondition is missing |
| 429 | Admission or rate limit; includes Retry-After where a later new intent can succeed |
| 503 | Owner, 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.
| Group | Fields and meaning |
|---|---|
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. |
mcp | definitions keyed by name: configured enablement, transport, URL or command with arguments, nonsecret environment and headers, timeouts, and secret-presence metadata. |
extensions | Global default enablement keyed by extension name. |
capabilities | Global skill, instruction, and MCP preferences keyed by their stable preference key. |
models | Per-model raised context choices and the cache TTL prior table, with source and units. |
ui | thinking, 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.revisionandfamily_revisionfor conditional edits and family operations.configuration_resource, containing its canonical URL, value, andetag.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, andinput_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_msis the span of the provider call that produced the usage, andtokens_per_secondis 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:
kind | Fields | Behavior |
|---|---|---|
new | workspace; optional name, provider_profile, model, effort | Create an independent session. |
fork | source_session_id, checkpoint_id; optional name | Copy the durable prefix and selection overrides into an independent session. |
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. |
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:
kind | Fields |
|---|---|
message | text; optional images, up to ten, each with mime_type and base64 data; optional pastes, up to twenty texts |
continue | No content fields |
skill | candidate_id, catalog_revision, and arguments as text |
command | command_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:
- Validate the complete batch, generation, sequence, and known event fields.
- Require a leading reset on first attachment or a generation change.
- 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.
- Deliver all events in order. Same-generation replay cannot regress the sequence without a reset.
- 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.
| Type | Body and behavior |
|---|---|
reset | Reason initial, generation_changed, replay_unavailable, or recovery; requires the complete batch snapshot. |
status | Current phase, run ID, interrupt state, and blocking reason. |
input | input, a bounded Input outcome update, including membership and delivery changes. |
text | Run ID, message ID, and appended assistant text. |
thinking | Run ID, message ID, and appended reasoning text. |
message | Completed display message with stable history entry identity. |
tool_progress | progress, a normalized progress object, or null to clear all live progress. |
tool | Completed call identity, progress identity, bounded arguments, result, trace, and full-content reference if needed. |
note | Entry identity, origin, display text, and related family mail IDs where present. |
retry | Run, provider attempt, safe reason, and retry delay in milliseconds; clears obsolete attempt progress. |
usage | Typed token, timing, model-window, and cache observations. |
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. |
turn_completed | Run ID, terminal outcome, and input membership. |
compacted | Strategy and observation, without replacing durable history. |
error | Safe error code and message for the turn; not transport authentication. |
invalidate | Resource kind and relative URL whose metadata changed. |
failure | Stream 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
| Path | Methods |
|---|---|
/extensions/work/items | GET, 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
| Path | Methods |
|---|---|
/extensions/paperclips/items | GET, 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
| Path | Methods |
|---|---|
/extensions/web-search/providers | GET |
/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
| Path | Methods |
|---|---|
/extensions/schedule/jobs | GET, 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
| Path | Methods |
|---|---|
/extensions/links/groups | GET, 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
| Path | Methods | Authentication |
|---|---|---|
/extensions/webhooks/hooks | GET, POST | Daemon bearer |
/extensions/webhooks/hooks/{hook_id} | GET, PATCH, DELETE | Daemon bearer |
/extensions/webhooks/hooks/{hook_id}/secret | POST | Daemon bearer |
/extensions/webhooks/hooks/{hook_id}/deliveries | GET | Daemon bearer |
/extensions/webhooks/hooks/{hook_id}/deliveries | POST | Signature over raw body |
/extensions/webhooks/deliveries/{delivery_id} | GET | Daemon bearer |
/extensions/webhooks/permissions/{session_id} | GET, PATCH | Daemon 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
| Path | Methods |
|---|---|
/extensions/proxy/v1/models | GET |
/extensions/proxy/v1/chat/completions | POST |
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 feature | Target operation or data |
|---|---|
| Session picker, search, workspace filter, grouping, recent previews | Filtered session summaries with names, activity, workspace, preview, and shared preferences |
| Pinned, archived, and frequent sessions | Session preferences, sort=frequent, and identified visits |
| New session, checkpoint fork, child spawn | Session creation variants with retry provenance and atomic child task |
| Rename, stable family address, leaf or subtree deletion | Session PATCH, separate name and address, conditional deletion result |
| Text, images, continue, selected skill, queued steering | Immutable input variants and ordered durable admission |
| Pending rows, uncertain sends, uncertain creation across navigation | Same-resource decision lookup and retained original handles |
| Targeted cancel, shared turn, whole-turn interrupt | Input cancel and captured session interrupt |
| Chat, thinking, code preview, full tool output, file diffs | Session SSE plus durable history content reads |
| Late attachment, reconnect, restart, replay eviction | Actor-generation cursor and durable reset snapshot |
| Older history and transcript tree | Immutable history pages and checkpoint view |
| Footer status, usage, cache fade, context window | Session snapshot, status and usage events, model metadata |
| Model picker, manual ID, effort, default profile, raised cap | Session selection, models, and explicit separate settings changes |
/reload, /reload session, /reload models | Typed reload targets with independent confirmed outcomes |
/compact [strategy] | Compaction result preserving confirmed strategy choice |
/kernel, /kernel upgrade | Session kernel metadata and explicit upgrade |
| Extensions, dependencies, quarantine, global defaults and inheritance | Fresh catalog, loaded composition, session overrides, shared settings |
| Skill and instruction discovery, source diagnostics, selection | Candidate revision checks and explicit resolved-source metadata |
| Context inspector and bounded section pages | Prepared snapshot identity and context section reads |
| Provider request telemetry | Context request-record view |
| MCP forms, credentials, validation and failed-save recovery | Atomic candidate settings patch with write-only secrets |
| Provider login, manual callback, choose or remove account | Auth flows, provider-profile selection, account deletion |
/cd, folder and repository preview, missing workspace repair | Workspace read and family-aware session move |
| SSH host completion, probe, sign-in handoff | Hosts, explicit probe, and actionable local operator instructions |
| 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 |
| Work, paperclips, schedule, linked-workspace pages and glances | Extension resources and declarative typed page actions |
| Webhook forms, copy URL, generated secret, rotation, permission toggle | Webhooks domain contract and one-time operation secrets |
| Shared thinking and tool display preferences | Settings UI group |
| Online storage report and offline diagnosis | Storage API and deliberate local diagnostic path |
| Ordinary CLI keep-or-restart offer | Existing launcher policy with instance-targeted shutdown |
| Drafts, editor, clipboard, mouse, animations, navigation, quit | Client-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 34includes 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 38actor lifetime and position. Transcript checkpoints identify durable history. 39 These identities are separate. 40 - Core operations have named, typed contracts. Extensions own separate domain 41routes. A command menu entry points to its canonical operation. 42 - The daemon owns execution state, argument parsing, storage inspection, and 43progress. Clients own navigation, rendering, local drafts, and confirmations. 44 - Bounds apply before subscriber mailboxes. Model execution does not wait for 45a client to read its socket. 46 47 SSE 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 115and required capabilities before opening a screen. 1162. Read `/sessions` for the picker or family graph. Each row includes its 117metadata and bounded live state. 1183. Read the selected session. Install its history, pending inputs, status, 119progress, and cursor together. Count a deliberate opening with one identified 120visit; background reads do not count. 1214. Watch that same session URL as SSE, passing the installed cursor pair. 122Apply subsequent events, or replace the daemon-derived view on reset. 1235. Send a message with an identified input PUT. Keep its local handle until 124the returned decision or matching stream event resolves it. Cancellation 125targets that input identity. 1266. Fetch older history by durable position. Open context, catalog, or extension 127resources when the user needs them. Use the validator already displayed 128with 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`, 534dependencies, quarantine diagnostics, and capability availability. 535- `kernel`, containing state, build identity, staleness reasons, live-job 536count, and up to 100 running-job summaries ordered by ID. Each summary names 537the job, its observed PID (nullable), and its command (at most 4096 scalars). 538Unknown counts and lists are null; known empty lists are `[]`. The count can 539exceed the bounded list. Reads observe the kernel without preparing it. 540No Python variables or executable process handles leave the daemon. 541- `pending_inputs`, at most 32 bounded input summaries with acceptance order, 542and `input_order`, the greatest order ever accepted into this session. 543- `usage`, including token and cache counters, elapsed time, model limits, 544observed cache TTL, and time-indexed cache fade observations. `elapsed_ms` is 545the span of the provider call that produced the usage, and `tokens_per_second` 546is 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. 1124Stage full active-output hydration before installing the replacement. 1125Restore history and the captured prefixes before delivering later deltas. 1126Clear partial arguments and transient text from the previous stream. 11274. Deliver all events in order. Same-generation replay cannot regress the 1128sequence 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, 1611and still recover the original intent. Reject changed intent and expired IDs. 1612- Retry a rejected queue-full input after capacity clears. It stays rejected. 1613Delete its session and retain accepted, cancelled, and rejected decisions. 1614- Race child creation with interruption and deletion. No child lacks its task, 1615and an old interrupt or deletion attempt cannot affect newer work. 1616- Attach during generation; replay, evict, restart the actor, and restart the 1617daemon. Committed history survives and old numeric cursors never authorize replay. 1618- Interleave tool calls with escaped Unicode, retry, cancel, and transition to 1619running. Progress remains bounded and full arguments remain in history. 1620- Stall one reader under sustained events from many agents. Measure queue bytes 1621and mailbox length; another reader stays responsive, overflow refreshes, and 1622disconnect releases subscriptions. 1623- Change collection membership while pages load. A removal or moved workspace 1624reaches the client, stale activity cannot replace newer state, and reconnect 1625refreshes graph, status, progress, tails, and glances without duplicate rows. 1626Interleave text, tools, and mail across agents; one collection subscription 1627preserves live tails and animation identities within the payload bounds. 1628- Page older history, retrieve a large tool result and image, and fork a valid 1629checkpoint. Full content is recoverable and history navigation survives reset. 1630- Change prepared context between summary and page. The old page fails by 1631identity rather than displaying another request's section. 1632- Race preference edits and stale catalog choices. Preconditions protect 1633changes, false differs from omission, and null restores inheritance. 1634- Fail candidate MCP validation and session reload. Previous secrets and 1635loaded composition remain usable; committed model changes remain visible 1636when a later default or cap change fails. Reject conflicting credential 1637sources before attempting a connection or changing saved settings. 1638- Exercise local and remote workspace browsing, missing workspaces, busy moves, 1639descendant moves, and explicit SSH authentication instructions. 1640- Complete, cancel, and expire provider login flows, including manual callback. 1641Restart during a flow; the old ID stays failed and cannot start another flow. 1642Verify secret redaction, origin rejection, bearer failures, and browser CORS. 1643- Exercise work, paperclip reply, scheduling while busy, linked-group merge, 1644webhook stale edits, signatures, deduplication, secret loss, and permissions. 1645Committed domain changes survive a failed notification. 1646- Read storage from a client without daemon filesystem or Python access and 1647compare 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.