Hook System
How ContextManager captures AI interactions from VS Code Copilot’s native transcript pipeline.
Overview
VS Code provides an Agent Hooks API (Preview) that fires lifecycle events during AI interactions. ContextManager uses these hooks to capture interactions from VS Code Copilot’s native transcript pipeline — primarily interactions that wouldn’t otherwise be visible to the extension.
The hook system consists of two halves:
capture.ps1- PowerShell script that runs as a hook handler, writes to a queue fileHookWatcher- TypeScript service in the extension that watches the queue file and processes entries
Hook Script (capture.ps1)
Located at ~/.contextmanager/scripts/capture.ps1 (also bundled at resources/hooks/capture.ps1).
Hook Events Handled
| Event | When | What capture.ps1 Does |
|---|---|---|
UserPromptSubmit | Before every prompt | Reads session-context.txt, injects as additional context |
SubagentStart | When a sub-agent spawns | Reads session-context.txt, injects as additional context |
PostToolUse | After each tool call | Records tool use; also harvests completed turns from transcript |
PreCompact | Before context summarization | Extracts all turns since last offset for multi-turn processing |
Stop | Session ends | Records final exchange from transcript |
Data Flow
graph TD
A[VS Code Hook Event] --> B[capture.ps1 receives JSON via stdin]
B --> C[Process based on hookEventName]
C --> D[Append entry to hook-queue.jsonl]
C --> E[UserPromptSubmit: output JSON to stdout]
style A fill:#1f6feb,stroke:#388bfd,color:#fff
style D fill:#1158c7,stroke:#388bfd,color:#fff
style E fill:#238636,stroke:#3fb950,color:#fff
PostToolUse - Transcript Harvesting
Since the Stop hook doesn’t reliably fire for VS Code Copilot, PostToolUse does double duty:
- Records the tool execution itself
- Scans the Copilot transcript for completed user+assistant turns
- If a new completed turn is found (ID-based dedup), writes a synthetic
Stopentry
# Transcript location
%APPDATA%\Code\User\workspaceStorage\<wsId>\GitHub.copilot-chat\transcripts\<sessionId>.jsonl
# Transcript format (VS Code Copilot)
{"type":"user.message","data":{"content":"..."},"id":"uuid"}
{"type":"tool.execution_start","data":{"toolCallId":"...","toolName":"read_file","arguments":{...}}}
{"type":"tool.execution_complete","data":{"toolCallId":"...","success":true}}
{"type":"assistant.message","data":{"content":"final response text","toolRequests":[...]}}
Tool Call Capture
Get-LastCompletedTurn collects tool.execution_start entries between user and assistant messages:
- Capped at 10 tool calls per turn (token budget control)
- Input truncated to 200 chars per call
- Tool output is not available in the transcript (empty string)
The result structure:
{
"userId": "uuid",
"user": "user prompt text",
"assistant": "assistant response text",
"toolCalls": [
{ "toolName": "read_file", "input": "{\"filePath\":\"src/auth.ts\",\"startLine\":1,...}", "output": "" },
{ "toolName": "grep_search", "input": "{\"query\":\"handleAuth\",...}", "output": "" }
]
}
HookWatcher (TypeScript)
Located at src/hooks/HookWatcher.ts.
File Watching
// Watch the queue file for changes
fs.watch(QUEUE_FILE, () => {
// 400ms debounce
setTimeout(() => this._processQueue(), 400);
});
Entry Processing
interface HookEntry {
hookType: string; // "Stop" | "PostToolUse" | "PreCompact" | "SessionStart"
timestamp: number;
sessionId?: string;
participant?: string;
prompt?: string;
response?: string;
toolName?: string;
toolInput?: unknown;
toolResponse?: string;
toolCalls?: Array<{ toolName: string; input: string; output: string }>;
turns?: Array<{ user: string; assistant: string }>;
}
Routing
| hookType | Action |
|---|---|
Stop | autoCapture.onModelResponse() + _queueCardCandidate() |
PostToolUse | Observation recording with tool detail extraction |
PreCompact | Multi-turn extraction via extractMultiTurnLearnings() + auto-distill |
Offset Management
The watcher tracks its position in the queue file by byte offset:
graph TD
A[Read queue file] --> B[Slice from lastOffset to end]
B --> C[Parse new lines]
C --> D[Process entries]
D --> E[Advance offset by byte length]
E --> F[Persist offset to .queue-offset file]
style A fill:#1f6feb,stroke:#388bfd,color:#fff
style D fill:#1158c7,stroke:#388bfd,color:#fff
style F fill:#238636,stroke:#3fb950,color:#fff
This ensures crash-safe resumption - entries are never processed twice or lost.
Session Context
The HookWatcher writes ~/.contextmanager/session-context.txt whenever the active project changes or the prompt injection settings are updated. The UserPromptSubmit hook reads this file and injects it as a system message into every prompt.
Contents include:
- Project name and root path
- Selected knowledge cards — cards checked in the Knowledge tab (
selectedCardIds), with titles and optionally full content - Custom instruction — free-form text from the dashboard injection section
Card selection for hook injection is driven by the same checkboxes used in the Knowledge tab — there is a single, unified selection system. Archived cards are automatically excluded.
Dual-Format Support
The transcript parser handles two formats:
| Format | Source | User Type | Assistant Type | Content Path |
|---|---|---|---|---|
| Claude Code | Claude Code sessions | user | assistant | message.content |
| VS Code Copilot | Copilot Chat | user.message | assistant.message | data.content |
Both formats are handled in Get-LastExchange, Get-LastCompletedTurn, and Get-AllTurnsSinceOffset.
Versioning
The capture script includes a cm-version header:
| Version | Changes | |:——–|:——–| | 1 | Initial: basic Stop/PostToolUse handling | | 2 | Added PreCompact multi-turn, dual-format support | | 3 | Added PostToolUse transcript harvesting, ID-based dedup | | 4 | Added tool call capture (tool.execution_start) | | 5 | SessionStart → UserPromptSubmit, hookSpecificOutput wrapper | | 6 | Added SubagentStart hook for sub-agent context injection | | 7 | Input truncation raised to 2000 chars, reasoningText fallback for assistant messages | | 8 | Input truncation raised to 2000 chars, reasoningText fallback for assistant messages | | 9 | UserPromptSubmit: hookSpecificOutput.additionalContext instead of systemMessage | The extension checks the installed version and updates the script if a newer version is bundled.
Debugging
Stop Debug Log
~/.contextmanager/stop-debug.log
Contains timestamped entries for every Stop hook invocation:
- Keys received in the event data
- Transcript path (provided or auto-discovered)
- Exchange content (truncated to 80 chars)
HookWatcher Debug Logging
Console output with [HookWatcher:DEBUG] and [HookWatcher/CardQueue:DEBUG] prefixes:
- Queue processing: line count, offset
- Entry processing: hookType, participant, prompt/response length, tool call count
- Card queue decisions: enabled, participant filter, response length threshold