UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

1,200 lines 59.2 kB
import { m as shortenHomePath } from "./utils-P__uGsPB.js";
import { n as normalizeAgentId } from "./agent-id-CeT3w4ap.js";
import { t as formatCliCommand } from "./command-format-C7YfyMTd.js";
import { c as resolveAgentConfig } from "./agent-scope-config-DcbEhP0R.js";
import { w as resolveStateDir } from "./paths-D2sRr1a_.js";
import { O as parseAgentSessionKey } from "./session-key-BnWWjqNc.js";
import { t as isIncognitoSessionKey } from "./incognito-session-key-BwpD1Lwd.js";
import { n as replaceFileAtomic } from "./replace-file-BAJ-TWzD.js";
import { a as getNodeSqliteKysely, i as executeSqliteQueryTakeFirstSync, o as iterateSqliteQuerySync, r as executeSqliteQuerySync } from "./kysely-sync-COmh4HWh.js";
import { r as withExistingOpenClawStateDatabaseReadOnly } from "./openclaw-state-db-readonly-BRgmrGHt.js";
import { i as openOpenClawStateDatabase, s as runOpenClawStateWriteTransaction } from "./openclaw-state-db-BRTnL-D8.js";
import { o as resolveSessionStorePathCore } from "./paths-CXdaYWF_.js";
import { _ as resolveSessionAgentId } from "./agent-scope-DbtJyKUL.js";
import { b as closeOpenClawAgentDatabaseByPath, g as runOpenClawAgentWriteTransaction, p as openOpenClawAgentDatabase, u as isOpenClawAgentDatabaseOpen } from "./openclaw-agent-db-CWtDoRbC.js";
import { Wt as applySessionEntryLifecycleMutation, dt as listCanonicalSessionRepairFacts, ft as loadCanonicalSessionRepairEntries, mt as scanDoctorSessionEntriesTolerant, ut as rewriteDoctorSessionEntries } from "./session-accessor-YsytfDtG.js";
import { b as loadTranscriptEvents } from "./session-accessor.sqlite-transcript-store-B0zw3fAU.js";
import { n as withOpenClawAgentDatabaseReadOnly } from "./openclaw-agent-db-readonly-CHjf8FxN.js";
import { o as listSessionEntryKeysReadOnly } from "./session-accessor.sqlite-entry-CWk3jL7s.js";
import { C as copySessionNodeArtifactsForRepair, S as collectSessionStateIdsForEntry, T as deleteSessionMembersForRepair } from "./session-accessor.sqlite-entry-store-BxYl0nro.js";
import { l as stripRuntimeOnlySessionSkillsFields } from "./store-writer-state-C4OG_EQ4.js";
import { o as resolveDeliveryProvenCanonicalSessionKey } from "./store-entry-CzRELcpv.js";
import { u as resolveSqliteTranscriptArchiveDirectory } from "./session-accessor.sqlite-scope-2KfMzb44.js";
import { _ as setCanonicalSqliteSessionMainKey, v as publishSessionEntryCacheInvalidation } from "./session-accessor.sqlite-transcript-state-DwF2owZS.js";
import { i as preserveCreationStamp } from "./session-entry-provenance-jzrCUpdQ.js";
import { w as writeTranscriptArchive } from "./session-accessor.sqlite-lifecycle-state-BTh4yZ7R.js";
import { r as resolveAgentMainSessionKey } from "./main-session-Br0F9dzh.js";
import { a as resolveStoredSessionKeyForAgentStore, n as resolveSessionStoreAgentId } from "./session-store-key-8xEjWSNi.js";
import { a as rehomeSqliteSessionDeliveryReferencesForCanonicalRepair, n as ensureSqliteTranscriptGenerationsForCanonicalRepair, o as rehomeSqliteSessionDeliveryReferencesForCanonicalRepairBatch, r as listSqliteSessionGenerationIdsForCanonicalRepair, t as copySqliteSessionOwnedStateForCanonicalRepair } from "./session-accessor.sqlite-canonical-repair-ChKhq4_W.js";
import { t as resolveAgentSessionDirs } from "./session-dirs-SATsXf4g.js";
import { o as resolveAllAgentSessionStoreTargetsSync } from "./targets-Cknmo7YZ.js";
import { n as replaceSessionOwnerInTransaction } from "./session-accessor.sqlite-owner-DysLwyNh.js";
import { _ as normalizeExecSecurity, b as resolveExecModeFromPolicy, m as normalizeExecAsk, v as normalizeExecTarget } from "./exec-approvals-policy-CCFUzTzd.js";
import { t as SESSION_PERMISSION_BY_EXEC_MODE } from "./session-permission-exec-mode-CJHB47SR.js";
import { t as applyExecPolicyLayer } from "./exec-policy-D6Y68q04.js";
import { h as resolveExecTarget } from "./bash-tools.exec-runtime-CbUMPDGW.js";
import { t as note } from "./note-DqHk3fA1.js";
import { n as normalizeLegacySessionEntryDelivery } from "./state-migrations.legacy-session-store-CQJX6yZy.js";
import { i as selectActiveTranscriptEntries, n as normalizeLegacyOpenAICodexTranscriptMetadata, r as selectActivePath, t as hasBrokenPromptRewriteBranch } from "./legacy-transcript-repair-nrJd98DH.js";
import { i as withDoctorSqliteMaintenanceLock, t as DoctorSqliteMaintenanceLockUnavailableError } from "./doctor-sqlite-maintenance-lock-BDyGk9pJ.js";
import { d as resolveTargetSqlitePath, i as projectExistingAgentDatabaseTargets, r as listExistingAgentDatabaseTargets, u as resolveTargetSqliteOptions } from "./doctor-session-sqlite-readers-DxvNNHSA.js";
import { i as runPostSessionPluginDoctorStateRepairs } from "./state-migrations.plugin-doctor-D3WjbYIE.js";
import { t as runDoctorAgentDatabaseOperation } from "./doctor-agent-database-operation-DzkjHFCH.js";
import path from "node:path";
import fs from "node:fs/promises";
import { sql } from "kysely";
//#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 resolveCanonicalKey = (item, seen = /* @__PURE__ */ new Set()) => {
		if (!item.canonicalOwnerSessionKey) return item.canonicalKey;
		const identity = `${item.target.sqlitePath}\0${item.sessionKey}`;
		const owner = bySessionKey.get(`${item.target.sqlitePath}\0${item.canonicalOwnerSessionKey}`);
		if (!owner || seen.has(identity)) return item.canonicalKey;
		seen.add(identity);
		return owner.canonicalOwnerSessionKey ? resolveCanonicalKey(owner, seen) : owner.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);
}
function collectCanonicalSessionCandidateFacts(params, stores) {
	const inventory = stores.flatMap((target) => listCanonicalSessionRepairFacts({
		agentId: target.agentId,
		storePath: target.storePath
	}).map((inventoryFact) => {
		const { canonicalOwnerSessionKey, sessionKey } = inventoryFact;
		const storedKey = resolveStoredSessionKeyForAgentStore({
			cfg: params.cfg,
			agentId: 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 = resolveStoredSessionKeyForAgentStore({
				cfg: params.cfg,
				agentId: canonicalAgentId,
				sessionKey: value
			});
			const ownerAgentId = parseAgentSessionKey(storedKey)?.agentId ?? canonicalAgentId;
			for (const key of [value, storedKey]) {
				const sameStore = canonicalKeysByStoredKey.get(`${target.sqlitePath}\0${ownerAgentId}\0${key}`);
				if (sameStore?.size === 1) return [...sameStore][0];
			}
			for (const key of [value, storedKey]) {
				const crossStore = canonicalKeysByStoredKey.get(`*\0${ownerAgentId}\0${key}`);
				if (crossStore?.size === 1) return [...crossStore][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 });
}
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;
	}
	return {
		agentId: fact.agentId,
		canonicalKey: fact.canonicalKey,
		entry,
		expectedEntry: loaded.entry,
		ownerEvidenceOnly: fact.ownerEvidenceOnly,
		...loaded.rawEntryJson !== void 0 ? { rawEntryJson: loaded.rawEntryJson } : {},
		sessionKey: fact.sessionKey,
		sqlitePath: fact.sqlitePath,
		storePath: fact.storePath
	};
}
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({
		agentId: destination.agentId,
		path: destination.sqlitePath
	}), 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({
		agentId: store.agentId,
		path: store.sqlitePath
	}), 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 = 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
function repairReservedIncognitoSessionKeys(params) {
	const 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: 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) updateSessionKeyColumns(database.db, rename);
	publishSessionEntryCacheInvalidation(database);
}
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"))) try {
		collectSessionEntryKeyFields(JSON.parse(row.entry_json), 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);
	const update = (query) => executeSqliteQuerySync(database, query);
	update(db.updateTable("session_windows").set({ session_key: rename.to }).where("session_key", "=", rename.from));
	update(db.updateTable("session_windows").set({ parent_session_key: rename.to }).where("parent_session_key", "=", rename.from));
	update(db.updateTable("session_windows").set({ spawned_by: rename.to }).where("spawned_by", "=", rename.from));
	update(db.updateTable("session_nodes").set({ session_key: rename.to }).where("session_key", "=", rename.from));
	update(db.updateTable("session_nodes").set({ parent_session_key: rename.to }).where("parent_session_key", "=", rename.from));
	update(db.updateTable("session_nodes").set({ spawned_by: rename.to }).where("spawned_by", "=", rename.from));
	update(db.updateTable("session_nodes").set({ fork_source_session_key: rename.to }).where("fork_source_session_key", "=", rename.from));
	update(db.updateTable("conversation_deliveries").set({ source_session_key: rename.to }).where("source_session_key", "=", rename.from));
	update(db.updateTable("session_members").set({ session_key: rename.to }).where("session_key", "=", rename.from));
	update(db.updateTable("board_tabs").set({ session_key: rename.to }).where("session_key", "=", rename.from));
	update(db.updateTable("board_widgets").set({ session_key: rename.to }).where("session_key", "=", rename.from));
	update(db.updateTable("heartbeat_outcomes").set({ session_key: rename.to }).where("session_key", "=", rename.from));
	update(db.updateTable("heartbeat_outcomes").set({ run_session_key: rename.to }).where("run_session_key", "=", 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 (!value || typeof value !== "object" || Array.isArray(value)) return;
	const entry = value;
	for (const key of [
		"heartbeatIsolatedBaseSessionKey",
		"spawnedBy",
		"completionOwnerSessionKey",
		"parentSessionKey"
	]) visit(entry, key);
	if (entry.forkSource && typeof entry.forkSource === "object" && !Array.isArray(entry.forkSource)) {
		const forkSource = entry.forkSource;
		visit(forkSource, "sessionKey");
	}
	if (Array.isArray(entry.compactionCheckpoints)) for (const checkpoint of entry.compactionCheckpoints) {
		if (!checkpoint || typeof checkpoint !== "object" || Array.isArray(checkpoint)) continue;
		visit(checkpoint, "sessionKey");
	}
	if (entry.systemPromptReport && typeof entry.systemPromptReport === "object" && !Array.isArray(entry.systemPromptReport)) {
		const report = entry.systemPromptReport;
		visit(report, "sessionKey");
	}
}
//#endregion
//#region src/commands/doctor-session-transcripts.ts
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;
}
/** Repairs one transcript file by keeping the active branch and backing up the original file. */
async function repairBrokenSessionTranscriptFile(params) {
	const result = {
		filePath: params.filePath,
		broken: false,
		repaired: false,
		originalEntries: 0,
		activeEntries: 0,
		legacyOpenAICodexEntries: 0
	};
	try {
		if (!params.shouldRepair && (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;
		const brokenBranch = activePath ? hasBrokenPromptRewriteBranch(entries, activePath.entries) : false;
		result.broken = brokenBranch || result.legacyOpenAICodexEntries > 0;
		if (!activePath) result.reason = "no active branch";
		if (!result.broken || !params.shouldRepair) return result;
		const content = `${(brokenBranch && activePath ? selectActiveTranscriptEntries({
			entries,
			activePath
		}) : entries).map((entry) => JSON.stringify(entry)).join("\n")}\n`;
		const repairKind = brokenBranch ? "branch" : "openai-codex";
		const backupPath = `${params.filePath}.pre-doctor-${repairKind}-repair-${(/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-")}.bak`;
		await fs.copyFile(params.filePath, backupPath);
		result.backupPath = backupPath;
		const dirMode = (await fs.stat(path.dirname(params.filePath))).mode & 4095;
		await replaceFileAtomic({
			filePath: params.filePath,
			content,
			dirMode,
			preserveExistingMode: true,
			copyFallbackOnPermissionError: false,
			syncTempFile: true,
			syncParentDir: true
		});
		result.repaired = true;
	} catch (err) {
		result.reason = String(err);
	}
	return result;
}
async function listSessionTranscriptFiles(sessionDirs) {
	const files = [];
	for (const sessionsDir of sessionDirs) {
		let entries;
		try {
			entries = await fs.readdir(sessionsDir, { withFileTypes: true });
		} catch {
			continue;
		}
		for (const entry of entries) if (entry.isFile() && entry.name.endsWith(".jsonl")) 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 repairBrokenSessionTranscriptFile({
			filePath,
			shouldRepair: false
		});
		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: "To clean up the advisory artifact, run `openclaw doctor --fix` to rewrite affected transcripts to their active branch."
	};
}
function sessionTranscriptIssueToRepairEffect(issue) {
	return {
		kind: "file",
		action: "would-rewrite-session-transcript",
		target: issue.filePath,
		dryRunSafe: false
	};
}
/** Scans session transcript files and reports or repairs legacy/broken transcript state. */
async function noteSessionTranscriptHealth(params) {
	if (params?.sessionDirs === void 0 || params.sessionSqlite === true) {
		await noteSessionSqliteMigrationHealth({
			cfg: params?.cfg,
			env: params?.env ?? process.env,
			shouldRepair: params?.shouldRepair === true
		});
		return;
	}
	const shouldRepair = params?.shouldRepair === true;
	let sessionDirs = params?.sessionDirs;
	try {
		sessionDirs ??= await resolveAgentSessionDirs(resolveStateDir(process.env));
	} catch (err) {
		note(`- Failed to inspect session transcripts: ${String(err)}`, "Session transcripts");
		return;
	}
	const results = [];
	const files = await listSessionTranscriptFiles(sessionDirs);
	if (files.length > 0 && shouldRepair) for (const filePath of files) results.push(await repairBrokenSessionTranscriptFile({
		filePath,
		shouldRepair
	}));
	else if (files.length > 0) results.push(...await detectSessionTranscriptHealthIssues({ sessionDirs }));
	const broken = results.filter((result) => result.broken);
	if (broken.length > 0) {
		const repairedCount = broken.filter((result) => result.repaired).length;
		const lines = [`- Found ${broken.length} transcript file${broken.length === 1 ? "" : "s"} with legacy state.`, ...broken.slice(0, 20).map((result) => {
			const backup = result.backupPath ? ` backup=${shortenHomePath(result.backupPath)}` : "";
			const status = result.repaired ? "repaired" : shouldRepair ? "repair failed" : "needs repair";
			const error = shouldRepair && !result.repaired && result.reason ? ` error=${result.reason}` : "";
			const metadata = result.legacyOpenAICodexEntries > 0 ? ` openai-codex=${result.legacyOpenAICodexEntries}` : "";
			return `- ${shortenHomePath(result.filePath)} ${status} entries=${result.originalEntries}->${result.activeEntries + 1}${metadata}${backup}${error}`;
		})];
		if (broken.length > 20) lines.push(`- ...and ${broken.length - 20} more.`);
		if (!shouldRepair) lines.push("- Run \"openclaw doctor --fix\" to rewrite affected files to their active branch.");
		else if (repairedCount > 0) lines.push(`- Repaired ${repairedCount} transcript file${repairedCount === 1 ? "" : "s"}.`);
		note(lines.join("\n"), "Session transcripts");
	}
}
async function noteSessionSqliteMigrationHealth(params) {
	const { runDoctorSessionSqlite } = await import("./doctor-session-sqlite-DM2cPm22.js");
	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 legacyMainSessionResult;
	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-L2j4sPD2.js");
		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);
		resolvedSkillsReport = repairCanonicalSessionResolvedSkills(repairParams);
		reservedKeyReport = repairReservedIncognitoSessionKeys(repairParams);
		deliveryReport = repairCanonicalSessionDeliveryStates(repairParams);
		repairLegacySessionExecPolicy(repairParams);
		const pluginRepair = await runPostSessionPluginDoctorStateRepairs({
			config: params.cfg ?? {},
			env: params.env,
			maintenanceAuthority
		});
		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)) throw error;
		note(`- Skipped: Gateway or another SQLite maintenance command owns the state directory. Stop the Gateway, then run "${formatCliCommand("openclaw doctor --fix", params.env)}" for session-store maintenance.`, "Session SQLite");
		return;
	}
	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;
	const lines = [`- Legacy entries: ${report.totals.legacyEntries}; SQLite entries: ${report.totals.sqliteEntries}.`, `- Transcript events: imported=${report.totals.importedTranscriptEvents}; validated=${report.totals.validatedTranscriptEvents}.`];
	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 (report.totals.issues > 0) lines.push(`- Found ${report.totals.issues} session SQLite issue(s). Inspect with "${formatCliCommand("openclaw doctor --session-sqlite dry-run --session-sqlite-all-agents", params.env)}".`);
	if (!params.shouldRepair) 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");
}
//#endregion
export { detectSessionTranscriptHealthIssues, noteSessionTranscriptHealth, sessionTranscriptIssueToHealthFinding, sessionTranscriptIssueToRepairEffect };