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}/history/content |
GET | Read several entries' full content in one walk |
/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}/context-window |
GET | The window this session compacts against, its source, and a sentence to show |
/sessions/{session_id}/catalog |
GET | Fresh discovery and separately identified loaded composition |
/sessions/{session_id}/files |
GET | Read one UTF-8 text file in the session's workspace, on its host |
/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 |
/sessions/{session_id}/rewind |
POST | Cut an idle session back to before a user message, archiving the removed tail |
/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. Frames use a 64 KiB
window (windowLog 16 in albedo_http_api), not level 1's 512 KiB default:
the window is the memory a client's decoder must hold, and an event stream
keeps one decoder for the client's whole life.
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, file_reading, host_probes, provider_auth,
history_rewind, 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. Kernels keep running for it unless stop_kernels is
true; then sessions save their variables and their kernels end. 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. |
extension_settings |
sections: the daemon's own sections, then each installed extension's that declares settings, with its name in extensions.json, a label saying what it configures, its category (web_search, context, agents, models_and_caching or tools, which /config groups sections by), its declared fields (key, label, description, kind integer, text or limit, default, range) and saved values, and the problem when its owner cannot use its saved section. A limit takes -1 for no limit. A patch is values keyed by section then key; null restores the default. |
capabilities |
Global skill, instruction, and MCP preferences keyed by their stable preference key. |
models |
Per-model raised context choices and saved compact-at windows, and the cache TTL prior table, with source and units. |
ui |
thinking, tools, dismissed_notices, and web_allow (the networks albedo web serves). 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. models.compact_at
maps the same keys to a token count (1 to 100,000,000) every session on that
model compacts against, or null to remove it. It only lowers: a count above the
model's window is kept but changes nothing. 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, models.compact_at, or ui.thinking, ui.tools, ui.dismissed_notices, and ui.web_allow.
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, extension_settings, 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. A session token resumes after the last
session ID shown, so sessions written, added or removed between pages never
void it. 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, role, started_by, 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.
POST /sessions/{session_id}/rewind is the in-place counterpart. It takes a
checkpoint_id of a user message and an optional summary request. The daemon
copies the whole session into a new archived session first, then deletes every
row from the checkpoint on, and recomputes what derives from them (preview,
tool-call index, captured traces, LCM nodes, image references). The session
must be idle; its actor stops, so attached streams reconnect and reset.
input returns the removed message's text for the composer. With summary,
the daemon summarises the removed tail before it changes anything and adds the
result after the cut as a note from origin rewind; a failed summary leaves the
session unchanged. The Python namespace and files on disk are not rewound.
Requires capability history_rewind: 1.
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, and revision, with a strong ETag.
Family membership is not configuration: the family revision appears only on the
session representation (and its summary), so a child spawning, closing, or being
deleted never makes a client's observed configuration stale. A workspace move and
a subtree deletion send the family revision that read observed alongside the
configuration 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. A name
the model chose with /rename is that automatic title, kept apart from
configuration: it never changes the configuration validator, and the person's
own name outranks it.
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 session 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.
GET /sessions/{session_id}/history/content?entries=a,b,c reads those parts for
up to 200 entries in one walk, in the order entries names them, so a page
whose entries are mostly references costs one request per byte budget instead
of one per entry. Each part carries the entry_id it belongs to, because a
page can stop in the middle of an entry; offsets, the per-piece and per-page
budgets, and the exclusion of image fields are the single-entry route's. A
next continuation is bound to that request's path and its exact set of
entries, so it cannot be replayed against another set. Entry IDs are at most
128 characters. Clients that need one entry's content keep using the
single-entry route.
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, saved compact-at window, 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 /sessions/{session_id}/files?path= reads one file for a client to show,
on the machine the session's workspace is on, local or over the same SSH
gather as browsing. path is absolute there, relative to the workspace,
~/... in a local workspace, or the host-prefixed spelling the answer uses.
The answer carries the canonical path (host-prefixed when remote), size
in bytes, modified_at, and text. Only regular UTF-8 files without NUL are
read: over 1 MiB, or too large to answer within the response limit, is 413
file_too_large; binary or invalid UTF-8 is 415 file_not_text; missing, a
directory, or not a regular file is 404 file_not_found. A host that is not
ready is 503 workspace_unavailable, as for browsing.
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.