Skip to content

Chat Channel

A chat channel carries the full state of a single conversation thread: turns, streaming responses, tool calls, pending messages, and input requests. A chat always belongs to a session; a session may contain one or many chats. Chats are independently subscribable so a client can observe a subset of activity without paying the bandwidth cost of every chat in the session.

URI

ahp-chat:/<uuid>

The path is a server-unique identifier (typically a UUID) allocated by the server when the chat is created. The owning session URI is not encoded in the chat URI — the relationship is expressed via the session's chats catalog and each chat's origin.

Multiple chat channels may be active simultaneously. Clients subscribe to each chat whose state they want to track.

State

Subscribers receive a ChatState snapshot. ChatState denormalizes the ChatSummary fields directly onto itself (resource, title, status, activity, modifiedAt, model, agent, origin, workingDirectory) and adds the conversation contents (history of completed turns, the active turn if any, pending messages, outstanding input requests). Producers MUST keep the chat's ChatSummary in the session catalog consistent with these inlined fields — typically by dispatching a matching session/chatUpdated whenever any summary field on the chat changes. Refer to the State Model guide for a structural overview.

Per-chat working directory

ChatState.workingDirectory (and its mirror on ChatSummary) is optional. When absent, the chat inherits the session's workingDirectory. Hosts MAY set a per-chat working directory to give individual chats their own filesystem context — for example, allocating a separate git worktree per chat so multiple chats in the same session can make independent edits that the orchestrating chat later merges back. The session-level workingDirectory is then the default/primary location for chats that do not override it.

Relationship to the session channel

  • A chat's ChatSummary appears in the session's SessionState.chats catalog. The session reducer keeps that catalog in sync with the underlying chat lifecycle.
  • The session may also expose defaultChat as a UI routing hint for input that is addressed to the session as a whole. This is advisory only — chats remain equal peers at the protocol level.
  • Session-level fields such as status, activity, and modifiedAt are aggregates derived from the session's chats. See the Session Channel specification for the derivation rules.

Lifecycle

1. Client subscribes to the owning session URI (ahp-session:/<sid>)
2. Client (or the server, via a tool call or fork) creates a chat with createChat
3. Server allocates a chat URI (ahp-chat:/<cid>) and mutates the session's chats catalog
4. Client subscribes to the chat URI to receive its ChatState snapshot
5. Server streams chat actions over the chat channel until the chat (or its session) is disposed

Creation

createChat is a JSON-RPC request. Callers identify the owning session via the request's channel parameter (ahp-session:/<sid>) and MAY supply:

  • an initialMessage to start the first turn immediately,
  • per-chat agent / model / config overrides that win over the session defaults, and
  • a source of type ChatForkSource to fork from an existing chat at a specific turn.

The server allocates the chat URI and adds the chat to the session's catalog (session/chatAdded on the session channel) before returning.

Origin

Each chat advertises how it came into existence via ChatOrigin:

KindMeaning
userUser created the chat explicitly (e.g. via the host UI).
forkForked from an existing chat at a specific turn — payload references the source chat URI and turn id.
toolSpawned by a tool call running in another chat — payload references the source chat URI and tool call id (e.g. a sub-agent delegation).

Clients MAY use the origin to render contextual UI (parent indicators, fork markers, "spawned by tool" badges), but origin is not a hierarchy — every chat is equally addressable.

A tool-spawned worker is described from both ends of the same edge. The worker chat carries the canonical record via its tool origin (the spawning chat URI and tool call id). The spawning tool call surfaces the same relationship forward through a ToolResultSubagentContent block in its result, whose resource is the worker chat URI (ahp-chat:/<cid>, not a session URI). The tool call that emits that block is the one named by the worker chat's origin.toolCallId; hosts MUST keep the two consistent.

Ancestry and nesting depth

A fork or tool origin names only the chat's immediate source chat (by URI), together with the turn or tool call that produced it. A chat's ancestry is therefore not stored directly; it is the chain you reconstruct by following origin.chat from one chat to the next. Because a tool-spawned chat can itself run tools that spawn further chats, these chains can be arbitrarily deep.

  • No protocol-imposed depth limit. AHP does not cap nesting depth or fan-out, and the wire carries no depth counter or maximum-depth field. Any bound is a host policy decision that the protocol neither enforces nor advertises; hosts SHOULD guard against runaway recursion or unbounded fan-out on their side.
  • Ancestry is advisory and may be incomplete. Every chat is a flat, equally-addressable peer in the session's chats catalog — origin is a rendering hint, not a structural parent link. A source chat MAY be pruned (session/chatRemoved) while a chat it spawned lives on, so an origin.chat URI is not guaranteed to resolve. Clients reconstructing ancestry MUST tolerate missing references and SHOULD guard against cycles and unbounded depth (for example, by capping how deep they walk or render).

Active chat

Once a chat exists and its session is lifecycle: 'ready', the chat accepts turns. The wire shape mirrors the legacy single-chat session shape:

  • The client dispatches chat/turnStarted to begin a turn.
  • The server streams chat/delta, chat/responsePart, chat/toolCallStart, chat/toolCallReady, and related actions.
  • The client dispatches chat/toolCallConfirmed / chat/toolCallResultConfirmed to approve or deny tool calls, or chat/turnCancelled to abort.
  • The server dispatches chat/turnComplete or chat/error when the turn ends.
  • The server MAY dispatch chat/inputRequested while a turn is active. Clients sync answer drafts with chat/inputAnswerChanged and finish the request with chat/inputCompleted.

All actions dispatched on this channel travel on ActionEnvelopes whose channel is the chat URI. Action payloads do NOT carry their own chat URI — the channel comes from the envelope.

Disposal

A chat is implicitly disposed when its owning session is disposed. The protocol does not currently expose a disposeChat command; chats live for the life of their session unless the server prunes them. When a chat is removed (whether explicitly or because its session was torn down), the server MUST update the session's chats catalog via session/chatRemoved so subscribers can release their per-chat subscriptions.

Methods and events on this channel

This section lists wire methods that are interpreted in the context of a chat URI (ahp-chat:/<uuid>).

Commands (params.channel = "ahp-chat:/<uuid>")

MethodKindPurpose
fetchTurnsrequestPage historical turns for this chat.
completionsrequestChat-scoped inline completions (e.g. user-message mentions).

createChat is dispatched against the owning session URI (params.channel = "ahp-session:/<sid>").

Notifications (params.channel = "ahp-chat:/<uuid>")

MethodKindMeaning
actionserver → client notificationChat action envelope (chat/* action payloads).
dispatchActionclient → server notificationDispatch client actions on this chat (chat/turnStarted, chat/toolCallConfirmed, ...).
unsubscribeclient → server notificationStop receiving messages for this chat channel.

Server Validation of Client Actions

When the server receives a client-dispatched action on this channel, it MUST validate it before applying. Invalid actions MUST be echoed back with a rejectionReason on the ActionEnvelope. The validation rules mirror the legacy session validation table — substitute chat/* for session/*:

ActionConditionServer Behavior
Any action referencing a non-existent chatChannel URI not foundServer MUST silently ignore the action (no echo)
chat/toolCallConfirmedTool call not in pending-confirmation stateServer MUST reject the action
chat/turnCancelledNo active turnServer MUST reject the action
chat/inputAnswerChangedNo input request with matching requestIdServer SHOULD reject the action
chat/inputAnswerChangedanswer.state requires a value but answer.value is absent, or answer.value.kind is missing the matching payload fieldServer SHOULD reject the action
chat/inputCompletedNo input request with matching requestIdServer SHOULD reject the action
chat/inputCompletedresponse is 'accept' but required questions do not have submitted answersServer SHOULD reject the action
chat/pendingMessageRemovedNo pending message with matching id and kindServer SHOULD reject the action

Pending Message Consumption

Pending messages live on the chat, not the session. The consumption rules mirror the legacy session behavior:

Queued Messages

When a turn completes and queuedMessages is non-empty, the server SHOULD:

  1. Dispatch chat/pendingMessageRemoved with kind: 'queued' for the first queued message.
  2. Dispatch chat/turnStarted with the queued message's message and queuedMessageId set to the message's id.

When a queued message is added while the chat is idle (no active turn), the server SHOULD immediately consume it using the same two-step sequence.

Steering Messages

When a turn is active and steeringMessages is non-empty, the server MAY consume steering messages at its discretion. To consume a steering message, the server:

  1. Dispatches chat/pendingMessageRemoved with kind: 'steering'.
  2. Injects the message content into the model context (the injection mechanism is opaque to the protocol).

Steering messages added while idle are silently stored and consumed when a turn becomes active.

Actions

Refer to the Chat Channel Reference for the full per-action reference. All chat-scoped action envelopes carry channel: "ahp-chat:/<uuid>".

Released under the MIT License.