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