blob: 271dbb86d2a2796473aee420fc5ec10d54a7c142 [file]
import type { Dirent } from 'node:fs';
import { open, readdir, realpath, stat } from 'node:fs/promises';
import { homedir } from 'node:os';
import { basename, join, resolve, sep } from 'node:path';
import type { StoredMessage } from '@maka/core';
import { sanitizeForeignTitle } from '@maka/core';
import type {
ExternalMakaSession,
ExternalSessionAdapter,
ExternalSessionQuery,
ExternalSessionSummary,
} from '@maka/core';
export const CODEX_SESSION_ADAPTER_ID = 'codex';
export const CODEX_ROLLOUT_MAX_BYTES = 64 * 1024 * 1024;
const CODEX_ROLLOUT_HEAD_BYTES = 512 * 1024;
const CODEX_SESSION_ID_PATTERN = /^[A-Za-z0-9_-]{1,128}$/;
const CODEX_UNSAFE_PATH_CHARS =
/[\u0000-\u001F\u007F\u0080-\u009F\u061C\u200B-\u200F\u202A-\u202E\u2060-\u2064\u2066-\u2069\uFEFF]/;
const CODEX_ROOT_SOURCE_TOKENS = new Set(['cli', 'exec', 'vscode']);
export interface CodexSessionAdapterOptions {
/** Codex's state root. Defaults to `$CODEX_HOME`, then `~/.codex`. */
codexHome?: string;
/** Test/host override for the bounded transcript read. */
maxRolloutBytes?: number;
}
interface CodexCatalogEntry extends ExternalSessionSummary {
rolloutPath: string;
}
interface CodexThreadRow {
id?: unknown;
rollout_path?: unknown;
cwd?: unknown;
name?: unknown;
title?: unknown;
preview?: unknown;
first_user_message?: unknown;
created_at_ms?: unknown;
created_at?: unknown;
updated_at_ms?: unknown;
updated_at?: unknown;
archived?: unknown;
source?: unknown;
}
type JsonRecord = Record<string, unknown>;
/**
* Read-only adapter for Codex rollout JSONL.
*
* Codex persists presentation history as `event_msg` records and provider
* protocol facts as `response_item` records. User, assistant, and reasoning
* messages come from `event_msg` to avoid importing their response-item
* mirrors twice. Tool calls/results come from response items because they own
* the stable call identity and raw arguments/output.
*/
export class CodexSessionAdapter implements ExternalSessionAdapter {
readonly id = CODEX_SESSION_ADAPTER_ID;
private readonly codexHome: string;
private readonly maxRolloutBytes: number;
constructor(options: CodexSessionAdapterOptions = {}) {
this.codexHome = resolve(
options.codexHome ?? process.env.CODEX_HOME ?? join(homedir(), '.codex'),
);
this.maxRolloutBytes = options.maxRolloutBytes ?? CODEX_ROLLOUT_MAX_BYTES;
if (!Number.isSafeInteger(this.maxRolloutBytes) || this.maxRolloutBytes <= 0) {
throw new Error('Codex rollout byte limit must be a positive safe integer');
}
}
async detect(): Promise<boolean> {
return (
(await isDirectory(join(this.codexHome, 'sessions'))) ||
(await isDirectory(join(this.codexHome, 'archived_sessions'))) ||
(await codexStateDbsNewestFirst(this.codexHome)).length > 0
);
}
async listSessions(query: ExternalSessionQuery = {}): Promise<readonly ExternalSessionSummary[]> {
const entries = await this.listCatalog(query);
return entries.map(({ rolloutPath: _rolloutPath, ...summary }) => summary);
}
async readSession(sessionId: string): Promise<ExternalMakaSession> {
assertSafeCodexSessionId(sessionId);
const catalogEntry = await this.findCatalogEntry(sessionId);
if (!catalogEntry) throw new Error(`Codex Session not found: ${sessionId}`);
const rolloutPath = await this.resolveRolloutPath(catalogEntry.rolloutPath, sessionId);
if (!rolloutPath) throw new Error(`Codex rollout is unavailable: ${sessionId}`);
const text = await readBoundedUtf8File(rolloutPath, this.maxRolloutBytes);
const converted = convertCodexRollout(text, sessionId, catalogEntry.name, catalogEntry.cwd);
return {
sourceSessionId: sessionId,
metadata: converted.metadata,
messages: converted.messages,
};
}
private async listCatalog(query: ExternalSessionQuery): Promise<CodexCatalogEntry[]> {
for (const dbPath of await codexStateDbsNewestFirst(this.codexHome)) {
const rows = await readCodexThreadRows(dbPath, query);
if (rows === undefined) continue;
const entries = await Promise.all(rows.map((row) => this.entryFromRow(row)));
return entries
.filter((entry): entry is CodexCatalogEntry => entry !== undefined)
.filter((entry) => matchesQuery(entry, query))
.sort(compareCatalogEntries);
}
return this.scanRolloutCatalog(query);
}
private async findCatalogEntry(sessionId: string): Promise<CodexCatalogEntry | undefined> {
for (const dbPath of await codexStateDbsNewestFirst(this.codexHome)) {
const rows = await readCodexThreadRows(dbPath, { includeArchived: true }, sessionId);
if (rows === undefined) continue;
for (const row of rows) {
const entry = await this.entryFromRow(row);
if (entry?.id === sessionId) return entry;
}
break;
}
return this.findRolloutEntry(sessionId);
}
private async entryFromRow(row: CodexThreadRow): Promise<CodexCatalogEntry | undefined> {
if (!isSafeCodexSessionId(row.id)) return undefined;
if (typeof row.rollout_path !== 'string' || row.rollout_path.length === 0) return undefined;
if (!isRootCodexSource(row.source)) return undefined;
const rolloutPath = await this.resolveRolloutPath(row.rollout_path, row.id);
if (!rolloutPath) return undefined;
const name =
firstNonEmptyTitle(row.name, row.title, row.preview, row.first_user_message) ?? row.id;
const createdAt = normalizeEpochMs(row.created_at_ms) ?? normalizeEpochMs(row.created_at);
const updatedAt = normalizeEpochMs(row.updated_at_ms) ?? normalizeEpochMs(row.updated_at);
return {
id: row.id,
name,
cwd: safeCodexCwd(row.cwd),
...(createdAt !== undefined ? { createdAt } : {}),
...(updatedAt !== undefined ? { updatedAt } : {}),
archived: row.archived === true || row.archived === 1,
rolloutPath,
};
}
private async scanRolloutCatalog(query: ExternalSessionQuery): Promise<CodexCatalogEntry[]> {
const candidates = [
...(await walkRolloutFiles(join(this.codexHome, 'sessions'), false)),
...(query.includeArchived
? await walkRolloutFiles(join(this.codexHome, 'archived_sessions'), true)
: []),
].sort((a, b) => b.mtimeMs - a.mtimeMs);
const entries: CodexCatalogEntry[] = [];
for (const candidate of candidates) {
const head = await readUtf8Prefix(candidate.path, CODEX_ROLLOUT_HEAD_BYTES).catch(
() => undefined,
);
if (head === undefined) continue;
const entry = catalogEntryFromRolloutHead(head, candidate);
if (!entry || !matchesQuery(entry, query)) continue;
const rolloutPath = await this.resolveRolloutPath(candidate.path, entry.id);
if (rolloutPath) entries.push({ ...entry, rolloutPath });
}
return entries.sort(compareCatalogEntries);
}
private async findRolloutEntry(sessionId: string): Promise<CodexCatalogEntry | undefined> {
for (const [root, archived] of [
[join(this.codexHome, 'sessions'), false],
[join(this.codexHome, 'archived_sessions'), true],
] as const) {
for (const candidate of await walkRolloutFiles(root, archived)) {
if (!rolloutFilenameMatchesId(basename(candidate.path), sessionId)) continue;
const head = await readUtf8Prefix(candidate.path, CODEX_ROLLOUT_HEAD_BYTES).catch(
() => undefined,
);
if (head === undefined) continue;
const entry = catalogEntryFromRolloutHead(head, candidate);
if (entry?.id !== sessionId) continue;
const rolloutPath = await this.resolveRolloutPath(candidate.path, sessionId);
if (rolloutPath) return { ...entry, rolloutPath };
}
}
return undefined;
}
private async resolveRolloutPath(
candidatePath: string,
expectedId: string,
): Promise<string | undefined> {
try {
const root = await realpath(this.codexHome);
const candidate = await realpath(resolve(candidatePath));
if (candidate !== root && !candidate.startsWith(root + sep)) return undefined;
if (!rolloutFilenameMatchesId(basename(candidate), expectedId)) return undefined;
if (!(await stat(candidate)).isFile()) return undefined;
return candidate;
} catch {
return undefined;
}
}
}
interface RolloutCandidate {
path: string;
mtimeMs: number;
archived: boolean;
}
function convertCodexRollout(
text: string,
expectedSessionId: string,
fallbackName: string,
fallbackCwd: string,
): ExternalMakaSession {
const records = parseRolloutRecords(text, expectedSessionId);
const sessionMeta = records.find((record) => record.value.type === 'session_meta')?.value;
const metaPayload = asRecord(sessionMeta?.payload);
const actualSessionId = stringField(metaPayload, 'session_id') ?? stringField(metaPayload, 'id');
if (actualSessionId !== expectedSessionId) {
throw new Error(`Codex rollout Session id mismatch: expected ${expectedSessionId}`);
}
const metaCwd = safeCodexCwd(metaPayload?.cwd);
const messages: StoredMessage[] = [];
let activeTurnId: string | undefined;
let activeTurnIsExplicit = false;
let activeModel = stringField(metaPayload, 'model_provider') ?? 'codex';
let lastTimestamp = normalizeEpochMs(sessionMeta?.timestamp) ?? 0;
let firstUserText: string | undefined;
const failedTurnIds = new Set<string>();
const timestampFor = (record: ParsedRolloutRecord): number => {
const parsed = normalizeEpochMs(record.value.timestamp);
if (parsed !== undefined) lastTimestamp = Math.max(lastTimestamp, parsed);
else lastTimestamp += 1;
return parsed ?? lastTimestamp;
};
const ensureTurnId = (line: number): string => {
activeTurnId ??= generatedCodexId(expectedSessionId, 'turn', line);
return activeTurnId;
};
for (const record of records) {
const envelope = record.value;
const payload = asRecord(envelope.payload);
if (!payload) continue;
if (envelope.type === 'turn_context') {
activeTurnId = stringField(payload, 'turn_id') ?? activeTurnId;
activeModel = stringField(payload, 'model') ?? activeModel;
continue;
}
if (envelope.type === 'event_msg') {
const eventType = stringField(payload, 'type');
if (eventType === 'task_started' || eventType === 'turn_started') {
const turnId = stringField(payload, 'turn_id');
if (turnId) {
activeTurnId = turnId;
activeTurnIsExplicit = true;
}
continue;
}
if (eventType === 'user_message') {
if (!activeTurnIsExplicit) {
activeTurnId = generatedCodexId(expectedSessionId, 'turn', record.line);
}
const text = stringField(payload, 'message') ?? mediaOnlyUserText(payload);
if (text.length === 0) continue;
firstUserText ??= text;
const turnId = ensureTurnId(record.line);
messages.push({
type: 'user',
id:
stringField(payload, 'client_id') ??
generatedCodexId(expectedSessionId, 'user', record.line),
turnId,
ts: timestampFor(record),
text,
});
continue;
}
if (eventType === 'agent_message') {
const text = stringField(payload, 'message');
if (!text) continue;
messages.push({
type: 'assistant',
id: generatedCodexId(expectedSessionId, 'assistant', record.line),
turnId: ensureTurnId(record.line),
ts: timestampFor(record),
text,
modelId: activeModel,
contentOrder: ['text'],
});
continue;
}
if (eventType === 'agent_reasoning') {
const reasoning = stringField(payload, 'text');
if (!reasoning) continue;
messages.push({
type: 'assistant',
id: generatedCodexId(expectedSessionId, 'reasoning', record.line),
turnId: ensureTurnId(record.line),
ts: timestampFor(record),
text: '',
thinking: { text: reasoning },
contentOrder: ['thinking'],
modelId: activeModel,
});
continue;
}
if (eventType === 'context_compacted') {
messages.push({
type: 'system_note',
id: generatedCodexId(expectedSessionId, 'compact', record.line),
turnId: activeTurnId,
ts: timestampFor(record),
kind: 'context_compacted',
});
continue;
}
if (eventType === 'error') {
if (activeTurnId && codexErrorAffectsTurnStatus(payload)) {
failedTurnIds.add(activeTurnId);
}
messages.push({
type: 'system_note',
id: generatedCodexId(expectedSessionId, 'error', record.line),
turnId: activeTurnId,
ts: timestampFor(record),
kind: 'error',
data: JSON.parse(JSON.stringify(payload)) as unknown,
});
continue;
}
if (eventType === 'task_complete' || eventType === 'turn_complete') {
const turnId = stringField(payload, 'turn_id') ?? ensureTurnId(record.line);
const failed = failedTurnIds.has(turnId) || payload.error != null;
messages.push({
type: 'turn_state',
id: generatedCodexId(expectedSessionId, 'turn-state', record.line),
turnId,
ts: timestampFor(record),
status: failed ? 'failed' : 'completed',
...(failed ? { errorClass: 'codex_error' } : {}),
partialOutputRetained: true,
});
failedTurnIds.delete(turnId);
if (activeTurnId === turnId) {
activeTurnId = undefined;
activeTurnIsExplicit = false;
}
continue;
}
if (eventType === 'turn_aborted') {
const turnId = stringField(payload, 'turn_id') ?? ensureTurnId(record.line);
const ts = timestampFor(record);
messages.push({
type: 'turn_state',
id: generatedCodexId(expectedSessionId, 'turn-state', record.line),
turnId,
ts,
status: 'aborted',
abortedAt: normalizeEpochMs(payload.completed_at) ?? ts,
abortSource: stringField(payload, 'reason') ?? 'codex',
partialOutputRetained: true,
});
if (activeTurnId === turnId) {
activeTurnId = undefined;
activeTurnIsExplicit = false;
}
continue;
}
}
if (envelope.type !== 'response_item') continue;
const itemType = stringField(payload, 'type');
if (itemType === 'function_call' || itemType === 'custom_tool_call') {
const callId = stringField(payload, 'call_id');
const toolName = namespacedToolName(payload);
if (!callId || !toolName) continue;
const rawArgs =
itemType === 'function_call'
? stringField(payload, 'arguments')
: stringField(payload, 'input');
messages.push({
type: 'tool_call',
id: callId,
turnId: ensureTurnId(record.line),
ts: timestampFor(record),
toolName,
args: parseJsonString(rawArgs),
});
continue;
}
if (itemType === 'function_call_output' || itemType === 'custom_tool_call_output') {
const callId = stringField(payload, 'call_id');
if (!callId) continue;
messages.push({
type: 'tool_result',
id:
stringField(payload, 'id') ??
generatedCodexId(expectedSessionId, 'tool-result', record.line),
turnId: ensureTurnId(record.line),
ts: timestampFor(record),
toolUseId: callId,
// Codex persists the output body but not FunctionCallOutputPayload.success.
// Preserve the raw body and avoid guessing failure from its text.
isError: false,
content: { kind: 'text', text: codexToolOutputText(payload.output) },
});
}
}
const name =
sanitizeForeignTitle(fallbackName) || sanitizeForeignTitle(firstUserText) || expectedSessionId;
return {
sourceSessionId: expectedSessionId,
metadata: { name, cwd: metaCwd || fallbackCwd },
messages,
};
}
interface ParsedRolloutRecord {
line: number;
value: JsonRecord;
}
function parseRolloutRecords(text: string, sessionId: string): ParsedRolloutRecord[] {
const endsWithNewline = text.endsWith('\n');
const lines = text.split('\n');
if (endsWithNewline) lines.pop();
const records: ParsedRolloutRecord[] = [];
for (let index = 0; index < lines.length; index += 1) {
const line = lines[index]!;
if (line.trim().length === 0) continue;
try {
const value = JSON.parse(line) as unknown;
if (!isRecord(value)) throw new Error('record is not an object');
records.push({ line: index + 1, value });
} catch (error) {
if (!endsWithNewline && index === lines.length - 1) break;
const detail = error instanceof Error ? error.message : String(error);
throw new Error(`Invalid Codex rollout ${sessionId} at line ${index + 1}: ${detail}`);
}
}
return records;
}
function catalogEntryFromRolloutHead(
text: string,
candidate: RolloutCandidate,
): Omit<CodexCatalogEntry, 'rolloutPath'> | undefined {
const lines = text.split('\n');
let id: string | undefined;
let cwd = '';
let createdAt: number | undefined;
let firstUserText: string | undefined;
for (const line of lines) {
let record: JsonRecord;
try {
const parsed = JSON.parse(line) as unknown;
if (!isRecord(parsed)) continue;
record = parsed;
} catch {
continue;
}
const payload = asRecord(record.payload);
if (!payload) continue;
if (record.type === 'session_meta') {
if (!isRootCodexSource(payload.source)) return undefined;
id = stringField(payload, 'session_id') ?? stringField(payload, 'id') ?? id;
cwd = safeCodexCwd(payload.cwd) || cwd;
createdAt =
normalizeEpochMs(record.timestamp) ?? normalizeEpochMs(payload.timestamp) ?? createdAt;
} else if (
record.type === 'event_msg' &&
payload.type === 'user_message' &&
firstUserText === undefined
) {
firstUserText = stringField(payload, 'message');
}
if (id && firstUserText !== undefined) break;
}
if (!isSafeCodexSessionId(id)) return undefined;
if (!rolloutFilenameMatchesId(basename(candidate.path), id)) return undefined;
return {
id,
name: sanitizeForeignTitle(firstUserText) || id,
cwd,
...(createdAt !== undefined ? { createdAt } : {}),
updatedAt: candidate.mtimeMs,
archived: candidate.archived,
};
}
async function readCodexThreadRows(
dbPath: string,
query: ExternalSessionQuery,
exactId?: string,
): Promise<CodexThreadRow[] | undefined> {
try {
const sqlite = await import('node:sqlite');
const db = new sqlite.DatabaseSync(dbPath, { readOnly: true });
try {
const columns = new Set(
(db.prepare('PRAGMA table_info(threads)').all() as { name?: unknown }[])
.map((column) => (typeof column.name === 'string' ? column.name : ''))
.filter(Boolean),
);
if (!columns.has('id') || !columns.has('rollout_path')) return undefined;
const wanted = [
'id',
'rollout_path',
'cwd',
'name',
'title',
'preview',
'first_user_message',
'created_at_ms',
'created_at',
'updated_at_ms',
'updated_at',
'archived',
'source',
].filter((column) => columns.has(column));
const where: string[] = [];
const params: Array<string | number> = [];
if (exactId !== undefined) {
where.push('id = ?');
params.push(exactId);
}
if (!query.includeArchived && columns.has('archived')) {
where.push('(archived IS NULL OR archived = 0)');
}
if (query.cwd !== undefined && columns.has('cwd')) {
const variants = cwdSqlVariants(query.cwd);
where.push(`cwd IN (${variants.map(() => '?').join(', ')})`);
params.push(...variants);
}
const orderColumn = columns.has('updated_at_ms')
? 'updated_at_ms'
: columns.has('updated_at')
? 'updated_at'
: 'id';
const sql =
`SELECT ${wanted.join(', ')} FROM threads` +
(where.length > 0 ? ` WHERE ${where.join(' AND ')}` : '') +
` ORDER BY ${orderColumn} DESC`;
return db.prepare(sql).all(...params) as CodexThreadRow[];
} finally {
db.close();
}
} catch {
return undefined;
}
}
async function codexStateDbsNewestFirst(codexHome: string): Promise<string[]> {
try {
const root = await realpath(codexHome);
const candidates = (await readdir(codexHome))
.filter((name) => /^state_\d+\.sqlite$/.test(name))
.sort((a, b) => stateGeneration(b) - stateGeneration(a));
const databases: string[] = [];
for (const name of candidates) {
const candidate = await realpath(join(codexHome, name)).catch(() => undefined);
if (!candidate || (candidate !== root && !candidate.startsWith(root + sep))) continue;
if ((await stat(candidate).catch(() => undefined))?.isFile()) databases.push(candidate);
}
return databases;
} catch {
return [];
}
}
async function walkRolloutFiles(root: string, archived: boolean): Promise<RolloutCandidate[]> {
const files: RolloutCandidate[] = [];
const visit = async (directory: string): Promise<void> => {
let entries: Dirent<string>[];
try {
entries = await readdir(directory, { withFileTypes: true });
} catch {
return;
}
for (const entry of entries) {
const path = join(directory, entry.name);
if (entry.isDirectory()) {
await visit(path);
} else if (
entry.isFile() &&
entry.name.startsWith('rollout-') &&
entry.name.endsWith('.jsonl')
) {
try {
files.push({ path, mtimeMs: (await stat(path)).mtimeMs, archived });
} catch {
// The external store may change while it is being scanned.
}
}
}
};
await visit(root);
return files;
}
async function readBoundedUtf8File(path: string, maxBytes: number): Promise<string> {
const handle = await open(path, 'r');
try {
const metadata = await handle.stat();
if (!metadata.isFile()) throw new Error('Codex rollout is not a regular file');
if (metadata.size > maxBytes) throw new Error(`Codex rollout exceeds ${maxBytes} bytes`);
const chunks: Buffer[] = [];
let total = 0;
for (;;) {
const buffer = Buffer.allocUnsafe(Math.min(64 * 1024, maxBytes + 1 - total));
const { bytesRead } = await handle.read(buffer, 0, buffer.length, total);
if (bytesRead === 0) break;
total += bytesRead;
if (total > maxBytes) throw new Error(`Codex rollout exceeds ${maxBytes} bytes`);
chunks.push(buffer.subarray(0, bytesRead));
if (total === maxBytes) {
const probe = Buffer.allocUnsafe(1);
if ((await handle.read(probe, 0, 1, total)).bytesRead > 0) {
throw new Error(`Codex rollout exceeds ${maxBytes} bytes`);
}
break;
}
}
return Buffer.concat(chunks, total).toString('utf8');
} finally {
await handle.close();
}
}
async function readUtf8Prefix(path: string, maxBytes: number): Promise<string> {
const handle = await open(path, 'r');
try {
if (!(await handle.stat()).isFile()) throw new Error('Codex rollout is not a regular file');
const buffer = Buffer.allocUnsafe(maxBytes);
const { bytesRead } = await handle.read(buffer, 0, maxBytes, 0);
return buffer.subarray(0, bytesRead).toString('utf8');
} finally {
await handle.close();
}
}
function asRecord(value: unknown): JsonRecord | undefined {
return isRecord(value) ? value : undefined;
}
function isRecord(value: unknown): value is JsonRecord {
return typeof value === 'object' && value !== null && !Array.isArray(value);
}
function stringField(record: JsonRecord | undefined, field: string): string | undefined {
const value = record?.[field];
return typeof value === 'string' && value.length > 0 ? value : undefined;
}
function isSafeCodexSessionId(value: unknown): value is string {
return typeof value === 'string' && CODEX_SESSION_ID_PATTERN.test(value);
}
function assertSafeCodexSessionId(value: string): void {
if (!isSafeCodexSessionId(value)) throw new Error(`Invalid Codex Session id: ${value}`);
}
function safeCodexCwd(value: unknown): string {
return typeof value === 'string' && !CODEX_UNSAFE_PATH_CHARS.test(value) ? value : '';
}
function firstNonEmptyTitle(...values: unknown[]): string | undefined {
for (const value of values) {
const title = sanitizeForeignTitle(value);
if (title.length > 0) return title;
}
return undefined;
}
function isRootCodexSource(value: unknown): boolean {
if (value === undefined || value === null) return true;
if (typeof value === 'string') {
if (CODEX_ROOT_SOURCE_TOKENS.has(value)) return true;
if (!value.startsWith('{')) return false;
try {
return isRootCodexSource(JSON.parse(value) as unknown);
} catch {
return false;
}
}
if (!isRecord(value)) return false;
return value.custom === 'atlas' || value.custom === 'chatgpt';
}
function codexErrorAffectsTurnStatus(payload: JsonRecord): boolean {
const info = payload.codex_error_info;
if (info === 'thread_rollback_failed' || info === 'active_turn_not_steerable') return false;
return !(isRecord(info) && Object.hasOwn(info, 'active_turn_not_steerable'));
}
function normalizeEpochMs(value: unknown): number | undefined {
if (typeof value === 'number' && Number.isFinite(value)) {
return value >= 1_000_000_000_000 ? value : value * 1000;
}
if (typeof value === 'string' && value.length > 0) {
const numeric = Number(value);
if (Number.isFinite(numeric)) return normalizeEpochMs(numeric);
const parsed = Date.parse(value);
return Number.isFinite(parsed) ? parsed : undefined;
}
return undefined;
}
function normalizePath(value: string): string {
const normalized = value.replaceAll('\\', '/').replace(/\/+$/, '');
return /^[A-Za-z]:\//.test(normalized) ? normalized.toLowerCase() : normalized;
}
function cwdSqlVariants(cwd: string): string[] {
const normalized = normalizePath(cwd);
const variants = new Set([cwd, normalized]);
if (/^[A-Za-z]:\//.test(normalized)) variants.add(normalized.replaceAll('/', '\\'));
if (normalized !== '/') {
variants.add(`${normalized}/`);
if (/^[A-Za-z]:\//.test(normalized)) variants.add(`${normalized.replaceAll('/', '\\')}\\`);
}
return [...variants];
}
function matchesQuery(entry: ExternalSessionSummary, query: ExternalSessionQuery): boolean {
if (!query.includeArchived && entry.archived) return false;
return query.cwd === undefined || normalizePath(entry.cwd) === normalizePath(query.cwd);
}
function compareCatalogEntries(a: CodexCatalogEntry, b: CodexCatalogEntry): number {
return (b.updatedAt ?? b.createdAt ?? 0) - (a.updatedAt ?? a.createdAt ?? 0);
}
function stateGeneration(path: string): number {
return Number(path.match(/\d+/)?.[0] ?? 0);
}
function rolloutFilenameMatchesId(filename: string, sessionId: string): boolean {
return filename.endsWith(`-${sessionId}.jsonl`);
}
function generatedCodexId(sessionId: string, kind: string, line: number): string {
return `codex-${sessionId}-${kind}-${line}`;
}
function namespacedToolName(payload: JsonRecord): string | undefined {
const name = stringField(payload, 'name');
if (!name) return undefined;
const namespace = stringField(payload, 'namespace');
return namespace ? `${namespace}.${name}` : name;
}
function parseJsonString(value: string | undefined): unknown {
if (value === undefined) return '';
try {
return JSON.parse(value) as unknown;
} catch {
return value;
}
}
function codexToolOutputText(value: unknown): string {
if (typeof value === 'string') return value;
if (Array.isArray(value)) {
const texts = value.flatMap((item) => {
const record = asRecord(item);
return record?.type === 'input_text' && typeof record.text === 'string' ? [record.text] : [];
});
if (texts.length > 0) return texts.join('\n');
}
try {
return JSON.stringify(value) ?? String(value);
} catch {
return String(value);
}
}
function mediaOnlyUserText(payload: JsonRecord): string {
const images = Array.isArray(payload.images) ? payload.images : [];
const localImages = Array.isArray(payload.local_images) ? payload.local_images : [];
if (images.length > 0 || localImages.length > 0) return '[Image]';
const audio = Array.isArray(payload.audio) ? payload.audio : [];
const localAudio = Array.isArray(payload.local_audio) ? payload.local_audio : [];
return audio.length > 0 || localAudio.length > 0 ? '[Audio]' : '';
}
async function isDirectory(path: string): Promise<boolean> {
try {
return (await stat(path)).isDirectory();
} catch {
return false;
}
}