Path: blob/main/extensions/copilot/src/platform/chat/common/chatDebugFileLoggerService.ts
13401 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import { createServiceIdentifier } from '../../../util/common/services';6import { decodeBase64 } from '../../../util/vs/base/common/buffer';7import { Event } from '../../../util/vs/base/common/event';8import { URI } from '../../../util/vs/base/common/uri';910export const IChatDebugFileLoggerService = createServiceIdentifier<IChatDebugFileLoggerService>('IChatDebugFileLoggerService');1112/**13* Extract the chat session ID string from a session resource URI.14*15* - `vscode-chat-session://local/<base64EncodedSessionId>` — decodes base6416* - `copilotcli:///<sessionId>` and `claude-code:///<sessionId>` — uses raw path segment17*/18export function sessionResourceToId(sessionResource: URI): string {19const pathSegment = sessionResource.path.replace(/^\//, '').split('/').pop() || '';20if (!pathSegment) {21return pathSegment;22}23// Only vscode-chat-session URIs use base64-encoded session IDs24if (sessionResource.scheme === 'vscode-chat-session') {25try {26return new TextDecoder().decode(decodeBase64(pathSegment).buffer);27} catch {28// Not valid base64 — fall through to raw segment29}30}31return pathSegment;32}3334/**35* Service that writes chat debug events (OTel spans + discovery events) to36* per-session JSONL files on disk. These files can be read by skills,37* subagents, etc via `read_file` tool to diagnose chat issues.38*/39export interface IChatDebugFileLoggerService {40readonly _serviceBrand: undefined;4142/**43* Begin logging for a session. Registers the session in memory;44* directory creation and file writes are deferred to the first flush.45*/46startSession(sessionId: string): Promise<void>;4748/**49* Register a child session that should be written under a parent session's directory.50* Call this before any spans arrive for the child session to ensure51* correct routing of all events (including tool calls that may arrive52* before the child's invoke_agent span completes).53*/54startChildSession(childSessionId: string, parentSessionId: string, label: string, parentToolSpanId?: string): void;5556/**57* Register a span ID → session ID mapping so that child spans58* (e.g. hooks) are routed to the correct session before the59* parent span completes.60*/61registerSpanSession(spanId: string, sessionId: string): void;6263/**64* End logging for a session. Performs a final flush and removes the65* session from the active set.66*/67endSession(sessionId: string): Promise<void>;6869/**70* Flush any buffered entries to disk for the given session.71*/72flush(sessionId: string): Promise<void>;7374/**75* Get the URI of the debug logs directory, or undefined if it cannot be76* determined (e.g. no workspace, or an error occurs). The directory may77* not actually exist on disk yet if no sessions have been started.78*/79readonly debugLogsDir: URI | undefined;8081/**82* Get the URI of the debug log file for a session, or undefined if the83* session has not been started.84*/85getLogPath(sessionId: string): URI | undefined;8687/**88* Get the session directory URI for a session. For both parent and child89* sessions this returns the parent session's directory90* (e.g. `debug-logs/<parentSessionId>/`).91*/92getSessionDir(sessionId: string): URI | undefined;9394/**95* Returns the session IDs of all currently active logging sessions.96*/97getActiveSessionIds(): string[];9899/**100* Check whether a URI is under the debug-logs storage directory.101* Used by {@link assertFileOkForTool} to allowlist tool reads.102*/103isDebugLogUri(uri: URI): boolean;104105/**106* Convenience method: decode a session resource URI and return the107* session directory, or `undefined` if the session is unknown.108*/109getSessionDirForResource(sessionResource: URI): URI | undefined;110111/**112* Cache the latest model list snapshot from the API. The data is written113* as `models.json` into each session directory when a session starts.114*/115setModelSnapshot(models: readonly unknown[]): void;116117/**118* Fired synchronously when an entry is buffered, before it is flushed to disk.119* Subscribers receive the entry in real-time for live streaming.120*/121readonly onDidEmitEntry: Event<{ sessionId: string; entry: IDebugLogEntry }>;122123/**124* Read all entries for a session from disk + unflushed buffer.125* Returns an empty array if the session has no log file yet.126*/127readEntries(sessionId: string): Promise<IDebugLogEntry[]>;128129/**130* Read the last `count` entries from a session's JSONL file + unflushed buffer.131* Reads only the tail of the file for performance on large files.132*/133readTailEntries(sessionId: string, count: number): Promise<IDebugLogEntry[]>;134135/**136* Stream entries from a session's JSONL file line by line.137* Calls `onEntry` for each parsed entry. Returns when all entries have been streamed.138* Uses a streaming parser to avoid loading the entire file into memory.139*/140streamEntries(sessionId: string, onEntry: (entry: IDebugLogEntry) => void): Promise<void>;141142/**143* List session IDs that have debug log directories on disk.144* Returns both active and historical sessions found in the debug-logs/ directory.145*/146listSessionIds(): Promise<string[]>;147}148149/**150* A single JSONL debug log entry — the canonical debug event format.151*/152export interface IDebugLogEntry {153/** Schema version. Absent or 1 = current schema. Bump on breaking changes. */154readonly v?: number;155/** Run index within a session. 0 (or absent) = first run; incremented on each VS Code restart that resumes the same session. */156readonly rIdx?: number;157/** Epoch ms timestamp */158readonly ts: number;159/** Duration in ms (0 for instant events) */160readonly dur: number;161/** Chat session ID */162readonly sid: string;163/** Event type */164readonly type: 'session_start' | 'tool_call' | 'llm_request' | 'user_message' | 'agent_response' | 'subagent' | 'discovery' | 'error' | 'generic' | 'child_session_ref' | 'hook' | 'turn_start' | 'turn_end';165/** Descriptive name */166readonly name: string;167/** Span or event ID */168readonly spanId: string;169/** Parent span ID for hierarchy */170readonly parentSpanId?: string;171/** Status */172readonly status: 'ok' | 'error';173/** Type-specific attributes */174readonly attrs: Record<string, string | number | boolean | undefined>;175}176177/**178* No-op implementation for testing and environments without workspace storage.179*/180export class NullChatDebugFileLoggerService implements IChatDebugFileLoggerService {181declare readonly _serviceBrand: undefined;182183async startSession(): Promise<void> { }184startChildSession(): void { }185registerSpanSession(): void { }186async endSession(): Promise<void> { }187async flush(): Promise<void> { }188getLogPath(_sessionId?: string): URI | undefined { return undefined; }189getSessionDir(_sessionId?: string): URI | undefined { return undefined; }190getActiveSessionIds(): string[] { return []; }191isDebugLogUri(): boolean { return false; }192getSessionDirForResource(): URI | undefined { return undefined; }193setModelSnapshot(): void { }194readonly debugLogsDir: URI | undefined = undefined;195readonly onDidEmitEntry: Event<{ sessionId: string; entry: IDebugLogEntry }> = Event.None;196async readEntries(): Promise<IDebugLogEntry[]> { return []; }197async readTailEntries(): Promise<IDebugLogEntry[]> { return []; }198async streamEntries(): Promise<void> { }199async listSessionIds(): Promise<string[]> { return []; }200}201202203