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:

  1. capture.ps1 - PowerShell script that runs as a hook handler, writes to a queue file
  2. HookWatcher - 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:

  1. Records the tool execution itself
  2. Scans the Copilot transcript for completed user+assistant turns
  3. If a new completed turn is found (ID-based dedup), writes a synthetic Stop entry
# 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

Next Steps

Configuration →