sessionDataService.ts ×1

Frontier kind: Code frontier

unlabeled · c_e14691a4c749

976 tests · 16240 LOC · 50 files · introduces 0 tests · 421 LOC · 1 file

Introduces — evidence that enters the hierarchy at this concept

Code
1 range421 lines · 1 files
Tests
0 tests

Contains — complete concept membership

All code (extent)
1166 ranges16240 lines · 50 files · Browse complete extent
All tests (intent)
976 testsBrowse complete intent

Neighbourhood graph

The orange circle is the focus. Violet and green circles are every ancestor and descendant, broader and narrower, at any distance; blue squares and pink diamonds are the introduced files and exact introduced tests of every visible concept, not only the focus's. Arrows point from broader to narrower concepts and bridge only concepts omitted from this view. Undirected links show source or test introduction. Concept and file size follows LOC; exact test nodes use test-count units.

Introduced files, introduced tests, and structurally relevant concept specialization

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 native relationship evidence on this page.

Graph controls are ready.

Interactive rendering requires JavaScript and WebGL. Use the native relationship evidence on this page while the interactive map is unavailable.

Native relationship evidence

Every exact file and test below is linked only from the concept that introduces it.

Introduced tests

Every collected test enters the hierarchy at exactly one concept.

No tests are introduced at this concept. Its intent tests are introduced by other concepts.

Introduced code

Every collected source range enters the hierarchy at exactly one concept.

1 file ranked by introduced lines: 421 introduced LOC across 1 ranges. Expand a file to inspect source; the > gutter marks introduced lines.

src/vs/platform/agentHost/common/sessionDataService.ts 421 introduced LOC · 1 range

Open complete file

1 > /*--------------------------------------------------------------------------------------------- sessionDataService.ts
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 > }