troubleshoot

tarafından microsoft

Investigate unexpected chat agent behavior by analyzing direct debug logs in JSONL files. Use when users ask why something happened, why a request was slow,…

npx skills add https://github.com/microsoft/vscode --skill troubleshoot

Troubleshoot

Purpose

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:

  • Why did this request take so long?
  • Why was a tool called (or not called)?
  • Why did an instruction/skill/agent file not load?
  • Why did a tool call fail?
  • Why did the model not follow expectations?

Base every conclusion on evidence from the event log. Do not guess.

Locating the Session Log

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.

  1. If a 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.
  2. Sticky reference: if no path is on the current message but an earlier turn in this conversation already established a target (a 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.
  3. Otherwise, self-discover it: pick the most recently modified 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.
  4. If none exists there, this isn't a Copilot CLI session — tell the user the skill supports Copilot CLI sessions only, and stop.

Data Source — events.jsonl

Each 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.

Event types (data.* fields)

  • session.start (once) — selectedModel, reasoningEffort.
  • user.messagecontent; transformedContent (expanded prompt, when it differs).
  • assistant.turn_start / assistant.turn_endturnId; measure a turn's duration vs the following assistant.message.
  • assistant.messagemodel, outputTokens, content, reasoningText (thinking, when present), turnId, parentToolCallId (set ⇒ sub-agent turn spawned by that tool call).
  • tool.execution_starttoolName, toolCallId, arguments, parentToolCallId (set ⇒ nested/sub-agent).
  • tool.execution_completetoolCallId (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.
  • Other (hook.*, permission.*, system.message) — inspect data as needed.

Reconstructing the flow

Iterate records in order and rebuild the logical tree from context:

  • session.start is the root.
  • a user.message begins a turn.
  • an 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.
  • a 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.
  • pair each tool.execution_start with its tool.execution_complete by toolCallId.

Secondary Source — Agent Host Wire Log (protocol communication)

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:

  • the agent host won't start, or the session never begins;
  • requests hang or time out, or the agent appears stuck "connecting" / unresponsive;
  • createSession / subscribe fails, or expected updates/notifications never arrive;
  • you see RPC / protocol / connection errors.

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.

Location

<VS Code user-data dir>/logs/<session-timestamp>/ahp/ahp-<timestamp>-<connectionId>.jsonl

The VS Code user-data dir depends on the build:

  • Windows: %APPDATA%\Code (Insiders: Code - Insiders; OSS/dev may use Code - OSS or a custom --user-data-dir)
  • macOS: ~/Library/Application Support/Code
  • Linux: ~/.config/Code

A 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.

Format

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.transportlocal / websocket / ssh / wsl.
  • Requests/notifications carry 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.

Locating it (terminal)

  • macOS/Linux: ls -t ~/Library/Application\ Support/Code*/logs/*/ahp/ahp-*.jsonl ~/.config/Code*/logs/*/ahp/ahp-*.jsonl 2>/dev/null | head -n 1
  • Windows (PowerShell): Get-ChildItem "$env:APPDATA\Code*\logs\*\ahp\ahp-*.jsonl" -ErrorAction SilentlyContinue | Sort-Object LastWriteTime -Descending | Select-Object -First 1

Tooling Strategy (important)

The event log is outside the workspace, so grep_search cannot read it. Use run_in_terminal.

Do a single triage pass first (important for efficiency)

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:

  • the event counts by type,
  • every tool call with success/failure and duration (pair each tool.execution_start with its tool.execution_complete by toolCallId),
  • the user messages.

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.

macOS / Linux / WSL / Git Bash (grep / jq)

  • Locate the newest log: ls -t "${XDG_STATE_HOME:-$HOME}"/.copilot/session-state/*/events.jsonl | head -n 1
  • Check size first: ls -lh <logPath>
  • Errors: grep '"success":false' <logPath> (tool failures) and grep '"type":"tool.execution_complete"' <logPath>
  • Count events by type (streaming): jq -nr 'reduce inputs as $i ({}; .[$i.type] += 1) | to_entries | sort_by(-.value)[] | "\(.value) \(.key)"' <logPath>
  • Tool calls: jq -c 'select(.type=="tool.execution_start") | {tool:.data.toolName, id:.data.toolCallId}' <logPath>
  • User messages: jq -c 'select(.type=="user.message") | .data.content' <logPath>
  • Assistant turns: jq -c 'select(.type=="assistant.message") | {model:.data.model, out:.data.outputTokens}' <logPath>

Windows (PowerShell + Node.js)

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.

  • Locate the newest log:
    $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
    
  • Check size first: (Get-Item <logPath>).Length
  • Plain-text matches (fast, streaming, no parsing): Select-String '"success":false' <logPath>
  • JSON queries — a streaming 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', …).
  • For a very large log, narrow with Select-String first to find the line, then read that small slice with read_file.

General rules

  • One streaming pass, not many. Each command is a round-trip; prefer the single triage pass, then targeted follow-ups. Never load a whole (hundreds-of-MB) log into memory (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).
  • Never deserialize JSON line-by-line in the shell (PowerShell ConvertFrom-Json in a loop) — slow. Use one jq filter or one node pass.
  • Use read_file only for small targeted ranges, never an entire log.

Investigation Workflow

  1. Locate events.jsonl and run the single triage pass.
  2. Match the symptom: errorssuccess: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.
  3. Read only the relevant slices, then determine the most likely root cause (order contributing factors by impact) and give concrete next steps.

Response Guidelines

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.

Important Rules

  • Base every claim on log evidence — never assume causality.
  • Search via run_in_terminal; never grep_search, and never read_file a whole (possibly huge) log — narrow first, then read small ranges.
  • If no Copilot CLI session log exists, say so and stop.