mcpbeat

Channels Migration

microsoft/channels-migration

>- Migrate code that talks to or implements the Agent Host Protocol from the pre-channels model to the current channel-based model. Use when asked to `envelope` field inside `action` params, a `resource` field on subscribe/unsubscribe/snapshot results, or `session` / `terminal` / `changeset` fields on command params or action payloads.

5k tokens
context cost
the whole folder, loaded on every use
1
files
instructions only
0
copies elsewhere
how many repositories repackaged it
188
stars on the repo
on the repository, not the skill itself

Install

one command, takes just this skill from the repository
npx skills add https://github.com/microsoft/agent-host-protocol --skill channels-migration

The instruction itself

17 sections, as written by the author

AHP Channels Migration

This skill helps you migrate a codebase that consumes (or implements) the

Agent Host Protocol from the pre-channels model to the **channel-based

model** introduced by RFC #117. The migration was folded into the protocol

as a single breaking step alongside the 0.2.0 version bump (which also

introduced changesets and the session/customizationUpdated action). The

combined wire format on 0.2.0 is what this skill targets; there is no

transitional version.

The migration is mechanical in most places but touches several shapes at once,

so do it in passes rather than file-by-file. The order below minimises the

chance of leaving the codebase in a half-broken state.

How to use this skill

  • Start with the Mental model section so you understand what "channel"

means and why fields moved.

  • Work through the Migration passes in order. Each pass is independent

enough that you can land it as its own commit.

  • Use the Grep cheat sheet at the bottom to find every site that needs

updating in your codebase.

  • When in doubt about a specific type's new shape, look it up in the AHP

repo's types/*.ts (state.ts, actions.ts, commands.ts,

messages.ts, notifications.ts) or in docs/specification/.

Mental model

In the pre-channels world, AHP had a privileged "state subscription"

mechanism: clients subscribed to a URI, got a snapshot, and received action

envelopes for that URI's state. Notifications (session catalogue events,

auth-required) were stuffed inside a single notification wrapper method.

The URI of a state-bearing resource was embedded directly into the action

payload (action.session, action.terminal).

In the channel-based world, every push-style interaction lives on a

channel, identified by a URI. Subscriptions, action delivery, and

protocol notifications all carry a top-level channel: URI field that

identifies which subscription a message belongs to. A channel MAY have

associated state (root, sessions, terminals), or it MAY be stateless (future

use: logging, MCP, LSP relay). The wire shape is uniform across types.

Concretely:

  • Root URI: agenthost:/rootahp-root://
  • Session URI scheme: provider-named (copilot:/<uuid>) →

ahp-session:/<uuid> in docs/examples. The provider lives on

SessionSummary.provider, not in the URI. (Live session URIs are

announced by the server, so this is mainly a docs/example change for

consumers — but type-level shape changes still apply.)

  • Terminal URI scheme (docs/examples): ahp-terminal:/<id>. Server-

defined; clients treat as opaque.

  • Changeset URI scheme (docs/examples): ahp-changeset:/<id>. Server-

defined; obtained by expanding a Changeset.uriTemplate.

Changesets are a new channel type introduced in the same step.

  • channel everywhere: subscribe/unsubscribe/action-envelope/

dispatchAction/every protocol notification AND every command has

channel: URI at the top level of its params. Commands that are

connection-level rather than channel-scoped (e.g. initialize, ping,

listSessions, the resource* filesystem commands, authenticate)

set channel to the literal 'ahp-root://'.

  • Action payloads are channel-less: session: URI and terminal: URI

fields are gone from individual actions. Routing is by envelope.

  • Notifications are top-level methods: the notification wrapper and

the ProtocolNotification union are gone.

  • SessionDiffsChangedAction is gone: replaced by

SessionChangesetsChangedAction (catalogue updates on a session) plus a

new family of per-changeset actions (changeset/statusChanged,

changeset/fileSet, changeset/fileRemoved,

changeset/operationsChanged, changeset/cleared) and the

invokeChangesetOperation command. See docs/guide/changesets.md.

Migration passes

Apply these in order. After each pass, run your typecheck/test loop.

Pass 1 — Rename the root URI

agenthost:/root  →  ahp-root://

This is the one immediately-wire-breaking rename. Any client that subscribes

to root state with the old URI will receive an error from a channels-era

server.

Update every literal occurrence in code, tests, fixtures, configuration, and

documentation. Search patterns:

  • TypeScript/JavaScript/JSON: agenthost:/root
  • Anywhere the URI is constructed from constants: look for an AHP_ROOT_URI

or similar constant.

Pass 2 — subscribe / unsubscribe params: resourcechannel

// before
{ method: 'subscribe',   params: { resource: <uri> } }
{ method: 'unsubscribe', params: { resource: <uri> } }

// after
{ method: 'subscribe',   params: { channel: <uri> } }
{ method: 'unsubscribe', params: { channel: <uri> } }

SubscribeParams.resource is now SubscribeParams.channel.

UnsubscribeParams.resource is now UnsubscribeParams.channel. The URI

value is unchanged for state channels — only the field name moves.

Pass 3 — SubscribeResult becomes { snapshot? }

The result of a subscribe request was a flat snapshot. It is now wrapped

in an optional snapshot field, because stateless channels return no

snapshot.

// before — SubscribeResult was the snapshot directly
{
  resource: 'ahp-session:/<uuid>',
  state:    { ... },
  fromSeq:  5,
}

// after — SubscribeResult is { snapshot?: Snapshot }
{
  snapshot: {
    resource: 'ahp-session:/<uuid>',
    state:    { ... },
    fromSeq:  5,
  }
}

For stateless channels, snapshot is omitted entirely:

// stateless channel: subscribe returns an empty result
{}

Update every place that destructures the subscribe response. If your client

threaded the snapshot directly into reducers, you now need to read

result.snapshot and handle undefined (stateless) as a separate path.

Pass 4 — Action envelopes carry channel; action payloads lose theirs

The envelope grew a top-level channel field. Every individual session and

terminal action lost its inner channel-identifier field.

// before — ActionEnvelope
{
  action: { type: 'session/delta', session: 'copilot:/<uuid>', turnId: 't1', ... },
  serverSeq: 6,
  origin: { ... },
}

// after — ActionEnvelope
{
  channel: 'ahp-session:/<uuid>',
  action:  { type: 'session/delta', turnId: 't1', ... },  // no `session` field
  serverSeq: 6,
  origin: { ... },
}

Producer-side changes (anything that builds action envelopes or

individual actions): stop populating session/terminal on action payloads.

Populate envelope.channel instead. The two are typically the same URI you

were already using.

Consumer-side changes (anything that routes actions to per-session or

per-terminal reducers): switch your dispatch keying from

action.session / action.terminal to envelope.channel. The reducer

selection becomes "look at the envelope's channel, route by URI scheme".

Action interfaces that lost their channel field — full list:

  • All session actions: SessionReadyAction, SessionCreationFailedAction,

SessionTurnStartedAction, SessionDeltaAction,

SessionResponsePartAction, SessionToolCallStartAction,

SessionToolCallDeltaAction, SessionToolCallReadyAction,

SessionToolCallConfirmedAction, SessionToolCallCompleteAction,

SessionToolCallResultConfirmedAction,

SessionToolCallContentChangedAction, SessionTurnCompleteAction,

SessionTurnCancelledAction, SessionErrorAction,

SessionTitleChangedAction, SessionUsageAction,

SessionReasoningAction, SessionModelChangedAction,

SessionServerToolsChangedAction,

SessionActiveClientChangedAction,

SessionActiveClientToolsChangedAction,

SessionPendingMessageSetAction,

SessionPendingMessageRemovedAction,

SessionQueuedMessagesReorderedAction,

SessionInputRequestedAction, SessionInputAnswerChangedAction,

SessionInputCompletedAction, SessionCustomizationsChangedAction,

SessionCustomizationToggledAction,

SessionCustomizationUpdatedAction, SessionTruncatedAction,

SessionIsReadChangedAction, SessionIsArchivedChangedAction,

SessionActivityChangedAction,

SessionChangesetsChangedAction (replaces the removed

SessionDiffsChangedAction),

SessionConfigChangedAction, SessionMetaChangedAction.

  • Tool-call actions also lost session because ToolCallActionBase no

longer carries it. turnId and toolCallId remain.

  • All terminal actions: TerminalDataAction, TerminalInputAction,

TerminalResizedAction, TerminalClaimedAction,

TerminalTitleChangedAction, TerminalCwdChangedAction,

TerminalExitedAction, TerminalClearedAction,

TerminalCommandDetectionAvailableAction,

TerminalCommandExecutedAction, TerminalCommandFinishedAction.

  • All changeset actions (new in 0.2.0):

ChangesetStatusChangedAction, ChangesetFileSetAction,

ChangesetFileRemovedAction, ChangesetOperationsChangedAction,

ChangesetClearedAction. These never carried a changeset: URI field

in shipping code — it was removed before they were released.

Pass 5 — action server notification: drop the envelope wrapper

The server → client action method previously wrapped its envelope:

// before
{ method: 'action', params: { envelope: ActionEnvelope } }

// after
{ method: 'action', params: ActionEnvelope }   // envelope is the params, flat

Wherever you build or parse action notifications, remove the extra

{ envelope: ... } nesting.

Pass 6 — dispatchAction gains a channel

Client → server still uses the dispatchAction method name (this did not

get renamed to action). The params type adds a top-level channel:

// before
{
  method: 'dispatchAction',
  params: { clientSeq: 1, action: { type: 'session/turnStarted', session: '...', ... } }
}

// after
{
  method: 'dispatchAction',
  params: {
    channel:   'ahp-session:/<uuid>',
    clientSeq: 1,
    action:    { type: 'session/turnStarted', turnId: '...', ... }   // no `session`
  }
}

Pass 7 — Protocol notifications become top-level methods

The notification wrapper method is gone. Each protocol notification is

now its own top-level JSON-RPC method with its own params type, and each

carries a top-level channel.

| Pre-channels | Post-channels (method) | Params type |

|------------------------------------------------------|---------------------------------|------------------------------|

| notificationnotify/sessionAdded | root/sessionAdded | SessionAddedParams |

| notificationnotify/sessionRemoved | root/sessionRemoved | SessionRemovedParams |

| notificationnotify/sessionSummaryChanged | root/sessionSummaryChanged | SessionSummaryChangedParams|

| notificationnotify/authRequired | auth/required | AuthRequiredParams |

Each new params type has channel: URI as its top-level field. For

root/* notifications, channel is ahp-root://. For auth/required,

channel can be any channel the auth requirement is scoped to.

// before
{
  method: 'notification',
  params: {
    notification: {
      type:    'notify/sessionAdded',
      summary: { resource: 'copilot:/<uuid>', ... }
    }
  }
}

// after
{
  method: 'root/sessionAdded',
  params: {
    channel: 'ahp-root://',
    summary: { resource: 'ahp-session:/<uuid>', ... }
  }
}

Drop any code that imports ProtocolNotification, the NotificationType

enum, NotificationMethodParams, or the combined NotificationMap — these

types were removed. Replace them with the per-method params types and a

direct lookup on the wire-level method name. The type-level constraint

that every entry in ClientNotificationMap / ServerNotificationMap has

params extends { channel: URI } is enforced in

types/version/message-checks.ts, alongside an equivalent check that

every entry in CommandMap / ServerCommandMap has

params extends BaseParams (see Pass 10).

Pass 8 — Session URI scheme (docs/examples + helpers)

If your code constructs session URIs via a helper like

AgentSession.uri(provider, rawId), the provider component is no longer

encoded in the scheme. Update the helper to produce ahp-session:/<rawId>

and remove any AgentSession.provider(session) lookups that extracted the

provider from the URI. Read the provider from SessionSummary.provider

instead.

For session URIs that are announced by an existing AHP server you talk

to, you do not need to change anything in your client beyond accepting the

new scheme. Treat session URIs as opaque strings except where you

specifically construct them.

Pass 9 — Reconnect / replay behaviour for stateless channels

reconnect still carries subscriptions: URI[]. State-channel replay

behaviour is unchanged. Stateless channels (where they exist) are

re-subscribed on reconnect — missed messages are dropped, never replayed.

If you implement a server: do not include stateless channels in the

replay result's actions list. They will simply re-subscribe.

Pass 10 — Commands carry channel: URI

Every command's params now extends BaseParams { channel: URI }. The

channel field tells the server which channel the command targets, so a

router can dispatch any incoming message — request, response, or

notification — by inspecting params.channel without further

deserialisation.

There are two flavours:

Channel-scoped commands — rename the existing

session / terminal / changeset field to channel. The URI value is

unchanged.

| Command | Old field | New field |

|-----------------------------|-----------------------|-----------|

| createSession | session: URI | channel: URI |

| disposeSession | session: URI | channel: URI |

| createTerminal | terminal: URI | channel: URI |

| disposeTerminal | terminal: URI | channel: URI |

| fetchTurns | session: URI | channel: URI |

| completions | session: URI | channel: URI |

| invokeChangesetOperation | changeset: URI | channel: URI |

| subscribe, unsubscribe, dispatchAction | already channel (Passes 2 & 6) | unchanged |

Connection-level commands — narrow channel to the literal

'ahp-root://'. Add the field explicitly; the TS types enforce it.

Methods: initialize, ping, reconnect, listSessions,

authenticate, resolveSessionConfig, sessionConfigCompletions,

resourceRead, resourceWrite, resourceList, resourceCopy,

resourceDelete, resourceMove, resourceRequest.

// before
{ method: 'initialize', params: { protocolVersions: ['0.2.0'], clientId: 'c1' } }
{ method: 'listSessions', params: {} }
{ method: 'createSession', params: { session: 'ahp-session:/<uuid>', provider: 'copilot' } }
{ method: 'fetchTurns', params: { session: 'ahp-session:/<uuid>', limit: 20 } }

// after
{ method: 'initialize',   params: { channel: 'ahp-root://', protocolVersions: ['0.2.0'], clientId: 'c1' } }
{ method: 'listSessions', params: { channel: 'ahp-root://' } }
{ method: 'createSession', params: { channel: 'ahp-session:/<uuid>', provider: 'copilot' } }
{ method: 'fetchTurns',   params: { channel: 'ahp-session:/<uuid>', limit: 20 } }

The compile-time check _CheckCommandsHaveChannel in

types/version/message-checks.ts verifies that every entry in

CommandMap / ServerCommandMap has params assignable to BaseParams.

If you forget to add channel to a new command's params, the check

fails to compile and points at the missing field.

Grep cheat sheet

Run these searches in your codebase. Each pattern is a strong signal that a

migration site still needs attention.

| Pattern | What it indicates |

|--------------------------------------|--------------------|

| agenthost:/root | Old root URI literal (Pass 1) |

| "resource"/resource: near subscribe/unsubscribe/Subscribe | Old subscribe param shape (Pass 2) |

| result.resource / result.state / result.fromSeq right after subscribe | Old flat-snapshot subscribe result (Pass 3) |

| action.session / action.terminal | Reading the channel off the action payload — should be from the envelope (Pass 4) |

| session: inside an action literal | Producer writing pre-channels payload (Pass 4) |

| params.envelope near 'action' | Old action server notification wrapper (Pass 5) |

| dispatchAction without channel | Client dispatch missing channel (Pass 6) |

| 'notification' as a JSON-RPC method name | Old protocol notification wrapper (Pass 7) |

| notify/sessionAdded, notify/sessionRemoved, notify/sessionSummaryChanged, notify/authRequired | Old protocol notification names (Pass 7) |

| ProtocolNotification, NotificationType, NotificationMethodParams, NotificationMap | Removed types (Pass 7) |

| SessionAddedNotification, SessionRemovedNotification, SessionSummaryChangedNotification, AuthRequiredNotification | Old notification interfaces — renamed to *Params (Pass 7) |

| AgentSession.provider, provider extracted from session scheme | Old provider-via-scheme helper (Pass 8) |

| SessionDiffsChangedAction, summary.diffs, session/diffsChanged | Removed in favour of changesets (Pass 4 list) |

| CreateSessionParams\W+session:, DisposeSessionParams\W+session:, CreateTerminalParams\W+terminal:, DisposeTerminalParams\W+terminal:, FetchTurnsParams\W+session:, CompletionsParams\W+session:, InvokeChangesetOperationParams\W+changeset: | Channel-scoped command params still using the old field name (Pass 10) |

| ListSessionsParams(), PingParams(), ResourceReadParams\(uri: (no channel:), or any other command params constructed without channel | Connection-level command missing the channel: 'ahp-root://' literal (Pass 10) |

Verification checklist

After the migration, your code should:

  • [ ] Subscribe to ahp-root:// instead of agenthost:/root.
  • [ ] Pass { channel } (not { resource }) to subscribe and unsubscribe.
  • [ ] Read result.snapshot from subscribe responses; tolerate

snapshot === undefined for stateless channels.

  • [ ] Build action envelopes with a top-level channel. No

session / terminal fields inside individual action payloads.

  • [ ] Consume the action server notification's params as the envelope

itself, not as { envelope }.

  • [ ] Build dispatchAction params with a top-level channel.
  • [ ] Receive protocol notifications as top-level methods

(root/sessionAdded, root/sessionRemoved,

root/sessionSummaryChanged, auth/required) rather than nested

inside notification.

  • [ ] No imports of ProtocolNotification, NotificationType,

NotificationMethodParams, or NotificationMap.

  • [ ] Resolve a session's provider via SessionSummary.provider, not via

the URI scheme.

  • [ ] No SessionDiffsChangedAction / summary.diffs references; consume

SessionState.changesets plus the changeset/* action family instead.

  • [ ] Every command's params carries channel: URI. Channel-scoped

commands (createSession, disposeSession, createTerminal,

disposeTerminal, fetchTurns, completions,

invokeChangesetOperation) pass the target channel URI;

connection-level commands pass the literal 'ahp-root://'.

When all these are true, your consumer is on the channels model.

References

For the full normative description of the channel model, consult these

documents in the microsoft/agent-host-protocol repository:

  • docs/specification/subscriptions.md — Channels & Subscriptions (the

framework, including stateless channels and the URI scheme table)

  • docs/specification/root-channel.md — Root channel state, actions, and

protocol notifications

  • docs/specification/session-channel.md — Session channel lifecycle,

client-action validation, pending-message consumption

  • docs/specification/terminal-channel.md — Terminal channel data flow

and command detection

  • docs/specification/lifecycle.md — Connection handshake and reconnection
  • docs/guide/changesets.md — Changeset channel model, catalogue,

per-changeset state, and invokeChangesetOperation

  • types/actions.ts, types/commands.ts, types/messages.ts,

types/notifications.ts — Source-of-truth type definitions

(BaseParams lives in commands.ts)

  • types/version/message-checks.ts — Compile-time checks that every

command and notification carries channel: URI

— The RFC that introduced the channel model

How to use it

Copy the folder

Take microsoft/channels-migration from the repository into ~/.claude/skills for personal use, or into .claude/skills inside a project.

Check the name does not clash

The agent identifies a skill by the name field in its header. Two skills with the same name cannot sit side by side — one of them will be ignored.