session-metadata-migration
Skill444 starsChanged 50 days agoMITView on GitHub
Migrate code that talks to or implements the Agent Host Protocol to the flattened SessionState model. Use when asked to upgrade an AHP client/server/binding and you see a `summary` sub-object on `SessionState` (e.g. `state.summary.title`, `session.summary.modifiedAt`), `model` / `agent` on `SessionState` / `SessionSummary` / `ChatState` / `ChatSummary`, the `session/modelChanged` or `session/agentChanged` actions, numeric `createdAt` / `modifiedAt`, or `model` / `agent` arguments to `createSession` / `createChat`.
In this repository10 other files it gives its agents.
- AGENTS.mdAGENTS.md
- clients/dotnet/AGENTS.mdAGENTS.md
- clients/go/AGENTS.mdAGENTS.md
- clients/kotlin/AGENTS.mdAGENTS.md
- clients/swift/AGENTS.mdAGENTS.md
- docs/guide/agents.mdAGENTS.md
- channels-migrationSkill
- ahp-clientSkill
- groom-backlogSkill
- release-ahpSkill
The file
Show its rules and text
We split this skill into the rules it gives an agent
17 rules. About 0 are common, 5 are distinctive, and 12 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈5
Distinctive · estimate
This author's own choices. Worth a look.
12
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Distinctive: line 24
If you are also coming from the pre-channels protocol, run the
- Distinctive: line 34
Read the Mental model section so you understand what moved and why.
- Distinctive: line 37
Use the Grep cheat sheet to find every site that needs updating.
- Very specific: line 38
When in doubt about a type's new shape, look it up in the AHP repo's types/ (channels-session/state.ts, channels-chat/state.ts, …
- Very specific: line 114
When constructing a SessionState, build it flat — do not nest a summary:
- Very specific: line 138
If you maintain a session-list cache that merges root/sessionSummaryChanged deltas, drop the model / agent cases (those fields no longer …
- Very specific: line 167
Delete every read/write of model / agent on SessionState, SessionSummary, ChatState, and ChatSummary, and remove all use of the …
- Very specific: line 175
Reducers/handlers: remove the SessionModelChanged / SessionAgentChanged cases.
- Very specific: line 202
When rendering an existing turn, the model/agent it ran with is on turn.message.model / turn.message.agent (absent ⇒ host default).
- Very specific: line 210
Initialize input UI from
draft: When you open/show a chat, seed your composer (text, attachments, model/agent) from chat.draft if present. - Very specific: line 228
If you maintain a reducer, the chat/draftChanged case is a plain set/clear ({ ...state, draft: action.draft }) and does not stamp …
- Very specific: line 268
Remove the session/modelChanged and session/agentChanged cases.
- Very specific: line 269
Remove any helper that stamped the session summary's modifiedAt (the session reducer no longer owns a timestamp).
- Very specific: line 272
Add a chat/draftChanged case: state.draft = action.draft (set or clear).
- Distinctive: line 280
Run these searches in your codebase.
- Distinctive: line 299
After the migration, your code should:
- Very specific: line 321
When all these are true, your consumer is on the flattened-SessionState model.
≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
AHP Session-Metadata Migration
This skill helps you migrate a codebase that consumes (or implements) the Agent
Host Protocol to the flattened SessionState model. The change moves all
session metadata directly onto SessionState, makes SessionSummary a
root-channel-only catalog type, relocates model/agent selection onto each
Message (with a new per-chat draft), switches the summary timestamps to
ISO-8601 strings, and removes the session/modelChanged /
session/agentChanged actions. It is a single breaking step; there is no
transitional version.
If you are also coming from the pre-channels protocol, run the
ahp-channels-migration skill first — this skill assumes you are already on
the channel-based model.
The migration is mechanical 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 half-broken. After each pass, run your typecheck/test loop.
How to use this skill
- Read the Mental model section so you understand what moved and why.
- Work through the Migration passes in order. Each is independent enough to land as its own commit.
- Use the Grep cheat sheet to find every site that needs updating.
- When in doubt about a type's new shape, look it up in the AHP repo's
types/(channels-session/state.ts,channels-chat/state.ts,channels-session/actions.ts,channels-chat/actions.ts) and the canonical reducers (types/channels-session/reducer.ts,types/channels-chat/reducer.ts), or indocs/specification/.
Mental model
Previously, SessionState embedded a summary: SessionSummary sub-object, and
the same SessionSummary shape was both the live session metadata and the
catalog entry surfaced on the root channel. Model and agent selection was
session-scoped (summary.model / summary.agent, mutated by
session/modelChanged / session/agentChanged), with optional per-chat
overrides on ChatState / ChatSummary.
Now:
SessionStatecarries its metadata inline. A newSessionMetadatabase interface (provider,title,status,activity?,project?,workingDirectory?,annotations?) is extended/inlined directly ontoSessionState. There is nostate.summaryanymore — readstate.title,state.status,state.activity, etc. directly.ChatStatealready denormalized its summary fields this way;SessionStatenow matches.SessionSummaryis a root-only catalog type. It still exists and is used bylistSessions,root/sessionAdded, androot/sessionSummaryChangedto keep a cached session list in sync. It shares theSessionMetadatafields and additionally owns the identity/catalog-only fields:resource,createdAt,modifiedAt, andchanges. It is not embedded inSessionState. The host updatesmodifiedAtat its own discretion and streams it viaroot/sessionSummaryChanged.- There is no session- or chat-level model/agent selection.
modelandagentare removed fromSessionState,SessionSummary,ChatState, andChatSummary. Selection now lives on theMessage:Message.model?andMessage.agent?. A historic message records the selection actually used (so a client editing/resending it can retain that choice); when a message omits them, the agent host's default applies. - Chats have a
draft.ChatState.draft?: Messageis the user's in-progress input (text + attachments + its model/agent). The new client-dispatchablechat/draftChangedaction sets or clears it. Clients MAY periodically (debounced, not eager) sync their local input state intodraft, and SHOULD initialize input UI for an existing chat from any presentdraft.draftis state-only and is not mirrored ontoChatSummary. - Timestamps are ISO-8601 strings.
SessionSummary.createdAt/modifiedAtchanged from numbers (epoch millis) to ISO-8601 strings (e.g."2025-03-10T18:42:03.123Z"), matchingChatSummary.modifiedAtand theresource*filesystemmtime/ctime. - Removed actions/params.
session/modelChanged(SessionModelChangedAction) andsession/agentChanged(SessionAgentChangedAction) are gone. Themodel/agentparameters are removed fromcreateSessionandcreateChat. - The session reducer no longer stamps a timestamp. Title/activity/read/
archived/config actions mutate the flat
SessionStatefields and do not touch a sessionmodifiedAt(there is none). The chat reducer continues to stampChatState.modifiedAt.
Migration passes
Apply these in order. Examples are TypeScript (the reference client); Rust,
Kotlin, Swift, and Go consumers apply the same shape changes with their own
casing (modified_at, ModifiedAt, etc.).
Pass 1 — Flatten SessionState reads and writes
SessionState.summary is gone. Every state.summary.<field> becomes
state.<field> for the metadata fields (provider, title, status,
activity, project, workingDirectory, annotations).
- const title = session.summary.title;
- const isInProgress = (session.summary.status & SessionStatus.InProgress) !== 0;
- const cwd = session.summary.workingDirectory;
+ const title = session.title;
+ const isInProgress = (session.status & SessionStatus.InProgress) !== 0;
+ const cwd = session.workingDirectory;When constructing a SessionState, build it flat — do not nest a summary:
const state: SessionState = {
- summary: { resource, provider, title, status, createdAt, modifiedAt },
+ provider,
+ title,
+ status,
lifecycle: SessionLifecycle.Creating,
activeClients: [],
chats: [],
};Note resource, createdAt, and modifiedAt are not on SessionState —
they live only on the root-channel SessionSummary (Pass 2).
Pass 2 — Treat SessionSummary as a root-only catalog type
SessionSummary still exists, but only as the catalog entry on the root
channel. Keep using it for your cached session list fed by listSessions,
root/sessionAdded, and root/sessionSummaryChanged. Do not read it off
SessionState, and do not expect model / agent on it (Pass 4).
If you maintain a session-list cache that merges root/sessionSummaryChanged
deltas, drop the model / agent cases (those fields no longer exist on the
summary) and keep modifiedAt as a string (Pass 3):
if (changes.title !== undefined) merged.title = changes.title;
if (changes.status !== undefined) merged.status = changes.status;
if (changes.modifiedAt !== undefined) merged.modifiedAt = changes.modifiedAt;
- if (changes.model !== undefined) merged.model = changes.model;
if (changes.workingDirectory !== undefined) merged.workingDirectory = changes.workingDirectory;Pass 3 — createdAt / modifiedAt are ISO-8601 strings
SessionSummary.createdAt and modifiedAt are now strings. Update any code
that parses, formats, compares, or arithmetics them.
- summaries.sort((a, b) => b.modifiedAt - a.modifiedAt); // numeric subtraction
+ summaries.sort((a, b) => (a.modifiedAt < b.modifiedAt ? 1 : a.modifiedAt > b.modifiedAt ? -1 : 0));ISO-8601 strings sort chronologically under lexicographic comparison, so a
plain string compare is correct for "most recently modified". To do date math,
parse with Date.parse(...) / your platform's ISO parser. Producers that
previously emitted Date.now() should emit new Date().toISOString().
Pass 4 — Remove session/chat model & agent selection
Delete every read/write of model / agent on SessionState,
SessionSummary, ChatState, and ChatSummary, and remove all use of the
session/modelChanged and session/agentChanged actions.
- Dispatching a model/agent change: there is no session-level selection to
change. The selection is whatever the next
Messagecarries (Pass 5), so a model picker simply updates the chat'sdraft(Pass 6) or setsmodel/agenton the message you are about to send. - Reducers/handlers: remove the
SessionModelChanged/SessionAgentChangedcases. Removemodel/agentfrom anyChatSummarypartial-update merge (e.g. thesession/chatUpdatedhandler).
- store.dispatch(session, { type: 'session/modelChanged', model: { id: 'gpt-4o' } });
+ // Keep the picked model in your local input state and attach it to the
+ // outgoing message (Pass 5), or sync it into the chat draft (Pass 6).Pass 5 — Put model/agent on the Message
Message gained model?: ModelSelection and agent?: AgentSelection. Set
them when you send a turn or steering/queued message, and read them back from
historic messages when editing/resending.
const message: Message = {
text,
origin: { kind: MessageKind.User },
attachments,
+ model: selectedModel, // omit to use the agent host's default
+ agent: selectedAgent, // omit for no custom agent
};
store.dispatch(chat, { type: 'chat/turnStarted', turnId, message });When rendering an existing turn, the model/agent it ran with is on
turn.message.model / turn.message.agent (absent ⇒ host default).
Pass 6 — Add the per-chat draft and chat/draftChanged
ChatState.draft?: Message holds the user's in-progress input. Wire two
directions:
- Initialize input UI from
draft. When you open/show a chat, seed your composer (text, attachments, model/agent) fromchat.draftif present. - Sync local input into
draft. Periodically dispatchchat/draftChangedwith the current composer contents. Debounce — eager per-keystroke sync is not required. Dispatch with nodraftto clear it once the message is sent.
// Debounced (e.g. on idle/blur), not on every keystroke:
store.dispatch(chat, {
type: 'chat/draftChanged',
draft: { text, origin: { kind: MessageKind.User }, attachments, model, agent },
});
// Clear after sending:
store.dispatch(chat, { type: 'chat/draftChanged' });If you maintain a reducer, the chat/draftChanged case is a plain set/clear
({ ...state, draft: action.draft }) and does not stamp modifiedAt.
Pass 7 — Drop model / agent from createSession / createChat
These command params no longer accept model / agent. Convey an initial
selection on the first message instead.
await client.createSession({
channel: sessionUri,
provider: 'copilot',
- model: { id: 'gpt-4o' },
- agent: { uri: agentUri },
workingDirectory,
}); await client.createChat({
channel: sessionUri,
chat: chatUri,
- model: { id: 'gpt-4o' },
- agent: { uri: agentUri },
- initialMessage: { text, origin: { kind: MessageKind.User } },
+ initialMessage: { text, origin: { kind: MessageKind.User }, model, agent },
});Pass 8 — Update your reducer (if you maintain one)
Mirror the canonical reducers in types/channels-session/reducer.ts and
types/channels-chat/reducer.ts:
session/titleChanged→ setstate.title; nomodifiedAtstamp.session/isReadChanged/session/isArchivedChanged→ flip the flag onstate.statusvia yourwithStatusFlaghelper.session/activityChanged→ setstate.activity.session/configChanged→ merge/replacestate.config.values; nomodifiedAtstamp.- Remove the
session/modelChangedandsession/agentChangedcases. - Remove any helper that stamped the session summary's
modifiedAt(the session reducer no longer owns a timestamp). Keep your chat reducer's ownmodifiedAtstamping and its injectable "now" seam. - Add a
chat/draftChangedcase:state.draft = action.draft(set or clear).
The shared conformance fixtures in types/test-cases/reducers/ already encode
the new shapes (including 223-chat-draftchanged-sets-draft and
224-chat-draftchanged-clears-draft); run them against your reducer.
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 |
.summary. near a session value (session.summary, state.summary, summary.title, summary.status, summary.workingDirectory) | Reading metadata off the removed SessionState.summary (Pass 1) |
summary: inside a SessionState literal | Constructing the old nested shape (Pass 1) |
summary.modifiedAt / summary.createdAt used in arithmetic or -/</> numeric compare | Numeric timestamp assumptions (Pass 3) |
createdAt: / modifiedAt: set to a number, Date.now() into a summary | Old numeric timestamps (Pass 3) |
session/modelChanged, session/agentChanged, SessionModelChangedAction, SessionAgentChangedAction | Removed actions (Pass 4) |
.model / .agent on a session or chat summary/state value | Removed selection fields (Pass 4) |
summary.model, summary.agent, changes.model, changes.agent | Removed summary fields / root/sessionSummaryChanged deltas (Pass 4) |
createSession( / CreateSessionParams with model: or agent: | Removed command params (Pass 7) |
createChat( / CreateChatParams with model: or agent: | Removed command params (Pass 7) |
chat/draftChanged, ChatDraftChangedAction, ChatState.draft | New draft surface — confirm you sync/initialize it (Pass 6) |
Message constructed for a turn without model / agent where a picker exists | Selection not carried on the message (Pass 5) |
Verification checklist
After the migration, your code should:
- Read session metadata from
SessionStatedirectly (state.title,state.status,state.activity,state.workingDirectory, …) — nostate.summary. - Build
SessionStateflat, without a nestedsummary, and withoutmodel/agent/createdAt/modifiedAton it. - Use
SessionSummaryonly as the root-channel catalog entry (listSessions,root/sessionAdded,root/sessionSummaryChanged). - Treat
SessionSummary.createdAt/modifiedAtas ISO-8601 strings everywhere they are parsed, formatted, sorted, or compared. - Carry model/agent selection on
Message.model/Message.agent; read a historic turn's selection fromturn.message. - No
session/modelChanged/session/agentChangeddispatch or handling, and nomodel/agentonSessionState/SessionSummary/ChatState/ChatSummary. - No
model/agentarguments tocreateSession/createChat. - Initialize chat input UI from
ChatState.draftand (debounced) sync the composer back viachat/draftChanged, clearing it on send. - If you maintain a reducer: the session reducer no longer stamps a
timestamp, and a
chat/draftChangedcase sets/clearsdraft.
When all these are true, your consumer is on the flattened-SessionState model.
References
For the full normative description, consult these documents in the
microsoft/agent-host-protocol repository:
docs/guide/state-model.md—SessionState,SessionSummary,Message, and the session/chat metadata relationshipdocs/specification/session-channel.md— session channel state, the chat-aggregation rules for the root-channel summary, client-action validationdocs/specification/chat-channel.md— chat state, message-level model/agent selection, and thedraft/chat/draftChangedbehaviordocs/specification/root-channel.md—listSessions,root/sessionAdded,root/sessionSummaryChanged, and the ISO-8601 timestamp examplestypes/channels-session/state.ts,types/channels-chat/state.ts—SessionMetadata,SessionState,SessionSummary,ChatState,ChatSummary,Messagesource-of-truth definitionstypes/channels-session/reducer.ts,types/channels-chat/reducer.ts— canonical reducer behavior to mirrortypes/test-cases/reducers/— shared conformance fixtures, including thechat/draftChangedcases
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

