openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
1,352 lines • 61 kB
JavaScript
import { c as isRecord } from "./record-coerce-DItp3I4t.js";
import { n as getRuntimeConfig } from "./io.runtime-B9iJRs3w.js";
import { l as readFileDescriptorBoundedSync } from "./boundary-file-read-uaJcf6X6.js";
import { r as isPathInside } from "./path-guards-Cp-mGr3-.js";
import { n as normalizeAgentId } from "./agent-id-CeT3w4ap.js";
import { D as tryResolveLegacyCompatibilityAgentId, E as tryResolveDefaultAgentId } from "./agent-scope-config-DcbEhP0R.js";
import { w as resolveStateDir } from "./paths-D2sRr1a_.js";
import { O as parseAgentSessionKey } from "./session-key-BnWWjqNc.js";
import "./legacy.default-agent-owner-BGwEdQRe.js";
import { t as formatErrorMessage } from "./errors-Db3Ymjlb.js";
import { t as openNodeSqliteDatabase } from "./node-sqlite-BpQX3W0e.js";
import { F as getCanonicalSqliteNamedIndexContracts } from "./openclaw-state-db-cache-C7ljO0xP.js";
import { o as OPENCLAW_SQLITE_BUSY_TIMEOUT_MS } from "./openclaw-state-db-contract-DYCYxE4w.js";
import { t as assertSqliteIntegrity } from "./sqlite-integrity-NpEtFIdK.js";
import { dt as resolveSqliteDatabaseFilePaths } from "./openclaw-state-db-BRTnL-D8.js";
import { i as resolveSessionFilePathCore } from "./paths-CXdaYWF_.js";
import "./agent-scope-DbtJyKUL.js";
import "./config-Cs0XXL3x.js";
import { k as resolveOpenClawAgentSqlitePath } from "./openclaw-agent-db-lease-Djvd6LWN.js";
import { V as migrateLegacySessionCreator, Y as OPENCLAW_AGENT_SCHEMA_SQL, r as migrateOpenClawAgentDatabaseForMaintenance, t as assertOpenClawAgentDatabaseForMaintenance, x as ensureOpenClawAgentDatabasePermissions } from "./openclaw-agent-db-maintenance-wTIy-jt-.js";
import { _ as withAgentDatabaseMaintenanceLease, b as closeOpenClawAgentDatabaseByPath, r as clearOpenClawAgentDatabaseOpenFailure, u as isOpenClawAgentDatabaseOpen } from "./openclaw-agent-db-CWtDoRbC.js";
import { i as resolveUnsuffixedSqliteTargetFromSessionStorePath } from "./session-sqlite-target-Dp-kgpcT.js";
import { a as normalizeStoreSessionKey } from "./store-entry-CzRELcpv.js";
import { o as resolveStoredSessionOwnerAgentId } from "./session-store-key-8xEjWSNi.js";
import { a as resolveAllAgentSessionStoreCandidateTargetsSync, i as resolveAgentSessionStoreTargetsSync, o as resolveAllAgentSessionStoreTargetsSync, u as resolveSessionStoreTargets } from "./targets-Cknmo7YZ.js";
import { n as normalizeLegacySessionEntryDelivery } from "./state-migrations.legacy-session-store-CQJX6yZy.js";
import { n as importSqliteSessionRowsBatch } from "./session-accessor.sqlite-import-Dt33AfH-.js";
import { C as moveMigrationArtifact, T as sameMigrationArtifact, _ as writeSessionSqliteMigrationManifest, b as isSessionSqliteMigrationWarning, f as recordCompletedMigrationMoves, g as updateMigrationManifestTarget, i as createSessionSqliteMigrationRun, m as resolveSessionSqliteMigrationRunsDir, n as assertSafeSessionSqliteMigrationMove, o as findLatestFailedSessionSqliteMigrationManifest, p as recordPlannedMigrationMoves, r as canonicalMigrationFilePath, t as assertSafeSessionSqliteMigrationDirectory, v as createDoctorSessionSqliteTargetReport, w as readMigrationArtifactIdentity, x as sumDoctorSessionSqliteTargets, y as createDoctorSessionSqliteTotals } from "./doctor-session-sqlite-migration-run-CJtP-RPT.js";
import { n as assertDoctorSqliteMaintenancePathsNotAliased, r as isDestructiveDoctorSessionSqliteMode } from "./doctor-sqlite-maintenance-lock-BDyGk9pJ.js";
import { i as restoreSessionSqliteMigrationRuns, n as reconcileSessionSqliteMigrationPublications, r as restoreSessionSqliteMigrationRun } from "./doctor-session-sqlite-restore-Bpz8ls7-.js";
import { a as readOnlySqliteDbStats, c as readTranscriptFingerprint, d as resolveTargetSqlitePath, f as scanReadOnlySqliteActiveTranscriptFiles, l as resolveLegacyTranscriptPaths, n as createTranscriptEventReader, o as readOnlySqliteValidationSnapshot, s as readSqliteEntryCount, t as countTranscriptEventsForPath, u as resolveTargetSqliteOptions } from "./doctor-session-sqlite-readers-DxvNNHSA.js";
import { i as writeSessionSqliteMigrationFailureReports, r as createSessionSqliteMigrationFailureIssue } from "./doctor-session-sqlite-failure-n7smFIuZ.js";
import { t as compactDoctorSqliteFile } from "./doctor-sqlite-compact-Cd8x-4Ie.js";
import fs from "node:fs";
import path from "node:path";
import os from "node:os";
//#region src/commands/doctor-session-sqlite-compact.ts
/** Runs doctor-owned SQLite file compaction for migrated session stores. */
/** Reclaim free pages from one agent session SQLite database. */
async function compactDoctorSessionSqliteTarget(target, options = {}) {
const databaseOptions = resolveTargetSqliteOptions(target, options.env);
const sqlitePath = resolveOpenClawAgentSqlitePath(databaseOptions);
const beforeFileSizes = readSqliteFileSizes(sqlitePath);
const stat = readSessionDatabaseStat(sqlitePath);
if (!stat) return {
dbSizeAfterBytes: 0,
dbSizeBeforeBytes: 0,
freelistAfterPages: 0,
freelistBeforePages: 0,
pageSizeBytes: 0,
reclaimedBytes: 0,
skipped: true,
walSizeAfterBytes: beforeFileSizes.walSizeBytes,
walSizeBeforeBytes: beforeFileSizes.walSizeBytes
};
if (!stat.isFile()) throw new Error(`OpenClaw agent database is not a regular file: ${sqlitePath}`);
if (isOpenClawAgentDatabaseOpen(sqlitePath)) throw new Error(`OpenClaw agent database ${sqlitePath} is already open in this process. Stop OpenClaw and retry.`);
const requireQuarantineCleared = () => {
if (!clearOpenClawAgentDatabaseOpenFailure(sqlitePath, { env: options.env })) throw new Error(`OpenClaw agent database ${sqlitePath} was repaired, but its persisted quarantine record could not be cleared. Rerun openclaw doctor --fix so the database is not refused again.`);
};
const compactTarget = () => {
if (options.operation === "import-finalize") {
migrateOpenClawAgentDatabaseForMaintenance({
agentId: databaseOptions.agentId,
pathname: sqlitePath
});
requireQuarantineCleared();
}
const compact = compactDoctorSqliteFile({
operation: options.operation,
afterSuccess: () => {
requireQuarantineCleared();
ensureOpenClawAgentDatabasePermissions(sqlitePath, databaseOptions);
},
sqlitePath,
validateBeforeMutation: (database) => assertOpenClawAgentDatabaseForMaintenance(database, {
agentId: databaseOptions.agentId,
pathname: sqlitePath
})
});
return {
dbSizeAfterBytes: compact.after.dbSizeBytes,
dbSizeBeforeBytes: compact.before.dbSizeBytes,
freelistAfterPages: compact.after.freelistPages,
freelistBeforePages: compact.before.freelistPages,
pageSizeBytes: compact.before.pageSizeBytes || compact.after.pageSizeBytes,
reclaimedBytes: compact.reclaimedBytes,
skipped: false,
walSizeAfterBytes: compact.after.walSizeBytes,
walSizeBeforeBytes: compact.before.walSizeBytes
};
};
return options.operation === "import-finalize" ? withAgentDatabaseMaintenanceLease({ env: databaseOptions.env }, async () => compactTarget()) : compactTarget();
}
function readSessionDatabaseStat(sqlitePath) {
try {
return fs.lstatSync(sqlitePath);
} catch (error) {
if (error.code === "ENOENT") return;
throw error;
}
}
function readSqliteFileSizes(sqlitePath) {
return {
dbSizeBytes: fileSize(sqlitePath),
walSizeBytes: fileSize(`${sqlitePath}-wal`)
};
}
function fileSize(filePath) {
try {
return fs.statSync(filePath).size;
} catch {
return 0;
}
}
//#endregion
//#region src/commands/doctor-session-sqlite-recover-report.ts
/** Builds doctor reports for session SQLite migration recovery mode. */
const CANONICAL_AGENT_INDEX_NAMES = getCanonicalSqliteNamedIndexContracts(OPENCLAW_AGENT_SCHEMA_SQL).map((index) => index.name);
/** Restores the latest failed migration run and validates only selected manifest targets. */
async function recoverDoctorSessionSqliteTargets(params) {
const trustedTargets = resolveRecoverTargets(params.targets, params.env);
const failedRun = findLatestFailedSessionSqliteMigrationManifest(params.env, trustedTargets);
if (!failedRun) {
const recoveredCorruptTargets = await withAgentDatabaseMaintenanceLease({ env: params.env }, async () => recoverCorruptSqliteTargets(params.targets, params.env));
if (recoveredCorruptTargets.length > 0) return summarizeRecoverReport(recoveredCorruptTargets);
return summarizeRecoverReport([createSyntheticRecoverTargetReport(params.env, "No failed session SQLite migration manifest found.")]);
}
const restore = await restoreSessionSqliteMigrationRun({
env: params.env,
manifestPath: failedRun.manifestPath,
trustedTargets
});
const targetReports = [];
for (const manifestTarget of failedRun.targets) targetReports.push(await params.validateTarget({
agentId: manifestTarget.agentId,
sqlitePath: manifestTarget.sqlitePath,
storePath: manifestTarget.storePath
}));
const reportTarget = targetReports[0] ?? createSyntheticRecoverTargetReport(params.env, failedRun.manifestPath);
reportTarget.restore = restore;
reportTarget.issues.push(...restore.conflicts.map((conflict) => ({
code: "restore_conflict",
message: `${conflict.sourcePath}: ${conflict.reason}`
})));
const failureReports = writeSessionSqliteMigrationFailureReports(failedRun.manifestPath, {
reason: "doctor recover restored and validated a failed session SQLite migration run",
trustedTargets
});
const report = summarizeRecoverReport(targetReports.length > 0 ? targetReports : [reportTarget]);
report.migrationRun = {
failureReportJsonPath: failureReports.jsonPath,
failureReportMarkdownPath: failureReports.markdownPath,
manifestPath: failedRun.manifestPath,
runId: failedRun.manifest.runId
};
report.supportIssue = createSessionSqliteMigrationFailureIssue(failedRun.manifestPath, trustedTargets);
return report;
}
function recoverCorruptSqliteTargets(targets, env) {
return targets.flatMap((target) => {
const databaseOptions = resolveTargetSqliteOptions(target, env);
const sqlitePath = resolveOpenClawAgentSqlitePath(databaseOptions);
let recoveryFiles;
try {
recoveryFiles = inspectSqliteRecoveryFiles(sqlitePath);
} catch (error) {
return [createRecoverInspectionFailureTargetReport(target, sqlitePath, error)];
}
if (recoveryFiles.existing.length === 0) return [];
if (!recoveryFiles.existing.includes(sqlitePath)) return [recoverCorruptSqliteTarget(target, sqlitePath, /* @__PURE__ */ new Error(`SQLite sidecars exist without their main database: ${sqlitePath}`))];
const inspection = inspectSqliteForRecovery(sqlitePath, recoveryFiles.existing);
if (inspection.ok) return [];
if (!isSqliteCorruptionError(inspection.error)) return [createRecoverInspectionFailureTargetReport(target, sqlitePath, inspection.error)];
if (!isCanonicalAgentIndexCorruptionError(inspection.error)) return [recoverCorruptSqliteTarget(target, sqlitePath, inspection.error)];
const repair = repairCanonicalIndexesForRecovery(databaseOptions, sqlitePath);
return [repair.ok ? createEmptyRecoverTargetReport(target, sqlitePath) : createRecoverInspectionFailureTargetReport(target, sqlitePath, repair.error)];
});
}
function repairCanonicalIndexesForRecovery(databaseOptions, sqlitePath) {
try {
migrateOpenClawAgentDatabaseForMaintenance({
agentId: databaseOptions.agentId,
pathname: sqlitePath
});
const sourcePaths = inspectSqliteRecoveryFiles(sqlitePath).existing;
const inspection = inspectSqliteForRecovery(sqlitePath, sourcePaths);
if (!inspection.ok) return inspection;
if (!clearOpenClawAgentDatabaseOpenFailure(sqlitePath, { env: databaseOptions.env })) throw new Error(`Repaired canonical SQLite indexes, but could not clear the quarantine for ${sqlitePath}.`);
return { ok: true };
} catch (error) {
return {
error,
ok: false
};
}
}
function inspectSqliteForRecovery(sqlitePath, sourcePaths) {
let inspectionDir;
let database;
let inspectionError;
try {
inspectionDir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-sqlite-recovery-"));
const inspectionPath = path.join(inspectionDir, path.basename(sqlitePath));
for (const sourcePath of sourcePaths) {
const inspectionFilePath = `${inspectionPath}${sourcePath.slice(sqlitePath.length)}`;
fs.copyFileSync(sourcePath, inspectionFilePath, fs.constants.COPYFILE_EXCL);
fs.chmodSync(inspectionFilePath, 384);
}
database = openNodeSqliteDatabase(inspectionPath);
database.exec(`PRAGMA busy_timeout = ${OPENCLAW_SQLITE_BUSY_TIMEOUT_MS};`);
database.exec("PRAGMA trusted_schema = OFF;");
assertSqliteIntegrity(database, inspectionPath);
} catch (error) {
inspectionError = error;
}
try {
database?.close();
} catch (error) {
inspectionError ??= error;
}
try {
if (inspectionDir) fs.rmSync(inspectionDir, {
force: true,
recursive: true
});
} catch (error) {
inspectionError ??= error;
}
return inspectionError === void 0 ? { ok: true } : {
error: inspectionError,
ok: false
};
}
function recoverCorruptSqliteTarget(target, sqlitePath, error) {
const report = createEmptyRecoverTargetReport(target, sqlitePath);
try {
report.corruptRecovery = moveCorruptSqliteFilesAside(sqlitePath);
} catch (moveError) {
report.issues.push({
code: "sqlite_corrupt_recovery_failed",
message: `${sqlitePath}: ${String(moveError)}; original error: ${String(error)}`
});
}
return report;
}
function createRecoverInspectionFailureTargetReport(target, sqlitePath, error) {
const report = createEmptyRecoverTargetReport(target, sqlitePath);
report.issues.push({
code: "sqlite_recovery_inspect_failed",
message: `${sqlitePath}: ${String(error)}`
});
return report;
}
function moveCorruptSqliteFilesAside(sqlitePath) {
const recoveryFiles = inspectSqliteRecoveryFiles(sqlitePath);
const moves = planCorruptSqliteMoves(recoveryFiles.existing);
const completed = [];
try {
for (const move of moves.toSorted((left, right) => {
if (left.sourcePath === sqlitePath) return 1;
if (right.sourcePath === sqlitePath) return -1;
return left.sourcePath.localeCompare(right.sourcePath);
})) {
fs.renameSync(move.sourcePath, move.destinationPath);
completed.push(move);
}
} catch (error) {
const rollbackErrors = [];
for (const move of completed.toReversed()) try {
if (pathExists(move.sourcePath)) throw new Error(`rollback source was recreated: ${move.sourcePath}`, { cause: error });
fs.renameSync(move.destinationPath, move.sourcePath);
} catch (rollbackError) {
rollbackErrors.push(rollbackError);
}
if (rollbackErrors.length > 0) {
const rollbackDetails = rollbackErrors.map((rollbackError) => String(rollbackError)).join("; ");
throw new Error(`Could not move corrupt SQLite file set aside or restore it: ${sqlitePath}; rollback failures: ${rollbackDetails}`, { cause: error });
}
throw error;
}
return {
movedFiles: moves.map((move) => move.destinationPath),
skippedFiles: recoveryFiles.missing
};
}
function inspectSqliteRecoveryFiles(sqlitePath) {
const existing = [];
const missing = [];
for (const candidate of resolveSqliteDatabaseFilePaths(sqlitePath)) try {
if (!fs.lstatSync(candidate).isFile()) throw new Error(`SQLite recovery path is not a regular file: ${candidate}`);
existing.push(candidate);
} catch (error) {
if (error.code === "ENOENT") {
missing.push(candidate);
continue;
}
throw error;
}
return {
existing,
missing
};
}
function planCorruptSqliteMoves(sourcePaths) {
const timestampSuffix = `.corrupt-${Date.now()}`;
for (let attempt = 0; attempt < 100; attempt += 1) {
const suffix = attempt === 0 ? timestampSuffix : `${timestampSuffix}.${attempt}`;
const moves = sourcePaths.map((sourcePath) => ({
destinationPath: `${sourcePath}${suffix}`,
sourcePath
}));
if (moves.every((move) => !pathExists(move.destinationPath))) return moves;
}
throw new Error(`Could not choose recovery paths for ${sourcePaths[0] ?? "SQLite files"}`);
}
function pathExists(filePath) {
try {
fs.lstatSync(filePath);
return true;
} catch (error) {
if (error.code === "ENOENT") return false;
throw error;
}
}
function isSqliteCorruptionError(error) {
const code = error && typeof error === "object" ? error.code : void 0;
if (code === "SQLITE_CORRUPT" || code === "SQLITE_NOTADB") return true;
const message = String(error).toLowerCase();
return message.includes("database disk image is malformed") || message.includes("not a database") || message.includes("sqlite quick_check failed") || message.includes("sqlite integrity_check failed") || message.includes("sqlite foreign_key_check failed");
}
function isCanonicalAgentIndexCorruptionError(error) {
if (!(error instanceof Error) || error.name !== "SqliteIntegrityError") return false;
return CANONICAL_AGENT_INDEX_NAMES.some((indexName) => error.message.includes(indexName));
}
function resolveRecoverTargets(targets, env) {
return targets.map((target) => ({
...target,
sqlitePath: resolveTargetSqlitePath(target, env)
}));
}
function createSyntheticRecoverTargetReport(env, message) {
return createDoctorSessionSqliteTargetReport({
agentId: "recover",
issues: [{
code: "recover_manifest_missing",
message
}],
sqlitePath: "",
storePath: resolveSessionSqliteMigrationRunsDir(env)
});
}
function createEmptyRecoverTargetReport(target, sqlitePath) {
return createDoctorSessionSqliteTargetReport({
agentId: target.agentId,
sqlitePath,
storePath: target.storePath
});
}
function summarizeRecoverReport(targets) {
const sum = (value) => sumDoctorSessionSqliteTargets(targets, value);
return {
mode: "recover",
targets,
totals: createDoctorSessionSqliteTotals(targets, {
legacyEntries: sum((target) => target.legacyEntries),
unreferencedJsonlFiles: sum((target) => target.unreferencedJsonlFiles.length),
validatedEntries: sum((target) => target.validatedEntries),
validatedTranscriptEvents: sum((target) => target.validatedTranscriptEvents)
})
};
}
//#endregion
//#region src/commands/doctor-session-sqlite-restore-report.ts
async function restoreDoctorSessionSqliteTargets(params) {
const targetReports = params.targets.map((target) => createEmptyTargetReport(target));
const trustedTargets = params.targets.map((target) => ({
...target,
sqlitePath: resolveTargetSqlitePath(target)
}));
const restore = await restoreSessionSqliteMigrationRuns({
env: params.env,
trustedTargets
});
const reportTarget = targetReports[0] ?? createSyntheticRestoreTargetReport(params.env, restore.manifestPaths[0] ?? resolveSessionSqliteMigrationRunsDir(params.env));
reportTarget.restore = restore;
reportTarget.issues.push(...restore.conflicts.map((conflict) => ({
code: "restore_conflict",
message: `${conflict.sourcePath}: ${conflict.reason}`
})));
return summarizeRestoreReport(targetReports.length > 0 ? targetReports : [reportTarget]);
}
function createEmptyTargetReport(target) {
return createDoctorSessionSqliteTargetReport({
agentId: target.agentId,
sqliteEntries: readSqliteEntryCount(target),
sqlitePath: resolveTargetSqlitePath(target),
storePath: target.storePath
});
}
function createSyntheticRestoreTargetReport(env, manifestPath) {
return createDoctorSessionSqliteTargetReport({
agentId: "restore",
sqlitePath: "",
storePath: manifestPath || resolveSessionSqliteMigrationRunsDir(env)
});
}
function summarizeRestoreReport(targets) {
return {
mode: "restore",
targets,
totals: createDoctorSessionSqliteTotals(targets)
};
}
//#endregion
//#region src/commands/doctor-session-sqlite.ts
const SESSION_IMPORT_BATCH_SIZE = 256;
/**
* Runs the targeted doctor SQLite session migration/inspection submode.
* Destructive production callers hold the Gateway/SQLite-maintenance state lock for the full call.
*/
async function runDoctorSessionSqlite(options) {
const env = options.env ?? process.env;
const cfg = resolveDoctorSessionSqliteConfig(options);
const targets = filterLegacySessionStoreTargets(resolveDoctorSessionSqliteTargets({
...options,
cfg,
env
}), options.mode);
if (isDestructiveDoctorSessionSqliteMode(options.mode)) {
const maintenancePaths = resolveDoctorSessionSqliteMaintenancePaths(targets);
assertDoctorSqliteMaintenancePathsNotAliased(`session SQLite ${options.mode}`, maintenancePaths, resolveDoctorSessionSqliteMaintenanceRoots(targets, env));
}
if (options.mode === "restore") return restoreDoctorSessionSqliteTargets({
env,
targets
});
if (options.mode === "recover") return recoverDoctorSessionSqliteTargets({
env,
options,
targets,
validateTarget: (target) => inspectOrMigrateTarget({
cfg,
env,
mode: "validate",
target
})
});
if (options.mode === "import") await reconcileSessionSqliteMigrationPublications({
env,
trustedTargets: targets.map(createMigrationTargetInput)
});
const activeRun = options.mode === "import" && targets.length > 0 ? createSessionSqliteMigrationRun(env, targets.map(createMigrationTargetInput)) : void 0;
const coverage = options.mode === "import" ? gatherLegacyArchiveCoverage(cfg, env, targets) : void 0;
const reports = [];
const archiveTargets = [];
for (const target of targets) reports.push(await inspectOrMigrateTarget({
activeRun,
archiveTargets,
cfg,
env,
mode: options.mode,
target
}));
if (activeRun && coverage) {
await archiveLegacyArtifacts(archiveTargets, coverage, activeRun);
for (const [index, target] of targets.entries()) {
const report = reports[index];
appendActiveSqliteTranscriptFileIssues(target, report);
updateMigrationManifestTarget(activeRun, createMigrationTargetInput(target), report.issues);
}
await archiveImportedLegacySessionStores(archiveTargets, activeRun, coverage);
const hasBlockingIssues = reports.some((report) => blockingIssueCount(report) > 0);
activeRun.manifest.completedAt = (/* @__PURE__ */ new Date()).toISOString();
if (hasBlockingIssues) {
activeRun.manifest.failedAt = activeRun.manifest.completedAt;
const failureReports = writeSessionSqliteMigrationFailureReports(activeRun.manifestPath, { reason: "doctor import reported session SQLite migration issues" });
activeRun.manifest.failureReports = failureReports;
}
writeSessionSqliteMigrationManifest(activeRun);
}
return summarizeDoctorSessionSqliteReport(options.mode, reports, activeRun);
}
/** Called only under the public maintenance lock, before its strict alias recheck. */
async function reconcileDoctorSessionSqlitePublication(options, sourcePath) {
const env = options.env ?? process.env;
const cfg = resolveDoctorSessionSqliteConfig(options);
const targets = resolveDoctorSessionSqliteTargets({
...options,
cfg,
env
});
assertDoctorSqliteMaintenancePathsNotAliased(`session SQLite ${options.mode}`, resolveDoctorSessionSqliteMaintenancePaths(targets), resolveDoctorSessionSqliteMaintenanceRoots(targets, env));
await reconcileSessionSqliteMigrationPublications({
env,
sourcePath,
trustedTargets: targets.map(createMigrationTargetInput)
});
}
function resolveDoctorSessionSqliteMaintenancePaths(targets) {
const protectedPaths = /* @__PURE__ */ new Set();
for (const target of targets) for (const databasePath of resolveSqliteDatabaseFilePaths(resolveTargetSqlitePath(target))) protectedPaths.add(databasePath);
return [...protectedPaths];
}
function resolveDoctorSessionSqliteMaintenanceRoots(targets, env) {
const stateDir = path.resolve(resolveStateDir(env));
const roots = /* @__PURE__ */ new Set([stateDir]);
for (const target of targets) {
const sqlitePath = resolveTargetSqlitePath(target);
if (isPathWithin(stateDir, target.storePath) && isPathWithin(stateDir, sqlitePath)) continue;
const commonRoot = commonPathAncestor(path.dirname(target.storePath), path.dirname(sqlitePath));
const parentRoot = path.dirname(commonRoot);
roots.add(parentRoot === path.parse(commonRoot).root ? commonRoot : parentRoot);
}
return [...roots];
}
function isPathWithin(rootPath, candidatePath) {
return isPathInside(rootPath, path.resolve(candidatePath));
}
function commonPathAncestor(leftPath, rightPath) {
let currentPath = path.resolve(leftPath);
const resolvedRightPath = path.resolve(rightPath);
while (!isPathWithin(currentPath, resolvedRightPath)) {
const parentPath = path.dirname(currentPath);
if (parentPath === currentPath) return currentPath;
currentPath = parentPath;
}
return currentPath;
}
function resolveDoctorSessionSqliteConfig(options) {
if (options.cfg) return options.cfg;
const requestedAgentId = normalizeAgentId(options.agent ?? "main");
return options.store ? { agents: { entries: { [requestedAgentId]: { default: true } } } } : getRuntimeConfig();
}
function resolveDoctorSessionSqliteTargets(params) {
if (params.store) return resolveSessionStoreTargets(params.cfg, { store: params.store }, { env: params.env });
if (params.mode === "restore" || params.mode === "recover") {
const candidates = resolveAllAgentSessionStoreCandidateTargetsSync(params.cfg, { env: params.env });
if (!params.agent) return candidates;
const requestedAgentId = normalizeAgentId(params.agent);
return candidates.filter((target) => normalizeAgentId(target.agentId) === requestedAgentId);
}
if (params.agent) return resolveAgentSessionStoreTargetsSync(params.cfg, params.agent, { env: params.env });
if (params.allAgents) {
const targets = resolveAllAgentSessionStoreTargetsSync(params.cfg, { env: params.env });
if (params.mode !== "dry-run" && params.mode !== "import" && params.mode !== "validate") return targets;
const legacyStorePath = path.join(resolveStateDir(params.env), "sessions", "sessions.json");
if (!fs.existsSync(legacyStorePath)) return targets;
return [...resolveSessionStoreTargets(params.cfg, { allAgents: true }, { env: params.env }).map((target) => ({
agentId: target.agentId,
sqlitePath: resolveTargetSqlitePath(target),
storePath: legacyStorePath
})), ...targets];
}
return resolveSessionStoreTargets(params.cfg, {}, { env: params.env });
}
function filterLegacySessionStoreTargets(targets, mode) {
if (mode === "inspect" || mode === "compact" || mode === "restore" || mode === "recover") return targets;
return targets.filter((target) => !target.storePath.endsWith(".sqlite") && fs.existsSync(target.storePath));
}
async function inspectOrMigrateTarget(params) {
const issues = [];
const isSqliteStore = params.target.storePath.endsWith(".sqlite");
const allRecords = isSqliteStore ? [] : readLegacySessionRecords(params.target, issues, { allowMissingStore: params.mode === "inspect" || params.mode === "compact" });
const records = shouldFilterLegacySessionRecordsByTarget(params.target) ? allRecords.filter((record) => isLegacySessionRecordOwnedByTarget(params.cfg, params.target, record.sessionKey)) : allRecords;
const referencedTranscriptFiles = new Set(allRecords.flatMap((record) => record.transcriptPath ? [record.transcriptPath] : []));
const report = createDoctorSessionSqliteTargetReport({
agentId: params.target.agentId,
archivedLegacyStoreFiles: [],
issues,
legacyEntries: records.length,
referencedTranscriptFiles: referencedTranscriptFiles.size,
sqliteEntries: readSqliteEntryCount(params.target),
sqlitePath: resolveTargetSqlitePath(params.target),
storePath: params.target.storePath,
unreferencedJsonlFiles: isSqliteStore ? [] : listUnreferencedJsonlFiles(params.target.storePath, [...referencedTranscriptFiles])
});
if (params.mode === "compact") {
await compactSqliteDatabase(params.target, report, { env: params.env });
report.sqliteEntries = readSqliteEntryCount(params.target);
}
if (isSqliteStore || params.mode === "inspect" || params.mode === "compact") {
appendSqliteDbStats(params.target, report);
if (params.mode !== "compact") appendActiveSqliteTranscriptFileIssues(params.target, report);
return report;
}
if (params.mode === "import") await importLegacySessionRecords(params.target, records, report);
else if (params.mode === "dry-run") for (const record of records) countLegacyTranscript(record, report);
else validateLegacySessionRecords(params.target, records, report);
let validationPassed = false;
if (params.mode === "import" && blockingIssueCount(report) === 0) {
validationPassed = validateImportedTargetBeforeArchive(params.target, records, report);
updateMigrationManifestTarget(params.activeRun, createMigrationTargetInput(params.target), report.issues, { validationBeforeArchive: validationPassed ? "passed" : "failed" });
if (validationPassed) await compactSqliteDatabase(params.target, report, {
env: params.env,
operation: "import-finalize"
});
}
if (params.mode === "import") params.archiveTargets?.push({
target: createMigrationTargetInput(params.target),
report,
validated: validationPassed,
records: records.map(({ entry, ...record }) => Object.assign(record, { sessionId: entry.sessionId }))
});
report.sqliteEntries = readSqliteEntryCount(params.target);
if (params.mode !== "import") appendActiveSqliteTranscriptFileIssues(params.target, report);
updateMigrationManifestTarget(params.activeRun, createMigrationTargetInput(params.target), report.issues);
return report;
}
function gatherLegacyArchiveCoverage(cfg, env, targets) {
const selectedStorePaths = /* @__PURE__ */ new Set();
const referencedPaths = /* @__PURE__ */ new Set();
const retainedPaths = /* @__PURE__ */ new Set();
const incompleteDirectories = /* @__PURE__ */ new Set();
const retainedDirectories = /* @__PURE__ */ new Set();
const indexIdentities = /* @__PURE__ */ new Map();
const targetsByStore = /* @__PURE__ */ new Map();
for (const target of targets) {
const storePath = canonicalMigrationFilePath(target.storePath);
targetsByStore.set(storePath, [...targetsByStore.get(storePath) ?? [], target]);
}
const directories = new Set([...targetsByStore.keys()].map((store) => path.dirname(store)));
const knownStores = new Map([...resolveAllAgentSessionStoreCandidateTargetsSync(cfg, { env }), ...targets].map((target) => [canonicalMigrationFilePath(target.storePath), target]));
for (const [storePath, target] of knownStores) {
if (storePath.endsWith(".sqlite") || !directories.has(path.dirname(storePath)) || !fs.existsSync(storePath)) continue;
const storeTargets = targetsByStore.get(storePath) ?? [];
const issues = [];
let records;
try {
assertSafeSessionSqliteMigrationDirectory(path.dirname(storePath));
indexIdentities.set(storePath, readMigrationArtifactIdentity(storePath));
records = readLegacySessionRecords(target, issues);
} catch (error) {
if (storeTargets.length > 0) throw error;
incompleteDirectories.add(path.dirname(storePath));
retainedDirectories.add(path.dirname(storePath));
continue;
}
const keys = [...records.map((record) => record.sessionKey), ...issues.flatMap((issue) => issue.sessionKey ? [issue.sessionKey] : [])];
const selected = storeTargets.length > 0 && issues.every(isSessionSqliteMigrationWarning) && keys.every((sessionKey) => storeTargets.some((candidate) => !shouldFilterLegacySessionRecordsByTarget(candidate) || isLegacySessionRecordOwnedByTarget(cfg, candidate, sessionKey)));
if (issues.length > 0) {
incompleteDirectories.add(path.dirname(storePath));
if (!selected) retainedDirectories.add(path.dirname(storePath));
}
if (selected) selectedStorePaths.add(storePath);
for (const record of records) {
if (!record.transcriptPath) continue;
for (const source of [
record.transcriptPath,
resolveTrajectoryPath(record.transcriptPath),
resolveTrajectoryPointerPath(record.transcriptPath)
]) {
if (!source) continue;
const canonical = canonicalMigrationFilePath(source);
referencedPaths.add(canonical);
if (!selected) retainedPaths.add(canonical);
}
}
}
return {
selectedStorePaths,
referencedPaths,
retainedPaths,
incompleteDirectories,
retainedDirectories,
indexIdentities
};
}
function readLegacySessionRecords(target, issues, options = {}) {
const openFlags = process.platform === "win32" ? "r" : fs.constants.O_RDONLY | fs.constants.O_NONBLOCK;
let fd;
try {
fd = fs.openSync(target.storePath, openFlags);
} catch (err) {
const nodeErr = err;
if (options.allowMissingStore === true && nodeErr.code === "ENOENT") {
try {
if (!fs.statSync(path.dirname(target.storePath)).isDirectory()) issues.push({
code: "store_unreadable",
message: `${target.storePath}: parent path is not a directory`
});
} catch (parentErr) {
if (parentErr.code !== "ENOENT") issues.push({
code: "store_unreadable",
message: `${target.storePath}: ${String(parentErr)}`
});
}
return [];
}
issues.push({
code: "store_unreadable",
message: `${target.storePath}: ${String(err)}`
});
return [];
}
try {
let parsed;
try {
const storeStat = fs.fstatSync(fd);
if (!storeStat.isFile()) {
issues.push({
code: "store_unreadable",
message: `${target.storePath}: not a regular file`
});
return [];
}
const raw = readFileDescriptorBoundedSync(fd, storeStat.size).toString("utf-8");
parsed = JSON.parse(raw);
} catch (err) {
issues.push({
code: "store_unreadable",
message: `${target.storePath}: ${String(err)}`
});
return [];
}
if (!isRecord(parsed)) {
issues.push({
code: "store_not_object",
message: `${target.storePath} does not contain an object session store.`
});
return [];
}
const records = [];
for (const [sessionKey, value] of Object.entries(parsed)) {
if (!isSessionEntry(value)) {
issues.push({
code: "entry_invalid",
message: "Session entry is missing a valid sessionId.",
sessionKey
});
continue;
}
const transcript = resolveLegacyTranscriptPaths(target, value);
records.push({
entry: migrateLegacySessionCreator(normalizeLegacySessionEntryDelivery(value)),
sessionKey,
...transcript
});
}
return records;
} finally {
fs.closeSync(fd);
}
}
function isLegacySessionRecordOwnedByTarget(cfg, target, sessionKey) {
if (target.sqlitePath) {
const ownerAgentId = parseAgentSessionKey(sessionKey)?.agentId ?? cfg.agents?.defaults?.sessionStore?.agentId?.trim() ?? tryResolveLegacyCompatibilityAgentId(cfg);
return ownerAgentId ? normalizeAgentId(ownerAgentId) === normalizeAgentId(target.agentId) : false;
}
const ownerAgentId = resolveStoredSessionOwnerAgentId({
cfg,
agentId: target.agentId,
sessionKey
});
return ownerAgentId ? ownerAgentId === target.agentId : target.agentId === tryResolveDefaultAgentId(cfg);
}
function shouldFilterLegacySessionRecordsByTarget(target) {
return !resolveUnsuffixedSqliteTargetFromSessionStorePath(target.storePath).agentId;
}
function countLegacyTranscript(record, report) {
const result = countTranscriptEvents(record);
if (result.status === "missing") {
report.issues.push({
code: "transcript_missing",
message: `Transcript file is missing: ${record.transcriptPath}`,
sessionKey: record.sessionKey
});
return;
}
if (result.status === "malformed") {
report.issues.push({
code: "transcript_malformed",
message: result.message,
sessionKey: record.sessionKey
});
return;
}
report.validatedEntries += 1;
report.validatedTranscriptEvents += result.events;
}
function blockingIssueCount(report) {
return report.issues.filter((issue) => !isSessionSqliteMigrationWarning(issue)).length;
}
async function importLegacySessionRecords(target, records, report) {
if (records.length === 0) return;
const importedTranscriptSources = /* @__PURE__ */ new Set();
const existingSnapshot = readOnlySqliteValidationSnapshot(target);
for (let offset = 0; offset < records.length; offset += SESSION_IMPORT_BATCH_SIZE) {
const pending = records.slice(offset, offset + SESSION_IMPORT_BATCH_SIZE).flatMap((record) => {
const prepared = prepareLegacySessionImport(target, record, report, importedTranscriptSources, existingSnapshot.ok ? existingSnapshot.snapshot : void 0);
return prepared ? [prepared] : [];
});
const imported = await importSqliteSessionRowsBatch(pending.map((entry) => entry.params));
for (const [index, result] of imported.entries()) {
const sessionKey = pending[index]?.params.sessionKey;
const record = records.find((candidate) => candidate.sessionKey === sessionKey);
if (record && result.recovery) record.recovery = result.recovery;
}
report.importedEntries += imported.length;
report.importedTranscriptEvents += imported.reduce((total, result) => total + result.transcriptEvents, 0);
report.issues.push(...pending.flatMap((entry) => entry.issue ? [entry.issue] : []));
}
}
function prepareLegacySessionImport(target, record, report, importedTranscriptSources, existingSnapshot) {
const transcriptSourceKey = record.transcriptPath ? `${record.entry.sessionId}\0${record.transcriptPath}` : void 0;
const transcriptFingerprint = transcriptSourceKey !== void 0 && !importedTranscriptSources.has(transcriptSourceKey) && record.transcriptPath && fs.existsSync(record.transcriptPath) ? readTranscriptFingerprint(record.transcriptPath) : void 0;
record.sourceFingerprint = transcriptFingerprint;
const result = countTranscriptEvents(record);
const transcriptMtimeMs = readLegacyTranscriptMtimeMs(record);
const params = {
allowMalformedRowRepair: true,
repairLegacyTranscript: true,
agentId: target.agentId,
entry: record.entry,
preserveExactStoredKey: true,
sessionKey: record.sessionKey,
storePath: target.sqlitePath ?? target.storePath
};
if (result.status === "missing") {
if (markAlreadyMigratedTranscript(record, report, existingSnapshot)) return;
return {
issue: {
code: "transcript_missing",
message: `Transcript file is missing: ${record.transcriptPath}`,
sessionKey: record.sessionKey
},
params
};
}
if (transcriptSourceKey) importedTranscriptSources.add(transcriptSourceKey);
return {
...result.status === "malformed" ? { issue: {
code: "transcript_malformed",
message: result.message,
sessionKey: record.sessionKey
} } : {},
params: {
...params,
...record.transcriptPath && transcriptFingerprint ? { readTranscriptEvents: createTranscriptEventReader(record.transcriptPath, record.entry.sessionId, result.status === "malformed", transcriptFingerprint) } : {},
...transcriptMtimeMs !== void 0 ? { transcriptMtimeMs } : {}
}
};
}
function markAlreadyMigratedTranscript(record, report, snapshot) {
const migratedEvents = countAlreadyMigratedTranscriptEventsForImport(snapshot, record);
if (migratedEvents === void 0) return false;
report.validatedEntries += 1;
report.validatedTranscriptEvents += migratedEvents;
return true;
}
function validateImportedTargetBeforeArchive(target, records, report) {
if (records.length === 0) return true;
const issueCountBeforeValidation = report.issues.length;
const validation = readOnlySqliteValidationSnapshot(target);
if (!validation.ok) {
report.issues.push({
code: "sqlite_read_failed",
message: `SQLite validation read failed: ${String(validation.error)}`
});
return false;
}
for (const record of records) validateImportedRecordBeforeArchive(record, report, validation.snapshot);
return report.issues.length === issueCountBeforeValidation;
}
function validateImportedRecordBeforeArchive(record, report, snapshot) {
const normalizedKey = record.sessionKey;
const sqliteSessionId = snapshot.sessionIdsBySessionKey.get(normalizedKey);
if (!sqliteSessionId) {
report.issues.push({
code: "sqlite_entry_missing",
message: `SQLite entry is missing for ${normalizedKey}.`,
sessionKey: record.sessionKey
});
return;
}
if (sqliteSessionId !== record.entry.sessionId) {
report.issues.push({
code: "sqlite_entry_mismatch",
message: `SQLite sessionId ${sqliteSessionId} does not match ${record.entry.sessionId}.`,
sessionKey: record.sessionKey
});
return;
}
const result = countTranscriptEvents(record);
if (result.status === "missing") return;
if (result.status !== "ok") {
if (!hasSessionIssue(report, "transcript_malformed", record.sessionKey)) report.issues.push({
code: "transcript_malformed",
message: result.message,
sessionKey: record.sessionKey
});
return;
}
const sqliteEvents = snapshot.transcriptEventCountsBySessionId.get(record.entry.sessionId) ?? 0;
const expectedEvents = record.recovery?.events ?? result.events;
if (sqliteEvents < expectedEvents) report.issues.push({
code: "sqlite_transcript_count_mismatch",
message: `SQLite transcript has ${sqliteEvents} events; verified import expects ${expectedEvents}.`,
sessionKey: record.sessionKey
});
}
async function archiveLegacyArtifacts(owners, coverage, activeRun) {
const { selectedStorePaths, referencedPaths, retainedPaths, incompleteDirectories, retainedDirectories } = coverage;
const references = /* @__PURE__ */ new Map();
for (const owner of owners) {
if (!owner.validated || blockingIssueCount(owner.report) > 0) selectedStorePaths.delete(owner.target.storePath);
for (const record of owner.records) {
if (!record.transcriptPath) continue;
const source = canonicalMigrationFilePath(record.transcriptPath);
references.set(source, [...references.get(source) ?? [], {
owner,
record
}]);
}
}
const retainedSources = [...references].filter(([source, refs]) => retainedPaths.has(source) || retainedDirectories.has(path.dirname(source)) || refs.some(({ owner }) => !selectedStorePaths.has(owner.target.storePath))).map(([source]) => source);
for (const source of retainedSources) {
for (const file of [
source,
resolveTrajectoryPath(source),
resolveTrajectoryPointerPath(source)
]) if (file) retainedPaths.add(file);
for (const { owner } of references.get(source) ?? []) {
const storePath = owner.target.storePath;
if (!selectedStorePaths.delete(storePath)) continue;
for (const sibling of owners.filter((item) => item.target.storePath === storePath)) for (const record of sibling.records) {
if (!record.transcriptPath) continue;
const siblingSource = canonicalMigrationFilePath(record.transcriptPath);
if (!retainedPaths.has(siblingSource)) {
retainedPaths.add(siblingSource);
retainedSources.push(siblingSource);
}
}
}
}
const reservedArchivePaths = /* @__PURE__ */ new Set();
const planned = /* @__PURE__ */ new Map();
const recordFailure = (owner, source, error, unreferenced = false) => {
owner.report.issues.push({
code: unreferenced ? "unreferenced_jsonl_archive_failed" : "transcript_archive_failed",
message: `${source}: ${formatErrorMessage(error)}`
});
};
for (const [source, refs] of references) {
const first = refs[0];
if (!fs.existsSync(source)) {
if (refs.some(({ record }) => record.sourceFingerprint)) for (const owner of new Set(refs.map((ref) => ref.owner))) recordFailure(owner, source, "Imported transcript disappeared before archival");
continue;
}
if (retainedPaths.has(source) || retainedDirectories.has(path.dirname(source))) {
for (const { owner, record } of refs) if (blockingIssueCount(owner.report) === 0) owner.report.issues.push({
code: "transcript_archive_deferred",
message: `${source}: retaining the original for an incomplete or unselected importing owner; rerun import for all known owners after resolving their index/import issues.`,
sessionKey: record.sessionKey
});
continue;
}
try {
const moves = planImportedTranscriptArtifactsToArchive(first.owner.target, first.record.sessionKey, source, reservedArchivePaths);
const imports = refs.map(({ owner, record }) => record.sourceFingerprint ? record : refs.find((ref) => ref.owner === owner && ref.record.sessionId === record.sessionId && ref.record.sourceFingerprint)?.record);
const fingerprints = imports.flatMap((record) => record?.sourceFingerprint ? [record.sourceFingerprint] : []);
const fingerprint = fingerprints[0];
if (fingerprint && fingerprints.some((current) => [
"ctimeNs",
"dev",
"ino",
"mtimeNs",
"size"
].some((key) => current[key] !== fingerprint[key]))) throw new Error("Transcript changed between imports; retaining the unverified original");
const complete = !incompleteDirectories.has(path.dirname(source)) && imports.every((record) => record?.sourceFingerprint && record.recovery?.complete) && refs.every(({ owner, record }) => !hasSessionIssue(owner.report, "transcript_malformed", record.sessionKey));
for (const move of moves) {
if (retainedPaths.has(move.sourcePath)) throw new Error("Artifact is required by an incomplete importing owner");
move.artifact = {
identity: readMigrationArtifactIdentity(move.sourcePath, 1n, move.kind === "transcript" ? fingerprint : void 0),
classification: complete && move.kind === "transcript" ? imports.some((record) => record?.recovery?.repaired) ? "repair-original" : "imported" : "protected",
reason: complete && move.kind === "transcript" ? "verified-import-original" : "unimported-or-unknown-history",
dependencies: [],
disposal: { state: "retained" }
};
const existing = planned.get(move.sourcePath);
if (existing) {
if (move.artifact.classification === "protected") existing.move.artifact = move.artifact;
for (const ref of refs) existing.owners.set(ref.owner, ref.record.sessionKey);
} else planned.set(move.sourcePath, {
move,
owners: new Map(refs.map((ref) => [ref.owner, ref.record.sessionKey]))
});
}
} catch (error) {
for (const owner of new Set(refs.map((ref) => ref.owner))) recordFailure(owner, source, error);
}
}
for (const owner of owners) {
const storePath = owner.target.storePath;
if (!selectedStorePaths.has(storePath) || blockingIssueCount(owner.report) > 0 || incompleteDirectories.has(path.dirname(storePath))) continue;
for (const source of listUnreferencedJsonlFiles(storePath, [...referencedPaths, ...planned.keys()])) try {
const move = planSessionJsonlArchiveMove({
archiveKey: "archive-tier",
baseNameRaw: path.basename(source),
kind: "unreferenced-jsonl",
reservedArchivePaths,
sourcePathRaw: source,
target: owner.target
});
move.artifact = {
identity: readMigrationArtifactIdentity(source),
classification: "protected",
reason: "unreferenced-history",
dependencies: [],
disposal: { state: "retained" }
};
reservedArchivePaths.add(move.archivePath);
planned.set(source, {
move,
owners: /* @__PURE__ */ new Map([[owner, void 0]])
});
} catch (error) {
recordFailure(owner, source, error, true);
}
}
const movesForOwner = (owner) => [...planned.values()].filter((item) => item.owners.has(owner)).map(({ move, owners: refs }) => Object.assign({}, move, { sessionKey: refs.get(owner) }));
for (const owner of owners) recordPlannedMigrationMoves(activeRun, owner.target, movesForOwner(owner));
const completed = /* @__PURE__ */ new Set();
for (const { move, owners: referencingOwners } of planned.values()) try {
for (const owner of referencingOwners.keys()) assertSafeSessionSqliteMigrationMove(move, owner.target);
await moveMigrationArtifact(move.sourcePath, move.archivePath, move.artifact.identity);
completed.add(move.sourcePath);
const report = [...referencingOwners.keys()][0].report;
(move.kind === "unreferenced-jsonl" ? report.archivedUnreferencedJsonlFiles : report.archivedTranscriptFiles).push(move.archivePath);
} catch (error) {
for (const owner of referencingOwners.keys()) recordFailure(owner, move.sourcePath, error, move.kind === "unreferenced-jsonl");
}
for (const owner of owners) {
recordCompletedMigrationMoves(activeRun, owner.target, movesForOwner(owner).filter((move) => completed.has(move.sourcePath)));
owner.report.unreferencedJsonlFiles = listUnreferencedJsonlFiles(owner.target.storePath, [...referencedPaths]);
}
}
async function archiveImportedLegacySessionStores(owners, activeRun, coverage) {
const byStore = /* @__PURE__ */ new Map();
for (const owner of owners) {
const storePath = owner.target.storePath;
byStore.set(storePath, [...byStore.get(storePath) ?? [], owner]);
}
for (const [storePath, entries] of byStore) {
if (!coverage.selectedStorePaths.has(storePath) || entries.some(({ report }) => blockingIssueCount(report) > 0)) continue;
const first = entries[0];
let publicationPlanned = false;
try {
const expected = coverage.indexIdentities.get(storePath);
if (!expected || !sameMigrationArtifact(readMigrationArtifactIdentity(storePath), expected)) throw new Error("Session index changed after import; retaining the unverified original");
const move = planSessionJsonlArchiveMove({
archiveKey: "legacy-store",
baseNameRaw: path.basename(storePath),
kind: "legacy-store",
sourcePathRaw: storePath,
target: first.target
});
const transcripts = activeRun.manifest.targets.filter((target) => target.storePath === storePath).flatMap((target) => target.plannedMoves.filter((item) => item.kind === "transcript"));
const complete = entries.every(({ validated, report }) => validated && report.issues.length === 0) && transcripts.every((item) => item.artifact?.classification !== "protected");
const dependencies = entries.flatMap(({ records }) => records.flatMap((record) => record.transcriptDependencies)).map(canonicalMigrationFilePath);
move.artifact = {
identity: expected,
classification: complete ? "imported" : "protected",
reason: complete ? "verified-index-import" : "incomplete-index-import",
dependencies: [...new Set(dependencies)],
disposal: { state: "retained" }
};
for (const { target } of entries) {
recordPlannedMigrationMoves(activeRun, target, [move]);
assertSafeSessionSqliteMigrationMove(move, target);
}
publicationPlanned = true;
await moveMigrationArtifact(move.sourcePath, move.archivePath, expected);
for (const { target } of entries) recordCompletedMigrationMoves(activeRun, target, [move]);
first.report.archivedLegacyStoreFiles.push(move.archivePath);
} catch (error) {
for (const { report, target } of entries) {
report.issues.push({
code: "legacy_store_archive_failed",
message: `${storePath}: ${formatErrorMessage(error)}`
});
if (!publicationPlanned) updateMigrationManifestTarget(activeRun, target, report.issues);
}
}
}
}
function validateLegacySessionRecords(target, records, report) {
const validation = readOnlySqliteValidationSnapshot(target);
if (!validation.ok) {
report.issues.push({
code: "sqlite_read_failed",
message: `SQLite validation read failed: ${String(validation.error)}`
});
return;
}
for (const record of records) validateLegacySessionRecord(record, report, validation.snapshot);
}
function validateLegacySessionRecord(record, report, snapshot) {
const normalizedKey = normalizeStoreSessionKey(record.sessionKey);
const sqliteSessionId = snapshot.sessionIdsBySessionKey.get(normalizedKey);
if (!sqliteSessionId) {
report.issues.push({
code: "sqlite_entry_missing",
message: `SQLite entry is missing for ${normalizedKey}.`,
sessionKey: record.sessionKey
});
return;
}
if (sqliteSessionId !== record.entry.sessionId) {
report.issues.push({
code: "sqlite_entry_mismatch",
message: `SQLite sessionId ${sqliteSessionId} does not match ${record.entry.sessionId}.`,
sessionKey: record.sessionKey
});
return;
}
report.validatedEntries += 1;
validateTranscriptEventCount(record, report, snapshot);
}
function validateTranscriptEventCount(record, report, snapshot) {
const result = countTranscriptEvents(record);
if (result.status === "missing") {
const migratedEvents = countAlreadyMigratedTranscriptEventsForValidate(snapshot, record);
if (migratedEvents !== void 0) report.validatedTranscriptEvents += migratedEvents;
return;
}
if (result.status !== "ok") {
if (!hasSessionIssue(report, "transcript_malformed", record.sessionKey)) report.issues.push({
code: "transcript_malformed",
message: result.message,
sessionKey: record.sessionKey
});
return;
}
const sqliteEvents = snapshot.transcriptEventCountsBySessionId.get(record.entry.sessionId) ?? 0;
if (sqliteEvents !== result.events) {
report.issues.push({
code: "sqlite_transcript_count_mismatch",
message: `SQLite transcript has ${sqliteEvents} events; source has ${result.events}.`,
sessionKey: record.sessionKey
});
return;
}
report.validatedTranscriptEvents += sqliteEvents;
}
function hasSessionIssue(report, code, sessionKey) {
return report.issues.some((issue) => issue.code === code && issue.sessionKey === sessionKey);
}
function countAlreadyMigratedTranscriptEventsForImport(snapshot, record) {
if (!snapshot) return;
const normalizedKey = record.sessionKey;
if (snapshot.sessionIdsBySessionKey.get(normalizedKey) !== record.entry.sessionId) return;
return snapshot.transcriptEventCountsBySessionId.get(record.entry.sessionId) ?? 0;
}
function countAlreadyMigratedTranscriptEventsForValidate(snapshot, record) {
const normalizedKey = normalizeStoreSessionKey(record.sessionKey);
if (snapshot.sessionIdsBySessionKey.get(normalizedKey) !== record.entry.sessionId) return;
return snapshot.transcriptEventCountsBySessionId.get(record.entry.sessionId) ?? 0;
}
function countTranscriptEvents(record) {
return countTranscriptEventsForPath(record.transcriptPath);
}
function readLegacyTranscriptMtimeMs(record) {
if (!record.transcriptPath) return;
try {
const mtimeMs = Math.floor(fs.statSync(record.transcriptPath).mtimeMs);
return Number.isFinite(mtimeMs) && mtimeMs >= 0 ? mtimeMs : void 0;
} catch {
return;
}
}
function listUnreferencedJsonlFiles(storePath, referencedPaths) {
const sessionsDir = path.dirname(storePath);
let entries;
try {
entries = fs.readdirSync(sessionsDir);
} catch {
return [];
}
const referenced = new Set(referencedPaths.map((filePath) => canonicalFilePath(filePath)));
return entries.filter((entry) => entry.endsWith(".jsonl")).map((entry) => path.join(sessionsDir, entry)).filter((filePath) => !referenced.has(canonicalFilePath(filePath))).toSorted((a, b) => a.localeCompare(b));
}
function appendActiveSqliteTranscriptFileIssues(target, report) {
const result = scanReadOnlySqliteActiveTranscriptFiles(target, (sessionKey, sessionId, sessionFile) => {
const transcriptPath = resolveActiveSqliteTranscriptFile(target, {
...sessionFile ? { sessionFile } : {},
sessionId
});
if (transcriptPath) report.issues.push({
code: "active_sqlite_transcript_jsonl",
message: `SQLite-backed session still has an active JSONL transcript file: ${transcriptPath}`,
sessionKey
});
});
if (!result.ok) report.issues.push({
code: "sqlite_active_transcript_scan_failed",
message: `Could not scan SQLite-backed sessions for active JSONL transcript files: ${String(result.error)}`
});
}
function appendSqliteDbStats(target, report) {
const result = readOnlySqliteDbStats(target);
if (!result.ok) {
report.issues.push({
code: "sqlite_corrupt",
message: `SQLite database could not be inspected: ${String(result.error)}`
});
return;
}
report.dbStats = result.stats;
if (result.stats.integrityCheck && result.stats.integrityCheck !== "ok") report.issues.push({
code: "sqlite_integrity_check_failed",
message: `SQLite quick_check reported: ${result.stats.integrityCheck}`
});
}
async function compactSqliteDatabase(target, report, options = {}) {
try {
if (options.operation === "import-finalize") closeOpenClawAgentDatabaseByPath(resolveTargetSqlitePath(target));
report.compact = await compactDoctorSessionSqliteTarget(target, options);
} catch (err) {
report.issues.push({
code: "sqlite_compact_failed",
message: `SQLite database compact failed: ${formatErrorMessage(err)}`
});
}
}
function resolveActiveSqliteTranscriptFile(target, entry) {
let transcriptPath;
try {
transcriptPath = resolveSessionFilePathCore(entry.sessionId, entry, {
agentId: target.agentId,
sessionsDir: path.dirname(target.storePath)
});
} catch {
return;
}
if (!transcriptPath.endsWith(".jsonl")) return;
let stat;
try {
stat = fs.statSync(transcriptPath);
} catch {
return;
}
if (!stat.isFile()) return;
const sessionsDir = canonicalFilePath(path.dirname(target.storePath));
const activePath = canonicalFilePath(transcriptPath);
if (path.dirname(activePath) !== sessionsDir) return;
return activePath;
}
function planImportedTranscriptArtifactsToArchive(target, sessionKey, transcriptPath, reservedArchivePaths) {
const moves = [];
const addMove = (sourcePathRaw, kind) => {
const move = planSessionJsonlArchiveMove({
archiveKey: sessionKey,
baseNameRaw: path.basename(sourcePathRaw),
kind,
reservedArchivePaths,
sessionKey,
sourcePathRaw,
target
});
reservedArchivePaths.add(move.archivePath);
moves.push(move);
};
addMove(transcriptPath, "transcript");
const trajectoryPath = resolveTrajectoryPath(transcriptPath);
if (trajectoryPath && fs.existsSync(trajectoryPath)) addMove(trajectoryPath, "trajectory");
const trajectoryPointerPath = resolveTrajectoryPointerPath(transcriptPath);
if (trajectoryPointerPath && fs.existsSync(trajectoryPointerPath)) addMove(trajectoryPointerPath, "trajectory");
return moves;
}
function resolveTrajectoryPath(transcriptPath) {
return transcriptPath.endsWith(".jsonl") ? `${transcriptPath.slice(0, -6)}.trajectory.jsonl` : void 0;
}
function resolveTrajectoryPointerPath(transcriptPath) {
return transcriptPath.endsWith(".jsonl") ? `${transcriptPath.slice(0, -6)}.trajectory-path.json` : void 0;
}
function planSessionJsonlArchiveMove(params) {
const sourcePathRaw = path.resolve(params.sourcePathRaw);
if (!fs.lstatSync(sourcePathRaw).isFile()) throw new Error("source is not a regular file");
const sourcePath = path.join(canonicalFilePath(path.dirname(sourcePathRaw)), path.basename(sourcePathRaw));
const sessionsDir = canonicalFilePath(path.dirname(path.resolve(params.target.storePath)));
if (path.dirname(sourcePath) !== sessionsDir) throw new Error(`Migration source is outside the target sessions directory: ${sourcePath}`);
const archiveDir = resolveImportedTranscriptArchiveDir(params.target.storePath);
assertSafeSessionSqliteMigrationDirectory(archiveDir);
fs.mkdirSync(archiveDir, { recursive: true });
assertSafeSessionSqliteMigrationDirectory(archiveDir);
const baseName = params.baseNameRaw.replace(/[^A-Za-z0-9_.-]+/g, "_").slice(0, 160) || "artifact";
const keySlug = params.archiveKey.replace(/[^A-Za-z0-9_.-]+/g, "_").slice(0, 120) || "session";
for (let attempt = 0; attempt < 100; attempt += 1) {
const suffix = attempt === 0 ? "" : `.${attempt}`;
const archivePath = path.join(archiveDir, `${keySlug}.${baseName}.imported-${Date.now()}${suffix}`);
if (fs.existsSync(archivePath) || params.reservedArchivePaths?.has(archivePath)) continue;
return {
archivePath,
kind: params.kind,
...params.sessionKey ? { sessionKey: params.sessionKey } : {},
sourcePath
};
}
throw new Error(`Could not archive ${baseName} for ${params.archiveKey}`);
}
function resolveImportedTranscriptArchiveDir(storePath) {
const storeDir = canonicalFilePath(path.dirname(path.resolve(storePath)));
return path.join(path.dirname(storeDir), "session-sqlite-import-archive");
}
function canonicalFilePath(filePath) {
try {
return fs.realpathSync.native(filePath);
} catch {
return path.resolve(filePath);
}
}
function createMigrationTargetInput(target) {
return {
agentId: target.agentId,
sqlitePath: canonicalMigrationFilePath(resolveTargetSqlitePath(target)),
storePath: canonicalMigrationFilePath(target.storePath)
};
}
function isSessionEntry(value) {
return isRecord(value) && typeof value.sessionId === "string" && value.sessionId.trim() !== "";
}
function summarizeDoctorSessionSqliteReport(mode, targets, activeRun) {
const sum = (value) => sumDoctorSessionSqliteTargets(targets, value);
return {
...activeRun ? { migrationRun: {
...activeRun.manifest.failureReports ? {
failureReportJsonPath: activeRun.manifest.failureReports.jsonPath,
failureReportMarkdownPath: activeRun.manifest.failureReports.markdownPath
} : {},
manifestPath: activeRun.manifestPath,
runId: activeRun.manifest.runId
} } : {},
mode,
targets,
totals: createDoctorSessionSqliteTotals(targets, {
archivedLegacyStoreFiles: sum((target) => target.archivedLegacyStoreFiles?.length ?? 0),
archivedTranscriptFiles: sum((target) => target.archivedTranscriptFiles.length),
archivedUnreferencedJsonlFiles: sum((target) => target.archivedUnreferencedJsonlFiles.length),
importedEntries: sum((target) => target.importedEntries),
importedTranscriptEvents: sum((target) => target.importedTranscriptEvents),
legacyEntries: sum((target) => target.legacyEntries),
reclaimedBytes: sum((target) => target.compact?.reclaimedBytes ?? 0),
unreferencedJsonlFiles: sum((target) => target.unreferencedJsonlFiles.length),
validatedEntries: sum((target) => target.validatedEntries),
validatedTranscriptEvents: sum((target) => target.validatedTranscriptEvents)
})
};
}
//#endregion
export { reconcileDoctorSessionSqlitePublication, runDoctorSessionSqlite };