sessionDatabase.ts ×50

Frontier kind: Code frontier

unlabeled · c_19a298e04e77

509 tests · 13939 LOC · 48 files · introduces 0 tests · 288 LOC · 1 file

Introduces — evidence that enters the hierarchy at this concept

Code
50 ranges288 lines · 1 files
Tests
0 tests

Contains — complete concept membership

All code (extent)
1192 ranges13939 lines · 48 files · Browse complete extent
All tests (intent)
509 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: 288 introduced LOC across 50 ranges. Expand a file to inspect source; the > gutter marks introduced lines.

src/vs/platform/agentHost/node/sessionDatabase.ts 288 introduced LOC · 50 ranges

Open complete file

1 > /*--------------------------------------------------------------------------------------------- sessionDatabase.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 * as fs from 'fs';
7 > import { SequencerByKey } from '../../../base/common/async.js';
8 > import type { Database, RunResult } from '@vscode/sqlite3';
9 > import type { IFileEditContent, IFileEditRecord, ILocalTurnRecord, IReviewedFileRecord, ISessionDatabase } from '../common/sessionDataService.js';
10 > import { dirname } from '../../../base/common/path.js';
11 > import { URI } from '../../../base/common/uri.js';
12 > import type { Message } from '../common/state/sessionState.js';
13 >
14 > /**
15 > * A single numbered migration. Migrations are applied in order of
16 > * {@link version} and tracked via `PRAGMA user_version`.
17 > */
18 > export interface ISessionDatabaseMigration {
19 > /** Monotonically-increasing version number (1-based). */
20 > readonly version: number;
21 > /** SQL to execute for this migration. */
22 > readonly sql: string;
23 > }
24 >
25 > /**
26 > * The set of migrations that define the current session database schema.
27 > * New migrations should be **appended** to this array with the next version
28 > * number. Never reorder or mutate existing entries.
29 > */
30 > export const sessionDatabaseMigrations: readonly ISessionDatabaseMigration[] = [
31 > {
32 > version: 1,
33 > sql: [
34 > `CREATE TABLE IF NOT EXISTS turns (
35 > id TEXT PRIMARY KEY NOT NULL
36 > )`,
37 > `CREATE TABLE IF NOT EXISTS file_edits (
38 > turn_id TEXT NOT NULL REFERENCES turns(id) ON DELETE CASCADE,
39 > tool_call_id TEXT NOT NULL,
40 > file_path TEXT NOT NULL,
41 > before_content BLOB NOT NULL,
42 > after_content BLOB NOT NULL,
43 > added_lines INTEGER,
44 > removed_lines INTEGER,
45 > PRIMARY KEY (tool_call_id, file_path)
46 > )`,
47 > ].join(';\n'),
48 > },
49 > {
50 > version: 2,
51 > sql: `CREATE TABLE IF NOT EXISTS session_metadata (
52 > key TEXT PRIMARY KEY NOT NULL,
53 > value TEXT NOT NULL
54 > )`,
55 > },
56 > {
57 > version: 3,
58 > sql: [
59 > // Recreate file_edits with new columns: edit_type, original_path,
60 > // and nullable before_content/after_content.
61 > `CREATE TABLE file_edits_v3 (
62 > turn_id TEXT NOT NULL REFERENCES turns(id) ON DELETE CASCADE,
63 > tool_call_id TEXT NOT NULL,
64 > file_path TEXT NOT NULL,
65 > edit_type TEXT NOT NULL DEFAULT 'edit',
66 > original_path TEXT,
67 > before_content BLOB,
68 > after_content BLOB,
69 > added_lines INTEGER,
70 > removed_lines INTEGER,
71 > PRIMARY KEY (tool_call_id, file_path)
72 > )`,
73 > `INSERT INTO file_edits_v3 (turn_id, tool_call_id, file_path, edit_type, before_content, after_content, added_lines, removed_lines)
74 > SELECT turn_id, tool_call_id, file_path, 'edit', before_content, after_content, added_lines, removed_lines FROM file_edits`,
75 > `DROP TABLE file_edits`,
76 > `ALTER TABLE file_edits_v3 RENAME TO file_edits`,
77 > ].join(';\n'),
78 > },
79 > {
80 > version: 4,
81 > sql: [
82 > `ALTER TABLE turns ADD COLUMN event_id TEXT`,
83 > `CREATE INDEX IF NOT EXISTS idx_turns_event_id ON turns(event_id)`,
84 > ].join(';\n'),
85 > },
86 > {
87 > version: 5,
88 > sql: `ALTER TABLE turns ADD COLUMN checkpoint_ref TEXT`,
89 > },
90 > {
91 > version: 6,
92 > sql: `CREATE TABLE IF NOT EXISTS chat_drafts (
93 > chat_uri TEXT PRIMARY KEY NOT NULL,
94 > draft TEXT NOT NULL
95 > )`,
96 > },
97 > {
98 > version: 7,
99 > sql: `CREATE TABLE IF NOT EXISTS reviewed_files (
100 > uri TEXT NOT NULL,
101 > nonce TEXT NOT NULL,
102 > PRIMARY KEY (uri, nonce)
103 > )`,
104 > },
105 > {
106 > version: 8,
107 > sql: `CREATE TABLE IF NOT EXISTS local_turns (
108 > turn_id TEXT PRIMARY KEY NOT NULL,
109 > chat_uri TEXT NOT NULL,
110 > anchor_turn_id TEXT,
111 > seq INTEGER NOT NULL,
112 > payload TEXT NOT NULL
113 > )`,
114 > },
115 > ];
116 >
117 > // ---- Promise wrappers around callback-based @vscode/sqlite3 API -----------
118 >
119 function dbExec(db: Database, sql: string): Promise<void> {
120 return new Promise((resolve, reject) => {
122 });
123 }
125 function dbRun(db: Database, sql: string, params: unknown[]): Promise<{ changes: number; lastID: number }> {
126 return new Promise((resolve, reject) => {
133 });
134 }
136 function dbGet(db: Database, sql: string, params: unknown[]): Promise<Record<string, unknown> | undefined> {
137 return new Promise((resolve, reject) => {
144 });
145 }
147 function dbAll(db: Database, sql: string, params: unknown[]): Promise<Record<string, unknown>[]> {
148 return new Promise((resolve, reject) => {
155 });
156 }
158 function dbClose(db: Database): Promise<void> {
159 return new Promise((resolve, reject) => {
161 });
162 }
164 function dbOpen(path: string): Promise<Database> {
165 return new Promise((resolve, reject) => {
174 });
175 }
177 > /**
178 > * Applies any pending {@link ISessionDatabaseMigration migrations} to a
179 > * database. Migrations whose version is greater than the current
180 > * `PRAGMA user_version` are run inside a serialized transaction. After all
181 > * migrations complete the pragma is updated to the highest applied version.
182 > */
183 export async function runMigrations(db: Database, migrations: readonly ISessionDatabaseMigration[]): Promise<void> {
184 // Enable foreign key enforcement — must be set outside a transaction
210 }
211 }
213 > /**
214 > * A wrapper around a `@vscode/sqlite3` {@link Database} instance with
215 > * lazy initialisation.
216 > *
217 > * The underlying connection is opened on the first async method call
218 > * (not at construction time), allowing the object to be created
219 > * synchronously and shared via a {@link ReferenceCollection}.
220 > *
221 > * Calling {@link dispose} closes the connection.
222 > */
223 > export class SessionDatabase implements ISessionDatabase {
224 >
225 > protected _dbPromise: Promise<Database> | undefined;
226 > protected _closed: Promise<void> | true | undefined;
227 > private readonly _fileEditSequencer = new SequencerByKey<string>();
228 >
229 > /**
230 > * Serializes `setMetadata` writes per key. `@vscode/sqlite3` runs in
231 > * parallelized mode, so two `db.run()` calls on the same connection
232 > * can be dispatched to the libuv thread pool and complete out of
233 > * submission order. For "last writer wins" keys (notably `configValues`
234 > * via {@link setMetadata}), that meant a fast-following second write
235 > * could be overtaken by the first and silently lose its value — see
236 > * the "Session Config persistence across restarts" integration test.
237 > * Sequencing by key preserves intra-key order while still allowing
238 > * writes for different keys to run concurrently.
239 > */
240 > private readonly _metadataSequencer = new SequencerByKey<string>();
241 >
242 > /**
243 > * In-flight write operations. Tracked so {@link whenIdle} can await them
244 > * before the process exits — without this, a `SIGTERM` arriving between
245 > * a fire-and-forget mutating call (e.g. `setMetadata`) being invoked and
246 > * its underlying SQLite query completing would silently drop the write.
247 > * Every public mutating method routes its returned promise through
248 > * {@link _track}; reads (`getMetadata`, `getFileEdits`, ...) skip
249 > * tracking since shutdown does not need to wait for them.
250 > */
251 > private readonly _pendingWrites = new Set<Promise<unknown>>();
252 >
253 > constructor(
254 private readonly _path: string,
255 private readonly _migrations: readonly ISessionDatabaseMigration[] = sessionDatabaseMigrations,
256 ) { }
258 > /**
259 > * Opens (or creates) a SQLite database at {@link path} and applies
260 > * any pending migrations. Only used in tests where synchronous
261 > * construction + immediate readiness is desired.
262 > */
263 > static async open(path: string, migrations: readonly ISessionDatabaseMigration[] = sessionDatabaseMigrations): Promise<SessionDatabase> {
264 const inst = new SessionDatabase(path, migrations);
265 await inst._ensureDb();
266 return inst;
267 }
269 > protected _ensureDb(): Promise<Database> {
270 if (this._closed) {
271 return Promise.reject(new Error('SessionDatabase has been disposed'));
297 return this._dbPromise;
298 }
300 > /**
301 > * Returns the names of all user-created tables in the database.
302 > * Useful for testing migration behavior.
303 > */
304 > async getAllTables(): Promise<string[]> {
305 const db = await this._ensureDb();
306 const rows = await dbAll(db, `SELECT name FROM sqlite_master WHERE type='table' ORDER BY name`, []);
307 return rows.map(r => r.name as string);
308 }
310 > // ---- Turns ----------------------------------------------------------
311 >
312 > createTurn(turnId: string): Promise<void> {
313 return this._track(async () => {
314 const db = await this._ensureDb();
316 });
317 }
319 > deleteTurn(turnId: string): Promise<void> {
320 return this._track(async () => {
321 const db = await this._ensureDb();
323 });
324 }
326 > setTurnEventId(turnId: string, eventId: string): Promise<void> {
327 return this._track(async () => {
328 const db = await this._ensureDb();
334 });
335 }
337 > async getTurnEventId(turnId: string): Promise<string | undefined> {
338 const db = await this._ensureDb();
339 const row = await dbGet(db, 'SELECT event_id FROM turns WHERE id = ?', [turnId]);
340 return row?.event_id as string | undefined ?? undefined;
341 }
343 > async getNextTurnEventId(turnId: string): Promise<string | undefined> {
344 const db = await this._ensureDb();
345 // `turns.id` is the canonical turn key — either a live `request_xxx`
360 return row?.event_id as string | undefined ?? undefined;
361 }
363 > async getFirstTurnEventId(): Promise<string | undefined> {
364 const db = await this._ensureDb();
365 const row = await dbGet(db, 'SELECT event_id FROM turns ORDER BY rowid LIMIT 1', []);
366 return row?.event_id as string | undefined ?? undefined;
367 }
369 > setTurnCheckpointRef(turnId: string, ref: string): Promise<void> {
370 return this._track(async () => {
371 const db = await this._ensureDb();
374 });
375 }
377 > async getTurnCheckpointRef(turnId: string): Promise<string | undefined> {
378 const db = await this._ensureDb();
379 const row = await dbGet(db, 'SELECT checkpoint_ref FROM turns WHERE id = ?1 OR event_id = ?1 LIMIT 1', [turnId]);
380 return row?.checkpoint_ref as string | undefined ?? undefined;
381 }
383 > async getPreviousCheckpointRef(turnId: string): Promise<string | undefined> {
384 const db = await this._ensureDb();
385 const row = await dbGet(
393 return row?.checkpoint_ref as string | undefined ?? undefined;
394 }
396 > async getAllCheckpointRefs(): Promise<string[]> {
397 const db = await this._ensureDb();
398 const rows = await dbAll(db, 'SELECT checkpoint_ref FROM turns WHERE checkpoint_ref IS NOT NULL ORDER BY rowid', []);
399 return rows.map(r => r.checkpoint_ref as string);
400 }
402 > truncateFromTurn(turnId: string): Promise<void> {
403 return this._track(async () => {
404 const db = await this._ensureDb();
411 });
412 }
414 > deleteTurnsAfter(turnId: string): Promise<void> {
415 return this._track(async () => {
416 const db = await this._ensureDb();
424 });
425 }
427 > deleteAllTurns(): Promise<void> {
428 return this._track(async () => {
429 const db = await this._ensureDb();
431 });
432 }
434 > // ---- Local (host-injected) turns ------------------------------------
435 >
436 > insertLocalTurn(record: ILocalTurnRecord): Promise<void> {
437 return this._track(async () => {
438 const db = await this._ensureDb();
443 });
444 }
446 > async getLocalTurns(): Promise<ILocalTurnRecord[]> {
447 const db = await this._ensureDb();
448 const rows = await dbAll(db, 'SELECT turn_id, chat_uri, anchor_turn_id, seq, payload FROM local_turns ORDER BY seq', []);
455 }));
456 }
458 > deleteLocalTurns(turnIds: readonly string[]): Promise<void> {
459 return this._track(async () => {
460 if (turnIds.length === 0) {
466 });
467 }
469 > // ---- File edits -----------------------------------------------------
470 >
471 > storeFileEdit(edit: IFileEditRecord & IFileEditContent): Promise<void> {
472 return this._track(() => this._fileEditSequencer.queue(edit.filePath, async () => {
473 const db = await this._ensureDb();
494 }));
495 }
497 > async getFileEdits(toolCallIds: string[]): Promise<IFileEditRecord[]> {
498 if (toolCallIds.length === 0) {
499 return [];
519 }));
520 }
522 > async getAllFileEdits(): Promise<IFileEditRecord[]> {
523 const db = await this._ensureDb();
524 const rows = await dbAll(
539 }));
540 }
542 > async getFileEditsByTurn(turnId: string): Promise<IFileEditRecord[]> {
543 const db = await this._ensureDb();
544 const rows = await dbAll(
560 }));
561 }
563 > async readFileEditContent(toolCallId: string, filePath: string): Promise<IFileEditContent | undefined> {
564 return this._fileEditSequencer.queue(filePath, async () => {
565 const db = await this._ensureDb();
580 });
581 }
583 > // ---- Session metadata -----------------------------------------------
584 >
585 > async getMetadata(key: string): Promise<string | undefined> {
586 const db = await this._ensureDb();
587 const row = await dbGet(db, 'SELECT value FROM session_metadata WHERE key = ?', [key]);
588 return row?.value as string | undefined;
589 }
591 > async getMetadataObject<T extends Record<string, unknown>>(obj: T): Promise<{ [K in keyof T]: string | undefined }> {
592 const keys = Object.keys(obj) as (keyof T & string)[];
593 // eslint-disable-next-line local/code-no-dangerous-type-assertions
607 return result;
608 }
610 > setMetadata(key: string, value: string): Promise<void> {
611 return this._track(() => this._metadataSequencer.queue(key, async () => {
612 const db = await this._ensureDb();
614 }));
615 }
617 > setChatDraft(chat: URI, draft: Message | undefined): Promise<void> {
618 const chatUri = chat.toString();
619 return this._track(async () => {
626 });
627 }
629 > async getChatDraft(chat: URI): Promise<Message | undefined> {
630 const db = await this._ensureDb();
631 const row = await dbGet(db, 'SELECT draft FROM chat_drafts WHERE chat_uri = ?', [chat.toString()]);
639 }
640 }
642 > // ---- Reviewed files -------------------------------------------------
643 >
644 > markFileReviewed(uri: URI, nonce: string): Promise<void> {
645 return this._track(async () => {
646 const db = await this._ensureDb();
648 });
649 }
651 > unmarkFileReviewed(uri: URI, nonce: string): Promise<void> {
652 return this._track(async () => {
653 const db = await this._ensureDb();
655 });
656 }
658 > async getReviewedFiles(): Promise<IReviewedFileRecord[]> {
659 const db = await this._ensureDb();
660 const rows = await dbAll(db, 'SELECT uri, nonce FROM reviewed_files ORDER BY rowid', []);
661 return rows.map(toReviewedFileRecord);
662 }
664 > async getReviewedFilesForUri(uri: URI): Promise<IReviewedFileRecord[]> {
665 const db = await this._ensureDb();
666 const rows = await dbAll(db, 'SELECT uri, nonce FROM reviewed_files WHERE uri = ? ORDER BY rowid', [uri.toString()]);
667 return rows.map(toReviewedFileRecord);
668 }
670 > async isFileReviewed(uri: URI, nonce: string): Promise<boolean> {
671 const db = await this._ensureDb();
672 const row = await dbGet(db, 'SELECT 1 FROM reviewed_files WHERE uri = ? AND nonce = ? LIMIT 1', [uri.toString(), nonce]);
673 return !!row;
674 }
676 > remapTurnIds(mapping: ReadonlyMap<string, string>): Promise<void> {
677 return this._track(async () => {
678 const db = await this._ensureDb();
718 });
719 }
721 > /**
722 > * Resolves once all currently in-flight write operations have settled.
723 > * Used by graceful shutdown to flush pending fire-and-forget writes
724 > * before the process exits. Should be called from a path where no
725 > * further writes are expected; loops until idle to also drain any
726 > * writes that get queued while we're awaiting.
727 > */
728 > async whenIdle(): Promise<void> {
729 while (this._pendingWrites.size > 0) {
730 await Promise.allSettled([...this._pendingWrites]);
731 }
732 }
734 > async vacuumInto(targetPath: string) {
735 const db = await this._ensureDb();
736 await dbRun(db, 'VACUUM INTO ?', [targetPath]);
737 }
739 > /**
740 > * Wrap a mutating operation's promise so {@link whenIdle} can await it.
741 > * Invoke at the **outermost** layer of every public mutating method so
742 > * that any internal awaits (notably `_ensureDb()`) are covered too —
743 > * tracking only the leaf `dbRun`/`dbExec` would miss the window
744 > * between the method being called and the query actually being queued.
745 > */
746 > private _track<T>(fn: () => Promise<T>): Promise<T> {
747 const p = fn();
748 this._pendingWrites.add(p);
751 return p;
752 }
754 > async close() {
755 await (this._closed ??= this._dbPromise?.then(db => dbClose(db)).catch(() => { }) || true);
756 }
758 > dispose(): void {
759 this.close();
760 }
762 >
763 function toReviewedFileRecord(row: Record<string, unknown>): IReviewedFileRecord {
764 return {
767 };
768 }
770 function toUint8Array(value: unknown): Uint8Array {
771 if (value instanceof Buffer) {