Files
vscode/src/vs/platform/agentHost/node/sessionDataService.ts
Connor Peet da5fb11b3b agentHost: add session-specific metadata
Adds a SQLite DB for session-specific metadata. Stores edits in there.
It can _almost_ restore edits, but I still need to make undoStops be
similarly persisted. That is a project for later this evening.
2026-03-26 15:46:46 -07:00

115 lines
3.9 KiB
TypeScript

/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { IReference, ReferenceCollection } from '../../../base/common/lifecycle.js';
import { URI } from '../../../base/common/uri.js';
import { IFileService } from '../../files/common/files.js';
import { ILogService } from '../../log/common/log.js';
import { AgentSession } from '../common/agentService.js';
import { ISessionDatabase, ISessionDataService } from '../common/sessionDataService.js';
import { SessionDatabase } from './sessionDatabase.js';
class SessionDatabaseCollection extends ReferenceCollection<ISessionDatabase> {
constructor(
private readonly _getDbPath: (key: string) => string,
private readonly _logService: ILogService,
) {
super();
}
protected createReferencedObject(key: string): ISessionDatabase {
const dbPath = this._getDbPath(key);
this._logService.trace(`[SessionDataService] Opening database: ${dbPath}`);
return new SessionDatabase(dbPath);
}
protected destroyReferencedObject(_key: string, object: ISessionDatabase): void {
object.dispose();
}
}
/**
* Implementation of {@link ISessionDataService} that stores per-session data
* under `{userDataPath}/agentSessionData/{sessionId}/`.
*/
export class SessionDataService implements ISessionDataService {
declare readonly _serviceBrand: undefined;
private readonly _basePath: URI;
private readonly _databases: SessionDatabaseCollection;
constructor(
userDataPath: URI,
@IFileService private readonly _fileService: IFileService,
@ILogService private readonly _logService: ILogService,
) {
this._basePath = URI.joinPath(userDataPath, 'agentSessionData');
this._databases = new SessionDatabaseCollection(
key => URI.joinPath(this._basePath, key, 'session.db').fsPath,
this._logService,
);
}
getSessionDataDir(session: URI): URI {
return this.getSessionDataDirById(AgentSession.id(session));
}
getSessionDataDirById(sessionId: string): URI {
const sanitized = sessionId.replace(/[^a-zA-Z0-9_.-]/g, '-');
return URI.joinPath(this._basePath, sanitized);
}
openDatabase(session: URI): IReference<ISessionDatabase> {
const sanitized = AgentSession.id(session).replace(/[^a-zA-Z0-9_.-]/g, '-');
return this._databases.acquire(sanitized);
}
async deleteSessionData(session: URI): Promise<void> {
const dir = this.getSessionDataDir(session);
try {
if (await this._fileService.exists(dir)) {
await this._fileService.del(dir, { recursive: true });
this._logService.trace(`[SessionDataService] Deleted session data: ${dir.toString()}`);
}
} catch (err) {
this._logService.warn(`[SessionDataService] Failed to delete session data: ${dir.toString()}`, err);
}
}
async cleanupOrphanedData(knownSessionIds: Set<string>): Promise<void> {
try {
const exists = await this._fileService.exists(this._basePath);
if (!exists) {
return;
}
const stat = await this._fileService.resolve(this._basePath);
if (!stat.children) {
return;
}
const deletions: Promise<void>[] = [];
for (const child of stat.children) {
if (!child.isDirectory) {
continue;
}
const name = child.name;
if (!knownSessionIds.has(name)) {
this._logService.trace(`[SessionDataService] Cleaning up orphaned session data: ${name}`);
deletions.push(
this._fileService.del(child.resource, { recursive: true }).catch(err => {
this._logService.warn(`[SessionDataService] Failed to clean up orphaned data: ${name}`, err);
})
);
}
}
await Promise.all(deletions);
} catch (err) {
this._logService.warn('[SessionDataService] Failed to run orphan cleanup', err);
}
}
}