How a client bootstraps a session list in one call, the common gateway event families, and the node helper and exec lifecycle contracts.

Session list bootstrap

Call sessions.subscribe with a non-empty sessions.list parameter object, such as { limit: 60, ownerFirst: true }, to subscribe and load the initial roster in one request. A successful WebSocket response has the payload { subscribed: true, list }, where list is the normal SessionsListResult. Calling with {} preserves the acknowledgment-only response { subscribed: true } and does not read a snapshot. List parameters select the snapshot; they do not filter the connection's session event subscription.

List views can pass rowMode: "compact" to omit repeated detail-only metadata: contextWindows, contextWindowDefault, thinkingLevels, thinkingOptions, thinkingDefault, toolOverrides, providerReview, nativeRuntimeConsent, contextBudgetStatus, agentRuntime, and pluginExtensions. Each returned row carries rowMode: "compact"; those omissions mean the detail was not requested, and must not clear previously admitted detail fields. Use sessions.describe or the full chat.history.sessionInfo for details. Omitting rowMode preserves the full row contract. Title, preview, and Activity recap enrichment remain controlled by their existing include flags.

Control UI list requests use compact rows and a bounded diagnostic source: sidebar, dashboard, activity, sessions-page, chat-pane, agent-roster, command-palette, or skill-workshop. The source does not change selection or access. List and event rows include hasBoard, the prepared dashboard-membership fact, so a dashboard gallery can apply row updates without enumerating its pages again. Expanding a compact row on the Sessions page reads its full details with sessions.describe. Rows also carry childOwnerSessionKeys, the current retained owners used by the Gateway's spawnedBy filter. These receipts include runtime-controller and navigation ownership, expire through that same owner, and let child windows apply events without reimplementing retention rules in the browser.

The Gateway registers the subscription before projecting the list. Clients must listen for sessions.changed before making the request: events can arrive while the snapshot is being built. Reconcile those events with the response and issue a trailing sessions.list refresh when needed, including when an event only invalidates the cached list. Reconnects require a new subscription and snapshot.

The Gateway keeps durable session metadata in memory and finishes its initial row materialization before normal startup completes. Reconnecting clients can read the initial roster as soon as the Gateway is ready. Committed owner changes refresh affected rows incrementally; responses consume current row facts without a staleness window. Keyed descriptions, resolution, and chat startup prepare their requested row without waiting for the bulk refresh. Newly admitted or replaced stores load their metadata once, and rows disappear when their store leaves the current topology. Each response applies the current viewer's visibility and current activity time. Equivalent viewers share immutable row presentations and encoded row bytes until their projection facts change; snapshotAt retains the row's sampling time. Runtime authority, permission changes, and clock-expiring facts are checked before reuse. WebSocket views of the same identity, sharing policy, and query also share selection and facets within the current row publication. Queries that depend on live runs, a clock window, child retention, or search select again for each read. Every read presents current rows; unchanged rows share their array and assembled JSON bytes. In-process callers retain their own selection and row wrappers for authorized enrichment.

Resident rows use stored titles and usage. Optional message previews and terminal fallback-model metadata fill in through bounded read-only background transcript reads; they can be absent from an early response. Foreground requests take priority. These reads do not restore cold archives, parse oversized messages, call a model, or change stored metadata or session activity ordering. Missing historical titles and legacy ACP keys are repaired only by apus doctor --fix. Missing usage remains absent until the normal usage writer records it.

Both methods accept activeOnly: true to select currently running or queued sessions before pagination. Activity comes from the live runtime owners, not a stored status flag. Ordinary listing behavior is unchanged when the option is omitted or false. Active-only results include each visible agent-owned global and unknown session with its raw key and captured agentId; callers identify rows by agent, key, and sessionId together. Literal agent:<id>:global and agent:<id>:unknown sessions remain different rows. Active-only raw sentinel rows omit the optional childSessions and hasActiveSubagentRun fields; use hasActiveRun for direct activity. Normal permissions, archive/inclusion filters, and page limits still apply. Sessionless/internal runs are outside the session index.

Both methods accept ownerFirst: true to prepend up to 60 matching viewer-owned rows (or limit, when smaller) to the normal first page, deduplicated by session key. This applies only when offset is zero or omitted; later pages use normal pagination. Owned rows must pass the same visibility and list filters as the shared page. The Gateway resolves the viewer from the authenticated connection; no client-supplied identity selects these rows. Without an authenticated viewer identity, or when ownerFirst is false or omitted, the list uses normal ordering.

The shared page still determines limitApplied, offset, nextOffset, hasMore, and totalCount. Prepended rows can make sessions.length and count exceed the shared page size. Use nextOffset to advance and deduplicate rows by session key across pages; do not derive the next offset from the displayed row count.

Session message subscriptions and narration

sessions.messages.subscribe subscribes one connection to a session's live messages. Its key and optional agentId select the session; this is separate from the broad roster subscription above. Omit mode for full chat and agent streams, including foreground transcripts and passive views of runs started by another client. Repeating a request replaces that observer's subscription mode. sessions.messages.unsubscribe removes the observer identified by the same optional subscriptionId; omission selects the legacy observer. The subscribe acknowledgment includes the canonical key and resolved agentId. Clients retain the resolved owner for later global requests, whose key alone does not identify an agent. The SDK sends a stable opaque subscriptionId for each wire observer and includes it in resubscriptions and unsubscribe requests. For multiple IDs on one connection and session, full-stream interest takes precedence until its last owner releases; approvals remain enabled while any owner requests them. Omitting the ID retains the legacy single-observer behavior. Older clients remain compatible with the updated Gateway; the updated SDK's ownership fields require an updated Gateway.

Background narration consumers declare mode: "narration". The Gateway replaces their token-level chat deltas and raw agent assistant events with session.narration snapshots. Each snapshot contains sessionKey, optional agentId, runId, and text: at most 16,384 characters of the current visible assistant tail. Hidden reasoning and internal context are removed before the tail is bounded. An empty text retracts the previous narration. Consumers can derive a compact line from this text without reconstructing token deltas.

The first text update can arrive immediately. Subsequent snapshots arrive at most once every two seconds per session per connection, using the latest text without postponing the pending deadline. Terminal chat events flush the last snapshot immediately before the terminal event, including final text corrections or retractions; this final flush is exempt from the two-second interval. Newer tool activity or a change of run discards pending older text, so a delayed snapshot cannot replace a newer tool line or switch the sidebar back to an older run. Lifecycle, status, tool, final, abort, and error events retain their existing delivery. Raw thinking streams and in-progress preamble or answer-candidate text are omitted; item completion and answer selection still arrive. Approval events still require includeApprovals: true and the normal approval authority. Queued narration is discarded on unsubscribe, mode changes, connection retirement, or run retirement, and delivery rechecks current access.

The Gateway client SDK shares matching session addresses among local owners. The Gateway also combines independently identified observers that resolve to the same subscription key. If any owner requires full streams, delivery remains full; it returns to narration only after the last full owner releases it. Narration consumers sharing a foreground subscription must also accept full stream events. The Control UI waits for foreground admission before fetching history, so the snapshot covers activity emitted before full streams were enabled.

The bundled Control UI declares narration intent for sidebar interests. It is version-locked to its Gateway and reloads on upgrade. Shared Apple chat clients use the default full mode for foreground sessions; Android and the TUI retain their broad event delivery. Existing SDK callers and older clients that omit mode retain full streams. Custom UI roots, development UIs, and cross-origin UIs exempt from build admission can therefore retain full-stream narration until updated. This is an additive protocol-v4 contract, with no capability negotiation or protocol-version change.

Common event families

Node helper methods

Nodes may call skills.bins to fetch the current list of skill executables for auto-allow checks.

Node exec lifecycle events

Nodes report system.run lifecycle through the node-role node.event RPC with event: "exec.started", "exec.finished", or "exec.denied". These are not the operator exec.approval.* broadcasts and do not use the retired TCP bridge.

The RPC accepts a JSON string in payloadJSON or an object in payload. A string payloadJSON takes precedence when both are supplied. For example:

{
  "event": "exec.finished",
  "payload": {
    "sessionKey": "agent:main:main",
    "runId": "<exec-run-id>",
    "host": "node",
    "exitCode": 0,
    "timedOut": false,
    "success": true,
    "output": "done"
  }
}

Current headless nodes include sessionKey, runId, and host: "node". Additional fields are:

Field Meaning
command Raw or formatted command text.
exitCode, timedOut Process completion code and timeout flag.
success Producer result flag, not the notification-gating predicate.
output Bounded combined stdout, stderr, and error text.
reason Denial reason for exec.denied.
suppressNotifyOnExit Suppress this invocation's system notification.

Echo the correlation fields forwarded with system.run; neither an ID nor the payload's host field grants authority. The Gateway matches the authenticated node and connection, run ID, and session key when the invocation binds one. Unmatched events return handled: false with reason: "unmatched_exec_event" and produce no system notification. A narrow legacy macOS-client path may match a missing or mismatched run ID only to one unambiguous invocation on that connection/session; new clients must send the issued run ID.

exec.started retains the authorization record; exec.finished and exec.denied consume it before notification filtering. tools.exec.notifyOnExit: false or suppressNotifyOnExit: true suppresses notifications. Denied events never enqueue a system event or wake agent work. Finished events notify only for timeout, nonzero or unknown exit code, or nonempty compacted output; successful exit 0 with no output stays quiet. Finished notifications with a run ID are deduplicated by canonical session and run ID. A heartbeat wake is requested only after a system event is queued.

Node event delivery is best-effort, not a durable completion ledger.