esc
Type to search across all notes

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.jsonl is the canonical log. One JSON object per line. Sizes range from a few KB to ~120 MB for very long sessions.
  • session-store.db is 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 / parentId form the causal chain (e.g., tool.execution_complete references the tool.execution_start it answers).
  • Every value inside data is 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)

CountTypePurpose
29 907tool.execution_startA tool call has begun
29 894tool.execution_completeTool call finished (paired by toolCallId)
19 045assistant.messageConsolidated assistant message (final, post-stream)
14 708assistant.turn_startStart of an assistant reasoning turn
14 688assistant.turn_endEnd of an assistant turn
6 700hook.startpreToolUse / postToolUse (etc.) hook fired
6 700hook.endHook completed (paired by hookInvocationId)
2 249user.messageUser input received
1 480system.messageSystem prompt / instruction injected
360permission.requestedTool / file-write permission prompt
360permission.completedUser decision (paired by requestId)
347system.notificationUI-rendered system notice
327subagent.startedSub-agent (explore/task/etc.) launched
327subagent.completedSub-agent finished (with telemetry)
277session.model_changeActive model switched
163session.compaction_startContext compaction kicked off
162session.compaction_completeCompaction finished, summary written
121session.shutdownSession ended (carries the session report)
104session.startFirst event of every session
103session.workspace_file_changedFile changed on disk in workspace
90session.resumeSession reopened (with eventCount of prior events)
54session.errorRecoverable error
38session.infoMisc informational notice
34external_tool.requestedMCP / external-server tool requested
34external_tool.completedExternal tool finished
20session.plan_changedplan.md / planner state updated
20session.mode_changedPermission / interaction mode changed
17abortA turn was aborted
17session.task_completeTracked task finished
5session.truncationHistory truncated to fit context
2session.permissions_changedAllow-all permission rule changed
2session.warningWarning surfaced (e.g., slow MCP)
1session.context_changedcwd / repo / branch changed

session.permissions_changed is 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

TypeReplaced by
assistant.message_startthe final assistant.message
assistant.message_delta(chunks merged)
assistant.streaming_delta(chunks merged)
assistant.reasoningmerged into assistant.message.reasoningText
assistant.reasoning_delta(chunks merged)
assistant.intent(intermediate planning, not part of conversation)
assistant.usagerolled up into session.shutdown.modelMetrics
tool.execution_partial_resultthe final tool.execution_complete
tool.execution_progressthe final tool.execution_complete
pending_messages.modifiedUI-only delta
model.call_failurepreserved as session.error if it persists

Interactive request/response — handled live by callbacks

TypeNotes
user_input.requested / user_input.completedDriven by the host’s on_user_input_request handler
elicitation.requested / elicitation.completedMCP elicitation flow
sampling.requested / sampling.completedMCP sampling flow
mcp.oauth_required / mcp.oauth_completedOAuth flow for MCP servers
auto_mode_switch.requested / auto_mode_switch.completedAuto-mode switching prompt
exit_plan_mode.requested / exit_plan_mode.completedPlan-mode exit prompt
tool.user_requestedUser 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:

PairCorrelation key
tool.execution_starttool.execution_completetoolCallId
hook.starthook.endhookInvocationId
permission.requestedpermission.completedrequestId (also toolCallId)
external_tool.requestedexternal_tool.completedrequestId
subagent.startedsubagent.completedtoolCallId
assistant.turn_startassistant.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 standalone copilot CLI).
  • On-disk layout: ~/.copilot/session-state/<id>/events.jsonl + ~/.copilot/session-store.db.