UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

1,532 lines • 72.8 kB
import { o as normalizeLowercaseStringOrEmpty } from "./string-coerce-6kR-86XC.mjs";
import { a as asOptionalRecord, c as isRecord } from "./record-coerce-TIsquoVR.mjs";
import { r as resolveStateDir } from "./state-dir-CfAW6S2K.mjs";
import "./paths-FklbMVOH.mjs";
import { t as formatErrorMessage } from "./errors-BSdPMFao.mjs";
import { t as formatCliCommand } from "./command-format-Cte1SWM4.mjs";
import { a as iterateSqliteQuerySync, i as getNodeSqliteKysely, n as executeSqliteQuerySync, r as executeSqliteQueryTakeFirstSync, u as sql } from "./kysely-sync-BMSZn9yz.mjs";
import { n as normalizeAgentId } from "./agent-id-D1-Q6vR4.mjs";
import { r as cloneEnvWithPlatformSemantics } from "./config-env-vars-oFV7TqIg.mjs";
import { f as withExistingOpenClawStateDatabaseReadOnly } from "./openclaw-state-db-readonly-BLZyK3So.mjs";
import { d as resolveAgentWorkspaceDir, r as resolveAgentConfig } from "./agent-scope-config-BU4qc9bA.mjs";
import { i as normalizeMainKey, r as isIncognitoSessionKey } from "./session-key-DDIJxXfQ.mjs";
import { A as parseAgentSessionKey, l as resolveAgentIdFromSessionKey, t as DEFAULT_AGENT_ID } from "./session-key-BC4m_Ly5.mjs";
import { i as listAgentIds } from "./agent-roster-B45JERT5.mjs";
import { l as resolveSessionStorePathCore } from "./paths-CAzSovN1.mjs";
import { l as runOpenClawStateWriteTransaction, r as openOpenClawStateDatabase } from "./openclaw-state-db-VZFEEleX.mjs";
import { g as resolveSessionAgentId } from "./agent-scope-DWB4x52T.mjs";
import { o as normalizeExecSecurity, r as normalizeExecAsk, s as normalizeExecTarget, u as resolveExecModeFromPolicy } from "./exec-approvals-core-GflUelcW.mjs";
import { a as closeOpenClawAgentDatabaseByPath, o as closeOpenClawAgentDatabaseByPathAsync } from "./openclaw-agent-db-lifecycle-BmHpWusP.mjs";
import { c as openOpenClawAgentDatabase, d as runOpenClawAgentWriteTransaction, s as isOpenClawAgentDatabaseOpen } from "./openclaw-agent-db-CFSa_Qik.mjs";
import { n as attachSessionEntrySnapshots, r as sessionEntrySnapshotColumns } from "./session-entry-snapshots-BPOf-joR.mjs";
import { r as resolveAgentMainSessionKey } from "./main-session-C5LOaIBF.mjs";
import { n as withOpenClawAgentDatabaseReadOnly } from "./openclaw-agent-db-readonly-C4g5X1l9.mjs";
import { o as resolveDeliveryProvenCanonicalSessionKey } from "./store-entry-CBksE8pX.mjs";
import { _ as toDatabaseOptions, f as resolveSqliteTranscriptArchiveDirectory, i as getSessionKysely, l as resolveSqliteScope } from "./session-accessor.sqlite-scope-Dovl36Ht.mjs";
import { t as applyExecPolicyLayer } from "./exec-policy-Fg0Bqqoy.mjs";
import { t as parseReadableSqliteSessionEntryRow } from "./session-accessor.sqlite-entry-read-BLXI9I-v.mjs";
import { p as setCanonicalSqliteSessionMainKey } from "./session-canonical-key-DKUMSxwU.mjs";
import { r as stripRuntimeOnlySessionSkillsFields } from "./store-entry-shape-C9fmmOjK.mjs";
import { r as publishSessionEntryCacheInvalidation } from "./session-accessor.sqlite-entry-cache-CR8WuEez.mjs";
import { o as preserveCreationStamp } from "./session-entry-provenance-BWZtrWXT.mjs";
import { t as resolveAgentSessionDirs } from "./session-dirs-SATsXf4g.mjs";
import { a as resolveAllAgentSessionStoreTargetsSync } from "./targets-B3vrlQbX.mjs";
import { g as copySessionNodeArtifactsForRepair, v as deleteSessionMembersForRepair } from "./session-accessor.sqlite-entry-store-BVV5x7kL.mjs";
import { r as collectSessionStateIdsForEntry } from "./session-accessor.sqlite-references-clcV8TKC.mjs";
import { t as SessionTranscriptColdError } from "./session-cold-storage-state-DymPEuGu.mjs";
import { t as note } from "./note-BpWs6H5N.mjs";
import { o as writeTranscriptArchive } from "./session-accessor.sqlite-archive-artifact-DlXthBPF.mjs";
import { r as isSessionTranscriptProjectionUnavailableError } from "./session-transcript-projection-error-B7C6SQei.mjs";
import { c as loadTranscriptEvents } from "./session-accessor.sqlite-read-CAR7IGtb.mjs";
import { i as listSessionEntryKeysReadOnly, s as loadSessionEntry, u as patchSessionEntryCore } from "./session-accessor.sqlite-entry-BuCmtpuj.mjs";
import { F as rewriteDoctorSessionEntries, I as listCanonicalSessionRepairFacts, L as loadCanonicalSessionRepairEntries, l as readSessionTranscriptWatermark, z as scanDoctorSessionEntriesTolerant } from "./session-accessor-BM3Wa1Bx.mjs";
import { a as resolveStoredSessionKeyForAgentStore, n as resolveSessionStoreAgentId } from "./session-store-key-11PJMhso.mjs";
import { a as rehomeSqliteSessionDeliveryReferencesForCanonicalRepair, n as ensureSqliteTranscriptGenerationsForCanonicalRepair, o as rehomeSqliteSessionDeliveryReferencesForCanonicalRepairBatch, r as listSqliteSessionGenerationIdsForCanonicalRepair, t as copySqliteSessionOwnedStateForCanonicalRepair } from "./session-accessor.sqlite-canonical-repair-38cWbKBJ.mjs";
import { b as applySessionEntryLifecycleMutation } from "./session-accessor.reset-BDT6VRzw.mjs";
import { n as replaceSessionOwnerInTransaction } from "./session-accessor.sqlite-owner-Ct3onpS0.mjs";
import { c as readSessionTranscriptBoundedMessageTailPage, l as readSessionTranscriptMessageEventPage } from "./session-accessor.sqlite-active-events-DgaYrqkj.mjs";
import { c as hasInterSessionUserProvenance } from "./input-provenance-v8gTqxPd.mjs";
import { t as SESSION_PERMISSION_BY_EXEC_MODE } from "./session-permission-exec-mode-Bm_ZQ6Ge.mjs";
import { h as resolveExecTarget } from "./bash-tools.exec-runtime-CrVYpYBr.mjs";
import { i as sqliteMessageEventWithSeq } from "./session-transcript-entry-message-B1t5Wym0.mjs";
import { t as deriveGoalSessionTitle } from "./derive-goal-session-title-CxMjELIW.mjs";
import { r as sessionTitleRequests, t as hasExplicitSessionName } from "./session-title-state-BQCPqYS_.mjs";
import { n as projectSessionDisplayMessage } from "./session-display-projection-Bl_ONcgC.mjs";
import { _ as listRegistryWorktreesForMigration } from "./registry-CvH4JdSJ.mjs";
import { a as normalizeLegacyOpenAICodexTranscriptMetadata, i as hasBrokenPromptRewriteBranch, o as selectActivePath } from "./session-sqlite-transcript-verification-BVJNZDCa.mjs";
import { d as resolveTargetSqlitePath, i as projectExistingAgentDatabaseTargets, r as listExistingAgentDatabaseTargets, u as resolveTargetSqliteOptions } from "./session-sqlite-migration-readers-BGenfPwJ.mjs";
import { n as normalizeLegacySessionEntryDelivery } from "./state-migrations.legacy-session-store-eX1Nynjl.mjs";
import { i as withDoctorSqliteMaintenanceLock, t as DoctorSqliteMaintenanceLockUnavailableError } from "./doctor-sqlite-maintenance-lock-CDczri2A.mjs";
import { t as formatSessionSqliteMigrationWarnings } from "./doctor-session-sqlite-warnings-Bbk_gRSH.mjs";
import { n as createLegacyStateMigrationStepReceipt } from "./state-migrations.messages-BNF0lAqi.mjs";
import { a as repairAcpSessionMetaKeysForDoctor, i as runPostSessionPluginDoctorStateRepairs } from "./state-migrations.plugin-doctor-L4YfouP2.mjs";
import { s as resolveProjectRegistry } from "./project-registry-3aLiY1zE.mjs";
import { i as isInformationalMissingSessionIndex } from "./doctor-session-sqlite-types-DXYBQ3Cs.mjs";
import { n as runDoctorAgentDatabaseOperationAsync, t as runDoctorAgentDatabaseOperation } from "./doctor-agent-database-operation-CsMNsFAs.mjs";
import path from "node:path";
import fs from "node:fs/promises";
import { walkDirectory } from "@openclaw/fs-safe/walk";
//#region src/config/sessions/transcript-jsonl.ts
function serializeJsonlLines(lines) {
	return lines.length > 0 ? `${lines.join("\n")}\n` : "";
}
//#endregion
//#region src/commands/doctor-session-canonical-owner-evidence.ts
/** Projects transcript-owner evidence through aliases and indexes every proven source key. */
function applyCanonicalOwnerEvidence(inventory) {
	const bySessionKey = new Map(inventory.map((item) => [`${item.target.sqlitePath}\0${item.sessionKey}`, item]));
	const resolved = /* @__PURE__ */ new Map();
	const resolveCanonicalKey = (item) => {
		const seen = /* @__PURE__ */ new Set();
		let current = item;
		let canonicalKey;
		for (;;) {
			const cached = resolved.get(current);
			if (cached !== void 0) {
				canonicalKey = cached;
				break;
			}
			if (seen.has(current)) return current.canonicalKey;
			seen.add(current);
			const owner = current.canonicalOwnerSessionKey ? bySessionKey.get(`${current.target.sqlitePath}\0${current.canonicalOwnerSessionKey}`) : void 0;
			if (!owner) {
				canonicalKey = current.canonicalKey;
				break;
			}
			current = owner;
		}
		for (const visited of seen) resolved.set(visited, canonicalKey);
		return canonicalKey;
	};
	const canonicalKeysByStoredKey = /* @__PURE__ */ new Map();
	for (const item of inventory) {
		item.canonicalKey = resolveCanonicalKey(item);
		const ownerAgentId = parseAgentSessionKey(item.storedKey)?.agentId ?? item.target.agentId;
		for (const key of [item.sessionKey, item.storedKey]) for (const sqlitePath of [item.target.sqlitePath, "*"]) {
			const mappingKey = `${sqlitePath}\0${ownerAgentId}\0${key}`;
			const mapped = canonicalKeysByStoredKey.get(mappingKey) ?? /* @__PURE__ */ new Set();
			mapped.add(item.canonicalKey);
			canonicalKeysByStoredKey.set(mappingKey, mapped);
		}
	}
	return canonicalKeysByStoredKey;
}
//#endregion
//#region src/commands/doctor-session-canonical-candidates.ts
function listCanonicalSessionStores(params) {
	return projectExistingAgentDatabaseTargets(resolveAllAgentSessionStoreTargetsSync(params.cfg, { env: params.env }), params.env, params.cfg);
}
function collectCanonicalSessionCandidateFacts(params, stores) {
	const defaultAgentRemoved = !listAgentIds(params.cfg).includes(DEFAULT_AGENT_ID);
	const mainKey = normalizeMainKey(params.cfg.session?.mainKey);
	const resolveStoredKey = (agentId, sessionKey) => {
		const parsed = parseAgentSessionKey(sessionKey);
		const rest = normalizeLowercaseStringOrEmpty(parsed?.rest);
		const repairLegacyMainHead = defaultAgentRemoved && parsed?.agentId === "main" && normalizeAgentId(agentId) !== "main" && (rest === "main" || rest === mainKey);
		return resolveStoredSessionKeyForAgentStore({
			cfg: params.cfg,
			agentId,
			sessionKey,
			preserveQualifiedAddress: !repairLegacyMainHead
		});
	};
	const inventory = stores.flatMap((target) => listCanonicalSessionRepairFacts({
		agentId: target.agentId,
		storePath: target.storePath
	}).map((inventoryFact) => {
		const { canonicalOwnerSessionKey, sessionKey } = inventoryFact;
		const storedKey = resolveStoredKey(target.agentId, sessionKey);
		return {
			canonicalKey: storedKey ? resolveDeliveryProvenCanonicalSessionKey(storedKey, inventoryFact) : resolveAgentMainSessionKey({
				cfg: params.cfg,
				agentId: target.agentId
			}),
			canonicalOwnerSessionKey,
			inventoryFact,
			sessionKey,
			storedKey,
			target
		};
	}));
	const canonicalKeysByStoredKey = applyCanonicalOwnerEvidence(inventory);
	return inventory.map(({ canonicalKey, canonicalOwnerSessionKey, inventoryFact, sessionKey, target }) => {
		const canonicalAgentId = canonicalKey === "global" || canonicalKey === "unknown" ? target.agentId : resolveSessionStoreAgentId(params.cfg, canonicalKey);
		const canonicalizeLineageKey = (value) => {
			if (!value) return;
			const storedKey = resolveStoredKey(canonicalAgentId, value);
			const ownerAgentId = parseAgentSessionKey(storedKey)?.agentId ?? canonicalAgentId;
			for (const sqlitePath of [target.sqlitePath, "*"]) for (const key of [value, storedKey]) {
				const mapped = canonicalKeysByStoredKey.get(`${sqlitePath}\0${ownerAgentId}\0${key}`);
				if (mapped?.size === 1) return [...mapped][0];
			}
			return storedKey;
		};
		const parentSessionKey = canonicalizeLineageKey(inventoryFact.parentSessionKey);
		const spawnedBy = canonicalizeLineageKey(inventoryFact.spawnedBy);
		const forkSourceSessionKey = canonicalizeLineageKey(inventoryFact.forkSourceSessionKey);
		return Object.assign({
			agentId: target.agentId,
			canonicalKey,
			inventoryFact,
			lineageRepairRequired: parentSessionKey !== inventoryFact.parentSessionKey || spawnedBy !== inventoryFact.spawnedBy || forkSourceSessionKey !== inventoryFact.forkSourceSessionKey,
			ownerEvidenceOnly: canonicalOwnerSessionKey !== void 0,
			sessionKey,
			sqlitePath: target.sqlitePath,
			storePath: target.storePath
		}, forkSourceSessionKey ? { normalizedForkSourceSessionKey: forkSourceSessionKey } : {}, parentSessionKey ? { normalizedParentSessionKey: parentSessionKey } : {}, spawnedBy ? { normalizedSpawnedBy: spawnedBy } : {});
	});
}
function resolveCanonicalSessionDestination(params) {
	const agentId = params.canonicalKey === "global" || params.canonicalKey === "unknown" ? normalizeAgentId(params.sourceAgentId ?? resolveSessionStoreAgentId(params.cfg, params.canonicalKey)) : resolveSessionStoreAgentId(params.cfg, params.canonicalKey);
	const storePath = resolveSessionStorePathCore(params.cfg.session?.store, {
		agentId,
		env: params.env
	});
	return {
		agentId,
		storePath,
		sqlitePath: resolveTargetSqlitePath({
			agentId,
			storePath
		})
	};
}
function groupRepairCandidates(candidates, params) {
	const byCanonicalKey = /* @__PURE__ */ new Map();
	for (const candidate of candidates) {
		const sentinelOwner = candidate.canonicalKey === "global" || candidate.canonicalKey === "unknown" ? candidate.agentId : "";
		const groupKey = `${candidate.canonicalKey}\0${sentinelOwner}`;
		const group = byCanonicalKey.get(groupKey) ?? [];
		group.push(candidate);
		byCanonicalKey.set(groupKey, group);
	}
	return [...byCanonicalKey.values()].flatMap((group) => {
		const first = group[0];
		const destination = resolveCanonicalSessionDestination({
			canonicalKey: first.canonicalKey,
			cfg: params.cfg,
			env: params.env,
			sourceAgentId: first.agentId
		});
		if (!(group.length > 1 || group.some((candidate) => candidate.inventoryFact.rawCompareRequired || candidate.lineageRepairRequired || candidate.sessionKey !== candidate.canonicalKey || candidate.sqlitePath !== destination.sqlitePath))) return [];
		const canonicalRowSurvives = group.some((candidate) => candidate.sqlitePath === destination.sqlitePath && candidate.sessionKey === candidate.canonicalKey);
		return [{
			candidates: group,
			removedRows: group.length - (canonicalRowSurvives ? 1 : 0)
		}];
	});
}
function collectCanonicalSessionRepairGroups(params, stores) {
	return groupRepairCandidates(collectCanonicalSessionCandidateFacts(params, stores), params);
}
//#endregion
//#region src/commands/doctor-session-canonical-keys.ts
function createCanonicalRepairRemoval(candidate, params) {
	const removal = {
		archiveRemovedTranscript: params.archiveRemovedTranscript,
		deleteOwnedWindows: params.deleteOwnedWindows,
		...params.deliveryCleanupKeys ? { deliveryCleanupKeys: params.deliveryCleanupKeys } : {},
		exactStoredKey: true,
		expectedEntry: candidate.expectedEntry,
		sessionKey: candidate.sessionKey
	};
	return candidate.rawEntryJson === void 0 ? removal : Object.assign(removal, {
		expectedRawEntryJson: candidate.rawEntryJson,
		expectedSnapshotRevision: candidate.rawSnapshotRevision
	});
}
const CANONICAL_SESSION_REPAIR_BATCH_GROUP_LIMIT = 64;
function hydrateCanonicalSessionCandidate(fact, loaded) {
	const entry = { ...loaded.entry };
	if (fact.normalizedParentSessionKey) entry.parentSessionKey = fact.normalizedParentSessionKey;
	else delete entry.parentSessionKey;
	if (fact.normalizedSpawnedBy) entry.spawnedBy = fact.normalizedSpawnedBy;
	else delete entry.spawnedBy;
	if (entry.forkSource && fact.normalizedForkSourceSessionKey) entry.forkSource = {
		...entry.forkSource,
		sessionKey: fact.normalizedForkSourceSessionKey
	};
	else if (entry.forkSource?.sessionKey !== void 0) {
		const { sessionKey: _invalidSessionKey, ...forkProvenance } = entry.forkSource;
		entry.forkSource = forkProvenance;
	}
	const candidate = {
		agentId: fact.agentId,
		canonicalKey: fact.canonicalKey,
		entry,
		expectedEntry: loaded.entry,
		ownerEvidenceOnly: fact.ownerEvidenceOnly,
		sessionKey: fact.sessionKey,
		sqlitePath: fact.sqlitePath,
		storePath: fact.storePath
	};
	return loaded.rawEntryJson !== void 0 ? {
		...candidate,
		rawEntryJson: loaded.rawEntryJson,
		rawSnapshotRevision: loaded.rawSnapshotRevision
	} : candidate;
}
function hydrateCanonicalSessionCandidates(facts) {
	const loaded = /* @__PURE__ */ new Map();
	const byStore = /* @__PURE__ */ new Map();
	for (const fact of facts) {
		const key = `${fact.agentId}\0${fact.storePath}`;
		byStore.set(key, [...byStore.get(key) ?? [], fact]);
	}
	for (const group of byStore.values()) {
		const first = group[0];
		if (!first) continue;
		const entries = loadCanonicalSessionRepairEntries({
			agentId: first.agentId,
			storePath: first.storePath
		}, group.map((fact) => fact.inventoryFact));
		group.forEach((fact, index) => loaded.set(fact, entries[index]));
	}
	return facts.map((fact) => hydrateCanonicalSessionCandidate(fact, loaded.get(fact)));
}
function mergeCanonicalSessionEntryCandidates(candidates) {
	let selected;
	for (const candidate of candidates) {
		const incomingUpdatedAt = typeof candidate.entry.updatedAt === "number" && Number.isFinite(candidate.entry.updatedAt) ? candidate.entry.updatedAt : 0;
		const selectedUpdatedAt = typeof selected?.entry.updatedAt === "number" && Number.isFinite(selected.entry.updatedAt) ? selected.entry.updatedAt : 0;
		if (!selected || incomingUpdatedAt > selectedUpdatedAt || incomingUpdatedAt === selectedUpdatedAt && (candidate.preferred === true ? !selected.preferred : !selected.preferred && Buffer.compare(Buffer.from(JSON.stringify(candidate.entry), "utf8"), Buffer.from(JSON.stringify(selected.entry), "utf8")) > 0)) selected = {
			entry: structuredClone(candidate.entry),
			preferred: candidate.preferred === true,
			winner: candidate.value
		};
	}
	return selected;
}
function selectCanonicalSessionCandidate(candidates, params) {
	const first = candidates[0];
	if (!first) return;
	const destination = resolveCanonicalSessionDestination({
		canonicalKey: first.canonicalKey,
		cfg: params.cfg,
		env: params.env,
		sourceAgentId: first.agentId
	});
	const rankedCandidates = candidates.toSorted((left, right) => Buffer.compare(Buffer.from(`${left.sqlitePath}\0${left.sessionKey}`, "utf8"), Buffer.from(`${right.sqlitePath}\0${right.sessionKey}`, "utf8"))).map((candidate) => ({
		entry: candidate.entry,
		preferred: candidate.sqlitePath === destination.sqlitePath && candidate.sessionKey === candidate.canonicalKey,
		value: candidate
	}));
	const metadataCandidates = rankedCandidates.filter(({ value }) => !value.ownerEvidenceOnly);
	const selected = mergeCanonicalSessionEntryCandidates(metadataCandidates.length > 0 ? metadataCandidates : rankedCandidates);
	if (!selected) return;
	const requiredCandidates = rankedCandidates.filter(({ entry }) => entry.sandbox === "required");
	const authoritativeStamp = requiredCandidates.find(({ preferred }) => preferred)?.entry ?? mergeCanonicalSessionEntryCandidates(requiredCandidates)?.entry;
	return {
		...selected,
		entry: preserveCreationStamp(selected.entry, authoritativeStamp),
		destination
	};
}
function resolveSingleDatabaseCanonicalRepairGroup(candidates, params) {
	const selected = selectCanonicalSessionCandidate(candidates, params);
	if (!selected || selected.winner.sqlitePath !== selected.destination.sqlitePath || candidates.some((candidate) => candidate.sqlitePath !== selected.destination.sqlitePath)) return;
	return {
		candidates,
		selected
	};
}
function createCanonicalDestinationRemovals(candidates, selected) {
	const relatedSessionIds = new Set([selected.entry.sessionId, selected.entry.previousSessionId].filter((value) => typeof value === "string" && value.length > 0));
	return candidates.filter((candidate) => candidate.sessionKey !== selected.winner.canonicalKey || candidate.rawEntryJson !== void 0).map((candidate) => createCanonicalRepairRemoval(candidate, {
		archiveRemovedTranscript: !relatedSessionIds.has(candidate.entry.sessionId),
		deleteOwnedWindows: false
	}));
}
function listCanonicalDestinationAliasKeys(destinationStore, winner) {
	return destinationStore.map((candidate) => candidate.sessionKey).filter((sessionKey) => sessionKey !== winner.canonicalKey);
}
function applyCanonicalDestinationArtifacts(params) {
	replaceSessionOwnerInTransaction(params.database, params.winner.canonicalKey, params.winner.entry.owner);
	const destinationAliasKeys = listCanonicalDestinationAliasKeys(params.destinationStore, params.winner);
	if (destinationAliasKeys.length > 0) {
		if (params.rehomeDeliveries) rehomeSqliteSessionDeliveryReferencesForCanonicalRepair(params.database, params.winner.canonicalKey, destinationAliasKeys);
		copySessionNodeArtifactsForRepair(params.database, params.database, destinationAliasKeys, params.winner.canonicalKey, { includeMembers: false });
	}
	if (!params.copyWinnerAlias || params.winner.sessionKey === params.winner.canonicalKey) return;
	deleteSessionMembersForRepair(params.database, params.winner.canonicalKey);
	copySessionNodeArtifactsForRepair(params.database, params.database, [params.winner.sessionKey], params.winner.canonicalKey, { includeParticipants: false });
}
async function repairCanonicalSessionGroupsInSingleDatabase(groups) {
	const first = groups[0];
	if (!first) return [];
	await ensureSqliteTranscriptGenerationsForCanonicalRepair(groups.flatMap((group) => group.candidates));
	const destination = first.selected.destination;
	return (await applySessionEntryLifecycleMutation({
		agentId: destination.agentId,
		allowCanonicalRepair: true,
		afterUpsertsInTransaction: (database) => {
			rehomeSqliteSessionDeliveryReferencesForCanonicalRepairBatch(database, groups.map((group) => ({
				canonicalKey: group.selected.winner.canonicalKey,
				previousKeys: listCanonicalDestinationAliasKeys(group.candidates, group.selected.winner)
			})));
			for (const group of groups) applyCanonicalDestinationArtifacts({
				copyWinnerAlias: true,
				database,
				destinationStore: group.candidates,
				rehomeDeliveries: false,
				winner: group.selected.winner
			});
		},
		removals: groups.flatMap((group) => createCanonicalDestinationRemovals(group.candidates, group.selected)),
		skipMaintenance: true,
		storePath: destination.storePath,
		upserts: groups.map((group) => ({
			entry: group.selected.entry,
			sessionKey: group.selected.winner.canonicalKey
		}))
	})).archivedTranscriptDirectories;
}
async function repairCanonicalSessionGroup(candidates, params) {
	const selected = selectCanonicalSessionCandidate(candidates, params);
	if (!selected) return [];
	await ensureSqliteTranscriptGenerationsForCanonicalRepair(candidates);
	const winner = selected.winner;
	const destination = selected.destination;
	const byDatabase = /* @__PURE__ */ new Map();
	for (const candidate of candidates) {
		const group = byDatabase.get(candidate.sqlitePath) ?? [];
		group.push(candidate);
		byDatabase.set(candidate.sqlitePath, group);
	}
	const destinationStore = byDatabase.get(destination.sqlitePath) ?? [];
	const preArchivedDirectories = [];
	if (winner.sqlitePath !== destination.sqlitePath) {
		const generationIds = /* @__PURE__ */ new Set([...listSqliteSessionGenerationIdsForCanonicalRepair({
			agentId: winner.agentId,
			canonicalKey: winner.canonicalKey,
			sourceKeys: [winner.sessionKey],
			storePath: winner.storePath
		}), ...collectSessionStateIdsForEntry(winner.entry)]);
		for (const sessionId of generationIds) {
			if (!sessionId) continue;
			const destinationCollision = destinationStore.find((candidate) => candidate.entry.sessionId === sessionId);
			const [destinationEvents, sourceEvents] = await Promise.all([loadTranscriptEvents({
				agentId: destinationCollision?.agentId ?? destination.agentId,
				sessionId,
				sessionKey: destinationCollision?.sessionKey ?? winner.canonicalKey,
				storePath: destinationCollision?.storePath ?? destination.storePath
			}), loadTranscriptEvents({
				agentId: winner.agentId,
				sessionId,
				sessionKey: winner.sessionKey,
				storePath: winner.storePath
			})]);
			const destinationContent = serializeJsonlLines(destinationEvents.map((event) => JSON.stringify(event)));
			const sourceContent = serializeJsonlLines(sourceEvents.map((event) => JSON.stringify(event)));
			if (!destinationContent || destinationContent === sourceContent) continue;
			const archiveDirectory = resolveSqliteTranscriptArchiveDirectory({
				agentId: destination.agentId,
				env: params.env,
				path: destination.sqlitePath
			});
			writeTranscriptArchive({
				archiveDirectory,
				content: destinationContent,
				reason: "deleted",
				sessionId
			});
			if (!preArchivedDirectories.includes(archiveDirectory)) preArchivedDirectories.push(archiveDirectory);
		}
	}
	setCanonicalSqliteSessionMainKey(openOpenClawAgentDatabase(resolveTargetSqliteOptions(destination, params.env)), params.cfg.session?.mainKey);
	const winnerResult = await applySessionEntryLifecycleMutation({
		agentId: destination.agentId,
		allowCanonicalRepair: true,
		afterUpsertsInTransaction: (destinationDatabase) => {
			applyCanonicalDestinationArtifacts({
				copyWinnerAlias: winner.sqlitePath === destination.sqlitePath,
				database: destinationDatabase,
				destinationStore,
				rehomeDeliveries: true,
				winner
			});
			if (winner.sqlitePath !== destination.sqlitePath) copySqliteSessionOwnedStateForCanonicalRepair({
				canonicalKey: winner.canonicalKey,
				destinationDatabase,
				preferredEntry: selected.entry,
				preferredSessionKey: winner.sessionKey,
				source: winner,
				sourceEntries: [winner.entry],
				sourceKeys: [winner.sessionKey]
			});
		},
		removals: createCanonicalDestinationRemovals(destinationStore, selected),
		skipMaintenance: true,
		storePath: destination.storePath,
		upserts: [{
			entry: selected.entry,
			sessionKey: winner.canonicalKey
		}]
	});
	const archivedDirectories = /* @__PURE__ */ new Set([...preArchivedDirectories, ...winnerResult.archivedTranscriptDirectories]);
	for (const [sqlitePath, storeCandidates] of byDatabase) {
		if (sqlitePath === destination.sqlitePath) continue;
		const [storeCandidate] = storeCandidates;
		if (!storeCandidate) continue;
		const result = await applySessionEntryLifecycleMutation({
			agentId: storeCandidate.agentId,
			allowCanonicalRepair: true,
			removals: storeCandidates.map((candidate) => createCanonicalRepairRemoval(candidate, {
				archiveRemovedTranscript: true,
				deleteOwnedWindows: true,
				deliveryCleanupKeys: [winner.canonicalKey]
			})),
			skipMaintenance: true,
			storePath: storeCandidate.storePath
		});
		for (const directory of result.archivedTranscriptDirectories) archivedDirectories.add(directory);
	}
	return [...archivedDirectories];
}
/** Doctor-owned durable repair; process-held incognito databases are intentionally excluded. */
async function repairCanonicalSessionKeys(params) {
	const env = params.env ?? process.env;
	const stores = listCanonicalSessionStores({
		cfg: params.cfg,
		env
	});
	const archivedTranscriptDirectories = /* @__PURE__ */ new Set();
	let repairBatches = 0;
	let repairedGroups = 0;
	if (params.apply) for (const store of stores) setCanonicalSqliteSessionMainKey(openOpenClawAgentDatabase(resolveTargetSqliteOptions(store, env)), params.cfg.session?.mainKey);
	let repairGroups = collectCanonicalSessionRepairGroups({
		cfg: params.cfg,
		env
	}, stores);
	const foundGroups = repairGroups.length;
	const removedRows = repairGroups.reduce((total, group) => total + group.removedRows, 0);
	if (params.apply) while (repairGroups.length > 0) {
		if (!repairGroups[0]) break;
		const candidateGroups = repairGroups.slice(0, CANONICAL_SESSION_REPAIR_BATCH_GROUP_LIMIT);
		const hydrated = hydrateCanonicalSessionCandidates(candidateGroups.flatMap((candidateGroup) => candidateGroup.candidates));
		let hydratedOffset = 0;
		const hydratedGroups = candidateGroups.map((candidateGroup) => {
			const candidates = hydrated.slice(hydratedOffset, hydratedOffset + candidateGroup.candidates.length);
			hydratedOffset += candidateGroup.candidates.length;
			return candidates;
		});
		const candidates = hydratedGroups[0];
		const singleDatabaseGroup = resolveSingleDatabaseCanonicalRepairGroup(candidates, {
			cfg: params.cfg,
			env
		});
		if (!singleDatabaseGroup) {
			for (const directory of await repairCanonicalSessionGroup(candidates, {
				cfg: params.cfg,
				env
			})) archivedTranscriptDirectories.add(directory);
			repairBatches += 1;
			repairedGroups += 1;
			repairGroups = collectCanonicalSessionRepairGroups({
				cfg: params.cfg,
				env
			}, stores);
			continue;
		}
		const batch = [singleDatabaseGroup];
		for (const nextCandidates of hydratedGroups.slice(1)) {
			const nextSingleDatabaseGroup = resolveSingleDatabaseCanonicalRepairGroup(nextCandidates, {
				cfg: params.cfg,
				env
			});
			if (!nextSingleDatabaseGroup || nextSingleDatabaseGroup.selected.destination.sqlitePath !== singleDatabaseGroup.selected.destination.sqlitePath) break;
			batch.push(nextSingleDatabaseGroup);
		}
		for (const directory of await repairCanonicalSessionGroupsInSingleDatabase(batch)) archivedTranscriptDirectories.add(directory);
		repairBatches += 1;
		repairedGroups += batch.length;
		repairGroups = collectCanonicalSessionRepairGroups({
			cfg: params.cfg,
			env
		}, stores);
	}
	return {
		archivedTranscriptDirectories: [...archivedTranscriptDirectories].toSorted(),
		foundGroups,
		repairBatches,
		removedRows,
		repairedGroups,
		scannedStores: stores.length
	};
}
//#endregion
//#region src/commands/doctor-session-delivery-state.ts
/** Scan or rewrite legacy delivery fields inside existing session row JSON. */
function repairCanonicalSessionDeliveryStates(params) {
	return repairCanonicalSessionEntries({
		...params,
		transform: normalizeLegacySessionEntryDelivery,
		updateDeliveryProjection: true
	});
}
/** Removes runtime-only skill catalogs from previously persisted session rows. */
function repairCanonicalSessionResolvedSkills(params) {
	return repairCanonicalSessionEntries({
		...params,
		transform: stripRuntimeOnlySessionSkillsFields,
		updateDeliveryProjection: false
	});
}
function repairCanonicalSessionEntries(params) {
	const targets = params.targets ?? listExistingAgentDatabaseTargets(params.cfg, params.env);
	let found = 0;
	let repaired = 0;
	for (const target of targets) {
		const sessionKeys = [];
		const operation = runDoctorAgentDatabaseOperation({
			agentId: target.agentId,
			path: target.sqlitePath,
			run: () => {
				scanDoctorSessionEntriesTolerant({
					agentId: target.agentId,
					env: params.env,
					storePath: target.storePath
				}, ({ entry, recoveredFromProjections, sessionKey }) => {
					if (!recoveredFromProjections && params.transform(entry, sessionKey, "scan") !== entry) sessionKeys.push(sessionKey);
				});
				return sessionKeys.length;
			}
		});
		if (!operation.ok) continue;
		found += operation.value;
		if (!params.apply || operation.value === 0) continue;
		const wasOpen = isOpenClawAgentDatabaseOpen(target.sqlitePath);
		try {
			repaired += rewriteDoctorSessionEntries({
				scope: {
					agentId: target.agentId,
					env: params.env,
					storePath: target.storePath
				},
				sessionKeys,
				transform: (entry, sessionKey) => params.transform(entry, sessionKey, "repair"),
				updateDeliveryProjection: params.updateDeliveryProjection
			});
		} finally {
			if (!wasOpen) closeOpenClawAgentDatabaseByPath(target.sqlitePath);
		}
	}
	return {
		found,
		repaired,
		scannedStores: targets.length
	};
}
//#endregion
//#region src/commands/doctor-session-exec-policy.ts
/** Retires session exec overrides without granting the full-mode approval-floor bypass. */
function repairLegacySessionExecPolicy(params) {
	const messages = [];
	repairCanonicalSessionEntries({
		...params,
		updateDeliveryProjection: false,
		transform(entry, sessionKey, phase) {
			const { execSecurity, execAsk, ...next } = entry;
			if (execSecurity === void 0 && execAsk === void 0) return entry;
			let ask = normalizeExecAsk(execAsk);
			if (!next.permissionMode) {
				const agentId = resolveSessionAgentId({
					sessionKey,
					config: params.cfg
				});
				const globalExec = params.cfg.tools?.exec;
				const agentExec = resolveAgentConfig(params.cfg, agentId)?.tools?.exec;
				const { effectiveHost } = resolveExecTarget({
					configuredTarget: normalizeExecTarget(entry.execHost) ?? agentExec?.host ?? globalExec?.host,
					elevatedRequested: false,
					sandboxRequired: entry.sandbox === "required",
					sandboxAvailable: true
				});
				const base = applyExecPolicyLayer(applyExecPolicyLayer({
					security: effectiveHost === "sandbox" ? "deny" : "full",
					ask: "off"
				}, globalExec), agentExec);
				ask ??= base.ask;
				const mode = resolveExecModeFromPolicy({
					security: normalizeExecSecurity(execSecurity) ?? base.security,
					ask
				});
				next.permissionMode = ask === "always" ? "read-only" : mode === "full" ? void 0 : SESSION_PERMISSION_BY_EXEC_MODE[mode];
			}
			if (phase === (params.apply ? "repair" : "scan")) {
				const outcome = next.permissionMode ? `${entry.permissionMode ? "kept" : "set"} permissionMode=${next.permissionMode}${ask === "always" && !entry.permissionMode ? " (ask=always has no mode equivalent; choose guarded or workspace to allow commands again)" : ""}` : "config default applies; full permission mode was not granted";
				messages.push(`- ${sessionKey}: ${params.apply ? "removed" : "would remove"} legacy exec policy; ${outcome}.`);
			}
			return next;
		}
	});
	if (messages.length > 0) {
		if (!params.apply) messages.push("- Run \"openclaw doctor --fix\" to migrate legacy session exec policy.");
		note(messages.join("\n"), "Session exec policy");
	}
}
//#endregion
//#region src/commands/doctor-session-incognito-key-repair-state.ts
const REPAIR_JOURNAL_SCOPE = "doctor-session-key-migration";
const REPAIR_JOURNAL_KEY = "reserved-incognito-v1";
function listTuiLastSessionStateRows(database) {
	const db = getNodeSqliteKysely(database);
	return executeSqliteQuerySync(database, db.selectFrom("config_machine_state").select(["state_key", "value_json"]).where("state_key", "like", "tui.lastSession.%")).rows;
}
function sqliteSchemaIdentifier(value) {
	return sql.id(value);
}
function listSharedStateSessionKeyColumns(database) {
	const db = getNodeSqliteKysely(database);
	return executeSqliteQuerySync(database, db.selectFrom("sqlite_schema").select("name").where("type", "=", "table").where("name", "not like", "sqlite_%")).rows.flatMap(({ name: table }) => {
		return executeSqliteQuerySync(database, db.selectFrom(sql`pragma_table_info(${table})`.as("pragma_columns")).select(sql`name`.as("name"))).rows.flatMap(({ name: column }) => {
			if (column === "session_key" || column.endsWith("_session_key")) return [{
				table,
				column,
				json: false
			}];
			return column.endsWith("_session_keys_json") ? [{
				table,
				column,
				json: true
			}] : [];
		});
	});
}
function collectSharedStateSessionKeys(database) {
	const db = getNodeSqliteKysely(database);
	const keys = /* @__PURE__ */ new Set();
	for (const { table, column, json } of listSharedStateSessionKeyColumns(database)) {
		const columnId = sqliteSchemaIdentifier(column);
		const rows = executeSqliteQuerySync(database, db.selectFrom(sql`${sqliteSchemaIdentifier(table)}`.as("session_key_table")).select(columnId.as("value")).where(columnId, "is not", null)).rows;
		for (const { value } of rows) if (!json && typeof value === "string") keys.add(value);
		else if (json && typeof value === "string") try {
			collectJsonStringValues(JSON.parse(value), keys);
		} catch {}
	}
	for (const row of listTuiLastSessionStateRows(database)) {
		const sessionKey = JSON.parse(row.value_json);
		if (typeof sessionKey === "string") keys.add(sessionKey);
	}
	return keys;
}
function rewriteSharedStateSessionKeys(database, renames) {
	const db = getNodeSqliteKysely(database);
	const rowId = sql`rowid`;
	for (const { table, column, json } of listSharedStateSessionKeyColumns(database)) {
		const tableId = sqliteSchemaIdentifier(table);
		const columnId = sqliteSchemaIdentifier(column);
		if (!json) {
			for (const [from, to] of renames) executeSqliteQuerySync(database, db.updateTable(sql`${tableId}`.as("session_key_table")).set(columnId, to).where(columnId, "=", from));
			continue;
		}
		const rows = executeSqliteQuerySync(database, db.selectFrom(sql`${tableId}`.as("session_key_table")).select([rowId.as("rowid"), columnId.as("value")])).rows;
		for (const row of rows) {
			if (typeof row.value !== "string") continue;
			let parsed;
			try {
				parsed = JSON.parse(row.value);
			} catch {
				continue;
			}
			const rewritten = JSON.stringify(replaceSessionKeyReferences(parsed, renames));
			if (rewritten !== row.value) executeSqliteQuerySync(database, db.updateTable(sql`${tableId}`.as("session_key_table")).set(columnId, rewritten).where(rowId, "=", row.rowid));
		}
	}
	const stateDb = getNodeSqliteKysely(database);
	for (const row of listTuiLastSessionStateRows(database)) {
		const sessionKey = JSON.parse(row.value_json);
		const renamedKey = typeof sessionKey === "string" ? renames.get(sessionKey) : void 0;
		if (renamedKey) executeSqliteQuerySync(database, stateDb.updateTable("config_machine_state").set({ value_json: JSON.stringify(renamedKey) }).where("state_key", "=", row.state_key));
	}
}
function collectJsonStringValues(value, values) {
	if (typeof value === "string") {
		values.add(value);
		return;
	}
	if (Array.isArray(value)) {
		for (const item of value) collectJsonStringValues(item, values);
		return;
	}
	if (!value || typeof value !== "object") return;
	for (const item of Object.values(value)) collectJsonStringValues(item, values);
}
function readRepairJournal(database) {
	const db = getNodeSqliteKysely(database);
	const row = executeSqliteQueryTakeFirstSync(database, db.selectFrom("state_leases").select("payload_json").where("scope", "=", REPAIR_JOURNAL_SCOPE).where("lease_key", "=", REPAIR_JOURNAL_KEY));
	if (!row?.payload_json) return [];
	const parsed = JSON.parse(row.payload_json);
	if (parsed.version !== 1 || !Array.isArray(parsed.renames)) throw new Error("Invalid reserved incognito session key repair journal");
	return parsed.renames.map((item) => {
		if (!item || typeof item !== "object" || Array.isArray(item) || typeof item.from !== "string" || typeof item.to !== "string") throw new Error("Invalid reserved incognito session key repair journal entry");
		return {
			from: item.from,
			to: item.to
		};
	});
}
function readRepairJournalReadOnly(env) {
	return withExistingOpenClawStateDatabaseReadOnly((database) => readRepairJournal(database.db), { env }) ?? [];
}
function writeRepairJournal(database, renames) {
	const now = Date.now();
	const db = getNodeSqliteKysely(database);
	executeSqliteQuerySync(database, db.insertInto("state_leases").values({
		scope: REPAIR_JOURNAL_SCOPE,
		lease_key: REPAIR_JOURNAL_KEY,
		owner: "openclaw-doctor",
		expires_at: null,
		heartbeat_at: null,
		payload_json: JSON.stringify({
			version: 1,
			renames
		}),
		created_at: now,
		updated_at: now
	}).onConflict((conflict) => conflict.columns(["scope", "lease_key"]).doUpdateSet({
		owner: "openclaw-doctor",
		payload_json: JSON.stringify({
			version: 1,
			renames
		}),
		updated_at: now
	})));
}
function deleteRepairJournal(database) {
	const db = getNodeSqliteKysely(database);
	executeSqliteQuerySync(database, db.deleteFrom("state_leases").where("scope", "=", REPAIR_JOURNAL_SCOPE).where("lease_key", "=", REPAIR_JOURNAL_KEY));
}
function replaceSessionKeyReferences(value, renames) {
	if (typeof value === "string") return renames.get(value) ?? value;
	if (Array.isArray(value)) return value.map((item) => replaceSessionKeyReferences(item, renames));
	if (!value || typeof value !== "object") return value;
	return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, replaceSessionKeyReferences(item, renames)]));
}
//#endregion
//#region src/commands/doctor-session-incognito-key-repair.ts
async function repairReservedIncognitoSessionKeys(params) {
	const targets = (params.targets ?? listExistingAgentDatabaseTargets(params.cfg, params.env)).map((target) => ({
		target,
		databaseOptions: resolveTargetSqliteOptions(target, params.env)
	}));
	const reservedKeys = /* @__PURE__ */ new Set();
	const sharedDatabase = params.apply ? openOpenClawStateDatabase({ env: params.env }) : void 0;
	const journalRenames = sharedDatabase ? readRepairJournal(sharedDatabase.db) : readRepairJournalReadOnly(params.env);
	for (const { target, databaseOptions } of targets) {
		const operation = runDoctorAgentDatabaseOperation({
			agentId: target.agentId,
			path: target.sqlitePath,
			run: () => withOpenClawAgentDatabaseReadOnly((database) => listReservedIncognitoKeys(database.db), databaseOptions)
		});
		if (!operation.ok || !operation.value.found) continue;
		for (const key of operation.value.value) reservedKeys.add(key);
	}
	const pendingKeys = new Set(reservedKeys);
	for (const rename of journalRenames) pendingKeys.add(rename.from);
	if (!params.apply) return {
		found: pendingKeys.size,
		repaired: 0
	};
	if (reservedKeys.size === 0 && journalRenames.length === 0) return {
		found: 0,
		repaired: 0
	};
	const occupiedKeys = sharedDatabase ? collectSharedStateSessionKeys(sharedDatabase.db) : /* @__PURE__ */ new Set();
	for (const { target, databaseOptions } of targets) {
		const operation = runDoctorAgentDatabaseOperation({
			agentId: target.agentId,
			path: target.sqlitePath,
			run: () => withOpenClawAgentDatabaseReadOnly((database) => collectOccupiedSessionKeys(database.db), databaseOptions)
		});
		if (operation.ok && operation.value.found) for (const key of operation.value.value) occupiedKeys.add(key);
	}
	for (const rename of journalRenames) occupiedKeys.add(rename.to);
	const journalSources = new Set(journalRenames.map((rename) => rename.from));
	const newRenames = planReservedIncognitoKeyRenames([...reservedKeys].filter((key) => !journalSources.has(key)).toSorted(), occupiedKeys);
	const renames = [...journalRenames, ...newRenames];
	const renameMap = new Map(renames.map((item) => [item.from, item.to]));
	runOpenClawStateWriteTransaction((database) => writeRepairJournal(database.db, renames), { env: params.env }, { operationLabel: "doctor.journal-reserved-incognito-session-keys" });
	runOpenClawStateWriteTransaction((database) => rewriteSharedStateSessionKeys(database.db, renameMap), { env: params.env }, { operationLabel: "doctor.rename-reserved-incognito-shared-state-keys" });
	for (const { target, databaseOptions } of targets) {
		const wasOpen = isOpenClawAgentDatabaseOpen(target.sqlitePath);
		try {
			runOpenClawAgentWriteTransaction((database) => applyReservedIncognitoKeyRenameColumns(database, renames), databaseOptions, { operationLabel: "doctor.rename-reserved-incognito-session-keys" });
			rewriteDoctorSessionEntries({
				scope: {
					agentId: target.agentId,
					env: params.env,
					storePath: target.storePath
				},
				sessionKeys: await listSessionEntryKeysReadOnly({
					agentId: target.agentId,
					env: params.env,
					storePath: target.storePath
				}),
				transform: (entry) => rewriteSessionEntryKeyFields(entry, renameMap)
			});
		} finally {
			if (!wasOpen) closeOpenClawAgentDatabaseByPath(target.sqlitePath);
		}
	}
	runOpenClawStateWriteTransaction((database) => deleteRepairJournal(database.db), { env: params.env }, { operationLabel: "doctor.complete-reserved-incognito-session-keys" });
	return {
		found: pendingKeys.size,
		repaired: renames.length
	};
}
function planReservedIncognitoKeyRenames(keys, occupied) {
	return keys.map((key) => {
		const base = legacyIncognitoSessionKey(key);
		if (parseAgentSessionKey(key)?.rest.startsWith("internal-session-effects:") && occupied.has(base)) throw new Error(`Cannot repair internal session key because ${base} already exists`);
		let candidate = base;
		let suffix = 1;
		while (occupied.has(candidate)) {
			candidate = `${base}-${suffix}`;
			suffix += 1;
		}
		occupied.add(candidate);
		return {
			from: key,
			to: candidate
		};
	});
}
function applyReservedIncognitoKeyRenameColumns(database, renames) {
	if (renames.length === 0) return;
	database.db.exec("PRAGMA defer_foreign_keys = ON;");
	for (const rename of renames) {
		const affected = executeSqliteQuerySync(database.db, getNodeSqliteKysely(database.db).selectFrom("session_nodes").select("session_key").where((eb) => eb.or([
			eb("session_key", "=", rename.from),
			eb("parent_session_key", "=", rename.from),
			eb("spawned_by", "=", rename.from),
			eb("fork_source_session_key", "=", rename.from)
		]))).rows;
		updateSessionKeyColumns(database.db, rename);
		for (const sessionKey of /* @__PURE__ */ new Set([rename.to, ...affected.map((row) => row.session_key)])) publishSessionEntryCacheInvalidation(database, { sessionKey });
	}
}
function legacyIncognitoSessionKey(sessionKey) {
	const parsed = parseAgentSessionKey(sessionKey);
	if (!parsed || !isIncognitoSessionKey(sessionKey)) throw new Error(`Cannot rename non-incognito session key: ${sessionKey}`);
	return `agent:${parsed.agentId}:${parsed.rest.replace(":incognito-", ":legacy-incognito-")}`;
}
function listReservedIncognitoKeys(database) {
	const db = getNodeSqliteKysely(database);
	const keys = /* @__PURE__ */ new Set();
	for (const row of executeSqliteQuerySync(database, db.selectFrom("session_nodes").select("session_key")).rows) keys.add(row.session_key);
	for (const row of executeSqliteQuerySync(database, db.selectFrom("session_windows").select("session_key")).rows) keys.add(row.session_key);
	return [...keys].filter(isIncognitoSessionKey).toSorted();
}
function collectOccupiedSessionKeys(database) {
	const db = getNodeSqliteKysely(database);
	const keys = /* @__PURE__ */ new Set();
	const collect = (values) => {
		for (const value of values) if (value) keys.add(value);
	};
	collect(executeSqliteQuerySync(database, db.selectFrom("session_windows").select([
		"session_key",
		"parent_session_key",
		"spawned_by"
	])).rows.flatMap((row) => [
		row.session_key,
		row.parent_session_key,
		row.spawned_by
	]));
	collect(executeSqliteQuerySync(database, db.selectFrom("session_nodes").select([
		"session_key",
		"parent_session_key",
		"spawned_by",
		"fork_source_session_key"
	])).rows.flatMap((row) => [
		row.session_key,
		row.parent_session_key,
		row.spawned_by,
		row.fork_source_session_key
	]));
	collect(executeSqliteQuerySync(database, db.selectFrom("conversation_deliveries").select("source_session_key")).rows.map((row) => row.source_session_key));
	for (const row of iterateSqliteQuerySync(database, db.selectFrom("session_nodes").select("entry_json").select(sessionEntrySnapshotColumns))) try {
		const entry = JSON.parse(row.entry_json);
		if (isRecord(entry)) collectSessionEntryKeyFields(attachSessionEntrySnapshots(entry, row), keys);
	} catch {}
	collect(executeSqliteQuerySync(database, db.selectFrom("board_tabs").select("session_key")).rows.map((row) => row.session_key));
	collect(executeSqliteQuerySync(database, db.selectFrom("board_widgets").select("session_key")).rows.map((row) => row.session_key));
	collect(executeSqliteQuerySync(database, db.selectFrom("heartbeat_outcomes").select(["session_key", "run_session_key"])).rows.flatMap((row) => [row.session_key, row.run_session_key]));
	return keys;
}
function updateSessionKeyColumns(database, rename) {
	const db = getNodeSqliteKysely(database);
	for (const [table, column] of [
		["session_windows", "session_key"],
		["session_windows", "parent_session_key"],
		["session_windows", "spawned_by"],
		["session_nodes", "session_key"],
		["session_entry_snapshots", "session_key"],
		["session_nodes", "parent_session_key"],
		["session_nodes", "spawned_by"],
		["session_nodes", "fork_source_session_key"],
		["conversation_deliveries", "source_session_key"],
		["session_members", "session_key"],
		["board_tabs", "session_key"],
		["board_widgets", "session_key"],
		["heartbeat_outcomes", "session_key"],
		["heartbeat_outcomes", "run_session_key"]
	]) executeSqliteQuerySync(database, db.updateTable(table).set(column, rename.to).where(column, "=", rename.from));
}
function rewriteSessionEntryKeyFields(value, renames) {
	visitSessionEntryKeyFields(value, (record, key) => {
		const current = record[key];
		if (typeof current === "string") record[key] = renames.get(current) ?? current;
	});
	return value;
}
function collectSessionEntryKeyFields(value, keys) {
	visitSessionEntryKeyFields(value, (record, key) => {
		const current = record[key];
		if (typeof current === "string") keys.add(current);
	});
}
function visitSessionEntryKeyFields(value, visit) {
	if (!isRecord(value)) return;
	const entry = value;
	for (const key of [
		"heartbeatIsolatedBaseSessionKey",
		"spawnedBy",
		"completionOwnerSessionKey",
		"parentSessionKey"
	]) visit(entry, key);
	if (isRecord(entry.forkSource)) visit(entry.forkSource, "sessionKey");
	if (Array.isArray(entry.compactionCheckpoints)) {
		for (const checkpoint of entry.compactionCheckpoints) if (isRecord(checkpoint)) visit(checkpoint, "sessionKey");
	}
	if (isRecord(entry.systemPromptReport)) visit(entry.systemPromptReport, "sessionKey");
}
//#endregion
//#region src/commands/doctor-session-title-repair.ts
function readLegacySessionTitle(scope) {
	try {
		const { totalMessages } = readSessionTranscriptMessageEventPage(scope, {
			maxMessages: 0,
			offset: 0
		});
		if (!totalMessages) return;
		const head = readSessionTranscriptBoundedMessageTailPage(scope, {
			maxMessages: 100,
			maxBytes: 65536,
			offset: Math.max(0, totalMessages - 100)
		});
		if (head.totalMessages !== totalMessages || head.events.length !== head.scannedMessages) return;
		for (const event of head.events) {
			const message = asOptionalRecord(sqliteMessageEventWithSeq(event));
			const projected = projectSessionDisplayMessage(message);
			if (projected?.role === "user" && !hasInterSessionUserProvenance(message)) {
				const displayName = deriveGoalSessionTitle(projected.text);
				return displayName ? {
					displayName,
					generation: head.snapshot.generation ?? null
				} : void 0;
			}
		}
		return;
	} catch (error) {
		if (isSessionTranscriptProjectionUnavailableError(error) || error instanceof SessionTranscriptColdError) return;
		throw error;
	}
}
/** Derive missing historical titles while Doctor owns session maintenance. */
async function repairLegacySessionTitles(params) {
	const authority = params.authority;
	const assertRepairAuthority = () => {
		if (!authority) throw new Error("Session title repair requires Doctor maintenance ownership.");
		authority.assertCurrent();
	};
	if (params.apply) assertRepairAuthority();
	const report = {
		found: 0,
		repaired: 0,
		scannedStores: 0,
		warnings: []
	};
	for (const target of params.targets ?? listExistingAgentDatabaseTargets(params.cfg, params.env)) {
		if (params.apply) assertRepairAuthority();
		report.scannedStores++;
		const scope = {
			agentId: target.agentId,
			storePath: target.storePath,
			env: params.env
		};
		const scan = runDoctorAgentDatabaseOperation({
			agentId: target.agentId,
			path: target.sqlitePath,
			run: () => {
				const keys = [];
				scanDoctorSessionEntriesTolerant(scope, ({ entry, sessionKey, recoveredFromProjections }) => {
					if (!recoveredFromProjections && !entry.incognito && !isIncognitoSessionKey(sessionKey) && entry.status !== "running" && !hasExplicitSessionName(entry)) keys.push(sessionKey);
				});
				return keys;
			}
		});
		if (!scan.ok) continue;
		for (const sessionKey of scan.value) {
			if (params.apply) assertRepairAuthority();
			try {
				const entry = loadSessionEntry({
					...scope,
					sessionKey
				});
				if (!entry || entry.incognito || entry.status === "running" || hasExplicitSessionName(entry)) continue;
				const session = {
					...scope,
					sessionKey,
					sessionId: entry.sessionId,
					sessionEntry: entry
				};
				if (sessionTitleRequests.get(session)) continue;
				const title = readLegacySessionTitle(session);
				if (!title) continue;
				report.found++;
				if (!params.apply) continue;
				await patchSessionEntryCore(session, (current) => current.sessionId === entry.sessionId && current.lifecycleRevision === entry.lifecycleRevision && current.status !== "running" && !current.incognito && !hasExplicitSessionName(current) ? { displayName: Buffer.from(title.displayName, "utf16le").toString("utf16le") } : null, {
					preserveActivity: true,
					skipMaintenance: true,
					assertCommitAllowed: assertRepairAuthority,
					shouldCommit: () => !sessionTitleRequests.get(session) && readSessionTranscriptWatermark(session).generation === title.generation,
					onCommitted: () => {
						report.repaired++;
					}
				});
				assertRepairAuthority();
			} catch (error) {
				if (params.apply) assertRepairAuthority();
				report.warnings.push(`Could not repair the title for ${sessionKey}: ${String(error)}`);
			}
		}
	}
	return report;
}
//#endregion
//#region src/config/sessions/worktree-workspace-migration.ts
function isInside(root, target) {
	const relative = path.relative(root, target);
	return relative === "" || !relative.startsWith("..") && !path.isAbsolute(relative);
}
async function resolveLegacyCanonicalWorkspace(params) {
	const worktree = params.entry.worktree;
	if (!worktree || worktree.canonicalWorkspaceDir) return;
	const recordedRepoRoot = path.resolve(worktree.repoRoot);
	if (params.entry.projectId) {
		const project = await resolveProjectRegistry(params.cfg, params.entry.projectId, { env: params.env });
		const projectRoot = project ? path.resolve(project.repoRoot) : void 0;
		return projectRoot && isInside(recordedRepoRoot, projectRoot) ? projectRoot : void 0;
	}
	const record = params.worktrees.find((candidate) => candidate.id === worktree.id);
	const spawnedCwd = params.entry.spawnedCwd;
	if (record?.ownerKind === "session" && record.ownerId === params.sessionKey && spawnedCwd && path.resolve(record.repoRoot) === path.resolve(worktree.repoRoot)) {
		const relative = path.relative(path.resolve(record.path), path.resolve(spawnedCwd));
		if (relative === "" || !relative.startsWith("..") && !path.isAbsolute(relative)) return path.resolve(recordedRepoRoot, relative);
	}
	const agentWorkspace = path.resolve(resolveAgentWorkspaceDir(params.cfg, params.agentId, params.env));
	return agentWorkspace && agentWorkspace === recordedRepoRoot ? agentWorkspace : void 0;
}
function listLegacyWorktreeSessionEntries(params) {
	const resolved = resolveSqliteScope({
		...params,
		sessionKey: ""
	});
	const result = withOpenClawAgentDatabaseReadOnly((database) => {
		const db = getSessionKysely(database.db);
		return executeSqliteQuerySync(database.db, db.selectFrom("session_nodes").selectAll().where(sql`session_nodes.entry_valid != 1 OR (
            json_valid(session_nodes.entry_json)
            AND json_type(session_nodes.entry_json, '$.worktree') = 'object'
            AND (
              json_type(session_nodes.entry_json, '$.worktree.canonicalWorkspaceDir') IS NULL
              OR json_extract(session_nodes.entry_json, '$.worktree.canonicalWorkspaceDir') = ''
            )
          )`).orderBy("session_key", "asc")).rows.flatMap((row) => {
			const entry = parseReadableSqliteSessionEntryRow(database, row);
			return entry ? [{
				databasePath: database.path,
				entry,
				sessionKey: row.session_key
			}] : [];
		});
	}, toDatabaseOptions(resolved));
	return result.found ? result.value : [];
}
async function migrateManagedWorktreeCanonicalWorkspaces(params) {
	const env = cloneEnvWithPlatformSemantics(params.env ?? process.env);
	env.OPENCLAW_STATE_DIR = resolveStateDir(env);
	const entries = listLegacyWorktreeSessionEntries({
		agentId: params.agentId,
		env,
		storePath: params.storePath
	});
	if (params.mode !== "doctor-fix") return {
		found: entries.length,
		repaired: 0
	};
	const worktrees = listRegistryWorktreesForMigration(env);
	let repaired = 0;
	for (const { databasePath, entry, sessionKey } of entries) {
		const agentId = resolveAgentIdFromSessionKey(sessionKey, params.agentId);
		const canonicalWorkspaceDir = await resolveLegacyCanonicalWorkspace({
			agentId,
			cfg: params.cfg,
			entry,
			env,
			sessionKey,
			worktrees
		});
		if (!canonicalWorkspaceDir) continue;
		if ((await patchSessionEntryCore({
			agentId,
			env,
			sessionKey,
			storePath: databasePath
		}, (current) => {
			if (!current.worktree || current.worktree.id !== entry.worktree?.id || current.worktree.repoRoot !== entry.worktree.repoRoot || current.projectId !== entry.projectId || current.spawnedCwd !== entry.spawnedCwd || current.worktree.canonicalWorkspaceDir) return null;
			return { worktree: {
				...current.worktree,
				canonicalWorkspaceDir
			} };
		}, {
			preserveActivity: true,
			skipMaintenance: true
		}))?.worktree?.canonicalWorkspaceDir === canonicalWorkspaceDir) repaired += 1;
	}
	return {
		found: entries.length,
		repaired
	};
}
//#endregion
//#region src/commands/doctor-session-worktree-workspace.ts
/** Run after key repairs, whose source claims include workspace metadata. */
async function repairLegacySessionWorktreeWorkspaces(params) {
	const targets = params.targets ?? listExistingAgentDatabaseTargets(params.cfg, params.env);
	let found = 0;
	let repaired = 0;
	for (const target of targets) {
		const wasOpen = isOpenClawAgentDatabaseOpen(target.sqlitePath);
		try {
			const operation = await runDoctorAgentDatabaseOperationAsync({
				agentId: target.agentId,
				path: target.sqlitePath,
				run: () => migrateManagedWorktreeCanonicalWorkspaces({
					agentId: target.agentId,
					cfg: params.cfg,
					env: params.env,
					storePath: target.sqlitePath,
					mode: params.apply ? "doctor-fix" : "detect"
				})
			});
			if (operation.ok) {
				found += operation.value.found;
				repaired += operation.value.repaired;
			}
		} finally {
			if (!wasOpen) await closeOpenClawAgentDatabaseByPathAsync(target.sqlitePath);
		}
	}
	return {
		found,
		repaired,
		scannedStores: targets.length
	};
}
//#endregion
//#region src/commands/doctor-session-transcripts.ts
/** Doctor repair for broken session transcript branches and legacy OpenAI Codex metadata. */
const SESSION_TRANSCRIPTS_CHECK_ID = "core/doctor/session-transcripts";
function parseTranscriptEntries(raw) {
	const entries = [];
	for (const line of raw.split(/\r?\n/)) {
		if (!line.trim()) continue;
		try {
			const parsed = JSON.parse(line);
			if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) entries.push(parsed);
		} catch {
			return [];
		}
	}
	return entries;
}
/** Classifies one legacy transcript without changing its contents. */
async function inspectSessionTranscriptFile(params) {
	const result = {
		filePath: params.filePath,
		broken: false,
		repaired: false,
		originalEntries: 0,
		activeEntries: 0,
		legacyOpenAICodexEntries: 0
	};
	try {
		if ((await fs.stat(params.filePath)).size > 1048576) {
			result.deferred = true;
			result.reason = "Detailed branch/provider classification deferred to offline staged import.";
			return result;
		}
		const entries = parseTranscriptEntries(await fs.readFile(params.filePath, "utf-8"));
		result.originalEntries = entries.length;
		result.legacyOpenAICodexEntries = normalizeLegacyOpenAICodexTranscriptMetadata(entries);
		const activePath = selectActivePath(entries);
		result.activeEntries = activePath?.entries.length ?? 0;
		result.broken = (activePath ? hasBrokenPromptRewriteBranch(entries, activePath.entries) : false) || result.legacyOpenAICodexEntries > 0;
		if (!activePath) result.reason = "no active branch";
	} catch (err) {
		result.reason = String(err);
	}
	return result;
}
async function listSessionTranscriptFiles(sessionDirs) {
	const files = [];
	for (const sessionsDir of sessionDirs) {
		const { entries } = await walkDirectory(sessionsDir, {
			maxDepth: 1,
			include: (entry) => entry.kind === "file" && entry.name.endsWith(".jsonl")
		});
		for (const entry of entries) files.push(path.join(sessionsDir, entry.name));
	}
	return files.toSorted((a, b) => a.localeCompare(b));
}
async function detectSessionTranscriptHealthIssues(params) {
	let sessionDirs = params?.sessionDirs;
	try {
		sessionDirs ??= await resolveAgentSessionDirs(resolveStateDir(process.env));
	} catch {
		return [];
	}
	const files = await listSessionTranscriptFiles(sessionDirs);
	const issues = [];
	for (const filePath of files) {
		const result = await inspectSessionTranscriptFile({ filePath });
		if (result.broken || result.deferred) issues.push(result);
	}
	return issues;
}
function sessionTranscriptIssueToHealthFinding(issue) {
	const metadata = issue.legacyOpenAICodexEntries > 0 ? ` ${issue.legacyOpenAICodexEntries} legacy OpenAI Codex metadata entr${issue.legacyOpenAICodexEntries === 1 ? "y" : "ies"}` : "";
	return {
		checkId: SESSION_TRANSCRIPTS_CHECK_ID,
		severity: "info",
		message: issue.deferred ? issue.reason : `Session transcript has legacy branch or provider metadata that can be cleaned up.${metadata}`,
		path: issue.filePath,
		fixHint: "Run `openclaw doctor --fix` to repair legacy transcripts during their staged import into SQLite."
	};
}
function sessionTranscriptIssueToRepairEffect(issue) {
	return {
		kind: "file",
		action: "would-rewrite-session-transcript",
		target: issue.filePath,
		dryRunSafe: false
	};
}
/** Reports or repairs session state through the canonical SQLite migration owner. */
async function noteSessionTranscriptHealth(options) {
	const params = {
		...options,
		env: options?.env ?? process.env,
		shouldRepair: options?.shouldRepair === true
	};
	const { runDoctorSessionSqlite } = await import("./doctor-session-sqlite-BE7od_3P.mjs");
	let reservedKeyReport = {
		found: 0,
		repaired: 0
	};
	let deliveryReport = {
		found: 0,
		repaired: 0,
		scannedStores: 0
	};
	let resolvedSkillsReport = {
		found: 0,
		repaired: 0,
		scannedStores: 0
	};
	let canonicalKeyReport = {
		archivedTranscriptDirectories: [],
		foundGroups: 0,
		repairBatches: 0,
		removedRows: 0,
		repairedGroups: 0,
		scannedStores: 0
	};
	let worktreeWorkspaceReport = {
		found: 0,
		repaired: 0,
		scannedStores: 0
	};
	let acpKeyReport = {
		found: 0,
		repaired: 0,
		scannedRows: 0,
		warnings: []
	};
	let titleReport = {
		found: 0,
		repaired: 0,
		scannedStores: 0,
		warnings: []
	};
	let legacyMainSessionResult;
	let postSessionPluginReceipt;
	const recordPostSessionRefusal = (refusal) => {
		if (!params.postSessionPluginMigration || postSessionPluginReceipt) return postSessionPluginReceipt;
		postSessionPluginReceipt = createLegacyStateMigrationStepReceipt({
			...params.postSessionPluginMigration.step,
			refusal
		}, {
			changes: [],
			warnings: [refusal.message]
		});
		params.onStepReceipt?.(postSessionPluginReceipt);
		return postSessionPluginReceipt;
	};
	const runSessionSqlite = async (maintenanceAuthority) => {
		const report = await runDoctorSessionSqlite({
			allAgents: true,
			...params.cfg ? { cfg: params.cfg } : {},
			env: params.env,
			mode: params.shouldRepair ? "import" : "dry-run"
		});
		const { migrateLegacyMainSessionKeys } = await import("./legacy-main-session-migration-KZH4GFmu.mjs");
		legacyMainSessionResult = await migrateLegacyMainSessionKeys({
			cfg: params.cfg ?? {},
			env: params.env,
			mode: params.shouldRepair ? "doctor-fix" : "detect"
		});
		const repairParams = {
			apply: params.shouldRepair,
			cfg: params.cfg ?? {},
			env: params.env
		};
		canonicalKeyReport = await repairCanonicalSessionKeys(repairParams);
		const rowRepairParams = {
			...repairParams,
			targets: listExistingAgentDatabaseTargets(repairParams.cfg, params.env)
		};
		resolvedSkillsReport = repairCanonicalSessionResolvedSkills(rowRepairParams);
		reservedKeyReport = await repairReservedIncognitoSessionKeys(rowRepairParams);
		deliveryReport = repairCanonicalSessionDeliveryStates(rowRepairParams);
		repairLegacySessionExecPolicy(rowRepairParams);
		acpKeyReport = await repairAcpSessionMetaKeysForDoctor({
			...repairParams,
			authority: maintenanceAuthority
		});
		titleReport = await repairLegacySessionTitles({
			...rowRepairParams,
			authority: maintenanceAuthority
		});
		worktreeWorkspaceReport = await repairLegacySessionWorktreeWorkspaces({
			...rowRepairParams,
			apply: params.shouldRepair && (!legacyMainSessionResult.armed || legacyMainSessionResult.complete)
		});
		if (params.postSessionPluginMigrationPlanBound && !params.postSessionPluginMigration) return report;
		if (params.postSessionPluginMigration?.step.requiredness === "not-required") {
			postSessionPluginReceipt = createLegacyStateMigrationStepReceipt(params.postSessionPluginMigration.step, {
				changes: [],
				warnings: []
			});
			params.onStepReceipt?.(postSessionPluginReceipt);
			return report;
		}
		if (!params.shouldRepair && params.postSessionPluginMigration) {
			recordPostSessionRefusal({
				code: "repair-not-authorized",
				message: "Post-session plugin repair was planned but Doctor repair was not authorized."
			});
			return report;
		}
		let pluginRepair;
		let receiptStep = params.postSessionPluginMigration?.step;
		try {
			pluginRepair = await runPostSessionPluginDoctorStateRepairs({
				config: params.cfg ?? {},
				env: params.env,
				maintenanceAuthority,
				...maintenanceAuthority ? { beforeCompletion: async (completedPluginIds, assertCurrent) => {
					const { settleRetainedDoctorSessionSources } = await import("./doctor-session-sqlite-BE7od_3P.mjs");
					await settleRetainedDoctorSessionSources(report, completedPluginIds, maintenanceAuthority, assertCurrent);
				} } : {},
				...params.postSessionPluginMigration ? {
					plannedActions: params.postSessionPluginMigration.plannedActions,
					inventory: params.postSessionPluginMigration.inventory
				} : {}
			});
		} catch (error) {
			const message = `Plugin session repair failed before the planned step completed: ${String(error)}`;
			pluginRepair = {
				changes: [],
				warnings: [message]
			};
			if (receiptStep) receiptStep = {
				...receiptStep,
				refusal: {
					code: "step-threw",
					message
				}
			};
		}
		if (receiptStep) {
			postSessionPluginReceipt = createLegacyStateMigrationStepReceipt(receiptStep, pluginRepair);
			params.onStepReceipt?.(postSessionPluginReceipt);
		}
		const pluginMessages = [...pluginRepair.changes, ...pluginRepair.warnings];
		if (pluginMessages.length > 0) note(pluginMessages.join("\n"), "Plugin session repair");
		return report;
	};
	let report;
	try {
		report = params.shouldRepair ? await withDoctorSqliteMaintenanceLock({
			env: params.env,
			operation: "session SQLite import",
			run: runSessionSqlite
		}) : await runSessionSqlite();
	} catch (error) {
		if (!(error instanceof DoctorSqliteMaintenanceLockUnavailableError)) {
			recordPostSessionRefusal({
				code: "blocked-by-session-repair-failure",
				message: `Post-session plugin repair was blocked because prerequisite session repair failed: ${String(error)}`
			});
			throw error;
		}
		const failure = formatErrorMessage(error);
		note(`- Skipped: ${failure} Then run "${formatCliCommand("openclaw doctor --fix", params.env)}" for session-store maintenance.`, "Session SQLite");
		recordPostSessionRefusal({
			code: "sqlite-maintenance-unavailable",
			message: failure
		});
		return postSessionPluginReceipt;
	}
	if (worktreeWorkspaceReport.found > 0) note(params.shouldRepair ? `- Repaired canonical workspace metadata for ${worktreeWorkspaceReport.repaired} of ${worktreeWorkspaceReport.found} managed-worktree session(s). Check project/worktree ownership for any remaining entries.` : `- Found ${worktreeWorkspaceReport.found} managed-worktree session(s) missing canonical workspace metadata. Run "openclaw doctor --fix" to repair them.`, "Session worktrees");
	if (acpKeyReport.found > 0 || acpKeyReport.warnings.length > 0) note([params.shouldRepair ? `- Repaired ${acpKeyReport.repaired} of ${acpKeyReport.found} legacy ACP metadata key(s).` : `- Found ${acpKeyReport.found} legacy ACP metadata key(s). Run "openclaw doctor --fix" to repair them.`, ...acpKeyReport.warnings].join("\n"), "ACP session keys");
	if (titleReport.found > 0 || titleReport.warnings.length > 0) note([params.shouldRepair ? `- Repaired ${titleReport.repaired} of ${titleReport.found} missing session title(s) without changing activity.` : `- Found ${titleReport.found} missing session title(s). Run "openclaw doctor --fix" to repair them.`, ...titleReport.warnings].join("\n"), "Session titles");
	if (reservedKeyReport.found > 0) note(params.shouldRepair ? `- Renamed ${reservedKeyReport.repaired} durable session key(s) that collided with the reserved incognito namespace.` : `- Found ${reservedKeyReport.found} durable session key(s) that collide with the reserved incognito namespace. Run "openclaw doctor --fix" to rename them.`, "Session SQLite");
	if (canonicalKeyReport.foundGroups > 0) note(params.shouldRepair ? `- Canonicalized ${canonicalKeyReport.repairedGroups} session-key group(s) in ${canonicalKeyReport.repairBatches} transaction batch(es), removed ${canonicalKeyReport.removedRows} duplicate or alias row(s), and preserved cross-store history in ${canonicalKeyReport.archivedTranscriptDirectories.length} archive director${canonicalKeyReport.archivedTranscriptDirectories.length === 1 ? "y" : "ies"}.` : `- Found ${canonicalKeyReport.foundGroups} non-canonical or duplicate session-key group(s). Run "openclaw doctor --fix" to preserve their history and canonicalize the rows.`, "Session SQLite");
	if (deliveryReport.found > 0) note(params.shouldRepair ? `- Canonicalized delivery state for ${deliveryReport.repaired} durable session row(s).` : `- Found ${deliveryReport.found} durable session row(s) with legacy delivery fields. Run "openclaw doctor --fix" to canonicalize them.`, "Session SQLite");
	if (resolvedSkillsReport.found > 0) note(params.shouldRepair ? `- Stripped the runtime-only skills catalog from ${resolvedSkillsReport.repaired} durable session row(s). Logical SQLite pages are freed; shrinking the on-disk database requires "openclaw doctor --session-sqlite compact --session-sqlite-all-agents".` : `- Found ${resolvedSkillsReport.found} durable session row(s) carrying a runtime-only skills catalog. Run "openclaw doctor --fix" to strip it.`, "Session SQLite");
	if (legacyMainSessionResult && (legacyMainSessionResult.changes.length > 0 || legacyMainSessionResult.warnings.length > 0)) note([...legacyMainSessionResult.changes.map((change) => `- ${change}`), ...legacyMainSessionResult.warnings.map((warning) => `- ${warning}`)].join("\n"), "Legacy main sessions");
	if (report.totals.legacyEntries === 0 && report.totals.unreferencedJsonlFiles === 0 && report.totals.issues === 0) return postSessionPluginReceipt;
	const informationalIndexes = report.targets.filter(isInformationalMissingSessionIndex);
	const actionableTargets = report.targets.filter((target) => !isInformationalMissingSessionIndex(target));
	const actionableIssues = actionableTargets.reduce((count, target) => count + target.issues.length, 0);
	const lines = [`- Legacy entries: ${report.totals.legacyEntries}; SQLite entries: ${report.totals.sqliteEntries}.`, `- Transcript events: imported=${report.totals.importedTranscriptEvents}; validated=${report.totals.validatedTranscriptEvents}.`];
	for (const target of informationalIndexes) lines.push(...target.issues.map((issue) => `- ${issue.message}`));
	if (report.totals.archivedTranscriptFiles > 0) lines.push(`- Archived ${report.totals.archivedTranscriptFiles} legacy transcript artifact(s).`);
	if (report.totals.archivedUnreferencedJsonlFiles > 0) lines.push(`- Archived ${report.totals.archivedUnreferencedJsonlFiles} unreferenced JSONL artifact(s).`);
	if (actionableIssues > 0) {
		const warnings = formatSessionSqliteMigrationWarnings(actionableTargets, params.env);
		params.onWarnings?.(warnings);
		lines.push(...warnings.map((warning) => `- ${warning}`));
		lines.push(`- Found ${actionableIssues} session SQLite issue(s). Inspect with "${formatCliCommand("openclaw doctor --session-sqlite dry-run --session-sqlite-all-agents", params.env)}".`);
	}
	if (!params.shouldRepair && actionableTargets.length > 0) lines.push("- Run \"openclaw doctor --fix\" to migrate legacy session metadata/transcripts to SQLite.");
	if (params.shouldRepair && report.migrationRun && report.totals.archivedTranscriptFiles > 0) lines.push(`- After verifying the upgrade, preview rollback retirement with "${formatCliCommand("openclaw update cleanup --dry-run", params.env)}" for state ${resolveStateDir(params.env)}. Keep the same OPENCLAW_STATE_DIR and OPENCLAW_CONFIG_PATH overrides.`);
	note(lines.join("\n"), "Session SQLite");
	return postSessionPluginReceipt;
}
//#endregion
export { detectSessionTranscriptHealthIssues, noteSessionTranscriptHealth, sessionTranscriptIssueToHealthFinding, sessionTranscriptIssueToRepairEffect };