Session History
Read the user's past AMC sessions and projects.
Availability: Webview only (AgentMC.sessionHistory) Required Permission: sessions.readHistory
This is a WEBVIEW capability, not a backend one
An earlier version of this page said "Backend only (ctx.sessionHistory)". That was backwards: the host builds its backend context without this namespace, so ctx.sessionHistory is undefined and calling it throws a TypeError at activation rather than producing a permission error.
Use AgentMC.sessionHistory from your plugin's webview. If a backend needs the result, forward it over the shared event bus (AgentMC.events -> ctx.events).
Default-deny, and it stays that way
Declaring the permission grants nothing. Your plugin sees an empty list until the user picks specific projects or sessions in the grant picker raised by requestAccess(). getMessages() throws for a session that was never granted, and every read is written to an audit log the plugin cannot touch.
This is deliberately not the same shape as the other permissions: consent at install time is not consent to read a particular conversation.
Distinct from the Sessions API, which spawns and drives new sessions. This one only ever reads history, and never writes.
Methods
listProjects(): Promise<HistoryProject[]>
Projects the user has granted. Empty until a grant exists.
listSessions(): Promise<HistorySession[]>
Sessions the user has granted, directly or via a granted project. Empty until a grant exists.
getMessages(options: { sessionId: string }): Promise<HistoryMessage[]>
The text-only turns of one granted session.
Rejects when the session was not granted. That is a rejection rather than an empty array on purpose -- an empty array reads as "this conversation had no messages", which is a different and misleading fact.
requestAccess(options?: { kinds?: ('session' | 'project')[] }): Promise<HistoryGrantResult>
Open the user's grant picker. Resolves once they choose or cancel.
Parameters:
| Name | Type | Description |
|---|---|---|
options.kinds | ('session' | 'project')[] | Limit the picker to sessions or to projects. Defaults to both |
Types
interface HistoryProject {
id: string
name: string
}
interface HistorySession {
id: string
name: string
projectId: string
status: string
lastActiveAt: string
}
interface HistoryMessage {
id: string
role: 'user' | 'assistant'
content: string
timestamp: string
}
interface HistoryGrantResult {
requestId: string
cancelled?: boolean
sessionIds?: string[]
projectIds?: string[]
}HistoryMessage field | Type | Description |
|---|---|---|
id | string | Message identifier |
role | 'user' | 'assistant' | Who spoke |
content | string | Plain conversation text. Tool calls, tool output and file contents are stripped by the host |
timestamp | string | ISO timestamp |
Example
export function activate(ctx: PluginContext) {
ctx.cli.handle('/summarize-history', async () => {
let sessions = await AgentMC.sessionHistory.listSessions()
if (sessions.length === 0) {
const grant = await AgentMC.sessionHistory.requestAccess({ kinds: ['session'] })
if (grant.cancelled) {
// The user declining is a normal outcome, not a failure.
return { status: 200, body: { summary: null, reason: 'No sessions shared' } }
}
sessions = await AgentMC.sessionHistory.listSessions()
}
const first = sessions[0]
if (!first) return { status: 200, body: { summary: null, reason: 'Nothing granted' } }
const messages = await AgentMC.sessionHistory.getMessages({ sessionId: first.id })
const transcript = messages.map((m) => `${m.role}: ${m.content}`).join('\n')
return { status: 200, body: { summary: await ctx.ai.generateTitle(transcript) } }
})
}Notes
contentis conversation text only. Do not build a feature that depends on reading tool output or file contents from history -- the host removes them before your plugin sees them.- A grant is the user's to give and to withdraw. Re-check
listSessions()rather than caching ids across runs; a previously readable session can start throwing. - Call
requestAccess()in response to something the user did. A picker that appears on activation, before they have asked for anything, is the fastest way to have your plugin uninstalled.
Testing
createTestContext() defaults to nothing granted, matching the host. Seed sessionHistory to exercise the granted paths.
const h = createTestContext({
sessionHistory: {
sessions: [
{ id: 's1', name: 'Refactor', projectId: 'p1', status: 'done', lastActiveAt: '2026-01-01' }
],
messages: {
s1: [{ id: 'm1', role: 'user', content: 'hello', timestamp: '2026-01-01T00:00:00Z' }]
},
grantResult: { sessionIds: ['s1'] }
}
})
await h.AgentMC.sessionHistory.getMessages({ sessionId: 's1' }) // resolves
await h.AgentMC.sessionHistory.getMessages({ sessionId: 's2' }) // rejects -- never grantedOnly ids present in messages are readable, so the harness reproduces the default-deny behaviour rather than merely returning empty arrays.