microsoft/vscode-troubleshoot
Investigate unexpected behavior in the current Copilot CLI agent session by analyzing its event log. Use when the user asks why something happened, why a request was slow, why a tool was or was not used, or why instructions/skills/agents did not load.
npx skills add https://github.com/microsoft/vscode --skill troubleshoot
<!-- Customize this skill and select save to override its behavior. Delete that copy to restore the built-in behavior. -->
This skill investigates and explains unexpected agent behavior in the current Copilot CLI agent session using its on-disk event log.
Use this skill for questions like:
Base every conclusion on evidence from the event log. Do not guess.
The skill runs inside the session's agent, so the log is on the same machine. It lives outside the workspace — read it via the terminal (run_in_terminal), never grep_search.
Session log: path is provided with this message, use it. It may point to a session other than the current one (via #session) and may be comma-separated paths — investigate all of them and compare.Session log: path, or a #session: reference), keep analyzing that same session for the follow-up. Do not fall back to self-discovery here — the newest log is the *current* session, which is the wrong one. Switch only if the user references a new session.events.jsonl under ${XDG_STATE_HOME:-$HOME}/.copilot/session-state/<sessionId>/ — this skill is appending to the current session's log, so it's reliably newest. Honor XDG_STATE_HOME, else $HOME.events.jsonlEach line is a JSON object sharing one envelope:
{ "type": "...", "id": "...", "parentId": "...", "agentId": "...", "timestamp": "ISO-8601", "data": { } }
type — the event kind (see below).id — unique event id.parentId — the chronologically preceding event, not a logical parent. It is a flat back-pointer over every event, *not* the user → turn → tool-call hierarchy. Do not treat it as a logical parent.agentId — present for sub-agent events; absent for the main agent and session-level events.timestamp — ISO-8601 time. Compute durations by differencing timestamps (e.g. a tool's start vs. its completion, a turn's assistant.turn_start vs. the assistant.message).data — type-specific payload.data.* fields)session.start (once) — selectedModel, reasoningEffort.user.message — content; transformedContent (expanded prompt, when it differs).assistant.turn_start / assistant.turn_end — turnId; measure a turn's duration vs the following assistant.message.assistant.message — model, outputTokens, content, reasoningText (thinking, when present), turnId, parentToolCallId (set ⇒ sub-agent turn spawned by that tool call).tool.execution_start — toolName, toolCallId, arguments, parentToolCallId (set ⇒ nested/sub-agent).tool.execution_complete — toolCallId (matches start), success, result. Pair with its start by toolCallId for the full call + duration.session.shutdown (once, at end) — modelMetrics[*].usage, totalNanoAiu. Absent mid-session.hook.*, permission.*, system.message) — inspect data as needed.Iterate records in order and rebuild the logical tree from context:
session.start is the root.user.message begins a turn.assistant.message answers the current user.message — unless it has data.parentToolCallId, in which case it belongs to the sub-agent spawned by that tool call.tool.execution_start belongs to the current assistant.message — unless it has data.parentToolCallId, in which case it is nested under that parent tool call.tool.execution_start with its tool.execution_complete by toolCallId.events.jsonl is the primary source and answers almost every question on its own. A *separate* log captures the transport/protocol between VS Code and the agent host process — the JSON-RPC-style frames that drive sessions.
Use the wire log only when the symptom points at the agent host / transport itself, not the model or a tool. Reach for it when:
createSession / subscribe fails, or expected updates/notifications never arrive;Do not open it for ordinary "why did the model/tool do X" questions — events.jsonl already answers those. If events.jsonl fully explains the behavior, stop there and don't read the wire log.
It is written by VS Code on the _client_ machine, so it is only reachable for a _local_ agent host (VS Code and the agent on the same machine). For a remote agent host it lives on the client, not the host this skill runs on — skip it there.
<VS Code user-data dir>/logs/<session-timestamp>/ahp/ahp-<timestamp>-<connectionId>.jsonl
The VS Code user-data dir depends on the build:
%APPDATA%\Code (Insiders: Code - Insiders; OSS/dev may use Code - OSS or a custom --user-data-dir)~/Library/Application Support/Code~/.config/CodeA new logs/<timestamp>/ folder is created per VS Code session — pick the most recently modified ahp/ahp-*.jsonl.
Named by _connection_ id, not session id — one log multiplexes all sessions on that connection (no per-session file). Long connections rotate into ahp-….1.jsonl, .2.jsonl, … (max 5); the active one is the most recently modified (the locate commands pick it). Read the newest log for connection-level health; to isolate one session, filter by its id (the session-state/<sessionId>/ folder name, which appears in frames' params/result).
If no ahp/ folder exists, the wire log is disabled (it is off by default). Tell the user to set "chat.agentHost.ahpJsonlLoggingEnabled": true, restart VS Code, reproduce the issue, then re-run.
Each line is a JSON-RPC frame plus an _ahpLog envelope:
{ "jsonrpc": "2.0", "id": 12, "method": "createSession", "params": {}, "_ahpLog": { "ts": "ISO-8601", "dir": "c2s", "connectionId": "…", "transport": "local" } }
_ahpLog.dir — direction: c2s = VS Code → host (requests/notifications), s2c = host → VS Code (results/errors/actions/notifications)._ahpLog.transport — local / websocket / ssh / wsl.method + params; responses carry result or error — pair a response to its request by id._ahpLog.truncated: true marks frames whose large payloads were elided.Triage: look for error frames, requests (c2s with an id) that have no matching s2c response (hangs/timeouts), or a missing s2c after createSession / subscribe. Apply the same streaming / jq / node rules as for events.jsonl.
ls -t ~/Library/Application\ Support/Code*/logs/*/ahp/ahp-*.jsonl ~/.config/Code*/logs/*/ahp/ahp-*.jsonl 2>/dev/null | head -n 1Get-ChildItem "$env:APPDATA\Code*\logs\*\ahp\ahp-*.jsonl" -ErrorAction SilentlyContinue | Sort-Object LastWriteTime -Descending | Select-Object -First 1The event log is outside the workspace, so grep_search cannot read it. Use run_in_terminal.
Each terminal command is a separate round-trip, so don't run one command per question. Do one streaming pass (constant memory — events.jsonl can be hundreds of MB, so never load it whole) that prints, in a single read:
tool.execution_start with its tool.execution_complete by toolCallId),Use a streaming jq program (macOS/Linux) or a streaming Node readline pass (Windows) — the per-query examples below are the building blocks. Then run targeted follow-ups only to drill in.
grep / jq)ls -t "${XDG_STATE_HOME:-$HOME}"/.copilot/session-state/*/events.jsonl | head -n 1ls -lh <logPath>grep '"success":false' <logPath> (tool failures) and grep '"type":"tool.execution_complete"' <logPath>jq -nr 'reduce inputs as $i ({}; .[$i.type] += 1) | to_entries | sort_by(-.value)[] | "\(.value) \(.key)"' <logPath>jq -c 'select(.type=="tool.execution_start") | {tool:.data.toolName, id:.data.toolCallId}' <logPath>jq -c 'select(.type=="user.message") | .data.content' <logPath>jq -c 'select(.type=="assistant.message") | {model:.data.model, out:.data.outputTokens}' <logPath>Do not parse the log with PowerShell JSON. Get-Content … | ForEach-Object { ConvertFrom-Json } (or any per-line ConvertFrom-Json) deserializes one object at a time and is slow even on small logs. Use Select-String for plain-text matches and a streaming node pass for anything that needs JSON. Use jq instead if it is available.
$stateHome = if ($env:XDG_STATE_HOME) { $env:XDG_STATE_HOME } else { $HOME }
Get-ChildItem (Join-Path $stateHome '.copilot\session-state\*\events.jsonl') | Sort-Object LastWriteTime -Descending | Select-Object -First 1
(Get-Item <logPath>).LengthSelect-String '"success":false' <logPath>node pass (constant memory, safe for hundreds-of-MB logs; pass the log path as an argument so it stays quote-safe). Use this shape and change only the per-line if (…) to select what you need (tool failures, tool calls, assistant turns, …):node -e "const rl=require('readline').createInterface({input:require('fs').createReadStream(process.argv[1])});rl.on('line',l=>{if(!l)return;let x;try{x=JSON.parse(l)}catch{return}if(x.type==='tool.execution_complete'&&x.data.success===false)console.log(x.data.toolCallId,JSON.stringify(x.data.result))})" "<logPath>"
For aggregates (e.g. counts by type), accumulate into an object as you go and print it on rl.on('close', …).
Select-String first to find the line, then read that small slice with read_file.readFileSync / Get-Content) — stream (jq / the readline pass) or narrow with grep / Select-String first. Check size first if unsure (ls -lh / (Get-Item).Length).ConvertFrom-Json in a loop) — slow. Use one jq filter or one node pass.read_file only for small targeted ranges, never an entire log.events.jsonl and run the single triage pass.success:false; latency → gaps between assistant.turn_start and its assistant.message, or a tool's start↔complete; tool / model / input → the relevant event type; agent-host / transport failure (local) → also the Wire Log.Cover what happened and why (root cause), key evidence (paraphrased — quote only a telling detail like an error message, never raw dumps), and how to fix it. A short combined explanation is fine; add structure (headers, bullets, tables) only for complex, multi-factor issues. Keep paragraphs short.
Abstraction: don't narrate your investigation or use internal terms (events.jsonl, tool.execution_start, parentId, "envelope"). Refer to "the session log" abstractly. Focus on what happened and why, not how you found it.
Example — instead of "I read events.jsonl and saw a tool.execution_complete with success:false…", say:
> The "testing" skill was found but not loaded because the folder name and the name in the skill file differ (testing vs testing2). Fix: make them match (rename the folder or change the name), then start a new session.
run_in_terminal; never grep_search, and never read_file a whole (possibly huge) log — narrow first, then read small ranges.Take microsoft/vscode-troubleshoot from the repository into ~/.claude/skills for personal
use, or into .claude/skills inside a project.
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.