GitHub Copilot CLI — Session Event Storage
How the GitHub Copilot CLI persists conversation state on disk, what events make it into the on-disk log, what gets dropped, and how client.resume_session() reconstructs a session.
1. Where session data lives
~/.copilot/
├── session-state/
│ └── <sessionId>/ ← one folder per session
│ ├── workspace.yaml (id, cwd, created_at, …)
│ ├── events.jsonl ◀── append-only NDJSON, the source of truth
│ ├── checkpoints/
│ │ └── index.md
│ ├── files/ (session-scoped artifacts)
│ └── research/
├── session-store.db (SQLite index: sessions / turns /
│ checkpoints / session_files — derived
│ from events.jsonl, NOT canonical)
└── logs/
└── process-*.log (INFO/WARN/ERROR diagnostics)
events.jsonlis the canonical log. One JSON object per line. Sizes range from a few KB to ~120 MB for very long sessions.session-store.dbis a SQLite index kept in sync with the event log. It only stores higher-level rows (one row per turn, one row per file the agent created/edited, etc.). It does not contain individual events.
2. Event envelope
Every line in events.jsonl is the same shape:
{
"id": "01c2d6...",
"parentId": "f7e2a3...",
"timestamp": "2026-06-01T22:50:38.123Z",
"type": "session.start",
"data": { ... type-specific fields ... }
}
id/parentIdform the causal chain (e.g.,tool.execution_completereferences thetool.execution_startit answers).- Every value inside
datais a string — numbers, booleans, and even nested JSON are serialised. The on-disk log is fully grep-friendly.
3. How client.resume_session() uses it
The Python SDK does not read events.jsonl itself. It spawns the bundled CLI binary (copilot/bin/copilot.exe, ~121 MB Node.js) over stdio JSON-RPC and asks the CLI to do the work:
your code Python SDK bundled copilot.exe
└─ await client.resume_session(id) ───RPC───▶ session.resume {sessionId}
│
▼
reads ~/.copilot/session-state/<id>/events.jsonl,
replays it into an in-memory Session,
returns { workspacePath, capabilities }
└─ await session.get_messages() ───RPC───▶ session.getMessages {sessionId}
│
▼
returns the in-memory event list
Interactive handlers (permission, user_input, elicitation) are not persisted — every resume must re-bind them; that’s why both create_session() and resume_session() accept on_permission_request, on_user_input_request, etc.
4. The 81 SDK event types
The SDK enum SessionEventType (in copilot.generated.session_events) defines 81 event types. Only 33 are written to disk; the other 48 are ephemeral wire events used for live streaming/UI updates and dropped during persistence.
4.1 The 33 PERSISTED types (with counts from a sample of 104 sessions / 128 356 events)
| Count | Type | Purpose |
|---|---|---|
| 29 907 | tool.execution_start | A tool call has begun |
| 29 894 | tool.execution_complete | Tool call finished (paired by toolCallId) |
| 19 045 | assistant.message | Consolidated assistant message (final, post-stream) |
| 14 708 | assistant.turn_start | Start of an assistant reasoning turn |
| 14 688 | assistant.turn_end | End of an assistant turn |
| 6 700 | hook.start | preToolUse / postToolUse (etc.) hook fired |
| 6 700 | hook.end | Hook completed (paired by hookInvocationId) |
| 2 249 | user.message | User input received |
| 1 480 | system.message | System prompt / instruction injected |
| 360 | permission.requested | Tool / file-write permission prompt |
| 360 | permission.completed | User decision (paired by requestId) |
| 347 | system.notification | UI-rendered system notice |
| 327 | subagent.started | Sub-agent (explore/task/etc.) launched |
| 327 | subagent.completed | Sub-agent finished (with telemetry) |
| 277 | session.model_change | Active model switched |
| 163 | session.compaction_start | Context compaction kicked off |
| 162 | session.compaction_complete | Compaction finished, summary written |
| 121 | session.shutdown | Session ended (carries the session report) |
| 104 | session.start | First event of every session |
| 103 | session.workspace_file_changed | File changed on disk in workspace |
| 90 | session.resume | Session reopened (with eventCount of prior events) |
| 54 | session.error | Recoverable error |
| 38 | session.info | Misc informational notice |
| 34 | external_tool.requested | MCP / external-server tool requested |
| 34 | external_tool.completed | External tool finished |
| 20 | session.plan_changed | plan.md / planner state updated |
| 20 | session.mode_changed | Permission / interaction mode changed |
| 17 | abort | A turn was aborted |
| 17 | session.task_complete | Tracked task finished |
| 5 | session.truncation | History truncated to fit context |
| 2 | session.permissions_changed | Allow-all permission rule changed |
| 2 | session.warning | Warning surfaced (e.g., slow MCP) |
| 1 | session.context_changed | cwd / repo / branch changed |
session.permissions_changedis observed on disk but not in the public SDK enum — likely added CLI-side and not yet rolled into generated bindings.
4.2 The 48 EPHEMERAL types (defined but never persisted)
Grouped by why they are dropped:
Streaming deltas — consolidated into assistant.message
| Type | Replaced by |
|---|---|
assistant.message_start | the final assistant.message |
assistant.message_delta | (chunks merged) |
assistant.streaming_delta | (chunks merged) |
assistant.reasoning | merged into assistant.message.reasoningText |
assistant.reasoning_delta | (chunks merged) |
assistant.intent | (intermediate planning, not part of conversation) |
assistant.usage | rolled up into session.shutdown.modelMetrics |
tool.execution_partial_result | the final tool.execution_complete |
tool.execution_progress | the final tool.execution_complete |
pending_messages.modified | UI-only delta |
model.call_failure | preserved as session.error if it persists |
Interactive request/response — handled live by callbacks
| Type | Notes |
|---|---|
user_input.requested / user_input.completed | Driven by the host’s on_user_input_request handler |
elicitation.requested / elicitation.completed | MCP elicitation flow |
sampling.requested / sampling.completed | MCP sampling flow |
mcp.oauth_required / mcp.oauth_completed | OAuth flow for MCP servers |
auto_mode_switch.requested / auto_mode_switch.completed | Auto-mode switching prompt |
exit_plan_mode.requested / exit_plan_mode.completed | Plan-mode exit prompt |
tool.user_requested | User explicitly invoked a tool |
Runtime catalogue updates — re-derived from current config on resume
| Type |
|---|
capabilities.changed, commands.changed |
command.queued, command.execute, command.completed |
session.tools_updated, session.mcp_servers_loaded, session.mcp_server_status_changed |
session.skills_loaded, session.extensions_loaded |
session.custom_agents_updated, session.background_tasks_changed |
session.usage_info, session.handoff, session.idle |
session.remote_steerable_changed, session.title_changed |
session.schedule_created, session.schedule_cancelled |
session.snapshot_rewind |
skill.invoked |
subagent.selected, subagent.deselected, subagent.failed |
unknown (fallback) |
5. Schema and sample payload for every persisted type
All examples below use the same envelope (id, parentId, timestamp, type, data); only data is shown.
session.start
{
"sessionId": "013f2c3f-dbc1-4ce2-9e7a-83e4ea1c5c7d",
"version": "1",
"producer": "copilot-agent",
"copilotVersion": "1.0.36-0",
"startTime": "2026-04-30T01:45:16.518Z",
"context": "{\"cwd\":\"C:\\\\Users\\\\AB000725\"}",
"alreadyInUse": "false",
"remoteSteerable":"false",
"selectedModel": "claude-opus-4.6",
"contextTier": "default"
}
session.resume
{
"resumeTime": "2026-04-17T04:21:28.410Z",
"eventCount": "2632",
"selectedModel": "claude-opus-4.6",
"context": "{\"cwd\":\"C:\\\\Users\\\\AB000725\\\\vwsre-project\"}",
"alreadyInUse": "false",
"remoteSteerable": "false",
"sessionWasActive": "false",
"continuePendingWork":"false",
"reasoningEffort": "medium",
"contextTier": "default"
}
user.message
{
"content": "Search Jira issues",
"transformedContent": "<current_datetime>2026-04-30T09:47:10.882+08:00</current_datetime>\n\n…",
"attachments": "[]",
"interactionId": "3277b99b-d9cf-4ed5-ace9-4ddadc2512d3",
"parentAgentTaskId": null,
"supportedNativeDocumentMimeTypes": "[]",
"agentMode": null,
"isAutopilotContinuation": "false",
"source": null
}
assistant.turn_start
{ "turnId": "0", "interactionId": "3277b99b-d9cf-4ed5-ace9-4ddadc2512d3" }
assistant.message (final, post-stream)
{
"messageId": "2e04176c-990b-4a61-9e06-9392c04026b4",
"content": "What would you like to search for in Jira? Please provide details…",
"toolRequests": "[]",
"interactionId": "3277b99b-d9cf-4ed5-ace9-4ddadc2512d3",
"outputTokens": "144",
"requestId": "D019:F03D7:19CFDA:1E6282:69F2B42D",
"reasoningOpaque": "<encrypted CoT blob>",
"reasoningText": "<plaintext reasoning, e.g. for Claude Opus>",
"turnId": "5",
"parentToolCallId": null,
"model": "claude-opus-4.7",
"encryptedContent": "<provider-side compliance blob>",
"serviceRequestId": null,
"phase": null
}
assistant.turn_end
{ "turnId": "0" }
system.message
{
"role": "system",
"content": "You are the GitHub Copilot CLI, a terminal assistant built by GitHub…"
}
system.notification
{
"kind": "{\"type\":\"agent_completed\",\"agentId\":\"rnp-activation-reasons\",…}",
"content": "<system_notification>\nAgent \"rnp-activation-reasons\" (explore)…</system_notification>"
}
tool.execution_start
{
"toolCallId": "tooluse_ycUA9DN2RHyjfBvngXDyNP",
"toolName": "report_intent",
"arguments": "{\"intent\":\"Searching for SKILL.md\"}",
"parentToolCallId": null,
"turnId": "5",
"mcpServerName": null,
"mcpToolName": null,
"model": "claude-opus-4.7"
}
tool.execution_complete
{
"toolCallId": "tooluse_ycUA9DN2RHyjfBvngXDyNP",
"model": "claude-opus-4.6",
"interactionId": "7d306a55-6f0e-44a7-a406-6098ef4d9496",
"success": "true",
"result": "{\"content\":\"Intent logged\",\"detailedContent\":\"Searching…\"}",
"toolTelemetry": "{}",
"parentToolCallId": null,
"turnId": "5",
"error": null
}
external_tool.requested (MCP / app-hosted tool)
{
"requestId": "0d3c9002-7065-44dc-8636-74886bba6748",
"sessionId": "307cbb62-0803-44d1-a0cf-1810c2b0c140",
"toolCallId": "toolu_bdrk_01BhWurQKcRcxM9dPxvB6ZpZ",
"toolName": "jira_wcar_api",
"arguments": "{\"endpoint\":\"/rest/api/2/search\",\"params\":{\"jql\":\"assignee = currentUser()\"}}",
"workingDirectory": "C:\\Users\\AB000725"
}
external_tool.completed
{ "requestId": "0d3c9002-7065-44dc-8636-74886bba6748" }
permission.requested
{
"requestId": "ed14a1fc-ddc7-4a9f-a3f3-81b49a0b02a3",
"permissionRequest": "{\"kind\":\"shell\",\"toolCallId\":\"toolu_bdrk_017YpdSD1yULiTUA2irZGVM9\"}",
"promptRequest": "{\"kind\":\"commands\",\"fullCommandText\":\"Get-Location\",\"intent\":\"…\"}"
}
permission.completed
{
"requestId": "ed14a1fc-ddc7-4a9f-a3f3-81b49a0b02a3",
"toolCallId": "toolu_bdrk_017YpdSD1yULiTUA2irZGVM9",
"result": "{\"kind\":\"approved\"}"
}
hook.start
{
"hookInvocationId": "f74dd6a7-0473-4b59-8094-0cf573c496f5",
"hookType": "postToolUse",
"input": "{\"sessionId\":\"0f4cd0bd-…\",\"timestamp\":\"…\"}"
}
hook.end
{
"hookInvocationId": "f74dd6a7-0473-4b59-8094-0cf573c496f5",
"hookType": "postToolUse",
"success": "true"
}
subagent.started
{
"toolCallId": "tooluse_9PlhA1d5U2gMJAykjv14dn",
"agentName": "explore",
"agentDisplayName": "Explore Agent",
"agentDescription": "Fast codebase exploration and answering questions. Uses code…"
}
subagent.completed
{
"toolCallId": "tooluse_4rJxVF9wiJh1gPgCGQvviA",
"agentName": "explore",
"agentDisplayName": "Explore Agent",
"model": "claude-haiku-4.5",
"totalToolCalls": "47",
"totalTokens": "795147",
"durationMs": "12340"
}
session.model_change
{
"newModel": "claude-opus-4.7",
"previousModel": "claude-opus-4.6",
"previousReasoningEffort":"medium",
"reasoningEffort": "high",
"contextTier": "default"
}
session.mode_changed
{ "previousMode": "interactive", "newMode": "plan" }
session.permissions_changed
{ "previousAllowAllPermissions": "false", "allowAllPermissions": "true" }
session.plan_changed
{ "operation": "create" }
session.context_changed
{
"cwd": "C:\\Users\\AB000725\\WirelessCar-Internal\\vwsre-tools",
"gitRoot": "C:\\Users\\AB000725\\WirelessCar-Internal\\vwsre-tools",
"branch": "dev-may-3",
"headCommit": "3e81ccf7006a97cb0f38cc1ba586ecd2fdab50da",
"repository": "WirelessCar-Internal/vwsre-tools",
"hostType": "github",
"repositoryHost": "github.com",
"baseCommit": "…"
}
session.workspace_file_changed
{ "path": "describe_tables.py", "operation": "create" }
session.compaction_start
{
"systemTokens": "9635",
"conversationTokens": "113044",
"toolDefinitionsTokens": "12663"
}
session.compaction_complete
{
"success": "true",
"preCompactionTokens": "135342",
"preCompactionMessagesLength": "180",
"summaryContent": "\n\n<overview>\nThe user is an SRE for WirelessCar's VW CN ops…\n</overview>\n",
"checkpointNumber": "1",
"checkpointPath": "C:\\Users\\AB000725\\.copilot\\session-state\\07777824-…\\checkpoints\\checkpoint-1.md",
"compactionTokensUsed": "42000",
"requestId": "…",
"error": null,
"serviceRequestId": null,
"postCompactionTokens": "53210",
"messagesRemoved": "120",
"tokensRemoved": "82132",
"systemTokens": "9635",
"conversationTokens": "31912",
"toolDefinitionsTokens": "12663"
}
session.truncation
{
"tokenLimit": "168000",
"preTruncationTokensInMessages": "194428",
"preTruncationMessagesLength": "227",
"postTruncationTokensInMessages": "155949",
"postTruncationMessagesLength": "183",
"tokensRemovedDuringTruncation": "38479",
"messagesRemovedDuringTruncation": "44",
"performedBy": "system",
"toolDefinitionsTokenCount": "12663"
}
session.task_complete
{
"summary": "Completed both requested features:\n\n1. **Home page partition…**",
"success": "true"
}
session.shutdown (rich session report)
{
"shutdownType": "routine",
"totalPremiumRequests": "7.5",
"totalApiDurationMs": "6219",
"sessionStartTime": "1777513516518",
"codeChanges": "{\"linesAdded\":0,\"linesRemoved\":0,\"filesModified\":[]}",
"modelMetrics": "{\"claude-opus-4.7\":{\"requests\":{\"count\":1,\"cost\":7.5},…}}",
"currentModel": "claude-opus-4.7",
"currentTokens": "53210",
"systemTokens": "9635",
"conversationTokens": "31912",
"toolDefinitionsTokens": "12663",
"totalNanoAiu": "120000",
"tokenDetails": "{…}"
}
session.error
{
"errorType": "query",
"message": "Execution failed: Error: Failed to get response from the AI model…",
"stack": "Error: Failed to get response from the AI model; retried 5 times…",
"statusCode": "503",
"providerCallId": "…",
"serviceRequestId":null
}
session.warning
{ "warningType": "mcp", "message": "MCP server 'github-mcp-server' is taking longer than expected…" }
session.info
{ "infoType": "folder_trust", "message": "Folder C:\\Users\\AB000725\\playground has been added to trusted folders." }
abort
{ "reason": "user_initiated" }
6. Wire vs disk — visual mental model
Wire (live SDK stream) Disk (events.jsonl)
━━━━━━━━━━━━━━━━━━━━━━ ━━━━━━━━━━━━━━━━━━━━━
message_start ┐
message_delta × 47 │─── consolidated ──▶ assistant.message (1 row, full content)
reasoning_delta × 23 │
assistant.usage ┘
tool.execution_start ─────────────────────▶ tool.execution_start
tool.execution_progress × 12 (dropped)
tool.execution_partial_result × 4 (dropped)
tool.execution_complete ─────────────────────▶ tool.execution_complete
user_input.requested (dropped) (no record of the interactive Q&A;
user_input.completed (dropped) the answer is already baked into
the next assistant.message)
capabilities.changed (dropped) (re-derived from live config on resume)
commands.changed (dropped)
Design trade-off: resume reconstructs a session at message-level granularity, not at the streaming-byte level. The conversation can be replayed exactly, but you cannot replay how it appeared chunk-by-chunk to the user. That’s the design choice — and it’s why a session is “only” ~120 MB instead of multiple GB.
7. Practical correlation keys
When walking the event stream (e.g., to build a UI like the vwsre-tools chat panel), pair events using:
| Pair | Correlation key |
|---|---|
tool.execution_start ↔ tool.execution_complete | toolCallId |
hook.start ↔ hook.end | hookInvocationId |
permission.requested ↔ permission.completed | requestId (also toolCallId) |
external_tool.requested ↔ external_tool.completed | requestId |
subagent.started ↔ subagent.completed | toolCallId |
assistant.turn_start ↔ assistant.turn_end (and its messages/tools) | turnId |
The vwsre-tools frontend (frontend/src/features/copilot-chat/api.ts:fetchSessionMessages) uses exactly this pattern to walk the SDK event stream and rebuild a RestoredMessage[] + RestoredStep[] model for replay.
8. References
- Python SDK (bundled with vwsre-tools):
copilot/client.py:1652(resume_session),copilot/session.py:2229(get_messages). - Generated event enum:
copilot/generated/session_events.py:106(class SessionEventType(Enum)). - Bundled CLI binary:
copilot/bin/copilot.exe(~121 MB, Node.js, same code path as the standalonecopilotCLI). - On-disk layout:
~/.copilot/session-state/<id>/events.jsonl+~/.copilot/session-store.db.