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

197 LOC · 153 covered · 44 uncovered · 31 ranges · 14 concepts · 9 introducers · 11 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.

Focused file, its introducer and connector concepts, their introduced files, and tests that run code from the filesessionDataService.ts ×1 · 3 introduced LOCsessionDataService.ts ×1sessionDataService.ts ×4 · 19 introduced LOCsessionDataService.ts ×4sessionDataService.test|title=SessionDataService — openDatabase ref-counting returns a functional database reference|occurrence=1 · 0 introduced LOCsessionDataService.test|…sessionDataService.test|title=SessionDataService — openDatabase ref-counting new reference after all disposed gets a fresh database|occurrence=1 · 0 introduced LOCsessionDataService.test|…sessionDataService.test|title=SessionDataService — openDatabase ref-counting database remains usable until last reference is disposed|occurrence=1 · 0 introduced LOCsessionDataService.test|…sessionDataService.test|title=SessionDataService getSessionDataDir gives each peer chat of a session its own directory|occurrence=1 · 0 introduced LOCsessionDataService.test|…sessionDataService.ts ×5 · 17 introduced LOCsessionDataService.ts ×5sessionDataService.test|title=SessionDataService getSessionDataDir returns correct URI|occurrence=1, sessionDataService.test|title=SessionDataService getSessionDataDir sanitizes unsafe characters|occurrence=1 · 0 introduced LOCsessionDataService.test|…sessionDataService.ts ×1 · 2 introduced LOCsessionDataService.ts ×1sessionDataService.ts ×2 · 4 introduced LOCsessionDataService.ts ×2sessionDataService.ts ×3 · 11 introduced LOCsessionDataService.ts ×3sessionDataService.ts ×1 · 2 introduced LOCsessionDataService.ts ×1sessionDataService.ts ×2 · 5 introduced LOCsessionDataService.ts ×2sessionDataService.ts ×12 · 90 introduced LOCsessionDataService.ts ×1…sessionDataService.test|title=SessionDataService cleanupOrphanedData deletes orphans but keeps known sessions|occurrence=1 · introduced test · mocha:v1|namespace=vscode@05c208e9e28d8c1c723fa08f85e2b7a96092e8e5|file=vs/platform/agentHost/test/node/sessionDataService.test|title=SessionDataService cleanupOrphanedData deletes orphans but keeps known sessions|occurrence=1sessionDataService.test|…sessionDataService.test|title=SessionDataService cleanupOrphanedData is a no-op when base directory does not exist|occurrence=1 · introduced test · mocha:v1|namespace=vscode@05c208e9e28d8c1c723fa08f85e2b7a96092e8e5|file=vs/platform/agentHost/test/node/sessionDataService.test|title=SessionDataService cleanupOrphanedData is a no-op when base directory does not exist|occurrence=1sessionDataService.test|…sessionDataService.test|title=SessionDataService deleteSessionData is a no-op when directory does not exist|occurrence=1 · introduced test · mocha:v1|namespace=vscode@05c208e9e28d8c1c723fa08f85e2b7a96092e8e5|file=vs/platform/agentHost/test/node/sessionDataService.test|title=SessionDataService deleteSessionData is a no-op when directory does not exist|occurrence=1sessionDataService.test|…sessionDataService.test|title=SessionDataService deleteSessionData removes directory|occurrence=1 · introduced test · mocha:v1|namespace=vscode@05c208e9e28d8c1c723fa08f85e2b7a96092e8e5|file=vs/platform/agentHost/test/node/sessionDataService.test|title=SessionDataService deleteSessionData removes directory|occurrence=1sessionDataService.test|…sessionDataService.test|title=SessionDataService getSessionDataDir gives each peer chat of a session its own directory|occurrence=1 · introduced test · mocha:v1|namespace=vscode@05c208e9e28d8c1c723fa08f85e2b7a96092e8e5|file=vs/platform/agentHost/test/node/sessionDataService.test|title=SessionDataService getSessionDataDir gives each peer chat of a session its own directory|occurrence=1sessionDataService.test|…sessionDataService.test|title=SessionDataService getSessionDataDir returns correct URI|occurrence=1 · introduced test · mocha:v1|namespace=vscode@05c208e9e28d8c1c723fa08f85e2b7a96092e8e5|file=vs/platform/agentHost/test/node/sessionDataService.test|title=SessionDataService getSessionDataDir returns correct URI|occurrence=1sessionDataService.test|…sessionDataService.test|title=SessionDataService getSessionDataDir sanitizes unsafe characters|occurrence=1 · introduced test · mocha:v1|namespace=vscode@05c208e9e28d8c1c723fa08f85e2b7a96092e8e5|file=vs/platform/agentHost/test/node/sessionDataService.test|title=SessionDataService getSessionDataDir sanitizes unsafe characters|occurrence=1sessionDataService.test|…sessionDataService.test|title=SessionDataService — openDatabase ref-counting database remains usable until last reference is disposed|occurrence=1 · introduced test · mocha:v1|namespace=vscode@05c208e9e28d8c1c723fa08f85e2b7a96092e8e5|file=vs/platform/agentHost/test/node/sessionDataService.test|title=SessionDataService — openDatabase ref-counting database remains usable until last reference is disposed|occurrence=1sessionDataService.test|…sessionDataService.test|title=SessionDataService — openDatabase ref-counting multiple references share the same database|occurrence=1 · introduced test · mocha:v1|namespace=vscode@05c208e9e28d8c1c723fa08f85e2b7a96092e8e5|file=vs/platform/agentHost/test/node/sessionDataService.test|title=SessionDataService — openDatabase ref-counting multiple references share the same database|occurrence=1sessionDataService.test|…sessionDataService.test|title=SessionDataService — openDatabase ref-counting new reference after all disposed gets a fresh database|occurrence=1 · introduced test · mocha:v1|namespace=vscode@05c208e9e28d8c1c723fa08f85e2b7a96092e8e5|file=vs/platform/agentHost/test/node/sessionDataService.test|title=SessionDataService — openDatabase ref-counting new reference after all disposed gets a fresh database|occurrence=1sessionDataService.test|…sessionDataService.test|title=SessionDataService — openDatabase ref-counting returns a functional database reference|occurrence=1 · introduced test · mocha:v1|namespace=vscode@05c208e9e28d8c1c723fa08f85e2b7a96092e8e5|file=vs/platform/agentHost/test/node/sessionDataService.test|title=SessionDataService — openDatabase ref-counting returns a functional database reference|occurrence=1sessionDataService.test|…Focused file · src/vs/platform/agentHost/node/sessionDataService.ts · 197 LOCnode/sessionDataService.…

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 ×12
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 { IReference, ReferenceCollection } from '../../../base/common/lifecycle.js';
7 > import { URI } from '../../../base/common/uri.js';
8 > import { Emitter, Event } from '../../../base/common/event.js';
9 > import { IFileService } from '../../files/common/files.js';
10 > import { ILogService } from '../../log/common/log.js';
11 > import { AgentSession } from '../common/agentService.js';
12 > import { ISessionDatabase, ISessionDataService, IWillDeleteSessionDataEvent, SESSION_DB_FILENAME } from '../common/sessionDataService.js';
13 > import { SessionDatabase } from './sessionDatabase.js';
14 >
15 > class SessionDatabaseCollection extends ReferenceCollection<ISessionDatabase> {
16 >
17 > /**
18 > * The set of currently-open databases. Mirrors what's held by the
19 > * underlying ref-counted map, but exposed so {@link SessionDataService.whenIdle}
20 > * can iterate without reaching into private state.
21 > */
22 > readonly liveDatabases = new Set<ISessionDatabase>();
23 >
24 > constructor(
25 > private readonly _getDbPath: (key: string) => string,
26 > private readonly _logService: ILogService,
27 > ) {
28 > super();
29 > }
30 >
31 > protected createReferencedObject(key: string): ISessionDatabase {
32 > const dbPath = this._getDbPath(key); sessionDataService.ts ×3
33 > this._logService.trace(`[SessionDataService] Opening database: ${dbPath}`);
34 > const db = new SessionDatabase(dbPath);
35 > this.liveDatabases.add(db);
36 > return db;
37 > }
39 > protected destroyReferencedObject(_key: string, object: ISessionDatabase): void {
40 > this.liveDatabases.delete(object); sessionDataService.ts ×3
41 > object.dispose();
42 > }
44 >
45 > /**
46 > * Implementation of {@link ISessionDataService} that stores per-session data
47 > * under `{userDataPath}/agentSessionData/{sessionId}/`.
48 > */
49 > export class SessionDataService implements ISessionDataService {
50 > declare readonly _serviceBrand: undefined;
51 >
52 > private readonly _basePath: URI;
53 > private readonly _databases: SessionDatabaseCollection;
54 > private readonly _onWillDeleteSessionData = new Emitter<IWillDeleteSessionDataEvent>();
55 >
56 > get onWillDeleteSessionData(): Event<IWillDeleteSessionDataEvent> {
57 > return this._onWillDeleteSessionData.event;
58 > }
59 >
60 > constructor(
61 > userDataPath: URI,
62 > @IFileService private readonly _fileService: IFileService,
63 > @ILogService private readonly _logService: ILogService,
64 > getDbPath?: (key: string) => string, // for testing
65 > ) {
66 > this._basePath = URI.joinPath(userDataPath, 'agentSessionData');
67 > this._databases = new SessionDatabaseCollection(
68 > getDbPath ?? (key => URI.joinPath(this._basePath, key, SESSION_DB_FILENAME).fsPath),
69 > this._logService,
70 > );
71 > }
72 >
73 > getSessionDataDir(session: URI): URI {
74 > return URI.joinPath(this._basePath, this._sanitizedSessionKey(session)); sessionDataService.ts ×1
75 > }
77 > getSessionDataDirById(sessionId: string): URI {
78 const sanitized = sessionId.replace(/[^a-zA-Z0-9_.-]/g, '-');
79 return URI.joinPath(this._basePath, sanitized);
80 }
82 > private _sanitizedSessionKey(session: URI): string {
83 > return this._dataKey(session).replace(/[^a-zA-Z0-9_.-]/g, '-'); sessionDataService.ts ×2
84 > }
86 > /**
87 > * Derives the per-URI storage key. Chat channel URIs
88 > * (`ahp-chat://<chatId>/<base64(session)>`) carry the chat id in the
89 > * authority while encoding the SAME owning-session URI in the path, so
90 > * keying only by the path (via {@link AgentSession.id}) would collapse
91 > * every peer chat of a session onto one data directory and database.
92 > * Prefixing with the authority gives each chat its own storage while
93 > * leaving plain session URIs (no authority) unchanged.
94 > */
95 > private _dataKey(uri: URI): string {
96 > const id = AgentSession.id(uri); sessionDataService.ts ×2
97 > return uri.authority ? `${uri.authority}-${id}` : id;
98 > }
100 > openDatabase(session: URI): IReference<ISessionDatabase> {
101 > return this._databases.acquire(this._sanitizedSessionKey(session)); sessionDataService.ts ×3
102 > }
104 > async tryOpenDatabase(session: URI): Promise<IReference<ISessionDatabase> | undefined> {
105 const key = this._sanitizedSessionKey(session);
106 const dbPath = URI.joinPath(this._basePath, key, SESSION_DB_FILENAME);
107 if (!await this._fileService.exists(dbPath)) {
108 return undefined;
109 }
110 return this._databases.acquire(key);
111 }
113 > async deleteSessionData(session: URI): Promise<void> {
114 > const dir = this.getSessionDataDir(session); sessionDataService.ts ×5
115 > // Fire the will-delete event first so subscribers (notably the
116 > // checkpoint service) can perform async cleanup that needs the
117 > // database to still be readable. `waitUntil` collects each
118 > // subscriber's promise; we await them all before touching disk.
119 > const pending: Promise<unknown>[] = [];
120 > try {
121 > this._onWillDeleteSessionData.fire({
122 > session,
123 > waitUntil: p => { pending.push(p); },
124 > });
125 > } catch (err) {
126 this._logService.warn(`[SessionDataService] onWillDeleteSessionData listener threw synchronously: ${dir.toString()}`, err);
127 }
128 > if (pending.length > 0) { sessionDataService.ts ×5
129 const results = await Promise.allSettled(pending);
130 for (const r of results) {
131 if (r.status === 'rejected') {
132 this._logService.warn(`[SessionDataService] onWillDeleteSessionData waitUntil rejected: ${dir.toString()}`, r.reason);
133 }
134 }
135 }
137 > if (await this._fileService.exists(dir)) {
138 > await this._fileService.del(dir, { recursive: true }); sessionDataService.ts ×1
139 > this._logService.trace(`[SessionDataService] Deleted session data: ${dir.toString()}`);
140 > }
141 > } catch (err) { sessionDataService.ts ×5
142 this._logService.warn(`[SessionDataService] Failed to delete session data: ${dir.toString()}`, err);
143 }
146 > async cleanupOrphanedData(knownSessionIds: Set<string>): Promise<void> {
148 > const exists = await this._fileService.exists(this._basePath);
149 > if (!exists) {
151 > }
153 > const stat = await this._fileService.resolve(this._basePath);
154 > if (!stat.children) {
155 return;
156 }
158 > const deletions: Promise<void>[] = [];
159 > for (const child of stat.children) {
160 > if (!child.isDirectory) {
161 continue;
162 }
163 > const name = child.name; sessionDataService.ts ×4
164 > if (!knownSessionIds.has(name)) {
165 > this._logService.trace(`[SessionDataService] Cleaning up orphaned session data: ${name}`);
166 > deletions.push(
167 > this._fileService.del(child.resource, { recursive: true }).catch(err => {
168 this._logService.warn(`[SessionDataService] Failed to clean up orphaned data: ${name}`, err);
170 > );
171 > }
172 > }
173 >
174 > await Promise.all(deletions);
175 > } catch (err) {
176 this._logService.warn('[SessionDataService] Failed to run orphan cleanup', err);
177 }
180 > async whenIdle(): Promise<void> {
181 // Each `SessionDatabase.whenIdle()` already loops internally until
182 // that DB is quiescent, so the outer loop only needs to handle the
183 // case where a new DB was opened (and writes queued against it)
184 // while we were awaiting an earlier pass.
185 while (true) {
186 const dbs = [...this._databases.liveDatabases];
187 if (dbs.length === 0) {
188 return;
189 }
190 await Promise.all(dbs.map(db => db.whenIdle()));
191 const newOnes = [...this._databases.liveDatabases].filter(db => !dbs.includes(db));
192 if (newOnes.length === 0) {
193 return;
194 }
195 }
196 }