src/vs/platform/agentHost/common/sessionDataService.ts

421 LOC · 421 covered · 0 uncovered · 1 ranges · 2092 concepts · 1 introducers · 976 tests

File neighbourhood

The centred file is linked to every concept that introduces one of its ranges, every test that runs code from the file, and the gray connector concepts standing between those tests and the file's own introducer concepts. Undirected links join concepts to every file where they introduce source and concepts to the tests they introduce; arrows show specialization between the displayed concepts and bridge only concepts omitted from this view. Concept colors match the source ranges below; connector concepts have no source color and are shown in gray.

Focused file, its introducer and connector concepts, their introduced files, and tests that run code from the file

In the embedded map, ordinary wheel input scrolls the page; use the visible controls to zoom and drag to pan. Open the full-screen map for canvas navigation: wheel pans, Ctrl/Command plus wheel zooms, and arrow keys pan when this region is focused. On touch screens, open the full-screen map to pan or pinch. If JavaScript or WebGL is unavailable, use the related-file, concept, and source links on this page.

Graph controls are ready.

Interactive rendering requires JavaScript and WebGL. Use the related-file, concept, and source links on this page while the interactive map is unavailable.

1 > /*--------------------------------------------------------------------------------------------- sessionDataService.ts ×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 { IDisposable, IReference } from '../../../base/common/lifecycle.js';
7 > import { URI } from '../../../base/common/uri.js';
8 > import { createDecorator } from '../../instantiation/common/instantiation.js';
9 > import { Event } from '../../../base/common/event.js';
10 > import type { FileEditKind, Message } from './state/sessionState.js';
11 >
12 > export const ISessionDataService = createDecorator<ISessionDataService>('sessionDataService');
13 >
14 > /** Filename of the per-session SQLite database. */
15 > export const SESSION_DB_FILENAME = 'session.db';
16 >
17 > /**
18 > * Subdirectory under a session's data directory that holds snapshotted
19 > * user-message attachments (e.g. pasted images, fetched file references).
20 > * The agent host writes these on dispatch so large blobs stay out of the
21 > * in-memory state tree, and reads of files under this directory are
22 > * auto-approved by the agent's permission flow.
23 > */
24 > export const SESSION_ATTACHMENTS_DIRNAME = 'attachments';
25 >
26 > // ---- File-edit types ----------------------------------------------------
27 >
28 > /**
29 > * Lightweight metadata for a file edit. Returned by {@link ISessionDatabase.getFileEdits}
30 > * without the (potentially large) file content blobs.
31 > */
32 > export interface IFileEditRecord {
33 > /** The turn that owns this file edit. */
34 > turnId: string;
35 > /** The tool call that produced this edit. */
36 > toolCallId: string;
37 > /** Primary file path (after-path for edits/creates/renames, before-path for deletes). */
38 > filePath: string;
39 > /** The kind of file operation. */
40 > kind: FileEditKind;
41 > /** For renames, the original file path before the move. */
42 > originalPath?: string;
43 > /** Number of lines added (informational, for diff metadata). */
44 > addedLines: number | undefined;
45 > /** Number of lines removed (informational, for diff metadata). */
46 > removedLines: number | undefined;
47 > }
48 >
49 > /**
50 > * The before/after content blobs for a single file edit.
51 > * Retrieved on demand via {@link ISessionDatabase.readFileEditContent}.
52 > *
53 > * For creates, `beforeContent` is absent.
54 > * For deletes, `afterContent` is absent.
55 > */
56 > export interface IFileEditContent {
57 > /** File content before the edit. Absent for file creations. */
58 > beforeContent?: Uint8Array;
59 > /** File content after the edit. Absent for file deletions. */
60 > afterContent?: Uint8Array;
61 > }
62 >
63 > // ---- Reviewed-file types ------------------------------------------------
64 >
65 > /**
66 > * A record of a file having been reviewed by the user at a specific content
67 > * nonce. Returned by {@link ISessionDatabase.getReviewedFiles} and
68 > * {@link ISessionDatabase.getReviewedFilesForUri}.
69 > */
70 > export interface IReviewedFileRecord {
71 > /** The reviewed file. */
72 > uri: URI;
73 > /** Content version/hash captured at review time. */
74 > nonce: string;
75 > }
76 >
77 > // ---- Session database ---------------------------------------------------
78 >
79 > /**
80 > * A host-injected ("local") turn: a completed protocol `Turn` the agent SDK
81 > * never saw — e.g. the `/rename` acknowledgement or a `!command` terminal run.
82 > * These are persisted separately from SDK turns so they survive reload, and are
83 > * interleaved back into the SDK-derived turns on restore.
84 > */
85 > export interface ILocalTurnRecord {
86 > /** The local turn's id (matches the payload `Turn.id`). */
87 > turnId: string;
88 > /** The chat this local turn belongs to (its channel URI string). */
89 > chatUri: string;
90 > /**
91 > * Id of the preceding concrete (SDK-backed) turn this local turn is
92 > * anchored after, or `undefined` when it precedes any real turn.
93 > */
94 > anchorTurnId: string | undefined;
95 > /** Monotonic ordering among local turns (used to interleave on restore). */
96 > seq: number;
97 > /** JSON-serialized protocol `Turn`. */
98 > payload: string;
99 > }
100 >
101 >
102 > /**
103 > * A disposable handle to a per-session SQLite database backed by
104 > * `@vscode/sqlite3`.
105 > *
106 > * Callers obtain an instance via {@link ISessionDataService.openDatabase} and
107 > * **must** dispose it when finished to close the underlying database connection.
108 > */
109 > export interface ISessionDatabase extends IDisposable {
110 > /**
111 > * Create a turn record. Must be called before storing file edits that
112 > * reference this turn.
113 > */
114 > createTurn(turnId: string): Promise<void>;
115 >
116 > /**
117 > * Delete a turn and all of its associated file edits (cascade).
118 > */
119 > deleteTurn(turnId: string): Promise<void>;
120 >
121 > /**
122 > * Associates a Copilot SDK event ID with a turn. The event ID corresponds
123 > * to the `user.message` event in the SDK event stream and is used by
124 > * the SDK's `history.truncate` and `sessions.fork` RPCs.
125 > */
126 > setTurnEventId(turnId: string, eventId: string): Promise<void>;
127 >
128 > /**
129 > * Retrieves the SDK event ID previously stored for a turn.
130 > * Returns `undefined` if no event ID has been set.
131 > */
132 > getTurnEventId(turnId: string): Promise<string | undefined>;
133 >
134 > /**
135 > * Returns the SDK event ID of the turn inserted immediately after the
136 > * given turn, or `undefined` if the given turn is the last one.
137 > */
138 > getNextTurnEventId(turnId: string): Promise<string | undefined>;
139 >
140 > /**
141 > * Returns the SDK event ID of the earliest turn in insertion order,
142 > * or `undefined` if there are no turns.
143 > */
144 > getFirstTurnEventId(): Promise<string | undefined>;
145 >
146 > /**
147 > * Associates a git checkpoint ref (e.g. `refs/agents/<sid>/checkpoints/turn/N`)
148 > * with a turn. Idempotent — last writer wins per turn.
149 > */
150 > setTurnCheckpointRef(turnId: string, ref: string): Promise<void>;
151 >
152 > /**
153 > * Retrieves the checkpoint ref previously stored for a turn, or
154 > * `undefined` if none.
155 > */
156 > getTurnCheckpointRef(turnId: string): Promise<string | undefined>;
157 >
158 > /**
159 > * Returns the checkpoint ref of the most recent turn (in insertion
160 > * order) prior to `turnId` that has a non-null `checkpoint_ref`.
161 > * Used to resolve the parent checkpoint for end-of-turn diffs without
162 > * persisting an explicit parent column.
163 > */
164 > getPreviousCheckpointRef(turnId: string): Promise<string | undefined>;
165 >
166 > /**
167 > * Returns every non-null `checkpoint_ref` recorded against any turn in
168 > * this session. Used by checkpoint cleanup to enumerate refs precisely
169 > * (rather than scanning `for-each-ref` on the underlying repo).
170 > */
171 > getAllCheckpointRefs(): Promise<string[]>;
172 >
173 > /**
174 > * Deletes the given turn and all turns inserted after it, along
175 > * with their associated file edits (cascade).
176 > */
177 > truncateFromTurn(turnId: string): Promise<void>;
178 >
179 > /**
180 > * Deletes all turns inserted after the given turn (but keeps the
181 > * given turn itself). Associated file edits cascade-delete.
182 > */
183 > deleteTurnsAfter(turnId: string): Promise<void>;
184 >
185 > /**
186 > * Deletes all turns and their associated file edits.
187 > */
188 > deleteAllTurns(): Promise<void>;
189 >
190 > // ---- Local (host-injected) turns -------------------------------------
191 >
192 > /**
193 > * Persist a host-injected local turn (e.g. `/rename` or `!command`).
194 > * Replaces any existing record with the same `turnId`.
195 > */
196 > insertLocalTurn(record: ILocalTurnRecord): Promise<void>;
197 >
198 > /**
199 > * Retrieve all persisted local turns in this session, in `seq` order.
200 > * Callers filter by {@link ILocalTurnRecord.chatUri} for a given chat.
201 > */
202 > getLocalTurns(): Promise<ILocalTurnRecord[]>;
203 >
204 > /**
205 > * Delete the local turns with the given ids. Ids not present are ignored.
206 > */
207 > deleteLocalTurns(turnIds: readonly string[]): Promise<void>;
208 >
209 > /**
210 > * Store a file-edit snapshot (metadata + content) for a tool invocation
211 > * within a turn.
212 > *
213 > * If a record for the same `toolCallId` and `filePath` already exists
214 > * it is replaced.
215 > */
216 > storeFileEdit(edit: IFileEditRecord & IFileEditContent): Promise<void>;
217 >
218 > /**
219 > * Retrieve file-edit metadata for the given tool call IDs.
220 > * Content blobs are **not** included — use {@link readFileEditContent}
221 > * to fetch them on demand. Results are returned in insertion order.
222 > */
223 > getFileEdits(toolCallIds: string[]): Promise<IFileEditRecord[]>;
224 >
225 > /**
226 > * Retrieve file-edit metadata for all edits in this session.
227 > * Content blobs are **not** included — use {@link readFileEditContent}
228 > * to fetch them on demand. Results are returned in insertion order.
229 > */
230 > getAllFileEdits(): Promise<IFileEditRecord[]>;
231 >
232 > /**
233 > * Retrieve file-edit metadata for all edits belonging to a specific turn.
234 > * Content blobs are **not** included — use {@link readFileEditContent}
235 > * to fetch them on demand. Results are returned in insertion order.
236 > */
237 > getFileEditsByTurn(turnId: string): Promise<IFileEditRecord[]>;
238 >
239 > /**
240 > * Read the before/after content blobs for a single file edit.
241 > * Returns `undefined` if no edit exists for the given key.
242 > */
243 > readFileEditContent(toolCallId: string, filePath: string): Promise<IFileEditContent | undefined>;
244 >
245 > // ---- Session metadata ------------------------------------------------
246 >
247 > /**
248 > * Read a metadata value by key.
249 > * Returns `undefined` if no value has been stored for the key.
250 > */
251 > getMetadata(key: string): Promise<string | undefined>;
252 >
253 > /**
254 > * Gets a bulk of metadata. For example `getMetadataObject({ foo: true }) -> { foo: 'data' }`
255 > */
256 > getMetadataObject<T extends Record<string, unknown>>(obj: T): Promise<{ [K in keyof T]: string | undefined }>;
257 >
258 > /**
259 > * Store a metadata key-value pair. Overwrites any existing value for the key.
260 > */
261 > setMetadata(key: string, value: string): Promise<void>;
262 >
263 > /**
264 > * Store or clear the draft for a chat in this session.
265 > */
266 > setChatDraft(chat: URI, draft: Message | undefined): Promise<void>;
267 >
268 > /**
269 > * Read the stored draft for a chat in this session.
270 > */
271 > getChatDraft(chat: URI): Promise<Message | undefined>;
272 >
273 > /**
274 > * Bulk-remaps turn IDs using the provided old→new mapping.
275 > * Used after copying a database file for a forked session.
276 > */
277 > remapTurnIds(mapping: ReadonlyMap<string, string>): Promise<void>;
278 >
279 > // ---- Reviewed files --------------------------------------------------
280 >
281 > /**
282 > * Mark a file (identified by URI + content nonce) as reviewed by the user.
283 > * Idempotent — re-marking the same `(uri, nonce)` pair is a no-op.
284 > */
285 > markFileReviewed(uri: URI, nonce: string): Promise<void>;
286 >
287 > /**
288 > * Remove the reviewed-file entry for the given URI + content nonce.
289 > * No-op if no such entry exists.
290 > */
291 > unmarkFileReviewed(uri: URI, nonce: string): Promise<void>;
292 >
293 > /**
294 > * Return every reviewed-file entry in this session, in insertion order.
295 > */
296 > getReviewedFiles(): Promise<IReviewedFileRecord[]>;
297 >
298 > /**
299 > * Return all reviewed-file entries for a specific URI (one per reviewed
300 > * content nonce), in insertion order.
301 > */
302 > getReviewedFilesForUri(uri: URI): Promise<IReviewedFileRecord[]>;
303 >
304 > /**
305 > * Return whether the given file has been reviewed at the given content nonce.
306 > */
307 > isFileReviewed(uri: URI, nonce: string): Promise<boolean>;
308 >
309 > /**
310 > * Creates a safe, consistent copy of the database at the given path
311 > * using SQLite's `VACUUM INTO` command.
312 > */
313 > vacuumInto(targetPath: string): Promise<void>;
314 >
315 > /**
316 > * Resolves once all in-flight write operations on this database have
317 > * settled. Used by graceful shutdown to flush fire-and-forget writes
318 > * before the process exits.
319 > */
320 > whenIdle(): Promise<void>;
321 >
322 > /**
323 > * Close the database connection. After calling this method, the object is
324 > * considered disposed and all other methods will reject with an error.
325 > */
326 > close(): Promise<void>;
327 > }
328 >
329 > /**
330 > * Provides persistent, per-session data directories on disk.
331 > *
332 > * Each session gets a directory under `{userDataPath}/agentSessionData/{sessionId}/`
333 > * where internal agent-host code can store arbitrary files (e.g. file snapshots).
334 > *
335 > * Directories are created lazily — callers should use {@link IFileService.createFolder}
336 > * before writing files. Cleanup happens eagerly on session removal and via startup
337 > * garbage collection for orphaned directories.
338 > */
339 > export interface ISessionDataService {
340 > readonly _serviceBrand: undefined;
341 >
342 > /**
343 > * Returns the root data directory URI for a session.
344 > * Does **not** create the directory on disk; callers use
345 > * `IFileService.createFolder()` as needed.
346 > */
347 > getSessionDataDir(session: URI): URI;
348 >
349 > /**
350 > * Returns the root data directory URI for a session given its raw ID.
351 > * Equivalent to {@link getSessionDataDir} but without requiring a full URI.
352 > */
353 > getSessionDataDirById(sessionId: string): URI;
354 >
355 > /**
356 > * Opens (or creates) a per-session SQLite database. The database file is
357 > * stored at `{sessionDataDir}/session.db`. Migrations are applied
358 > * automatically on first use.
359 > *
360 > * Returns a ref-counted reference. Multiple callers for the same session
361 > * share the same underlying connection. The connection is closed when
362 > * the last reference is disposed.
363 > */
364 > openDatabase(session: URI): IReference<ISessionDatabase>;
365 >
366 > /**
367 > * Opens an existing per-session database **only if the database file
368 > * already exists on disk**. Returns `undefined` when no database has
369 > * been created yet, avoiding the side effect of materializing empty
370 > * database files during read-only operations like listing sessions.
371 > */
372 > tryOpenDatabase(session: URI): Promise<IReference<ISessionDatabase> | undefined>;
373 >
374 > /**
375 > * Recursively deletes the data directory for a session, if it exists.
376 > */
377 > deleteSessionData(session: URI): Promise<void>;
378 >
379 > /**
380 > * Fires immediately before a session's data directory (and the
381 > * SQLite database within it) is deleted by {@link deleteSessionData}.
382 > *
383 > * Subscribers can register asynchronous cleanup work via
384 > * {@link IWillDeleteSessionDataEvent.waitUntil}; the deletion is
385 > * blocked until all registered promises settle. Used by
386 > * `IAgentHostCheckpointService.disposeSessionData` to read the exact
387 > * list of checkpoint refs from the (still-readable) database and
388 > * delete them before the directory is removed.
389 > *
390 > * Subscribers must own their own error handling — exceptions
391 > * propagated out of `waitUntil` promises are logged and ignored;
392 > * deletion proceeds regardless.
393 > */
394 > readonly onWillDeleteSessionData: Event<IWillDeleteSessionDataEvent>;
395 >
396 > /**
397 > * Deletes data directories that do not correspond to any known session.
398 > * Called at startup; safe to call multiple times.
399 > */
400 > cleanupOrphanedData(knownSessionIds: Set<string>): Promise<void>;
401 >
402 > /**
403 > * Resolves once all in-flight write operations across every currently
404 > * open per-session database have settled. Intended for graceful
405 > * shutdown — fire-and-forget writes (e.g. metadata persistence) would
406 > * otherwise be lost when the process exits.
407 > */
408 > whenIdle(): Promise<void>;
409 > }
410 >
411 > /**
412 > * Payload of {@link ISessionDataService.onWillDeleteSessionData}.
413 > */
414 > export interface IWillDeleteSessionDataEvent {
415 > readonly session: URI;
416 > /**
417 > * Register an asynchronous task that must settle before the session's
418 > * data directory is removed.
419 > */
420 > waitUntil(promise: Promise<unknown>): void;
421 > }