refactor: implement session synchronizer interfaces and logic for multiple providers

This commit is contained in:
Haileyesus
2026-04-07 14:11:27 +03:00
parent 664713776a
commit b54a2839e3
16 changed files with 81 additions and 91 deletions

View File

@@ -0,0 +1,113 @@
import os from 'node:os';
import path from 'node:path';
import { sessionsDb } from '@/shared/database/repositories/sessions.db.js';
import {
buildLookupMap,
extractFirstValidJsonlData,
findFilesRecursivelyCreatedAfter,
normalizeSessionName,
readFileTimestamps,
} from '@/modules/ai-runtime/providers/shared/session-synchronizer/session-synchronizer.utils.js';
import type { IProviderSessionSynchronizerRuntime } from '@/modules/ai-runtime/types/index.js';
type ParsedSession = {
sessionId: string;
workspacePath: string;
sessionName?: string;
};
/**
* Session indexer for Codex transcript artifacts.
*/
export class CodexSessionSynchronizerRuntime implements IProviderSessionSynchronizerRuntime {
private readonly provider = 'codex' as const;
private readonly codexHome = path.join(os.homedir(), '.codex');
/**
* Scans ~/.codex sessions and upserts discovered sessions into DB.
*/
async synchronize(since?: Date): Promise<number> {
const nameMap = await buildLookupMap(path.join(this.codexHome, 'session_index.jsonl'), 'id', 'thread_name');
const files = await findFilesRecursivelyCreatedAfter(
path.join(this.codexHome, 'sessions'),
'.jsonl',
since ?? null,
);
let processed = 0;
for (const filePath of files) {
const parsed = await this.processSessionFile(filePath, nameMap);
if (!parsed) {
continue;
}
const timestamps = await readFileTimestamps(filePath);
sessionsDb.createSession(
parsed.sessionId,
this.provider,
parsed.workspacePath,
parsed.sessionName,
timestamps.createdAt,
timestamps.updatedAt,
filePath,
);
processed += 1;
}
return processed;
}
/**
* Parses and upserts one Codex session JSONL file.
*/
async synchronizeFile(filePath: string): Promise<boolean> {
if (!filePath.endsWith('.jsonl')) {
return false;
}
const nameMap = await buildLookupMap(path.join(this.codexHome, 'session_index.jsonl'), 'id', 'thread_name');
const parsed = await this.processSessionFile(filePath, nameMap);
if (!parsed) {
return false;
}
const timestamps = await readFileTimestamps(filePath);
sessionsDb.createSession(
parsed.sessionId,
this.provider,
parsed.workspacePath,
parsed.sessionName,
timestamps.createdAt,
timestamps.updatedAt,
filePath,
);
return true;
}
/**
* Extracts session metadata from one Codex JSONL session file.
*/
private async processSessionFile(
filePath: string,
nameMap: Map<string, string>,
): Promise<ParsedSession | null> {
return extractFirstValidJsonlData(filePath, (rawData) => {
const data = rawData as Record<string, unknown>;
const payload = data.payload as Record<string, unknown> | undefined;
const sessionId = typeof payload?.id === 'string' ? payload.id : undefined;
const workspacePath = typeof payload?.cwd === 'string' ? payload.cwd : undefined;
if (!sessionId || !workspacePath) {
return null;
}
return {
sessionId,
workspacePath,
sessionName: normalizeSessionName(nameMap.get(sessionId), 'Untitled Codex Session'),
};
});
}
}