UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

2,513 lines • 123 kB
import { c as isRecord } from "./record-coerce-TIsquoVR.mjs";
import { t as FsSafeError } from "./fs-safe-BUXBSN-4.mjs";
import { c as readFileDescriptorBoundedSync } from "./boundary-file-read-CCth2Hwg.mjs";
import { r as resolveRealpathOrAbsolute } from "./boundary-path-CAv_plX7.mjs";
import { r as isPathInside$1 } from "./path-guards-CH7noY0Z.mjs";
import { r as resolveStateDir } from "./state-dir-BHJORNly.mjs";
import "./paths-DGIwWbpu.mjs";
import { t as formatErrorMessage } from "./errors-O33D01XW.mjs";
import { t as openNodeSqliteDatabase } from "./node-sqlite-XIcwmOrl.mjs";
import { s as OPENCLAW_SQLITE_BUSY_TIMEOUT_MS } from "./openclaw-state-db-contract-Cm9fKF2T.mjs";
import { a as resolveSqliteDatabaseFilePaths, n as hasOrphanedSqliteSidecars } from "./sqlite-files-BEC3zbaw.mjs";
import { s as getCanonicalSqliteNamedIndexContracts } from "./sqlite-schema-contract-BTxZz0OV.mjs";
import { p as syncDirectorySync, s as requireDirectorySync } from "./directory-durability-CqSvP1tc.mjs";
import { n as hashFileDescriptorSync } from "./file-descriptor-B4yBUiAP.mjs";
import { n as assertSqliteIntegrity } from "./sqlite-integrity-GtqcmwG3.mjs";
import { t as OPENCLAW_AGENT_SCHEMA_SQL } from "./openclaw-agent-schema-BcJs-SM4.mjs";
import { n as normalizeAgentId } from "./agent-id-D1-Q6vR4.mjs";
import "./session-key-C09ow4h0.mjs";
import { i as listAgentIds } from "./agent-roster-BXD1uMJD.mjs";
import { c as resolveOpenClawAgentSqlitePath } from "./openclaw-agent-db.paths-DVbbFaej.mjs";
import { a as isPrimarySessionTranscriptFileName, m as resolveTrajectoryPointerPath, p as resolveTrajectoryPath } from "./artifacts-BvSTVPj-.mjs";
import { s as resolveSessionFilePathCore } from "./paths-dpReqUI8.mjs";
import { l as runOpenClawStateWriteTransaction } from "./openclaw-state-db-D1jWDzK3.mjs";
import { n as markLegacyMigrationSourceRemovedInDatabase } from "./state-migrations.receipts-gEcgM3Aq.mjs";
import { c as readDeferredPluginMigrations, m as withDeferredPluginMigrationsCurrent, t as DeferredPluginMigrationConflictError, u as recordDeferredPluginMigrations } from "./deferred-plugin-migrations-by1ypkxx.mjs";
import { c as normalizePluginId, u as normalizePluginsConfig } from "./config-state-DY_WJYue.mjs";
import { r as getRuntimeConfig } from "./io.runtime-BsUXSsD4.mjs";
import "./config-DLycWFvB.mjs";
import { C as inspectSqliteRecoveryFiles, w as moveSqliteFilesAside } from "./agent-database-admission-dWNS6Rcs.mjs";
import { o as ensureOpenClawAgentDatabasePermissions } from "./openclaw-agent-db-schema-kFBuwbLx.mjs";
import { u as invalidateOpenClawAgentDatabaseIntegrityBeforeMutation } from "./openclaw-agent-db-lease-D8zYIRzn.mjs";
import { i as closeOpenClawAgentDatabaseByPath } from "./openclaw-agent-db-lifecycle-BJiYtXuW.mjs";
import { n as clearOpenClawAgentDatabaseOpenFailure, o as isOpenClawAgentDatabaseOpen } from "./openclaw-agent-db-DBug3LUA.mjs";
import { r as loadExactSessionEntryCandidates } from "./session-accessor.sqlite-exact-read-JW6IveXD.mjs";
import { a as resolveAllAgentSessionStoreTargetsSync, i as resolveAllAgentSessionStoreCandidateTargetsSync, r as resolveAgentSessionStoreTargetsSync, s as resolveConfiguredAgentDatabaseTargets, u as resolveSessionStoreTargets } from "./targets-BbcRkdUC.mjs";
import { u as prepareLegacyAcpMigrationSource } from "./session-accessor.sqlite-acp-provenance-tjrzB-wk.mjs";
import { r as migrateOpenClawAgentDatabaseForMaintenance, t as assertOpenClawAgentDatabaseForMaintenance } from "./openclaw-agent-db-maintenance-CYe8pVkd.mjs";
import { i as hasSqliteFileFamily, n as createRetainedAgentDatabaseMatcher } from "./agent-deletion-discovery-BRLBtW0y.mjs";
import { C as moveMigrationArtifact, E as statMigrationPath, T as sameMigrationArtifact, a as collectRecordedConsumedArchives, b as writeSessionSqliteMigrationManifest, c as findLatestFailedSessionSqliteMigrationManifest, d as listSessionSqliteMigrationManifestPaths, f as migrationMoveKey, g as resolveSessionSqliteMigrationRunsDir, h as recordPlannedMigrationMoves, i as canonicalMigrationFilePath, l as hasSymbolicLinkInDirectoryPath, m as recordCompletedMigrationMoves, n as assertSafeSessionSqliteMigrationDirectory, o as createSessionSqliteMigrationRun, p as readSessionSqliteMigrationManifest, r as assertSafeSessionSqliteMigrationMove, s as filterRestoreManifestTargets, t as HISTORICAL_IMPORT_REASON, u as isRegularFileWithoutFollowingSymlinks, v as uniqueRestoreMoves, w as readMigrationArtifactIdentity, y as updateMigrationManifestTarget } from "./session-sqlite-migration-manifest-CT3ckmkL.mjs";
import { a as collectHistoricalArchiveSources, c as readLegacySessionRecords, i as appendLegacyHistoricalTranscripts, l as collectRecoveryInventory, n as settleDuplicateSessionSqliteArchives, o as gatherLegacyArchiveCoverage, r as validateLegacySessionRecords, s as listUnreferencedJsonlFiles } from "./doctor-session-sqlite-retirement-B7PGtafq.mjs";
import { c as listLegacySessionTranscriptFiles, d as shouldFilterLegacySessionRecordsByTarget, s as isLegacySessionRecordOwnedByTarget, t as verifyCanonicalSessionTranscriptSources } from "./session-sqlite-transcript-verification-CkQD9IPF.mjs";
import { a as readLegacyPrimaryTranscriptIdentity, c as readSqliteEntryCount, d as resolveTargetSqlitePath, f as scanReadOnlySqliteActiveTranscriptFiles, i as projectExistingAgentDatabaseTargets, l as readTranscriptFingerprint, n as createTranscriptEventReader, o as readOnlySqliteDbStats, s as readOnlySqliteValidationSnapshot, t as countTranscriptEventsForPath, u as resolveTargetSqliteOptions } from "./session-sqlite-migration-readers-D3juTq9n.mjs";
import { o as normalizePersistedSessionEntryShape } from "./state-migrations.legacy-session-store-CwqflQxE.mjs";
import { n as assertDoctorSqliteMaintenancePathsNotAliased, r as isDestructiveDoctorSessionSqliteMode } from "./doctor-sqlite-maintenance-lock-B-Ck8335.mjs";
import { c as readStaleDeferredPluginSessionImport, f as resolveVerifiedSessionSource, h as importSqliteSessionRowsBatch, i as prepareSessionSourceVerification, l as rebuildDeferredPluginSessionSourceIndex, m as captureDeferredPluginSessionSources, n as hasDeferredPluginSessionImport, o as readDeferredPluginSessionImport, p as DeferredPluginSessionImportSchema, s as readDeferredPluginSessionImportReceipt, t as deferredPluginSessionStoreIds, u as recordDeferredPluginSessionImport } from "./deferred-plugin-session-sources-ByVMoRwu.mjs";
import { t as withAgentDatabaseMaintenanceLease } from "./openclaw-agent-db-maintenance-lease-Dvppzhox.mjs";
import { i as writeSessionSqliteMigrationFailureReports, r as createSessionSqliteMigrationFailureIssue } from "./doctor-session-sqlite-failure-DF5_dSiG.mjs";
import { i as isRetainedSourceIssue, n as createDoctorSessionSqliteTargetReport, r as isInformationalMissingSessionIndex, t as countBlockingSessionSqliteIssues } from "./doctor-session-sqlite-types-CchNvmQZ.mjs";
import { n as compactDoctorSqliteFile } from "./doctor-sqlite-compact-CFxC8S-a.mjs";
import fs from "node:fs";
import path from "node:path";
import os from "node:os";
import { isDeepStrictEqual } from "node:util";
import { createHash } from "node:crypto";
import { safeStatSync } from "@openclaw/fs-safe/path";
import { setImmediate } from "node:timers/promises";
//#region src/commands/doctor-session-sqlite-compact.ts
/** Runs doctor-owned SQLite file compaction for migrated session stores. */
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 = () => {
		invalidateOpenClawAgentDatabaseIntegrityBeforeMutation(sqlitePath, databaseOptions.env);
		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 (maintenance) => {
		await migrateOpenClawAgentDatabaseForMaintenance({
			agentId: databaseOptions.agentId,
			pathname: sqlitePath
		}, maintenance);
		maintenance.assertOwned();
		requireQuarantineCleared();
		return compactTarget();
	}) : compactTarget();
}
function readSessionDatabaseStat(sqlitePath) {
	try {
		return fs.lstatSync(sqlitePath);
	} catch (error) {
		if (error.code === "ENOENT") return;
		throw error;
	}
}
function readSqliteFileSizes(sqlitePath) {
	return {
		dbSizeBytes: safeStatSync(sqlitePath)?.size ?? 0,
		walSizeBytes: safeStatSync(`${sqlitePath}-wal`)?.size ?? 0
	};
}
//#endregion
//#region src/commands/doctor-session-sqlite-diagnostics.ts
function countLegacyTranscript(record, report) {
	const result = countTranscriptEventsForPath(record.transcriptPath);
	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 appendRetainedPluginSessionSourceIssue(report, pluginIds) {
	const pending = pluginIds.length ? `remain pending for plugin(s): ${pluginIds.join(", ")}. Install the plugin and run openclaw doctor --fix to finish.` : "await archival. Run openclaw doctor --fix to finish.";
	report.issues.push({
		code: "plugin_migration_source_retained",
		message: `Canonical session import is verified. Original session migration inputs, including unindexed history, ${pending}`
	});
}
function appendActiveSqliteTranscriptFileIssues(target, report, retainedPaths) {
	try {
		for (const { sessionKey, transcriptPath } of readActiveSqliteTranscriptFiles(target)) if (!retainedPaths?.has(canonicalMigrationFilePath(transcriptPath))) report.issues.push({
			code: "active_sqlite_transcript_jsonl",
			message: `SQLite-backed session has a legacy JSONL transcript awaiting verification: ${transcriptPath}. Run openclaw doctor --fix or openclaw doctor --session-sqlite recover with the Gateway stopped to verify, import any missing events, and archive the original.`,
			sessionKey
		});
	} catch (error) {
		report.issues.push({
			code: "sqlite_active_transcript_scan_failed",
			message: `Could not scan SQLite-backed sessions for active JSONL transcript files: ${String(error)}`
		});
	}
}
function readActiveSqliteTranscriptFiles(target) {
	const sources = [];
	const result = scanReadOnlySqliteActiveTranscriptFiles(target, (sessionKey, sessionId, sessionFile) => {
		const transcriptPath = resolveActiveSqliteTranscriptFile(target, {
			...sessionFile ? { sessionFile } : {},
			sessionId
		});
		if (transcriptPath) sources.push({
			sessionKey,
			sessionId,
			transcriptPath
		});
	});
	if (!result.ok) throw result.error;
	return sources;
}
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 = resolveRealpathOrAbsolute(path.dirname(target.storePath));
	const activePath = resolveRealpathOrAbsolute(transcriptPath);
	if (path.dirname(activePath) !== sessionsDir) return;
	return activePath;
}
function summarizeDoctorSessionSqliteReport(mode, targets, activeRun) {
	const sum = (value) => targets.reduce((total, target) => total + value(target), 0);
	const archives = (paths) => new Set(targets.flatMap(paths)).size;
	const sqliteEntries = /* @__PURE__ */ new Map();
	for (const target of targets) sqliteEntries.set(target.sqlitePath, Math.max(sqliteEntries.get(target.sqlitePath) ?? 0, target.sqliteEntries));
	const reportsArchival = mode !== "restore" && mode !== "recover";
	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: {
			...reportsArchival ? { archivedLegacyStoreFiles: archives((target) => target.archivedLegacyStoreFiles ?? []) } : {},
			archivedTranscriptFiles: archives((target) => target.archivedTranscriptFiles),
			archivedUnreferencedJsonlFiles: archives((target) => target.archivedUnreferencedJsonlFiles),
			importedEntries: sum((target) => target.importedEntries),
			importedTranscriptEvents: sum((target) => target.importedTranscriptEvents),
			issues: sum((target) => target.issues.length),
			legacyEntries: sum((target) => target.legacyEntries),
			...reportsArchival ? { reclaimedBytes: sum((target) => target.compact?.reclaimedBytes ?? 0) } : {},
			sqliteEntries: [...sqliteEntries.values()].reduce((total, count) => total + count, 0),
			targets: targets.length,
			unreferencedJsonlFiles: sum((target) => target.unreferencedJsonlFiles.length),
			validatedEntries: sum((target) => target.validatedEntries),
			validatedTranscriptEvents: sum((target) => target.validatedTranscriptEvents)
		}
	};
}
//#endregion
//#region src/commands/doctor-session-sqlite-active.ts
/** Old imports can leave active originals outside a later plugin receipt. Never replay their index. */
async function prepareActiveSqliteTranscriptSettlement(params) {
	const records = [];
	const target = {
		...params.target,
		sqlitePath: resolveTargetSqlitePath(params.target, params.env)
	};
	let sourcesToSettle;
	try {
		sourcesToSettle = readActiveSqliteTranscriptFiles(target);
	} catch (error) {
		params.report.issues.push({
			code: "sqlite_active_transcript_scan_failed",
			message: formatErrorMessage(error)
		});
		return records;
	}
	for (const source of sourcesToSettle) {
		if (params.excludedPaths.has(canonicalMigrationFilePath(source.transcriptPath))) continue;
		try {
			const fingerprint = readTranscriptFingerprint(source.transcriptPath);
			readMigrationArtifactIdentity(source.transcriptPath, 1n, fingerprint);
			const primary = readLegacyPrimaryTranscriptIdentity(source.transcriptPath, source.transcriptPath, void 0, true);
			if (!primary) continue;
			if (primary.sessionId !== source.sessionId) throw new Error("Legacy transcript has no matching primary session identity");
			const sources = [{
				path: source.transcriptPath,
				sessionId: source.sessionId
			}];
			const verify = (mode = "contained") => verifyCanonicalSessionTranscriptSources({
				target,
				sources,
				env: params.env,
				mode
			});
			let verified = verify("appendable");
			if (!verified) throw new Error("Missing history requires legacy format or branch repair before it can be appended");
			if (verified.missingEvents > 0) {
				const [imported] = await importSqliteSessionRowsBatch([{
					agentId: target.agentId,
					storePath: target.sqlitePath,
					env: params.env,
					sessionKey: source.sessionKey,
					entry: {
						sessionId: source.sessionId,
						updatedAt: 0
					},
					historicalOnly: true,
					preserveExactStoredKey: true,
					readTranscriptEvents: createTranscriptEventReader(source.transcriptPath, source.sessionId, false, fingerprint)
				}]);
				params.report.importedTranscriptEvents += imported.transcriptEvents;
				verified = verify();
			}
			if (!verified) throw new Error("The original transcript order and content could not be verified in SQLite");
			readMigrationArtifactIdentity(source.transcriptPath, 1n, fingerprint);
			params.report.validatedEntries += 1;
			params.report.validatedTranscriptEvents += verified.events;
			records.push({
				sessionKey: source.sessionKey,
				entry: {
					sessionId: source.sessionId,
					updatedAt: 0
				},
				transcriptPath: source.transcriptPath,
				transcriptDependencies: [source.transcriptPath],
				sourceFingerprint: fingerprint,
				recovery: {
					complete: true,
					repaired: false,
					events: verified.events,
					sqliteEvents: verified.sqliteEvents
				}
			});
		} catch (error) {
			params.report.issues.push({
				code: "active_sqlite_transcript_verification_failed",
				sessionKey: source.sessionKey,
				message: `${source.transcriptPath}: ${formatErrorMessage(error)}. Original retained. Compare the named events with a verified backup, restore a corrected JSONL at this path, then rerun openclaw doctor --session-sqlite recover.`
			});
		}
	}
	return records;
}
//#endregion
//#region src/commands/doctor-session-sqlite-archive.ts
/** Plans Doctor archive paths; publication and receipts remain with the migration owner. */
function planImportedTranscriptArtifactsToArchive(target, sessionKey, transcriptPath, reservedArchivePaths, capturedSources) {
	const moves = [];
	const addMove = (sourcePathRaw, kind) => {
		if (capturedSources && !capturedSources.has(canonicalMigrationFilePath(sourcePathRaw))) return;
		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 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(resolveRealpathOrAbsolute(path.dirname(sourcePathRaw)), path.basename(sourcePathRaw));
	const sessionsDir = resolveRealpathOrAbsolute(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 = resolveRealpathOrAbsolute(path.dirname(path.resolve(storePath)));
	return path.join(path.dirname(storeDir), "session-sqlite-import-archive");
}
async function archiveImportedLegacySessionStores(owners, activeRun, coverage, assertCurrent, publishSourceRemoval) {
	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) {
		assertCurrent?.();
		if (!coverage.indexIdentities.has(storePath) && !fs.existsSync(storePath)) continue;
		if (!coverage.selectedStorePaths.has(storePath) || entries.some(({ report }) => countBlockingSessionSqliteIssues(report) > 0 || report.issues.some((issue) => issue.code === "active_sqlite_transcript_jsonl"))) 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.every((issue) => issue.code === "historical_duplicate_settled")) && 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) {
				assertCurrent?.();
				recordPlannedMigrationMoves(activeRun, target, [move]);
				assertSafeSessionSqliteMigrationMove(move, target);
			}
			publicationPlanned = true;
			assertCurrent?.();
			await moveMigrationArtifact(move.sourcePath, move.archivePath, expected, assertCurrent ? () => {
				assertCurrent();
			} : void 0, publishSourceRemoval);
			assertCurrent?.();
			for (const { target, report } of entries) {
				recordCompletedMigrationMoves(activeRun, target, [move]);
				report.archivedLegacyStoreFiles.push(move.archivePath);
			}
		} catch (error) {
			if (error instanceof DeferredPluginMigrationConflictError && error.pending.length > 0) break;
			for (const { report, target } of entries) {
				report.issues.push({
					code: "legacy_store_archive_failed",
					message: `${storePath}: ${formatErrorMessage(error)}`
				});
				if (!publicationPlanned) {
					assertCurrent?.();
					updateMigrationManifestTarget(activeRun, target, report.issues);
				}
			}
		}
	}
}
//#endregion
//#region src/commands/doctor-session-sqlite-import.ts
const SESSION_IMPORT_BATCH_SIZE = 256;
async function importLegacySessionRecords({ target, env, expectedIndexIdentity, recoveryInventory }, records, report, activeRun) {
	if (records.length === 0) return;
	try {
		const recoveryHistoryUnverified = Boolean(recoveryInventory?.report.artifacts.some((artifact) => ["unreadable-manifest", "manifest-directory-alias"].includes(artifact.reason)));
		let assertRestoredIndexCurrent;
		try {
			if (!recoveryHistoryUnverified) assertRestoredIndexCurrent = prepareRestoredSessionIndex({
				target,
				env,
				expectedIndexIdentity,
				recoveryInventory
			});
		} catch (error) {
			report.issues.push({
				code: "legacy_import_deferred",
				message: `${formatErrorMessage(error)}. Originals retained; preserve the files and receipts, restore verified recovery evidence, then rerun openclaw doctor --fix.`
			});
			return;
		}
		const importedTranscriptSources = /* @__PURE__ */ new Set();
		const existingSnapshot = readOnlySqliteValidationSnapshot(target);
		if (!existingSnapshot.ok) {
			report.issues.push({
				code: "legacy_import_deferred",
				message: `Cannot verify the destination for ${target.storePath}: ${formatErrorMessage(existingSnapshot.error)}. Originals retained; repair the named SQLite destination before retrying openclaw doctor --fix.`
			});
			return;
		}
		for (let offset = 0; offset < records.length; offset += SESSION_IMPORT_BATCH_SIZE) {
			const pending = records.slice(offset, offset + SESSION_IMPORT_BATCH_SIZE).flatMap((record) => {
				record.preserveCurrentSession ||= recoveryHistoryUnverified || Boolean(assertRestoredIndexCurrent);
				try {
					const prepared = prepareLegacySessionImport(target, record, report, importedTranscriptSources, existingSnapshot.snapshot, env, recoveryHistoryUnverified);
					return prepared ? [{
						...prepared,
						params: {
							...prepared.params,
							env
						},
						record
					}] : [];
				} catch (error) {
					report.issues.push({
						code: "legacy_import_deferred",
						sessionKey: record.sessionKey,
						message: `${record.transcriptPath ?? target.storePath}: ${formatErrorMessage(error)}. Original retained; verify this source against a backup and rerun openclaw doctor --fix.`
					});
					return [];
				}
			});
			const imported = await importSqliteSessionRowsBatch(pending.map((entry, index) => ({
				...entry.params,
				...index === 0 && assertRestoredIndexCurrent ? { beforePersistentApply: assertRestoredIndexCurrent } : {}
			})));
			for (const [index, result] of imported.entries()) {
				const record = pending[index]?.record;
				const recovery = pending[index]?.recovery ?? result.recovery;
				if (record && recovery) record.recovery = 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] : []));
			await setImmediate();
		}
	} catch (error) {
		const failures = [error];
		report.issues.push({
			code: "sqlite_import_failed",
			message: formatErrorMessage(error)
		});
		if (activeRun) {
			activeRun.manifest.failedAt = (/* @__PURE__ */ new Date()).toISOString();
			try {
				updateMigrationManifestTarget(activeRun, report, report.issues);
			} catch (recordError) {
				failures.push(recordError);
			}
		}
		if (failures.length > 1) throw new AggregateError(failures, `${formatErrorMessage(error)}; could not record session SQLite migration failure: ${formatErrorMessage(failures[1])}`, { cause: error });
		throw error;
	}
}
function prepareRestoredSessionIndex(params) {
	const { expectedIndexIdentity, recoveryInventory, target } = params;
	if (!expectedIndexIdentity || !recoveryInventory) return;
	const storePath = canonicalMigrationFilePath(target.storePath);
	const sqlitePath = resolveTargetSqlitePath(target, params.env);
	const evidence = /* @__PURE__ */ new Map();
	let hasSelectedOwner = false;
	for (const refs of recoveryInventory.references.values()) for (const ref of refs) {
		if (ref.move.kind !== "legacy-store" || ref.move.sourcePath !== storePath || !ref.consumedByRestore && !ref.run.manifest.restore?.restoredFiles.includes(storePath) && !ref.target.completedMoves.some((move) => migrationMoveKey(move) === migrationMoveKey(ref.move))) continue;
		const artifact = ref.move.artifact;
		if (!artifact) {
			assertSafeSessionSqliteMigrationMove(ref.move, ref.target);
			if (statMigrationPath(ref.move.archivePath)) {
				const archiveIdentity = readMigrationArtifactIdentity(ref.move.archivePath);
				if (archiveIdentity.size !== expectedIndexIdentity.size || archiveIdentity.sha256 !== expectedIndexIdentity.sha256) continue;
				evidence.set(ref.move.archivePath, archiveIdentity);
			}
		}
		if (artifact && (artifact.identity.dev !== expectedIndexIdentity.dev || artifact.identity.ino !== expectedIndexIdentity.ino)) continue;
		const selectedOwner = filterRestoreManifestTargets(ref.run.manifest, [{
			agentId: target.agentId,
			storePath,
			sqlitePath
		}]).includes(ref.target);
		if (artifact && (!sameMigrationArtifact(artifact.identity, expectedIndexIdentity) || !ref.consumedByRestore || artifact.disposal.state !== "retained") || ref.target.storePath !== storePath || ref.target.agentId === target.agentId && !selectedOwner) throw new Error(`Restored session index evidence cannot be verified: ${storePath}`);
		assertSafeSessionSqliteMigrationMove(ref.move, ref.target);
		const identity = readMigrationArtifactIdentity(ref.run.manifestPath);
		if (JSON.stringify(readSessionSqliteMigrationManifest(ref.run.manifestPath)) !== JSON.stringify(ref.run.manifest) || !sameMigrationArtifact(identity, readMigrationArtifactIdentity(ref.run.manifestPath))) throw new Error(`Session restore receipt changed: ${ref.run.manifestPath}`);
		evidence.set(ref.run.manifestPath, identity);
		hasSelectedOwner ||= selectedOwner;
	}
	if (evidence.size === 0) return;
	if (!hasSelectedOwner) throw new Error(`Restored session index evidence cannot be verified: ${storePath}`);
	const assertCurrent = () => {
		for (const filePath of [
			storePath,
			sqlitePath,
			...evidence.keys()
		]) if (hasSymbolicLinkInDirectoryPath(path.dirname(filePath))) throw new Error(`Session restore path changed: ${filePath}`);
		for (const [filePath, identity] of [[storePath, expectedIndexIdentity], ...evidence]) if (!sameMigrationArtifact(identity, readMigrationArtifactIdentity(filePath))) throw new Error(`Session restore source or receipt changed: ${filePath}`);
	};
	assertCurrent();
	return assertCurrent;
}
function prepareLegacySessionImport(target, record, report, importedTranscriptSources, existingSnapshot, env, recoveryHistoryUnverified) {
	if (record.historical && record.transcriptPath && !sameMigrationArtifact(record.historical.identity, readMigrationArtifactIdentity(record.transcriptPath))) {
		report.issues.push({
			code: "historical_transcript_deferred",
			sessionKey: record.sessionKey,
			message: `${record.historical.originalPath}: source changed after discovery; retained without importing`
		});
		return;
	}
	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 = countTranscriptEventsForPath(record.transcriptPath);
	const currentOwner = existingSnapshot?.sessionKeysBySessionId.get(record.entry.sessionId);
	if (record.preserveCurrentSession && currentOwner && currentOwner !== record.sessionKey) throw new Error(`Historical transcript ${record.entry.sessionId} already belongs to ${currentOwner}`);
	if (recoveryHistoryUnverified && result.status === "malformed" && (existingSnapshot?.transcriptEventCountsBySessionId.get(record.entry.sessionId) ?? 0) > 0) throw new Error(result.message);
	const transcriptMtimeMs = readLegacyTranscriptMtimeMs(record);
	const normalizedEntry = normalizePersistedSessionEntryShape(record.entry, { sessionKey: record.sessionKey });
	const params = {
		historicalOnly: Boolean(record.historical || record.preserveCurrentSession),
		allowMalformedRowRepair: true,
		repairLegacyTranscript: true,
		agentId: target.agentId,
		entry: {
			...normalizedEntry ?? record.entry,
			sessionId: record.entry.sessionId
		},
		...!record.historical && normalizedEntry?.acp ? { legacyAcpMigrationSource: prepareLegacyAcpMigrationSource({
			sourcePath: target.storePath,
			sourceSessionKey: record.sessionKey,
			sessionId: normalizedEntry.sessionId,
			lifecycleRevision: normalizedEntry.lifecycleRevision,
			meta: normalizedEntry.acp
		}) } : {},
		preserveExactStoredKey: true,
		sessionKey: record.sessionKey,
		storePath: target.sqlitePath ?? target.storePath
	};
	let recovery;
	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,
			recovery
		};
	}
	if (result.status === "ok" && transcriptFingerprint && record.transcriptPath && (existingSnapshot?.transcriptEventCountsBySessionId.get(record.entry.sessionId) ?? 0) > 0) try {
		const verified = verifyCanonicalSessionTranscriptSources({
			target: {
				...target,
				sqlitePath: report.sqlitePath
			},
			sources: [{
				path: record.transcriptPath,
				sessionId: record.entry.sessionId,
				originalPath: record.historical?.originalPath ?? record.transcriptPath
			}],
			env,
			mode: "appendable"
		});
		if (!verified) throw new Error("Missing history requires legacy format or branch repair before it can be appended");
		if (verified.missingEvents === 0) recovery = {
			complete: true,
			repaired: false,
			events: verified.events,
			sqliteEvents: verified.sqliteEvents
		};
	} catch (error) {
		report.issues.push({
			code: recoveryHistoryUnverified ? "legacy_import_deferred" : "sqlite_transcript_count_mismatch",
			sessionKey: record.sessionKey,
			message: `${record.transcriptPath}: ${formatErrorMessage(error)}. Original retained. Compare the named events with a verified backup, restore a corrected JSONL at this path, then rerun openclaw doctor --session-sqlite recover.`
		});
		return;
	}
	if (transcriptSourceKey) importedTranscriptSources.add(transcriptSourceKey);
	return {
		recovery,
		...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, record.historical?.originalPath ?? record.transcriptPath) } : {},
			...transcriptMtimeMs !== void 0 ? { transcriptMtimeMs } : {}
		}
	};
}
function markAlreadyMigratedTranscript(record, report, snapshot) {
	if (!snapshot || snapshot.sessionIdsBySessionKey.get(record.sessionKey) !== record.entry.sessionId) return false;
	report.validatedEntries += 1;
	report.validatedTranscriptEvents += snapshot.transcriptEventCountsBySessionId.get(record.entry.sessionId) ?? 0;
	return true;
}
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;
	}
}
//#endregion
//#region src/commands/doctor-session-sqlite-missing-index.ts
/** Missing index receipts describe history; only unimported transcript content requires action. */
function createMissingSessionIndexVerifier(params) {
	let inventory;
	return (target) => {
		if (target.issues.length > 0) return false;
		try {
			if (statMigrationPath(target.storePath)) return false;
			inventory ??= collectRecoveryInventory(params);
			const missingIndexes = [...inventory.references.values()].filter((refs) => refs.every((ref) => ref.trusted && !ref.consumedByRestore && ref.target.agentId === target.agentId && ref.target.storePath === target.storePath && ref.target.sqlitePath === target.sqlitePath && ref.move.kind === "legacy-store" && ref.move.artifact?.classification === "protected" && ref.move.artifact.reason === "incomplete-index-import" && ref.move.artifact.disposal.state === "retained" && !statMigrationPath(ref.move.sourcePath) && !statMigrationPath(ref.move.archivePath)));
			if (missingIndexes.length === 0) return false;
			const snapshot = readOnlySqliteValidationSnapshot(target);
			const stats = readOnlySqliteDbStats(target);
			if (!snapshot.ok || !stats.ok || stats.stats.integrityCheck !== "ok") return false;
			const sources = listLegacySessionTranscriptFiles(path.dirname(target.storePath)).map((sourcePath) => {
				readMigrationArtifactIdentity(sourcePath);
				const primary = readLegacyPrimaryTranscriptIdentity(sourcePath, sourcePath);
				if (!primary || !snapshot.snapshot.sessionKeysBySessionId.has(primary.sessionId)) throw new Error("Legacy transcript has no verified canonical owner");
				return {
					path: sourcePath,
					sessionId: primary.sessionId
				};
			});
			const sourcePaths = new Set(sources.map((source) => source.path));
			const dependenciesPresent = missingIndexes.every((refs) => refs.every((ref) => ref.move.artifact.dependencies.every((dependency) => !isPrimarySessionTranscriptFileName(path.basename(dependency)) || sourcePaths.has(dependency))));
			if (sources.length === 0 || !dependenciesPresent) return false;
			const verified = verifyCanonicalSessionTranscriptSources({
				target,
				sources,
				env: params.env
			});
			if (!verified) return false;
			target.validatedEntries = verified.entries;
			target.validatedTranscriptEvents = verified.events;
			target.issues.push({
				code: "legacy_index_informational",
				message: `${target.storePath}: Canonical SQLite transcripts are complete. The legacy index source and archive are missing; legacy index entries are informational. No import is needed.`
			});
			return true;
		} catch {
			return false;
		}
	};
}
//#endregion
//#region src/commands/doctor-session-sqlite-restore.ts
async function restoreSessionSqliteMigrationRuns(params) {
	const restoreReport = emptyRestoreReport();
	const contexts = loadRestoreManifestContexts(listSessionSqliteMigrationManifestPaths(params.env).toReversed(), params.trustedTargets);
	await reconcileRestorePublications(contexts, params.env);
	const restorePlan = createRestorePlan(contexts);
	for (const { manifest, manifestPath, targets } of contexts) {
		const manifestRestoreReport = {
			...emptyRestoreReport(),
			manifestPaths: [manifestPath]
		};
		restoreReport.manifestPaths.push(manifestPath);
		await restoreSessionSqliteMigrationManifest(manifest, manifestPath, targets, manifestRestoreReport, restorePlan);
		restoreReport.conflicts.push(...manifestRestoreReport.conflicts);
		restoreReport.restoredFiles.push(...manifestRestoreReport.restoredFiles);
		restoreReport.skippedFiles.push(...manifestRestoreReport.skippedFiles);
		writeSessionSqliteMigrationManifest({
			manifest,
			manifestPath
		});
	}
	return restoreReport;
}
/** Undo only recorded, still-linked publication intermediates before a fresh import or restore. */
async function reconcileSessionSqliteMigrationPublications(params) {
	await reconcileRestorePublications(loadRestoreManifestContexts(listSessionSqliteMigrationManifestPaths(params.env), params.trustedTargets), params.env, params.sourcePath);
}
async function reconcileRestorePublications(contexts, env, sourcePath) {
	const stateDir = path.dirname(canonicalMigrationFilePath(path.join(resolveStateDir(env), "anchor")));
	for (const context of contexts) for (const target of context.targets) for (const move of uniqueRestoreMoves(target)) {
		if (sourcePath && canonicalMigrationFilePath(sourcePath) !== move.sourcePath || !move.artifact || move.artifact.disposal.state !== "retained") continue;
		const source = statMigrationPath(move.sourcePath);
		const archive = statMigrationPath(move.archivePath);
		if (!source || !archive) continue;
		if (!source.isFile() || !archive.isFile() || source.dev !== archive.dev || source.ino !== archive.ino) continue;
		if (source.nlink !== 2 || archive.nlink !== 2) continue;
		if (![
			target.storePath,
			target.sqlitePath,
			move.archivePath
		].every((file) => isPathInside$1(stateDir, file))) continue;
		assertSafeSessionSqliteMigrationMove(move, target);
		assertDoctorSqliteMaintenancePathsNotAliased("session recovery publication", [context.manifestPath, ...resolveSqliteDatabaseFilePaths(target.sqlitePath)], [stateDir]);
		await moveMigrationArtifact(move.archivePath, move.sourcePath, readRestoreArchiveIdentity(move, 2n), () => {
			assertSafeSessionSqliteMigrationMove(move, target);
			recordRestoredMigrationMove(context.manifest, context.manifestPath, move);
		});
	}
}
function recordRestoredMigrationMove(manifest, manifestPath, move) {
	requireDirectorySync(syncDirectorySync(path.dirname(path.dirname(move.sourcePath))), "Restored session directory");
	const consumed = collectRecordedConsumedArchives(manifest);
	consumed.add(move.archivePath);
	manifest.restore = {
		attemptedAt: (/* @__PURE__ */ new Date()).toISOString(),
		consumedArchives: [...consumed].toSorted(),
		conflicts: [],
		restoredFiles: [.../* @__PURE__ */ new Set([...manifest.restore?.restoredFiles ?? [], move.sourcePath])],
		skippedFiles: [],
		status: "restored"
	};
	writeSessionSqliteMigrationManifest({
		manifest,
		manifestPath
	});
}
function loadRestoreManifestContexts(manifestPaths, trustedTargets) {
	const contexts = [];
	for (const manifestPath of manifestPaths) {
		const stat = statMigrationPath(manifestPath);
		const manifest = stat?.isFile() && stat.nlink === 1 && !hasSymbolicLinkInDirectoryPath(path.dirname(manifestPath)) ? readSessionSqliteMigrationManifest(manifestPath) : void 0;
		if (!manifest) continue;
		const targets = filterRestoreManifestTargets(manifest, trustedTargets);
		if (targets.length > 0) contexts.push({
			manifest,
			manifestPath,
			targets
		});
	}
	return contexts;
}
/**
* Resolve every duplicate destination before moving an archive. A missing archive only disappears
* from the conflict set when its own manifest proves that an earlier restore consumed it.
*/
function createRestorePlan(contexts) {
	const plan = /* @__PURE__ */ new Map();
	const candidatesBySource = /* @__PURE__ */ new Map();
	for (const context of contexts) {
		const consumedArchives = collectRecordedConsumedArchives(context.manifest);
		for (const target of context.targets) for (const move of uniqueRestoreMoves(target)) {
			if (move.artifact && move.artifact.disposal.state !== "retained") {
				plan.set(restoreMovePlanKey(context.manifestPath, move), {
					action: "conflict",
					reason: move.artifact.disposal.state === "disposed" ? "rollback original was intentionally disposed by update cleanup" : "rollback original has pending cleanup; finish cleanup before restore"
				});
				continue;
			}
			const candidates = candidatesBySource.get(move.sourcePath) ?? [];
			candidates.push({
				consumed: consumedArchives.has(move.archivePath),
				context,
				move
			});
			candidatesBySource.set(move.sourcePath, candidates);
		}
	}
	for (const [sourcePath, candidates] of candidatesBySource) {
		if (fs.existsSync(sourcePath) || candidates.length === 1) {
			for (const candidate of candidates) plan.set(restoreMovePlanKey(candidate.context.manifestPath, candidate.move), { action: "standard" });
			continue;
		}
		const available = [];
		let blocked = false;
		for (const candidate of candidates) {
			const key = restoreMovePlanKey(candidate.context.manifestPath, candidate.move);
			const inspection = inspectRestoreArchive(candidate.move);
			if (inspection.state === "available") {
				available.push({
					...candidate,
					snapshot: inspection.snapshot
				});
				continue;
			}
			if (inspection.state === "missing" && candidate.consumed) {
				plan.set(key, { action: "skip-consumed" });
				continue;
			}
			blocked = true;
			plan.set(key, {
				action: "conflict",
				reason: inspection.state === "missing" ? "archive is missing without a recorded prior restore; refusing another candidate" : inspection.reason
			});
		}
		if (blocked) {
			for (const candidate of available) plan.set(restoreMovePlanKey(candidate.context.manifestPath, candidate.move), {
				action: "conflict",
				reason: "another archive for this source is unavailable without prior restore evidence; refusing automatic selection"
			});
			continue;
		}
		if (available.length === 0) continue;
		if (new Set(available.map((candidate) => candidate.move.kind)).size !== 1) {
			setRestoreCandidateConflicts(plan, available, "recorded archives disagree on artifact kind; refusing automatic selection");
			continue;
		}
		const winner = selectRestoreCandidate(available);
		if (!winner) {
			setRestoreCandidateConflicts(plan, available, available[0]?.move.kind === "legacy-store" ? "multiple distinct nonempty session indexes require explicit archive selection" : "multiple distinct archives require explicit archive selection");
			continue;
		}
		const winnerKey = restoreMovePlanKey(winner.context.manifestPath, winner.move);
		for (const candidate of available) {
			const candidateKey = restoreMovePlanKey(candidate.context.manifestPath, candidate.move);
			plan.set(candidateKey, candidateKey === winnerKey ? {
				action: "restore",
				snapshot: candidate.snapshot
			} : { action: "skip-superseded" });
		}
	}
	return plan;
}
function selectRestoreCandidate(candidates) {
	if (new Set(candidates.map((candidate) => candidate.snapshot.digest)).size === 1) return candidates[0];
	if (candidates[0]?.move.kind !== "legacy-store") return;
	const nonemptyDigests = new Set(candidates.filter((candidate) => (candidate.snapshot.legacyEntryCount ?? 0) > 0).map((candidate) => candidate.snapshot.digest));
	if (nonemptyDigests.size === 0) return candidates[0];
	return nonemptyDigests.size === 1 ? candidates.find((candidate) => (candidate.snapshot.legacyEntryCount ?? 0) > 0) : void 0;
}
function setRestoreCandidateConflicts(plan, candidates, reason) {
	for (const candidate of candidates) plan.set(restoreMovePlanKey(candidate.context.manifestPath, candidate.move), {
		action: "conflict",
		reason
	});
}
function restoreMovePlanKey(manifestPath, move) {
	return `${manifestPath}\u0000${migrationMoveKey(move)}`;
}
function inspectRestoreArchive(move) {
	if (hasSymbolicLinkInDirectoryPath(path.dirname(move.archivePath))) return {
		state: "invalid",
		reason: "archive parent is a symbolic link; refusing restore"
	};
	let pathStat;
	try {
		pathStat = fs.lstatSync(move.archivePath);
	} catch (error) {
		const code = isRecord(error) ? error.code : void 0;
		return code === "ENOENT" || code === "ENOTDIR" ? { state: "missing" } : {
			state: "invalid",
			reason: "archive could not be inspected; refusing restore"
		};
	}
	if (!pathStat.isFile()) return {
		state: "invalid",
		reason: "archive is not a regular file; refusing restore"
	};
	const changed = {
		state: "invalid",
		reason: "archive changed while it was inspected; refusing restore"
	};
	let fd;
	try {
		const flags = process.platform === "win32" ? "r" : fs.constants.O_RDONLY | (fs.constants.O_NOFOLLOW ?? 0) | (fs.constants.O_NONBLOCK ?? 0);
		fd = fs.openSync(move.archivePath, flags);
		const descriptorStat = fs.fstatSync(fd);
		if (!descriptorStat.isFile() || descriptorStat.dev !== pathStat.dev || descriptorStat.ino !== pathStat.ino) return changed;
		let digest;
		let legacyEntryCount;
		if (move.kind === "legacy-store") {
			const content = readFileDescriptorBoundedSync(fd, descriptorStat.size);
			digest = createHash("sha256").update(content).digest("hex");
			let parsed;
			try {
				parsed = JSON.parse(content.toString("utf-8"));
			} catch {
				return {
					state: "invalid",
					reason: "session index archive is not valid JSON; refusing automatic selection"
				};
			}
			if (!isRecord(parsed)) return {
				state: "invalid",
				reason: "session index archive is not a JSON object; refusing automatic selection"
			};
			legacyEntryCount = Object.keys(parsed).length;
		} else {
			const hashed = hashFileDescriptorSync(fd, descriptorStat.size);
			if (hashed.sizeBytes !== descriptorStat.size) throw new Error("archive changed while it was inspected");
			digest = hashed.sha256;
		}
		const finalPathStat = fs.lstatSync(move.archivePath);
		if (finalPathStat.dev !== descriptorStat.dev || finalPathStat.ino !== descriptorStat.ino || finalPathStat.size !== descriptorStat.size) return changed;
		return {
			state: "available",
			snapshot: {
				digest,
				...legacyEntryCount === void 0 ? {} : { legacyEntryCount },
				size: descriptorStat.size
			}
		};
	} catch (error) {
		if (move.kind !== "legacy-store" && error instanceof FsSafeError && error.code === "too-large") return changed;
		const code = isRecord(error) ? error.code : void 0;
		return code === "ENOENT" || code === "ENOTDIR" ? { state: "missing" } : {
			state: "invalid",
			reason: "archive could not be read safely; refusing restore"
		};
	} finally {
		if (fd !== void 0) fs.closeSync(fd);
	}
}
async function restoreSessionSqliteMigrationRun(params) {
	const restoreReport = {
		...emptyRestoreReport(),
		manifestPaths: [params.manifestPath]
	};
	const manifest = readSessionSqliteMigrationManifest(params.manifestPath);
	if (!manifest) {
		restoreReport.conflicts.push({
			archivePath: params.manifestPath,
			reason: "manifest is missing or unreadable",
			sourcePath: params.manifestPath
		});
		return restoreReport;
	}
	const targetManifests = filterRestoreManifestTargets(manifest, params.trustedTargets);
	if (targetManifests.length === 0) {
		restoreReport.conflicts.push({
			archivePath: params.manifestPath,
			reason: "manifest does not match a trusted session target",
			sourcePath: params.manifestPath
		});
		return restoreReport;
	}
	await reconcileRestorePublications([{
		manifest,
		manifestPath: params.manifestPath,
		targets: targetManifests
	}], params.env ?? process.env);
	await restoreSessionSqliteMigrationManifest(manifest, params.manifestPath, targetManifests, restoreReport, createRestorePlan([{
		manifest,
		manifestPath: params.manifestPath,
		targets: targetManifests
	}]));
	writeSessionSqliteMigrationManifest({
		manifest,
		manifestPath: params.manifestPath
	});
	return restoreReport;
}
function emptyRestoreReport() {
	return {
		conflicts: [],
		manifestPaths: [],
		restoredFiles: [],
		skippedFiles: []
	};
}
async function restoreSessionSqliteMigrationManifest(manifest, manifestPath, targets, restoreReport, restorePlan) {
	for (const target of targets) for (const move of uniqueRestoreMoves(target)) await restoreMigrationMove({
		manifest,
		manifestPath,
		target,
		move,
		restorePlan,
		restoreReport
	});
	const consumedArchives = collectRecordedConsumedArchives(manifest);
	manifest.restore = {
		attemptedAt: (/* @__PURE__ */ new Date()).toISOString(),
		...consumedArchives.size > 0 ? { consumedArchives: [...consumedArchives].toSorted() } : {},
		conflicts: restoreReport.conflicts,
		restoredFiles: restoreReport.restoredFiles,
		skippedFiles: restoreReport.skippedFiles,
		status: resolveRestoreStatus(restoreReport)
	};
}
function readRestoreArchiveIdentity(move, expectedLinks = 1n) {
	const identity = readMigrationArtifactIdentity(move.archivePath, expectedLinks);
	if (move.artifact && !sameMigrationArtifact(identity, move.artifact.identity, { ignoreDevice: true })) throw new Error("archive identity or contents changed; refusing restore");
	return identity;
}
async function restoreMigrationMove(params) {
	const { manifest, manifestPath, target, move, restorePlan, restoreReport } = params;
	const recordConflict = (reason) => {
		restoreReport.conflicts.push({
			archivePath: move.archivePath,
			reason,
			sourcePath: move.sourcePath
		});
	};
	const planned = restorePlan.get(restoreMovePlanKey(manifestPath, move)) ?? { action: "standard" };
	if (planned.action === "conflict") {
		recordConflict(planned.reason);
		return;
	}
	if (planned.action === "skip-consumed" || planned.action === "skip-superseded") {
		restoreReport.skippedFiles.push(move.sourcePath);
		return;
	}
	const sourceExists = statMigrationPath(move.sourcePath) !== void 0;
	const archiveExists = statMigrationPath(move.archivePath) !== void 0;
	if (sourceExists || !archiveExists) {
		if (sourceExists && !archiveExists) restoreReport.skippedFiles.push(move.sourcePath);
		else recordConflict(sourceExists ? "source and archive both exist; refusing to overwrite source" : "source and archive are both missing");
		return;
	}
	try {
		if (!isRegularFileWithoutFollowingSymlinks(move.archivePath)) throw new Error("archive is not a regular file; refusing restore");
		assertRestoreDirectories(move);
		fs.mkdirSync(path.dirname(move.sourcePath), {
			recursive: true,
			mode: 448
		});
		assertRestoreDirectories(move);
		const identity = readRestoreArchiveIdentity(move);
		if (planned.action === "restore" && (identity.sha256 !== planned.snapshot.digest || identity.size !== planned.snapshot.size)) throw new Error("archive changed after restore planning; refusing restore");
		if (!move.artifact) {
			move.artifact = {
				identity,
				classification: "protected",
				reason: "historical-restore-original",
				dependencies: move.kind === "legacy-store" ? uniqueRestoreMoves(target).filter((item) => item.kind === "transcript").map((item) => item.sourcePath) : [],
				disposal: { state: "retained" }
			};
			for (const recorded of [...target.plannedMoves, ...target.completedMoves]) if (migrationMoveKey(recorded) === migrationMoveKey(move)) recorded.artifact = move.artifact;
			writeSessionSqliteMigrationManifest({
				manifest,
				manifestPath
			});
		}
		await moveMigrationArtifact(move.archivePath, move.sourcePath, identity, () => {
			assertRestoreDirectories(move);
			recordRestoredMigrationMove(manifest, manifestPath, move);
		});
		restoreReport.restoredFiles.push(move.sourcePath);
	} catch (error) {
		recordConflict(error instanceof Error ? error.message : String(error));
	}
}
function assertRestoreDirectories(move) {
	if (hasSymbolicLinkInDirectoryPath(path.dirname(move.sourcePath)) || hasSymbolicLinkInDirectoryPath(path.dirname(move.archivePath))) throw new Error("source or archive parent is a symbolic link; refusing restore");
}
function resolveRestoreStatus(report) {
	if (report.conflicts.length > 0 && report.restoredFiles.length > 0) return "partial";
	if (report.conflicts.length > 0) return "conflicts";
	if (report.restoredFiles.length > 0) return "restored";
	return "noop";
}
//#endregion
//#region src/commands/doctor-session-sqlite-recover-report.ts
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 = params.targets.map((target) => ({
		...target,
		sqlitePath: resolveTargetSqlitePath(target, params.env)
	}));
	const failedRun = findLatestFailedSessionSqliteMigrationManifest(params.env, trustedTargets);
	if (!failedRun) {
		const recoveredCorruptTargets = await withAgentDatabaseMaintenanceLease({ env: params.env }, (maintenance) => recoverCorruptSqliteTargets(params.targets, params.env, maintenance));
		const retainedReports = [...recoveredCorruptTargets];
		for (const target of trustedTargets) {
			const recovered = recoveredCorruptTargets.find((report) => report.sqlitePath === target.sqlitePath);
			if (recovered && recovered.issues.length > 0) continue;
			try {
				const preparation = await params.prepareTarget?.(target);
				if (recovered) continue;
				if (hasDeferredPluginSessionImport({
					target,
					sqlitePath: target.sqlitePath,
					env: params.env
				}) || readActiveSqliteTranscriptFiles(target).length > 0 || params.historicalArchiveStores?.has(target.storePath)) retainedReports.push(await params.validateTarget(target));
				else if (preparation?.repaired) retainedReports.push(createEmptyRecoverTargetReport(target, target.sqlitePath));
			} catch (error) {
				retainedReports.push(createRecoverInspectionFailureTargetReport(target, target.sqlitePath, error));
			}
		}
		if (retainedReports.length > 0 || params.recoveryInventory && !params.recoveryInventory.report.artifacts.some((artifact) => artifact.outcome === "blocked" || artifact.reason === "unsupported-target-ownership")) return summarizeDoctorSessionSqliteReport("recover", retainedReports);
		return summarizeDoctorSessionSqliteReport("recover", [createSyntheticRecoverTargetReport(params.env, "No failed session SQLite migration manifest found.")]);
	}
	const restore = await restoreSessionSqliteMigrationRun({
		env: params.env,
		manifestPath: failedRun.manifestPath,
		trustedTargets
	});
	const targetReports = [];
	const recoveryTargets = trustedTargets.filter((target) => failedRun.targets.some((failed) => failed.agentId === target.agentId && failed.storePath === target.storePath) || params.historicalArchiveStores?.has(target.storePath));
	for (const manifestTarget of recoveryTargets) try {
		await params.prepareTarget?.(manifestTarget);
		targetReports.push(await params.validateTarget({
			agentId: manifestTarget.agentId,
			sqlitePath: manifestTarget.sqlitePath,
			storePath: manifestTarget.storePath
		}));
	} catch (error) {
		targetReports.push(createRecoverInspectionFailureTargetReport(manifestTarget, manifestTarget.sqlitePath, error));
	}
	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 report = summarizeDoctorSessionSqliteReport("recover", targetReports.length > 0 ? targetReports : [reportTarget]);
	if (report.totals.issues === 0) {
		report.migrationRun = {
			manifestPath: failedRun.manifestPath,
			runId: failedRun.manifest.runId
		};
		return report;
	}
	const failureReports = writeSessionSqliteMigrationFailureReports(failedRun.manifestPath, {
		reason: "doctor recover completed with remaining issues",
		recoveryTargets: report.targets,
		trustedTargets
	});
	report.migrationRun = {
		failureReportJsonPath: failureReports.jsonPath,
		failureReportMarkdownPath: failureReports.markdownPath,
		manifestPath: failedRun.manifestPath,
		runId: failedRun.manifest.runId
	};
	report.supportIssue = createSessionSqliteMigrationFailureIssue(failedRun.manifestPath, trustedTargets);
	return report;
}
async function recoverCorruptSqliteTargets(targets, env, maintenance) {
	const reports = [];
	for (const target of targets) {
		maintenance.assertOwned();
		const databaseOptions = resolveTargetSqliteOptions(target, env);
		const sqlitePath = resolveOpenClawAgentSqlitePath(databaseOptions);
		let recoveryFiles;
		try {
			recoveryFiles = inspectSqliteRecoveryFiles(sqlitePath);
		} catch (error) {
			reports.push(createRecoverInspectionFailureTargetReport(target, sqlitePath, error));
			continue;
		}
		if (recoveryFiles.existing.length === 0) continue;
		if (!recoveryFiles.existing.includes(sqlitePath)) {
			reports.push(recoverCorruptSqliteTarget(target, sqlitePath, /* @__PURE__ */ new Error(`SQLite sidecars exist without their main database: ${sqlitePath}`), () => maintenance.assertOwned()));
			continue;
		}
		const inspection = inspectSqliteForRecovery(sqlitePath, recoveryFiles.existing);
		if (inspection.ok) continue;
		if (!isSqliteCorruptionError(inspection.error)) {
			reports.push(createRecoverInspectionFailureTargetReport(target, sqlitePath, inspection.error));
			continue;
		}
		if (!isCanonicalAgentIndexCorruptionError(inspection.error)) {
			reports.push(recoverCorruptSqliteTarget(target, sqlitePath, inspection.error, () => maintenance.assertOwned()));
			continue;
		}
		const repair = await repairCanonicalIndexesForRecovery(databaseOptions, sqlitePath, maintenance);
		reports.push(repair.ok ? createEmptyRecoverTargetReport(target, sqlitePath) : createRecoverInspectionFailureTargetReport(target, sqlitePath, repair.error));
	}
	return reports;
}
async function repairCanonicalIndexesForRecovery(databaseOptions, sqlitePath, maintenance) {
	try {
		await migrateOpenClawAgentDatabaseForMaintenance({
			agentId: databaseOptions.agentId,
			pathname: sqlitePath
		}, maintenance);
		maintenance.assertOwned();
		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, assertCurrent) {
	const report = createEmptyRecoverTargetReport(target, sqlitePath);
	try {
		report.corruptRecovery = moveSqliteFilesAside(sqlitePath, assertCurrent);
	} 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 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 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
	});
}
//#endregion
//#region src/commands/doctor-session-sqlite-restore-report.ts
async function restoreDoctorSessionSqliteTargets(params) {
	const targetReports = params.targets.map((target) => createDoctorSessionSqliteTargetReport({
		agentId: target.agentId,
		sqliteEntries: readSqliteEntryCount(target),
		sqlitePath: resolveTargetSqlitePath(target),
		storePath: target.storePath
	}));
	const trustedTargets = params.targets.map((target) => ({
		...target,
		sqlitePath: resolveTargetSqlitePath(target)
	}));
	const restore = await restoreSessionSqliteMigrationRuns({
		env: params.env,
		trustedTargets
	});
	const reportTarget = targetReports[0] ?? createDoctorSessionSqliteTargetReport({
		agentId: "restore",
		sqlitePath: "",
		storePath: 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 summarizeDoctorSessionSqliteReport("restore", targetReports.length > 0 ? targetReports : [reportTarget]);
}
//#endregion
//#region src/commands/doctor-session-sqlite-retained.ts
/** Archival, not plugin completion alone, ends the original index's no-replay obligation. */
function retireDeferredPluginSessionImport(params) {
	const receipt = readDeferredPluginSessionImportReceipt(params);
	if (!receipt) return;
	const recorded = DeferredPluginSessionImportSchema.parse(JSON.parse(receipt.reportJson));
	if (recorded.superseded) return;
	const expectedPending = readDeferredPluginMigrations({ env: params.env });
	if (expectedPending.some((pending) => recorded.pluginIds.includes(pending.pluginId) && !params.completedPluginIds?.includes(pending.pluginId))) return;
	if (statMigrationPath(params.target.storePath) || recorded.sources.some((source) => statMigrationPath(source.path))) return;
	runOpenClawStateWriteTransaction(({ db }) => withDeferredPluginMigrationsCurrent({
		env: params.env,
		expectedPending
	}, () => {
		params.assertCurrent?.();
		if (!isDeepStrictEqual(readDeferredPluginSessionImportReceipt({
			...params,
			database: db
		}), receipt)) throw new Error("Deferred session import receipt changed before retirement.");
		if (statMigrationPath(params.target.storePath) || recorded.sources.some((source) => statMigrationPath(source.path))) return;
		readDeferredPluginSessionImport({
			cfg: params.cfg,
			env: params.env,
			target: params.target,
			sqlitePath: params.sqlitePath,
			database: db
		});
		markLegacyMigrationSourceRemovedInDatabase(db, receipt.sourceKey);
	}), { env: params.env }, { operationLabel: "state.retire-plugin-session-source" });
}
/** Receipt recovery belongs to offline Doctor; canonical session data is never replayed. */
async function prepareRetainedSessionImport(params, report) {
	const issues = report.issues;
	const isSqliteStore = params.target.storePath.endsWith(".sqlite");
	const sqlitePath = resolveTargetSqlitePath(params.target, params.env);
	if (!isSqliteStore && (params.mode === "import" || params.mode === "recover")) {
		const isHeld = createRetainedAgentDatabaseMatcher(params.env, () => resolveConfiguredAgentDatabaseTargets(params.cfg, { env: params.env }), {
			kind: "legacy-database",
			readDatabasePaths: () => [sqlitePath]
		});
		const disposition = isHeld(params.target.storePath, params.target.agentId) || isHeld(sqlitePath, params.target.agentId);
		if (disposition && (disposition !== "unavailable" || hasSqliteFileFamily(sqlitePath) || hasDeferredPluginSessionImport({
			target: {
				...params.target,
				sqlitePath
			},
			sqlitePath,
			env: params.env
		}))) {
			issues.push({
				code: "plugin_migration_source_retained",
				message: `Retained session sources skipped: store held for agent ${params.target.agentId} database ${sqlitePath}. Run openclaw doctor --fix for deletion-history repair and explicit restoration guidance.`
			});
			return;
		}
	}
	let retainedImport;
	const sourceConflicts = /* @__PURE__ */ new Map();
	const emptySources = /* @__PURE__ */ new Map();
	const sourceVerification = {
		...prepareSessionSourceVerification({
			...params,
			sqlitePath
		}),
		allowMissingIndex: true,
		onEmptySource: (sourcePath, reason) => emptySources.set(sourcePath, reason),
		onSourceConflict: (sourcePath, artifactPath = sourcePath, error) => {
			if (sourceConflicts.has(artifactPath)) return;
			const reason = error === void 0 ? "Retained plugin input no longer matches its verified source." : formatErrorMessage(error);
			sourceConflicts.set(sourcePath, reason);
			sourceConflicts.set(artifactPath, reason);
			issues.push({
				code: fs.existsSync(params.target.storePath) ? "retained_plugin_source_conflict" : "historical_transcript_deferred",
				message: `${artifactPath}: ${reason} Canonical SQLite sessions remain authoritative. Preserve this input and its backups. Restore the verified original at ${sourcePath}, then run openclaw doctor --session-sqlite recover --session-sqlite-all-agents against the same state/config.`
			});
		}
	};
	if (!isSqliteStore) try {
		if (params.mode === "import" || params.mode === "recover") {
			if (await rebuildDeferredPluginSessionSourceIndex(sourceVerification)) {
				if (!readStaleDeferredPluginSessionImport(sourceVerification)) issues.push({
					code: "retained_plugin_source_index_rebuilt",
					message: `Rebuilt the verified source index and database binding from the deferred import receipt: ${params.target.storePath}. Canonical SQLite sessions were not replayed.`
				});
			}
			retireDeferredPluginSessionImport(sourceVerification);
		}
		retainedImport = readDeferredPluginSessionImport(sourceVerification);
	} catch (error) {
		issues.push({
			code: "retained_plugin_source_conflict",
			message: formatErrorMessage(error)
		});
		return;
	}
	const staleImport = isSqliteStore ? void 0 : readStaleDeferredPluginSessionImport(sourceVerification);
	if (staleImport) {
		issues.push({
			code: "retained_plugin_receipt_superseded",
			message: `Session import receipt is bound to a different database (${staleImport.databaseIdentity}); it cannot certify ${sqlitePath}. ${staleImport.superseded ? "Preserved the superseded receipt in migration_runs." : "Doctor import will preserve and supersede it."} Retained originals will be checked against live session state without replacing current settings.`
		});
		for (const source of staleImport.sources) {
			if (!isPrimarySessionTranscriptFileName(path.basename(source.path))) continue;
			let detail = "";
			try {
				const current = statMigrationPath(source.path);
				if (current?.isFile() && current.size > 0) continue;
				if (resolveVerifiedSessionSource(source, sourceVerification.resolvedTarget, params.env)) continue;
			} catch (error) {
				detail = ` ${formatErrorMessage(error)}`;
			}
			issues.push({
				code: "historical_transcript_deferred",
				message: `Retained history source unavailable: ${source.path}.${detail} The foreign receipt cannot prove its history exists in ${sqlitePath}; restore this original from a verified backup and rerun openclaw doctor --fix.`
			});
		}
	}
	const retainedIndex = retainedImport?.sources.find((source) => source.path === path.resolve(params.target.storePath));
	const retainedIndexPath = retainedIndex && resolveVerifiedSessionSource(retainedIndex, sourceVerification.resolvedTarget, params.env, sourceVerification.verification);
	if (retainedImport && (params.mode === "import" || params.mode === "recover") && sourceConflicts.has(path.resolve(params.target.storePath))) appendRetainedIndexComparison(params, issues);
	if (emptySources.size) {
		await archiveConflictingRetainedSessionSources({
			...params,
			verifiedEmpty: true
		}, emptySources, report);
		sourceVerification.verification.clear();
	}
	return {
		retainedImport,
		staleImport,
		sourceConflicts,
		sourceVerification,
		retainedIndexPath
	};
}
/** Compare current rows for diagnosis only; changed index values never gain receipt authority. */
function appendRetainedIndexComparison(params, issues) {
	try {
		const parsingIssues = [];
		const records = readLegacySessionRecords(params.target, parsingIssues).filter(({ sessionKey }) => !shouldFilterLegacySessionRecordsByTarget(params.target) || isLegacySessionRecordOwnedByTarget(params.cfg, params.target, sessionKey));
		if (parsingIssues.length || !records.length) return;
		const current = new Map(loadExactSessionEntryCandidates({
			readOnly: true,
			env: params.env,
			readSource: {
				agentId: params.target.agentId,
				path: resolveTargetSqlitePath(params.target, params.env)
			},
			sessionKeys: records.map(({ sessionKey }) => sessionKey)
		}).map(({ sessionKey, entry }) => [sessionKey, entry]));
		for (const { sessionKey, entry } of records) {
			const canonical = current.get(sessionKey);
			let message;
			if (!canonical || canonical.sessionId !== entry.sessionId) message = "Retained session identity differs from the current canonical SQLite row.";
			else {
				const sourceFields = new Map(Object.entries(entry));
				const canonicalFields = new Map(Object.entries(canonical));
				const changedFields = [.../* @__PURE__ */ new Set([...sourceFields.keys(), ...canonicalFields.keys()])].filter((field) => field !== "sessionFile" && !isDeepStrictEqual(sourceFields.get(field), canonicalFields.get(field))).toSorted();
				if (!changedFields.length) continue;
				message = `Retained session identity matches canonical SQLite, but metadata differs: ${changedFields.join(", ")}.`;
			}
			issues.push({
				code: "retained_plugin_source_conflict",
				sessionKey,
				message: `${message} Canonical values were kept; the retained index remains protected for recovery.`
			});
		}
	} catch (error) {
		issues.push({
			code: "retained_plugin_source_conflict",
			message: `Could not compare retained session metadata: ${formatErrorMessage(error)}. Canonical SQLite sessions were not changed.`
		});
	}
}
/** Preserve unverifiable plugin inputs through the existing reversible archive lifecycle. */
async function archiveConflictingRetainedSessionSources(params, sourceConflicts, report) {
	const target = {
		...params.target,
		sqlitePath: resolveTargetSqlitePath(params.target, params.env)
	};
	const conflicts = [...sourceConflicts].filter(([source]) => !params.protectedPaths?.has(canonicalMigrationFilePath(source)) && fs.existsSync(source) && path.dirname(source) === path.dirname(target.storePath));
	if (!conflicts.length) return;
	const run = params.activeRun ?? createSessionSqliteMigrationRun(params.env, [target]);
	const targets = params.targets ?? [target];
	let indexPath = target.storePath;
	const indexIdentity = params.expectedIndexIdentity ?? (!params.activeRun && fs.existsSync(indexPath) ? readMigrationArtifactIdentity(indexPath) : void 0);
	const assertIndexCurrent = () => {
		if (indexPath !== target.storePath && fs.existsSync(target.storePath) || (indexIdentity ? !sameMigrationArtifact(readMigrationArtifactIdentity(indexPath), indexIdentity) : fs.existsSync(indexPath))) throw new Error("Session index changed after owner discovery; original retained for a fresh Doctor pass.");
	};
	for (const [source, reason] of conflicts) try {
		assertIndexCurrent();
		const move = planSessionJsonlArchiveMove({
			target,
			sourcePathRaw: source,
			baseNameRaw: path.basename(source),
			archiveKey: "retained-plugin-conflict",
			kind: source === target.storePath ? "legacy-store" : "transcript"
		});
		move.artifact = {
			identity: move.kind === "legacy-store" && indexIdentity ? indexIdentity : readMigrationArtifactIdentity(source),
			classification: "protected",
			reason,
			dependencies: [],
			disposal: { state: "retained" }
		};
		if (params.verifiedEmpty && move.artifact.identity.size !== 0) throw new Error("Retained transcript is no longer empty; source needs verification");
		for (const owner of targets) recordPlannedMigrationMoves(run, owner, [move]);
		await moveMigrationArtifact(move.sourcePath, move.archivePath, move.artifact.identity, void 0, (remove, retain) => {
			if (move.kind !== "legacy-store") try {
				assertIndexCurrent();
			} catch (error) {
				retain();
				throw error;
			}
			remove();
		});
		for (const owner of targets) recordCompletedMigrationMoves(run, owner, [move]);
		if (move.kind === "legacy-store") indexPath = move.archivePath;
		sourceConflicts.set(move.archivePath, reason);
		(move.kind === "legacy-store" ? report.archivedLegacyStoreFiles ??= [] : report.archivedTranscriptFiles).push(move.archivePath);
		report.issues.push({
			code: params.verifiedEmpty ? "retained_empty_transcript_superseded" : "retained_plugin_source_conflict",
			message: `${source}: ${reason} Preserved at ${move.archivePath}; canonical SQLite sessions were not replayed.`
		});
	} catch (error) {
		report.issues.push({
			code: "retained_plugin_source_conflict",
			message: `${source}: could not archive retained input: ${formatErrorMessage(error)}. Original remains protected.`
		});
	}
	updateMigrationManifestTarget(run, target, report.issues);
	if (!params.activeRun) {
		run.manifest.completedAt = (/* @__PURE__ */ new Date()).toISOString();
		writeSessionSqliteMigrationManifest(run);
	}
}
/** Historical discovery yields; verify the receipt again before counting or authorizing archival. */
function countRetainedSessionSources(retained, records, report) {
	const { retainedImport, retainedIndexPath, sourceVerification, sourceConflicts } = retained;
	if (!retainedImport) return;
	if (!retainedIndexPath) {
		sourceVerification.verification.clear();
		if (!isDeepStrictEqual(readDeferredPluginSessionImport(sourceVerification), retainedImport)) throw new Error("Verified retained session import receipt changed during discovery.");
	}
	const verifiedSources = new Map(retainedImport.sources.map((source) => [source.path, source]));
	for (const record of records) {
		if (record.transcriptPath && sourceConflicts.has(record.transcriptPath)) continue;
		const source = record.transcriptPath && verifiedSources.get(path.resolve(record.transcriptPath));
		if (record.transcriptPath && !source) report.issues.push({
			code: "transcript_missing",
			message: `Transcript file is missing: ${record.transcriptPath}`,
			sessionKey: record.sessionKey
		});
		else if (record.transcriptPath && source) {
			if (fs.existsSync(record.transcriptPath)) record.sourceFingerprint = readTranscriptFingerprint(record.transcriptPath);
			const transcriptPath = resolveVerifiedSessionSource(source, sourceVerification.resolvedTarget, sourceVerification.env, sourceVerification.verification);
			if (!transcriptPath) throw new Error(`Retained session migration source changed: ${record.transcriptPath}`);
			countLegacyTranscript({
				...record,
				transcriptPath
			}, report);
			record.recovery = {
				complete: !report.issues.some((issue) => issue.code === "transcript_malformed" && issue.sessionKey === record.sessionKey),
				repaired: false,
				events: 0
			};
		}
	}
}
//#endregion
//#region src/commands/doctor-session-sqlite-targets.ts
/** Offline Doctor target discovery and legacy-source admission. */
async function prepareDoctorSessionSqliteTargets(params) {
	const resolved = resolveDoctorSessionSqliteTargets(params);
	if (isDestructiveDoctorSessionSqliteMode(params.mode)) assertDoctorSqliteMaintenancePathsNotAliased(`session SQLite ${params.mode}`, resolveDoctorSessionSqliteMaintenancePaths(resolved.targets), resolveDoctorSessionSqliteMaintenanceRoots(resolved.targets, params.env));
	const repairEntryStates = async (targets) => {
		params.authority?.assertCurrent();
		const { repairLegacySessionEntryStates } = await import("./doctor-session-delivery-state-DzKJ_thf.mjs");
		return await repairLegacySessionEntryStates({
			apply: true,
			cfg: params.cfg,
			env: params.env,
			authority: params.authority,
			targets: projectExistingAgentDatabaseTargets(targets, params.env, params.cfg),
			deferSchemaRepair: params.mode === "import"
		});
	};
	if (params.mode === "import") await repairEntryStates(resolved.targets);
	return {
		...resolved,
		repairEntryStates
	};
}
function createMigrationTargetInput(target) {
	return {
		agentId: target.agentId,
		sqlitePath: canonicalMigrationFilePath(resolveTargetSqlitePath(target)),
		storePath: canonicalMigrationFilePath(target.storePath)
	};
}
function resolveDoctorSessionSqliteConfig(options) {
	if (options.cfg) return options.cfg;
	const requestedAgentId = normalizeAgentId(options.agent ?? "main");
	return options.store ? { agents: { entries: { [requestedAgentId]: {} } } } : getRuntimeConfig();
}
function resolveDoctorSessionSqliteMaintenancePaths(targets) {
	return [...new Set(targets.flatMap((target) => resolveSqliteDatabaseFilePaths(resolveTargetSqlitePath(target))))];
}
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$1(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 resolveDoctorSessionSqliteTargets(params) {
	if (params.store) return { targets: resolveSessionStoreTargets(params.cfg, {
		store: params.store,
		agent: params.agent,
		allAgents: params.allAgents
	}, { env: params.env }) };
	const discoversHistory = params.mode === "dry-run" || params.mode === "import" || params.mode === "validate";
	if (params.mode === "restore" || params.mode === "recover" || discoversHistory && params.agent) {
		const candidates = resolveAllAgentSessionStoreCandidateTargetsSync(params.cfg, { env: params.env });
		if (!params.agent) return {
			targets: candidates,
			knownTargets: candidates
		};
		const requestedAgentId = normalizeAgentId(params.agent);
		return {
			targets: candidates.filter((target) => normalizeAgentId(target.agentId) === requestedAgentId),
			knownTargets: candidates
		};
	}
	if (params.agent) return { targets: resolveAgentSessionStoreTargetsSync(params.cfg, params.agent, { env: params.env }) };
	if (params.allAgents) {
		const targets = (discoversHistory ? resolveAllAgentSessionStoreCandidateTargetsSync(params.cfg, { env: params.env }) : resolveAllAgentSessionStoreTargetsSync(params.cfg, { env: params.env })).map((target) => ({
			target,
			sqlitePath: resolveTargetSqlitePath(target, params.env)
		}));
		const isRetained = createRetainedAgentDatabaseMatcher(params.env, () => resolveConfiguredAgentDatabaseTargets(params.cfg, { env: params.env }), {
			kind: "legacy-database",
			readDatabasePaths: () => targets.map(({ sqlitePath }) => sqlitePath)
		});
		return {
			targets: targets.filter(({ target, sqlitePath }) => {
				const orphanedSidecars = hasOrphanedSqliteSidecars(sqlitePath);
				return [target.storePath, sqlitePath].every((pathname) => {
					const disposition = isRetained(pathname, target.agentId);
					return !disposition || disposition === "unavailable" && !orphanedSidecars;
				});
			}).map(({ target }) => target),
			knownTargets: targets.map(({ target }) => target)
		};
	}
	return { targets: resolveSessionStoreTargets(params.cfg, {}, { env: params.env }) };
}
function filterLegacySessionStoreTargets(targets, mode, historicalArchives, settledStores) {
	if (mode === "inspect" || mode === "compact" || mode === "restore" || mode === "recover") return targets;
	return targets.filter((target) => !target.storePath.endsWith(".sqlite") && (settledStores.has(target.storePath) || fs.existsSync(target.storePath) || (historicalArchives.get(canonicalMigrationFilePath(target.storePath))?.transcripts.length ?? 0) > 0 || fs.existsSync(path.dirname(target.storePath)) && fs.readdirSync(path.dirname(target.storePath)).some((file) => file.endsWith(".jsonl"))));
}
//#endregion
//#region src/commands/doctor-session-sqlite.ts
const retainedArchivePlans = /* @__PURE__ */ new WeakMap();
/** Destructive production callers hold the Gateway/SQLite-maintenance state lock for the full call. */
async function runDoctorSessionSqlite(options, authority) {
	const env = options.env ?? process.env;
	const cfg = resolveDoctorSessionSqliteConfig(options);
	const configuredAgentIds = new Set(listAgentIds(cfg));
	let pendingPlugins = readDeferredPluginMigrations({ env });
	const verifyMissingIndex = createMissingSessionIndexVerifier({
		cfg,
		env
	});
	const { targets: candidates, knownTargets, repairEntryStates } = await prepareDoctorSessionSqliteTargets({
		...options,
		cfg,
		env,
		authority
	});
	if (options.mode === "import" || options.mode === "recover") {
		const plugins = normalizePluginsConfig(cfg.plugins);
		const disabled = pendingPlugins.filter(({ pluginId }) => {
			const id = normalizePluginId(pluginId);
			return !plugins.enabled || plugins.entries[id]?.enabled === false || plugins.deny.includes(id);
		}).map(({ pluginId }) => pluginId);
		if (disabled.length > 0) {
			authority?.assertCurrent();
			pendingPlugins = await recordDeferredPluginMigrations({
				env,
				pending: [],
				resolvedPluginIds: disabled,
				expectedPending: pendingPlugins
			}) ?? pendingPlugins;
		}
	}
	const settlements = options.mode === "import" || options.mode === "recover" ? await settleDuplicateSessionSqliteArchives({
		cfg,
		env,
		targets: candidates.map(createMigrationTargetInput)
	}) : [];
	let historicalSources = [
		"import",
		"dry-run",
		"validate",
		"recover"
	].includes(options.mode) ? collectHistoricalArchiveSources({
		cfg,
		env
	}) : void 0;
	let historicalArchives = historicalSources?.sources ?? /* @__PURE__ */ new Map();
	const targets = filterLegacySessionStoreTargets(candidates, options.mode, historicalArchives, new Set(settlements.map(({ target }) => target.storePath)));
	if (options.mode === "restore") return restoreDoctorSessionSqliteTargets({
		env,
		targets
	});
	if (options.mode === "recover") return recoverDoctorSessionSqliteTargets({
		env,
		options,
		targets,
		prepareTarget: (target) => repairEntryStates([target]),
		recoveryInventory: historicalSources?.inventory,
		historicalArchiveStores: /* @__PURE__ */ new Set([...historicalArchives.keys(), ...settlements.map(({ target }) => target.storePath)]),
		validateTarget: async (target) => {
			authority?.assertCurrent();
			const report = collectHistoricalArchiveSources({
				cfg,
				env
			}).sources.get(target.storePath)?.transcripts.length || readStaleDeferredPluginSessionImport({
				target,
				sqlitePath: target.sqlitePath,
				env
			}) ? (await runDoctorSessionSqlite({
				cfg,
				env,
				mode: "import",
				store: target.storePath,
				agent: target.agentId
			}, authority)).targets[0] : await inspectOrMigrateTarget({
				configuredAgentIds,
				cfg,
				env,
				mode: "recover",
				target,
				verifyMissingIndex,
				deferredPluginIds: deferredPluginSessionStoreIds({
					target,
					pending: pendingPlugins
				})
			});
			report.issues.push(...settlements.filter((item) => item.target.storePath === target.storePath).flatMap((item) => item.issues));
			return report;
		}
	});
	if (options.mode === "import") {
		await reconcileSessionSqliteMigrationPublications({
			env,
			trustedTargets: targets.map(createMigrationTargetInput)
		});
		historicalSources = collectHistoricalArchiveSources({
			cfg,
			env
		});
		historicalArchives = historicalSources.sources;
	}
	const activeRun = options.mode === "import" && targets.length > 0 ? createSessionSqliteMigrationRun(env, targets.map(createMigrationTargetInput)) : void 0;
	const coverage = options.mode === "import" || options.mode === "dry-run" || options.mode === "validate" ? gatherLegacyArchiveCoverage(cfg, env, targets, knownTargets) : void 0;
	const reports = [];
	const archiveTargets = [];
	for (const target of targets) reports.push(await inspectOrMigrateTarget({
		configuredAgentIds,
		activeRun,
		archiveTargets,
		verifyMissingIndex,
		cfg,
		env,
		mode: options.mode,
		target,
		historicalArchives,
		recoveryInventory: historicalSources?.inventory,
		referencedPaths: coverage?.referencedPaths,
		expectedIndexIdentity: coverage?.indexIdentities.get(canonicalMigrationFilePath(target.storePath)),
		deferredPluginIds: deferredPluginSessionStoreIds({
			target,
			pending: pendingPlugins
		})
	}));
	for (const report of reports) report.issues.push(...settlements.filter((item) => item.target.storePath === report.storePath).flatMap((item) => item.issues));
	if (activeRun && coverage) {
		for (const owner of archiveTargets) if (owner.sourceConflicts?.size && (!shouldFilterLegacySessionRecordsByTarget(owner.sourceTarget) || options.allAgents && !options.agent && !options.store && (coverage.selectedStorePaths.has(owner.target.storePath) || coverage.incompleteDirectories.has(path.dirname(owner.target.storePath))) && coverage.knownTargets.filter((known) => canonicalMigrationFilePath(known.storePath) === owner.target.storePath).every((known) => archiveTargets.some((candidate) => candidate.target.agentId === known.agentId && candidate.target.storePath === owner.target.storePath && candidate.validated))) && targets.filter((target) => canonicalMigrationFilePath(target.storePath) === owner.target.storePath).every((target) => archiveTargets.some((candidate) => candidate.sourceTarget === target && candidate.validated))) await archiveConflictingRetainedSessionSources({
			cfg,
			env,
			target: owner.sourceTarget,
			activeRun,
			protectedPaths: coverage.retainedPaths,
			expectedIndexIdentity: coverage.indexIdentities.get(owner.target.storePath),
			targets: archiveTargets.filter((candidate) => candidate.target.storePath === owner.target.storePath).map((candidate) => candidate.target)
		}, owner.sourceConflicts, owner.report);
		const deferredSourcePaths = /* @__PURE__ */ new Set();
		for (const target of reports.filter(isInformationalMissingSessionIndex)) {
			coverage.selectedStorePaths.delete(canonicalMigrationFilePath(target.storePath));
			coverage.retainedDirectories.add(path.dirname(canonicalMigrationFilePath(target.storePath)));
		}
		for (const owner of archiveTargets) for (const source of owner.sourceConflicts?.keys() ?? []) {
			coverage.retainedPaths.add(canonicalMigrationFilePath(source));
			deferredSourcePaths.add(canonicalMigrationFilePath(source));
		}
		const retainDeferredSources = () => {
			for (const owner of archiveTargets) if (owner.deferredPluginIds.length > 0) {
				coverage.selectedStorePaths.delete(canonicalMigrationFilePath(owner.target.storePath));
				coverage.retainedDirectories.add(path.dirname(canonicalMigrationFilePath(owner.target.storePath)));
				if (owner.retainedImportVerified) for (const source of owner.verifiedSources ?? []) deferredSourcePaths.add(canonicalMigrationFilePath(source.path));
			}
		};
		const publishArchive = (remove, retainSource) => {
			const conflict = withDeferredPluginMigrationsCurrent({
				env,
				expectedPending: pendingPlugins,
				onConflict(pending) {
					retainSource();
					if (pending.length > 0) {
						for (const owner of archiveTargets) if (!owner.retainedImportVerified && owner.verifiedSources) recordDeferredPluginSessionImport({
							cfg,
							target: owner.sourceTarget,
							sqlitePath: owner.target.sqlitePath,
							env,
							pluginIds: pending.map((plugin) => plugin.pluginId),
							sources: owner.verifiedSources,
							recordCount: owner.report.legacyEntries
						});
					}
					return pending;
				}
			}, () => {
				remove();
			});
			if (conflict) {
				for (const owner of archiveTargets) {
					owner.deferredPluginIds = deferredPluginSessionStoreIds({
						target: owner.target,
						pending: conflict
					});
					if (owner.deferredPluginIds.length > 0) {
						owner.retainedImportVerified ||= owner.verifiedSources !== void 0;
						owner.report.issues.push({
							code: "plugin_migration_source_retained",
							message: `Plugin migration obligations changed before archival. Original session migration inputs remain pending for plugin(s): ${owner.deferredPluginIds.join(", ")}. Run openclaw doctor --fix after the plugin is available.`
						});
					}
				}
				retainDeferredSources();
				throw new DeferredPluginMigrationConflictError(conflict);
			}
		};
		retainDeferredSources();
		await archiveLegacyArtifacts(archiveTargets, coverage, activeRun, void 0, void 0, publishArchive);
		for (const { target, report } of archiveTargets) appendActiveSqliteTranscriptFileIssues(target, report, deferredSourcePaths);
		for (const report of reports) updateMigrationManifestTarget(activeRun, createMigrationTargetInput(report), report.issues);
		await archiveImportedLegacySessionStores(archiveTargets, activeRun, coverage, void 0, publishArchive);
		for (const owner of archiveTargets) if (owner.retainedImportVerified && owner.deferredPluginIds.length === 0) try {
			retireDeferredPluginSessionImport({
				cfg,
				env,
				target: owner.sourceTarget,
				sqlitePath: owner.target.sqlitePath
			});
		} catch (error) {
			owner.report.issues.push({
				code: "retained_plugin_source_conflict",
				message: `Deferred import receipt awaits retirement: ${formatErrorMessage(error)}. Run openclaw doctor --fix to retry.`
			});
			updateMigrationManifestTarget(activeRun, owner.target, owner.report.issues);
		}
		const hasBlockingIssues = reports.some((report) => countBlockingSessionSqliteIssues(report) > 0);
		activeRun.manifest.completedAt = (/* @__PURE__ */ new Date()).toISOString();
		if (hasBlockingIssues) {
			activeRun.manifest.failedAt = activeRun.manifest.completedAt;
			writeSessionSqliteMigrationManifest(activeRun);
			const failureReports = writeSessionSqliteMigrationFailureReports(activeRun.manifestPath, { reason: "doctor import reported session SQLite migration issues" });
			activeRun.manifest.failureReports = failureReports;
		}
		writeSessionSqliteMigrationManifest(activeRun);
	}
	const report = summarizeDoctorSessionSqliteReport(options.mode, reports, activeRun);
	if (activeRun) {
		const owners = archiveTargets.filter((owner) => owner.retainedImportVerified && owner.deferredPluginIds.length > 0 && !owner.sourceConflicts?.size).map((owner) => {
			const receipt = readDeferredPluginSessionImport({
				cfg,
				target: owner.sourceTarget,
				sqlitePath: owner.target.sqlitePath,
				env
			});
			if (!receipt) throw new Error("Verified retained session import receipt is missing.");
			return {
				owner,
				receipt
			};
		});
		if (owners.length > 0) retainedArchivePlans.set(report, {
			cfg,
			env: { ...env },
			owners
		});
	}
	return report;
}
/** Verified originals retained for unavailable plugins still await settlement. */
function hasRetainedDoctorSessionSources(report) {
	return retainedArchivePlans.has(report);
}
/** Retire only this import's verified originals before the last plugin obligation clears. */
async function settleRetainedDoctorSessionSources(report, completedPluginIds, authority, assertCompletionCurrent) {
	const plan = retainedArchivePlans.get(report);
	if (!plan) return;
	const assertCurrent = () => {
		authority.assertCurrent();
		assertCompletionCurrent();
	};
	assertCurrent();
	retainedArchivePlans.delete(report);
	const completed = new Set(completedPluginIds);
	const expectedPending = readDeferredPluginMigrations({ env: plan.env });
	if (expectedPending.filter((plugin) => !completed.has(plugin.pluginId)).length > 0) return;
	const publishArchive = (remove, retainSource) => {
		const conflict = withDeferredPluginMigrationsCurrent({
			env: plan.env,
			expectedPending,
			onConflict(pending) {
				authority.assertCurrent();
				retainSource();
				return pending;
			}
		}, () => {
			remove();
		});
		if (conflict) throw new DeferredPluginMigrationConflictError(conflict);
	};
	const verifyImports = () => {
		assertCurrent();
		for (const { owner, receipt } of plan.owners) {
			const current = readDeferredPluginSessionImport({
				cfg: plan.cfg,
				target: owner.sourceTarget,
				sqlitePath: owner.target.sqlitePath,
				env: plan.env
			});
			if (!current || !isDeepStrictEqual(current, receipt)) throw new Error("Verified retained session import receipt changed before settlement.");
		}
	};
	const owners = plan.owners.map(({ owner }) => ({
		...owner,
		deferredPluginIds: [],
		report: {
			...owner.report,
			archivedLegacyStoreFiles: [...owner.report.archivedLegacyStoreFiles ?? []],
			archivedTranscriptFiles: [...owner.report.archivedTranscriptFiles],
			archivedUnreferencedJsonlFiles: [...owner.report.archivedUnreferencedJsonlFiles],
			issues: owner.report.issues.filter((issue) => issue.code !== "plugin_migration_source_retained")
		}
	}));
	let activeRun;
	let failure;
	try {
		verifyImports();
		const coverage = gatherLegacyArchiveCoverage(plan.cfg, plan.env, owners.map(({ sourceTarget }) => sourceTarget));
		verifyImports();
		const targets = owners.map(({ target }) => target);
		assertDoctorSqliteMaintenancePathsNotAliased("retained session source settlement", resolveDoctorSessionSqliteMaintenancePaths(targets), resolveDoctorSessionSqliteMaintenanceRoots(targets, plan.env));
		assertCurrent();
		activeRun = createSessionSqliteMigrationRun(plan.env, targets);
		for (const owner of owners) updateMigrationManifestTarget(activeRun, owner.target, owner.report.issues, { validationBeforeArchive: "passed" });
		const capturedSources = new Set(plan.owners.flatMap(({ receipt }) => receipt.sources.map((source) => source.path)));
		await archiveLegacyArtifacts(owners, coverage, activeRun, assertCurrent, capturedSources, publishArchive);
		verifyImports();
		await archiveImportedLegacySessionStores(owners.filter((owner) => owner.report.issues.every(isRetainedSourceIssue)), activeRun, coverage, assertCurrent, publishArchive);
		verifyImports();
		const issue = owners.flatMap((owner) => owner.report.issues).find((candidate) => !isRetainedSourceIssue(candidate));
		if (issue || owners.some((owner) => fs.existsSync(owner.target.storePath))) throw new Error(issue?.message ?? "Retained session sources could not be archived.");
		for (const { owner } of plan.owners) retireDeferredPluginSessionImport({
			cfg: plan.cfg,
			env: plan.env,
			target: owner.sourceTarget,
			sqlitePath: owner.target.sqlitePath,
			completedPluginIds,
			assertCurrent
		});
	} catch (error) {
		failure = error instanceof Error ? error : new Error(formatErrorMessage(error));
		const failedOwners = owners.filter((owner) => owner.report.issues.some((issue) => !isRetainedSourceIssue(issue)));
		for (const [index, owner] of owners.entries()) {
			owner.report.issues.push(...plan.owners[index].owner.report.issues.filter((issue) => issue.code === "plugin_migration_source_retained"));
			if (failedOwners.length === 0 || failedOwners.includes(owner)) {
				const ownIssue = owner.report.issues.find((issue) => !isRetainedSourceIssue(issue) && issue.code !== "plugin_migration_source_retained");
				owner.report.issues.push({
					code: "retained_plugin_source_settlement_failed",
					message: ownIssue?.message ?? formatErrorMessage(error)
				});
			}
		}
	}
	for (const [index, owner] of owners.entries()) Object.assign(plan.owners[index].owner.report, owner.report);
	if (activeRun) {
		assertCurrent();
		for (const owner of owners) updateMigrationManifestTarget(activeRun, owner.target, owner.report.issues);
		activeRun.manifest.completedAt = (/* @__PURE__ */ new Date()).toISOString();
		if (failure) activeRun.manifest.failedAt = activeRun.manifest.completedAt;
		writeSessionSqliteMigrationManifest(activeRun);
	}
	Object.assign(report, summarizeDoctorSessionSqliteReport(report.mode, report.targets, activeRun));
	if (failure) throw failure;
}
/** 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)
	});
}
async function inspectOrMigrateTarget(params) {
	const issues = [];
	const isSqliteStore = params.target.storePath.endsWith(".sqlite");
	const report = createDoctorSessionSqliteTargetReport({
		...params.target,
		sqlitePath: resolveTargetSqlitePath(params.target, params.env),
		sqliteEntries: readSqliteEntryCount(params.target),
		archivedLegacyStoreFiles: [],
		issues
	});
	const retained = await prepareRetainedSessionImport(params, report);
	if (!retained) return report;
	const { retainedImport, staleImport, sourceConflicts, retainedIndexPath } = retained;
	if (params.mode === "recover" && !shouldFilterLegacySessionRecordsByTarget(params.target)) await archiveConflictingRetainedSessionSources(params, sourceConflicts, report);
	const allRecords = isSqliteStore || sourceConflicts.has(path.resolve(params.target.storePath)) ? [] : readLegacySessionRecords(params.target, issues, {
		allowMissingStore: true,
		...retainedIndexPath ? { sourcePath: retainedIndexPath } : {},
		verifiedSourcePaths: retainedImport ? new Set(retainedImport.sources.map((source) => source.path)) : void 0
	});
	if (!isSqliteStore && (!retainedImport || !retainedIndexPath) && params.mode !== "inspect" && params.mode !== "compact" && (issues.every(({ code }) => code === "retained_plugin_source_index_rebuilt") || retainedImport || staleImport)) await appendLegacyHistoricalTranscripts({
		...params,
		allRecords,
		retainedImport,
		sourceConflicts,
		issues
	});
	let records = shouldFilterLegacySessionRecordsByTarget(params.target) ? allRecords.filter((record) => isLegacySessionRecordOwnedByTarget(params.cfg, params.target, record.sessionKey)) : allRecords;
	if (staleImport) {
		const snapshot = readOnlySqliteValidationSnapshot(params.target);
		if (!snapshot.ok) {
			issues.push({
				code: "sqlite_read_failed",
				message: String(snapshot.error)
			});
			return report;
		}
		records = records.filter((record) => {
			if (!snapshot.snapshot.archivedSessionIds.has(record.entry.sessionId)) {
				record.preserveCurrentSession = true;
				return true;
			}
			issues.push({
				code: "historical_transcript_deferred",
				sessionKey: record.sessionKey,
				message: `${record.transcriptPath}: live SQLite already archives this history. Retained source was not replayed; existing deletion and archive state remain authoritative.`
			});
			return false;
		});
	}
	const referencedTranscriptFiles = new Set(allRecords.flatMap((record) => record.transcriptPath ? [record.transcriptPath] : []));
	Object.assign(report, {
		legacyEntries: records.length,
		referencedTranscriptFiles: referencedTranscriptFiles.size,
		unreferencedJsonlFiles: isSqliteStore ? [] : listUnreferencedJsonlFiles(params.target.storePath, [...referencedTranscriptFiles])
	});
	const retainedSourcePaths = retainedImport ? new Set(retainedImport.sources.map((source) => canonicalMigrationFilePath(source.path))) : void 0;
	if (params.mode === "import" && retainedImport || params.mode === "recover") {
		const activeRecords = await prepareActiveSqliteTranscriptSettlement({
			target: params.target,
			env: params.env,
			report,
			excludedPaths: retainedSourcePaths ?? /* @__PURE__ */ new Set()
		});
		if (activeRecords.length > 0) {
			const target = createMigrationTargetInput(params.target);
			const activeRun = params.activeRun ?? createSessionSqliteMigrationRun(params.env, [target]);
			const activeCoverage = gatherLegacyArchiveCoverage(params.cfg, params.env, [params.target]);
			if (isSqliteStore) activeCoverage.selectedStorePaths.add(target.storePath);
			await archiveLegacyArtifacts([{
				sourceTarget: params.target,
				target,
				report,
				validated: true,
				deferredPluginIds: [],
				retainedImportVerified: false,
				records: activeRecords.map(({ entry, ...record }) => ({
					...record,
					sessionId: entry.sessionId
				}))
			}], activeCoverage, activeRun, void 0, new Set(activeRecords.map((record) => record.transcriptPath)));
			updateMigrationManifestTarget(activeRun, target, report.issues, { validationBeforeArchive: "passed" });
			if (!params.activeRun) {
				activeRun.manifest.completedAt = (/* @__PURE__ */ new Date()).toISOString();
				writeSessionSqliteMigrationManifest(activeRun);
			}
		}
	}
	if (params.mode === "import" && retainedImport) {
		for (const source of report.unreferencedJsonlFiles) if (!retainedSourcePaths?.has(canonicalMigrationFilePath(source)) && fs.existsSync(source)) report.issues.push({
			code: "plugin_migration_source_retained",
			message: `${source}: skipped because the deferred-plugin-session-import receipt for ${params.target.storePath} still retains inputs for plugin(s): ${retainedImport.pluginIds.join(", ")}. Run openclaw doctor --fix to finish or retire the plugin migration, then rerun openclaw doctor --session-sqlite import. The new transcript remains in place.`
		});
	}
	if (retainedImport && params.mode !== "import" && retainedImport.sources.some((source) => fs.existsSync(source.path))) appendRetainedPluginSessionSourceIssue(report, params.deferredPluginIds ?? []);
	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, retainedSourcePaths);
		return report;
	}
	if (records.length === 0 && report.unreferencedJsonlFiles.length === 0 && !fs.existsSync(params.target.storePath) && !retainedImport) {
		if (issues.length === 0) report.sqliteEntries = 0;
		updateMigrationManifestTarget(params.activeRun, createMigrationTargetInput(params.target), issues);
		return report;
	}
	if (!retainedImport && params.verifyMissingIndex(report)) {
		updateMigrationManifestTarget(params.activeRun, createMigrationTargetInput(params.target), report.issues, { validationBeforeArchive: "passed" });
		return report;
	}
	if (retainedImport) countRetainedSessionSources(retained, records, report);
	else if (params.mode === "import") await importLegacySessionRecords(params, records, report, params.activeRun);
	else if (params.mode === "dry-run") for (const record of records) countLegacyTranscript(record, report);
	else validateLegacySessionRecords(params.target, records, report, "validate", params.env);
	let validationPassed = retainedImport !== void 0;
	if (params.mode === "import" && retainedImport) updateMigrationManifestTarget(params.activeRun, createMigrationTargetInput(params.target), report.issues, { validationBeforeArchive: "passed" });
	if (params.mode === "import" && !retainedImport && countBlockingSessionSqliteIssues(report) === 0) {
		validationPassed = validateLegacySessionRecords(params.target, records, report, "before-archive", params.env);
		updateMigrationManifestTarget(params.activeRun, createMigrationTargetInput(params.target), report.issues, { validationBeforeArchive: validationPassed ? "passed" : "failed" });
		if (validationPassed && params.activeRun) {
			const recoveredMoves = records.flatMap((record) => record.historical?.archiveMove && record.recovery?.complete ? [{
				...record.historical.archiveMove,
				sessionKey: record.sessionKey,
				artifact: {
					...record.historical.archiveMove.artifact,
					classification: "protected",
					reason: HISTORICAL_IMPORT_REASON
				}
			}] : []);
			if (recoveredMoves.length > 0) {
				recordPlannedMigrationMoves(params.activeRun, createMigrationTargetInput(params.target), recoveredMoves);
				recordCompletedMigrationMoves(params.activeRun, createMigrationTargetInput(params.target), recoveredMoves);
			}
		}
		if (validationPassed) await compactSqliteDatabase(params.target, report, {
			env: params.env,
			operation: "import-finalize"
		});
	}
	if (params.mode === "import") {
		const indexIdentity = params.expectedIndexIdentity;
		const indexless = !indexIdentity && !fs.existsSync(params.target.storePath);
		const deferredPluginIds = indexless && (!params.configuredAgentIds.has(params.target.agentId) || records.length === 0) ? [] : params.deferredPluginIds ?? [];
		const verifiedImport = validationPassed && (retainedImport !== void 0 || records.length > 0 || fs.existsSync(resolveTargetSqlitePath(params.target, params.env)));
		let retainedImportVerified = retainedImport !== void 0;
		let verifiedSources = retainedImport?.sources;
		if (!retainedImport && verifiedImport && (indexIdentity || records.some((record) => !record.historical?.archiveMove) || report.unreferencedJsonlFiles.length > 0) && report.issues.every(isRetainedSourceIssue)) try {
			verifiedSources = captureDeferredPluginSessionSources({
				storePath: params.target.storePath,
				indexIdentity,
				records,
				unreferencedJsonlFiles: report.unreferencedJsonlFiles,
				referencedPaths: params.referencedPaths
			});
		} catch (error) {
			report.issues.push({
				code: "transcript_archive_failed",
				message: formatErrorMessage(error)
			});
		}
		if (deferredPluginIds.length > 0 && verifiedImport && report.issues.every(isRetainedSourceIssue)) {
			if (!retainedImport) {
				if (!verifiedSources) {
					report.sqliteEntries = readSqliteEntryCount(params.target);
					return report;
				}
				recordDeferredPluginSessionImport({
					cfg: params.cfg,
					target: params.target,
					sqlitePath: resolveTargetSqlitePath(params.target, params.env),
					env: params.env,
					pluginIds: deferredPluginIds,
					sources: verifiedSources,
					recordCount: records.length
				});
				retainedImportVerified = true;
				if (indexless) report.issues.push({
					code: "retained_plugin_source_index_rebuilt",
					message: `Derived the retained source index from verified transcripts for ${path.dirname(params.target.storePath)}. Recorded their identities in the completed import receipt without creating sessions.json or replaying canonical metadata.`
				});
			}
			appendRetainedPluginSessionSourceIssue(report, deferredPluginIds);
		}
		params.archiveTargets?.push({
			sourceTarget: params.target,
			target: createMigrationTargetInput(params.target),
			report,
			validated: validationPassed,
			deferredPluginIds,
			retainedImportVerified,
			sourceConflicts,
			verifiedSources,
			records: records.filter((record) => !record.historical?.archiveMove && !sourceConflicts.has(record.transcriptPath ?? "")).map(({ entry, ...record }) => Object.assign(record, { sessionId: entry.sessionId }))
		});
	}
	report.sqliteEntries = readSqliteEntryCount(params.target);
	if (params.mode !== "import") appendActiveSqliteTranscriptFileIssues(params.target, report, retainedSourcePaths);
	updateMigrationManifestTarget(params.activeRun, createMigrationTargetInput(params.target), report.issues);
	return report;
}
async function archiveLegacyArtifacts(owners, coverage, activeRun, assertCurrent, capturedSources, publishSourceRemoval) {
	const { selectedStorePaths, referencedPaths, retainedPaths, incompleteDirectories, retainedDirectories } = coverage;
	const references = /* @__PURE__ */ new Map();
	for (const owner of owners) {
		if (!owner.validated || countBlockingSessionSqliteIssues(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 (countBlockingSessionSqliteIssues(owner.report) === 0 && owner.deferredPluginIds.length === 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, capturedSources);
			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 }) => !owner.report.issues.some((issue) => issue.code === "transcript_malformed" && issue.sessionKey === 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" && !first.record.historical ? imports.some((record) => record?.recovery?.repaired) ? "repair-original" : "imported" : "protected",
					reason: complete && first.record.historical && move.kind === "transcript" ? HISTORICAL_IMPORT_REASON : complete && move.kind === "transcript" ? "verified-import-original" : "unimported-or-unknown-history",
					...complete && move.kind === "transcript" && first.record.recovery?.sqliteEvents !== void 0 ? { verification: `superseded by SQLite (${first.record.recovery.events} of ${first.record.recovery.sqliteEvents} events present)` } : {},
					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) || countBlockingSessionSqliteIssues(owner.report) > 0 || incompleteDirectories.has(path.dirname(storePath))) continue;
		const planUnreferencedMove = (source, kind) => {
			try {
				const move = planSessionJsonlArchiveMove({
					archiveKey: "archive-tier",
					baseNameRaw: path.basename(source),
					kind,
					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]])
				});
				return true;
			} catch (error) {
				recordFailure(owner, source, error, true);
				return false;
			}
		};
		const pointers = /* @__PURE__ */ new Set();
		const receiptSources = new Set(owner.retainedImportVerified ? (owner.verifiedSources ?? []).map((source) => source.path) : []);
		for (const source of listUnreferencedJsonlFiles(storePath, [...referencedPaths, ...planned.keys()])) {
			if (retainedPaths.has(source)) continue;
			if (capturedSources && !capturedSources.has(source) || owner.retainedImportVerified && !receiptSources.has(source)) continue;
			const pointer = resolveTrajectoryPointerPath(source);
			if (planUnreferencedMove(source, "unreferenced-jsonl") && pointer) pointers.add(pointer);
		}
		for (const transcript of receiptSources) {
			const pointer = resolveTrajectoryPointerPath(transcript);
			if (pointer && receiptSources.has(pointer) && !owner.sourceConflicts?.has(transcript) && !fs.existsSync(transcript)) pointers.add(pointer);
		}
		for (const pointer of pointers) {
			const source = canonicalMigrationFilePath(pointer);
			if (fs.existsSync(source) && !planned.has(source) && !referencedPaths.has(source) && !retainedPaths.has(source) && !owner.sourceConflicts?.has(pointer) && (!capturedSources || capturedSources.has(source))) planUnreferencedMove(source, "trajectory");
		}
	}
	for (const owner of owners) for (const { path: source } of owner.verifiedSources ?? []) {
		const shared = planned.get(source);
		if (shared && !shared.owners.has(owner)) shared.owners.set(owner, void 0);
	}
	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) {
		assertCurrent?.();
		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);
		assertCurrent?.();
		await moveMigrationArtifact(move.sourcePath, move.archivePath, move.artifact.identity, assertCurrent ? () => {
			assertCurrent();
		} : void 0, publishSourceRemoval);
		assertCurrent?.();
		completed.add(move.sourcePath);
		for (const { report } of referencingOwners.keys()) (move.kind === "unreferenced-jsonl" ? report.archivedUnreferencedJsonlFiles : report.archivedTranscriptFiles).push(move.archivePath);
	} catch (error) {
		if (error instanceof DeferredPluginMigrationConflictError && error.pending.length > 0) break;
		for (const owner of referencingOwners.keys()) recordFailure(owner, move.sourcePath, error, move.kind === "unreferenced-jsonl");
	}
	for (const owner of owners) {
		assertCurrent?.();
		recordCompletedMigrationMoves(activeRun, owner.target, movesForOwner(owner).filter((move) => completed.has(move.sourcePath)));
		owner.report.unreferencedJsonlFiles = listUnreferencedJsonlFiles(owner.target.storePath, [...referencedPaths]);
	}
}
//#endregion
export { hasRetainedDoctorSessionSources, reconcileDoctorSessionSqlitePublication, runDoctorSessionSqlite, settleRetainedDoctorSessionSources };