UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

9,254 lines 396 kB
import { f as asSafeIntegerInRange } from "./number-coercion-CLj0HTDM.js";
import "./src-vebZIeLe.js";
import { c as isRecord } from "./record-coerce-DItp3I4t.js";
import { n as safeParseJsonRecord } from "./json-coercion-AulM0PZ6.js";
import { t as stableStringify } from "./stable-stringify-C8X7niaI.js";
import { l as normalizeOptionalString, m as readNonBlankString, o as normalizeLowercaseStringOrEmpty } from "./string-coerce-CIXf7egm.js";
import { c as resolveUserPath, o as resolveRequiredHomeDir, t as expandHomePrefix } from "./home-dir-BPhrG-aM.js";
import { At as boolean, Et as array, Lt as custom, Rn as string, Zn as unknown, dn as literal, fn as looseObject, wn as number } from "./schemas-zxit8y5H.js";
import { n as isErrno } from "./errno-CkbDOfLk.js";
import { a as isWithinDir } from "./path-safety-Bi0ppMWC.js";
import { I as sameFileIdentity, M as assertNoSymlinkParents, w as root$1 } from "./fs-safe-B6pvPGnf.js";
import { n as normalizeAgentId, t as isValidAgentId } from "./agent-id-CeT3w4ap.js";
import { i as listAgentEntries, o as listAgentIds, w as tryResolveAmbientOwnerAgentId } from "./agent-scope-config-DcbEhP0R.js";
import { C as resolveOAuthDir, b as resolveLegacyStateDirs, w as resolveStateDir } from "./paths-D2sRr1a_.js";
import { E as normalizeSessionKeyPreservingOpaquePeerIds, O as parseAgentSessionKey, a as buildAgentMainSessionKey, n as DEFAULT_MAIN_KEY, r as LEGACY_IMPLICIT_AGENT_ID, u as normalizeMainKey } from "./session-key-BnWWjqNc.js";
import { t as DEFAULT_ACCOUNT_ID } from "./account-id-CETVCrTz.js";
import { n as resolveSessionStoreCompatibilityAgentId } from "./legacy.default-agent-owner-BGwEdQRe.js";
import { t as formatErrorMessage } from "./errors-Db3Ymjlb.js";
import { r as replaceFileAtomicSync } from "./replace-file-BAJ-TWzD.js";
import { h as clearNodeSqliteKyselyCacheForDatabase, l as runSqliteDeferredTransactionSync, t as openNodeSqliteDatabase, u as runSqliteImmediateTransactionSync } from "./node-sqlite-BpQX3W0e.js";
import { a as getNodeSqliteKysely, i as executeSqliteQueryTakeFirstSync, r as executeSqliteQuerySync } from "./kysely-sync-COmh4HWh.js";
import { L as quoteSqliteIdentifier } from "./openclaw-state-db-cache-C7ljO0xP.js";
import { o as OPENCLAW_SQLITE_BUSY_TIMEOUT_MS } from "./openclaw-state-db-contract-DYCYxE4w.js";
import { n as readFileWindowFullySync } from "./file-read-DtMn74uz.js";
import { s as resolveOpenClawStateSqlitePath } from "./openclaw-state-db-schema-version-c1ZL6JGz.js";
import { _t as coerceRequiredSqliteNumber, a as repairOpenClawStateDatabaseSchema, dt as resolveSqliteDatabaseFilePaths, ft as detectOpenClawStateDatabaseSchemaMigrations, i as openOpenClawStateDatabase, o as repairOpenClawStateDatabaseSchemaIfNeeded, r as openExistingOpenClawStateDatabaseReadOnly, s as runOpenClawStateWriteTransaction } from "./openclaw-state-db-BRTnL-D8.js";
import { a as sha256Hex, i as sha256File } from "./crypto-digest-C4hqTb_e.js";
import { n as importConfigMachineState, r as readConfigMachineState } from "./config-machine-state-BCereLZr.js";
import { t as CHANNEL_IDS } from "./ids-BVZRYG0I.js";
import { f as resolveLegacyInstalledPluginIndexStorePath } from "./installed-plugin-index-record-reader-SXWwf_BU.js";
import "./installed-plugin-index-store-MVV6aa7C.js";
import { o as listPluginDoctorSessionStoreAgentIds, r as collectRelevantDoctorPluginIds } from "./doctor-contract-registry-DOa9Ye7j.js";
import { h as encodeSessionArchiveContent, m as decodeSessionArchiveBytes } from "./artifacts-DMDO9PHy.js";
import { n as resolveAgentsDirFromSessionStorePath } from "./paths-CXdaYWF_.js";
import { t as isPerAgentSessionStoreConfig } from "./session-store-config-C5gZ4u4F.js";
import { t as resolvePersistedSessionStoreOwner } from "./session-store-owner-CR2Ag3xK.js";
import { i as resolveWorkspaceStateIdentity } from "./workspace-state-identity-UVKTnO1Z.js";
import { t as createSqliteAuditRecordStore } from "./sqlite-audit-record-store-BaAEhG05.js";
import { n as CONFIG_AUDIT_SCOPE, t as CONFIG_AUDIT_MAX_ENTRIES } from "./io.audit-Dor_5i6r.js";
import { i as isPidDefinitelyDead, t as getFileLockProcessStartTime } from "./pid-alive-XuW58Ofz.js";
import { t as isLockOwnerDefinitelyStale } from "./stale-lock-file-B5kcl7Ks.js";
import { s as withFileLock } from "./file-lock-B0wiaenm.js";
import "./file-lock-DPooFrLa.js";
import { n as asFsSafeFileLockRoot, r as createFileLockManager } from "./file-lock-manager-Cqz0dyf7.js";
import { n as acquireGatewayLock } from "./gateway-lock-B0QIxQaj.js";
import { t as pathMayExistSync } from "./path-existence-CzRtGGnO.js";
import { f as mcpOAuthStoreKeyFromLegacyFileName, o as parseMcpOAuthStoreJson } from "./mcp-oauth-store-rMpCI-eU.js";
import { c as resolveSharedMainAuthAgentDir, o as resolveSharedAuthStoreOwnership, r as noteCommittedSharedAuthStoreOwnership, t as SHARED_AUTH_STORE_STATE_KEY } from "./path-resolve-oRkRBkQd.js";
import { a as assertNoOpenClawAgentDatabaseLeases, n as OpenClawAgentDatabaseLeaseActiveError, r as assertAgentDatabaseMaintenanceAuthority } from "./openclaw-agent-db-lease-Djvd6LWN.js";
import { E as readExistingAgentSchemaMeta, r as migrateOpenClawAgentDatabaseForMaintenance, t as assertOpenClawAgentDatabaseForMaintenance } from "./openclaw-agent-db-maintenance-wTIy-jt-.js";
import "./openclaw-agent-db-contract-CGTyjij4.js";
import { _ as withAgentDatabaseMaintenanceLease, b as closeOpenClawAgentDatabaseByPath, g as runOpenClawAgentWriteTransaction } from "./openclaw-agent-db-CWtDoRbC.js";
import { C as inspectSharedAuthLegacySourceFile, S as inspectSharedAuthLegacyRowsReadOnly, b as SharedAuthStoreSourceInspectionError, m as resolveAuthProfileDatabaseOwnerId, t as closeAuthProfileReadPool, w as readSharedAuthLegacyRowsFromDatabase, x as hasPendingSharedAuthCleanup } from "./sqlite-MN_7y26V.js";
import { s as getMediaDir } from "./store-Ph57PZ9P.js";
import { n as replaceOutsideCodeRegions } from "./directive-tags-Wp1S72ap.js";
import { in as applyAssistantDeliveryDirectives } from "./session-accessor-YsytfDtG.js";
import { d as updateSqliteTranscriptEventJsonInTransaction } from "./session-accessor.sqlite-transcript-store-B0zw3fAU.js";
import { t as getChannelPlugin } from "./registry-Cz6cv4VC.js";
import { s as normalizePersistedSessionEntryShape } from "./store-writer-state-C4OG_EQ4.js";
import { u as resolveSqliteTranscriptArchiveDirectory } from "./session-accessor.sqlite-scope-2KfMzb44.js";
import { n as canonicalizeMainSessionAlias } from "./main-session-Br0F9dzh.js";
import { c as resolveConfiguredAgentDatabaseTargets, n as listConfiguredSessionStoreAgentIds, o as resolveAllAgentSessionStoreTargetsSync, u as resolveSessionStoreTargets } from "./targets-Cknmo7YZ.js";
import { S as resolveLegacyWorkspaceSourcePaths, a as isSafeWorkspaceAttestationFilename, d as LEGACY_WORKSPACE_ATTESTATION_DIRNAME, g as WORKSPACE_DOCTOR_CLAIM_SUFFIX, l as registerWorkspaceStateAliasesInTransaction, n as WORKSPACE_LEGACY_STATE_MIGRATION_KIND, p as LEGACY_WORKSPACE_ATTESTATION_MAX_BYTES, y as legacyWorkspaceSiblingAttestationMayExist } from "./workspace-state-store-DdpcP8H6.js";
import { n as normalizeConversationRef } from "./session-binding-normalization-DA3Q-Bzn.js";
import { l as writeAcpSessionMetaForMigration } from "./session-meta-dzG4VkYo.js";
import { n as parseRestartSentinelEnvelope, r as readRestartSentinelRowSync, s as writeRestartSentinelRowSync } from "./restart-sentinel-store-DfF-fRR-.js";
import { a as sessionFromRow, c as safeTranscriptPathSegment, d as ensureMeetingTranscriptsSchema, l as transcriptSessionExportKey, o as TRANSCRIPT_EXPORT_FILE_NAMES, r as renderTranscriptsMarkdown, t as TranscriptsStore, u as transcriptSessionSelector } from "./store-CU1KZD9W.js";
import { i as resolveChannelDefaultAccountId } from "./helpers-VcK_WJ24.js";
import { a as updateChannelPairingStateSnapshot, c as resolveAllowFromAccountId, l as safeAccountKey, s as dedupePreserveOrder, u as getPairingAdapter } from "./pairing-store-sqlite-CNgVQSBb.js";
import { n as SYSTEM_AGENT_AUDIT_SCOPE, t as SYSTEM_AGENT_AUDIT_MAX_ENTRIES } from "./audit-D3_LINzS.js";
import { r as saveLegacySessionStore } from "./state-migrations.legacy-session-store-CQJX6yZy.js";
import { a as readSessionStoreJson5, i as parseSessionStoreJson5, n as existsDir, o as safeReadDir, r as migrationFileExists, t as ensureMigrationDir } from "./state-migrations.fs-DyoUSxwp.js";
import { S as rewriteRegistryWorktreePathsForMigration, b as listRegistryWorktreesForMigration, h as hasLegacyRegistryWorktrees, s as discardLegacyRegistryWorktrees } from "./registry-Dvi_fTvk.js";
import { t as migrateLegacyMainSessionKeys } from "./legacy-main-session-migration-BUkRsA4g.js";
import { c as managedImageRecordToRow, l as managedImageRecordsEqual, s as managedImageRecordFromRow, t as MANAGED_OUTGOING_ORIGINALS_SUBDIR } from "./managed-image-record-store-DnURBN77.js";
import { n as listWorkspaceStateDirs } from "./workspace-state-dirs-DBJICig7.js";
import { n as LEGACY_NODE_HOST_CONFIG_FILE, r as NODE_HOST_CONFIG_KEY, t as LEGACY_NODE_HOST_CONFIG_CLAIM_SUFFIX, u as normalizeNodeHostCloudflareAccessConfig } from "./config-o0eKhywR.js";
import { t as withLegacyMigrationStateLock } from "./state-migrations.lock-C0ywqz7v.js";
import { n as migrateLegacyDeviceAuth, t as detectLegacyDeviceAuth } from "./state-migrations.device-auth-C0Zr0Em6.js";
import { n as detectLegacyDeviceIdentity, t as migrateLegacyDeviceIdentity } from "./state-migrations.device-identity-BJLLfNb6.js";
import { a as recordLegacyMigrationRun, i as recordLegacyMigrationReceipt, n as readLegacyMigrationReceipt, o as recordLegacyMigrationSource, r as readLegacyMigrationReceiptFromDatabase, s as resolveLegacyMigrationSourceKey, t as markLegacyMigrationSourceRemoved } from "./state-migrations.receipts-BIGn8ljb.js";
import { a as claimAndRemoveLegacyMigrationSource, c as legacyMigrationSourceSnapshotsMatch, d as resolveLegacyMigrationRelativePath, f as restoreLegacyMigrationSourceClaims, i as assertLegacyMigrationSourceUnchanged, l as readLegacyMigrationSourceSnapshot, n as migrateLegacyExecApprovals, o as claimLegacyMigrationSourceClaims, r as LegacyMigrationSourceClaim, s as legacyMigrationSourceOrClaimMayExist, t as detectLegacyExecApprovals, u as readLegacyMigrationSourceSnapshotSync } from "./state-migrations.exec-approvals-IZ2-HGwX.js";
import { c as recordsAfterLegacyAuditRawCheckpoint, d as prepareLegacyAuditRecords, f as serializePreparedAuditRecords, g as legacyAuditSourceGenerationKey, l as restoreInterruptedAuditRecoveryArchive, m as hasLegacyAuditRawCheckpointCapacity, n as finalizeLegacyAuditRecoveryArchive, o as readLegacyAuditSourceSnapshot, p as detectLegacyAuditLogs, r as findPreviousLegacyAuditRawCheckpoint, s as recordLegacyAuditRawCheckpoint, t as withLegacyAuditMigrationLease, u as scrubLegacyAuditRecoveryArchive } from "./state-migrations.audit-coordination-CJaFTJ5E.js";
import { r as resolveAgentDatabaseMigrationTargets } from "./state-migrations.media-persistence-targets-D45MAuwp.js";
import { n as logStateMigrationResult, r as mergeNotices, t as formatStartupMigrationFailure } from "./state-migrations.messages-oWDQJF4s.js";
import { n as collectPluginDoctorStateMigrationPlans, r as runPluginDoctorStateMigrationPlans } from "./state-migrations.plugin-doctor-D3WjbYIE.js";
import { a as PLUGIN_STATE_SQLITE_SIDECAR_SUFFIXES, c as hasPendingSqliteSidecarArchive, d as migrateLegacyDeliveryQueues, f as migrateLegacyTaskStateSidecars, g as resolveLegacyTaskRunsSidecarPath, h as resolveLegacyPluginStateSidecarPath, l as listLegacyDeliveryQueueDeliveredMarkers, m as resolveLegacyFlowRunsSidecarPath, n as migrateLegacyPluginStateSidecar, o as TASK_STATE_SQLITE_SIDECAR_SUFFIXES, p as resolveLegacyDeliveryQueuePath, s as archiveLegacyImportSource, t as migrateLegacyInstalledPluginIndex, u as listLegacyDeliveryQueueFiles } from "./state-migrations.plugin-state-V0Kd8Mka.js";
import { r as migrateLegacyProfileWorkspace, t as autoMigrateLegacyStateDir } from "./state-migrations.state-dir-BnFhRPt5.js";
import { n as normalizeVoiceWakeRoutingConfig } from "./voicewake-routing-i9PtlWt7.js";
import { C as webPushSubscriptionToRow, S as webPushSubscriptionFromRow, c as ensureWebPushSubscriptionBindingColumns, f as isValidWebPushEndpoint, i as createWebPushVapidKeyPair, n as WEB_PUSH_VAPID_STATE_KEY, p as isValidWebPushKey, u as hashWebPushEndpoint, w as webPushSubscriptionsEqual } from "./push-web-store-DllT5THN.js";
import { d as normalizeApnsNodeId, m as normalizeCanonicalApnsRegistration, n as apnsRegistrationFromRow, o as isValidApnsNodeId, r as apnsRegistrationToRow, u as normalizeApnsEnvironment } from "./push-apns-store-cDZzCfro.js";
import { t as migrateLegacyConfigMachineState } from "./state-migrations.config-machine-state-C6bU4JX8.js";
import { t as EMPTY_LEGACY_SESSION_SURFACES } from "./legacy-session-surfaces.types-BmAZdMse.js";
import { t as migrateLegacyMediaPersistence } from "./state-migrations.media-persistence-BbW15Isv.js";
import fs, { createReadStream } from "node:fs";
import { TextDecoder as TextDecoder$1, isDeepStrictEqual } from "node:util";
import path from "node:path";
import fs$1 from "node:fs/promises";
import os from "node:os";
import { createHash, randomUUID } from "node:crypto";
import { gunzipSync } from "node:zlib";
import { OAuthClientInformationSchema, OAuthMetadataSchema, OAuthProtectedResourceMetadataSchema, OAuthTokensSchema, OpenIdProviderDiscoveryMetadataSchema } from "@modelcontextprotocol/sdk/shared/auth.js";
import { createInterface } from "node:readline";
import { root } from "@openclaw/fs-safe";
//#region src/infra/state-migrations.acp-replay.ts
const LEGACY_LEDGER_VERSION = 1;
const LEGACY_LEDGER_LOCK_OPTIONS = {
	retries: {
		retries: 8,
		factor: 2,
		minTimeout: 50,
		maxTimeout: 5e3,
		randomize: true
	},
	stale: 15e3,
	staleRecovery: "fail-closed"
};
const legacyAcpReplayRecordSchema = custom(isRecord);
const legacyAcpReplayUpdateSchema = legacyAcpReplayRecordSchema.refine((update) => typeof update.sessionUpdate === "string");
const legacyAcpReplayEventSchema = looseObject({
	seq: number().refine(Number.isInteger).refine((value) => value >= 1),
	at: number().finite(),
	sessionId: string(),
	sessionKey: string(),
	runId: unknown().optional(),
	update: legacyAcpReplayUpdateSchema
});
const legacyAcpReplaySessionSchema = looseObject({
	sessionId: string(),
	sessionKey: string(),
	cwd: string(),
	complete: boolean(),
	createdAt: number().finite(),
	updatedAt: number().finite(),
	nextSeq: number().refine(Number.isInteger).refine((value) => value >= 1),
	events: array(unknown())
});
const legacyAcpReplayLedgerSchema = looseObject({
	version: literal(LEGACY_LEDGER_VERSION),
	sessions: legacyAcpReplayRecordSchema
});
function resolveLegacyAcpReplayLedgerPath(stateDir) {
	return path.join(stateDir, "acp", "event-ledger.json");
}
function resolveLegacyAcpReplayClaimPath(sourcePath) {
	return `${sourcePath}.doctor-import`;
}
/** Detect the retired ledger only when an explicit doctor flow opts in. */
function detectLegacyAcpReplayLedger(params) {
	const sourcePath = resolveLegacyAcpReplayLedgerPath(params.stateDir);
	const claimPath = resolveLegacyAcpReplayClaimPath(sourcePath);
	return {
		sourcePath,
		hasLegacy: params.doctorOnlyStateMigrations === true && (fs.existsSync(sourcePath) || fs.existsSync(claimPath))
	};
}
function parseLegacyEvent(raw, sessionId) {
	const parsed = legacyAcpReplayEventSchema.safeParse(raw);
	if (!parsed.success || parsed.data.sessionId !== sessionId) throw new Error(`legacy ACP replay session ${sessionId} contains an invalid event`);
	const event = parsed.data;
	if (event.runId !== void 0 && (typeof event.runId !== "string" || event.runId.length === 0)) throw new Error(`legacy ACP replay session ${sessionId} contains an invalid run id`);
	return {
		seq: event.seq,
		at: event.at,
		sessionId,
		sessionKey: event.sessionKey,
		...typeof event.runId === "string" ? { runId: event.runId } : {},
		update: structuredClone(event.update)
	};
}
function parseLegacySession(raw, expectedSessionId) {
	const parsed = legacyAcpReplaySessionSchema.safeParse(raw);
	if (!parsed.success || parsed.data.sessionId !== expectedSessionId) throw new Error(`legacy ACP replay session ${expectedSessionId} is invalid`);
	const session = parsed.data;
	const events = session.events.map((event) => parseLegacyEvent(event, expectedSessionId));
	const sequences = new Set(events.map((event) => event.seq));
	const maxSeq = events.reduce((max, event) => Math.max(max, event.seq), 0);
	if (sequences.size !== events.length || session.nextSeq <= maxSeq) throw new Error(`legacy ACP replay session ${expectedSessionId} has invalid sequencing`);
	return {
		sessionId: expectedSessionId,
		sessionKey: session.sessionKey,
		cwd: session.cwd,
		complete: session.complete,
		createdAt: session.createdAt,
		updatedAt: session.updatedAt,
		nextSeq: session.nextSeq,
		events: events.toSorted((left, right) => left.seq - right.seq)
	};
}
function parseLegacyLedger(raw) {
	const parsed = legacyAcpReplayLedgerSchema.safeParse(JSON.parse(raw));
	if (!parsed.success) throw new Error("legacy ACP replay ledger must be a version 1 JSON object");
	return Object.entries(parsed.data.sessions).map(([sessionId, session]) => parseLegacySession(session, sessionId));
}
function estimateSessionBytes(session) {
	return session.sessionId.length + session.sessionKey.length + session.cwd.length + 32;
}
function estimateEventBytes(event, updateJson) {
	return event.sessionId.length + event.sessionKey.length + updateJson.length + (event.runId?.length ?? 0) + 32;
}
function sourceIdentity(stat, raw) {
	return {
		dev: stat.dev,
		ino: stat.ino,
		mtimeMs: stat.mtimeMs,
		sha256: createHash("sha256").update(raw).digest("hex"),
		size: stat.size
	};
}
function sourceIdentityMatches(left, right) {
	return left.dev === right.dev && left.ino === right.ino && left.mtimeMs === right.mtimeMs && left.sha256 === right.sha256 && left.size === right.size;
}
function reconcileCanonicalSession(db, session) {
	const replayDb = getNodeSqliteKysely(db);
	const stored = executeSqliteQueryTakeFirstSync(db, replayDb.selectFrom("acp_replay_sessions").select([
		"session_key",
		"cwd",
		"complete",
		"created_at",
		"updated_at",
		"next_seq",
		"estimated_bytes"
	]).where("session_id", "=", session.sessionId));
	if (!stored || stored.session_key !== session.sessionKey || stored.cwd !== session.cwd || stored.complete !== (session.complete ? 1 : 0) || stored.created_at !== session.createdAt || stored.updated_at !== session.updatedAt || stored.next_seq !== session.nextSeq) return false;
	const storedEvents = executeSqliteQuerySync(db, replayDb.selectFrom("acp_replay_events").select([
		"seq",
		"at",
		"session_key",
		"run_id",
		"update_json",
		"estimated_bytes"
	]).where("session_id", "=", session.sessionId).orderBy("seq", "asc")).rows;
	if (storedEvents.length !== session.events.length) return false;
	const expectedEventBytes = [];
	for (const [index, event] of session.events.entries()) {
		const storedEvent = storedEvents[index];
		if (!storedEvent) return false;
		let storedUpdate;
		try {
			storedUpdate = JSON.parse(storedEvent.update_json);
		} catch {
			return false;
		}
		if (storedEvent.seq !== event.seq || storedEvent.at !== event.at || storedEvent.session_key !== event.sessionKey || storedEvent.run_id !== (event.runId ?? null) || !isDeepStrictEqual(storedUpdate, event.update)) return false;
		expectedEventBytes.push(estimateEventBytes(event, JSON.stringify(event.update)));
	}
	for (const [index, event] of session.events.entries()) {
		const expectedBytes = expectedEventBytes[index];
		if (expectedBytes !== void 0 && storedEvents[index]?.estimated_bytes !== expectedBytes) executeSqliteQuerySync(db, replayDb.updateTable("acp_replay_events").set({ estimated_bytes: expectedBytes }).where("session_id", "=", session.sessionId).where("seq", "=", event.seq));
	}
	const expectedSessionBytes = estimateSessionBytes(session) + expectedEventBytes.reduce((sum, value) => sum + value, 0);
	if (stored.estimated_bytes !== expectedSessionBytes) executeSqliteQuerySync(db, replayDb.updateTable("acp_replay_sessions").set({ estimated_bytes: expectedSessionBytes }).where("session_id", "=", session.sessionId));
	return true;
}
/** Import, verify, and remove the retired JSON ledger during explicit doctor repair. */
async function migrateLegacyAcpReplayLedger(params) {
	const changes = [];
	const warnings = [];
	if (!params.detected.hasLegacy) return {
		changes,
		warnings
	};
	try {
		const result = await withFileLock(params.detected.sourcePath, LEGACY_LEDGER_LOCK_OPTIONS, async () => {
			const claimPath = resolveLegacyAcpReplayClaimPath(params.detected.sourcePath);
			const resumedClaim = fs.existsSync(claimPath);
			const activePath = resumedClaim ? claimPath : params.detected.sourcePath;
			const before = await fs$1.lstat(activePath);
			if (!before.isFile() || before.isSymbolicLink()) throw new Error("legacy ACP replay source is not a regular non-symlink file");
			const raw = await fs$1.readFile(activePath, "utf8");
			const identity = sourceIdentity(before, raw);
			const sessions = parseLegacyLedger(raw);
			let importedSessions = 0;
			let importedEvents = 0;
			let retainedSessions = 0;
			let claimedThisRun = false;
			try {
				if (!resumedClaim) {
					await fs$1.rename(params.detected.sourcePath, claimPath);
					claimedThisRun = true;
					if (!sourceIdentityMatches(identity, sourceIdentity(await fs$1.lstat(claimPath), await fs$1.readFile(claimPath, "utf8")))) throw new Error("legacy ACP replay source changed while doctor was claiming it");
				}
				runOpenClawStateWriteTransaction(({ db }) => {
					const replayDb = getNodeSqliteKysely(db);
					const missingSessions = [];
					for (const session of sessions) {
						if (executeSqliteQueryTakeFirstSync(db, replayDb.selectFrom("acp_replay_sessions").select("session_id").where("session_id", "=", session.sessionId))) {
							if (!reconcileCanonicalSession(db, session)) throw new Error(`canonical ACP replay session ${session.sessionId} conflicts with the legacy source`);
							retainedSessions += 1;
							continue;
						}
						missingSessions.push(session);
					}
					for (const session of missingSessions) {
						let estimatedBytes = estimateSessionBytes(session);
						executeSqliteQuerySync(db, replayDb.insertInto("acp_replay_sessions").values({
							session_id: session.sessionId,
							session_key: session.sessionKey,
							cwd: session.cwd,
							complete: session.complete ? 1 : 0,
							created_at: session.createdAt,
							updated_at: session.updatedAt,
							next_seq: session.nextSeq,
							estimated_bytes: estimatedBytes
						}));
						for (const event of session.events) {
							const updateJson = JSON.stringify(event.update);
							const eventBytes = estimateEventBytes(event, updateJson);
							executeSqliteQuerySync(db, replayDb.insertInto("acp_replay_events").values({
								session_id: event.sessionId,
								seq: event.seq,
								at: event.at,
								session_key: event.sessionKey,
								run_id: event.runId ?? null,
								update_json: updateJson,
								estimated_bytes: eventBytes
							}));
							estimatedBytes += eventBytes;
							importedEvents += 1;
						}
						executeSqliteQuerySync(db, replayDb.updateTable("acp_replay_sessions").set({ estimated_bytes: estimatedBytes }).where("session_id", "=", session.sessionId));
						if (!reconcileCanonicalSession(db, session)) throw new Error(`failed verifying imported ACP replay session ${session.sessionId}`);
						importedSessions += 1;
					}
				}, { env: {
					...process.env,
					OPENCLAW_STATE_DIR: params.stateDir
				} });
				await fs$1.unlink(claimPath);
				return {
					importedSessions,
					importedEvents,
					retainedSessions,
					pendingSource: fs.existsSync(params.detected.sourcePath)
				};
			} catch (error) {
				if (claimedThisRun && !fs.existsSync(params.detected.sourcePath)) await fs$1.rename(claimPath, params.detected.sourcePath).catch(() => {});
				throw error;
			}
		});
		changes.push(`Migrated ${result.importedSessions} ACP replay session(s) and ${result.importedEvents} event(s) → shared SQLite state`);
		if (result.retainedSessions > 0) changes.push(`Kept ${result.retainedSessions} existing ACP replay session(s) from shared SQLite state`);
		changes.push(`Removed retired ACP replay ledger ${params.detected.sourcePath}`);
		if (result.pendingSource) warnings.push(`A newer ACP replay ledger remains at ${params.detected.sourcePath}; rerun doctor to migrate it`);
	} catch (error) {
		warnings.push(`Failed migrating legacy ACP replay ledger ${params.detected.sourcePath}: ${String(error)}`);
	}
	return {
		changes,
		warnings
	};
}
//#endregion
//#region src/infra/legacy-json-object-stream.ts
const JSON_WHITESPACE = /* @__PURE__ */ new Set([
	" ",
	"	",
	"\r",
	"\n"
]);
var JsonCharacterCursor = class {
	constructor(chunks) {
		this.chunk = "";
		this.offset = 0;
		this.chunks = chunks[Symbol.asyncIterator]();
	}
	async fill() {
		while (this.offset >= this.chunk.length) {
			const next = await this.chunks.next();
			if (next.done) return false;
			this.chunk = next.value;
			this.offset = 0;
		}
		return true;
	}
	async peek() {
		return await this.fill() ? this.chunk[this.offset] ?? null : null;
	}
	async take() {
		if (!await this.fill()) return null;
		return this.chunk[this.offset++] ?? null;
	}
	async skipWhitespace() {
		while (true) {
			const next = await this.peek();
			if (next === null || !JSON_WHITESPACE.has(next)) return;
			await this.take();
		}
	}
};
function parseLegacyJson(raw) {
	try {
		return JSON.parse(raw);
	} catch {
		throw new Error("legacy JSON store contains invalid JSON");
	}
}
async function expectCharacter(cursor, expected) {
	await cursor.skipWhitespace();
	if (await cursor.take() !== expected) throw new Error(`expected ${JSON.stringify(expected)} in legacy JSON store`);
}
async function readJsonString(cursor) {
	await cursor.skipWhitespace();
	if (await cursor.take() !== "\"") throw new Error("expected string in legacy JSON store");
	let raw = "\"";
	let escaped = false;
	while (true) {
		const character = await cursor.take();
		if (character === null) throw new Error("unterminated string in legacy JSON store");
		raw += character;
		if (escaped) {
			escaped = false;
			continue;
		}
		if (character === "\\") {
			escaped = true;
			continue;
		}
		if (character === "\"") {
			const parsed = parseLegacyJson(raw);
			if (typeof parsed !== "string") throw new Error("invalid string in legacy JSON store");
			return parsed;
		}
	}
}
async function readJsonObject(cursor) {
	await cursor.skipWhitespace();
	if (await cursor.take() !== "{") throw new Error("legacy JSON entries must be objects");
	let raw = "{";
	let depth = 1;
	let escaped = false;
	let inString = false;
	while (depth > 0) {
		const character = await cursor.take();
		if (character === null) throw new Error("unterminated object in legacy JSON store");
		raw += character;
		if (inString) {
			if (escaped) escaped = false;
			else if (character === "\\") escaped = true;
			else if (character === "\"") inString = false;
			continue;
		}
		if (character === "\"") inString = true;
		else if (character === "{") depth += 1;
		else if (character === "}") depth -= 1;
	}
	return parseLegacyJson(raw);
}
async function parseSinglePropertyObject(params) {
	const cursor = new JsonCharacterCursor(params.chunks);
	await expectCharacter(cursor, "{");
	if (await readJsonString(cursor) !== params.property) throw new Error(`legacy JSON store must contain only ${params.property}`);
	await expectCharacter(cursor, ":");
	await expectCharacter(cursor, "{");
	await cursor.skipWhitespace();
	if (await cursor.peek() !== "}") while (true) {
		const key = await readJsonString(cursor);
		await expectCharacter(cursor, ":");
		params.onEntry(key, await readJsonObject(cursor));
		await cursor.skipWhitespace();
		const separator = await cursor.take();
		if (separator === "}") break;
		if (separator !== ",") throw new Error("expected comma or object end in legacy JSON store");
	}
	else await cursor.take();
	await expectCharacter(cursor, "}");
	await cursor.skipWhitespace();
	if (await cursor.take() !== null) throw new Error("legacy JSON store has trailing content");
}
async function* decodeUtf8Chunks(params) {
	const decoder = new TextDecoder("utf-8", { fatal: true });
	const stream = params.handle.createReadStream({
		autoClose: false,
		start: 0
	});
	for await (const rawChunk of stream) {
		const chunk = Buffer.isBuffer(rawChunk) ? rawChunk : Buffer.from(rawChunk);
		params.hash.update(chunk);
		params.onBytes(chunk.byteLength);
		const text = decoder.decode(chunk, { stream: true });
		if (text) yield text;
	}
	const tail = decoder.decode();
	if (tail) yield tail;
}
function assertStableRead(before, after, bytesRead) {
	if (before.dev !== after.dev || before.ino !== after.ino || before.mtimeMs !== after.mtimeMs || before.size !== after.size || bytesRead !== after.size) throw new Error("legacy JSON store changed while it was being read");
}
/** Hash a safely opened file, optionally parsing its single object property entry by entry. */
async function readLegacyJsonObjectStream(params) {
	const opened = await params.stateRoot.open(params.relativePath, {
		hardlinks: "reject",
		symlinks: "reject"
	});
	const hash = createHash("sha256");
	let size = 0;
	try {
		const before = opened.stat;
		if (params.property && params.onEntry) await parseSinglePropertyObject({
			chunks: decodeUtf8Chunks({
				handle: opened.handle,
				hash,
				onBytes: (length) => {
					size += length;
				}
			}),
			property: params.property,
			onEntry: params.onEntry
		});
		else {
			const stream = opened.handle.createReadStream({
				autoClose: false,
				start: 0
			});
			for await (const rawChunk of stream) {
				const chunk = Buffer.isBuffer(rawChunk) ? rawChunk : Buffer.from(rawChunk);
				hash.update(chunk);
				size += chunk.byteLength;
			}
		}
		const after = await opened.handle.stat();
		assertStableRead(before, after, size);
		return {
			dev: after.dev,
			ino: after.ino,
			mtimeMs: after.mtimeMs,
			sha256: hash.digest("hex"),
			size
		};
	} catch (error) {
		if (error instanceof TypeError && /encoded data was not valid/i.test(error.message)) throw new Error("legacy JSON store is not valid UTF-8", { cause: error });
		throw error;
	} finally {
		await opened[Symbol.asyncDispose]();
	}
}
//#endregion
//#region src/infra/state-migrations.json-fields.ts
function assertAllowedJsonFields(value, allowed, label, options = {}) {
	const unexpected = Object.keys(value).find((key) => !allowed.has(key));
	if (unexpected !== void 0) {
		const field = unexpected === "" ? "\"\"" : unexpected;
		const detail = options.reportField === false ? "an unexpected field" : `unexpected field ${options.fieldPrefix ?? ""}${field}`;
		throw new Error(`${label} has ${detail}`);
	}
}
//#endregion
//#region src/infra/state-migrations.apns.ts
const LEGACY_APNS_REGISTRATION_PATH = "push/apns-registrations.json";
const APNS_DOCTOR_CLAIM_SUFFIX = ".doctor-importing";
const MIGRATION_KIND$6 = "legacy-apns-registrations-json";
const MAX_LEGACY_APNS_UPDATED_AT_MS = 864e13;
const DIRECT_REGISTRATION_KEYS = /* @__PURE__ */ new Set([
	"nodeId",
	"transport",
	"token",
	"topic",
	"environment",
	"updatedAtMs"
]);
const RELAY_REGISTRATION_KEYS = /* @__PURE__ */ new Set([
	"nodeId",
	"transport",
	"relayHandle",
	"sendGrant",
	"installationId",
	"topic",
	"environment",
	"distribution",
	"updatedAtMs",
	"relayOrigin",
	"tokenDebugSuffix"
]);
function resolveLegacyApnsPath(stateDir) {
	return path.join(stateDir, LEGACY_APNS_REGISTRATION_PATH);
}
/** Detect the retired APNs store only when an explicit Doctor flow opts in. */
function detectLegacyApnsRegistrations(params) {
	const sourcePath = resolveLegacyApnsPath(params.stateDir);
	return {
		sourcePath,
		hasLegacy: params.doctorOnlyStateMigrations === true && legacyMigrationSourceOrClaimMayExist(sourcePath, APNS_DOCTOR_CLAIM_SUFFIX)
	};
}
function relativeLegacyPath$1(stateDir, filePath) {
	return resolveLegacyMigrationRelativePath(stateDir, filePath, "APNs", false);
}
async function readLegacySourceSnapshot$3(stateRoot, stateDir, sourcePath, onEntry) {
	return {
		sourcePath,
		...await readLegacyJsonObjectStream({
			stateRoot,
			relativePath: relativeLegacyPath$1(stateDir, sourcePath),
			...onEntry ? {
				property: "registrationsByNodeId",
				onEntry
			} : {}
		})
	};
}
function isValidLegacyApnsTimestamp(value) {
	return typeof value === "number" && Number.isSafeInteger(value) && value >= 0 && value <= MAX_LEGACY_APNS_UPDATED_AT_MS;
}
function parseLegacyApnsRegistration(rawNodeId, rawRegistration, env) {
	if (!isRecord(rawRegistration)) throw new Error("legacy APNs registration is not an object");
	const transport = rawRegistration.transport ?? "direct";
	if (transport !== "direct" && transport !== "relay") throw new Error("legacy APNs registration has invalid transport");
	assertAllowedJsonFields(rawRegistration, transport === "relay" ? RELAY_REGISTRATION_KEYS : DIRECT_REGISTRATION_KEYS, "legacy APNs registration", { reportField: false });
	const normalizedNodeId = normalizeApnsNodeId(rawNodeId);
	if (!isValidApnsNodeId(normalizedNodeId)) throw new Error("legacy APNs registration has an invalid node id");
	if (!isValidLegacyApnsTimestamp(rawRegistration.updatedAtMs)) throw new Error("legacy APNs registration has an invalid updated timestamp");
	const candidate = transport === "direct" ? {
		...rawRegistration,
		transport,
		environment: normalizeApnsEnvironment(rawRegistration.environment) ?? "sandbox"
	} : {
		...rawRegistration,
		transport
	};
	const registration = normalizeCanonicalApnsRegistration(candidate, env);
	const invalidRelayOrigin = transport === "relay" && Object.hasOwn(rawRegistration, "relayOrigin") && (!registration || registration.transport !== "relay" || !registration.relayOrigin);
	const invalidTokenDebugSuffix = transport === "relay" && Object.hasOwn(rawRegistration, "tokenDebugSuffix") && typeof rawRegistration.tokenDebugSuffix !== "string";
	if (!registration || registration.nodeId !== normalizedNodeId || invalidRelayOrigin || invalidTokenDebugSuffix) throw new Error("legacy APNs registration is invalid");
	return [normalizedNodeId, registration];
}
function importAndRecordReceipt$2(params) {
	const sourceKey = resolveLegacyMigrationSourceKey("apns-json", params.sourcePath);
	const runId = `${sourceKey}:${params.snapshot.sha256.slice(0, 16)}`;
	const now = Date.now();
	return runOpenClawStateWriteTransaction(({ db }) => {
		const stateDb = getNodeSqliteKysely(db);
		if (readLegacyMigrationReceiptFromDatabase(db, sourceKey)) return {
			sourceKey,
			imported: 0,
			preserved: 0,
			suppressed: 0,
			receiptAuthoritative: true
		};
		let imported = 0;
		let preserved = 0;
		let suppressed = 0;
		const expectedNodeIds = [];
		for (const [nodeId, registration] of params.registrations) {
			const existing = executeSqliteQueryTakeFirstSync(db, stateDb.selectFrom("apns_registrations").selectAll().where("node_id", "=", nodeId));
			const tombstone = executeSqliteQueryTakeFirstSync(db, stateDb.selectFrom("apns_registration_tombstones").select("node_id").where("node_id", "=", nodeId));
			if (existing && tombstone) throw new Error("APNs state has both a registration and deletion tombstone");
			if (existing) {
				apnsRegistrationFromRow(existing);
				preserved += 1;
				expectedNodeIds.push(nodeId);
			} else if (tombstone) suppressed += 1;
			else {
				executeSqliteQuerySync(db, stateDb.insertInto("apns_registrations").values(apnsRegistrationToRow(registration)));
				imported += 1;
				expectedNodeIds.push(nodeId);
			}
		}
		for (const nodeId of expectedNodeIds) {
			const verified = executeSqliteQueryTakeFirstSync(db, stateDb.selectFrom("apns_registrations").selectAll().where("node_id", "=", nodeId));
			if (!verified) throw new Error("SQLite verification failed for an APNs registration");
			apnsRegistrationFromRow(verified);
		}
		const reportJson = JSON.stringify({
			source: MIGRATION_KIND$6,
			target: "apns_registrations",
			sourceSha256: params.snapshot.sha256,
			sourceRecordCount: params.registrations.size,
			importedRecordCount: imported,
			preservedSqliteRecordCount: preserved,
			suppressedDeletedRecordCount: suppressed
		});
		recordLegacyMigrationReceipt(db, {
			sourceKey,
			migrationKind: MIGRATION_KIND$6,
			sourcePath: params.sourcePath,
			targetTable: "apns_registrations",
			sourceSha256: params.snapshot.sha256,
			sourceSizeBytes: params.snapshot.size,
			sourceRecordCount: params.registrations.size,
			runId,
			now,
			reportJson
		});
		return {
			sourceKey,
			imported,
			preserved,
			suppressed,
			receiptAuthoritative: false
		};
	}, { env: params.env });
}
async function cleanupReceiptAuthoritativeSources$1(params) {
	let removed = 0;
	for (const candidate of [params.sourcePath, `${params.sourcePath}${APNS_DOCTOR_CLAIM_SUFFIX}`]) {
		if (!await params.stateRoot.exists(relativeLegacyPath$1(params.stateDir, candidate))) continue;
		await readLegacySourceSnapshot$3(params.stateRoot, params.stateDir, candidate);
		if (params.removeSource) await params.removeSource(candidate);
		else await params.stateRoot.remove(relativeLegacyPath$1(params.stateDir, candidate));
		removed += 1;
	}
	if (!params.receipt.removedSource || removed > 0) markLegacyMigrationSourceRemoved(params.receipt.sourceKey, params.env);
	return removed;
}
async function migrateWithExclusiveStateOwnership$5(params) {
	const changes = [];
	const warnings = [];
	const notices = [];
	if (!params.detected.hasLegacy) return {
		changes,
		warnings
	};
	const receipt = readLegacyMigrationReceipt(resolveLegacyMigrationSourceKey("apns-json", params.detected.sourcePath), params.env);
	if (receipt) {
		try {
			if (await cleanupReceiptAuthoritativeSources$1({
				...params,
				sourcePath: params.detected.sourcePath,
				receipt
			}) > 0) notices.push("Discarded retired APNs JSON state already covered by its SQLite receipt.");
		} catch (error) {
			warnings.push(`APNs state is in SQLite, but legacy cleanup failed: ${String(error)}`);
		}
		return notices.length > 0 ? {
			changes,
			warnings,
			notices
		} : {
			changes,
			warnings
		};
	}
	const sourcePath = params.detected.sourcePath;
	const source = new LegacyMigrationSourceClaim({
		stateRoot: params.stateRoot,
		stateDir: params.stateDir,
		sourcePath,
		label: "APNs",
		includeFilePath: false,
		claimSuffix: APNS_DOCTOR_CLAIM_SUFFIX,
		readSnapshot: (snapshotPath) => readLegacySourceSnapshot$3(params.stateRoot, params.stateDir, snapshotPath)
	});
	const hasSource = await source.exists();
	const hasClaim = await source.exists(true);
	if (hasSource && hasClaim) return {
		changes,
		warnings: ["Failed migrating legacy APNs state: source and interrupted claim both exist."]
	};
	const activePath = hasSource ? sourcePath : hasClaim ? source.claimPath : null;
	if (!activePath) return {
		changes,
		warnings
	};
	let snapshot;
	const registrations = /* @__PURE__ */ new Map();
	try {
		snapshot = await readLegacySourceSnapshot$3(params.stateRoot, params.stateDir, activePath, (rawNodeId, rawRegistration) => {
			const [nodeId, registration] = parseLegacyApnsRegistration(rawNodeId, rawRegistration, params.env);
			if (registrations.has(nodeId)) throw new Error("legacy APNs registration has a duplicate node id");
			registrations.set(nodeId, registration);
		});
	} catch (error) {
		warnings.push(`Failed reading legacy APNs state: ${String(error)}`);
		return {
			changes,
			warnings
		};
	}
	if (activePath === sourcePath) try {
		snapshot = await source.claim({
			snapshot,
			mismatchMessage: "legacy APNs source changed before Doctor could claim it",
			beforeClaim: params.beforeClaim
		});
	} catch (error) {
		const restoreError = await source.restore();
		warnings.push(`Failed migrating legacy APNs state: ${String(error)}${restoreError ? `; restore failure: ${restoreError}` : ""}`);
		return {
			changes,
			warnings
		};
	}
	let result;
	try {
		result = importAndRecordReceipt$2({
			env: params.env,
			sourcePath,
			snapshot,
			registrations
		});
	} catch (error) {
		const restoreError = await source.restore();
		warnings.push(`Failed migrating legacy APNs state: ${String(error)}${restoreError ? `; restore failure: ${restoreError}` : ""}`);
		return {
			changes,
			warnings
		};
	}
	try {
		await source.remove({
			removeSource: params.removeSource,
			sourceReappearedMessage: "legacy APNs source reappeared during import"
		});
		markLegacyMigrationSourceRemoved(result.sourceKey, params.env);
	} catch (error) {
		warnings.push(`APNs state is in SQLite, but legacy cleanup failed: ${String(error)}`);
		return {
			changes,
			warnings
		};
	}
	changes.push(`Migrated ${result.imported} APNs registration${result.imported === 1 ? "" : "s"} to SQLite.`);
	if (result.preserved > 0) notices.push(`Preserved ${result.preserved} canonical SQLite APNs registration${result.preserved === 1 ? "" : "s"}.`);
	if (result.suppressed > 0) notices.push(`Kept ${result.suppressed} deleted APNs registration${result.suppressed === 1 ? "" : "s"} retired.`);
	notices.push("Removed retired APNs JSON state after verified SQLite import.");
	return {
		changes,
		warnings,
		notices
	};
}
/** Import the retired APNs store while excluding old Gateways that can recreate it. */
async function migrateLegacyApnsRegistrations(params) {
	if (!params.detected.hasLegacy) return {
		changes: [],
		warnings: []
	};
	return await withLegacyMigrationStateLock({
		stateDir: params.stateDir,
		env: params.env,
		label: "legacy APNs state",
		releaseLabel: "APNs",
		errorLabel: "Failed reading legacy APNs state",
		run: async (env) => {
			const stateRoot = await root(params.stateDir, {
				hardlinks: "reject",
				symlinks: "reject"
			});
			return await migrateWithExclusiveStateOwnership$5({
				...params,
				env,
				stateRoot
			});
		}
	});
}
//#endregion
//#region src/infra/state-migrations.audit-sanitized.ts
async function writeRecoveredSanitizedAuditArchive(params) {
	const current = await params.root.exists(params.relativePath) ? await readLegacyAuditSourceSnapshot(params.root, params.relativePath) : void 0;
	let desired;
	if (params.previousCheckpoint) {
		if (!current || current.rawBytes.length < params.previousCheckpoint.sanitizedSize) {
			params.warnings.push(`Skipped ${params.sourceLabel} recovery because its sanitized archive is missing or truncated`);
			return false;
		}
		const checkpointedPrefix = current.rawBytes.subarray(0, params.previousCheckpoint.sanitizedSize);
		if (createHash("sha256").update(checkpointedPrefix).digest("hex") !== params.previousCheckpoint.sanitizedContentHash) {
			params.warnings.push(`Skipped ${params.sourceLabel} recovery because its sanitized archive changed after checkpoint`);
			return false;
		}
		desired = Buffer.concat([checkpointedPrefix, Buffer.from(params.candidateRecordsJsonl, "utf8")]);
		if (current.rawBytes.equals(desired)) return true;
		const currentIsVerifiedDesiredPrefix = desired.subarray(0, current.rawBytes.length).equals(current.rawBytes);
		if (current.rawBytes.length !== params.previousCheckpoint.sanitizedSize && !currentIsVerifiedDesiredPrefix) {
			params.warnings.push(`Skipped ${params.sourceLabel} recovery because its sanitized archive has an uncheckpointed tail`);
			return false;
		}
	} else {
		desired = Buffer.from(params.allRecordsJsonl, "utf8");
		if (current?.rawBytes.equals(desired)) return true;
	}
	await params.root.write(params.relativePath, desired, {
		mkdir: false,
		mode: 384
	});
	return true;
}
//#endregion
//#region src/infra/state-migrations.audit-logs.ts
function legacyAuditClaimPathForArchive(sourcePath, sanitizedArchivePath) {
	const archivePrefix = `${sourcePath}.migrated`;
	if (!sanitizedArchivePath.startsWith(archivePrefix)) throw new Error(`Invalid legacy audit archive path ${sanitizedArchivePath}`);
	const generationSuffix = sanitizedArchivePath.slice(archivePrefix.length);
	return path.join(path.dirname(sourcePath), `.${path.basename(sourcePath)}.doctor-importing${generationSuffix}`);
}
async function resolveAuditArchiveRelativePaths(root, sourceRelativePath) {
	const directoryPath = path.join(root.rootReal, path.dirname(sourceRelativePath));
	const baseName = path.basename(sourceRelativePath).replace(/[.*+?^${}()|[\]\\]/gu, "\\$&");
	const archivePattern = new RegExp(`^${baseName}\\.migrated(?:\\.([2-9]|[1-9][0-9]+))?(?:\\.raw)?$`, "u");
	const claimPattern = new RegExp(`^\\.${baseName}\\.doctor-importing(?:\\.([2-9]|[1-9][0-9]+))?$`, "u");
	let latestGeneration = 0n;
	for (const entry of fs.readdirSync(directoryPath)) {
		const match = archivePattern.exec(entry) ?? claimPattern.exec(entry);
		if (!match) continue;
		const generation = BigInt(match[1] ?? "1");
		if (generation > latestGeneration) latestGeneration = generation;
	}
	const generation = latestGeneration + 1n;
	const sanitized = `${sourceRelativePath}.migrated${generation === 1n ? "" : `.${generation}`}`;
	return {
		sanitized,
		raw: `${sanitized}.raw`,
		resumeSanitized: false
	};
}
async function secureAuditArchiveFile(params) {
	try {
		const opened = await params.root.openWritable(params.relativePath, { writeMode: "update" });
		try {
			await opened.handle.chmod(384);
			await opened.handle.sync();
		} finally {
			await opened.handle.close();
		}
		return true;
	} catch (error) {
		params.warnings.push(`Failed securing ${params.label} legacy source: ${String(error)}`);
		return false;
	}
}
async function archiveLegacyAuditClaim(params) {
	let moved = false;
	let sanitizedCreated = false;
	const archivePaths = params.archivePaths;
	try {
		if (archivePaths.resumeSanitized) await params.root.write(archivePaths.sanitized, params.sanitizedJsonl, {
			mkdir: false,
			mode: 384
		});
		else await params.root.create(archivePaths.sanitized, params.sanitizedJsonl, { mode: 384 });
		sanitizedCreated = true;
		if (!await secureAuditArchiveFile({
			root: params.root,
			relativePath: archivePaths.sanitized,
			label: `sanitized ${params.source.label}`,
			warnings: params.warnings
		})) return { moved: false };
		await params.root.move(params.claimRelativePath, archivePaths.raw);
		if (!await secureAuditArchiveFile({
			root: params.root,
			relativePath: archivePaths.raw,
			label: `raw archived ${params.source.label}`,
			warnings: params.warnings
		})) {
			try {
				await params.root.move(archivePaths.raw, params.claimRelativePath);
			} catch (error) {
				params.warnings.push(`Failed restoring unsecured ${params.source.label} legacy source: ${String(error)}`);
			}
			return { moved: false };
		}
		moved = true;
		const scrubbedSnapshot = await scrubLegacyAuditRecoveryArchive({
			root: params.root,
			relativePath: archivePaths.raw,
			expectedSnapshot: params.snapshot,
			label: params.source.label,
			warnings: params.warnings
		});
		params.changes.push(`Archived sanitized ${params.source.label} legacy source → ${path.join(path.dirname(params.source.logicalSourcePath), path.basename(archivePaths.sanitized))}; ${scrubbedSnapshot ? "scrubbed same-inode append recovery archive" : "retained same-inode append recovery archive for Doctor retry"} → ${path.join(path.dirname(params.source.logicalSourcePath), path.basename(archivePaths.raw))}`);
		return {
			moved: true,
			rawRelativePath: archivePaths.raw,
			...scrubbedSnapshot ? { scrubbedSnapshot } : {}
		};
	} catch (error) {
		params.warnings.push(`Failed archiving ${params.source.label} ${params.source.logicalSourcePath}: ${String(error)}`);
	} finally {
		if (!moved && sanitizedCreated) await params.root.remove(archivePaths.sanitized).catch(() => void 0);
	}
	return {
		moved,
		...moved ? { rawRelativePath: archivePaths.raw } : {}
	};
}
async function restoreOrPreserveLegacyAuditClaim(params) {
	try {
		if (!await params.root.exists(params.claimRelativePath)) return;
		if (!await params.root.exists(params.sourceRelativePath)) {
			await params.root.move(params.claimRelativePath, params.sourceRelativePath);
			await secureAuditArchiveFile({
				root: params.root,
				relativePath: params.sourceRelativePath,
				label: params.source.label,
				warnings: params.warnings
			});
			return;
		}
		await params.root.move(params.claimRelativePath, params.archivePaths.raw);
		await secureAuditArchiveFile({
			root: params.root,
			relativePath: params.archivePaths.raw,
			label: `preserved ${params.source.label}`,
			warnings: params.warnings
		});
		params.warnings.push(`Preserved claimed ${params.source.label} at ${path.join(path.dirname(params.source.logicalSourcePath), path.basename(params.archivePaths.raw))} because an old writer recreated ${params.source.logicalSourcePath}`);
	} catch (error) {
		params.warnings.push(`Failed restoring claimed ${params.source.label} ${params.source.logicalSourcePath}: ${String(error)}`);
	}
}
async function migrateLegacyAuditLogSource(params) {
	const changes = [];
	const warnings = [];
	const result = (completed) => ({
		changes,
		warnings,
		completed
	});
	const root = await root$1(params.stateDir, {
		hardlinks: "reject",
		maxBytes: Number.MAX_SAFE_INTEGER,
		mkdir: false,
		mode: 384,
		symlinks: "reject"
	});
	const sourceRelativePath = path.relative(path.resolve(params.stateDir), params.source.logicalSourcePath);
	const detectedRelativePath = path.relative(path.resolve(params.stateDir), params.source.sourcePath);
	let archivePaths;
	let claimRelativePath = detectedRelativePath;
	if (params.source.storage === "active") {
		archivePaths = await resolveAuditArchiveRelativePaths(root, sourceRelativePath);
		claimRelativePath = path.relative(path.resolve(params.stateDir), legacyAuditClaimPathForArchive(params.source.logicalSourcePath, path.join(params.stateDir, archivePaths.sanitized)));
		await root.move(detectedRelativePath, claimRelativePath);
	} else if (params.source.storage === "claim") {
		if (!params.source.sanitizedArchivePath || !params.source.rawArchivePath) throw new Error(`Missing reserved archive generation for ${params.source.sourcePath}`);
		const sanitized = path.relative(path.resolve(params.stateDir), params.source.sanitizedArchivePath);
		const raw = path.relative(path.resolve(params.stateDir), params.source.rawArchivePath);
		archivePaths = {
			sanitized,
			raw,
			resumeSanitized: await root.exists(sanitized) && !await root.exists(raw)
		};
	}
	let claimFinalized = params.source.storage === "raw-archive";
	try {
		if (!await secureAuditArchiveFile({
			root,
			relativePath: claimRelativePath,
			label: `claimed ${params.source.label}`,
			warnings
		})) return result(false);
		const rawArchiveRelativePath = archivePaths?.raw ?? detectedRelativePath;
		if (!hasLegacyAuditRawCheckpointCapacity(params.stateDir, rawArchiveRelativePath)) {
			warnings.push(`Skipped ${params.source.label} migration because durable raw-archive checkpoint capacity is exhausted; left the legacy source in place`);
			return result(false);
		}
		if (!await restoreInterruptedAuditRecoveryArchive({
			root,
			relativePath: claimRelativePath,
			label: params.source.label,
			warnings
		})) return result(false);
		const snapshot = await readLegacyAuditSourceSnapshot(root, claimRelativePath);
		const sourceGeneration = legacyAuditSourceGenerationKey(rawArchiveRelativePath);
		const sanitizedRelativePath = params.source.storage === "raw-archive" && params.source.sanitizedArchivePath ? path.relative(path.resolve(params.stateDir), params.source.sanitizedArchivePath) : void 0;
		const previousCheckpoint = params.source.storage === "raw-archive" ? findPreviousLegacyAuditRawCheckpoint(params.stateDir, rawArchiveRelativePath) : void 0;
		if (params.source.storage === "raw-archive" && !previousCheckpoint) {
			if (!sanitizedRelativePath) throw new Error(`Missing sanitized archive path for ${params.source.sourcePath}`);
			const firstContentByte = snapshot.rawBytes.findIndex((byte) => byte !== 32 && byte !== 9 && byte !== 10 && byte !== 13);
			if (snapshot.rawBytes.length > 0 && firstContentByte !== 0) {
				warnings.push(`Skipped ${params.source.label} recovery because its checkpointless raw archive begins with ambiguous whitespace; left the archive in place`);
				return result(false);
			}
		}
		const prepared = prepareLegacyAuditRecords(params.source, snapshot.raw, sourceGeneration, previousCheckpoint?.recordOrdinalBase ?? 0);
		if (!prepared.ok) {
			warnings.push(...prepared.warnings);
			return result(false);
		}
		const env = {
			...process.env,
			OPENCLAW_STATE_DIR: params.stateDir
		};
		const maxEntries = params.source.kind === "config" ? CONFIG_AUDIT_MAX_ENTRIES : SYSTEM_AGENT_AUDIT_MAX_ENTRIES;
		const store = createSqliteAuditRecordStore({
			scope: params.source.kind === "config" ? CONFIG_AUDIT_SCOPE : SYSTEM_AGENT_AUDIT_SCOPE,
			maxEntries,
			env
		});
		const existingEntries = store.entries();
		const existingKeys = new Set(existingEntries.map((entry) => entry.key));
		let candidateRecords = prepared.records;
		if (params.source.storage === "raw-archive") {
			if (previousCheckpoint) {
				const appendedRecords = recordsAfterLegacyAuditRawCheckpoint({
					checkpoint: previousCheckpoint,
					snapshot,
					records: prepared.records
				});
				if (!appendedRecords) {
					warnings.push(`Skipped ${params.source.label} recovery because ${params.source.sourcePath} changed other than by append; left the raw archive in place`);
					return result(false);
				}
				candidateRecords = appendedRecords;
			}
		}
		if (!previousCheckpoint && candidateRecords === prepared.records) {
			const lastRetainedSourceIndex = prepared.records.findLastIndex((record) => existingKeys.has(record.key));
			if (lastRetainedSourceIndex >= 0) candidateRecords = prepared.records.slice(lastRetainedSourceIndex + 1);
		}
		const missing = candidateRecords.filter((record) => !existingKeys.has(record.key));
		store.registerLegacyMany(missing);
		const importedKeys = new Set(store.entries().map((entry) => entry.key));
		const retainedNewRows = missing.filter((record) => importedKeys.has(record.key)).length;
		const retentionNote = retainedNewRows === missing.length ? "" : `; ${retainedNewRows} retained after bounded retention`;
		if (params.source.storage === "raw-archive") {
			if (!sanitizedRelativePath) throw new Error(`Missing sanitized archive path for ${params.source.sourcePath}`);
			if (!await writeRecoveredSanitizedAuditArchive({
				sourceLabel: params.source.label,
				root,
				relativePath: sanitizedRelativePath,
				allRecordsJsonl: prepared.sanitizedJsonl,
				candidateRecordsJsonl: serializePreparedAuditRecords(candidateRecords),
				previousCheckpoint,
				warnings
			})) return result(false);
			if (previousCheckpoint?.phase !== "merge-intent" || candidateRecords.length > 0) {
				if (!await recordLegacyAuditRawCheckpoint({
					stateDir: params.stateDir,
					rawPath: params.source.sourcePath,
					rawRelativePath: claimRelativePath,
					sanitizedRelativePath,
					root,
					snapshot,
					phase: "merge-intent",
					recordCount: prepared.records.length,
					recordOrdinalBase: previousCheckpoint?.recordOrdinalBase ?? 0,
					warnings
				})) return result(false);
			}
			if (!await secureAuditArchiveFile({
				root,
				relativePath: sanitizedRelativePath,
				label: `sanitized ${params.source.label}`,
				warnings
			})) return result(false);
			if (missing.length > 0) changes.push(`Recovered ${missing.length} later ${params.source.label} row(s) from ${params.source.sourcePath}${retentionNote}`);
			const scrubbedSnapshot = await scrubLegacyAuditRecoveryArchive({
				root,
				relativePath: claimRelativePath,
				expectedSnapshot: snapshot,
				label: params.source.label,
				warnings
			});
			if (!scrubbedSnapshot) return result(false);
			const scrubbedRecords = prepareLegacyAuditRecords(params.source, scrubbedSnapshot.raw, legacyAuditSourceGenerationKey(rawArchiveRelativePath));
			if (!scrubbedRecords.ok) {
				warnings.push(...scrubbedRecords.warnings);
				warnings.push(`Retained uncheckpointed ${params.source.label} recovery archive; rerun openclaw doctor --fix`);
				return result(false);
			}
			if (scrubbedRecords.records.length !== 0) {
				warnings.push(`A legacy ${params.source.label} writer appended during recovery; rerun openclaw doctor --fix to import the retained rows`);
				return result(false);
			}
			const checkpointed = await recordLegacyAuditRawCheckpoint({
				stateDir: params.stateDir,
				rawPath: params.source.sourcePath,
				rawRelativePath: claimRelativePath,
				sanitizedRelativePath,
				root,
				snapshot: scrubbedSnapshot,
				phase: "raw",
				recordCount: 0,
				recordOrdinalBase: (previousCheckpoint?.recordOrdinalBase ?? 0) + Math.max(previousCheckpoint?.recordCount ?? 0, prepared.records.length),
				warnings
			});
			if (checkpointed) await finalizeLegacyAuditRecoveryArchive({
				root,
				relativePath: claimRelativePath
			}).catch((error) => {
				warnings.push(`Failed removing completed ${params.source.label} recovery journal: ${String(error)}`);
			});
			return result(checkpointed);
		}
		if (!archivePaths) throw new Error(`Missing archive generation for ${params.source.sourcePath}`);
		changes.push(`Migrated ${params.source.label} -> shared SQLite state (${missing.length} new row(s)${retentionNote})`);
		const archived = await archiveLegacyAuditClaim({
			source: params.source,
			claimRelativePath,
			archivePaths,
			snapshot,
			sanitizedJsonl: prepared.sanitizedJsonl,
			root,
			changes,
			warnings
		});
		claimFinalized = archived.moved;
		if (!archived.moved || !archived.rawRelativePath) {
			changes.pop();
			return result(false);
		}
		if (!archived.scrubbedSnapshot) return result(false);
		const scrubbedRecords = prepareLegacyAuditRecords(params.source, archived.scrubbedSnapshot.raw, legacyAuditSourceGenerationKey(archived.rawRelativePath));
		if (!scrubbedRecords.ok) {
			warnings.push(...scrubbedRecords.warnings);
			warnings.push(`Retained uncheckpointed ${params.source.label} recovery archive; rerun openclaw doctor --fix`);
			return result(false);
		}
		if (scrubbedRecords.records.length !== 0) {
			warnings.push(`A legacy ${params.source.label} writer appended during migration; rerun openclaw doctor --fix to import the retained rows`);
			return result(false);
		}
		const rawPath = path.join(params.stateDir, archived.rawRelativePath);
		const checkpointed = await recordLegacyAuditRawCheckpoint({
			stateDir: params.stateDir,
			rawPath,
			rawRelativePath: archived.rawRelativePath,
			sanitizedRelativePath: archivePaths.sanitized,
			root,
			snapshot: archived.scrubbedSnapshot,
			phase: "raw",
			recordCount: 0,
			recordOrdinalBase: prepared.records.length,
			warnings
		});
		if (checkpointed) await finalizeLegacyAuditRecoveryArchive({
			root,
			relativePath: archived.rawRelativePath
		}).catch((error) => {
			warnings.push(`Failed removing completed ${params.source.label} recovery journal: ${String(error)}`);
		});
		if (await root.exists(sourceRelativePath) && !params.recreatedSourceScheduled) warnings.push(`An old writer recreated ${params.source.label} at ${params.source.logicalSourcePath}; rerun openclaw doctor --fix to import the retained rows`);
		return result(checkpointed);
	} finally {
		if (!claimFinalized && params.source.storage === "active" && archivePaths) await restoreOrPreserveLegacyAuditClaim({
			source: params.source,
			claimRelativePath,
			sourceRelativePath,
			archivePaths,
			root,
			warnings
		});
	}
}
async function migrateLegacyAuditLogs(params) {
	const changes = [];
	const warnings = [];
	if (params.detected.sources.length === 0) return {
		changes,
		warnings
	};
	const env = {
		...process.env,
		OPENCLAW_STATE_DIR: params.stateDir
	};
	let lock;
	try {
		lock = await acquireGatewayLock({
			allowInTests: true,
			env,
			pollIntervalMs: 25,
			role: "sqlite-maintenance",
			timeoutMs: 250
		});
	} catch (error) {
		warnings.push(`Skipped legacy audit migration because exclusive state ownership is unavailable: ${String(error)}`);
		return {
			changes,
			warnings
		};
	}
	if (!lock) {
		warnings.push("Skipped legacy audit migration because exclusive state ownership is unavailable");
		return {
			changes,
			warnings
		};
	}
	try {
		await withLegacyAuditMigrationLease(params.stateDir, async () => {
			const blockedLogicalSources = /* @__PURE__ */ new Set();
			for (const [index, source] of params.detected.sources.entries()) {
				if (blockedLogicalSources.has(source.logicalSourcePath)) continue;
				try {
					const recreatedSourceScheduled = params.detected.sources.slice(index + 1).some((candidate) => candidate.storage === "active" && candidate.logicalSourcePath === source.logicalSourcePath);
					const result = await migrateLegacyAuditLogSource({
						source,
						stateDir: params.stateDir,
						...recreatedSourceScheduled ? { recreatedSourceScheduled: true } : {}
					});
					changes.push(...result.changes);
					warnings.push(...result.warnings);
					if (!result.completed) blockedLogicalSources.add(source.logicalSourcePath);
				} catch (error) {
					warnings.push(`Failed migrating ${source.label}: ${String(error)}`);
					blockedLogicalSources.add(source.logicalSourcePath);
				}
			}
		});
	} catch (error) {
		warnings.push(`Skipped legacy audit migration because coordination failed: ${String(error)}`);
	} finally {
		await lock.release();
	}
	return {
		changes,
		warnings
	};
}
//#endregion
//#region src/infra/state-migrations.channel-pairing.ts
const PAIRING_SUFFIX = "-pairing.json";
const ALLOW_FROM_SUFFIX = "-allowFrom.json";
function detectLegacyChannelPairingState(params) {
	let directoryEntries = [];
	try {
		directoryEntries = fs.readdirSync(params.sourceDir, { withFileTypes: true });
	} catch (err) {
		if (err.code !== "ENOENT") throw err;
	}
	const files = directoryEntries.filter((entry) => entry.isFile() && (entry.name.endsWith(PAIRING_SUFFIX) || entry.name.endsWith(ALLOW_FROM_SUFFIX))).map((entry) => entry.name).toSorted();
	const pairedChannelIds = files.filter((filename) => filename.endsWith(PAIRING_SUFFIX)).map((filename) => filename.slice(0, -13));
	const accounts = files.some((filename) => filename.endsWith(ALLOW_FROM_SUFFIX)) ? params.resolveAccounts?.() : void 0;
	const knownChannelIds = dedupePreserveOrder([
		...CHANNEL_IDS,
		...params.configuredChannelIds ?? [],
		...pairedChannelIds
	]).toSorted((left, right) => right.length - left.length || left.localeCompare(right));
	return {
		sourceDir: params.sourceDir,
		files,
		knownChannelIds,
		defaultAccountIds: { ...accounts?.defaultAccountIds },
		accountIds: Object.fromEntries(Object.entries(accounts?.accountIds ?? {}).map(([channel, accountIds]) => [channel, dedupePreserveOrder(accountIds.map((accountId) => resolveAllowFromAccountId(accountId)))])),
		hasLegacy: files.length > 0
	};
}
function parsePairingFilename(filename) {
	return filename.endsWith(PAIRING_SUFFIX) ? filename.slice(0, -13) : null;
}
function findCaseFoldedAllowFromCollisions(filenames, knownChannelIds) {
	const firstFilenameByKey = /* @__PURE__ */ new Map();
	const collisions = /* @__PURE__ */ new Set();
	for (const filename of filenames) {
		if (!filename.endsWith(ALLOW_FROM_SUFFIX)) continue;
		const stem = filename.slice(0, -15);
		for (const channel of knownChannelIds) {
			if (!stem.startsWith(`${channel}-`)) continue;
			const collisionKey = `${channel}\0${stem.slice(channel.length + 1).toLowerCase()}`;
			const firstFilename = firstFilenameByKey.get(collisionKey);
			if (firstFilename) {
				collisions.add(firstFilename);
				collisions.add(filename);
			} else firstFilenameByKey.set(collisionKey, filename);
		}
	}
	return collisions;
}
function parseAllowFromFilename(filename, knownChannelIds, defaultAccountIds, accountIds) {
	if (!filename.endsWith(ALLOW_FROM_SUFFIX)) return null;
	const stem = filename.slice(0, -15);
	const targets = [];
	let hasAccountCollision = false;
	for (const channel of knownChannelIds) {
		if (stem === channel) {
			targets.push({
				channel,
				accountId: normalizeOptionalString(defaultAccountIds[channel]) ?? "default"
			});
			continue;
		}
		if (!stem.startsWith(`${channel}-`)) continue;
		const accountKey = stem.slice(channel.length + 1);
		const matchingAccountIds = (accountIds[channel] ?? []).filter((accountId) => {
			try {
				safeAccountKey(accountId);
				if (accountId === "default" && accountKey !== "default") return false;
				return accountId.toLowerCase() === accountKey.toLowerCase();
			} catch {
				return false;
			}
		});
		if (matchingAccountIds.length === 1 && matchingAccountIds[0]) targets.push({
			channel,
			accountId: matchingAccountIds[0]
		});
		else if (matchingAccountIds.length > 1) hasAccountCollision = true;
		else if (accountKey === "default" && CHANNEL_IDS.includes(channel)) targets.push({
			channel,
			accountId: DEFAULT_ACCOUNT_ID
		});
	}
	if (hasAccountCollision || targets.length > 1) return {
		target: null,
		reason: "ambiguous"
	};
	return targets[0] ? { target: targets[0] } : {
		target: null,
		reason: "unresolved"
	};
}
function normalizeLegacyPairingRequest(value) {
	if (!isRecord(value)) return null;
	const id = normalizeOptionalString(value.id);
	const code = normalizeOptionalString(value.code);
	const createdAt = normalizeOptionalString(value.createdAt);
	const lastSeenAt = normalizeOptionalString(value.lastSeenAt) ?? createdAt;
	if (!id || !code || !createdAt || !lastSeenAt) return null;
	const meta = isRecord(value.meta) ? Object.fromEntries(Object.entries(value.meta).map(([key, entry]) => [key, normalizeOptionalString(entry) ?? ""]).filter(([, entry]) => Boolean(entry))) : void 0;
	return {
		id,
		code,
		createdAt,
		lastSeenAt,
		...meta && Object.keys(meta).length ? { meta } : {}
	};
}
function readLegacyPairingRequests(filePath) {
	try {
		const parsed = JSON.parse(fs.readFileSync(filePath, "utf8"));
		if (!isRecord(parsed) || !Array.isArray(parsed.requests)) return null;
		return parsed.requests.flatMap((entry) => {
			const request = normalizeLegacyPairingRequest(entry);
			return request ? [request] : [];
		});
	} catch {
		return null;
	}
}
function normalizeAllowEntry(channel, value) {
	const raw = typeof value === "string" || typeof value === "number" ? String(value).trim() : "";
	if (!raw || raw === "*") return "";
	let adapter;
	try {
		adapter = getPairingAdapter(channel);
	} catch {
		adapter = null;
	}
	const normalized = adapter?.normalizeAllowEntry ? adapter.normalizeAllowEntry(raw) : raw;
	const entry = normalizeOptionalString(normalized) ?? "";
	return entry === "*" ? "" : entry;
}
function readLegacyAllowFrom(filePath, channel) {
	try {
		const parsed = JSON.parse(fs.readFileSync(filePath, "utf8"));
		const values = Array.isArray(parsed) ? parsed : isRecord(parsed) && Array.isArray(parsed.allowFrom) ? parsed.allowFrom : null;
		if (!values) return null;
		return dedupePreserveOrder(values.map((value) => normalizeAllowEntry(channel, value)).filter(Boolean));
	} catch {
		return null;
	}
}
function mergePairingRequests(current, legacy) {
	const merged = current.slice();
	const keys = new Set(current.map((request) => `${resolveAllowFromAccountId(request.meta?.accountId)}\0${request.id}`));
	for (const request of legacy) {
		const key = `${resolveAllowFromAccountId(request.meta?.accountId)}\0${request.id}`;
		if (!keys.has(key)) {
			keys.add(key);
			merged.push(request);
		}
	}
	return merged;
}
function removeImportedSource(filePath, warnings) {
	try {
		fs.rmSync(filePath, { force: true });
		return true;
	} catch (err) {
		warnings.push(`Imported legacy channel pairing state but failed removing ${filePath}: ${String(err)}`);
		return false;
	}
}
function migrateLegacyChannelPairingState(params) {
	const changes = [];
	const warnings = [];
	const caseFoldedCollisions = findCaseFoldedAllowFromCollisions(params.detected.files, params.detected.knownChannelIds);
	for (const filename of params.detected.files) {
		const filePath = path.join(params.detected.sourceDir, filename);
		const pairingChannel = parsePairingFilename(filename);
		if (pairingChannel) {
			const requests = readLegacyPairingRequests(filePath);
			if (!requests) {
				warnings.push(`Legacy channel pairing file unreadable; left in place at ${filePath}`);
				continue;
			}
			updateChannelPairingStateSnapshot(pairingChannel, params.env, (state) => {
				state.requests = mergePairingRequests(state.requests, requests);
			});
			removeImportedSource(filePath, warnings);
			changes.push(`Migrated ${requests.length} ${pairingChannel} pairing request(s) → shared SQLite state`);
			continue;
		}
		const allowTarget = parseAllowFromFilename(filename, params.detected.knownChannelIds, params.detected.defaultAccountIds, params.detected.accountIds);
		if (!allowTarget) continue;
		const hasCaseFoldedCollision = caseFoldedCollisions.has(filename);
		if (hasCaseFoldedCollision || !allowTarget.target) {
			const reason = hasCaseFoldedCollision || allowTarget.reason === "ambiguous" ? "ambiguous" : "unresolved";
			warnings.push(`Legacy channel allowFrom channel/account is ${reason}; left in place at ${filePath}`);
			continue;
		}
		const entries = readLegacyAllowFrom(filePath, allowTarget.target.channel);
		if (!entries) {
			warnings.push(`Legacy channel allowFrom file unreadable; left in place at ${filePath}`);
			continue;
		}
		const accountId = resolveAllowFromAccountId(allowTarget.target.accountId);
		updateChannelPairingStateSnapshot(allowTarget.target.channel, params.env, (state) => {
			state.allowFrom ??= {};
			state.allowFrom[accountId] = dedupePreserveOrder([...state.allowFrom[accountId] ?? [], ...entries]);
		});
		removeImportedSource(filePath, warnings);
		changes.push(`Migrated ${entries.length} ${allowTarget.target.channel}/${accountId} allowFrom entr${entries.length === 1 ? "y" : "ies"} → shared SQLite state`);
	}
	return {
		changes,
		warnings
	};
}
//#endregion
//#region src/infra/state-migrations.commitments.ts
const LEGACY_COMMITMENTS_PATH = "commitments/commitments.json";
const DOCTOR_CLAIM_SUFFIX$3 = ".doctor-discarding";
const MAX_LEGACY_COMMITMENTS_BYTES = 16777216;
const MIGRATION_KIND$5 = "legacy-commitments-json";
const LEGACY_STORE_KEYS = /* @__PURE__ */ new Set(["version", "commitments"]);
const utf8Decoder$3 = new TextDecoder("utf-8", { fatal: true });
function resolveLegacyCommitmentsPath(stateDir) {
	return path.join(stateDir, LEGACY_COMMITMENTS_PATH);
}
/** Detect the exact retired store only when an explicit Doctor flow opts in. */
async function detectLegacyCommitments(params) {
	const sourcePath = resolveLegacyCommitmentsPath(params.stateDir);
	let hasPendingReceipt = false;
	if (params.doctorOnlyStateMigrations === true && !legacyMigrationSourceOrClaimMayExist(sourcePath, DOCTOR_CLAIM_SUFFIX$3)) try {
		const database = await openExistingOpenClawStateDatabaseReadOnly({ env: params.env });
		try {
			const receipt = database ? readLegacyMigrationReceiptFromDatabase(database.db, resolveLegacyMigrationSourceKey("commitments-json", sourcePath)) : null;
			hasPendingReceipt = receipt !== null && !receipt.removedSource;
		} finally {
			database?.walMaintenance.close();
		}
	} catch {}
	return {
		sourcePath,
		hasLegacy: params.doctorOnlyStateMigrations === true && (legacyMigrationSourceOrClaimMayExist(sourcePath, DOCTOR_CLAIM_SUFFIX$3) || hasPendingReceipt)
	};
}
function parseLegacyCommitments(buffer) {
	let parsed;
	try {
		parsed = JSON.parse(utf8Decoder$3.decode(buffer));
	} catch {
		throw new Error("retired commitments store is not valid UTF-8 JSON");
	}
	if (!isRecord(parsed) || parsed.version !== 1 || !Array.isArray(parsed.commitments) || Object.keys(parsed).some((key) => !LEGACY_STORE_KEYS.has(key))) throw new Error("retired commitments store must contain only version 1 and a commitments array");
	return parsed.commitments.length;
}
async function readLegacyCommitmentsSnapshot(params) {
	const snapshot = await readLegacyMigrationSourceSnapshot({
		...params,
		maxBytes: MAX_LEGACY_COMMITMENTS_BYTES,
		label: "commitments"
	});
	return {
		...snapshot,
		recordCount: parseLegacyCommitments(snapshot.buffer)
	};
}
function recordDiscardDecision(params) {
	const sourceKey = resolveLegacyMigrationSourceKey("commitments-json", params.sourcePath);
	const runId = `${sourceKey}:${params.snapshot.sha256.slice(0, 16)}`;
	const now = Date.now();
	let recreated = false;
	runOpenClawStateWriteTransaction(({ db }) => {
		recreated = readLegacyMigrationReceiptFromDatabase(db, sourceKey)?.removedSource === true;
		const reportJson = JSON.stringify({
			source: MIGRATION_KIND$5,
			target: null,
			decision: "retired-source-discarded",
			sourceSha256: params.snapshot.sha256,
			sourceRecordCount: params.snapshot.recordCount,
			importedRecordCount: 0,
			archivedRecordCount: 0,
			exportedRecordCount: 0
		});
		recordLegacyMigrationReceipt(db, {
			sourceKey,
			migrationKind: MIGRATION_KIND$5,
			sourcePath: params.sourcePath,
			targetTable: "commitments",
			sourceSha256: params.snapshot.sha256,
			sourceSizeBytes: params.snapshot.size,
			sourceRecordCount: params.snapshot.recordCount,
			runId,
			now,
			reportJson,
			upsert: true
		});
	}, { env: params.env }, { operationLabel: "state-migration.commitments" });
	return {
		recreated,
		sourceKey
	};
}
async function migrateWithExclusiveStateOwnership$4(params) {
	const sourcePath = params.detected.sourcePath;
	const sourceKey = resolveLegacyMigrationSourceKey("commitments-json", sourcePath);
	const source = new LegacyMigrationSourceClaim({
		stateRoot: params.stateRoot,
		stateDir: params.stateDir,
		sourcePath,
		label: "commitments",
		claimSuffix: DOCTOR_CLAIM_SUFFIX$3,
		readSnapshot: (candidate) => readLegacyCommitmentsSnapshot({
			stateRoot: params.stateRoot,
			stateDir: params.stateDir,
			sourcePath: candidate
		})
	});
	try {
		await source.recover("retired commitments source conflicts with its interrupted Doctor claim");
		if (!await source.exists()) {
			const receipt = readLegacyMigrationReceipt(sourceKey, params.env);
			if (receipt && !receipt.removedSource) try {
				markLegacyMigrationSourceRemoved(sourceKey, params.env, "state-migration.commitments");
				return {
					changes: ["Finalized the retired commitments JSON discard receipt."],
					warnings: []
				};
			} catch (error) {
				return {
					changes: [],
					warnings: [`Retired commitments JSON was removed, but its receipt could not be finalized: ${String(error)}`]
				};
			}
			return {
				changes: [],
				warnings: []
			};
		}
	} catch (error) {
		return {
			changes: [],
			warnings: [`Failed recovering retired commitments JSON: ${String(error)}`]
		};
	}
	let snapshot;
	try {
		snapshot = await source.read();
		params.beforeVerify?.();
		if (!legacyMigrationSourceSnapshotsMatch(snapshot, await source.read())) throw new Error("retired commitments source changed after Doctor loaded it");
	} catch (error) {
		return {
			changes: [],
			warnings: [`Failed reading retired commitments JSON: ${String(error)}`]
		};
	}
	try {
		await source.claim({
			snapshot,
			mismatchMessage: "retired commitments source changed before Doctor could claim it",
			beforeClaim: params.beforeClaim
		});
	} catch (error) {
		const restoreError = await source.restore();
		return {
			changes: [],
			warnings: [`Failed claiming retired commitments JSON: ${String(error)}${restoreError ? `; restore failure: ${restoreError}` : ""}`]
		};
	}
	let decision;
	try {
		decision = recordDiscardDecision({
			env: params.env,
			sourcePath,
			snapshot
		});
	} catch (error) {
		const restoreError = await source.restore();
		return {
			changes: [],
			warnings: [`Failed recording retired commitments discard: ${String(error)}${restoreError ? `; restore failure: ${restoreError}` : ""}`]
		};
	}
	try {
		if (!legacyMigrationSourceSnapshotsMatch(snapshot, await source.read(true))) throw new Error("retired commitments Doctor claim changed before cleanup");
		await source.remove({
			removeSource: params.removeSource,
			sourceReappearedMessage: "retired commitments source reappeared during cleanup",
			sourceRemainingMessage: "retired commitments source remains after cleanup",
			claimRemainingMessage: "retired commitments Doctor claim remains after cleanup"
		});
	} catch (error) {
		return {
			changes: [],
			warnings: [`Retired commitments discard was recorded, but cleanup failed: ${String(error)}`]
		};
	}
	const warnings = [];
	try {
		markLegacyMigrationSourceRemoved(decision.sourceKey, params.env, "state-migration.commitments");
	} catch (error) {
		warnings.push(`Retired commitments JSON was removed, but its receipt could not be finalized: ${String(error)}`);
	}
	const rowLabel = snapshot.recordCount === 1 ? "row" : "rows";
	return {
		changes: [decision.recreated ? `Discarded recreated retired commitments JSON with ${snapshot.recordCount} ${rowLabel}; no data was imported, archived, or exported.` : `Discarded retired commitments JSON with ${snapshot.recordCount} ${rowLabel}; no data was imported, archived, or exported.`],
		warnings
	};
}
/** Discard retired persistent state while excluding active Gateway owners. */
async function migrateLegacyCommitments(params) {
	if (!params.detected.hasLegacy) return {
		changes: [],
		warnings: []
	};
	return await withLegacyMigrationStateLock({
		stateDir: params.stateDir,
		env: params.env,
		label: "retired commitments JSON",
		releaseLabel: "Commitments",
		errorLabel: "Failed retiring commitments JSON",
		retryGuidance: "Stop the Gateway, then run `openclaw doctor --fix` again.",
		run: async (env) => {
			const stateRoot = await root(params.stateDir, {
				hardlinks: "reject",
				maxBytes: MAX_LEGACY_COMMITMENTS_BYTES,
				symlinks: "reject"
			});
			return await migrateWithExclusiveStateOwnership$4({
				...params,
				env,
				stateRoot
			});
		}
	});
}
//#endregion
//#region src/infra/state-migrations.debug-proxy.ts
const DEBUG_PROXY_SQLITE_SIDECAR_SUFFIXES = [
	"",
	"-shm",
	"-wal",
	"-journal"
];
var LegacyDebugProxyBlobConflictError = class extends Error {
	constructor(blobId) {
		super(`legacy debug proxy blob conflicts with shared state: ${blobId}`);
		this.blobId = blobId;
	}
};
var LegacyDebugProxySessionConflictError = class extends Error {
	constructor(sessionId) {
		super(`legacy debug proxy session conflicts with shared state: ${sessionId}`);
		this.sessionId = sessionId;
	}
};
function fileExists(filePath) {
	try {
		return fs.statSync(filePath).isFile();
	} catch {
		return false;
	}
}
function dirExists(dirPath) {
	try {
		return fs.statSync(dirPath).isDirectory();
	} catch {
		return false;
	}
}
function resolveLegacyDebugProxyCapturePaths(stateDir) {
	const rootDir = path.join(stateDir, "debug-proxy");
	return {
		sourcePath: path.join(rootDir, "capture.sqlite"),
		blobDir: path.join(rootDir, "blobs")
	};
}
function hasPendingSqliteArchive(sourcePath) {
	return !fileExists(sourcePath) && fileExists(`${sourcePath}.migrated`) && DEBUG_PROXY_SQLITE_SIDECAR_SUFFIXES.some((suffix) => suffix !== "" && fileExists(`${sourcePath}${suffix}`));
}
function detectLegacyDebugProxyCaptureSidecar(stateDir, env = process.env) {
	const paths = resolveLegacyDebugProxyCapturePaths(stateDir);
	if (path.resolve(paths.sourcePath) === path.resolve(resolveOpenClawStateSqlitePath({
		...env,
		OPENCLAW_STATE_DIR: stateDir
	}))) return {
		...paths,
		hasLegacy: false
	};
	const hasArchivedDatabase = fileExists(`${paths.sourcePath}.migrated`);
	return {
		...paths,
		hasLegacy: fileExists(paths.sourcePath) || hasPendingSqliteArchive(paths.sourcePath) || hasArchivedDatabase && dirExists(paths.blobDir)
	};
}
function listSqliteColumns(db, table) {
	const rows = db.prepare(`PRAGMA table_info(${table})`).all();
	return new Set(rows.flatMap((row) => typeof row.name === "string" ? [row.name] : []));
}
function assertTableColumns(db, table, expected) {
	const columns = listSqliteColumns(db, table);
	const missing = expected.filter((column) => !columns.has(column));
	if (missing.length > 0) throw new Error(`legacy ${table} table is missing ${missing.join(", ")}`);
}
function normalizeSqliteInteger(value) {
	return value === null ? null : coerceRequiredSqliteNumber(value);
}
function readLegacyDebugProxyCapture(params) {
	const db = openNodeSqliteDatabase(params.sourcePath, { readOnly: true });
	try {
		assertTableColumns(db, "capture_sessions", [
			"id",
			"started_at",
			"ended_at",
			"mode",
			"source_scope",
			"source_process",
			"proxy_url",
			"db_path",
			"blob_dir"
		]);
		assertTableColumns(db, "capture_events", [
			"session_id",
			"ts",
			"source_scope",
			"source_process",
			"protocol",
			"direction",
			"kind",
			"flow_id",
			"method",
			"host",
			"path",
			"status",
			"close_code",
			"content_type",
			"headers_json",
			"data_text",
			"data_blob_id",
			"data_sha256",
			"error_text",
			"meta_json"
		]);
		const sessions = db.prepare(`SELECT id, started_at, ended_at, mode, source_scope, source_process, proxy_url, blob_dir
         FROM capture_sessions
         ORDER BY started_at ASC, id ASC`).all();
		const events = db.prepare(`SELECT
           session_id, ts, source_scope, source_process, protocol, direction, kind, flow_id,
           method, host, path, status, close_code, content_type, headers_json, data_text,
           data_blob_id, data_sha256, error_text, meta_json
         FROM capture_events
         ORDER BY ts ASC, id ASC`).all();
		const sessionIds = new Set(sessions.map((session) => session.id));
		for (const event of events) {
			if (sessionIds.has(event.session_id)) continue;
			sessions.push({
				id: event.session_id,
				started_at: event.ts,
				ended_at: null,
				mode: "implicit",
				source_scope: event.source_scope,
				source_process: event.source_process,
				proxy_url: null,
				blob_dir: params.blobDir
			});
			sessionIds.add(event.session_id);
		}
		const blobEvents = /* @__PURE__ */ new Map();
		for (const event of events) {
			if (!event.data_blob_id) continue;
			const rows = blobEvents.get(event.data_blob_id) ?? [];
			rows.push(event);
			blobEvents.set(event.data_blob_id, rows);
		}
		const blobDirBySession = new Map(sessions.map((session) => [session.id, session.blob_dir]));
		const usedBlobDirs = /* @__PURE__ */ new Set();
		const blobs = [];
		for (const [blobId, referencingEvents] of blobEvents) {
			const candidateBlobDirs = [.../* @__PURE__ */ new Set([...referencingEvents.map((event) => blobDirBySession.get(event.session_id) ?? params.blobDir), params.blobDir])];
			const blobPath = candidateBlobDirs.map((blobDir) => path.join(blobDir, `${blobId}.bin.gz`)).find(fileExists) ?? path.join(candidateBlobDirs[0] ?? params.blobDir, `${blobId}.bin.gz`);
			const data = fs.readFileSync(blobPath);
			const raw = gunzipSync(data);
			const sha256 = sha256Hex(raw);
			if (sha256.slice(0, 24) !== blobId) throw new Error(`legacy debug proxy blob hash mismatch: ${blobPath}`);
			usedBlobDirs.add(path.dirname(blobPath));
			blobs.push({
				blobId,
				contentType: referencingEvents.find((event) => event.content_type)?.content_type ?? null,
				encoding: "gzip",
				sizeBytes: raw.byteLength,
				sha256,
				data,
				createdAt: Math.min(...referencingEvents.map((event) => normalizeSqliteInteger(event.ts) ?? 0))
			});
		}
		return {
			sessions,
			events,
			blobs,
			blobDirs: [...usedBlobDirs]
		};
	} finally {
		db.close();
	}
}
function eventValues(event) {
	return [
		event.session_id,
		normalizeSqliteInteger(event.ts),
		event.source_scope,
		event.source_process,
		event.protocol,
		event.direction,
		event.kind,
		event.flow_id,
		event.method,
		event.host,
		event.path,
		normalizeSqliteInteger(event.status),
		normalizeSqliteInteger(event.close_code),
		event.content_type,
		event.headers_json,
		event.data_text,
		event.data_blob_id,
		event.data_sha256,
		event.error_text,
		event.meta_json
	];
}
function eventKey(values) {
	return JSON.stringify(values);
}
function archiveLegacyDebugProxySqlite(params) {
	const existingSources = DEBUG_PROXY_SQLITE_SIDECAR_SUFFIXES.map((suffix) => `${params.sourcePath}${suffix}`).filter(fileExists);
	if (existingSources.length === 0) return;
	const resolutions = [];
	for (const sourcePath of existingSources) {
		const archivedPath = `${sourcePath}.migrated`;
		try {
			if (fileExists(archivedPath)) {
				if (fs.readFileSync(sourcePath).equals(fs.readFileSync(archivedPath))) {
					fs.rmSync(sourcePath, { force: true });
					resolutions.push({
						sourcePath,
						targetPath: archivedPath,
						removed: true
					});
					continue;
				}
				let index = 2;
				while (fs.existsSync(`${sourcePath}.migrated.${index}`)) index++;
				const nextArchivePath = `${sourcePath}.migrated.${index}`;
				fs.renameSync(sourcePath, nextArchivePath);
				resolutions.push({
					sourcePath,
					targetPath: nextArchivePath,
					removed: false
				});
				continue;
			}
			fs.renameSync(sourcePath, archivedPath);
			resolutions.push({
				sourcePath,
				targetPath: archivedPath,
				removed: false
			});
		} catch (err) {
			params.warnings.push(`Failed archiving debug proxy capture sidecar ${sourcePath}: ${String(err)}`);
			return;
		}
	}
	if (resolutions.every((resolution) => !resolution.removed && resolution.targetPath === `${resolution.sourcePath}.migrated`)) {
		params.changes.push(`Archived debug proxy capture sidecar legacy source → ${params.sourcePath}.migrated`);
		return;
	}
	for (const resolution of resolutions) params.changes.push(resolution.removed ? `Removed already-archived debug proxy capture sidecar legacy source ${resolution.sourcePath}` : `Archived debug proxy capture sidecar legacy source → ${resolution.targetPath}`);
}
function archiveLegacyDebugProxyBlobs(params) {
	if (!dirExists(params.blobDir)) return;
	const archivePath = `${params.blobDir}.migrated`;
	try {
		let targetPath = archivePath;
		if (dirExists(archivePath)) {
			let index = 2;
			while (fs.existsSync(`${params.blobDir}.migrated.${index}`)) index++;
			targetPath = `${params.blobDir}.migrated.${index}`;
		}
		fs.renameSync(params.blobDir, targetPath);
		params.changes.push(`Archived debug proxy capture blobs → ${targetPath}`);
	} catch (err) {
		params.warnings.push(`Failed archiving debug proxy capture blobs ${params.blobDir}: ${String(err)}`);
	}
}
function migrateLegacyDebugProxyCaptureSidecar(params) {
	const detected = params.detected ?? detectLegacyDebugProxyCaptureSidecar(params.stateDir);
	const changes = [];
	const warnings = [];
	if (!detected.hasLegacy) return {
		changes,
		warnings
	};
	if (!fileExists(detected.sourcePath)) {
		archiveLegacyDebugProxySqlite({
			sourcePath: detected.sourcePath,
			changes,
			warnings
		});
		if (fileExists(`${detected.sourcePath}.migrated`)) archiveLegacyDebugProxyBlobs({
			blobDir: detected.blobDir,
			changes,
			warnings
		});
		return {
			changes,
			warnings
		};
	}
	let legacy;
	try {
		legacy = readLegacyDebugProxyCapture(detected);
	} catch (err) {
		return {
			changes,
			warnings: [`Failed reading debug proxy capture sidecar ${detected.sourcePath}: ${String(err)}`]
		};
	}
	try {
		runOpenClawStateWriteTransaction(({ db }) => {
			const selectBlob = db.prepare(`SELECT encoding, size_bytes AS sizeBytes, sha256, data
           FROM capture_blobs
           WHERE blob_id = ?`);
			const insertBlob = db.prepare(`INSERT INTO capture_blobs (
            blob_id, content_type, encoding, size_bytes, sha256, data, created_at
          ) VALUES (?, ?, ?, ?, ?, ?, ?)`);
			for (const blob of legacy.blobs) {
				const existing = selectBlob.get(blob.blobId);
				if (existing) {
					if (existing.encoding !== blob.encoding || Number(existing.sizeBytes) !== blob.sizeBytes || existing.sha256 !== blob.sha256 || !existing.data || !Buffer.from(existing.data).equals(blob.data)) throw new LegacyDebugProxyBlobConflictError(blob.blobId);
					continue;
				}
				insertBlob.run(blob.blobId, blob.contentType, blob.encoding, blob.sizeBytes, blob.sha256, blob.data, blob.createdAt);
			}
			const selectSession = db.prepare(`SELECT
            started_at AS startedAt,
            ended_at AS endedAt,
            mode,
            source_scope AS sourceScope,
            source_process AS sourceProcess,
            proxy_url AS proxyUrl
           FROM capture_sessions
           WHERE id = ?`);
			const insertSession = db.prepare(`INSERT INTO capture_sessions (
            id, started_at, ended_at, mode, source_scope, source_process, proxy_url
          ) VALUES (?, ?, ?, ?, ?, ?, ?)`);
			for (const session of legacy.sessions) {
				const values = [
					session.id,
					normalizeSqliteInteger(session.started_at),
					normalizeSqliteInteger(session.ended_at),
					session.mode,
					session.source_scope,
					session.source_process,
					session.proxy_url
				];
				const existing = selectSession.get(session.id);
				if (existing) {
					const expected = {
						startedAt: values[1],
						endedAt: values[2],
						mode: values[3],
						sourceScope: values[4],
						sourceProcess: values[5],
						proxyUrl: values[6]
					};
					if (JSON.stringify(existing) !== JSON.stringify(expected)) throw new LegacyDebugProxySessionConflictError(session.id);
					continue;
				}
				insertSession.run(...values);
			}
			const existingEventCount = db.prepare(`SELECT COUNT(*) AS count
           FROM capture_events
           WHERE session_id IS ? AND ts IS ? AND source_scope IS ? AND source_process IS ?
             AND protocol IS ? AND direction IS ? AND kind IS ? AND flow_id IS ?
             AND method IS ? AND host IS ? AND path IS ? AND status IS ? AND close_code IS ?
             AND content_type IS ? AND headers_json IS ? AND data_text IS ? AND data_blob_id IS ?
             AND data_sha256 IS ? AND error_text IS ? AND meta_json IS ?
          `);
			const insertEvent = db.prepare(`INSERT INTO capture_events (
            session_id, ts, source_scope, source_process, protocol, direction, kind, flow_id,
            method, host, path, status, close_code, content_type, headers_json, data_text,
            data_blob_id, data_sha256, error_text, meta_json
          ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`);
			const existingCounts = /* @__PURE__ */ new Map();
			const seenCounts = /* @__PURE__ */ new Map();
			for (const event of legacy.events) {
				const values = eventValues(event);
				const key = eventKey(values);
				const seenCount = (seenCounts.get(key) ?? 0) + 1;
				seenCounts.set(key, seenCount);
				let existingCount = existingCounts.get(key);
				if (existingCount === void 0) {
					const row = existingEventCount.get(...values);
					existingCount = Number(row?.count ?? 0);
					existingCounts.set(key, existingCount);
				}
				if (seenCount > existingCount) insertEvent.run(...values);
			}
		}, { env: {
			...process.env,
			OPENCLAW_STATE_DIR: params.stateDir
		} });
		changes.push(`Migrated ${legacy.sessions.length} debug proxy capture ${legacy.sessions.length === 1 ? "session" : "sessions"}, ${legacy.events.length} ${legacy.events.length === 1 ? "event" : "events"}, and ${legacy.blobs.length} ${legacy.blobs.length === 1 ? "blob" : "blobs"} → shared SQLite state`);
	} catch (err) {
		const detail = err instanceof LegacyDebugProxyBlobConflictError ? `blob ${err.blobId} already exists with different data` : err instanceof LegacyDebugProxySessionConflictError ? `session ${err.sessionId} already exists with different data` : String(err);
		return {
			changes,
			warnings: [`Failed migrating debug proxy capture sidecar ${detected.sourcePath}: ${detail}`]
		};
	}
	archiveLegacyDebugProxySqlite({
		sourcePath: detected.sourcePath,
		changes,
		warnings
	});
	if (!fileExists(detected.sourcePath) && fileExists(`${detected.sourcePath}.migrated`)) {
		archiveLegacyDebugProxyBlobs({
			blobDir: detected.blobDir,
			changes,
			warnings
		});
		for (const blobDir of legacy.blobDirs) {
			if (path.resolve(blobDir) === path.resolve(detected.blobDir) || !dirExists(blobDir)) continue;
			warnings.push(`Left migrated debug proxy capture blobs in stored session directory: ${blobDir}`);
		}
	}
	return {
		changes,
		warnings
	};
}
//#endregion
//#region src/infra/state-migrations.session-surfaces.ts
function isSurfaceGroupKey(key) {
	return key.includes(":group:") || key.includes(":channel:");
}
function isLegacyGroupKey(key, surfaces = []) {
	const trimmed = key.trim();
	if (!trimmed) return false;
	const lower = normalizeLowercaseStringOrEmpty(trimmed);
	if (lower.startsWith("group:") || lower.startsWith("channel:")) return true;
	for (const surface of surfaces) if (surface.isLegacyGroupSessionKey?.(trimmed)) return true;
	return false;
}
//#endregion
//#region src/infra/state-migrations.session-store.ts
function isLegacyDefaultMainAliasKey(key, mainKey) {
	const lower = normalizeLowercaseStringOrEmpty(key.trim());
	const canonicalMainKey = normalizeMainKey(mainKey);
	return lower === `agent:main:main` || lower === `agent:main:${canonicalMainKey}`;
}
function resolveCanonicalAgentSessionOwner(key) {
	const parsed = parseAgentSessionKey(key);
	if (parsed === null || !isValidAgentId(parsed.agentId) || normalizeAgentId(parsed.agentId) !== parsed.agentId) return;
	return parsed.agentId;
}
function canonicalizeSessionKeyForAgent(params) {
	const raw = params.key.trim();
	if (!raw) return raw;
	const rawLower = normalizeLowercaseStringOrEmpty(raw);
	const legacyDefaultMainAlias = isLegacyDefaultMainAliasKey(rawLower, params.mainKey);
	const configuredAgentId = normalizeAgentId(params.agentId);
	const canonicalRowOwner = resolveCanonicalAgentSessionOwner(raw);
	const candidateOwner = params.preserveCanonicalAgentOwner ? canonicalRowOwner : void 0;
	const agentId = (candidateOwner === "main" && configuredAgentId !== "main" && legacyDefaultMainAlias ? void 0 : candidateOwner) ?? configuredAgentId;
	const normalized = normalizeSessionKeyPreservingOpaquePeerIds(raw);
	if (rawLower === "global" || rawLower === "unknown") return rawLower;
	if (params.preserveForeignMainAliases && legacyDefaultMainAlias) return params.key;
	const canonicalMain = canonicalizeMainSessionAlias({
		cfg: { session: {
			scope: params.scope,
			mainKey: params.mainKey
		} },
		agentId,
		sessionKey: normalized
	});
	if (params.scope === "global" && canonicalMain === "global") return canonicalMain;
	if (params.preserveAmbiguousKeys && (!canonicalRowOwner || legacyDefaultMainAlias)) return params.key;
	if (params.skipCrossAgentRemap) {
		const parsed = parseAgentSessionKey(raw);
		if (parsed && normalizeAgentId(parsed.agentId) !== agentId) return normalized;
		if (agentId !== "main" && (rawLower === "main" || rawLower === params.mainKey)) return rawLower;
	}
	if (canonicalMain !== normalized) return normalizeLowercaseStringOrEmpty(canonicalMain);
	const defaultPrefix = `agent:${LEGACY_IMPLICIT_AGENT_ID}:`;
	if (rawLower.startsWith(defaultPrefix) && agentId !== "main" && !params.skipCrossAgentRemap) {
		const rest = rawLower.slice(defaultPrefix.length);
		if (rest === "main" || rest === params.mainKey) {
			const remapped = `agent:${agentId}:${rest}`;
			const canonicalized = canonicalizeMainSessionAlias({
				cfg: { session: {
					scope: params.scope,
					mainKey: params.mainKey
				} },
				agentId,
				sessionKey: remapped
			});
			return normalizeLowercaseStringOrEmpty(canonicalized);
		}
	}
	if (rawLower.startsWith("agent:") && canonicalRowOwner) return normalized;
	if (rawLower.startsWith("subagent:")) {
		const rest = raw.slice(9);
		return normalizeLowercaseStringOrEmpty(`agent:${agentId}:subagent:${rest}`);
	}
	for (const surface of params.legacySessionSurfaces ?? []) {
		const canonicalized = surface.canonicalizeLegacySessionKey?.({
			key: raw,
			agentId
		});
		const normalizedCanonicalized = normalizeSessionKeyPreservingOpaquePeerIds(canonicalized);
		if (normalizedCanonicalized) return normalizedCanonicalized;
	}
	if (rawLower.startsWith("group:") || rawLower.startsWith("channel:")) return normalizeLowercaseStringOrEmpty(`agent:${agentId}:unknown:${raw}`);
	if (isSurfaceGroupKey(raw)) return `agent:${agentId}:${normalized}`;
	return normalizeSessionKeyPreservingOpaquePeerIds(`agent:${agentId}:${raw}`);
}
function pickLatestLegacyDirectEntry(store, legacySessionSurfaces = []) {
	let best = null;
	let bestUpdated = -1;
	for (const [key, entry] of Object.entries(store)) {
		if (!entry || typeof entry !== "object") continue;
		const normalized = key.trim();
		if (!normalized) continue;
		const normalizedLower = normalizeLowercaseStringOrEmpty(normalized);
		if (normalizedLower === "global") continue;
		if (normalizedLower.startsWith("agent:")) continue;
		if (normalizedLower.startsWith("subagent:")) continue;
		if (isLegacyGroupKey(normalized, legacySessionSurfaces) || isSurfaceGroupKey(normalized)) continue;
		const updatedAt = typeof entry.updatedAt === "number" ? entry.updatedAt : 0;
		if (updatedAt > bestUpdated) {
			bestUpdated = updatedAt;
			best = entry;
		}
	}
	return best;
}
function normalizeSessionEntry(entry, sessionKey) {
	const { room, ...entryWithoutRoom } = entry;
	const shaped = normalizePersistedSessionEntryShape(entryWithoutRoom, { sessionKey });
	if (!shaped) return null;
	const normalized = { ...shaped };
	if (typeof normalized.sessionId === "string") normalized.updatedAt = typeof normalized.updatedAt === "number" && Number.isFinite(normalized.updatedAt) ? normalized.updatedAt : Date.now();
	if (typeof normalized.groupChannel !== "string" && typeof room === "string") normalized.groupChannel = room;
	return normalized;
}
function resolveUpdatedAt(entry) {
	return typeof entry.updatedAt === "number" && Number.isFinite(entry.updatedAt) ? entry.updatedAt : 0;
}
function selectNewerSessionEntry(params) {
	if (!params.existing) return params.incoming;
	const existingUpdated = resolveUpdatedAt(params.existing);
	const incomingUpdated = resolveUpdatedAt(params.incoming);
	if (incomingUpdated > existingUpdated) return params.incoming;
	if (incomingUpdated < existingUpdated) return params.existing;
	return params.preferIncomingOnTie ? params.incoming : params.existing;
}
function canonicalizeSessionStore(params) {
	const canonical = Object.create(null);
	const meta = /* @__PURE__ */ new Map();
	const legacyKeys = [];
	for (const [key, entry] of Object.entries(params.store)) {
		if (!entry || typeof entry !== "object") continue;
		const canonicalKey = canonicalizeSessionKeyForAgent({
			key,
			agentId: params.agentId,
			mainKey: params.mainKey,
			scope: params.scope,
			skipCrossAgentRemap: params.skipCrossAgentRemap,
			preserveCanonicalAgentOwner: params.preserveCanonicalAgentOwner,
			preserveAmbiguousKeys: params.preserveAmbiguousKeys,
			preserveForeignMainAliases: params.preserveForeignMainAliases,
			legacySessionSurfaces: params.legacySessionSurfaces
		});
		const isCanonical = canonicalKey === key;
		if (!isCanonical) legacyKeys.push(key);
		const existing = canonical[canonicalKey];
		if (!existing) {
			canonical[canonicalKey] = entry;
			meta.set(canonicalKey, {
				isCanonical,
				updatedAt: resolveUpdatedAt(entry)
			});
			continue;
		}
		const existingMeta = meta.get(canonicalKey);
		const incomingUpdated = resolveUpdatedAt(entry);
		const existingUpdated = existingMeta?.updatedAt ?? resolveUpdatedAt(existing);
		if (incomingUpdated > existingUpdated) {
			canonical[canonicalKey] = entry;
			meta.set(canonicalKey, {
				isCanonical,
				updatedAt: incomingUpdated
			});
			continue;
		}
		if (incomingUpdated < existingUpdated) continue;
		if (existingMeta?.isCanonical && !isCanonical) continue;
		if (!existingMeta?.isCanonical && isCanonical) {
			canonical[canonicalKey] = entry;
			meta.set(canonicalKey, {
				isCanonical,
				updatedAt: incomingUpdated
			});
			continue;
		}
	}
	return {
		store: canonical,
		legacyKeys
	};
}
function isAmbiguousSharedStoreKey(key, mainKey, scope) {
	const raw = key.trim();
	const lower = normalizeLowercaseStringOrEmpty(raw);
	if (!raw || lower === "global" || lower === "unknown") return false;
	if (scope === "global" && canonicalizeMainSessionAlias({
		cfg: { session: {
			scope,
			mainKey
		} },
		agentId: "main",
		sessionKey: lower
	}) === "global") return false;
	return !resolveCanonicalAgentSessionOwner(raw) || isLegacyDefaultMainAliasKey(lower, mainKey);
}
function aliasedSessionStoreMigrationWarning(params) {
	return `Deferred ${params.subject} ${params.count} ambiguous session key(s) in aliased store ${params.storePath}; remove filesystem aliases or configure one canonical session.store path, then rerun openclaw doctor --fix`;
}
function unresolvedSessionStoreIdentityWarning(subject, storePath) {
	return `Deferred ${subject} for ${storePath}; filesystem identity could not be established for every configured store path. Restore path access or configure one canonical session.store path, then rerun openclaw doctor --fix`;
}
function distinctSessionStoreAliasWarning(subject, storePath) {
	return `Deferred ${subject} in aliased store ${storePath}; atomic replacement cannot update distinct filesystem aliases as one operation. Remove filesystem aliases or configure one canonical session.store path, then rerun openclaw doctor --fix`;
}
function resolveStaleLegacySessionFile(params) {
	if (!params.entry || typeof params.entry !== "object" || Array.isArray(params.entry)) return;
	const entry = params.entry;
	const rawSessionFile = entry.sessionFile;
	if (typeof rawSessionFile !== "string") return;
	const legacySessionFile = path.isAbsolute(rawSessionFile) ? path.resolve(rawSessionFile) : path.resolve(params.legacyDir, rawSessionFile);
	const relative = path.relative(path.resolve(params.legacyDir), legacySessionFile);
	if (relative.startsWith("..") || path.isAbsolute(relative) || migrationFileExists(legacySessionFile)) return;
	if (safeReadDir(path.dirname(params.legacyDir)).some((dirent) => dirent.isDirectory() && dirent.name.startsWith(`${path.basename(params.legacyDir)}.legacy-`) && migrationFileExists(path.join(path.dirname(params.legacyDir), dirent.name, path.basename(legacySessionFile))))) return;
	const parsed = path.parse(path.basename(legacySessionFile));
	if (safeReadDir(params.targetDir).some((dirent) => dirent.isFile() && dirent.name.startsWith(`${parsed.name}.legacy-`) && dirent.name.endsWith(parsed.ext))) return;
	const targetSessionFile = path.join(params.targetDir, path.basename(legacySessionFile));
	if (!migrationFileExists(targetSessionFile) || typeof entry.sessionId !== "string") return;
	const readFirstLine = () => {
		const fd = fs.openSync(targetSessionFile, "r");
		try {
			const buffer = Buffer.alloc(8192);
			const bytesRead = readFileWindowFullySync(fd, buffer, 0);
			if (bytesRead <= 0) return;
			const chunk = buffer.subarray(0, bytesRead).toString("utf8");
			const newline = chunk.indexOf("\n");
			return newline >= 0 ? chunk.slice(0, newline) : chunk;
		} finally {
			fs.closeSync(fd);
		}
	};
	try {
		const firstLine = readFirstLine();
		const header = firstLine ? JSON.parse(firstLine) : void 0;
		if (!header || typeof header !== "object" || Array.isArray(header)) return;
		if (header.type === "session") return header.id === entry.sessionId ? targetSessionFile : void 0;
		return (path.basename(entry.sessionId) === entry.sessionId ? `${entry.sessionId}.jsonl` : void 0) === path.basename(targetSessionFile) ? targetSessionFile : void 0;
	} catch {
		return;
	}
}
function sessionStoreMayNeedCanonicalization(params) {
	const storeAgentIds = new Set([...params.storeAgentIds].map((id) => normalizeAgentId(id)));
	const hasNonMainAgent = [...storeAgentIds].some((id) => id !== LEGACY_IMPLICIT_AGENT_ID);
	for (const key of Object.keys(params.store)) {
		const rawKey = key.trim();
		if (rawKey !== key) return true;
		if (!rawKey) continue;
		const lowerKey = normalizeLowercaseStringOrEmpty(rawKey);
		if (lowerKey !== rawKey) return true;
		if (lowerKey === "global" || lowerKey === "unknown") continue;
		if (params.preserveForeignMainAliases && isLegacyDefaultMainAliasKey(lowerKey, params.mainKey)) return true;
		if (lowerKey === "main" || lowerKey === params.mainKey) return true;
		if (lowerKey.startsWith("subagent:")) return true;
		if (lowerKey.startsWith("group:") || lowerKey.startsWith("channel:")) return true;
		if (!lowerKey.startsWith("agent:")) return true;
		const rowOwner = resolveCanonicalAgentSessionOwner(rawKey);
		if (!rowOwner) return true;
		const agentMainAlias = `agent:${rowOwner}:${DEFAULT_MAIN_KEY}`;
		const agentMainKey = `agent:${rowOwner}:${params.mainKey}`;
		if (lowerKey === agentMainAlias && (params.mainKey !== "main" || params.scope === "global")) return true;
		if (lowerKey === agentMainKey && params.scope === "global") return true;
		if (lowerKey === `agent:main:main` && (params.mainKey !== "main" || hasNonMainAgent || params.scope === "global")) return true;
		if (lowerKey === `agent:main:${params.mainKey}` && hasNonMainAgent && !storeAgentIds.has("main")) return true;
	}
	return false;
}
function listLegacySessionKeys(params) {
	const legacy = [];
	for (const key of Object.keys(params.store)) if (canonicalizeSessionKeyForAgent({
		key,
		agentId: params.agentId,
		mainKey: params.mainKey,
		scope: params.scope,
		skipCrossAgentRemap: params.preserveAmbiguousKeys,
		preserveCanonicalAgentOwner: params.preserveAmbiguousKeys,
		preserveAmbiguousKeys: params.preserveAmbiguousKeys,
		preserveForeignMainAliases: params.preserveForeignMainAliases,
		legacySessionSurfaces: params.legacySessionSurfaces
	}) !== key) legacy.push(key);
	return legacy;
}
function emptyDirOrMissing(dir) {
	if (!existsDir(dir)) return true;
	return safeReadDir(dir).length === 0;
}
function removeDirIfEmpty(dir) {
	if (!existsDir(dir)) return;
	if (!emptyDirOrMissing(dir)) return;
	try {
		fs.rmdirSync(dir);
	} catch {}
}
async function migrateOrphanedSessionKeys(params) {
	const changes = [];
	const warnings = [];
	const env = params.env ?? process.env;
	let preparedLegacySessionSurfaces;
	const resolveLegacySessionSurfaces = () => preparedLegacySessionSurfaces ??= typeof params.legacySessionSurfaces === "function" ? params.legacySessionSurfaces() : params.legacySessionSurfaces;
	const stateDir = resolveStateDir(env);
	const mainKey = normalizeMainKey(params.cfg.session?.mainKey);
	const scope = params.cfg.session?.scope;
	const storeConfig = params.cfg.session?.store;
	const persistedStoreOwner = resolvePersistedSessionStoreOwner(params.cfg);
	const persistedStoreAgentId = persistedStoreOwner.kind === "configured" ? persistedStoreOwner.agentId : void 0;
	const persistedStorePath = persistedStoreAgentId && storeConfig ? resolveStorePathFromTemplate(storeConfig, persistedStoreAgentId, env) : void 0;
	const pluginAgentIds = params.additionalAgentIds ?? listPluginDoctorSessionStoreAgentIds({
		config: params.cfg,
		env,
		pluginIds: collectRelevantDoctorPluginIds(params.cfg)
	});
	const pluginAgentIdSet = new Set(pluginAgentIds.map((id) => normalizeAgentId(id)));
	const storeMap = /* @__PURE__ */ new Map();
	const storeAliasCandidates = /* @__PURE__ */ new Map();
	const addToStoreMap = (p, id) => {
		try {
			if (p.endsWith(".sqlite") || !fs.statSync(p, { throwIfNoEntry: false })?.isFile()) return;
		} catch {}
		const ownerId = persistedStoreAgentId && persistedStorePath && sessionStorePathsMatch(p, persistedStorePath) ? persistedStoreAgentId : id;
		const storePath = [...storeMap.keys()].find((candidate) => sessionStorePathsMatch(candidate, p)) ?? p;
		const aliasCandidates = storeAliasCandidates.get(storePath) ?? /* @__PURE__ */ new Set([storePath]);
		aliasCandidates.add(p);
		storeAliasCandidates.set(storePath, aliasCandidates);
		storeMap.set(storePath, (storeMap.get(storePath) ?? /* @__PURE__ */ new Set()).add(ownerId));
	};
	for (const configuredAgentId of listConfiguredSessionStoreAgentIds(params.cfg)) {
		const id = normalizeAgentId(configuredAgentId);
		addToStoreMap(storeConfig ? resolveStorePathFromTemplate(storeConfig, id, env) : path.join(stateDir, "agents", id, "sessions", "sessions.json"), id);
	}
	for (const pluginAgentId of pluginAgentIds) {
		const id = normalizeAgentId(pluginAgentId);
		addToStoreMap(storeConfig ? resolveStorePathFromTemplate(storeConfig, id, env) : path.join(stateDir, "agents", id, "sessions", "sessions.json"), id);
	}
	const agentsDir = path.join(stateDir, "agents");
	if (existsDir(agentsDir)) {
		for (const dirEntry of safeReadDir(agentsDir)) if (dirEntry.isDirectory()) {
			const diskAgentId = normalizeAgentId(dirEntry.name);
			if (diskAgentId) addToStoreMap(path.join(agentsDir, diskAgentId, "sessions", "sessions.json"), diskAgentId);
		}
	}
	for (const [mappedStorePath, storeAgentIds] of storeMap) {
		const storePaths = storeAliasCandidates.get(mappedStorePath) ?? /* @__PURE__ */ new Set([mappedStorePath]);
		const storePath = [...storePaths].find((candidate) => migrationFileExists(candidate));
		if (!storePath) continue;
		const pluginForeignMainAliasRisk = [...storeAgentIds].some((id) => pluginAgentIdSet.has(id) && id !== "main");
		let parsed;
		try {
			parsed = parseSessionStoreJson5(fs.readFileSync(storePath, "utf-8"));
		} catch (err) {
			warnings.push(`Could not read ${storePath}: ${String(err)}`);
			continue;
		}
		if (!parsed.ok || !sessionStoreMayNeedCanonicalization({
			store: parsed.store,
			storeAgentIds,
			mainKey,
			scope,
			preserveForeignMainAliases: pluginForeignMainAliasRisk
		})) continue;
		const legacySessionSurfaces = resolveLegacySessionSurfaces();
		if (legacySessionSurfaces.failures.length > 0) return {
			changes,
			warnings: [...warnings, ...legacySessionSurfaces.failures]
		};
		let working = parsed.store;
		let totalLegacy = 0;
		const storeAliases = resolveSessionStoreAliasPlan(storePath, storePaths);
		const hasDistinctAliases = storeAliases.hasDistinctAliases;
		const preserveAmbiguousKeys = storeAgentIds.size > 1;
		const preservedAmbiguousKeyCount = Object.keys(working).filter((key) => preserveAmbiguousKeys && isAmbiguousSharedStoreKey(key, mainKey, scope) || pluginForeignMainAliasRisk && isLegacyDefaultMainAliasKey(key, mainKey)).length;
		if (storeAliases.hasUnresolvedIdentity) {
			warnings.push(unresolvedSessionStoreIdentityWarning("session key migration", storePath));
			continue;
		}
		if (hasDistinctAliases && preservedAmbiguousKeyCount > 0) {
			warnings.push(aliasedSessionStoreMigrationWarning({
				subject: "migration of",
				count: preservedAmbiguousKeyCount,
				storePath
			}));
			continue;
		}
		if (storeAliases.hasFinalSymlink) {
			warnings.push(`Deferred session key migration in final-component symlink store ${storePath}; configure one canonical session.store path, then rerun openclaw doctor --fix`);
			continue;
		}
		if (hasDistinctAliases) {
			warnings.push(distinctSessionStoreAliasWarning("session key migration", storePath));
			continue;
		}
		for (const storeAgentId of storeAgentIds) {
			const { store: canonicalized, legacyKeys } = canonicalizeSessionStore({
				store: working,
				agentId: storeAgentId,
				mainKey,
				scope,
				skipCrossAgentRemap: preserveAmbiguousKeys,
				preserveCanonicalAgentOwner: true,
				preserveAmbiguousKeys,
				preserveForeignMainAliases: pluginForeignMainAliasRisk,
				legacySessionSurfaces: legacySessionSurfaces.surfaces
			});
			working = canonicalized;
			totalLegacy += legacyKeys.length;
		}
		if (preservedAmbiguousKeyCount > 0) warnings.push(`Preserved ${preservedAmbiguousKeyCount} ambiguous session key(s) in potentially shared store ${storePath}`);
		if (totalLegacy === 0) continue;
		const normalized = Object.create(null);
		for (const [key, entry] of Object.entries(working)) {
			const ne = normalizeSessionEntry(entry, key);
			if (ne) normalized[key] = ne;
		}
		try {
			await saveSessionStoreStrict(storePath, normalized);
			changes.push(`Canonicalized ${totalLegacy} orphaned session key(s) in ${storePath}`);
		} catch (err) {
			warnings.push(`Failed to write canonicalized store ${storePath}: ${String(err)}`);
		}
	}
	return {
		changes,
		warnings
	};
}
async function migrateLegacyAcpSessionMetadata(params) {
	const changes = [];
	const warnings = [];
	const env = params.env ?? process.env;
	if (params.legacySessionSurfaces.failures.length > 0) return {
		changes,
		warnings: [...params.legacySessionSurfaces.failures]
	};
	const now = params.now ?? (() => Date.now());
	const stateDir = resolveStateDir(env);
	const storeConfig = params.cfg.session?.store;
	const pluginAgentIds = params.pluginSessionStoreAgentIds ?? listPluginDoctorSessionStoreAgentIds({
		config: params.cfg,
		env,
		pluginIds: collectRelevantDoctorPluginIds(params.cfg)
	});
	const normalizedPluginAgentIds = new Set(pluginAgentIds.map((id) => normalizeAgentId(id)));
	const declaredAgentIds = /* @__PURE__ */ new Set([...listConfiguredSessionStoreAgentIds(params.cfg).map((id) => normalizeAgentId(id)), ...normalizedPluginAgentIds]);
	const declaredTargets = [...declaredAgentIds].map((agentId) => ({
		agentId,
		storePath: storeConfig ? resolveStorePathFromTemplate(storeConfig, agentId, env) : path.join(stateDir, "agents", agentId, "sessions", "sessions.json")
	}));
	const pluginTargets = declaredTargets.filter(({ agentId }) => agentId !== "main" && normalizedPluginAgentIds.has(agentId));
	const configuredAgents = listAgentEntries(params.cfg);
	const configuredAgentIds = new Set(configuredAgents.flatMap((entry) => entry?.id ? [normalizeAgentId(entry.id)] : []));
	const targets = resolveLegacyAcpMetadataSessionStoreTargets([...declaredAgentIds].some((agentId) => !configuredAgentIds.has(agentId)) ? {
		...params.cfg,
		agents: {
			...params.cfg.agents,
			list: [...configuredAgents, ...[...declaredAgentIds].filter((agentId) => !configuredAgentIds.has(agentId)).map((id) => ({ id }))]
		}
	} : params.cfg, env);
	const mainKey = normalizeMainKey(params.cfg.session?.mainKey);
	const scope = params.cfg.session?.scope;
	const storeGroups = [];
	for (const target of targets) {
		if (!migrationFileExists(target.storePath)) continue;
		const group = storeGroups.find(({ target: existing }) => sessionStorePathsMatch(existing.storePath, target.storePath));
		const matchingDeclaredTargets = declaredTargets.filter((declaredTarget) => sessionStorePathsMatch(target.storePath, declaredTarget.storePath));
		if (group) {
			group.agentIds.add(normalizeAgentId(target.agentId));
			group.aliasCandidates.add(target.storePath);
			for (const declaredTarget of matchingDeclaredTargets) {
				group.agentIds.add(declaredTarget.agentId);
				group.aliasCandidates.add(declaredTarget.storePath);
			}
			continue;
		}
		storeGroups.push({
			target,
			agentIds: /* @__PURE__ */ new Set([normalizeAgentId(target.agentId), ...matchingDeclaredTargets.map((declaredTarget) => declaredTarget.agentId)]),
			aliasCandidates: /* @__PURE__ */ new Set([target.storePath, ...matchingDeclaredTargets.map((declaredTarget) => declaredTarget.storePath)])
		});
	}
	for (const { target, agentIds, aliasCandidates } of storeGroups) {
		const storePath = target.storePath;
		const storeAliases = resolveSessionStoreAliasPlan(storePath, aliasCandidates);
		const pluginForeignMainAliasRisk = pluginTargets.some((pluginTarget) => sessionStorePathsMatch(storePath, pluginTarget.storePath));
		let parsed;
		try {
			parsed = readSessionStoreJson5(storePath);
		} catch (err) {
			warnings.push(`Could not read ${storePath}: ${String(err)}`);
			continue;
		}
		if (!parsed.ok) continue;
		const ambiguousKeyCount = Object.keys(parsed.store).filter((key) => isAmbiguousSharedStoreKey(key, mainKey, scope) || pluginForeignMainAliasRisk && isLegacyDefaultMainAliasKey(key, mainKey)).length;
		const hasLegacyAcpMetadata = Object.entries(parsed.store).some(([sessionKey, entry]) => normalizeSessionEntry(entry, sessionKey)?.acp !== void 0);
		if (hasLegacyAcpMetadata && storeAliases.hasUnresolvedIdentity) {
			warnings.push(unresolvedSessionStoreIdentityWarning("ACP metadata migration", storePath));
			continue;
		}
		if (hasLegacyAcpMetadata && storeAliases.hasFinalSymlink) {
			warnings.push(`Deferred ACP metadata migration in final-component symlink store ${storePath}; configure one canonical session.store path, then rerun openclaw doctor --fix`);
			continue;
		}
		if (hasLegacyAcpMetadata && storeAliases.hasDistinctAliases) {
			warnings.push(ambiguousKeyCount > 0 ? aliasedSessionStoreMigrationWarning({
				subject: "ACP metadata migration for",
				count: ambiguousKeyCount,
				storePath
			}) : distinctSessionStoreAliasWarning("ACP metadata migration", storePath));
			continue;
		}
		const normalized = Object.create(null);
		let migrated = 0;
		let preserved = 0;
		for (const [sessionKey, entry] of Object.entries(parsed.store)) {
			const normalizedEntry = normalizeSessionEntry(entry, sessionKey);
			if (!normalizedEntry) continue;
			if (normalizedEntry.acp) {
				const ambiguousSharedStoreKey = isAmbiguousSharedStoreKey(sessionKey, mainKey, scope);
				const ambiguousMultiOwnerKey = agentIds.size > 1 && ambiguousSharedStoreKey;
				const foreignMainAlias = pluginForeignMainAliasRisk && isLegacyDefaultMainAliasKey(sessionKey, mainKey);
				if (ambiguousMultiOwnerKey || foreignMainAlias) {
					preserved++;
					normalized[sessionKey] = normalizedEntry;
					continue;
				}
				const canonicalSessionKey = canonicalizeSessionKeyForAgent({
					key: sessionKey,
					agentId: resolveCanonicalAgentSessionOwner(sessionKey) ?? target.agentId,
					mainKey,
					scope,
					skipCrossAgentRemap: true,
					legacySessionSurfaces: params.legacySessionSurfaces.surfaces
				});
				writeAcpSessionMetaForMigration({
					sessionKey: canonicalSessionKey,
					sessionId: normalizedEntry.sessionId,
					lifecycleRevision: normalizedEntry.lifecycleRevision,
					meta: normalizedEntry.acp,
					env,
					now
				});
				delete normalizedEntry.acp;
				migrated++;
			}
			normalized[sessionKey] = normalizedEntry;
		}
		if (preserved > 0) warnings.push(`Preserved ACP metadata for ${preserved} ambiguous session key(s) in potentially shared store ${storePath}`);
		if (migrated === 0) continue;
		try {
			await saveSessionStoreStrict(storePath, normalized);
			changes.push(`Migrated ${migrated} ACP session metadata ${migrated === 1 ? "row" : "rows"} → shared SQLite state`);
		} catch (err) {
			warnings.push(`Failed to write ACP metadata migration source ${storePath}: ${String(err)}`);
		}
	}
	return {
		changes,
		warnings
	};
}
function resolveLegacyAcpMetadataSessionStoreTargets(cfg, env) {
	const stateDir = resolveStateDir(env);
	const agentsDirs = /* @__PURE__ */ new Set([path.join(stateDir, "agents")]);
	const targets = /* @__PURE__ */ new Map();
	const addTarget = (agentId, storePath) => {
		if (storePath.endsWith(".sqlite") || !isManagedLegacySessionStorePathSafe(storePath)) return;
		const agentsDir = resolveAgentsDirFromSessionStorePath(storePath);
		if (agentsDir) agentsDirs.add(agentsDir);
		if (!targets.has(storePath)) targets.set(storePath, {
			agentId,
			storePath
		});
	};
	for (const target of resolveAllAgentSessionStoreTargetsSync(cfg, { env })) addTarget(target.agentId, target.storePath);
	for (const target of resolveSessionStoreTargets(cfg, { allAgents: true }, { env })) addTarget(target.agentId, target.storePath);
	for (const agentsDir of agentsDirs) {
		if (!existsDir(agentsDir)) continue;
		for (const entry of safeReadDir(agentsDir)) {
			if (!entry.isDirectory()) continue;
			const agentId = normalizeAgentId(entry.name);
			const normalizedDirName = normalizeLowercaseStringOrEmpty(entry.name);
			if (agentId === "main" && normalizedDirName !== agentId) continue;
			addTarget(agentId, path.join(agentsDir, entry.name, "sessions", "sessions.json"));
		}
	}
	return [...targets.values()];
}
function isManagedLegacySessionStorePathSafe(storePath) {
	const resolvedStorePath = path.resolve(storePath);
	const agentsDir = resolveAgentsDirFromSessionStorePath(resolvedStorePath);
	if (!agentsDir) return true;
	if (!migrationFileExists(resolvedStorePath)) return true;
	try {
		const stat = fs.lstatSync(resolvedStorePath);
		if (stat.isSymbolicLink() || !stat.isFile()) return false;
		const resolvedAgentsDir = path.resolve(agentsDir);
		const realStorePath = fs.realpathSync.native(resolvedStorePath);
		const realAgentsDir = fs.realpathSync.native(resolvedAgentsDir);
		return isWithinDir(realAgentsDir, realStorePath);
	} catch {
		return false;
	}
}
function resolveStorePathFromTemplate(template, agentId, env) {
	const expand = (s) => s.startsWith("~") ? expandHomePrefix(s, {
		env: env ?? process.env,
		homedir: os.homedir
	}) : s;
	if (template.includes("{agentId}")) return path.resolve(expand(template.replaceAll("{agentId}", agentId)));
	return path.resolve(expand(template));
}
function resolveSessionStorePathRelationship(left, right) {
	if (left === right) return "same";
	try {
		return sameFileIdentity(fs.statSync(left, { bigint: true }), fs.statSync(right, { bigint: true })) ? "same" : "different";
	} catch (err) {
		const code = err.code;
		if (code !== "ENOENT" && code !== "ENOTDIR") return "unknown";
		const resolvedLeft = resolvePathThroughExistingParents(left);
		const resolvedRight = resolvePathThroughExistingParents(right);
		if (resolvedLeft === void 0 || resolvedRight === void 0) return "unknown";
		return resolvedLeft === resolvedRight ? "same" : "different";
	}
}
function sessionStorePathsMatch(left, right) {
	return resolveSessionStorePathRelationship(left, right) !== "different";
}
function resolvePathThroughExistingParents(filePath) {
	const resolvedPath = path.resolve(filePath);
	const suffix = [path.basename(resolvedPath)];
	let parentPath = path.dirname(resolvedPath);
	while (true) try {
		return path.join(fs.realpathSync.native(parentPath), ...suffix);
	} catch (err) {
		const code = err.code;
		if (code !== "ENOENT" && code !== "ENOTDIR") return;
		const nextParent = path.dirname(parentPath);
		if (nextParent === parentPath) return;
		suffix.unshift(path.basename(parentPath));
		parentPath = nextParent;
	}
}
function sessionStorePathIsFinalSymlink(storePath) {
	try {
		return fs.lstatSync(storePath).isSymbolicLink();
	} catch {
		return false;
	}
}
function sessionStorePathsHaveDistinctEntries(left, right) {
	if (left === right) return false;
	try {
		if (fs.lstatSync(left).isSymbolicLink() || fs.lstatSync(right).isSymbolicLink()) return true;
		return fs.realpathSync.native(left) !== fs.realpathSync.native(right);
	} catch (err) {
		const code = err.code;
		if (code !== "ENOENT" && code !== "ENOTDIR") return true;
		const resolvedLeft = resolvePathThroughExistingParents(left);
		const resolvedRight = resolvePathThroughExistingParents(right);
		return resolvedLeft === void 0 || resolvedLeft !== resolvedRight;
	}
}
function resolveSessionStoreAliasPlan(storePath, candidatePaths) {
	let hasDistinctEntries = false;
	let hasFinalSymlink = sessionStorePathIsFinalSymlink(storePath);
	let hasUnresolvedIdentity = false;
	for (const candidatePath of candidatePaths) {
		const relationship = resolveSessionStorePathRelationship(storePath, candidatePath);
		if (relationship === "different") continue;
		if (relationship === "unknown") {
			hasUnresolvedIdentity = true;
			continue;
		}
		hasFinalSymlink ||= sessionStorePathIsFinalSymlink(candidatePath);
		if (sessionStorePathsHaveDistinctEntries(storePath, candidatePath)) hasDistinctEntries = true;
	}
	return {
		hasDistinctAliases: hasFinalSymlink || hasDistinctEntries || hasUnresolvedIdentity,
		hasFinalSymlink,
		hasUnresolvedIdentity
	};
}
function mergeSessionStoreAliasPlans(left, right) {
	if (!left) return right;
	return {
		hasDistinctAliases: left.hasDistinctAliases || right.hasDistinctAliases,
		hasFinalSymlink: left.hasFinalSymlink || right.hasFinalSymlink,
		hasUnresolvedIdentity: left.hasUnresolvedIdentity || right.hasUnresolvedIdentity
	};
}
async function saveSessionStoreStrict(storePath, store) {
	await saveLegacySessionStore(storePath, store, {
		requireWriteSuccess: true,
		skipMaintenance: true
	});
}
function resolveSessionStoreOwnership(params) {
	const targetStorePath = path.join(params.stateDir, "agents", params.targetAgentId, "sessions", "sessions.json");
	const configuredStore = params.cfg.session?.store;
	const resolveAgentStorePath = (agentId) => configuredStore ? resolveStorePathFromTemplate(configuredStore, agentId, params.env) : path.join(params.stateDir, "agents", agentId, "sessions", "sessions.json");
	const preserveForeignMainAliases = params.pluginSessionStoreAgentIds.some((pluginAgentId) => {
		const id = normalizeAgentId(pluginAgentId);
		if (id === "main") return false;
		return sessionStorePathsMatch(resolveAgentStorePath(id), targetStorePath);
	});
	const configuredOwnerStorePaths = [.../* @__PURE__ */ new Set([...listConfiguredSessionStoreAgentIds(params.cfg).map((id) => normalizeAgentId(id)), ...params.pluginSessionStoreAgentIds.map((id) => normalizeAgentId(id))])].map(resolveAgentStorePath);
	const preserveAmbiguousKeys = configuredOwnerStorePaths.filter((storePath) => sessionStorePathsMatch(storePath, targetStorePath)).length > 1;
	const candidateStorePaths = [...configuredOwnerStorePaths];
	const agentsDir = path.join(params.stateDir, "agents");
	for (const entry of safeReadDir(agentsDir)) if (entry.isDirectory()) candidateStorePaths.push(path.join(agentsDir, entry.name, "sessions", "sessions.json"));
	return {
		preserveAmbiguousKeys,
		preserveForeignMainAliases,
		targetStoreAliases: resolveSessionStoreAliasPlan(targetStorePath, candidateStorePaths)
	};
}
//#endregion
//#region src/infra/state-migrations.legacy-sessions.ts
const LEGACY_AGENT_DATABASE_BASENAME = "openclaw-agent.sqlite";
function legacyAgentInspectionFailure(subject, error) {
	return {
		status: "failed",
		warning: `Failed inspecting ${subject}: ${String(error)}`
	};
}
function inspectLegacyAgentDir(legacyDir) {
	let entries;
	try {
		entries = fs.readdirSync(legacyDir, { withFileTypes: true });
	} catch (error) {
		if (isErrno(error) && error.code === "ENOENT") return { status: "empty" };
		return legacyAgentInspectionFailure(`legacy agent directory ${legacyDir}`, error);
	}
	if (entries.length === 0) return { status: "empty" };
	const databasePath = path.join(legacyDir, LEGACY_AGENT_DATABASE_BASENAME);
	const databaseFiles = new Set(resolveSqliteDatabaseFilePaths(databasePath).map((pathname) => path.basename(pathname)));
	const hasFilePayload = entries.some((entry) => !databaseFiles.has(entry.name));
	if (!entries.some((entry) => databaseFiles.has(entry.name))) return { status: hasFilePayload ? "payload" : "empty" };
	if (!migrationFileExists(databasePath)) return legacyAgentInspectionFailure(`legacy agent database ${databasePath}`, "main database is missing or not a regular file");
	let database;
	try {
		const opened = openNodeSqliteDatabase(databasePath, { readOnly: true });
		database = opened;
		const schemaOwner = readExistingAgentSchemaMeta(opened);
		if (!schemaOwner || schemaOwner.role !== "agent") return legacyAgentInspectionFailure(`legacy agent database ${databasePath}`, "agent schema ownership metadata is missing");
		if (!schemaOwner.agentId) return legacyAgentInspectionFailure(`legacy agent database ${databasePath}`, "agent schema owner is missing or blank");
		return { status: opened.prepare(`SELECT name FROM pragma_table_list
         WHERE schema = 'main' AND type IN ('table', 'virtual')
           AND substr(name, 1, 7) <> 'sqlite_'
           AND name NOT IN ('schema_meta', 'session_key_contract', 'memory_index_state')`).all().flatMap((row) => row && typeof row === "object" && "name" in row && typeof row.name === "string" ? [row.name] : []).some((name) => opened.prepare(`SELECT 1 FROM ${quoteSqliteIdentifier(name)} LIMIT 1`).get()) || hasFilePayload ? "payload" : "empty" };
	} catch (error) {
		return legacyAgentInspectionFailure(`legacy agent database ${databasePath}`, error);
	} finally {
		database?.close();
	}
}
function normalizeMergedSessionStore(merged, protectedKeys) {
	const store = Object.create(null);
	let rejectedProtectedKeyCount = 0;
	for (const [key, entry] of Object.entries(merged)) {
		const normalizedEntry = normalizeSessionEntry(entry, key);
		if (!normalizedEntry) {
			if (protectedKeys.has(key)) rejectedProtectedKeyCount++;
			continue;
		}
		store[key] = normalizedEntry;
	}
	return {
		store,
		rejectedProtectedKeyCount
	};
}
async function migrateLegacySessions(detected, now, options) {
	const changes = [];
	const warnings = [];
	if (!detected.sessions.hasLegacy) return {
		changes,
		warnings
	};
	if (options.legacySessionSurfaces.failures.length > 0) return {
		changes,
		warnings: [...options.legacySessionSurfaces.failures]
	};
	ensureMigrationDir(detected.sessions.targetDir);
	const legacyParsed = migrationFileExists(detected.sessions.legacyStorePath) ? readSessionStoreJson5(detected.sessions.legacyStorePath) : {
		store: {},
		ok: true
	};
	const targetParsed = migrationFileExists(detected.sessions.targetStorePath) ? readSessionStoreJson5(detected.sessions.targetStorePath) : {
		store: {},
		ok: true
	};
	const legacyStore = legacyParsed.store;
	const targetStore = targetParsed.store;
	if (detected.sessions.targetStoreAliases.hasUnresolvedIdentity) {
		warnings.push(unresolvedSessionStoreIdentityWarning("legacy session migration", detected.sessions.targetStorePath));
		return {
			changes,
			warnings
		};
	}
	if (detected.sessions.targetStoreAliases.hasFinalSymlink) {
		warnings.push(`Deferred legacy session migration in final-component symlink store ${detected.sessions.targetStorePath}; configure one canonical session.store path, then rerun openclaw doctor --fix`);
		return {
			changes,
			warnings
		};
	}
	const ambiguousAliasedKeys = new Set([...Object.keys(targetStore), ...Object.keys(legacyStore)].filter((key) => isAmbiguousSharedStoreKey(key, detected.targetMainKey, detected.targetScope) || detected.sessions.preserveForeignMainAliases && isLegacyDefaultMainAliasKey(key, detected.targetMainKey)));
	if (detected.sessions.targetStoreAliases.hasDistinctAliases) {
		warnings.push(ambiguousAliasedKeys.size > 0 ? aliasedSessionStoreMigrationWarning({
			subject: "migration of",
			count: ambiguousAliasedKeys.size,
			storePath: detected.sessions.targetStorePath
		}) : distinctSessionStoreAliasWarning("legacy session migration", detected.sessions.targetStorePath));
		return {
			changes,
			warnings
		};
	}
	const canonicalizedTarget = canonicalizeSessionStore({
		store: targetStore,
		agentId: detected.targetAgentId,
		mainKey: detected.targetMainKey,
		scope: detected.targetScope,
		skipCrossAgentRemap: detected.sessions.preserveAmbiguousKeys,
		preserveCanonicalAgentOwner: true,
		preserveAmbiguousKeys: detected.sessions.preserveAmbiguousKeys,
		preserveForeignMainAliases: detected.sessions.preserveForeignMainAliases,
		legacySessionSurfaces: options.legacySessionSurfaces.surfaces
	});
	const canonicalizedLegacy = canonicalizeSessionStore({
		store: legacyStore,
		agentId: detected.targetAgentId,
		mainKey: detected.targetMainKey,
		scope: detected.targetScope,
		preserveCanonicalAgentOwner: true,
		preserveForeignMainAliases: detected.sessions.preserveForeignMainAliases,
		legacySessionSurfaces: options.legacySessionSurfaces.surfaces
	});
	const targetKeys = new Set(Object.keys(canonicalizedTarget.store));
	const preservedLegacyForeignMainAliasCount = detected.sessions.preserveForeignMainAliases ? Object.keys(legacyStore).filter((key) => isLegacyDefaultMainAliasKey(key, detected.targetMainKey)).length : 0;
	let repairedStaleSessionFiles = false;
	for (const entry of Object.values(canonicalizedTarget.store)) {
		const targetSessionFile = resolveStaleLegacySessionFile({
			entry,
			legacyDir: detected.sessions.legacyDir,
			targetDir: detected.sessions.targetDir
		});
		if (targetSessionFile) {
			entry.sessionFile = targetSessionFile;
			repairedStaleSessionFiles = true;
		}
	}
	const merged = Object.create(null);
	for (const [key, entry] of Object.entries(canonicalizedTarget.store)) merged[key] = entry;
	for (const [key, entry] of Object.entries(canonicalizedLegacy.store)) merged[key] = selectNewerSessionEntry({
		existing: merged[key],
		incoming: entry,
		preferIncomingOnTie: false
	});
	const mainKey = buildAgentMainSessionKey({
		agentId: detected.targetAgentId,
		mainKey: detected.targetMainKey
	});
	let migratedDirectChatKey;
	if (!merged[mainKey]) {
		const latest = pickLatestLegacyDirectEntry(legacyStore, options.legacySessionSurfaces.surfaces);
		if (latest?.sessionId) {
			merged[mainKey] = latest;
			migratedDirectChatKey = mainKey;
		}
	}
	if (!legacyParsed.ok) warnings.push(`Legacy sessions store unreadable; left in place at ${detected.sessions.legacyStorePath}`);
	let targetReadable = !migrationFileExists(detected.sessions.targetStorePath) || targetParsed.ok;
	if (!targetReadable) {
		if (options.recoverCorruptTargetStore) {
			const archivedTargetPath = `${detected.sessions.targetStorePath}.corrupt-${now()}`;
			try {
				fs.renameSync(detected.sessions.targetStorePath, archivedTargetPath);
				changes.push(`Archived corrupt target sessions store → ${archivedTargetPath}`);
				targetReadable = true;
			} catch (err) {
				warnings.push(`Target sessions store unreadable; failed to archive ${detected.sessions.targetStorePath}: ${String(err)}`);
			}
		} else warnings.push(`Target sessions store unreadable; left untouched to avoid overwriting at ${detected.sessions.targetStorePath}. Run openclaw doctor --fix to archive it and retry the legacy merge.`);
	}
	if (targetReadable && (legacyParsed.ok || targetParsed.ok) && (Object.keys(legacyStore).length > 0 || Object.keys(targetStore).length > 0)) {
		const normalized = normalizeMergedSessionStore(merged, targetKeys);
		if (normalized.rejectedProtectedKeyCount > 0) {
			warnings.push(`Refused legacy session migration because normalization rejected ${normalized.rejectedProtectedKeyCount} existing target session ${normalized.rejectedProtectedKeyCount === 1 ? "key" : "keys"}; left ${detected.sessions.targetStorePath} and ${detected.sessions.legacyStorePath} in place. Repair the conflicting rows, then rerun openclaw doctor --fix.`);
			return {
				changes,
				warnings
			};
		}
		await saveSessionStoreStrict(detected.sessions.targetStorePath, normalized.store);
		if (migratedDirectChatKey) changes.push(`Migrated latest direct-chat session → ${migratedDirectChatKey}`);
		changes.push(`Merged sessions store → ${detected.sessions.targetStorePath}`);
		if (preservedLegacyForeignMainAliasCount > 0) warnings.push(`Preserved ${preservedLegacyForeignMainAliasCount} ambiguous session key(s) while importing legacy sessions into ${detected.sessions.targetStorePath}`);
		if (canonicalizedTarget.legacyKeys.length > 0) changes.push(`Canonicalized ${canonicalizedTarget.legacyKeys.length} legacy session key(s)`);
		if (repairedStaleSessionFiles) changes.push("Repaired migrated session transcript paths");
	}
	if (!targetReadable) return {
		changes,
		warnings
	};
	const entries = safeReadDir(detected.sessions.legacyDir);
	for (const entry of entries) {
		if (!entry.isFile()) continue;
		if (entry.name === "sessions.json") continue;
		const from = path.join(detected.sessions.legacyDir, entry.name);
		let to = path.join(detected.sessions.targetDir, entry.name);
		if (migrationFileExists(to)) {
			const parsed = path.parse(entry.name);
			to = path.join(detected.sessions.targetDir, `${parsed.name}.legacy-${now()}${parsed.ext}`);
		}
		try {
			fs.renameSync(from, to);
			changes.push(`Moved ${entry.name} → agents/${detected.targetAgentId}/sessions`);
		} catch (err) {
			warnings.push(`Failed moving ${from}: ${String(err)}`);
		}
	}
	if (legacyParsed.ok && targetReadable) try {
		if (migrationFileExists(detected.sessions.legacyStorePath)) fs.rmSync(detected.sessions.legacyStorePath, { force: true });
	} catch {}
	removeDirIfEmpty(detected.sessions.legacyDir);
	if (safeReadDir(detected.sessions.legacyDir).filter((e) => e.isFile()).length > 0) {
		const backupDir = `${detected.sessions.legacyDir}.legacy-${now()}`;
		try {
			fs.renameSync(detected.sessions.legacyDir, backupDir);
			warnings.push(`Left legacy sessions at ${backupDir}`);
		} catch {}
	}
	return {
		changes,
		warnings
	};
}
async function migrateLegacyAgentDir(detected, now) {
	const changes = [];
	const warnings = [];
	if (!detected.agentDir.hasLegacy) return {
		changes,
		warnings
	};
	ensureMigrationDir(detected.agentDir.targetDir);
	const entries = safeReadDir(detected.agentDir.legacyDir);
	for (const entry of entries) {
		const from = path.join(detected.agentDir.legacyDir, entry.name);
		const to = path.join(detected.agentDir.targetDir, entry.name);
		if (fs.existsSync(to)) continue;
		try {
			fs.renameSync(from, to);
			changes.push(`Moved agent file ${entry.name} → agents/${detected.targetAgentId}/agent`);
		} catch (err) {
			warnings.push(`Failed moving ${from}: ${String(err)}`);
		}
	}
	removeDirIfEmpty(detected.agentDir.legacyDir);
	if (!emptyDirOrMissing(detected.agentDir.legacyDir)) {
		const backupDir = path.join(detected.stateDir, "agents", detected.targetAgentId, `agent.legacy-${now()}`);
		try {
			fs.renameSync(detected.agentDir.legacyDir, backupDir);
			warnings.push(`Left legacy agent dir at ${backupDir}`);
		} catch (err) {
			warnings.push(`Failed relocating legacy agent dir: ${String(err)}`);
		}
	}
	return {
		changes,
		warnings
	};
}
//#endregion
//#region src/infra/state-migrations.managed-outgoing-images.ts
const LEGACY_RECORD_MAX_BYTES = 1048576;
const DEFAULT_TRANSIENT_TTL_MS = 9e5;
const ATTACHMENT_ID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
const DOCTOR_CLAIM_MARKER = ".json.doctor-importing-";
const DOCTOR_CLAIM_SUFFIX_RE = /^\d+-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
const RECORD_KEYS = /* @__PURE__ */ new Set([
	"attachmentId",
	"sessionKey",
	"agentId",
	"messageId",
	"createdAt",
	"updatedAt",
	"retentionClass",
	"alt",
	"original"
]);
const ORIGINAL_KEYS = /* @__PURE__ */ new Set([
	"path",
	"contentType",
	"width",
	"height",
	"sizeBytes",
	"filename"
]);
function resolveLegacyManagedOutgoingImageRecordsDir(stateDir) {
	return path.join(stateDir, "media", "outgoing", "records");
}
function sourceNameFromDoctorClaim(name) {
	const markerIndex = name.indexOf(DOCTOR_CLAIM_MARKER);
	if (markerIndex < 0) return null;
	const attachmentId = name.slice(0, markerIndex);
	const suffix = name.slice(markerIndex + 23);
	return ATTACHMENT_ID_RE.test(attachmentId) && DOCTOR_CLAIM_SUFFIX_RE.test(suffix) ? `${attachmentId}.json` : null;
}
function isLegacyManagedImageSourceName(name) {
	return name.endsWith(".json") || sourceNameFromDoctorClaim(name) !== null;
}
function detectLegacyManagedOutgoingImages(params) {
	const sourceDir = resolveLegacyManagedOutgoingImageRecordsDir(params.stateDir);
	let hasLegacy = false;
	if (params.doctorOnlyStateMigrations === true) try {
		hasLegacy = fs.readdirSync(sourceDir).some(isLegacyManagedImageSourceName);
	} catch {
		hasLegacy = false;
	}
	return {
		sourceDir,
		hasLegacy
	};
}
function recoverInterruptedDoctorClaims(sourceDir) {
	for (const claimName of fs.readdirSync(sourceDir).toSorted()) {
		const sourceName = sourceNameFromDoctorClaim(claimName);
		if (!sourceName) continue;
		const claimPath = path.join(sourceDir, claimName);
		const sourcePath = path.join(sourceDir, sourceName);
		const claimSnapshot = readLegacySourceSnapshot$2(claimPath);
		if (!fs.existsSync(sourcePath)) {
			fs.renameSync(claimPath, sourcePath);
			continue;
		}
		const sourceSnapshot = readLegacySourceSnapshot$2(sourcePath);
		if (sourceSnapshot.size !== claimSnapshot.size || sourceSnapshot.sha256 !== claimSnapshot.sha256) throw new Error(`interrupted managed image claim conflicts with ${sourcePath}`);
		fs.unlinkSync(claimPath);
	}
}
function readLegacySourceSnapshot$2(sourcePath) {
	return readLegacyMigrationSourceSnapshotSync({
		sourcePath,
		label: "managed image",
		maxBytes: LEGACY_RECORD_MAX_BYTES
	});
}
function nullableNonNegativeInteger(value) {
	if (value === null) return null;
	return asSafeIntegerInRange(value, { min: 0 });
}
function parseLegacyManagedImageRecord(params) {
	const raw = JSON.parse(params.snapshot.raw);
	if (!isRecord(raw) || !isRecord(raw.original)) throw new Error("legacy managed image record must be an object");
	assertAllowedJsonFields(raw, RECORD_KEYS, "legacy managed image record");
	assertAllowedJsonFields(raw.original, ORIGINAL_KEYS, "legacy managed image record", { fieldPrefix: "original." });
	const attachmentId = readNonBlankString(raw.attachmentId);
	const sessionKey = readNonBlankString(raw.sessionKey);
	const agentId = readNonBlankString(raw.agentId);
	const messageId = raw.messageId === null ? null : readNonBlankString(raw.messageId);
	const createdAt = readNonBlankString(raw.createdAt);
	const updatedAt = readNonBlankString(raw.updatedAt);
	const alt = typeof raw.alt === "string" ? raw.alt : void 0;
	const retentionClass = raw.retentionClass;
	const originalPath = readNonBlankString(raw.original.path);
	const contentType = readNonBlankString(raw.original.contentType);
	const width = nullableNonNegativeInteger(raw.original.width);
	const height = nullableNonNegativeInteger(raw.original.height);
	const sizeBytes = nullableNonNegativeInteger(raw.original.sizeBytes);
	const filename = raw.original.filename === null ? null : readNonBlankString(raw.original.filename);
	if (!attachmentId || !ATTACHMENT_ID_RE.test(attachmentId) || path.basename(params.snapshot.sourcePath) !== `${attachmentId}.json` || !sessionKey || raw.agentId !== void 0 && !agentId || raw.messageId !== null && messageId === void 0 || !createdAt || !Number.isFinite(Date.parse(createdAt)) || raw.updatedAt !== void 0 && (!updatedAt || !Number.isFinite(Date.parse(updatedAt))) || retentionClass !== void 0 && retentionClass !== "transient" && retentionClass !== "history" || alt === void 0 || !originalPath || !contentType || width === void 0 || height === void 0 || sizeBytes === void 0 || raw.original.filename !== null && filename === void 0) throw new Error(`legacy managed image record is invalid: ${params.snapshot.sourcePath}`);
	const resolvedOriginalPath = path.resolve(originalPath);
	const mediaRoot = path.dirname(path.dirname(path.dirname(resolvedOriginalPath)));
	if (!(/* @__PURE__ */ new Set([path.resolve(params.stateDir, "media"), path.resolve(getMediaDir())])).has(mediaRoot) || path.dirname(resolvedOriginalPath) !== path.join(mediaRoot, "outgoing/originals")) throw new Error("legacy managed image original is outside managed outgoing storage");
	const mediaId = path.basename(resolvedOriginalPath);
	if (!mediaId || mediaId === "." || mediaId === "..") throw new Error("legacy managed image original has an invalid media id");
	return {
		snapshot: params.snapshot,
		originalPath: resolvedOriginalPath,
		record: {
			attachmentId,
			sessionKey,
			...agentId ? { agentId } : {},
			messageId: messageId ?? null,
			createdAt,
			...updatedAt ? { updatedAt } : {},
			...retentionClass === "transient" || retentionClass === "history" ? { retentionClass } : {},
			alt,
			original: {
				mediaRoot,
				mediaId,
				mediaSubdir: MANAGED_OUTGOING_ORIGINALS_SUBDIR,
				contentType,
				width,
				height,
				sizeBytes,
				filename: filename ?? null
			}
		}
	};
}
function restoreClaimedSources(claimed) {
	const restoreErrors = [];
	for (const entry of claimed.toReversed()) {
		if (!fs.existsSync(entry.claimPath)) continue;
		if (fs.existsSync(entry.sourcePath)) {
			restoreErrors.push(`source path already exists: ${entry.sourcePath}`);
			continue;
		}
		try {
			fs.renameSync(entry.claimPath, entry.sourcePath);
		} catch (error) {
			restoreErrors.push(String(error));
		}
	}
	return restoreErrors;
}
function appendRestoreFailures(error, restoreErrors) {
	return `${String(error)}${restoreErrors.length > 0 ? `; restore failures: ${restoreErrors.join("; ")}` : ""}`;
}
function claimLegacySources(params) {
	params.beforeClaim?.();
	const claimed = [];
	try {
		for (const parsed of params.records) {
			const sourcePath = parsed.snapshot.sourcePath;
			const claimPath = `${sourcePath}.doctor-importing-${process.pid}-${randomUUID()}`;
			fs.renameSync(sourcePath, claimPath);
			claimed.push({
				claimPath,
				sourcePath,
				parsed
			});
			const claimedSnapshot = readLegacySourceSnapshot$2(claimPath);
			if (!legacyMigrationSourceSnapshotsMatch(claimedSnapshot, parsed.snapshot)) throw new Error(`legacy managed image source changed before doctor claimed it: ${sourcePath}`);
		}
		return claimed;
	} catch (error) {
		throw new Error(appendRestoreFailures(error, restoreClaimedSources(claimed)), { cause: error });
	}
}
function verifyClaimedSources(claimed) {
	for (const entry of claimed) {
		const claimedSnapshot = readLegacySourceSnapshot$2(entry.claimPath);
		if (!legacyMigrationSourceSnapshotsMatch(claimedSnapshot, entry.parsed.snapshot)) throw new Error(`claimed legacy managed image source changed: ${entry.sourcePath}`);
		if (fs.existsSync(entry.sourcePath)) throw new Error(`legacy managed image source was replaced while doctor imported it`);
	}
}
function removeClaimedSources$1(params) {
	try {
		for (const entry of params.claimed) (params.removeSource ?? fs.unlinkSync)(entry.claimPath);
	} catch (error) {
		throw new Error(appendRestoreFailures(error, restoreClaimedSources(params.claimed)), { cause: error });
	}
}
function isExpiredTransient(record, nowMs, transientTtlMs) {
	const createdAtMs = Date.parse(record.createdAt);
	return record.messageId === null && Number.isFinite(createdAtMs) && nowMs - createdAtMs >= transientTtlMs;
}
function rollbackImportedRecords(params) {
	try {
		runOpenClawStateWriteTransaction(({ db }) => {
			const stateDb = getNodeSqliteKysely(db);
			for (const parsed of params.records) {
				const row = executeSqliteQueryTakeFirstSync(db, stateDb.selectFrom("managed_outgoing_image_records").selectAll().where("attachment_id", "=", parsed.record.attachmentId));
				if (!row || row.cleanup_pending === 1 || !managedImageRecordsEqual(managedImageRecordFromRow(row), parsed.record)) continue;
				executeSqliteQuerySync(db, stateDb.deleteFrom("managed_outgoing_image_records").where("attachment_id", "=", parsed.record.attachmentId));
			}
		}, { env: {
			...process.env,
			OPENCLAW_STATE_DIR: params.stateDir
		} });
		return null;
	} catch (error) {
		return String(error);
	}
}
/** Import, verify, and remove retired record JSON during explicit Doctor repair. */
function migrateLegacyManagedOutgoingImages(params) {
	const changes = [];
	const warnings = [];
	if (!params.detected.hasLegacy) return {
		changes,
		warnings
	};
	let parsedRecords;
	try {
		const sourceDirStat = fs.lstatSync(params.detected.sourceDir);
		if (!sourceDirStat.isDirectory() || sourceDirStat.isSymbolicLink()) throw new Error("legacy managed image records owner is not a regular directory");
		recoverInterruptedDoctorClaims(params.detected.sourceDir);
		parsedRecords = fs.readdirSync(params.detected.sourceDir).filter((name) => name.endsWith(".json")).toSorted().map((name) => parseLegacyManagedImageRecord({
			snapshot: readLegacySourceSnapshot$2(path.join(params.detected.sourceDir, name)),
			stateDir: params.stateDir
		}));
	} catch (error) {
		warnings.push(`Failed reading legacy managed outgoing image state: ${String(error)}`);
		return {
			changes,
			warnings
		};
	}
	const nowMs = params.nowMs ?? Date.now();
	const transientTtlMs = params.transientTtlMs ?? DEFAULT_TRANSIENT_TTL_MS;
	const discardedIds = /* @__PURE__ */ new Set();
	const insertedRecords = [];
	let claimed;
	try {
		claimed = claimLegacySources({
			records: parsedRecords,
			beforeClaim: params.beforeClaim
		});
	} catch (error) {
		warnings.push(`Failed claiming legacy managed outgoing image state: ${String(error)}`);
		return {
			changes,
			warnings
		};
	}
	try {
		runOpenClawStateWriteTransaction(({ db }) => {
			const stateDb = getNodeSqliteKysely(db);
			for (const parsed of parsedRecords) {
				const existing = executeSqliteQueryTakeFirstSync(db, stateDb.selectFrom("managed_outgoing_image_records").selectAll().where("attachment_id", "=", parsed.record.attachmentId));
				if (existing) {
					if (!managedImageRecordsEqual(managedImageRecordFromRow(existing), parsed.record)) throw new Error(`legacy managed image record conflicts with shared SQLite state: ${parsed.record.attachmentId}`);
					continue;
				}
				if (isExpiredTransient(parsed.record, nowMs, transientTtlMs)) {
					discardedIds.add(parsed.record.attachmentId);
					continue;
				}
				executeSqliteQuerySync(db, stateDb.insertInto("managed_outgoing_image_records").values(managedImageRecordToRow(parsed.record)));
				insertedRecords.push(parsed);
			}
		}, { env: {
			...process.env,
			OPENCLAW_STATE_DIR: params.stateDir
		} });
	} catch (error) {
		warnings.push(`Failed migrating legacy managed outgoing image state: ${appendRestoreFailures(error, restoreClaimedSources(claimed))}`);
		return {
			changes,
			warnings
		};
	}
	try {
		params.beforeVerify?.();
		const database = openOpenClawStateDatabase({ env: {
			...process.env,
			OPENCLAW_STATE_DIR: params.stateDir
		} });
		const stateDb = getNodeSqliteKysely(database.db);
		for (const parsed of parsedRecords) {
			const row = executeSqliteQueryTakeFirstSync(database.db, stateDb.selectFrom("managed_outgoing_image_records").selectAll().where("attachment_id", "=", parsed.record.attachmentId));
			if (discardedIds.has(parsed.record.attachmentId)) {
				if (row) throw new Error(`discarded transient record unexpectedly exists: ${parsed.record.attachmentId}`);
			} else if (!row || !managedImageRecordsEqual(managedImageRecordFromRow(row), parsed.record)) throw new Error(`managed image verification failed: ${parsed.record.attachmentId}`);
		}
		verifyClaimedSources(claimed);
	} catch (error) {
		const rollbackError = rollbackImportedRecords({
			records: insertedRecords,
			stateDir: params.stateDir
		});
		const restoreErrors = restoreClaimedSources(claimed);
		warnings.push(`Failed verifying legacy managed outgoing image migration: ${appendRestoreFailures(error, restoreErrors)}` + (rollbackError ? `; SQLite rollback failure: ${rollbackError}` : ""));
		return {
			changes,
			warnings
		};
	}
	let deletedExpiredFiles = 0;
	try {
		for (const parsed of parsedRecords) {
			if (!discardedIds.has(parsed.record.attachmentId)) continue;
			fs.rmSync(parsed.originalPath, { force: true });
			deletedExpiredFiles += 1;
		}
	} catch (error) {
		warnings.push(`Failed deleting expired legacy managed image attachments: ${appendRestoreFailures(error, restoreClaimedSources(claimed))}`);
		return {
			changes,
			warnings
		};
	}
	try {
		removeClaimedSources$1({
			claimed,
			removeSource: params.removeSource
		});
	} catch (error) {
		warnings.push(`Migrated managed outgoing images but could not remove legacy JSON: ${String(error)}`);
		return {
			changes,
			warnings
		};
	}
	try {
		fs.rmdirSync(params.detected.sourceDir);
	} catch {}
	const importedCount = parsedRecords.length - discardedIds.size;
	if (importedCount > 0) changes.push(`Migrated ${importedCount} managed outgoing image record(s) → shared SQLite state`);
	if (discardedIds.size > 0) changes.push(`Discarded ${discardedIds.size} expired managed outgoing image record(s)` + (deletedExpiredFiles > 0 ? ` and ${deletedExpiredFiles} attachment file(s)` : ""));
	changes.push("Removed legacy managed outgoing image JSON after SQLite verification");
	return {
		changes,
		warnings
	};
}
//#endregion
//#region src/infra/state-migrations.mcp-oauth-format.ts
const MAX_TIMESTAMP_MS = 864e13;
const STORE_KEYS = /* @__PURE__ */ new Set([
	"clientInformation",
	"tokens",
	"tokenExpiresAt",
	"codeVerifier",
	"discoveryState",
	"lastAuthorizationUrl",
	"redirectUrl",
	"state"
]);
const DISCOVERY_KEYS = /* @__PURE__ */ new Set([
	"authorizationServerUrl",
	"authorizationServerMetadata",
	"resourceMetadata",
	"resourceMetadataUrl"
]);
function assertOnlyKeys(value, allowed, label) {
	if (Object.keys(value).some((key) => !allowed.has(key))) throw new Error(`${label} has an unexpected field`);
}
function parseSafeUrl(value, label) {
	if (typeof value !== "string") throw new Error(`${label} is not a string`);
	let parsed;
	try {
		parsed = new URL(value);
	} catch {
		throw new Error(`${label} is not a valid URL`);
	}
	if ([
		"javascript:",
		"data:",
		"vbscript:"
	].includes(parsed.protocol)) throw new Error(`${label} uses an unsafe URL scheme`);
	return value;
}
function parseDiscoveryState(value) {
	if (!isRecord(value)) throw new Error("legacy MCP OAuth discovery state is not an object");
	assertOnlyKeys(value, DISCOVERY_KEYS, "legacy MCP OAuth discovery state");
	const result = { authorizationServerUrl: parseSafeUrl(value.authorizationServerUrl, "legacy MCP OAuth authorization server URL") };
	if (value.authorizationServerMetadata !== void 0) {
		const oauth = OAuthMetadataSchema.safeParse(value.authorizationServerMetadata);
		const oidc = oauth.success ? null : OpenIdProviderDiscoveryMetadataSchema.safeParse(value.authorizationServerMetadata);
		if (!oauth.success && !oidc?.success) throw new Error("legacy MCP OAuth authorization server metadata is invalid");
		result.authorizationServerMetadata = value.authorizationServerMetadata;
	}
	if (value.resourceMetadata !== void 0) {
		if (!OAuthProtectedResourceMetadataSchema.safeParse(value.resourceMetadata).success) throw new Error("legacy MCP OAuth resource metadata is invalid");
		result.resourceMetadata = value.resourceMetadata;
	}
	if (value.resourceMetadataUrl !== void 0) result.resourceMetadataUrl = parseSafeUrl(value.resourceMetadataUrl, "legacy MCP OAuth resource metadata URL");
	return result;
}
function parseLegacyMcpOAuthStore(value) {
	if (!isRecord(value)) throw new Error("legacy MCP OAuth store is not an object");
	assertOnlyKeys(value, STORE_KEYS, "legacy MCP OAuth store");
	const result = {};
	if (value.clientInformation !== void 0) {
		if (!OAuthClientInformationSchema.safeParse(value.clientInformation).success) throw new Error("legacy MCP OAuth client information is invalid");
		result.clientInformation = value.clientInformation;
	}
	if (value.tokens !== void 0) {
		if (!OAuthTokensSchema.safeParse(value.tokens).success) throw new Error("legacy MCP OAuth tokens are invalid");
		result.tokens = value.tokens;
	}
	if (value.tokenExpiresAt !== void 0) {
		if (typeof value.tokenExpiresAt !== "number" || !Number.isFinite(value.tokenExpiresAt) || value.tokenExpiresAt < 0 || value.tokenExpiresAt > MAX_TIMESTAMP_MS) throw new Error("legacy MCP OAuth token expiry is invalid");
		if (result.tokens !== void 0) result.tokenExpiresAt = value.tokenExpiresAt;
	}
	if (value.codeVerifier !== void 0) {
		if (typeof value.codeVerifier !== "string" || value.codeVerifier.length === 0) throw new Error("legacy MCP OAuth code verifier is invalid");
		result.codeVerifier = value.codeVerifier;
	}
	if (value.discoveryState !== void 0) result.discoveryState = parseDiscoveryState(value.discoveryState);
	if (value.lastAuthorizationUrl !== void 0) result.lastAuthorizationUrl = parseSafeUrl(value.lastAuthorizationUrl, "legacy MCP OAuth authorization URL");
	if (value.redirectUrl !== void 0) result.redirectUrl = parseSafeUrl(value.redirectUrl, "legacy MCP OAuth redirect URL");
	return result;
}
//#endregion
//#region src/infra/state-migrations.mcp-oauth-lock-stale.ts
const LEGACY_LOCK_STALE_MS = 6e4;
function parseLockPayload(raw) {
	return safeParseJsonRecord(raw) ?? null;
}
/** Classify only retired-runtime owners whose age and process identity are provably stale. */
function isDefinitelyStaleLegacyMcpOAuthLock(params) {
	const payload = parseLockPayload(params.raw);
	if (!payload) return false;
	const pid = payload.pid;
	const createdAt = payload.createdAt;
	const starttime = payload.starttime;
	if (typeof pid !== "number" || !Number.isSafeInteger(pid) || pid <= 0 || typeof createdAt !== "string" || starttime !== void 0 && (typeof starttime !== "number" || !Number.isSafeInteger(starttime) || starttime < 0)) return false;
	const createdAtMs = Date.parse(createdAt);
	const ageMs = (params.nowMs ?? Date.now()) - createdAtMs;
	if (!Number.isFinite(createdAtMs) || new Date(createdAtMs).toISOString() !== createdAt || !Number.isFinite(ageMs) || ageMs < LEGACY_LOCK_STALE_MS) return false;
	return isLockOwnerDefinitelyStale({
		payload,
		isPidDefinitelyDead: params.isPidDefinitelyDead ?? isPidDefinitelyDead,
		getProcessStartTime: params.getProcessStartTime ?? getFileLockProcessStartTime
	});
}
//#endregion
//#region src/infra/state-migrations.mcp-oauth-lock.ts
const LOCK_RETRIES = 20;
const LOCK_RETRY_FACTOR = 1.3;
const LOCK_RETRY_MIN_MS = 25;
const LOCK_RETRY_MAX_MS = 500;
const MCP_OAUTH_LOCKS = createFileLockManager("openclaw.mcp-oauth-legacy-migration");
function createLockPayload() {
	const payload = {
		pid: process.pid,
		createdAt: (/* @__PURE__ */ new Date()).toISOString(),
		nonce: randomUUID()
	};
	const starttime = getFileLockProcessStartTime(process.pid);
	if (starttime !== null) payload.starttime = starttime;
	return payload;
}
function readLegacyRawPayload(payload) {
	return payload && typeof payload === "object" && "raw" in payload && typeof payload.raw === "string" ? payload.raw : "";
}
/** Share the retired runtime's sidecar protocol without leaving the pinned state root. */
async function withRootBoundedLegacyFileLock(params, run) {
	const targetPath = path.resolve(params.stateRoot.rootReal, params.targetRelativePath);
	return await MCP_OAUTH_LOCKS.withLock(targetPath, {
		lockPath: `${targetPath}.lock`,
		lockRoot: asFsSafeFileLockRoot(params.stateRoot),
		retry: {
			retries: LOCK_RETRIES,
			factor: LOCK_RETRY_FACTOR,
			minTimeout: LOCK_RETRY_MIN_MS,
			maxTimeout: LOCK_RETRY_MAX_MS
		},
		staleRecovery: "fail-closed",
		payload: createLockPayload,
		parsePayload: (raw) => ({ raw }),
		shouldReclaim: ({ payload }) => isDefinitelyStaleLegacyMcpOAuthLock({ raw: readLegacyRawPayload(payload) })
	}, run);
}
//#endregion
//#region src/infra/state-migrations.mcp-oauth.ts
const LEGACY_MCP_OAUTH_DIR = "mcp-oauth";
const DOCTOR_CLAIM_SUFFIX$2 = ".doctor-importing";
const MIGRATION_KIND$4 = "legacy-mcp-oauth-json";
const MAX_LEGACY_STORE_BYTES = 4194304;
const utf8Decoder$2 = new TextDecoder("utf-8", { fatal: true });
function parseLegacyMcpOAuthJson(buffer) {
	try {
		return JSON.parse(utf8Decoder$2.decode(buffer));
	} catch {
		throw new Error("legacy MCP OAuth store contains invalid JSON");
	}
}
function exactLegacyBaseName(name) {
	const baseName = name.endsWith(DOCTOR_CLAIM_SUFFIX$2) ? name.slice(0, -17) : name;
	return mcpOAuthStoreKeyFromLegacyFileName(baseName) ? baseName : null;
}
function exactLegacyBaseNames(entries) {
	const baseNames = /* @__PURE__ */ new Set();
	for (const entry of entries) {
		const baseName = exactLegacyBaseName(entry.name);
		if (baseName) baseNames.add(baseName);
	}
	return Array.from(baseNames).toSorted();
}
function listLegacySourcePaths(sourceDir) {
	return exactLegacyBaseNames(fs.readdirSync(sourceDir, { withFileTypes: true })).map((baseName) => path.join(sourceDir, baseName));
}
async function listLegacySourcePathsFromRoot(params) {
	return exactLegacyBaseNames(await params.stateRoot.list(LEGACY_MCP_OAUTH_DIR, { withFileTypes: true })).map((baseName) => path.join(params.stateDir, LEGACY_MCP_OAUTH_DIR, baseName));
}
/** Detect exact retired MCP OAuth filenames only for an explicit Doctor flow. */
function detectLegacyMcpOAuthStores(params) {
	const sourceDir = path.join(params.stateDir, LEGACY_MCP_OAUTH_DIR);
	if (params.doctorOnlyStateMigrations !== true) return {
		sourceDir,
		sourcePaths: [],
		hasLegacy: false
	};
	try {
		const sourcePaths = listLegacySourcePaths(sourceDir);
		return {
			sourceDir,
			sourcePaths,
			hasLegacy: sourcePaths.length > 0
		};
	} catch {
		return {
			sourceDir,
			sourcePaths: [],
			hasLegacy: pathMayExistSync(sourceDir)
		};
	}
}
function relativeLegacyPath(stateDir, filePath) {
	return resolveLegacyMigrationRelativePath(stateDir, filePath, "MCP OAuth", false);
}
async function readLegacySourceSnapshot$1(stateRoot, stateDir, sourcePath, options = {}) {
	const snapshot = await readLegacyMigrationSourceSnapshot({
		stateRoot,
		stateDir,
		sourcePath,
		maxBytes: MAX_LEGACY_STORE_BYTES,
		label: "MCP OAuth"
	});
	const parsed = options.parseStore === false ? {} : parseLegacyMcpOAuthStore(parseLegacyMcpOAuthJson(snapshot.buffer));
	return {
		...snapshot,
		store: parsed
	};
}
function storeKeyForSource(sourcePath) {
	const storeKey = mcpOAuthStoreKeyFromLegacyFileName(path.basename(sourcePath));
	if (!storeKey) throw new Error("legacy MCP OAuth filename is invalid");
	return storeKey;
}
function importAndRecordReceipt$1(params) {
	const sourceKey = resolveLegacyMigrationSourceKey("mcp-oauth-json", params.sourcePath);
	const storeKey = storeKeyForSource(params.sourcePath);
	const runId = `${sourceKey}:${params.snapshot.sha256.slice(0, 16)}`;
	const now = Date.now();
	return runOpenClawStateWriteTransaction(({ db }) => {
		const stateDb = getNodeSqliteKysely(db);
		if (readLegacyMigrationReceiptFromDatabase(db, sourceKey)) return {
			sourceKey,
			imported: false
		};
		const existingStore = executeSqliteQueryTakeFirstSync(db, stateDb.selectFrom("mcp_oauth_stores").selectAll().where("store_key", "=", storeKey));
		let importedLegacyState;
		if (existingStore) {
			if (existingStore.format_version !== 1) throw new Error("canonical MCP OAuth store has an unsupported format version");
			const canonicalStore = parseMcpOAuthStoreJson(storeKey, existingStore.store_json);
			const canMergeLegacyState = canonicalStore.credentialState === "uninitialized";
			const legacyStore = { ...params.snapshot.store };
			if (canonicalStore.pendingAuthorizationChallenge?.resourceMetadataUrl) delete legacyStore.discoveryState;
			importedLegacyState = canMergeLegacyState && Object.keys(legacyStore).some((key) => !Object.hasOwn(canonicalStore, key));
			if (importedLegacyState) {
				const mergedStore = {
					...legacyStore,
					...canonicalStore
				};
				delete mergedStore.credentialState;
				executeSqliteQuerySync(db, stateDb.updateTable("mcp_oauth_stores").set({
					store_json: JSON.stringify(mergedStore),
					updated_at: now
				}).where("store_key", "=", storeKey));
			}
		} else {
			importedLegacyState = true;
			executeSqliteQuerySync(db, stateDb.insertInto("mcp_oauth_stores").values({
				store_key: storeKey,
				format_version: 1,
				store_json: JSON.stringify(params.snapshot.store),
				updated_at: now
			}));
		}
		const verified = executeSqliteQueryTakeFirstSync(db, stateDb.selectFrom("mcp_oauth_stores").selectAll().where("store_key", "=", storeKey));
		if (!verified || verified.format_version !== 1) throw new Error("SQLite verification failed for an MCP OAuth store");
		parseMcpOAuthStoreJson(storeKey, verified.store_json);
		const reportJson = JSON.stringify({
			source: MIGRATION_KIND$4,
			target: "mcp_oauth_stores",
			storeKey,
			sourceSha256: params.snapshot.sha256,
			importedRecordCount: importedLegacyState ? 1 : 0,
			preservedSqliteRecordCount: existingStore ? 1 : 0
		});
		recordLegacyMigrationReceipt(db, {
			sourceKey,
			migrationKind: MIGRATION_KIND$4,
			sourcePath: params.sourcePath,
			targetTable: "mcp_oauth_stores",
			sourceSha256: params.snapshot.sha256,
			sourceSizeBytes: params.snapshot.size,
			sourceRecordCount: 1,
			runId,
			now,
			reportJson
		});
		return {
			sourceKey,
			imported: importedLegacyState
		};
	}, { env: params.env });
}
async function cleanupReceiptAuthoritativeSources(params) {
	let removed = 0;
	for (const candidate of [params.sourcePath, `${params.sourcePath}${DOCTOR_CLAIM_SUFFIX$2}`]) {
		if (!await params.stateRoot.exists(relativeLegacyPath(params.stateDir, candidate))) continue;
		await readLegacySourceSnapshot$1(params.stateRoot, params.stateDir, candidate, { parseStore: false });
		if (params.removeSource) await params.removeSource(candidate);
		else await params.stateRoot.remove(relativeLegacyPath(params.stateDir, candidate));
		removed += 1;
	}
	if (!params.receipt.removedSource || removed > 0) markLegacyMigrationSourceRemoved(params.receipt.sourceKey, params.env);
	return removed;
}
async function migrateOneStore(params) {
	const changes = [];
	const warnings = [];
	const notices = [];
	const receipt = readLegacyMigrationReceipt(resolveLegacyMigrationSourceKey("mcp-oauth-json", params.sourcePath), params.env);
	if (receipt) {
		try {
			if (await cleanupReceiptAuthoritativeSources({
				...params,
				receipt
			}) > 0) changes.push("Discarded recreated retired MCP OAuth JSON without importing it.");
		} catch (error) {
			warnings.push(`MCP OAuth state is in SQLite, but legacy cleanup failed: ${String(error)}`);
		}
		return notices.length > 0 ? {
			changes,
			warnings,
			notices
		} : {
			changes,
			warnings
		};
	}
	const source = new LegacyMigrationSourceClaim({
		stateRoot: params.stateRoot,
		stateDir: params.stateDir,
		sourcePath: params.sourcePath,
		label: "MCP OAuth",
		includeFilePath: false,
		claimSuffix: DOCTOR_CLAIM_SUFFIX$2,
		readSnapshot: (snapshotPath) => readLegacySourceSnapshot$1(params.stateRoot, params.stateDir, snapshotPath)
	});
	const hasSource = await source.exists();
	const hasClaim = await source.exists(true);
	if (hasSource && hasClaim) return {
		changes,
		warnings: [`Failed migrating legacy MCP OAuth store ${path.basename(params.sourcePath)}: source and interrupted claim both exist.`]
	};
	const activePath = hasSource ? params.sourcePath : hasClaim ? source.claimPath : null;
	if (!activePath) return {
		changes,
		warnings
	};
	let snapshot;
	try {
		snapshot = await readLegacySourceSnapshot$1(params.stateRoot, params.stateDir, activePath);
	} catch (error) {
		warnings.push(`Failed reading legacy MCP OAuth store ${path.basename(params.sourcePath)}: ${String(error)}`);
		return {
			changes,
			warnings
		};
	}
	if (activePath === params.sourcePath) try {
		snapshot = await source.claim({
			snapshot,
			mismatchMessage: "legacy MCP OAuth source changed before Doctor could claim it",
			beforeClaim: () => params.beforeClaim?.(params.sourcePath)
		});
	} catch (error) {
		const restoreError = await source.restore();
		warnings.push(`Failed migrating legacy MCP OAuth store ${path.basename(params.sourcePath)}: ${String(error)}${restoreError ? `; restore failure: ${restoreError}` : ""}`);
		return {
			changes,
			warnings
		};
	}
	let result;
	try {
		result = importAndRecordReceipt$1({
			env: params.env,
			sourcePath: params.sourcePath,
			snapshot
		});
	} catch (error) {
		const restoreError = await source.restore();
		warnings.push(`Failed migrating legacy MCP OAuth store ${path.basename(params.sourcePath)}: ${String(error)}${restoreError ? `; restore failure: ${restoreError}` : ""}`);
		return {
			changes,
			warnings
		};
	}
	try {
		if (await source.exists()) throw new Error("legacy MCP OAuth source reappeared during import");
		const finalSnapshot = await source.read(true);
		if (!legacyMigrationSourceSnapshotsMatch(snapshot, finalSnapshot)) throw new Error("legacy MCP OAuth claim changed after SQLite import");
		await source.remove({
			removeSource: params.removeSource,
			claimRemainingMessage: "legacy MCP OAuth Doctor claim remains after cleanup",
			skipSourceCheck: true
		});
		markLegacyMigrationSourceRemoved(result.sourceKey, params.env);
	} catch (error) {
		warnings.push(`MCP OAuth state is in SQLite, but legacy cleanup failed: ${String(error)}`);
		return {
			changes,
			warnings
		};
	}
	changes.push(result.imported ? `Migrated MCP OAuth store ${path.basename(params.sourcePath)} to SQLite.` : `Preserved canonical SQLite MCP OAuth store for ${path.basename(params.sourcePath)}.`);
	notices.push("Removed retired MCP OAuth JSON after verified SQLite import.");
	return {
		changes,
		warnings,
		notices
	};
}
async function migrateWithExclusiveStateOwnership$3(params) {
	const changes = [];
	const warnings = [];
	const notices = [];
	let sourcePaths;
	try {
		sourcePaths = await listLegacySourcePathsFromRoot(params);
	} catch (error) {
		const code = error.code;
		if (code === "ENOENT" || code === "not-found") return {
			changes,
			warnings
		};
		return {
			changes,
			warnings: [`Failed reading legacy MCP OAuth directory: ${String(error)}`]
		};
	}
	for (const sourcePath of sourcePaths) try {
		params.beforeLegacyLock?.(sourcePath);
		const result = await withRootBoundedLegacyFileLock({
			stateRoot: params.stateRoot,
			targetRelativePath: relativeLegacyPath(params.stateDir, sourcePath)
		}, async () => await migrateOneStore({
			...params,
			sourcePath
		}));
		changes.push(...result.changes);
		warnings.push(...result.warnings);
		notices.push(...result.notices ?? []);
	} catch (error) {
		const staleGuidance = error.code === "file_lock_stale" ? " Verify no older OpenClaw process is running, remove the retired .lock sidecar, and rerun Doctor." : "";
		warnings.push(`Failed locking legacy MCP OAuth store ${path.basename(sourcePath)}: ${String(error)}.${staleGuidance}`);
	}
	return notices.length > 0 ? {
		changes,
		warnings,
		notices
	} : {
		changes,
		warnings
	};
}
/** Import retired MCP OAuth stores while excluding old Gateways that can recreate them. */
async function migrateLegacyMcpOAuthStores(params) {
	if (!params.detected.hasLegacy) return {
		changes: [],
		warnings: []
	};
	return await withLegacyMigrationStateLock({
		stateDir: params.stateDir,
		env: params.env,
		label: "legacy MCP OAuth stores",
		releaseLabel: "MCP OAuth",
		errorLabel: "Failed reading legacy MCP OAuth state",
		run: async (env) => {
			const stateRoot = await root(params.stateDir, {
				hardlinks: "reject",
				maxBytes: MAX_LEGACY_STORE_BYTES,
				symlinks: "reject"
			});
			return await migrateWithExclusiveStateOwnership$3({
				...params,
				env,
				stateRoot
			});
		}
	});
}
//#endregion
//#region src/infra/state-migrations.meeting-transcripts-database.ts
function migrationDb(db) {
	return getNodeSqliteKysely(db);
}
//#endregion
//#region src/infra/state-migrations.meeting-transcripts-files.ts
const LEGACY_UTTERANCE_STAGE_BATCH_SIZE = 256;
function sha256FileSync(filePath) {
	const digest = createHash("sha256");
	const descriptor = fs.openSync(filePath, "r");
	const buffer = Buffer.allocUnsafe(65536);
	try {
		while (true) {
			const bytesRead = fs.readSync(descriptor, buffer, 0, buffer.length, null);
			if (bytesRead === 0) break;
			digest.update(buffer.subarray(0, bytesRead));
		}
	} finally {
		fs.closeSync(descriptor);
	}
	return digest.digest("hex");
}
function isRecordedCanonicalTranscriptExport(params) {
	const entries = fs.readdirSync(params.sessionDir, { withFileTypes: true });
	for (const entry of entries) {
		const canonicalName = entry.name.toLowerCase();
		if (!TRANSCRIPT_EXPORT_FILE_NAMES.has(canonicalName)) continue;
		const filePath = path.join(params.sessionDir, entry.name);
		const stat = fs.lstatSync(filePath);
		if (stat.isSymbolicLink() || !stat.isFile()) return false;
		const expectedHash = params.manifest[canonicalName];
		if (params.pending?.has(canonicalName) !== true && (!expectedHash || sha256FileSync(filePath) !== expectedHash)) return false;
	}
	return true;
}
function hasMatchingRecordedTranscriptArtifact(params) {
	for (const entry of fs.readdirSync(params.sessionDir, { withFileTypes: true })) {
		const canonicalName = entry.name.toLowerCase();
		const expectedHash = params.manifest[canonicalName];
		if (!TRANSCRIPT_EXPORT_FILE_NAMES.has(canonicalName) || !expectedHash) continue;
		const filePath = path.join(params.sessionDir, entry.name);
		const stat = fs.lstatSync(filePath);
		if (!stat.isSymbolicLink() && stat.isFile() && sha256FileSync(filePath) === expectedHash) return true;
	}
	return false;
}
async function validateMeetingTranscriptRoot(rootDir, options = {}) {
	try {
		const stat = await fs$1.lstat(rootDir);
		if (stat.isSymbolicLink() || !stat.isDirectory()) throw new Error(`meeting transcript root must be a regular directory: ${rootDir}`);
		return true;
	} catch (error) {
		if (isRecord(error) && error.code === "ENOENT" && options.allowMissing === true) return false;
		throw error;
	}
}
function parseSession(value, sourcePath) {
	if (!isRecord(value) || typeof value.sessionId !== "string" || !value.sessionId) throw new Error(`invalid transcripts metadata sessionId at ${sourcePath}`);
	if (typeof value.startedAt !== "string" || !value.startedAt) throw new Error(`invalid transcripts metadata startedAt at ${sourcePath}`);
	if (!isRecord(value.source) || typeof value.source.providerId !== "string") throw new Error(`invalid transcripts metadata source at ${sourcePath}`);
	if (value.title !== void 0 && typeof value.title !== "string") throw new Error(`invalid transcripts metadata title at ${sourcePath}`);
	if (value.stoppedAt !== void 0 && typeof value.stoppedAt !== "string") throw new Error(`invalid transcripts metadata stoppedAt at ${sourcePath}`);
	if (value.metadata !== void 0 && !isRecord(value.metadata)) throw new Error(`invalid transcripts metadata payload at ${sourcePath}`);
	return value;
}
function parseUtterance(value, sourcePath, lineNumber) {
	if (!isRecord(value) || typeof value.text !== "string") throw new Error(`invalid transcript utterance at ${sourcePath}:${lineNumber}`);
	if (value.speaker !== void 0 && (!isRecord(value.speaker) || typeof value.speaker.label !== "string")) throw new Error(`invalid transcript speaker at ${sourcePath}:${lineNumber}`);
	return value;
}
function parseSummary(value, sourcePath) {
	if (!isRecord(value) || typeof value.sessionId !== "string" || typeof value.title !== "string" || typeof value.generatedAt !== "string" || typeof value.overview !== "string" || !Array.isArray(value.transcript) || !Array.isArray(value.decisions) || !Array.isArray(value.actionItems) || !Array.isArray(value.risks) || !Number.isSafeInteger(value.utteranceCount) || value.utteranceCount < 0) throw new Error(`invalid transcripts summary at ${sourcePath}`);
	return value;
}
function legacyTranscriptRelativeDir(session) {
	const date = session.startedAt.match(/^(\d{4}-\d{2}-\d{2})T/)?.[1];
	if (!date) throw new Error(`legacy transcript startedAt has no date: ${session.startedAt}`);
	const legacySegment = session.sessionId.replace(/[^a-zA-Z0-9._-]+/g, "-").replace(/^-+|-+$/g, "") || "session";
	return path.normalize(path.join(date, legacySegment));
}
async function optionalRegularFile(filePath) {
	try {
		const stat = await fs$1.lstat(filePath);
		if (stat.isSymbolicLink() || !stat.isFile()) throw new Error(`legacy transcript source must be a regular file: ${filePath}`);
		return true;
	} catch (error) {
		if (isRecord(error) && error.code === "ENOENT") return false;
		throw error;
	}
}
function openLegacyMeetingTranscriptStage(databasePath) {
	const database = openNodeSqliteDatabase(databasePath);
	database.exec(`
    PRAGMA journal_mode = WAL;
    CREATE TABLE staged_utterances (
      stage_key TEXT NOT NULL,
      sequence INTEGER NOT NULL,
      utterance_json TEXT NOT NULL,
      PRIMARY KEY (stage_key, sequence)
    ) STRICT;
  `);
	return database;
}
async function stageUtterances(params) {
	const filePath = params.filePath;
	if (!await optionalRegularFile(filePath)) return 0;
	const stream = createReadStream(filePath, { encoding: "utf8" });
	const lines = createInterface({
		input: stream,
		crlfDelay: Infinity
	});
	let lineNumber = 0;
	let sequence = 0;
	let pending = [];
	const insert = params.stageDatabase.prepare("INSERT INTO staged_utterances (stage_key, sequence, utterance_json) VALUES (?, ?, ?)");
	const flush = () => {
		if (pending.length === 0) return;
		params.stageDatabase.exec("BEGIN IMMEDIATE");
		try {
			for (const utteranceJson of pending) {
				insert.run(params.stageKey, sequence, utteranceJson);
				sequence += 1;
			}
			params.stageDatabase.exec("COMMIT");
			pending = [];
		} catch (error) {
			params.stageDatabase.exec("ROLLBACK");
			throw error;
		}
	};
	try {
		for await (const line of lines) {
			lineNumber += 1;
			if (!line.trim()) continue;
			const utterance = parseUtterance(JSON.parse(line), filePath, lineNumber);
			pending.push(JSON.stringify(utterance));
			if (pending.length >= LEGACY_UTTERANCE_STAGE_BATCH_SIZE) flush();
		}
		flush();
	} finally {
		lines.close();
		stream.destroy();
	}
	return sequence;
}
function readStagedMeetingTranscriptUtterances(params) {
	return params.stageDatabase.prepare("SELECT utterance_json FROM staged_utterances WHERE stage_key = ? AND sequence >= ? ORDER BY sequence ASC LIMIT ?").all(params.stageKey, params.start, params.limit).map((row) => JSON.parse(String(row.utterance_json)));
}
async function snapshotFile(filePath) {
	if (!await optionalRegularFile(filePath)) return { sizeBytes: 0 };
	const stat = await fs$1.stat(filePath);
	return {
		hash: await sha256File(filePath),
		sizeBytes: stat.size
	};
}
async function snapshotSourceFiles(files) {
	return await Promise.all(files.map(snapshotFile));
}
function sourceFilesHash(files, snapshots) {
	return sha256Hex(snapshots.map((snapshot, index) => `${path.basename(files[index] ?? "")}\0${snapshot.hash ?? "-"}`).join("\n"));
}
async function snapshotLegacyMeetingTranscriptSession(params) {
	const sourceDir = path.join(params.rootDir, params.relativeDir);
	const sourceStat = await fs$1.lstat(sourceDir);
	if (sourceStat.isSymbolicLink() || !sourceStat.isDirectory()) throw new Error(`legacy transcript session must be a regular directory: ${sourceDir}`);
	const metadataPath = path.join(sourceDir, "metadata.json");
	const transcriptPath = path.join(sourceDir, "transcript.jsonl");
	const summaryJsonPath = path.join(sourceDir, "summary.json");
	const summaryMarkdownPath = path.join(sourceDir, "summary.md");
	const files = [
		metadataPath,
		transcriptPath,
		summaryJsonPath,
		summaryMarkdownPath
	];
	const beforeSnapshots = await snapshotSourceFiles(files);
	if (!beforeSnapshots[0]?.hash) throw new Error(`legacy transcript session is missing metadata.json: ${sourceDir}`);
	const session = parseSession(JSON.parse(await fs$1.readFile(metadataPath, "utf8")), metadataPath);
	const expectedRelativeDir = legacyTranscriptRelativeDir(session);
	if (path.normalize(params.relativeDir) !== expectedRelativeDir) throw new Error(`legacy transcript selector mismatch at ${sourceDir}: expected ${expectedRelativeDir}`);
	const utteranceCount = await stageUtterances({
		filePath: transcriptPath,
		stageDatabase: params.stageDatabase,
		stageKey: params.relativeDir
	});
	const hasSummaryJson = await optionalRegularFile(summaryJsonPath);
	const hasSummaryMarkdown = await optionalRegularFile(summaryMarkdownPath);
	const summary = hasSummaryJson ? parseSummary(JSON.parse(await fs$1.readFile(summaryJsonPath, "utf8")), summaryJsonPath) : void 0;
	const markdown = hasSummaryMarkdown ? await fs$1.readFile(summaryMarkdownPath, "utf8") : summary ? renderTranscriptsMarkdown(summary) : void 0;
	if (summary && summary.sessionId !== session.sessionId) throw new Error(`legacy transcript summary session mismatch at ${summaryJsonPath}`);
	const fileSnapshots = await snapshotSourceFiles(files);
	if (fileSnapshots.some((snapshot, index) => snapshot.hash !== beforeSnapshots[index]?.hash || snapshot.sizeBytes !== beforeSnapshots[index]?.sizeBytes)) throw new Error(`legacy transcript files changed while being staged: ${sourceDir}`);
	const sourceHash = sourceFilesHash(files, fileSnapshots);
	return {
		sourceDir,
		relativeDir: params.relativeDir,
		stageKey: params.relativeDir,
		session,
		utteranceCount,
		summary,
		markdown,
		sourceHash,
		sourceSizeBytes: fileSnapshots.reduce((total, file) => total + file.sizeBytes, 0)
	};
}
async function hasLegacyTranscriptArtifacts(directory) {
	const entries = await fs$1.readdir(directory, { withFileTypes: true });
	let found = false;
	for (const entry of entries) {
		if (!TRANSCRIPT_EXPORT_FILE_NAMES.has(entry.name.toLowerCase())) continue;
		const stat = await fs$1.lstat(path.join(directory, entry.name));
		if (stat.isSymbolicLink() || !stat.isFile()) throw new Error(`legacy transcript source must be a regular file: ${directory}`);
		found = true;
	}
	return found;
}
async function listLegacyMeetingTranscriptDirs(rootDir, mode) {
	if (!await validateMeetingTranscriptRoot(rootDir, { allowMissing: true })) return [];
	let dateEntries;
	try {
		dateEntries = await fs$1.readdir(rootDir, { withFileTypes: true });
	} catch (error) {
		if (isRecord(error) && error.code === "ENOENT") return [];
		throw error;
	}
	const include = async (directory) => mode === "sessions" ? await optionalRegularFile(path.join(directory, "metadata.json")) : await hasLegacyTranscriptArtifacts(directory);
	const sessions = [];
	if (await include(rootDir)) sessions.push(".");
	for (const dateEntry of dateEntries.toSorted((a, b) => a.name.localeCompare(b.name))) {
		if (!/^\d{4}-\d{2}-\d{2}$/.test(dateEntry.name)) continue;
		if (dateEntry.isSymbolicLink()) throw new Error(`legacy transcript date directory cannot be a symlink: ${dateEntry.name}`);
		if (!dateEntry.isDirectory()) continue;
		const dateDir = path.join(rootDir, dateEntry.name);
		if (await include(dateDir)) sessions.push(dateEntry.name);
		const sessionEntries = await fs$1.readdir(dateDir, { withFileTypes: true });
		for (const sessionEntry of sessionEntries.toSorted((a, b) => a.name.localeCompare(b.name))) {
			if (sessionEntry.isSymbolicLink()) throw new Error(`legacy transcript session cannot be a symlink: ${sessionEntry.name}`);
			if (sessionEntry.isDirectory() && await include(path.join(dateDir, sessionEntry.name))) sessions.push(path.join(dateEntry.name, sessionEntry.name));
		}
	}
	return sessions;
}
async function listLegacyMeetingTranscriptSessionDirs(rootDir) {
	return await listLegacyMeetingTranscriptDirs(rootDir, "sessions");
}
async function listLegacyMeetingTranscriptArtifactDirs(rootDir) {
	return await listLegacyMeetingTranscriptDirs(rootDir, "artifacts");
}
async function archivePartialMeetingTranscriptArtifacts(params) {
	const moves = [];
	const sourceDirs = /* @__PURE__ */ new Set();
	for (const relativeDir of params.relativeDirs) {
		const sourceDir = path.join(params.sourceRoot, relativeDir);
		if (relativeDir !== ".") sourceDirs.add(sourceDir);
		const destinationDir = path.join(params.recoveryRoot, relativeDir);
		for (const entry of await fs$1.readdir(sourceDir, { withFileTypes: true })) {
			if (!TRANSCRIPT_EXPORT_FILE_NAMES.has(entry.name.toLowerCase())) continue;
			const source = path.join(sourceDir, entry.name);
			const stat = await fs$1.lstat(source);
			if (stat.isSymbolicLink() || !stat.isFile()) throw new Error(`legacy transcript source must be a regular file: ${sourceDir}`);
			const destination = path.join(destinationDir, entry.name);
			try {
				await fs$1.lstat(destination);
				throw new Error(`partial transcript recovery destination already exists: ${destination}`);
			} catch (error) {
				if (!(isRecord(error) && error.code === "ENOENT")) throw error;
			}
			moves.push({
				source,
				destination
			});
		}
	}
	for (const destinationDir of new Set(moves.map((move) => path.dirname(move.destination)))) await fs$1.mkdir(destinationDir, { recursive: true });
	const moved = [];
	try {
		for (const move of moves) {
			await fs$1.rename(move.source, move.destination);
			moved.push(move);
		}
	} catch (error) {
		const rollbackErrors = [];
		for (const move of moved.toReversed()) try {
			await fs$1.rename(move.destination, move.source);
		} catch (rollbackError) {
			rollbackErrors.push(String(rollbackError));
		}
		if (rollbackErrors.length > 0) throw new Error(`partial transcript recovery failed and rollback was incomplete; inspect ${params.recoveryRoot}: ${String(error)}; rollback errors: ${rollbackErrors.join("; ")}`, { cause: error });
		throw error;
	}
	for (const sourceDir of [...sourceDirs].toSorted((a, b) => b.length - a.length)) await fs$1.rmdir(sourceDir).catch(() => void 0);
}
async function rehashLegacyMeetingTranscriptSnapshots(snapshots) {
	for (const snapshot of snapshots) {
		const files = [
			"metadata.json",
			"transcript.jsonl",
			"summary.json",
			"summary.md"
		].map((fileName) => path.join(snapshot.sourceDir, fileName));
		if (sourceFilesHash(files, await snapshotSourceFiles(files)) !== snapshot.sourceHash) return false;
	}
	return true;
}
async function archiveLegacyMeetingTranscriptSnapshots(params) {
	await validateMeetingTranscriptRoot(params.sourceRoot);
	const currentRelativeDirs = await listLegacyMeetingTranscriptSessionDirs(params.sourceRoot);
	const expectedRelativeDirs = params.expectedRelativeDirs.toSorted((a, b) => a.localeCompare(b));
	if (JSON.stringify(currentRelativeDirs) !== JSON.stringify(expectedRelativeDirs)) throw new Error("legacy transcript session tree changed before archive");
	await fs$1.rename(params.sourceRoot, params.archiveRoot);
	try {
		if (!await rehashLegacyMeetingTranscriptSnapshots(params.snapshots.map((snapshot) => ({
			...snapshot,
			sourceDir: path.join(params.archiveRoot, path.relative(params.sourceRoot, snapshot.sourceDir) || ".")
		})))) throw new Error("legacy transcript files changed at the archive boundary");
		await restoreCanonicalMeetingTranscriptExports({
			sourceRoot: params.sourceRoot,
			archiveRoot: params.archiveRoot,
			migratedSourcePaths: params.snapshots.map((snapshot) => snapshot.sourceDir),
			canonicalRelativeDirs: params.canonicalRelativeDirs
		});
	} catch (error) {
		throw new LegacyMeetingTranscriptArchiveMovedError(error);
	}
	return params.archiveRoot;
}
var LegacyMeetingTranscriptArchiveMovedError = class extends Error {
	constructor(cause) {
		super(`legacy transcript source moved but canonical export restoration failed: ${String(cause)}`);
		this.name = "LegacyMeetingTranscriptArchiveMovedError";
	}
};
async function restoreCanonicalMeetingTranscriptExports(params) {
	await validateMeetingTranscriptRoot(params.archiveRoot);
	if (!await validateMeetingTranscriptRoot(params.sourceRoot, { allowMissing: true })) {
		await fs$1.mkdir(params.sourceRoot, { recursive: true });
		await validateMeetingTranscriptRoot(params.sourceRoot);
	}
	const migratedRelativeDirs = new Set(params.migratedSourcePaths.map((sourcePath) => path.relative(params.sourceRoot, sourcePath) || "."));
	for (const relativeDir of params.canonicalRelativeDirs) {
		const archiveRelative = path.relative(path.resolve(params.archiveRoot), path.resolve(params.archiveRoot, relativeDir));
		const sourceRelative = path.relative(path.resolve(params.sourceRoot), path.resolve(params.sourceRoot, relativeDir));
		if (!archiveRelative || archiveRelative.startsWith("..") || path.isAbsolute(archiveRelative) || !sourceRelative || sourceRelative.startsWith("..") || path.isAbsolute(sourceRelative)) throw new Error(`canonical transcript export path escaped its root: ${relativeDir}`);
		if (migratedRelativeDirs.has(relativeDir)) continue;
		const source = path.join(params.archiveRoot, relativeDir);
		const destination = path.join(params.sourceRoot, relativeDir);
		try {
			const sourceStat = await fs$1.lstat(source);
			if (sourceStat.isSymbolicLink() || !sourceStat.isDirectory()) throw new Error(`canonical transcript export source is not a directory: ${source}`);
			await assertNoSymlinkParents({
				rootDir: params.archiveRoot,
				targetPath: source,
				allowMissing: false,
				messagePrefix: "Canonical transcript export source"
			});
		} catch (error) {
			if (!(isRecord(error) && error.code === "ENOENT")) throw error;
			const destinationStat = await fs$1.lstat(destination);
			if (destinationStat.isSymbolicLink() || !destinationStat.isDirectory()) throw new Error(`canonical transcript export destination is not a directory: ${destination}`, { cause: error });
			await assertNoSymlinkParents({
				rootDir: params.sourceRoot,
				targetPath: destination,
				allowMissing: false,
				messagePrefix: "Canonical transcript export destination"
			});
			continue;
		}
		try {
			const destinationStat = await fs$1.lstat(destination);
			if (destinationStat.isSymbolicLink() || !destinationStat.isDirectory()) throw new Error(`canonical transcript export destination is not a directory: ${destination}`);
			await assertNoSymlinkParents({
				rootDir: params.sourceRoot,
				targetPath: destination,
				allowMissing: false,
				messagePrefix: "Canonical transcript export destination"
			});
			const readMetadata = async (directory) => parseSession(JSON.parse(await fs$1.readFile(path.join(directory, "metadata.json"), "utf8")), path.join(directory, "metadata.json"));
			const [sourceMetadata, destinationMetadata] = await Promise.all([readMetadata(source), readMetadata(destination)]);
			if (sourceMetadata.sessionId !== destinationMetadata.sessionId || sourceMetadata.startedAt !== destinationMetadata.startedAt) throw new Error(`canonical transcript export destination changed identity: ${destination}`);
			continue;
		} catch (error) {
			if (!(isRecord(error) && error.code === "ENOENT")) throw error;
		}
		await assertNoSymlinkParents({
			rootDir: params.sourceRoot,
			targetPath: destination,
			allowMissing: true,
			messagePrefix: "Canonical transcript export destination"
		});
		await fs$1.mkdir(path.dirname(destination), { recursive: true });
		await fs$1.rename(source, destination);
	}
}
async function archiveDivergentMeetingTranscriptExport(params) {
	const source = path.join(params.sourceRoot, params.relativeDir);
	const destination = path.join(params.recoveryRoot, params.relativeDir);
	await fs$1.mkdir(path.dirname(destination), { recursive: true });
	await fs$1.rename(source, destination);
	return destination;
}
//#endregion
//#region src/infra/state-migrations.meeting-transcripts-detection.ts
function hasLegacyArtifactsSync(directory) {
	const entries = fs.readdirSync(directory, { withFileTypes: true });
	let found = false;
	for (const entry of entries) {
		if (!TRANSCRIPT_EXPORT_FILE_NAMES.has(entry.name.toLowerCase())) continue;
		const stat = fs.lstatSync(path.join(directory, entry.name));
		if (stat.isSymbolicLink() || !stat.isFile()) throw new Error(`legacy transcript source must be a regular file: ${directory}`);
		found = true;
	}
	return found;
}
function resolveMeetingTranscriptExportOwnership(params) {
	const exact = params.state.exportOwnership.get(params.selector);
	if (exact) return exact;
	const folded = params.state.exportOwnershipByFoldedSelector.get(params.selector.toLowerCase());
	if (!folded || folded.length === 0) return;
	try {
		const metadataEntries = fs.readdirSync(params.sessionDir, { withFileTypes: true }).filter((entry) => entry.name.toLowerCase() === "metadata.json");
		if (metadataEntries.length > 0) {
			if (metadataEntries.length !== 1) return;
			const metadataPath = path.join(params.sessionDir, metadataEntries[0].name);
			const stat = fs.lstatSync(metadataPath);
			if (stat.isSymbolicLink() || !stat.isFile()) return;
			const metadata = JSON.parse(fs.readFileSync(metadataPath, "utf8"));
			const matches = folded.filter((ownership) => metadata.sessionId === ownership.sessionId && metadata.startedAt === ownership.startedAt);
			return matches.length === 1 ? matches[0] : void 0;
		}
	} catch {
		return;
	}
	const manifestMatches = folded.filter((ownership) => {
		try {
			const canonicalDir = path.join(params.sourceRoot, ownership.selector);
			const canonicalStat = fs.statSync(canonicalDir);
			const observedStat = fs.statSync(params.sessionDir);
			if (canonicalStat.dev !== observedStat.dev || canonicalStat.ino !== observedStat.ino) return false;
			return hasMatchingRecordedTranscriptArtifact({
				sessionDir: params.sessionDir,
				manifest: ownership.manifest
			});
		} catch {
			return false;
		}
	});
	return manifestMatches.length === 1 ? manifestMatches[0] : void 0;
}
function detectLegacyMeetingTranscripts(params) {
	const sourceDir = path.join(params.stateDir, "transcripts");
	if (params.doctorOnlyStateMigrations !== true) return {
		sourceDir,
		hasLegacy: false,
		pendingImportCount: 0
	};
	const databaseState = readMeetingTranscriptMigrationDetectionState({ env: {
		...params.env ?? process.env,
		OPENCLAW_STATE_DIR: params.stateDir
	} });
	const pendingImportCount = databaseState.pendingImportCount;
	const needsDatabaseRepair = pendingImportCount > 0 || databaseState.hasOversizedSessionSlugs;
	try {
		const rootStat = fs.lstatSync(sourceDir);
		if (rootStat.isSymbolicLink() || !rootStat.isDirectory()) throw new Error(`meeting transcript root must be a regular directory: ${sourceDir}`);
		const dateEntries = fs.readdirSync(sourceDir, { withFileTypes: true });
		const sourceSelectors = hasLegacyArtifactsSync(sourceDir) ? ["."] : [];
		for (const dateEntry of dateEntries) {
			if (!/^\d{4}-\d{2}-\d{2}$/.test(dateEntry.name)) continue;
			if (dateEntry.isSymbolicLink()) throw new Error(`legacy transcript date directory cannot be a symlink: ${dateEntry.name}`);
			if (!dateEntry.isDirectory()) continue;
			const dateDir = path.join(sourceDir, dateEntry.name);
			if (hasLegacyArtifactsSync(dateDir)) sourceSelectors.push(dateEntry.name);
			for (const entry of fs.readdirSync(dateDir, { withFileTypes: true })) {
				if (entry.isSymbolicLink()) throw new Error(`legacy transcript session cannot be a symlink: ${entry.name}`);
				if (entry.isDirectory() && hasLegacyArtifactsSync(path.join(dateDir, entry.name))) sourceSelectors.push(`${dateEntry.name}/${entry.name}`);
			}
		}
		return {
			sourceDir,
			hasLegacy: sourceSelectors.some((selector) => {
				const ownership = resolveMeetingTranscriptExportOwnership({
					state: databaseState,
					selector,
					sessionDir: path.join(sourceDir, selector),
					sourceRoot: sourceDir
				});
				return !ownership || !isRecordedCanonicalTranscriptExport({
					sessionDir: path.join(sourceDir, selector),
					manifest: ownership.manifest,
					pending: ownership.pending
				});
			}) || needsDatabaseRepair,
			pendingImportCount
		};
	} catch (error) {
		if (isRecord(error) && error.code === "ENOENT") return {
			sourceDir,
			hasLegacy: needsDatabaseRepair,
			pendingImportCount
		};
		throw error;
	}
}
function readMeetingTranscriptMigrationDetectionState(params) {
	const databasePath = resolveOpenClawStateSqlitePath(params.env);
	if (!fs.existsSync(databasePath)) return {
		exportOwnership: /* @__PURE__ */ new Map(),
		exportOwnershipByFoldedSelector: /* @__PURE__ */ new Map(),
		pendingImportCount: 0,
		hasOversizedSessionSlugs: false
	};
	const database = openNodeSqliteDatabase(databasePath, { readOnly: true });
	try {
		const tables = new Set(database.prepare("SELECT name FROM sqlite_schema WHERE type = 'table' AND name IN ('meeting_transcript_sessions', 'migration_sources')").all().map((row) => String(row.name)));
		const exportOwnership = /* @__PURE__ */ new Map();
		const exportOwnershipByFoldedSelector = /* @__PURE__ */ new Map();
		let hasOversizedSessionSlugs = false;
		if (tables.has("meeting_transcript_sessions")) {
			const rows = database.prepare("SELECT session_id, started_at, selector, session_slug, export_manifest_json, export_pending_json FROM meeting_transcript_sessions").all();
			for (const row of rows) {
				hasOversizedSessionSlugs ||= String(row.session_slug).length > 255;
				const selector = String(row.selector);
				const parsed = JSON.parse(String(row.export_manifest_json));
				if (isRecord(parsed)) {
					const ownership = {
						selector,
						sessionId: String(row.session_id),
						startedAt: String(row.started_at),
						manifest: parsed,
						pending: new Set(JSON.parse(String(row.export_pending_json)))
					};
					exportOwnership.set(selector, ownership);
					const foldedSelector = selector.toLowerCase();
					const foldedOwners = exportOwnershipByFoldedSelector.get(foldedSelector) ?? [];
					foldedOwners.push(ownership);
					exportOwnershipByFoldedSelector.set(foldedSelector, foldedOwners);
				}
			}
		}
		const pendingRow = tables.has("migration_sources") ? database.prepare("SELECT COUNT(*) AS count FROM migration_sources WHERE migration_kind = ? AND status = ? AND removed_source = 0").get("meeting-transcripts-files-v1", "imported") : void 0;
		return {
			exportOwnership,
			exportOwnershipByFoldedSelector,
			pendingImportCount: typeof pendingRow?.count === "number" ? pendingRow.count : 0,
			hasOversizedSessionSlugs
		};
	} finally {
		database.close();
	}
}
//#endregion
//#region src/infra/state-migrations.meeting-transcripts-insert.ts
function insertMeetingTranscriptSnapshots(params) {
	runOpenClawStateWriteTransaction(({ db: database }) => {
		const db = migrationDb(database);
		recordLegacyMigrationRun(database, {
			runId: params.runId,
			startedAt: params.now,
			finishedAt: null,
			status: "imported",
			reportJson: JSON.stringify({
				format: "meeting-transcripts-files-v1",
				sessions: params.snapshots.length,
				utterances: params.snapshots.reduce((total, snapshot) => total + snapshot.utteranceCount, 0),
				archiveRoot: params.archiveRoot,
				canonicalRelativeDirs: params.canonicalRelativeDirs
			})
		});
		for (const snapshot of params.snapshots) {
			executeSqliteQuerySync(database, db.insertInto("meeting_transcript_sessions").values({
				session_id: snapshot.session.sessionId,
				started_at: snapshot.session.startedAt,
				selector: transcriptSessionSelector(snapshot.session),
				export_key: transcriptSessionExportKey(snapshot.session),
				session_slug: safeTranscriptPathSegment(snapshot.session.sessionId),
				provider_id: snapshot.session.source.providerId,
				title: snapshot.session.title ?? null,
				source_json: JSON.stringify(snapshot.session.source),
				stopped_at: snapshot.session.stoppedAt ?? null,
				metadata_json: snapshot.session.metadata ? JSON.stringify(snapshot.session.metadata) : null,
				export_manifest_json: "{}",
				export_pending_json: "[]",
				next_utterance_seq: snapshot.utteranceCount,
				created_at_ms: params.now,
				updated_at_ms: params.now
			}));
			if (snapshot.utteranceCount > 0) for (let start = 0; start < snapshot.utteranceCount; start += 64) {
				const chunk = readStagedMeetingTranscriptUtterances({
					stageDatabase: params.stageDatabase,
					stageKey: snapshot.stageKey,
					start,
					limit: 64
				});
				executeSqliteQuerySync(database, db.insertInto("meeting_transcript_utterances").values(chunk.map((utterance, offset) => ({
					session_id: snapshot.session.sessionId,
					session_started_at: snapshot.session.startedAt,
					sequence: start + offset,
					utterance_id: utterance.id ?? null,
					started_at: utterance.startedAt ?? null,
					ended_at: utterance.endedAt ?? null,
					speaker_id: utterance.speaker?.id ?? null,
					speaker_label: utterance.speaker?.label ?? null,
					text: utterance.text,
					final: utterance.final === void 0 ? null : utterance.final ? 1 : 0,
					metadata_json: utterance.metadata ? JSON.stringify(utterance.metadata) : null
				}))));
			}
			if (snapshot.summary !== void 0 || snapshot.markdown !== void 0) executeSqliteQuerySync(database, db.insertInto("meeting_transcript_summaries").values({
				session_id: snapshot.session.sessionId,
				session_started_at: snapshot.session.startedAt,
				generated_at: snapshot.summary?.generatedAt ?? null,
				summary_json: snapshot.summary ? JSON.stringify(snapshot.summary) : null,
				markdown: snapshot.markdown ?? null,
				utterance_count: snapshot.summary?.utteranceCount ?? snapshot.utteranceCount
			}));
			recordLegacyMigrationSource(database, {
				sourceKey: resolveLegacyMigrationSourceKey("meeting-transcripts", snapshot.sourceDir),
				migrationKind: "meeting-transcripts-files-v1",
				sourcePath: snapshot.sourceDir,
				targetTable: "meeting_transcript_sessions",
				sourceSha256: snapshot.sourceHash,
				sourceSizeBytes: snapshot.sourceSizeBytes,
				sourceRecordCount: snapshot.utteranceCount,
				runId: params.runId,
				status: "imported",
				importedAt: params.now,
				reportJson: JSON.stringify({ selector: transcriptSessionSelector(snapshot.session) }),
				upsert: true,
				updateReportOnConflict: false
			});
		}
	}, { env: {
		...params.env,
		OPENCLAW_STATE_DIR: params.stateDir
	} }, { operationLabel: "meeting-transcripts.legacy-import" });
}
//#endregion
//#region src/infra/state-migrations.meeting-transcripts-verify.ts
function storedUtteranceFromRow(row) {
	const utterance = {
		sessionId: row.session_id,
		text: row.text
	};
	if (row.utterance_id !== null) utterance.id = row.utterance_id;
	if (row.started_at !== null) utterance.startedAt = row.started_at;
	if (row.ended_at !== null) utterance.endedAt = row.ended_at;
	if (row.speaker_label !== null) {
		const speaker = { label: row.speaker_label };
		if (row.speaker_id !== null) speaker.id = row.speaker_id;
		utterance.speaker = speaker;
	}
	if (row.final !== null) utterance.final = row.final === 1;
	if (row.metadata_json) utterance.metadata = JSON.parse(row.metadata_json);
	return utterance;
}
async function verifyImportedMeetingTranscriptSnapshots(params) {
	const selectUtterances = params.database.prepare(`
    SELECT
      ended_at,
      final,
      metadata_json,
      session_id,
      speaker_id,
      speaker_label,
      started_at,
      text,
      utterance_id
    FROM meeting_transcript_utterances
    WHERE session_id = ? AND session_started_at = ?
    ORDER BY sequence ASC
    LIMIT ? OFFSET ?
  `);
	for (const snapshot of params.snapshots) {
		const session = await params.store.readSession(transcriptSessionSelector(snapshot.session));
		if (!session || stableStringify(session) !== stableStringify(snapshot.session)) throw new Error(`meeting transcript import verification failed: ${snapshot.relativeDir}`);
		for (let start = 0; start < snapshot.utteranceCount; start += 64) {
			const expected = readStagedMeetingTranscriptUtterances({
				stageDatabase: params.stageDatabase,
				stageKey: snapshot.stageKey,
				start,
				limit: 64
			});
			const actual = selectUtterances.all(snapshot.session.sessionId, snapshot.session.startedAt, 64, start).map((row) => storedUtteranceFromRow(row));
			if (stableStringify(actual) !== stableStringify(expected)) throw new Error(`meeting transcript import verification failed: ${snapshot.relativeDir}`);
		}
		const summary = await params.store.readSummary(session);
		if (stableStringify(summary.summary) !== stableStringify(snapshot.summary) || stableStringify(summary.markdown?.trimEnd()) !== stableStringify(snapshot.markdown?.trimEnd())) throw new Error(`meeting transcript summary verification failed: ${snapshot.relativeDir}`);
	}
}
//#endregion
//#region src/infra/state-migrations.meeting-transcripts.ts
function resolveArchiveRoot(sourceRoot, now) {
	const base = `${sourceRoot}.migrated-${new Date(now).toISOString().replace(/[:.]/g, "-")}`;
	return fs.existsSync(base) ? `${base}-${randomUUID()}` : base;
}
function rollbackImportedSnapshots(params) {
	runOpenClawStateWriteTransaction(({ db: database }) => {
		const db = migrationDb(database);
		for (const snapshot of params.snapshots) executeSqliteQuerySync(database, db.deleteFrom("meeting_transcript_sessions").where("session_id", "=", snapshot.session.sessionId).where("started_at", "=", snapshot.session.startedAt));
		executeSqliteQuerySync(database, db.deleteFrom("migration_sources").where("last_run_id", "=", params.runId));
		executeSqliteQuerySync(database, db.deleteFrom("migration_runs").where("id", "=", params.runId));
	}, { env: {
		...params.env,
		OPENCLAW_STATE_DIR: params.stateDir
	} }, { operationLabel: "meeting-transcripts.legacy-import.rollback" });
}
function finishPendingMigration(params) {
	runOpenClawStateWriteTransaction(({ db: database }) => {
		const db = migrationDb(database);
		executeSqliteQuerySync(database, db.updateTable("migration_sources").set({
			status: "archived",
			removed_source: 1
		}).where("last_run_id", "=", params.runId).where("migration_kind", "=", "meeting-transcripts-files-v1"));
		const run = executeSqliteQueryTakeFirstSync(database, db.selectFrom("migration_runs").select("report_json").where("id", "=", params.runId));
		const report = run ? JSON.parse(run.report_json) : {};
		executeSqliteQuerySync(database, db.updateTable("migration_runs").set({
			finished_at: params.now,
			status: "completed",
			report_json: JSON.stringify({
				...report,
				archiveRoot: params.archiveRoot
			})
		}).where("id", "=", params.runId));
	}, { env: {
		...params.env,
		OPENCLAW_STATE_DIR: params.stateDir
	} }, { operationLabel: "meeting-transcripts.legacy-import.finish" });
}
function isStrictRelativePathWithinRoot(root, relativePath) {
	if (!relativePath || relativePath === "." || path.isAbsolute(relativePath)) return false;
	const resolvedRoot = path.resolve(root);
	const resolvedPath = path.resolve(resolvedRoot, relativePath);
	const relative = path.relative(resolvedRoot, resolvedPath);
	return Boolean(relative && !relative.startsWith("..") && !path.isAbsolute(relative));
}
async function snapshotPendingImportRun(params) {
	const snapshots = [];
	let hashesMatch = true;
	for (const source of params.run.sources) {
		const relativeDir = path.relative(params.sourceRoot, source.sourcePath) || ".";
		if (relativeDir.startsWith("..") || path.isAbsolute(relativeDir)) throw new Error(`pending meeting transcript source escaped its root: ${source.sourcePath}`);
		const snapshot = await snapshotLegacyMeetingTranscriptSession({
			rootDir: params.snapshotRoot,
			relativeDir,
			stageDatabase: params.stageDatabase
		});
		snapshots.push(snapshot);
		hashesMatch &&= snapshot.sourceHash === source.sourceHash;
	}
	return {
		snapshots,
		hashesMatch
	};
}
function readPendingImportRuns(params) {
	const database = openOpenClawStateDatabase({ env: {
		...params.env,
		OPENCLAW_STATE_DIR: params.stateDir
	} });
	const db = migrationDb(database.db);
	const rows = executeSqliteQuerySync(database.db, db.selectFrom("migration_sources as source").innerJoin("migration_runs as run", "run.id", "source.last_run_id").select([
		"source.last_run_id as run_id",
		"source.source_path",
		"source.source_sha256",
		"run.report_json as run_report_json"
	]).where("source.migration_kind", "=", "meeting-transcripts-files-v1").where("source.status", "=", "imported").where("source.removed_source", "=", 0).orderBy("source.source_path", "asc")).rows;
	const runs = /* @__PURE__ */ new Map();
	for (const row of rows) {
		const report = JSON.parse(row.run_report_json);
		const format = report.format;
		const archiveRoot = report.archiveRoot;
		const canonicalRelativeDirs = report.canonicalRelativeDirs;
		if (typeof archiveRoot !== "string" || format !== "meeting-transcripts-files-v1" || !archiveRoot.startsWith(`${params.sourceRoot}.migrated-`) || !Array.isArray(canonicalRelativeDirs) || !canonicalRelativeDirs.every((relativeDir) => typeof relativeDir === "string" && isStrictRelativePathWithinRoot(params.sourceRoot, relativeDir)) || typeof row.source_sha256 !== "string") throw new Error(`invalid pending meeting transcript migration receipt: ${row.run_id}`);
		const run = runs.get(row.run_id) ?? {
			runId: row.run_id,
			archiveRoot,
			canonicalRelativeDirs,
			sources: []
		};
		if (run.archiveRoot !== archiveRoot || JSON.stringify(run.canonicalRelativeDirs) !== JSON.stringify(canonicalRelativeDirs)) throw new Error(`conflicting meeting transcript archive receipts: ${row.run_id}`);
		run.sources.push({
			sourcePath: row.source_path,
			sourceHash: row.source_sha256
		});
		runs.set(row.run_id, run);
	}
	return [...runs.values()];
}
async function listCanonicalMeetingTranscriptExportDirs(params) {
	const state = readMeetingTranscriptMigrationDetectionState({ env: params.env });
	return (await listLegacyMeetingTranscriptArtifactDirs(params.rootDir)).filter((relativeDir) => {
		const selector = relativeDir.split(path.sep).join("/");
		const sessionDir = path.join(params.rootDir, relativeDir);
		const ownership = resolveMeetingTranscriptExportOwnership({
			state,
			selector,
			sessionDir,
			sourceRoot: params.rootDir
		});
		return Boolean(ownership && isRecordedCanonicalTranscriptExport({
			sessionDir,
			manifest: ownership.manifest,
			pending: ownership.pending
		}));
	});
}
async function resumePendingImports(params) {
	const runs = readPendingImportRuns(params);
	if (runs.length === 0) return;
	const changes = [];
	const warnings = [];
	for (const run of runs) {
		if (fs.existsSync(run.archiveRoot)) {
			try {
				const archived = await snapshotPendingImportRun({
					run,
					snapshotRoot: run.archiveRoot,
					sourceRoot: params.sourceRoot,
					stageDatabase: params.stageDatabase
				});
				if (!archived.hashesMatch) throw new Error("archived source hashes do not match migration receipts");
				const database = openOpenClawStateDatabase({ env: {
					...params.env,
					OPENCLAW_STATE_DIR: params.stateDir
				} });
				await verifyImportedMeetingTranscriptSnapshots({
					store: params.store,
					snapshots: archived.snapshots,
					stageDatabase: params.stageDatabase,
					database: database.db
				});
				await restoreCanonicalMeetingTranscriptExports({
					sourceRoot: params.sourceRoot,
					archiveRoot: run.archiveRoot,
					migratedSourcePaths: run.sources.map((source) => source.sourcePath),
					canonicalRelativeDirs: run.canonicalRelativeDirs
				});
				finishPendingMigration({
					runId: run.runId,
					archiveRoot: run.archiveRoot,
					now: Date.now(),
					env: params.env,
					stateDir: params.stateDir
				});
				changes.push(`Finalized interrupted meeting transcript archive → ${run.archiveRoot}`);
			} catch (error) {
				warnings.push(`Pending meeting transcript migration ${run.runId} archive could not be verified or restored; left its rows and files for recovery: ${String(error)}`);
			}
			continue;
		}
		if (!fs.existsSync(params.sourceRoot)) {
			warnings.push(`Pending meeting transcript migration ${run.runId} has neither source tree nor archive`);
			continue;
		}
		const canonicalRelativeDirs = [.../* @__PURE__ */ new Set([...run.canonicalRelativeDirs, ...await listCanonicalMeetingTranscriptExportDirs({
			rootDir: params.sourceRoot,
			env: {
				...params.env,
				OPENCLAW_STATE_DIR: params.stateDir
			}
		})])];
		const expectedRelativeDirs = [.../* @__PURE__ */ new Set([...run.sources.map((source) => path.relative(params.sourceRoot, source.sourcePath) || "."), ...canonicalRelativeDirs])].toSorted((a, b) => a.localeCompare(b));
		const pending = await snapshotPendingImportRun({
			run,
			snapshotRoot: params.sourceRoot,
			sourceRoot: params.sourceRoot,
			stageDatabase: params.stageDatabase
		});
		if (!pending.hashesMatch) {
			warnings.push(`Pending meeting transcript migration ${run.runId} source tree changed; left its imported rows and files for manual recovery`);
			continue;
		}
		const database = openOpenClawStateDatabase({ env: {
			...params.env,
			OPENCLAW_STATE_DIR: params.stateDir
		} });
		await verifyImportedMeetingTranscriptSnapshots({
			store: params.store,
			snapshots: pending.snapshots,
			stageDatabase: params.stageDatabase,
			database: database.db
		});
		await archiveLegacyMeetingTranscriptSnapshots({
			sourceRoot: params.sourceRoot,
			snapshots: pending.snapshots,
			expectedRelativeDirs,
			canonicalRelativeDirs,
			archiveRoot: run.archiveRoot
		});
		finishPendingMigration({
			runId: run.runId,
			archiveRoot: run.archiveRoot,
			now: Date.now(),
			env: params.env,
			stateDir: params.stateDir
		});
		changes.push(`Resumed and archived meeting transcript migration → ${run.archiveRoot}`);
	}
	return {
		changes,
		warnings
	};
}
async function migrateLegacyMeetingTranscripts(params) {
	const detected = params.detected;
	if (!detected?.hasLegacy) return {
		changes: [],
		warnings: []
	};
	const env = params.env ?? process.env;
	let lock;
	try {
		lock = await acquireGatewayLock({
			allowInTests: true,
			env: {
				...env,
				OPENCLAW_STATE_DIR: params.stateDir
			},
			role: "sqlite-maintenance",
			timeoutMs: 5e3
		});
	} catch (error) {
		return {
			changes: [],
			warnings: [`Skipped meeting transcript migration because exclusive state ownership is unavailable: ${String(error)}`]
		};
	}
	if (!lock) return {
		changes: [],
		warnings: ["Skipped meeting transcript migration because exclusive state ownership is unavailable"]
	};
	let stageDatabase;
	let stagePath;
	const recoveryChanges = [];
	try {
		fs.mkdirSync(params.stateDir, { recursive: true });
		stagePath = path.join(params.stateDir, `.meeting-transcripts-migration-${randomUUID()}.sqlite`);
		const stage = openLegacyMeetingTranscriptStage(stagePath);
		stageDatabase = stage;
		await validateMeetingTranscriptRoot(detected.sourceDir, { allowMissing: true });
		const databaseOptions = { env: {
			...env,
			OPENCLAW_STATE_DIR: params.stateDir
		} };
		ensureMeetingTranscriptsSchema(databaseOptions);
		const repaired = runOpenClawStateWriteTransaction(({ db: database }) => {
			const db = migrationDb(database);
			const rows = executeSqliteQuerySync(database, db.selectFrom("meeting_transcript_sessions").selectAll().where(({ fn, eb }) => eb(fn("length", ["session_slug"]), ">", 255)).orderBy("session_id")).rows;
			for (const row of rows) {
				const session = sessionFromRow(row);
				executeSqliteQuerySync(database, db.updateTable("meeting_transcript_sessions").set({
					selector: transcriptSessionSelector(session),
					session_slug: safeTranscriptPathSegment(session.sessionId),
					export_key: transcriptSessionExportKey(session)
				}).where("session_id", "=", row.session_id).where("started_at", "=", row.started_at));
			}
			return rows.length;
		}, databaseOptions, { operationLabel: "meeting-transcripts.oversized-projections" });
		if (repaired > 0) recoveryChanges.push(`Repaired ${repaired} oversized meeting transcript export name${repaired === 1 ? "" : "s"} in SQLite`);
		const store = new TranscriptsStore(detected.sourceDir, databaseOptions);
		const resumed = await resumePendingImports({
			env,
			stateDir: params.stateDir,
			sourceRoot: detected.sourceDir,
			store,
			stageDatabase: stage
		});
		if (resumed) return {
			changes: [...recoveryChanges, ...resumed.changes],
			warnings: resumed.warnings
		};
		const now = params.now?.() ?? Date.now();
		const relativeDirs = await listLegacyMeetingTranscriptArtifactDirs(detected.sourceDir);
		const sessionRelativeDirs = await listLegacyMeetingTranscriptSessionDirs(detected.sourceDir);
		const sessionRelativeDirSet = new Set(sessionRelativeDirs);
		const detectionState = readMeetingTranscriptMigrationDetectionState({ env: {
			...env,
			OPENCLAW_STATE_DIR: params.stateDir
		} });
		const legacyRelativeDirs = [];
		const partialRelativeDirs = [];
		const divergentExportDirs = [];
		for (const relativeDir of relativeDirs) {
			const ownership = resolveMeetingTranscriptExportOwnership({
				state: detectionState,
				selector: relativeDir.split(path.sep).join("/"),
				sessionDir: path.join(detected.sourceDir, relativeDir),
				sourceRoot: detected.sourceDir
			});
			if (ownership && isRecordedCanonicalTranscriptExport({
				sessionDir: path.join(detected.sourceDir, relativeDir),
				manifest: ownership.manifest,
				pending: ownership.pending
			})) continue;
			if (ownership) divergentExportDirs.push({
				relativeDir,
				ownerSelector: ownership.selector
			});
			else if (!sessionRelativeDirSet.has(relativeDir)) partialRelativeDirs.push(relativeDir);
			else legacyRelativeDirs.push(relativeDir);
		}
		const snapshots = [];
		for (const relativeDir of legacyRelativeDirs) snapshots.push(await snapshotLegacyMeetingTranscriptSession({
			rootDir: detected.sourceDir,
			relativeDir,
			stageDatabase: stage
		}));
		for (const snapshot of snapshots) {
			const database = openOpenClawStateDatabase(databaseOptions);
			if (executeSqliteQueryTakeFirstSync(database.db, migrationDb(database.db).selectFrom("meeting_transcript_sessions").select("session_id").where("session_id", "=", snapshot.session.sessionId).where("started_at", "=", snapshot.session.startedAt))) throw new Error(`legacy transcript conflicts with canonical SQLite state: ${snapshot.relativeDir}`);
		}
		if (divergentExportDirs.length > 0) {
			const recoveryRoot = `${detected.sourceDir}.exports-recovered-${new Date(now).toISOString().replace(/[:.]/g, "-")}`;
			for (const { relativeDir, ownerSelector } of divergentExportDirs) {
				const session = await store.readSession(ownerSelector);
				if (!session) throw new Error(`divergent transcript export has no SQLite owner: ${relativeDir}`);
				await archiveDivergentMeetingTranscriptExport({
					sourceRoot: detected.sourceDir,
					relativeDir,
					recoveryRoot
				});
				recoveryChanges.push(`Archived modified meeting transcript export ${relativeDir} → ${recoveryRoot}`);
				await store.materializeSessionArtifacts(session, "all");
			}
		}
		if (snapshots.length === 0 && partialRelativeDirs.length > 0) {
			const recoveryRoot = `${detected.sourceDir}.partials-recovered-${new Date(now).toISOString().replace(/[:.]/g, "-")}`;
			await archivePartialMeetingTranscriptArtifacts({
				sourceRoot: detected.sourceDir,
				relativeDirs: partialRelativeDirs,
				recoveryRoot
			});
			recoveryChanges.push(`Archived ${partialRelativeDirs.length} incomplete meeting transcript director${partialRelativeDirs.length === 1 ? "y" : "ies"} → ${recoveryRoot}`);
		}
		const expectedArchiveRelativeDirs = await listLegacyMeetingTranscriptSessionDirs(detected.sourceDir);
		if (snapshots.length === 0) return {
			changes: recoveryChanges,
			warnings: []
		};
		const runId = randomUUID();
		const archiveRoot = resolveArchiveRoot(detected.sourceDir, now);
		const canonicalRelativeDirs = await listCanonicalMeetingTranscriptExportDirs({
			rootDir: detected.sourceDir,
			env: {
				...env,
				OPENCLAW_STATE_DIR: params.stateDir
			}
		});
		insertMeetingTranscriptSnapshots({
			snapshots,
			runId,
			now,
			archiveRoot,
			canonicalRelativeDirs,
			stageDatabase: stage,
			env,
			stateDir: params.stateDir
		});
		try {
			await verifyImportedMeetingTranscriptSnapshots({
				store,
				snapshots,
				stageDatabase: stage,
				database: openOpenClawStateDatabase(databaseOptions).db
			});
			if (!await rehashLegacyMeetingTranscriptSnapshots(snapshots)) {
				rollbackImportedSnapshots({
					snapshots,
					runId,
					env,
					stateDir: params.stateDir
				});
				return {
					changes: recoveryChanges,
					warnings: ["Legacy meeting transcript files changed after import; rolled back SQLite rows and left every source in place for a Doctor retry"]
				};
			}
		} catch (error) {
			rollbackImportedSnapshots({
				snapshots,
				runId,
				env,
				stateDir: params.stateDir
			});
			throw error;
		}
		params.testHooks?.afterImport?.();
		let archiveRootAfterMove;
		try {
			archiveRootAfterMove = await archiveLegacyMeetingTranscriptSnapshots({
				sourceRoot: detected.sourceDir,
				snapshots,
				expectedRelativeDirs: expectedArchiveRelativeDirs,
				canonicalRelativeDirs,
				archiveRoot
			});
		} catch (error) {
			if (error instanceof LegacyMeetingTranscriptArchiveMovedError) return {
				changes: [...recoveryChanges, `Imported ${snapshots.length} meeting transcript session${snapshots.length === 1 ? "" : "s"} into shared SQLite state`],
				warnings: [`Meeting transcript archive needs Doctor resume after moving the source tree: ${String(error)}`]
			};
			rollbackImportedSnapshots({
				snapshots,
				runId,
				env,
				stateDir: params.stateDir
			});
			return {
				changes: recoveryChanges,
				warnings: [`Failed archiving verified legacy meeting transcripts; rolled back SQLite rows and left every source in place for Doctor retry: ${String(error)}`]
			};
		}
		params.testHooks?.afterArchive?.();
		finishPendingMigration({
			runId,
			archiveRoot: archiveRootAfterMove,
			now,
			env,
			stateDir: params.stateDir
		});
		const utteranceCount = snapshots.reduce((total, snapshot) => total + snapshot.utteranceCount, 0);
		return {
			changes: [
				...recoveryChanges,
				`Migrated ${snapshots.length} meeting transcript session${snapshots.length === 1 ? "" : "s"} and ${utteranceCount} utterance${utteranceCount === 1 ? "" : "s"} to shared SQLite state`,
				`Archived legacy meeting transcript files → ${archiveRootAfterMove}`
			],
			warnings: []
		};
	} catch (error) {
		return {
			changes: recoveryChanges,
			warnings: [`Failed migrating meeting transcripts: ${String(error)}`]
		};
	} finally {
		try {
			stageDatabase?.close();
			if (stagePath) {
				fs.rmSync(stagePath, { force: true });
				fs.rmSync(`${stagePath}-shm`, { force: true });
				fs.rmSync(`${stagePath}-wal`, { force: true });
			}
		} finally {
			await lock.release();
		}
	}
}
//#endregion
//#region src/infra/state-migrations.node-host.ts
const LEGACY_NODE_HOST_MAX_BYTES = 65536;
const CONFIG_KEYS = /* @__PURE__ */ new Set([
	"version",
	"nodeId",
	"token",
	"displayName",
	"gateway"
]);
const GATEWAY_KEYS = /* @__PURE__ */ new Set([
	"host",
	"port",
	"tls",
	"tlsFingerprint",
	"contextPath"
]);
/** Detect retired node-host state only when an explicit Doctor flow opts in. */
function detectLegacyNodeHostConfig(params) {
	const sourcePath = path.join(params.stateDir, LEGACY_NODE_HOST_CONFIG_FILE);
	return {
		sourcePath,
		hasLegacy: params.doctorOnlyStateMigrations === true && legacyMigrationSourceOrClaimMayExist(sourcePath, ".doctor-importing")
	};
}
function optionalLegacyString(value, label) {
	if (value === void 0) return;
	if (typeof value !== "string" || !value.trim()) throw new Error(`${label} must be a non-empty string`);
	return value.trim();
}
function optionalLegacyContextPath(value) {
	if (value === void 0) return;
	if (typeof value !== "string") throw new Error("legacy node-host gateway contextPath must be a string");
	return value.trim() || void 0;
}
function parseLegacyGateway(value) {
	if (value === void 0) return;
	if (!isRecord(value)) throw new Error("legacy node-host gateway must be an object");
	assertAllowedJsonFields(value, GATEWAY_KEYS, "legacy node-host gateway");
	const port = value.port;
	if (port !== void 0 && (typeof port !== "number" || !Number.isSafeInteger(port) || port <= 0 || port > 65535)) throw new Error("legacy node-host gateway port is invalid");
	if (value.tls !== void 0 && typeof value.tls !== "boolean") throw new Error("legacy node-host gateway tls must be a boolean");
	const gateway = {
		host: optionalLegacyString(value.host, "legacy node-host gateway host"),
		port,
		tls: value.tls,
		tlsFingerprint: optionalLegacyString(value.tlsFingerprint, "legacy node-host gateway tlsFingerprint"),
		contextPath: optionalLegacyContextPath(value.contextPath)
	};
	return Object.values(gateway).some((entry) => entry !== void 0) ? gateway : void 0;
}
function parseLegacyNodeHostConfig(snapshot) {
	const parsed = JSON.parse(snapshot.raw);
	if (!isRecord(parsed)) throw new Error("legacy node-host config must be an object");
	assertAllowedJsonFields(parsed, CONFIG_KEYS, "legacy node-host config");
	if (parsed.version !== 1) throw new Error("legacy node-host config version must be 1");
	if (typeof parsed.nodeId !== "string" || !parsed.nodeId.trim()) throw new Error("legacy node-host nodeId must be a non-empty string");
	if (parsed.token !== void 0 && typeof parsed.token !== "string") throw new Error("legacy node-host token must be a string when present");
	return {
		config: {
			version: 1,
			nodeId: parsed.nodeId.trim(),
			displayName: optionalLegacyString(parsed.displayName, "legacy node-host displayName"),
			gateway: parseLegacyGateway(parsed.gateway)
		},
		updatedAtMs: Math.max(0, Math.floor(snapshot.mtimeMs))
	};
}
function nullableNonEmptyString(value, label) {
	if (value === null || value === void 0) return;
	if (typeof value !== "string" || !value.trim()) throw new Error(`invalid node-host SQLite row: ${label} must not be empty`);
	return value.trim();
}
function rowToCanonicalState(row) {
	const value = JSON.parse(row.value_json);
	if (!isRecord(value) || value.version !== 1 || typeof value.nodeId !== "string" || !value.nodeId.trim()) throw new Error("invalid canonical node-host SQLite identity");
	if (!Number.isSafeInteger(row.updated_at_ms) || row.updated_at_ms < 0) throw new Error("invalid canonical node-host SQLite timestamp");
	const storedGateway = value.gateway;
	if (storedGateway !== void 0 && !isRecord(storedGateway)) throw new Error("invalid canonical node-host SQLite gateway");
	const gatewayPort = storedGateway?.port;
	if (gatewayPort !== void 0 && (typeof gatewayPort !== "number" || !Number.isSafeInteger(gatewayPort) || gatewayPort <= 0 || gatewayPort > 65535)) throw new Error("invalid canonical node-host SQLite gateway port");
	const gatewayTls = storedGateway?.tls;
	if (gatewayTls !== void 0 && typeof gatewayTls !== "boolean") throw new Error("invalid canonical node-host SQLite gateway tls");
	if (value.installedAppsSharing !== void 0 && typeof value.installedAppsSharing !== "boolean") throw new Error("invalid canonical node-host SQLite installed-app sharing");
	const cloudflareAccess = normalizeNodeHostCloudflareAccessConfig(storedGateway?.cloudflareAccess);
	const gateway = {
		host: nullableNonEmptyString(storedGateway?.host, "gateway_host"),
		port: typeof gatewayPort === "number" ? gatewayPort : void 0,
		tls: typeof gatewayTls === "boolean" ? gatewayTls : void 0,
		tlsFingerprint: nullableNonEmptyString(storedGateway?.tlsFingerprint, "gateway_tls_fingerprint"),
		contextPath: nullableNonEmptyString(storedGateway?.contextPath, "gateway_context_path"),
		...cloudflareAccess ? { cloudflareAccess } : {}
	};
	return {
		config: {
			version: 1,
			nodeId: value.nodeId.trim(),
			displayName: nullableNonEmptyString(value.displayName, "display_name"),
			gateway: Object.values(gateway).some((entry) => entry !== void 0) ? gateway : void 0,
			installedAppsSharing: value.installedAppsSharing === true
		},
		updatedAtMs: row.updated_at_ms
	};
}
function configsEqual(left, right) {
	return left.nodeId === right.nodeId && left.displayName === right.displayName && left.gateway?.host === right.gateway?.host && left.gateway?.port === right.gateway?.port && left.gateway?.tls === right.gateway?.tls && left.gateway?.tlsFingerprint === right.gateway?.tlsFingerprint && left.gateway?.contextPath === right.gateway?.contextPath && JSON.stringify(left.gateway?.cloudflareAccess) === JSON.stringify(right.gateway?.cloudflareAccess);
}
function writeCanonicalState(db, state) {
	const row = {
		state_key: NODE_HOST_CONFIG_KEY,
		value_json: JSON.stringify({
			...state.config,
			installedAppsSharing: state.config.installedAppsSharing ?? false
		}),
		updated_at_ms: state.updatedAtMs
	};
	const { state_key: _stateKey, ...updates } = row;
	executeSqliteQuerySync(db, getNodeSqliteKysely(db).insertInto("config_machine_state").values(row).onConflict((conflict) => conflict.column("state_key").doUpdateSet(updates)));
}
function migrateIntoDatabase$1(params) {
	let imported = false;
	let preservedCanonical = false;
	runOpenClawStateWriteTransaction(({ db }) => {
		const stateDb = getNodeSqliteKysely(db);
		const row = executeSqliteQueryTakeFirstSync(db, stateDb.selectFrom("config_machine_state").selectAll().where("state_key", "=", NODE_HOST_CONFIG_KEY));
		const existing = row ? rowToCanonicalState(row) : null;
		if (existing && existing.config.nodeId !== params.legacy.config.nodeId) throw new Error("legacy node-host nodeId conflicts with canonical SQLite identity");
		let expected = params.legacy;
		if (existing) {
			if (configsEqual(existing.config, params.legacy.config)) expected = existing.updatedAtMs >= params.legacy.updatedAtMs ? existing : params.legacy;
			else if (existing.updatedAtMs === params.legacy.updatedAtMs) throw new Error("legacy node-host config diverges at the same timestamp");
			else if (existing.updatedAtMs > params.legacy.updatedAtMs) {
				expected = existing;
				preservedCanonical = true;
			}
		}
		if (!existing || !configsEqual(existing.config, expected.config) || existing.updatedAtMs !== expected.updatedAtMs) {
			if (expected === params.legacy && existing?.config.installedAppsSharing) expected = {
				...expected,
				config: {
					...expected.config,
					installedAppsSharing: true
				}
			};
			writeCanonicalState(db, expected);
			imported = expected.updatedAtMs === params.legacy.updatedAtMs;
		}
		const verifiedRow = executeSqliteQueryTakeFirstSync(db, stateDb.selectFrom("config_machine_state").selectAll().where("state_key", "=", NODE_HOST_CONFIG_KEY));
		if (!verifiedRow) throw new Error("SQLite verification failed for node-host config");
		const verified = rowToCanonicalState(verifiedRow);
		if (!configsEqual(verified.config, expected.config) || verified.updatedAtMs !== expected.updatedAtMs) throw new Error("SQLite verification failed for node-host config");
	}, { env: params.env });
	return {
		imported,
		preservedCanonical
	};
}
async function migrateWithExclusiveStateOwnership$2(params) {
	if (!params.detected.hasLegacy) return {
		changes: [],
		warnings: []
	};
	const changes = [];
	const warnings = [];
	const notices = [];
	const sourcePath = params.detected.sourcePath;
	const source = new LegacyMigrationSourceClaim({
		stateRoot: params.stateRoot,
		stateDir: params.stateDir,
		sourcePath,
		label: "node-host",
		claimSuffix: LEGACY_NODE_HOST_CONFIG_CLAIM_SUFFIX,
		readSnapshot: (snapshotPath) => readLegacyMigrationSourceSnapshot({
			stateRoot: params.stateRoot,
			stateDir: params.stateDir,
			sourcePath: snapshotPath,
			maxBytes: LEGACY_NODE_HOST_MAX_BYTES,
			label: "node-host",
			hashDecodedText: true
		})
	});
	let snapshot;
	let legacy;
	try {
		await source.recover("interrupted node-host Doctor claim conflicts with its source");
		if (!await source.exists()) return {
			changes,
			warnings
		};
		snapshot = await source.read();
		legacy = parseLegacyNodeHostConfig(snapshot);
		params.beforeVerify?.();
		if (!legacyMigrationSourceSnapshotsMatch(await source.read(), snapshot)) throw new Error("legacy node-host source changed after Doctor loaded it");
	} catch (error) {
		warnings.push(`Failed reading legacy node-host state: ${String(error)}`);
		return {
			changes,
			warnings
		};
	}
	try {
		await source.claim({
			snapshot,
			mismatchMessage: "legacy node-host source changed before Doctor could claim it",
			beforeClaim: params.beforeClaim
		});
	} catch (error) {
		const restoreError = await source.restore();
		warnings.push(`Failed migrating legacy node-host state: ${String(error)}${restoreError ? `; restore failure: ${restoreError}` : ""}`);
		return {
			changes,
			warnings
		};
	}
	let result;
	try {
		result = migrateIntoDatabase$1({
			env: params.env,
			legacy
		});
	} catch (error) {
		const restoreError = await source.restore();
		warnings.push(`Failed migrating legacy node-host state: ${String(error)}${restoreError ? `; restore failure: ${restoreError}` : ""}`);
		return {
			changes,
			warnings
		};
	}
	try {
		await source.remove({
			removeSource: params.removeSource,
			sourceReappearedMessage: `legacy node-host source reappeared during import: ${sourcePath}`,
			remainingMessage: "legacy node-host source or Doctor claim remains after cleanup"
		});
	} catch (error) {
		warnings.push(`Node-host state is in SQLite, but legacy cleanup failed: ${String(error)}`);
		return {
			changes,
			warnings
		};
	}
	changes.push(result.preservedCanonical ? "Kept newer canonical node-host SQLite state." : result.imported ? "Migrated node-host config to shared SQLite state." : "Verified node-host config in shared SQLite state.");
	notices.push("Removed retired node.json after verified SQLite import.");
	return {
		changes,
		warnings,
		notices
	};
}
/** Import retired node-host state while excluding active Gateway/state maintenance owners. */
async function migrateLegacyNodeHostConfig(params) {
	if (!params.detected.hasLegacy) return {
		changes: [],
		warnings: []
	};
	return await withLegacyMigrationStateLock({
		stateDir: params.stateDir,
		env: params.env,
		label: "legacy node-host state",
		releaseLabel: "Node-host",
		errorLabel: "Failed reading legacy node-host state",
		retryGuidance: "Stop the Gateway and node host, then run `openclaw doctor --fix` again.",
		run: async (env) => {
			const stateRoot = await root(params.stateDir, {
				hardlinks: "reject",
				maxBytes: LEGACY_NODE_HOST_MAX_BYTES,
				symlinks: "reject"
			});
			return await migrateWithExclusiveStateOwnership$2({
				...params,
				env,
				stateRoot
			});
		}
	});
}
//#endregion
//#region src/infra/state-migrations.rescue-pending.ts
function resolveLegacyRescuePendingPaths(stateDir) {
	return ["crestodian", "openclaw"].map((owner) => path.join(stateDir, owner, "rescue-pending"));
}
function isSafeLegacyOwnerDirectory(stateDir, sourcePath) {
	const ownerPath = path.dirname(sourcePath);
	try {
		const owner = fs.lstatSync(ownerPath);
		return owner.isDirectory() && !owner.isSymbolicLink() && path.resolve(path.dirname(ownerPath)) === path.resolve(stateDir);
	} catch {
		return false;
	}
}
/** Detect retired security capabilities only during an explicit doctor run. */
function detectLegacyRescuePending(params) {
	const sourcePaths = resolveLegacyRescuePendingPaths(params.stateDir);
	return {
		sourcePaths,
		hasLegacy: params.doctorOnlyStateMigrations === true && sourcePaths.some((sourcePath) => fs.existsSync(sourcePath))
	};
}
/** Discard retired one-shot capabilities; importing them could reactivate stale writes. */
function discardLegacyRescuePending(params) {
	if (!params.detected.hasLegacy) return {
		changes: [],
		warnings: []
	};
	const removed = [];
	const warnings = [];
	for (const sourcePath of resolveLegacyRescuePendingPaths(params.stateDir)) {
		if (!fs.existsSync(sourcePath)) continue;
		if (!isSafeLegacyOwnerDirectory(params.stateDir, sourcePath)) {
			warnings.push(`Refused to remove retired rescue approvals through unsafe path ${sourcePath}`);
			continue;
		}
		try {
			fs.rmSync(sourcePath, {
				recursive: true,
				force: true
			});
			removed.push(sourcePath);
		} catch (error) {
			warnings.push(`Failed removing retired rescue approvals at ${sourcePath}: ${String(error)}`);
		}
	}
	return {
		changes: removed.length > 0 ? [`Discarded retired system-agent rescue approvals from ${removed.join(", ")}`] : [],
		warnings
	};
}
//#endregion
//#region src/infra/state-migrations.restart-sentinel.ts
const LEGACY_RESTART_SENTINEL_FILENAME = "restart-sentinel.json";
const DOCTOR_CLAIM_SUFFIX$1 = ".doctor-importing";
const MAX_LEGACY_RESTART_SENTINEL_BYTES = 4194304;
const MIGRATION_KIND$3 = "legacy-restart-sentinel-json";
const utf8Decoder$1 = new TextDecoder("utf-8", { fatal: true });
/** Detect the exact retired file for startup preflight and explicit Doctor alike. */
function detectLegacyRestartSentinel(params) {
	const sourcePath = path.join(params.stateDir, LEGACY_RESTART_SENTINEL_FILENAME);
	return {
		sourcePath,
		hasLegacy: legacyMigrationSourceOrClaimMayExist(sourcePath, DOCTOR_CLAIM_SUFFIX$1)
	};
}
function parseLegacyEnvelope(snapshot) {
	try {
		return parseRestartSentinelEnvelope(JSON.parse(utf8Decoder$1.decode(snapshot.buffer)));
	} catch {
		return null;
	}
}
function decideAndRecordMigration(params) {
	const sourceKey = resolveLegacyMigrationSourceKey("restart-sentinel-json", params.sourcePath);
	const runId = `${sourceKey}:${params.snapshot.sha256.slice(0, 16)}`;
	const now = Date.now();
	return runOpenClawStateWriteTransaction(({ db }) => {
		const receipt = readLegacyMigrationReceiptFromDatabase(db, sourceKey);
		const before = readRestartSentinelRowSync(db);
		let decision;
		if (receipt) decision = "receipt-authoritative";
		else if (!params.envelope) decision = "malformed-legacy-discarded";
		else if (before.kind === "valid") decision = "canonical-preserved";
		else {
			const written = writeRestartSentinelRowSync(db, params.envelope.payload);
			const verified = readRestartSentinelRowSync(db);
			if (verified.kind !== "valid" || verified.sentinel.revision !== written.revision || !isDeepStrictEqual(verified.sentinel.payload, params.envelope.payload)) throw new Error("SQLite verification failed for the restart sentinel migration");
			decision = before.kind === "invalid" ? "invalid-canonical-repaired" : "legacy-imported";
		}
		const reportJson = JSON.stringify({
			source: MIGRATION_KIND$3,
			target: "gateway_restart_sentinel",
			decision,
			sourceSha256: params.snapshot.sha256,
			sourceValid: params.envelope !== null,
			importedRecordCount: decision === "legacy-imported" || decision === "invalid-canonical-repaired" ? 1 : 0,
			preservedSqliteRecordCount: decision === "canonical-preserved" ? 1 : 0
		});
		recordLegacyMigrationReceipt(db, {
			sourceKey,
			migrationKind: MIGRATION_KIND$3,
			sourcePath: params.sourcePath,
			targetTable: "gateway_restart_sentinel",
			sourceSha256: params.snapshot.sha256,
			sourceSizeBytes: params.snapshot.size,
			sourceRecordCount: params.envelope ? 1 : 0,
			runId,
			now,
			reportJson,
			upsert: true
		});
		return {
			decision,
			sourceKey
		};
	}, { env: params.env });
}
async function recoverInterruptedClaim$1(params) {
	if (!await params.source.exists(true)) return;
	if (!await params.source.exists()) {
		const restoreError = await params.source.restore();
		if (restoreError) throw new Error(restoreError);
		return;
	}
	if (!readLegacyMigrationReceipt(resolveLegacyMigrationSourceKey("restart-sentinel-json", params.source.sourcePath), params.env)) throw new Error("legacy restart sentinel source and interrupted claim both exist");
	await params.source.read(true);
	await params.source.remove({ skipSourceCheck: true });
}
function decisionChange(decision) {
	switch (decision) {
		case "legacy-imported": return "Imported the legacy restart sentinel into shared SQLite state.";
		case "invalid-canonical-repaired": return "Replaced an invalid SQLite restart sentinel with validated legacy state.";
		case "canonical-preserved": return "Preserved the canonical SQLite restart sentinel and discarded conflicting legacy JSON.";
		case "malformed-legacy-discarded": return "Discarded malformed retired restart sentinel JSON without importing it.";
		case "receipt-authoritative": return "Discarded recreated retired restart sentinel JSON using its migration receipt.";
	}
	return decision;
}
async function migrateWithExclusiveStateOwnership$1(params) {
	const changes = [];
	const warnings = [];
	const notices = [];
	const sourcePath = params.detected.sourcePath;
	const source = new LegacyMigrationSourceClaim({
		stateRoot: params.stateRoot,
		stateDir: params.stateDir,
		sourcePath,
		label: "restart sentinel",
		includeFilePath: false,
		claimSuffix: DOCTOR_CLAIM_SUFFIX$1,
		readSnapshot: (snapshotPath) => readLegacyMigrationSourceSnapshot({
			stateRoot: params.stateRoot,
			stateDir: params.stateDir,
			sourcePath: snapshotPath,
			maxBytes: MAX_LEGACY_RESTART_SENTINEL_BYTES,
			label: "restart sentinel"
		})
	});
	try {
		await recoverInterruptedClaim$1({
			source,
			env: params.env
		});
	} catch (error) {
		return {
			changes,
			warnings: [`Failed recovering a legacy restart sentinel Doctor claim: ${String(error)}`]
		};
	}
	if (!await source.exists()) return {
		changes,
		warnings
	};
	let snapshot;
	try {
		snapshot = await source.read();
	} catch (error) {
		return {
			changes,
			warnings: [`Failed reading the legacy restart sentinel: ${String(error)}`]
		};
	}
	const envelope = parseLegacyEnvelope(snapshot);
	try {
		params.beforeVerify?.();
		const current = await source.read();
		if (!legacyMigrationSourceSnapshotsMatch(current, snapshot)) throw new Error("legacy restart sentinel changed after migration loaded it");
		await source.claim({
			snapshot,
			mismatchMessage: "legacy restart sentinel changed before migration could claim it",
			beforeClaim: params.beforeClaim
		});
	} catch (error) {
		const restoreError = await source.restore();
		return {
			changes,
			warnings: [`Failed claiming the legacy restart sentinel: ${String(error)}${restoreError ? `; restore failure: ${restoreError}` : ""}`]
		};
	}
	let result;
	try {
		result = decideAndRecordMigration({
			env: params.env,
			sourcePath,
			snapshot,
			envelope
		});
	} catch (error) {
		const restoreError = await source.restore();
		return {
			changes,
			warnings: [`Failed migrating the legacy restart sentinel: ${String(error)}${restoreError ? `; restore failure: ${restoreError}` : ""}`]
		};
	}
	try {
		await source.remove({
			removeSource: params.removeSource,
			sourceReappearedMessage: "legacy restart sentinel reappeared during migration cleanup",
			remainingMessage: "legacy restart sentinel remains after migration cleanup"
		});
	} catch (error) {
		warnings.push(`Legacy restart sentinel cleanup failed: ${String(error)}`);
		return {
			changes,
			warnings
		};
	}
	try {
		markLegacyMigrationSourceRemoved(result.sourceKey, params.env);
	} catch (error) {
		warnings.push(`Legacy restart sentinel was removed, but its receipt could not be finalized: ${String(error)}`);
	}
	changes.push(decisionChange(result.decision));
	notices.push("Removed retired restart-sentinel.json after recording its migration decision.");
	return {
		changes,
		warnings,
		notices
	};
}
/** Import or retire the old file under exclusive state ownership. */
async function migrateLegacyRestartSentinel(params) {
	const detected = params.detected;
	if (!detected?.hasLegacy) return {
		changes: [],
		warnings: []
	};
	return await withLegacyMigrationStateLock({
		stateDir: params.stateDir,
		env: params.env,
		label: "the legacy restart sentinel",
		releaseLabel: "Restart sentinel",
		errorLabel: "Failed reading the legacy restart sentinel",
		retryGuidance: "Stop the Gateway, then run `openclaw doctor --fix` again.",
		run: async (env) => {
			const stateRoot = await root(params.stateDir, {
				hardlinks: "reject",
				maxBytes: MAX_LEGACY_RESTART_SENTINEL_BYTES,
				symlinks: "reject"
			});
			return await migrateWithExclusiveStateOwnership$1({
				...params,
				detected,
				env,
				stateRoot
			});
		}
	});
}
//#endregion
//#region src/infra/state-migrations.runtime-state.ts
const VOICEWAKE_TRIGGERS_STATE_KEY = "voicewake.triggers";
const VOICEWAKE_ROUTING_STATE_KEY = "voicewake.routing";
const DEFAULT_VOICEWAKE_TRIGGERS = [
	"openclaw",
	"claude",
	"computer"
];
function resolveLegacyVoiceWakeTriggersPath(stateDir) {
	return path.join(stateDir, "settings", "voicewake.json");
}
function resolveLegacyVoiceWakeRoutingPath(stateDir) {
	return path.join(stateDir, "settings", "voicewake-routing.json");
}
function readLegacyJsonObject(sourcePath) {
	return JSON.parse(fs.readFileSync(sourcePath, "utf8"));
}
/** Import and archive legacy JSON only after its synchronous SQLite commit succeeds. */
function migrateLegacyJsonState(params) {
	const changes = [];
	const warnings = [];
	if (!migrationFileExists(params.sourcePath)) return {
		changes,
		warnings
	};
	let value;
	try {
		value = params.normalize(readLegacyJsonObject(params.sourcePath));
	} catch (err) {
		warnings.push(`Failed reading legacy ${params.label} ${params.sourcePath}: ${String(err)}`);
		return {
			changes,
			warnings
		};
	}
	if (params.shouldMigrate && !params.shouldMigrate(value)) return {
		changes,
		warnings
	};
	let outcome;
	try {
		outcome = runOpenClawStateWriteTransaction(({ db }) => params.migrate(db, value), { env: {
			...process.env,
			OPENCLAW_STATE_DIR: params.stateDir
		} });
	} catch (err) {
		warnings.push(`Failed migrating legacy ${params.label}: ${String(err)}`);
		return {
			changes,
			warnings
		};
	}
	changes.push(...outcome.changes);
	if (params.retire) params.retire({
		sourcePath: params.sourcePath,
		changes,
		warnings
	});
	else archiveLegacyImportSource({
		sourcePath: params.sourcePath,
		label: params.label,
		changes,
		warnings
	});
	return outcome.notices?.length ? {
		changes,
		warnings,
		notices: outcome.notices
	} : {
		changes,
		warnings
	};
}
function normalizeLegacyVoiceWakeTriggers(input) {
	const rec = input && typeof input === "object" ? input : {};
	const triggers = Array.isArray(rec.triggers) ? rec.triggers.flatMap((entry) => typeof entry === "string" ? [entry.trim()] : []).filter((entry) => entry.length > 0) : [];
	return triggers.length > 0 ? triggers : DEFAULT_VOICEWAKE_TRIGGERS;
}
function importLegacyVoiceWakeMachineState(database, key, value) {
	const db = getNodeSqliteKysely(database);
	const existing = executeSqliteQueryTakeFirstSync(database, db.selectFrom("config_machine_state").select("value_json").where("state_key", "=", key));
	if (existing) return {
		current: JSON.parse(existing.value_json),
		imported: false
	};
	executeSqliteQuerySync(database, db.insertInto("config_machine_state").values({
		state_key: key,
		value_json: JSON.stringify(value),
		updated_at_ms: Date.now()
	}));
	return {
		current: value,
		imported: true
	};
}
function migrateLegacyVoiceWakeSettings(params) {
	const triggerMigration = migrateLegacyJsonState({
		sourcePath: params.detected.triggersPath,
		stateDir: params.stateDir,
		label: "voice wake triggers",
		normalize: normalizeLegacyVoiceWakeTriggers,
		shouldMigrate: (triggers) => triggers.length > 0,
		migrate(db, triggers) {
			const imported = importLegacyVoiceWakeMachineState(db, VOICEWAKE_TRIGGERS_STATE_KEY, triggers);
			if (!imported.imported) return {
				changes: [],
				...JSON.stringify(imported.current) === JSON.stringify(triggers) ? {} : { notices: [`Kept shared SQLite voice wake triggers because legacy file differs: ${params.detected.triggersPath}`] }
			};
			return { changes: [`Migrated ${triggers.length} voice wake ${triggers.length === 1 ? "trigger" : "triggers"} → shared SQLite state`] };
		}
	});
	const routingMigration = migrateLegacyJsonState({
		sourcePath: params.detected.routingPath,
		stateDir: params.stateDir,
		label: "voice wake routing",
		normalize: normalizeVoiceWakeRoutingConfig,
		shouldMigrate: Boolean,
		migrate(db, routingConfig) {
			const imported = importLegacyVoiceWakeMachineState(db, VOICEWAKE_ROUTING_STATE_KEY, {
				...routingConfig,
				updatedAtMs: Date.now()
			});
			if (!imported.imported) {
				const existing = normalizeVoiceWakeRoutingConfig(imported.current);
				return {
					changes: [],
					...JSON.stringify(existing.defaultTarget) === JSON.stringify(routingConfig.defaultTarget) && JSON.stringify(existing.routes) === JSON.stringify(routingConfig.routes) ? {} : { notices: [`Kept shared SQLite voice wake routing because legacy file differs: ${params.detected.routingPath}`] }
				};
			}
			return { changes: [`Migrated voice wake routing config with ${routingConfig.routes.length} ${routingConfig.routes.length === 1 ? "route" : "routes"} → shared SQLite state`] };
		}
	});
	const changes = [...triggerMigration.changes, ...routingMigration.changes];
	const warnings = [...triggerMigration.warnings, ...routingMigration.warnings];
	const notices = [...triggerMigration.notices ?? [], ...routingMigration.notices ?? []];
	return notices.length > 0 ? {
		changes,
		warnings,
		notices
	} : {
		changes,
		warnings
	};
}
function resolveLegacyConfigHealthPath(stateDir) {
	return path.join(stateDir, "logs", "config-health.json");
}
function normalizeLegacyConfigHealthEntry(configPath, input) {
	if (!configPath.trim() || !input || typeof input !== "object" || Array.isArray(input)) return null;
	const entry = input;
	const lastKnownGoodJson = entry.lastKnownGood && typeof entry.lastKnownGood === "object" ? JSON.stringify(entry.lastKnownGood) : null;
	const lastPromotedGoodJson = entry.lastPromotedGood && typeof entry.lastPromotedGood === "object" ? JSON.stringify(entry.lastPromotedGood) : null;
	const lastObservedSuspiciousSignature = typeof entry.lastObservedSuspiciousSignature === "string" ? entry.lastObservedSuspiciousSignature : null;
	if (!lastKnownGoodJson && !lastPromotedGoodJson && !lastObservedSuspiciousSignature) return null;
	return {
		configPath,
		lastKnownGoodJson,
		lastPromotedGoodJson,
		lastObservedSuspiciousSignature
	};
}
function normalizeLegacyConfigHealthFile(input) {
	const entries = (input && typeof input === "object" ? input : {}).entries;
	if (!entries || typeof entries !== "object" || Array.isArray(entries)) return [];
	return Object.entries(entries).flatMap(([configPath, entry]) => {
		const normalized = normalizeLegacyConfigHealthEntry(configPath, entry);
		return normalized ? [normalized] : [];
	}).toSorted((a, b) => a.configPath.localeCompare(b.configPath));
}
function configHealthRow(entry) {
	return {
		config_path: entry.configPath,
		last_known_good_json: entry.lastKnownGoodJson,
		last_promoted_good_json: entry.lastPromotedGoodJson,
		last_observed_suspicious_signature: entry.lastObservedSuspiciousSignature,
		updated_at_ms: Date.now()
	};
}
function retireLegacyConfigHealthSource(params) {
	const archivedPath = `${params.sourcePath}.migrated`;
	if (!migrationFileExists(archivedPath)) {
		archiveLegacyImportSource({
			sourcePath: params.sourcePath,
			label: "config health state",
			changes: params.changes,
			warnings: params.warnings
		});
		return;
	}
	try {
		fs.rmSync(params.sourcePath, { force: true });
		params.changes.push("Removed regenerated config health legacy source");
	} catch (err) {
		params.warnings.push(`Failed removing regenerated config health legacy source: ${String(err)}`);
	}
}
function migrateLegacyConfigHealth(params) {
	return migrateLegacyJsonState({
		sourcePath: params.detected.sourcePath,
		stateDir: params.stateDir,
		label: "config health state",
		normalize: normalizeLegacyConfigHealthFile,
		retire: retireLegacyConfigHealthSource,
		migrate(db, entries) {
			const stateDb = getNodeSqliteKysely(db);
			const existing = executeSqliteQuerySync(db, stateDb.selectFrom("config_health_entries").select([
				"config_path",
				"last_known_good_json",
				"last_promoted_good_json",
				"last_observed_suspicious_signature"
			])).rows;
			const existingByPath = new Map(existing.map((row) => [row.config_path, row]));
			const entriesToInsert = [];
			let reconciledCount = 0;
			for (const entry of entries) {
				const existingEntry = existingByPath.get(entry.configPath);
				if (!existingEntry) {
					entriesToInsert.push(entry);
					continue;
				}
				const lastKnownGoodJson = existingEntry.last_known_good_json ?? entry.lastKnownGoodJson;
				const lastPromotedGoodJson = existingEntry.last_promoted_good_json ?? entry.lastPromotedGoodJson;
				if (lastKnownGoodJson === existingEntry.last_known_good_json && lastPromotedGoodJson === existingEntry.last_promoted_good_json) continue;
				executeSqliteQuerySync(db, stateDb.updateTable("config_health_entries").set({
					last_known_good_json: lastKnownGoodJson,
					last_promoted_good_json: lastPromotedGoodJson,
					updated_at_ms: Date.now()
				}).where("config_path", "=", entry.configPath));
				reconciledCount += 1;
			}
			if (entriesToInsert.length > 0) executeSqliteQuerySync(db, stateDb.insertInto("config_health_entries").values(entriesToInsert.map(configHealthRow)));
			const changes = [];
			if (entriesToInsert.length > 0) changes.push(`Migrated ${entriesToInsert.length} config health ${entriesToInsert.length === 1 ? "entry" : "entries"} → shared SQLite state`);
			if (reconciledCount > 0) changes.push(`Reconciled ${reconciledCount} config health ${reconciledCount === 1 ? "entry" : "entries"} → shared SQLite state`);
			return { changes };
		}
	});
}
function resolveLegacyPluginBindingApprovalsPath(env, homedir) {
	return path.join(resolveRequiredHomeDir(env, homedir), ".openclaw", "plugin-binding-approvals.json");
}
function pluginBindingApprovalScopeKey(entry) {
	return [
		entry.pluginRoot,
		normalizeLowercaseStringOrEmpty(entry.channel),
		entry.accountId
	].join("::");
}
function normalizeLegacyPluginBindingApprovalEntry(input) {
	const entry = input && typeof input === "object" ? input : {};
	const pluginRoot = typeof entry.pluginRoot === "string" ? entry.pluginRoot.trim() : "";
	const pluginId = typeof entry.pluginId === "string" ? entry.pluginId.trim() : "";
	const channel = typeof entry.channel === "string" ? normalizeLowercaseStringOrEmpty(entry.channel) : "";
	const accountId = typeof entry.accountId === "string" && entry.accountId.trim() ? entry.accountId.trim() : "default";
	if (!pluginRoot || !pluginId || !channel) return null;
	return {
		pluginRoot,
		pluginId,
		pluginName: typeof entry.pluginName === "string" ? entry.pluginName : void 0,
		channel,
		accountId,
		approvedAt: typeof entry.approvedAt === "number" && Number.isFinite(entry.approvedAt) ? Math.floor(entry.approvedAt) : Date.now()
	};
}
function normalizeLegacyPluginBindingApprovalsFile(input) {
	const file = input && typeof input === "object" ? input : {};
	if (file.version !== 1 || !Array.isArray(file.approvals)) return [];
	const approvals = /* @__PURE__ */ new Map();
	for (const item of file.approvals) {
		const entry = normalizeLegacyPluginBindingApprovalEntry(item);
		if (!entry) continue;
		approvals.set(pluginBindingApprovalScopeKey(entry), entry);
	}
	return [...approvals.values()].toSorted((a, b) => pluginBindingApprovalScopeKey(a).localeCompare(pluginBindingApprovalScopeKey(b)));
}
function pluginBindingApprovalRow(entry) {
	return {
		plugin_root: entry.pluginRoot,
		channel: entry.channel,
		account_id: entry.accountId,
		plugin_id: entry.pluginId,
		plugin_name: entry.pluginName ?? null,
		approved_at: entry.approvedAt
	};
}
function pluginBindingApprovalComparable(entry) {
	return JSON.stringify(pluginBindingApprovalRow(entry));
}
function migrateLegacyPluginBindingApprovals(params) {
	if (!params.detected.hasLegacy) return {
		changes: [],
		warnings: []
	};
	return migrateLegacyJsonState({
		sourcePath: params.detected.sourcePath,
		stateDir: params.stateDir,
		label: "plugin binding approvals",
		normalize: normalizeLegacyPluginBindingApprovalsFile,
		migrate(db, approvals) {
			const stateDb = getNodeSqliteKysely(db);
			const existing = executeSqliteQuerySync(db, stateDb.selectFrom("plugin_binding_approvals").select([
				"plugin_root",
				"channel",
				"account_id",
				"plugin_id",
				"plugin_name",
				"approved_at"
			])).rows;
			const existingByKey = new Map(existing.map((row) => [pluginBindingApprovalScopeKey({
				pluginRoot: row.plugin_root,
				channel: row.channel,
				accountId: row.account_id
			}), JSON.stringify(row)]));
			const approvalsToInsert = [];
			let conflictCount = 0;
			for (const approval of approvals) {
				const existingApprovalJson = existingByKey.get(pluginBindingApprovalScopeKey(approval));
				if (existingApprovalJson === void 0) approvalsToInsert.push(approval);
				else if (existingApprovalJson !== pluginBindingApprovalComparable(approval)) conflictCount += 1;
			}
			if (approvalsToInsert.length > 0) executeSqliteQuerySync(db, stateDb.insertInto("plugin_binding_approvals").values(approvalsToInsert.map(pluginBindingApprovalRow)));
			return {
				changes: approvalsToInsert.length > 0 ? [`Migrated ${approvalsToInsert.length} plugin binding ${approvalsToInsert.length === 1 ? "approval" : "approvals"} → shared SQLite state`] : [],
				...conflictCount > 0 ? { notices: [`Kept shared SQLite plugin binding approvals because ${conflictCount} ${conflictCount === 1 ? "legacy approval conflicts" : "legacy approvals conflict"}: ${params.detected.sourcePath}`] } : {}
			};
		}
	});
}
const CURRENT_BINDING_CONVERSATION_KIND = "current";
function resolveLegacyCurrentConversationBindingsPath(stateDir) {
	return path.join(stateDir, "bindings", "current-conversations.json");
}
function currentConversationBindingKey(ref) {
	const normalized = normalizeConversationRef(ref);
	return [
		normalized.channel,
		normalized.accountId,
		normalized.parentConversationId ?? "",
		normalized.conversationId
	].join("␟");
}
function normalizeLegacyCurrentConversationBindingRecord(input) {
	const record = input && typeof input === "object" ? input : {};
	if (!record.conversation?.conversationId) return null;
	const conversation = normalizeConversationRef(record.conversation);
	const targetSessionKey = typeof record.targetSessionKey === "string" ? record.targetSessionKey.trim() : "";
	if (!targetSessionKey) return null;
	const targetKind = record.targetKind === "subagent" ? "subagent" : "session";
	const status = record.status === "ending" || record.status === "ended" ? record.status : "active";
	const boundAt = typeof record.boundAt === "number" && Number.isFinite(record.boundAt) ? Math.floor(record.boundAt) : Date.now();
	const expiresAt = typeof record.expiresAt === "number" && Number.isFinite(record.expiresAt) ? Math.floor(record.expiresAt) : void 0;
	return {
		bindingId: `generic:${currentConversationBindingKey(conversation)}`,
		targetSessionKey,
		targetKind,
		conversation,
		status,
		boundAt,
		...expiresAt !== void 0 ? { expiresAt } : {},
		...record.metadata && typeof record.metadata === "object" && !Array.isArray(record.metadata) ? { metadata: record.metadata } : {}
	};
}
function normalizeLegacyCurrentConversationBindingFile(input) {
	const file = input && typeof input === "object" ? input : {};
	if (file.version !== 1 || !Array.isArray(file.bindings)) return [];
	const records = /* @__PURE__ */ new Map();
	for (const item of file.bindings) {
		const record = normalizeLegacyCurrentConversationBindingRecord(item);
		if (!record) continue;
		records.set(currentConversationBindingKey(record.conversation), record);
	}
	return [...records.values()].toSorted((a, b) => a.bindingId.localeCompare(b.bindingId));
}
function currentConversationBindingRow(record) {
	const conversation = normalizeConversationRef(record.conversation);
	return {
		binding_key: currentConversationBindingKey(conversation),
		binding_id: record.bindingId,
		target_session_key: record.targetSessionKey,
		channel: conversation.channel,
		account_id: conversation.accountId,
		conversation_kind: CURRENT_BINDING_CONVERSATION_KIND,
		parent_conversation_id: conversation.parentConversationId ?? null,
		conversation_id: conversation.conversationId,
		target_kind: record.targetKind,
		status: record.status,
		bound_at: record.boundAt,
		expires_at: record.expiresAt ?? null,
		metadata_json: record.metadata ? JSON.stringify(record.metadata) : null,
		record_json: JSON.stringify(record),
		updated_at: Date.now()
	};
}
function migrateLegacyCurrentConversationBindings(params) {
	return migrateLegacyJsonState({
		sourcePath: params.detected.sourcePath,
		stateDir: params.stateDir,
		label: "current-conversation bindings",
		normalize: normalizeLegacyCurrentConversationBindingFile,
		migrate(db, records) {
			const stateDb = getNodeSqliteKysely(db);
			const existing = executeSqliteQuerySync(db, stateDb.selectFrom("current_conversation_bindings").select(["binding_key", "record_json"])).rows;
			const existingByKey = new Map(existing.map((row) => [row.binding_key, row.record_json]));
			const recordsToInsert = [];
			let conflictCount = 0;
			for (const record of records) {
				const existingRecordJson = existingByKey.get(currentConversationBindingKey(record.conversation));
				if (existingRecordJson === void 0) recordsToInsert.push(record);
				else if (existingRecordJson !== JSON.stringify(record)) conflictCount += 1;
			}
			if (recordsToInsert.length > 0) executeSqliteQuerySync(db, stateDb.insertInto("current_conversation_bindings").values(recordsToInsert.map(currentConversationBindingRow)));
			return {
				changes: recordsToInsert.length > 0 ? [`Migrated ${recordsToInsert.length} current-conversation ${recordsToInsert.length === 1 ? "binding" : "bindings"} → shared SQLite state`] : [],
				...conflictCount > 0 ? { notices: [`Kept shared SQLite current-conversation bindings because ${conflictCount} ${conflictCount === 1 ? "legacy binding conflicts" : "legacy bindings conflict"}: ${params.detected.sourcePath}`] } : {}
			};
		}
	});
}
//#endregion
//#region src/infra/state-migrations.shared-auth-store.ts
/** Doctor-owned staged relocation of legacy shared auth rows into shared SQLite state. */
const MIGRATION_KIND$2 = "shared-auth-store-state-db";
const AUTH_JSON_MIGRATION_KIND = "auth-profile-json-to-sqlite-v2";
const SOURCE_STORE_KEY = "primary";
const TARGET_STORE_KEY = "shared";
function sourceMigrationKey(sourcePath, sourceTable) {
	return `shared-auth-store:${createHash("sha256").update(path.resolve(sourcePath)).update("\0").update(sourceTable).digest("hex")}`;
}
function readSourceSnapshot(params) {
	if (inspectSharedAuthLegacySourceFile(params.sourcePath).status === "missing") return {
		rows: {
			store: null,
			state: null
		},
		size: null
	};
	try {
		const rows = runOpenClawAgentWriteTransaction(({ db }) => readSharedAuthLegacyRowsFromDatabase(db), {
			agentId: resolveAuthProfileDatabaseOwnerId(path.dirname(params.sourcePath)),
			path: params.sourcePath,
			env: params.env
		}, { operationLabel: "state-migration.shared-auth-source-read" });
		closeAuthProfileReadPool({
			kind: "database",
			databasePath: params.sourcePath
		});
		closeOpenClawAgentDatabaseByPath(params.sourcePath);
		return {
			rows,
			size: fs.statSync(params.sourcePath).size
		};
	} catch (error) {
		throw new SharedAuthStoreSourceInspectionError(params.sourcePath, "read", error);
	}
}
function readTargetRows(database) {
	const db = getNodeSqliteKysely(database);
	const cells = executeSqliteQuerySync(database, db.selectFrom("config_machine_state").select([
		"state_key",
		"value_json",
		"updated_at_ms"
	]).where("state_key", "in", ["authProfiles.store", "authProfiles.state"])).rows;
	const store = cells.find((cell) => cell.state_key === "authProfiles.store");
	const state = cells.find((cell) => cell.state_key === "authProfiles.state");
	return {
		store: store ? {
			store_json: store.value_json,
			updated_at: store.updated_at_ms
		} : null,
		state: state ? {
			state_json: state.value_json,
			updated_at: state.updated_at_ms
		} : null
	};
}
function rowDigest(row) {
	return createHash("sha256").update(JSON.stringify(row)).digest("hex");
}
function rowsMatch(left, right) {
	return JSON.stringify(left) === JSON.stringify(right);
}
function storeConflicts(sourceRow, targetRow) {
	const source = safeParseJsonRecord(sourceRow.store_json);
	const target = safeParseJsonRecord(targetRow.store_json);
	if (!source || !target || typeof source.version !== "number" || !Number.isFinite(source.version) || source.version <= 0 || !isRecord(source.profiles) || !isRecord(target.profiles)) return ["invalid credential payload"];
	const conflicts = isDeepStrictEqual({
		...source,
		profiles: target.profiles
	}, target) ? [] : ["store metadata differs"];
	for (const id of [.../* @__PURE__ */ new Set([...Object.keys(source.profiles), ...Object.keys(target.profiles)])].toSorted()) {
		const inSource = Object.hasOwn(source.profiles, id);
		const inTarget = Object.hasOwn(target.profiles, id);
		if (inSource && !isRecord(source.profiles[id]) || inTarget && !isRecord(target.profiles[id])) conflicts.push(`${JSON.stringify(id)}: malformed credential`);
		else if (inSource && !inTarget) conflicts.push(`${JSON.stringify(id)}: missing from target`);
		else if (inSource && !isDeepStrictEqual(source.profiles[id], target.profiles[id])) conflicts.push(`${JSON.stringify(id)}: credential differs`);
	}
	return conflicts;
}
function assertRowsMatch(expected, actual, label) {
	if (expected.store !== null && !rowsMatch(expected.store, actual.store) || expected.state !== null && !rowsMatch(expected.state, actual.state)) throw new Error(`shared auth relocation ${label} verification failed`);
}
function recordMigrationLedger(params) {
	const db = getNodeSqliteKysely(params.database);
	const entries = [{
		sourceTable: "auth_profile_store",
		targetTable: "auth_profile_stores",
		row: params.sourceRows.store
	}, {
		sourceTable: "auth_profile_state",
		targetTable: "auth_profile_state",
		row: params.sourceRows.state
	}].map((entry) => {
		const sourceKey = sourceMigrationKey(params.sourcePath, entry.sourceTable);
		const pending = entry.row ? void 0 : executeSqliteQueryTakeFirstSync(params.database, db.selectFrom("migration_sources").select([
			"source_sha256",
			"source_record_count",
			"source_size_bytes"
		]).where("source_key", "=", sourceKey).where("removed_source", "=", 0));
		return Object.assign(entry, {
			sourceKey,
			sourceSha256: pending?.source_sha256 ?? rowDigest(entry.row),
			sourceRecordCount: pending?.source_record_count ?? Number(entry.row !== null),
			sourceSizeBytes: pending?.source_size_bytes ?? params.sourceSize
		});
	});
	const runHash = createHash("sha256");
	for (const entry of entries) runHash.update(entry.sourceSha256);
	const runId = `shared-auth-store:${runHash.digest("hex").slice(0, 24)}`;
	recordLegacyMigrationRun(params.database, {
		runId,
		startedAt: params.now,
		finishedAt: params.stage === "completed" ? params.now : null,
		status: params.stage,
		reportJson: JSON.stringify({
			source: MIGRATION_KIND$2,
			target: "auth_profile_stores,auth_profile_state",
			stage: params.stage,
			importedRecordCount: entries.reduce((count, entry) => count + entry.sourceRecordCount, 0)
		}),
		upsert: true
	});
	for (const entry of entries) recordLegacyMigrationSource(params.database, {
		sourceKey: entry.sourceKey,
		migrationKind: MIGRATION_KIND$2,
		sourcePath: params.sourcePath,
		targetTable: entry.targetTable,
		sourceSha256: entry.sourceSha256,
		sourceSizeBytes: entry.sourceSizeBytes,
		sourceRecordCount: entry.sourceRecordCount,
		runId,
		status: params.stage,
		importedAt: params.now,
		reportJson: JSON.stringify({
			source: entry.sourceTable,
			target: entry.targetTable,
			stage: params.stage,
			sourceSha256: entry.sourceSha256,
			importedRecordCount: entry.sourceRecordCount
		}),
		upsert: true
	});
	if (params.stage === "completed") executeSqliteQuerySync(params.database, db.updateTable("migration_sources").set({ removed_source: 1 }).where("source_key", "in", entries.map((entry) => entry.sourceKey)));
}
function rewriteAuthJsonMigrationReceipts(database, sourceDatabasePath, targetDatabasePath) {
	const db = getNodeSqliteKysely(database);
	const receipts = executeSqliteQuerySync(database, db.selectFrom("migration_sources").select([
		"source_key",
		"last_run_id",
		"target_table",
		"report_json"
	]).where("migration_kind", "=", AUTH_JSON_MIGRATION_KIND)).rows;
	for (const receipt of receipts) {
		let report;
		try {
			report = JSON.parse(receipt.report_json);
		} catch {
			continue;
		}
		if (typeof report.targetDatabasePath !== "string" || path.resolve(report.targetDatabasePath) !== path.resolve(sourceDatabasePath) || receipt.target_table !== "auth_profile_store" && receipt.target_table !== "auth_profile_state") continue;
		const targetTable = receipt.target_table === "auth_profile_store" ? "auth_profile_stores" : "auth_profile_state";
		const reportJson = JSON.stringify({
			...report,
			relocatedFromDatabasePath: report.targetDatabasePath,
			targetDatabasePath,
			targetTable,
			targetStoreKey: TARGET_STORE_KEY
		});
		executeSqliteQuerySync(database, db.updateTable("migration_sources").set({
			target_table: targetTable,
			report_json: reportJson
		}).where("source_key", "=", receipt.source_key));
		executeSqliteQuerySync(database, db.updateTable("migration_runs").set({ report_json: reportJson }).where("id", "=", receipt.last_run_id));
	}
}
function copyRowsToState(params) {
	return runOpenClawStateWriteTransaction(({ db: database, path: targetDatabasePath }) => {
		const db = getNodeSqliteKysely(database);
		const target = readTargetRows(database);
		const conflicts = params.sourceRows.store && target.store && !rowsMatch(params.sourceRows.store, target.store) ? storeConflicts(params.sourceRows.store, target.store).map((detail) => `auth_profile_store[primary] -> config_machine_state[authProfiles.store]: ${detail}`) : [];
		if (params.sourceRows.state && target.state && !rowsMatch(params.sourceRows.state, target.state)) conflicts.push("auth_profile_state[primary] -> config_machine_state[authProfiles.state]: runtime state or timestamp differs");
		if (conflicts.length > 0) throw new Error(`shared auth rows conflict with the relocation target: ${conflicts.join("; ")}. Back up source ${JSON.stringify(params.sourcePath)} and target ${JSON.stringify(targetDatabasePath)} with OpenClaw stopped. Preserve target-only profiles, copy missing source profiles into the target, and reconcile differing entries/metadata/state locally; then rerun openclaw doctor --fix. No auth rows were changed.`);
		if (params.sourceRows.store && !target.store) executeSqliteQuerySync(database, db.insertInto("config_machine_state").values({
			state_key: "authProfiles.store",
			value_json: params.sourceRows.store.store_json,
			updated_at_ms: params.sourceRows.store.updated_at
		}));
		if (params.sourceRows.state && !target.state) executeSqliteQuerySync(database, db.insertInto("config_machine_state").values({
			state_key: "authProfiles.state",
			value_json: params.sourceRows.state.state_json,
			updated_at_ms: params.sourceRows.state.updated_at
		}));
		const canonicalRows = readTargetRows(database);
		assertRowsMatch({
			store: target.store ?? params.sourceRows.store,
			state: target.state ?? params.sourceRows.state
		}, canonicalRows, "copy");
		rewriteAuthJsonMigrationReceipts(database, params.sourcePath, targetDatabasePath);
		recordMigrationLedger({
			...params,
			database,
			stage: "copied"
		});
		return canonicalRows;
	}, { env: params.env }, { operationLabel: "state-migration.shared-auth-copy" });
}
function advanceMigration(params) {
	const flipping = params.stage === "ownership-flipped";
	const flipped = flipping && resolveSharedAuthStoreOwnership(params.env).location !== "state-db";
	runOpenClawStateWriteTransaction(({ db: database }) => {
		assertRowsMatch(params.rows, readTargetRows(database), params.stage);
		if (flipping) {
			const db = getNodeSqliteKysely(database);
			const ownership = {
				value_json: JSON.stringify({ location: "state-db" }),
				updated_at_ms: params.now
			};
			executeSqliteQuerySync(database, db.insertInto("config_machine_state").values({
				state_key: SHARED_AUTH_STORE_STATE_KEY,
				...ownership
			}).onConflict((conflict) => conflict.column("state_key").doUpdateSet(ownership)));
		}
		recordMigrationLedger({
			...params,
			database
		});
	}, { env: params.env }, { operationLabel: flipping ? "state-migration.shared-auth-ownership" : "state-migration.shared-auth-finalize" });
	if (flipping) noteCommittedSharedAuthStoreOwnership({ location: "state-db" }, params.env);
	return flipped;
}
function cleanupSourceRows(params) {
	if (inspectSharedAuthLegacySourceFile(params.sourcePath).status === "missing") return false;
	try {
		const removed = runOpenClawAgentWriteTransaction(({ db: database }) => {
			const db = getNodeSqliteKysely(database);
			const before = readSharedAuthLegacyRowsFromDatabase(database);
			executeSqliteQuerySync(database, db.deleteFrom("auth_profile_store").where("store_key", "=", SOURCE_STORE_KEY));
			executeSqliteQuerySync(database, db.deleteFrom("auth_profile_state").where("state_key", "=", SOURCE_STORE_KEY));
			const after = readSharedAuthLegacyRowsFromDatabase(database);
			if (after.store || after.state) throw new Error("legacy shared auth rows remain after cleanup");
			return before.store !== null || before.state !== null;
		}, {
			agentId: resolveAuthProfileDatabaseOwnerId(path.dirname(params.sourcePath)),
			path: params.sourcePath,
			env: params.env
		}, { operationLabel: "state-migration.shared-auth-cleanup" });
		closeAuthProfileReadPool({
			kind: "database",
			databasePath: params.sourcePath
		});
		closeOpenClawAgentDatabaseByPath(params.sourcePath);
		return removed;
	} catch (error) {
		throw new SharedAuthStoreSourceInspectionError(params.sourcePath, "clean", error);
	}
}
/** Detect relocation or unfinished cleanup only in the explicit Doctor repair path. */
function detectSharedAuthStoreMigration(params) {
	const env = {
		...params.env ?? process.env,
		OPENCLAW_STATE_DIR: params.stateDir
	};
	const sourcePath = path.join(resolveSharedMainAuthAgentDir(env), "openclaw-agent.sqlite");
	if (params.doctorOnlyStateMigrations !== true) return {
		sourcePath,
		hasLegacy: false
	};
	const hasLegacy = resolveSharedAuthStoreOwnership(env).location === "legacy-main" || hasPendingSharedAuthCleanup(env, sourcePath);
	if (hasLegacy) inspectSharedAuthLegacyRowsReadOnly(sourcePath);
	return {
		sourcePath,
		hasLegacy
	};
}
/** Converge copy, ownership, and cleanup while excluding live Gateway writers. */
async function migrateSharedAuthStore(params) {
	if (!params.detected.hasLegacy) return {
		changes: [],
		warnings: []
	};
	return await withLegacyMigrationStateLock({
		stateDir: params.stateDir,
		env: params.env,
		label: "legacy shared auth store",
		releaseLabel: "Shared auth store",
		errorLabel: "Failed relocating the shared auth store",
		run: async (env) => {
			const now = params.now?.() ?? Date.now();
			const source = readSourceSnapshot({
				env,
				sourcePath: params.detected.sourcePath
			});
			const snapshot = {
				env,
				sourcePath: params.detected.sourcePath,
				sourceSize: source.size,
				sourceRows: source.rows,
				now
			};
			const rows = copyRowsToState(snapshot);
			const ownershipFlipped = advanceMigration({
				...snapshot,
				rows,
				stage: "ownership-flipped"
			});
			const sourceCleaned = cleanupSourceRows({
				env,
				sourcePath: params.detected.sourcePath
			});
			const relocatedRows = rows.store !== null || rows.state !== null || sourceCleaned;
			advanceMigration({
				...snapshot,
				rows,
				stage: "completed"
			});
			return {
				changes: [...ownershipFlipped && relocatedRows ? ["Relocated shared auth profiles into shared SQLite state."] : [], ...sourceCleaned && !ownershipFlipped ? ["Completed legacy shared auth row cleanup."] : []],
				warnings: [],
				...ownershipFlipped && rows.store !== null ? { notices: ["The main agent no longer owns shared credentials and can now be deleted."] } : {}
			};
		}
	});
}
//#endregion
//#region src/infra/state-migrations.subagent-registry-db.ts
const MIGRATION_KIND$1 = "legacy-subagent-registry-json";
/** Records the irreversible retirement decision before Doctor removes the claimed file. */
function recordLegacySubagentRegistryDiscard(params) {
	const sourceKey = `subagent-json:${createHash("sha256").update(params.sourcePath).digest("hex")}`;
	const now = Date.now();
	const runId = `${sourceKey}:${params.sourceSha256.slice(0, 16)}`;
	let decision = "retired-source-discarded";
	runOpenClawStateWriteTransaction(({ db }) => {
		const stateDb = getNodeSqliteKysely(db);
		if (executeSqliteQueryTakeFirstSync(db, stateDb.selectFrom("migration_sources").select("source_key").where("source_key", "=", sourceKey))) decision = "receipt-authoritative";
		const reportJson = JSON.stringify({
			source: MIGRATION_KIND$1,
			target: "subagent_runs",
			decision,
			sourceSha256: params.sourceSha256,
			importedRecordCount: 0,
			reason: "retired transient state is never imported into the canonical SQLite registry"
		});
		executeSqliteQuerySync(db, stateDb.insertInto("migration_runs").values({
			id: runId,
			started_at: now,
			finished_at: now,
			status: "completed",
			report_json: reportJson
		}).onConflict((conflict) => conflict.column("id").doUpdateSet({
			finished_at: now,
			status: "completed",
			report_json: reportJson
		})));
		executeSqliteQuerySync(db, stateDb.insertInto("migration_sources").values({
			source_key: sourceKey,
			migration_kind: MIGRATION_KIND$1,
			source_path: params.sourcePath,
			target_table: "subagent_runs",
			source_sha256: params.sourceSha256,
			source_size_bytes: params.sourceSize,
			source_record_count: null,
			last_run_id: runId,
			status: "completed",
			imported_at: now,
			removed_source: 0,
			report_json: reportJson
		}).onConflict((conflict) => conflict.column("source_key").doUpdateSet({
			source_sha256: params.sourceSha256,
			source_size_bytes: params.sourceSize,
			source_record_count: null,
			last_run_id: runId,
			status: "completed",
			imported_at: now,
			removed_source: 0,
			report_json: reportJson
		})));
	}, { env: params.env });
	return {
		decision,
		sourceKey
	};
}
function markLegacySubagentRegistrySourceRemoved(sourceKey, env) {
	runOpenClawStateWriteTransaction(({ db }) => {
		const stateDb = getNodeSqliteKysely(db);
		executeSqliteQuerySync(db, stateDb.updateTable("migration_sources").set({ removed_source: 1 }).where("source_key", "=", sourceKey));
	}, { env });
}
//#endregion
//#region src/infra/state-migrations.subagent-registry.ts
const LEGACY_SUBAGENT_REGISTRY_MAX_BYTES = 16777216;
const DOCTOR_CLAIM_SUFFIX = ".doctor-importing";
function resolveLegacySubagentRegistryPath(stateDir) {
	return path.join(stateDir, "subagents", "runs.json");
}
/** Detect retired subagent state only when an explicit Doctor flow opts in. */
function detectLegacySubagentRegistry(params) {
	const sourcePath = resolveLegacySubagentRegistryPath(params.stateDir);
	return {
		sourcePath,
		hasLegacy: params.doctorOnlyStateMigrations === true && legacyMigrationSourceOrClaimMayExist(sourcePath)
	};
}
async function recoverInterruptedClaim(params) {
	if (!await params.source.exists(true)) return;
	const claimed = await params.source.read(true);
	if (!await params.source.exists()) {
		const restoreError = await params.source.restore();
		if (restoreError) throw new Error(restoreError);
		return;
	}
	await params.source.read();
	const result = recordLegacySubagentRegistryDiscard({
		env: params.env,
		sourcePath: params.source.sourcePath,
		sourceSha256: claimed.sha256,
		sourceSize: claimed.size
	});
	await params.source.remove({ skipSourceCheck: true });
	markLegacySubagentRegistrySourceRemoved(result.sourceKey, params.env);
}
async function migrateWithExclusiveStateOwnership(params) {
	const changes = [];
	const warnings = [];
	const notices = [];
	const sourcePath = params.detected.sourcePath;
	const source = new LegacyMigrationSourceClaim({
		stateRoot: params.stateRoot,
		stateDir: params.stateDir,
		sourcePath,
		label: "subagent registry",
		claimSuffix: DOCTOR_CLAIM_SUFFIX,
		readSnapshot: (snapshotPath) => readLegacyMigrationSourceSnapshot({
			stateRoot: params.stateRoot,
			stateDir: params.stateDir,
			sourcePath: snapshotPath,
			maxBytes: LEGACY_SUBAGENT_REGISTRY_MAX_BYTES,
			label: "subagent registry"
		})
	});
	if (!params.detected.hasLegacy) return {
		changes,
		warnings
	};
	let snapshot;
	try {
		await recoverInterruptedClaim({
			source,
			env: params.env
		});
		if (!await source.exists()) return {
			changes,
			warnings
		};
		snapshot = await source.read();
	} catch (error) {
		warnings.push(`Failed reading legacy subagent registry: ${String(error)}`);
		return {
			changes,
			warnings
		};
	}
	try {
		params.beforeVerify?.();
		const current = await source.read();
		if (!legacyMigrationSourceSnapshotsMatch(current, snapshot)) throw new Error("legacy subagent registry changed after Doctor loaded it");
		await source.claim({
			snapshot,
			mismatchMessage: "legacy subagent registry changed before Doctor could claim it",
			beforeClaim: params.beforeClaim
		});
	} catch (error) {
		const restoreError = await source.restore();
		warnings.push(`Failed migrating legacy subagent registry: ${String(error)}${restoreError ? `; restore failure: ${restoreError}` : ""}`);
		return {
			changes,
			warnings
		};
	}
	let result;
	try {
		result = recordLegacySubagentRegistryDiscard({
			env: params.env,
			sourcePath: snapshot.sourcePath,
			sourceSha256: snapshot.sha256,
			sourceSize: snapshot.size
		});
	} catch (error) {
		const restoreError = await source.restore();
		warnings.push(`Failed migrating legacy subagent registry: ${String(error)}${restoreError ? `; restore failure: ${restoreError}` : ""}`);
		return {
			changes,
			warnings
		};
	}
	try {
		await source.remove({
			removeSource: params.removeSource,
			sourceReappearedMessage: `legacy subagent registry reappeared during retirement: ${sourcePath}`,
			sourceRemainingMessage: `legacy subagent registry reappeared during cleanup: ${sourcePath}`,
			claimRemainingMessage: `legacy subagent registry Doctor claim remains after cleanup: ${source.claimPath}`
		});
	} catch (error) {
		warnings.push(`Legacy subagent registry retirement cleanup failed: ${String(error)}`);
		return {
			changes,
			warnings
		};
	}
	try {
		markLegacySubagentRegistrySourceRemoved(result.sourceKey, params.env);
	} catch (error) {
		warnings.push(`Legacy subagent registry was removed, but its receipt could not be finalized: ${String(error)}`);
	}
	changes.push(result.decision === "receipt-authoritative" ? "Discarded recreated retired subagent JSON without importing it." : "Discarded retired subagent JSON without importing transient run state.");
	notices.push("Removed retired subagents/runs.json after the discard decision was recorded.");
	return {
		changes,
		warnings,
		notices
	};
}
/** Discard retired transient state while excluding active Gateway owners. */
async function migrateLegacySubagentRegistry(params) {
	if (!params.detected.hasLegacy) return {
		changes: [],
		warnings: []
	};
	return await withLegacyMigrationStateLock({
		stateDir: params.stateDir,
		env: params.env,
		label: "legacy subagent registry",
		releaseLabel: "Subagent registry",
		errorLabel: "Failed reading legacy subagent registry",
		retryGuidance: "Stop the Gateway, then run `openclaw doctor --fix` again.",
		run: async (env) => {
			const stateRoot = await root(params.stateDir, {
				hardlinks: "reject",
				maxBytes: LEGACY_SUBAGENT_REGISTRY_MAX_BYTES,
				symlinks: "reject"
			});
			return await migrateWithExclusiveStateOwnership({
				...params,
				env,
				stateRoot
			});
		}
	});
}
//#endregion
//#region src/infra/state-migrations.transcript-directives-transform.ts
const LEGACY_REACTION_DIRECTIVE_RE = /\[\[\s*(?:react|react_to_current)\s*:\s*([^\]\n]+?)\s*\]\]/giu;
function stripLegacyReactionDirectives(message) {
	if (!Array.isArray(message.content)) return;
	for (const part of message.content) {
		if (!isRecord(part) || part.type !== "text" || typeof part.text !== "string") continue;
		let changed = false;
		const stripped = replaceOutsideCodeRegions(part.text, LEGACY_REACTION_DIRECTIVE_RE, () => {
			changed = true;
			return "";
		});
		if (changed) part.text = stripped.trimStart();
	}
}
function transformHistoricalTranscriptEvent(event) {
	if (!isRecord(event) || event.type !== "message" || !isRecord(event.message) || event.message.role !== "assistant" || !Array.isArray(event.message.content)) return {
		changed: false,
		event
	};
	const before = JSON.stringify(event.message);
	stripLegacyReactionDirectives(event.message);
	applyAssistantDeliveryDirectives(event.message);
	return {
		changed: JSON.stringify(event.message) !== before,
		event
	};
}
function parseTranscriptEvent$1(raw, owner) {
	try {
		return JSON.parse(raw);
	} catch (error) {
		throw new Error(`${owner} contains invalid transcript JSON`, { cause: error });
	}
}
function transformArchiveContent(content, owner) {
	if (!content) return {
		changed: false,
		content
	};
	const trailingNewline = content.endsWith("\n");
	const lines = trailingNewline ? content.slice(0, -1).split("\n") : content.split("\n");
	let changed = false;
	const rewritten = lines.map((line, index) => {
		if (!line) throw new Error(`${owner} contains a blank JSONL record at line ${index + 1}`);
		const transformed = transformHistoricalTranscriptEvent(parseTranscriptEvent$1(line, `${owner}:${index + 1}`));
		changed ||= transformed.changed;
		return transformed.changed ? JSON.stringify(transformed.event) : line;
	});
	return {
		changed,
		content: `${rewritten.join("\n")}${trailingNewline ? "\n" : ""}`
	};
}
function encodeArchiveContent(content, encoding, owner) {
	if (encoding === "identity") return Buffer.from(content, "utf8");
	const encoded = encodeSessionArchiveContent(content);
	if (encoded.suffix !== ".zst") throw new Error(`${owner} could not be re-encoded with its zstd codec`);
	return encoded.bytes;
}
function readArchiveEncoding(value, owner) {
	if (value === "identity" || value === "zstd") return value;
	throw new Error(`${owner} has unsupported transcript archive encoding ${value}`);
}
function listArchiveBatch(database, cursor) {
	if (!database.prepare("SELECT 1 FROM sqlite_schema WHERE type = 'table' AND name = ?").get("session_transcript_archives")) return [];
	let query = getNodeSqliteKysely(database).selectFrom("session_transcript_archives").select([
		"archive_blob",
		"archive_name",
		"archive_sha256",
		"encoding",
		"generation",
		"published_at",
		"session_id"
	]).orderBy("session_id", "asc").orderBy("generation", "asc").limit(32);
	if (cursor.sessionId) query = query.where((eb) => eb.or([eb("session_id", ">", cursor.sessionId), eb.and([eb("session_id", "=", cursor.sessionId), eb("generation", ">", cursor.generation)])]));
	return executeSqliteQuerySync(database, query).rows.map((row) => {
		const owner = `${row.session_id}:${row.generation}`;
		const encoding = readArchiveEncoding(row.encoding, owner);
		const bytes = Buffer.from(row.archive_blob);
		if (createHash("sha256").update(bytes).digest("hex") !== row.archive_sha256) throw new Error(`Canonical SQLite transcript archive is corrupt for ${row.session_id}`);
		const transformed = transformArchiveContent(decodeSessionArchiveBytes(bytes, encoding === "zstd"), owner);
		const nextBytes = transformed.changed ? encodeArchiveContent(transformed.content, encoding, owner) : bytes;
		return {
			archiveName: row.archive_name,
			archiveSha256: row.archive_sha256,
			bytes,
			changed: transformed.changed,
			encoding,
			generation: row.generation,
			nextBytes,
			nextSha256: createHash("sha256").update(nextBytes).digest("hex"),
			publishedAt: row.published_at,
			sessionId: row.session_id
		};
	});
}
function transcriptDirectiveArchivesNeedMigration(database, start) {
	let cursor = start;
	while (true) {
		const batch = listArchiveBatch(database, cursor);
		if (batch.length === 0) return false;
		if (batch.some((planned) => planned.changed)) return true;
		const last = batch.at(-1);
		if (!last) return false;
		cursor = {
			generation: last.generation,
			sessionId: last.sessionId
		};
	}
}
function assertArchiveSourceUnchanged(database, planned) {
	const db = getNodeSqliteKysely(database);
	const current = executeSqliteQueryTakeFirstSync(database, db.selectFrom("session_transcript_archives").select([
		"archive_blob",
		"archive_name",
		"archive_sha256",
		"encoding"
	]).where("session_id", "=", planned.sessionId).where("generation", "=", planned.generation));
	if (!current) return false;
	if (current.archive_name !== planned.archiveName || current.archive_sha256 !== planned.archiveSha256 || current.encoding !== planned.encoding || !Buffer.from(current.archive_blob).equals(planned.bytes)) throw new Error(`Transcript archive source changed before migration commit for ${planned.sessionId}`);
	return true;
}
function rewriteArchiveRow(database, planned) {
	if (!assertArchiveSourceUnchanged(database, planned)) return false;
	if (!planned.changed) return true;
	const db = getNodeSqliteKysely(database);
	if (executeSqliteQuerySync(database, db.updateTable("session_transcript_archives").set({
		archive_blob: planned.nextBytes,
		archive_sha256: planned.nextSha256,
		published_at: null
	}).where("session_id", "=", planned.sessionId).where("generation", "=", planned.generation).where("archive_sha256", "=", planned.archiveSha256)).numAffectedRows !== 1n) throw new Error(`Transcript archive changed before rewrite for ${planned.sessionId}`);
	return true;
}
function repairPublishedArchiveFile(params) {
	const archiveDirectory = path.resolve(params.archiveDirectory);
	const archivePath = path.resolve(archiveDirectory, params.planned.archiveName);
	if (path.dirname(archivePath) !== archiveDirectory || path.basename(archivePath) !== params.planned.archiveName) throw new Error(`Cannot migrate transcript archive outside ${archiveDirectory}`);
	if (!fs.existsSync(archivePath)) return false;
	if (createHash("sha256").update(fs.readFileSync(archivePath)).digest("hex") === params.planned.nextSha256) return true;
	assertAgentDatabaseMaintenanceAuthority();
	replaceFileAtomicSync({
		beforeRename: ({ tempPath }) => {
			if (createHash("sha256").update(fs.readFileSync(tempPath)).digest("hex") !== params.planned.nextSha256) throw new Error(`Transcript archive staging verification failed for ${archivePath}`);
			assertAgentDatabaseMaintenanceAuthority();
		},
		content: params.planned.nextBytes,
		filePath: archivePath,
		preserveExistingMode: true,
		syncParentDir: true,
		syncTempFile: true,
		tempPrefix: `${path.basename(archivePath)}.directive-migration`
	});
	if (createHash("sha256").update(fs.readFileSync(archivePath)).digest("hex") !== params.planned.nextSha256) throw new Error(`Transcript archive verification failed for ${archivePath}`);
	return true;
}
function finalizeArchiveCursor(params) {
	const db = getNodeSqliteKysely(params.database);
	const current = executeSqliteQueryTakeFirstSync(params.database, db.selectFrom("session_transcript_archives").select(["archive_blob", "archive_sha256"]).where("session_id", "=", params.planned.sessionId).where("generation", "=", params.planned.generation));
	if (current) {
		if (current.archive_sha256 !== params.planned.nextSha256 || !Buffer.from(current.archive_blob).equals(params.planned.nextBytes)) throw new Error(`Transcript archive changed before migration commit for ${params.planned.sessionId}`);
		if (params.planned.changed && params.planned.publishedAt !== null && params.fileCurrent) executeSqliteQuerySync(params.database, db.updateTable("session_transcript_archives").set({ published_at: params.planned.publishedAt }).where("session_id", "=", params.planned.sessionId).where("generation", "=", params.planned.generation).where("archive_sha256", "=", params.planned.nextSha256));
	}
	params.writeCursor({
		generation: params.planned.generation,
		sessionId: params.planned.sessionId
	});
}
async function migrateTranscriptDirectiveArchives(params) {
	let rewrittenArchives = 0;
	let cursor = params.start;
	const archiveDirectory = resolveSqliteTranscriptArchiveDirectory({
		agentId: params.agentId,
		path: params.pathname
	});
	while (true) {
		const batch = listArchiveBatch(params.database, cursor);
		if (batch.length === 0) {
			runSqliteImmediateTransactionSync(params.database, () => {
				assertAgentDatabaseMaintenanceAuthority();
				params.writeCursor({ phase: "complete" });
				assertAgentDatabaseMaintenanceAuthority();
			});
			return rewrittenArchives;
		}
		for (const planned of batch) {
			const rowPresent = runSqliteImmediateTransactionSync(params.database, () => {
				assertAgentDatabaseMaintenanceAuthority();
				const currentRowPresent = rewriteArchiveRow(params.database, planned);
				assertAgentDatabaseMaintenanceAuthority();
				return currentRowPresent;
			}, {
				busyTimeoutMs: OPENCLAW_SQLITE_BUSY_TIMEOUT_MS,
				databaseLabel: params.pathname,
				operationLabel: "historical-transcript-archive-directives"
			});
			const fileCurrent = rowPresent ? repairPublishedArchiveFile({
				archiveDirectory,
				planned
			}) : false;
			runSqliteImmediateTransactionSync(params.database, () => {
				assertAgentDatabaseMaintenanceAuthority();
				finalizeArchiveCursor({
					database: params.database,
					fileCurrent,
					planned,
					writeCursor: params.writeCursor
				});
				assertAgentDatabaseMaintenanceAuthority();
			}, {
				busyTimeoutMs: OPENCLAW_SQLITE_BUSY_TIMEOUT_MS,
				databaseLabel: params.pathname,
				operationLabel: "historical-transcript-archive-cursor"
			});
			rewrittenArchives += planned.changed && rowPresent ? 1 : 0;
			cursor = {
				generation: planned.generation,
				sessionId: planned.sessionId
			};
		}
		await new Promise((resolve) => {
			setImmediate(resolve);
		});
	}
}
//#endregion
//#region src/infra/state-migrations.transcript-directives.ts
const MIGRATION_META_KEY = "historical-transcript-directives-v1";
function createMigrationDatabaseHandle(database, agentId, pathname) {
	return {
		agentId,
		db: database,
		path: pathname,
		walMaintenance: {
			checkpoint: () => false,
			close: () => false
		}
	};
}
function parseMigrationCursor(value, pathname) {
	if (!value) return {
		phase: "transcripts",
		sessionId: ""
	};
	let parsed;
	try {
		parsed = JSON.parse(value);
	} catch (error) {
		throw new Error(`${pathname} has an invalid historical transcript migration cursor`, { cause: error });
	}
	if (!isRecord(parsed)) throw new Error(`${pathname} has an invalid historical transcript migration cursor`);
	if (parsed.phase === "complete") return { phase: "complete" };
	if (parsed.phase === "transcripts" && typeof parsed.sessionId === "string") return {
		phase: "transcripts",
		sessionId: parsed.sessionId
	};
	if (parsed.phase === "archives" && typeof parsed.sessionId === "string" && typeof parsed.generation === "string") return {
		generation: parsed.generation,
		phase: "archives",
		sessionId: parsed.sessionId
	};
	throw new Error(`${pathname} has an invalid historical transcript migration cursor`);
}
function readMigrationCursor(database, pathname) {
	const db = getNodeSqliteKysely(database);
	return parseMigrationCursor(executeSqliteQueryTakeFirstSync(database, db.selectFrom("schema_meta").select("app_version").where("meta_key", "=", MIGRATION_META_KEY))?.app_version, pathname);
}
function writeMigrationCursor(database, agentId, cursor) {
	const now = Date.now();
	const db = getNodeSqliteKysely(database);
	executeSqliteQuerySync(database, db.insertInto("schema_meta").values({
		agent_id: agentId,
		app_version: JSON.stringify(cursor),
		created_at: now,
		meta_key: MIGRATION_META_KEY,
		role: "agent",
		schema_version: 1,
		updated_at: now
	}).onConflict((conflict) => conflict.column("meta_key").doUpdateSet({
		agent_id: agentId,
		app_version: JSON.stringify(cursor),
		role: "agent",
		schema_version: 1,
		updated_at: now
	})));
}
function parseTranscriptEvent(raw, owner) {
	try {
		return JSON.parse(raw);
	} catch (error) {
		throw new Error(`${owner} contains invalid transcript JSON`, { cause: error });
	}
}
function listTranscriptSessionBatch(database, afterSessionId) {
	const db = getNodeSqliteKysely(database);
	return executeSqliteQuerySync(database, db.selectFrom("transcript_events").select("session_id").distinct().where("session_id", ">", afterSessionId).where("event_json", "like", "%[[%").orderBy("session_id", "asc").limit(32)).rows.map((row) => row.session_id);
}
function planTranscriptSession(database, pathname, sessionId) {
	const db = getNodeSqliteKysely(database);
	return executeSqliteQuerySync(database, db.selectFrom("transcript_events").select(["event_json", "seq"]).where("session_id", "=", sessionId).where("event_json", "like", "%[[%").orderBy("seq", "asc")).rows.map((row) => {
		const transformed = transformHistoricalTranscriptEvent(parseTranscriptEvent(row.event_json, `${pathname}:${sessionId}:${row.seq}`));
		return {
			eventJson: row.event_json,
			rewrittenEventJson: transformed.changed ? JSON.stringify(transformed.event) : row.event_json,
			seq: row.seq
		};
	});
}
function assertTranscriptSessionSourceUnchanged(database, sessionId, planned) {
	const db = getNodeSqliteKysely(database);
	const current = executeSqliteQuerySync(database, db.selectFrom("transcript_events").select(["event_json", "seq"]).where("session_id", "=", sessionId).where("event_json", "like", "%[[%").orderBy("seq", "asc")).rows;
	if (current.length !== planned.length || current.some((row, index) => row.seq !== planned[index]?.seq || row.event_json !== planned[index]?.eventJson)) throw new Error(`Transcript source changed before migration commit for ${sessionId}`);
}
function transcriptSessionsNeedMigration(database, pathname, afterSessionId) {
	let cursor = afterSessionId;
	while (true) {
		const sessionIds = listTranscriptSessionBatch(database, cursor);
		if (sessionIds.length === 0) return false;
		for (const sessionId of sessionIds) if (planTranscriptSession(database, pathname, sessionId).some((row) => row.rewrittenEventJson !== row.eventJson)) return true;
		cursor = sessionIds.at(-1) ?? cursor;
	}
}
function hasActiveAgentDatabaseLease(agentId, env) {
	try {
		assertNoOpenClawAgentDatabaseLeases(agentId, { env });
		return false;
	} catch (error) {
		if (error instanceof OpenClawAgentDatabaseLeaseActiveError) return true;
		throw error;
	}
}
async function yieldBetweenTranscriptBatches() {
	await new Promise((resolve) => {
		setImmediate(resolve);
	});
}
async function migrateTranscriptSessions(params) {
	let rewrittenSessions = 0;
	let afterSessionId = params.start.sessionId;
	while (true) {
		const sessionIds = listTranscriptSessionBatch(params.database, afterSessionId);
		if (sessionIds.length === 0) {
			runSqliteImmediateTransactionSync(params.database, () => {
				assertAgentDatabaseMaintenanceAuthority();
				writeMigrationCursor(params.database, params.agentId, {
					generation: "",
					phase: "archives",
					sessionId: ""
				});
				assertAgentDatabaseMaintenanceAuthority();
			});
			return rewrittenSessions;
		}
		for (const sessionId of sessionIds) {
			const planned = planTranscriptSession(params.database, params.pathname, sessionId);
			const changedRows = planned.filter((row) => row.rewrittenEventJson !== row.eventJson);
			runSqliteImmediateTransactionSync(params.database, () => {
				assertAgentDatabaseMaintenanceAuthority();
				assertTranscriptSessionSourceUnchanged(params.database, sessionId, planned);
				updateSqliteTranscriptEventJsonInTransaction(params.owner, sessionId, changedRows.map((row) => ({
					eventJson: row.rewrittenEventJson,
					seq: row.seq
				})));
				writeMigrationCursor(params.database, params.agentId, {
					phase: "transcripts",
					sessionId
				});
				assertAgentDatabaseMaintenanceAuthority();
			}, {
				busyTimeoutMs: OPENCLAW_SQLITE_BUSY_TIMEOUT_MS,
				databaseLabel: params.pathname,
				operationLabel: "historical-transcript-directives"
			});
			rewrittenSessions += changedRows.length > 0 ? 1 : 0;
			afterSessionId = sessionId;
		}
		await yieldBetweenTranscriptBatches();
	}
}
async function migrateAgentDatabase(params) {
	migrateOpenClawAgentDatabaseForMaintenance(params);
	const database = openNodeSqliteDatabase(params.pathname);
	try {
		database.exec(`PRAGMA busy_timeout = ${OPENCLAW_SQLITE_BUSY_TIMEOUT_MS};`);
		assertOpenClawAgentDatabaseForMaintenance(database, params);
		const cursor = readMigrationCursor(database, params.pathname);
		if (cursor.phase === "complete") return {
			archivedTranscripts: 0,
			transcriptSessions: 0
		};
		const owner = createMigrationDatabaseHandle(database, params.agentId, params.pathname);
		const transcriptSessions = cursor.phase === "transcripts" ? await migrateTranscriptSessions({
			agentId: params.agentId,
			database,
			owner,
			pathname: params.pathname,
			start: cursor
		}) : 0;
		const archiveCursor = readMigrationCursor(database, params.pathname);
		return {
			archivedTranscripts: archiveCursor.phase === "archives" ? await migrateTranscriptDirectiveArchives({
				agentId: params.agentId,
				database,
				pathname: params.pathname,
				start: archiveCursor,
				writeCursor: (next) => writeMigrationCursor(database, params.agentId, "phase" in next ? next : {
					...next,
					phase: "archives"
				})
			}) : 0,
			transcriptSessions
		};
	} finally {
		clearNodeSqliteKyselyCacheForDatabase(database);
		database.close();
	}
}
function agentDatabaseNeedsTranscriptDirectiveMigration(params) {
	const database = openNodeSqliteDatabase(params.pathname, { readOnly: true });
	try {
		if (Number(database.prepare("PRAGMA user_version").get()?.user_version ?? 0) !== 19) return true;
		try {
			assertOpenClawAgentDatabaseForMaintenance(database, params);
		} catch {
			return true;
		}
		const cursor = readMigrationCursor(database, params.pathname);
		if (cursor.phase === "complete") return false;
		if (cursor.phase === "transcripts" && transcriptSessionsNeedMigration(database, params.pathname, cursor.sessionId)) return true;
		if (transcriptDirectiveArchivesNeedMigration(database, cursor.phase === "archives" ? {
			generation: cursor.generation,
			sessionId: cursor.sessionId
		} : {
			generation: "",
			sessionId: ""
		})) return true;
		return !hasActiveAgentDatabaseLease(params.agentId, params.env);
	} finally {
		clearNodeSqliteKyselyCacheForDatabase(database);
		database.close();
	}
}
/** One-time startup migration from inline assistant directives to typed delivery facts. */
async function migrateHistoricalTranscriptDirectives(params = {}) {
	const env = params.env ?? process.env;
	const changes = [];
	const warnings = [];
	try {
		const discoveredTargets = resolveAgentDatabaseMigrationTargets({
			changes,
			configuredAgentDatabaseTargets: params.configuredAgentDatabaseTargets ?? [],
			env,
			warnings
		});
		const targets = [];
		for (const target of discoveredTargets) try {
			if (agentDatabaseNeedsTranscriptDirectiveMigration({
				agentId: target.agentId,
				env,
				pathname: target.path
			})) targets.push(target);
		} catch (error) {
			warnings.push(`Skipped historical transcript directive migration preflight for ${target.path}: ${String(error)}`);
		}
		if (targets.length === 0) return {
			changes,
			warnings
		};
		await withAgentDatabaseMaintenanceLease({ env }, async () => {
			for (const target of targets) try {
				const result = await migrateAgentDatabase({
					agentId: target.agentId,
					pathname: target.path
				});
				if (result.transcriptSessions > 0 || result.archivedTranscripts > 0) changes.push(`Migrated historical transcript directives in ${target.path}: ${result.transcriptSessions} active session(s), ${result.archivedTranscripts} archived transcript(s).`);
			} catch (error) {
				warnings.push(`Skipped historical transcript directive migration for ${target.path}: ${String(error)}`);
			}
		});
	} catch (error) {
		warnings.push(`Skipped historical transcript directive migration: ${String(error)}`);
	}
	return {
		changes,
		warnings
	};
}
//#endregion
//#region src/infra/state-migrations.tui-last-session.ts
const LEGACY_RECORD_KEYS = /* @__PURE__ */ new Set(["sessionKey", "updatedAt"]);
const TUI_LAST_SESSION_STATE_KEY_PREFIX = "tui.lastSession.";
function resolveLegacyTuiLastSessionPath(stateDir) {
	return path.join(stateDir, "tui", "last-session.json");
}
/** Detect retired TUI state only when an explicit doctor flow opts in. */
function detectLegacyTuiLastSessions(params) {
	const sourcePath = resolveLegacyTuiLastSessionPath(params.stateDir);
	return {
		sourcePath,
		hasLegacy: params.doctorOnlyStateMigrations === true && fs.existsSync(sourcePath)
	};
}
function readLegacySourceSnapshot(sourcePath) {
	return readLegacyMigrationSourceSnapshotSync({
		sourcePath,
		label: "TUI last-session",
		followSymlinks: true
	});
}
function assertLegacySourceUnchanged(sourcePath, expected) {
	assertLegacyMigrationSourceUnchanged({
		sourcePath,
		snapshot: expected,
		label: "TUI last-session",
		followSymlinks: true
	});
}
function isHeartbeatSessionKey(sessionKey) {
	return sessionKey.toLowerCase().endsWith(":heartbeat");
}
function parseLegacyTuiLastSessions(raw) {
	const parsed = JSON.parse(raw);
	if (!isRecord(parsed)) throw new Error("legacy TUI last-session store must be a JSON object");
	const records = [];
	for (const [scopeKey, value] of Object.entries(parsed)) {
		if (!scopeKey || scopeKey.trim() !== scopeKey) throw new Error("legacy TUI last-session store contains an invalid scope key");
		if (!isRecord(value)) throw new Error(`legacy TUI last-session record ${scopeKey} must be an object`);
		assertAllowedJsonFields(value, LEGACY_RECORD_KEYS, `legacy TUI last-session record ${scopeKey}`);
		const sessionKey = value.sessionKey;
		const updatedAt = value.updatedAt;
		if (typeof sessionKey !== "string" || !sessionKey || sessionKey.trim() !== sessionKey || sessionKey === "unknown") throw new Error(`legacy TUI last-session record ${scopeKey} has an invalid session key`);
		if (!Number.isSafeInteger(updatedAt) || updatedAt < 0) throw new Error(`legacy TUI last-session record ${scopeKey} has an invalid timestamp`);
		records.push({
			scopeKey,
			sessionKey,
			updatedAt
		});
	}
	return records;
}
function rowMatches(row, expected) {
	return row?.value_json === JSON.stringify(expected.sessionKey) && row.updated_at_ms === expected.updatedAt;
}
/** Import, verify, and remove the retired JSON store during an explicit doctor repair. */
function migrateLegacyTuiLastSessions(params) {
	const changes = [];
	const warnings = [];
	const notices = [];
	if (!params.detected.hasLegacy) return {
		changes,
		warnings
	};
	let snapshot;
	let records;
	try {
		snapshot = readLegacySourceSnapshot(params.detected.sourcePath);
		records = parseLegacyTuiLastSessions(snapshot.raw);
	} catch (error) {
		warnings.push(`Failed reading legacy TUI last-session state ${params.detected.sourcePath}: ${String(error)}`);
		return {
			changes,
			warnings
		};
	}
	const activeRecords = records.filter((record) => !isHeartbeatSessionKey(record.sessionKey));
	const discardedHeartbeatCount = records.length - activeRecords.length;
	const expectedRows = /* @__PURE__ */ new Map();
	let importedCount = 0;
	let supersededCount = 0;
	try {
		assertLegacySourceUnchanged(params.detected.sourcePath, snapshot);
		runOpenClawStateWriteTransaction(({ db }) => {
			const tuiDb = getNodeSqliteKysely(db);
			for (const record of activeRecords) {
				const stateKey = `${TUI_LAST_SESSION_STATE_KEY_PREFIX}${record.scopeKey}`;
				const existing = executeSqliteQueryTakeFirstSync(db, tuiDb.selectFrom("config_machine_state").select(["value_json", "updated_at_ms"]).where("state_key", "=", stateKey));
				if (!existing) {
					executeSqliteQuerySync(db, tuiDb.insertInto("config_machine_state").values({
						state_key: stateKey,
						value_json: JSON.stringify(record.sessionKey),
						updated_at_ms: record.updatedAt
					}));
					expectedRows.set(record.scopeKey, record);
					importedCount += 1;
					continue;
				}
				const existingSessionKey = JSON.parse(existing.value_json);
				if (existing.updated_at_ms === record.updatedAt) {
					if (existingSessionKey !== record.sessionKey) throw new Error(`scope ${record.scopeKey} has divergent JSON and SQLite pointers at the same timestamp`);
					expectedRows.set(record.scopeKey, record);
					continue;
				}
				if (existing.updated_at_ms > record.updatedAt) {
					expectedRows.set(record.scopeKey, {
						scopeKey: record.scopeKey,
						sessionKey: existingSessionKey,
						updatedAt: existing.updated_at_ms
					});
					supersededCount += 1;
					continue;
				}
				executeSqliteQuerySync(db, tuiDb.updateTable("config_machine_state").set({
					value_json: JSON.stringify(record.sessionKey),
					updated_at_ms: record.updatedAt
				}).where("state_key", "=", stateKey));
				expectedRows.set(record.scopeKey, record);
				importedCount += 1;
			}
		}, { env: {
			...process.env,
			OPENCLAW_STATE_DIR: params.stateDir
		} });
	} catch (error) {
		warnings.push(`Failed migrating legacy TUI last-session state: ${String(error)}`);
		return {
			changes,
			warnings
		};
	}
	try {
		params.beforeVerify?.();
		const database = openOpenClawStateDatabase({ env: {
			...process.env,
			OPENCLAW_STATE_DIR: params.stateDir
		} });
		const tuiDb = getNodeSqliteKysely(database.db);
		for (const expected of expectedRows.values()) if (!rowMatches(executeSqliteQueryTakeFirstSync(database.db, tuiDb.selectFrom("config_machine_state").select(["value_json", "updated_at_ms"]).where("state_key", "=", `${TUI_LAST_SESSION_STATE_KEY_PREFIX}${expected.scopeKey}`)), expected)) throw new Error(`SQLite verification failed for scope ${expected.scopeKey}`);
		assertLegacySourceUnchanged(params.detected.sourcePath, snapshot);
	} catch (error) {
		warnings.push(`Failed verifying legacy TUI last-session migration: ${String(error)}`);
		return {
			changes,
			warnings
		};
	}
	try {
		claimAndRemoveLegacyMigrationSource({
			sourcePath: params.detected.sourcePath,
			snapshot,
			label: "TUI last-session",
			followSymlinks: true,
			beforeClaim: params.beforeClaim,
			removeSource: params.removeSource
		});
	} catch (error) {
		warnings.push(`Migrated TUI last-session state but could not remove legacy source ${params.detected.sourcePath}: ${String(error)}`);
		return {
			changes,
			warnings
		};
	}
	if (importedCount > 0) changes.push(`Migrated ${importedCount} TUI last-session pointer(s) → shared SQLite state`);
	if (discardedHeartbeatCount > 0) changes.push(`Discarded ${discardedHeartbeatCount} legacy heartbeat TUI restore pointer(s)`);
	changes.push("Removed legacy TUI last-session JSON after SQLite verification");
	if (supersededCount > 0) notices.push(`Kept ${supersededCount} newer shared SQLite TUI last-session pointer(s) over legacy JSON`);
	return notices.length > 0 ? {
		changes,
		warnings,
		notices
	} : {
		changes,
		warnings
	};
}
//#endregion
//#region src/infra/state-migrations.update-check.ts
const UPDATE_CHECK_STATE_KEY = "update.checkState";
const UPDATE_CHECK_STATE_FIELDS = [
	"lastCheckedAt",
	"lastNotifiedVersion",
	"lastNotifiedTag",
	"lastAvailableVersion",
	"lastAvailableTag",
	"autoInstallId",
	"autoFirstSeenVersion",
	"autoFirstSeenTag",
	"autoFirstSeenAt",
	"autoLastAttemptVersion",
	"autoLastAttemptAt",
	"autoLastSuccessVersion",
	"autoLastSuccessAt"
];
function resolveLegacyUpdateCheckPath(stateDir) {
	return path.join(stateDir, "update-check.json");
}
function normalizeLegacyUpdateCheckState(input) {
	const record = input && typeof input === "object" ? input : {};
	return Object.fromEntries(UPDATE_CHECK_STATE_FIELDS.map((field) => {
		const value = record[field];
		return [field, typeof value === "string" && value.trim().length > 0 ? value : void 0];
	}));
}
function legacyUpdateCheckStateMatches(existing, state) {
	return UPDATE_CHECK_STATE_FIELDS.every((field) => state[field] === existing[field]);
}
function migrateLegacyUpdateCheckState(params) {
	return migrateLegacyJsonState({
		sourcePath: params.detected.sourcePath,
		stateDir: params.stateDir,
		label: "update-check state",
		normalize: normalizeLegacyUpdateCheckState,
		migrate(_db, state) {
			const options = { env: {
				...process.env,
				OPENCLAW_STATE_DIR: params.stateDir
			} };
			if (importConfigMachineState([[UPDATE_CHECK_STATE_KEY, state]], options).kept.length > 0) {
				const existing = readConfigMachineState(UPDATE_CHECK_STATE_KEY, options);
				return {
					changes: [],
					...existing && legacyUpdateCheckStateMatches(existing, state) ? {} : { notices: [`Kept shared SQLite update-check state because legacy cache differs: ${params.detected.sourcePath}`] }
				};
			}
			return { changes: ["Migrated update-check state → shared SQLite state"] };
		}
	});
}
//#endregion
//#region src/infra/state-migrations.web-push-parse.ts
const SUBSCRIPTION_STORE_KEYS = /* @__PURE__ */ new Set(["subscriptionsByEndpointHash"]);
const SUBSCRIPTION_KEYS = /* @__PURE__ */ new Set([
	"subscriptionId",
	"endpoint",
	"keys",
	"createdAtMs",
	"updatedAtMs"
]);
const PUSH_KEYS = /* @__PURE__ */ new Set(["p256dh", "auth"]);
const VAPID_KEYS = /* @__PURE__ */ new Set([
	"publicKey",
	"privateKey",
	"subject"
]);
const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
function parseLegacySubscriptions(raw) {
	const parsed = JSON.parse(raw);
	if (!isRecord(parsed) || !isRecord(parsed.subscriptionsByEndpointHash)) throw new Error("legacy Web Push subscriptions must be an object");
	assertAllowedJsonFields(parsed, SUBSCRIPTION_STORE_KEYS, "legacy Web Push subscriptions store");
	const subscriptions = /* @__PURE__ */ new Map();
	const subscriptionIds = /* @__PURE__ */ new Set();
	for (const [endpointHash, rawSubscription] of Object.entries(parsed.subscriptionsByEndpointHash)) {
		if (!isRecord(rawSubscription) || !isRecord(rawSubscription.keys)) throw new Error("legacy Web Push subscription is not an object");
		assertAllowedJsonFields(rawSubscription, SUBSCRIPTION_KEYS, "legacy Web Push subscription");
		assertAllowedJsonFields(rawSubscription.keys, PUSH_KEYS, "legacy Web Push subscription keys");
		const { subscriptionId, endpoint, createdAtMs, updatedAtMs } = rawSubscription;
		const p256dh = rawSubscription.keys.p256dh;
		const auth = rawSubscription.keys.auth;
		if (typeof subscriptionId !== "string" || !UUID_RE.test(subscriptionId) || typeof endpoint !== "string" || !isValidWebPushEndpoint(endpoint) || hashWebPushEndpoint(endpoint) !== endpointHash || !isValidWebPushKey(p256dh) || !isValidWebPushKey(auth) || typeof createdAtMs !== "number" || !Number.isSafeInteger(createdAtMs) || createdAtMs < 0 || typeof updatedAtMs !== "number" || !Number.isSafeInteger(updatedAtMs) || updatedAtMs < createdAtMs) throw new Error("legacy Web Push subscription is invalid");
		if (subscriptionIds.has(subscriptionId)) throw new Error("legacy Web Push subscriptions contain a duplicate subscription id");
		subscriptionIds.add(subscriptionId);
		subscriptions.set(endpointHash, {
			subscriptionId,
			endpoint,
			keys: {
				p256dh,
				auth
			},
			createdAtMs,
			updatedAtMs
		});
	}
	return subscriptions;
}
function parseLegacyVapidKeys(raw, env) {
	const parsed = JSON.parse(raw);
	if (!isRecord(parsed)) throw new Error("legacy Web Push VAPID keys must be an object");
	assertAllowedJsonFields(parsed, VAPID_KEYS, "legacy Web Push VAPID keys");
	if (parsed.subject !== void 0 && typeof parsed.subject !== "string") throw new Error("legacy Web Push VAPID keys are invalid");
	const subject = normalizeOptionalString(parsed.subject) ?? normalizeOptionalString(env.OPENCLAW_VAPID_SUBJECT) ?? "https://openclaw.ai";
	if (!isValidWebPushKey(parsed.publicKey) || !isValidWebPushKey(parsed.privateKey) || subject.length > 512) throw new Error("legacy Web Push VAPID keys are invalid");
	return createWebPushVapidKeyPair(parsed.publicKey, parsed.privateKey, subject);
}
//#endregion
//#region src/infra/state-migrations.web-push.ts
const LEGACY_SUBSCRIPTIONS_MAX_BYTES = 4194304;
const LEGACY_VAPID_KEYS_MAX_BYTES = 65536;
function resolveLegacyWebPushPaths(stateDir) {
	return {
		subscriptionsPath: path.join(stateDir, "push", "web-push-subscriptions.json"),
		vapidKeysPath: path.join(stateDir, "push", "vapid-keys.json")
	};
}
function detectLegacyWebPush(params) {
	const paths = resolveLegacyWebPushPaths(params.stateDir);
	return {
		...paths,
		hasLegacy: params.doctorOnlyStateMigrations === true && (legacyMigrationSourceOrClaimMayExist(paths.subscriptionsPath) || legacyMigrationSourceOrClaimMayExist(paths.vapidKeysPath))
	};
}
function createLegacySourceClaim$1(stateRoot, stateDir, sourcePath, maxBytes) {
	return new LegacyMigrationSourceClaim({
		stateRoot,
		stateDir,
		sourcePath,
		label: "Web Push",
		readSnapshot: (snapshotPath) => readLegacyMigrationSourceSnapshot({
			stateRoot,
			stateDir,
			sourcePath: snapshotPath,
			maxBytes,
			label: "Web Push",
			hashDecodedText: true
		})
	});
}
async function readLegacyState(stateRoot, stateDir, detected, env) {
	const subscriptionsSource = createLegacySourceClaim$1(stateRoot, stateDir, detected.subscriptionsPath, LEGACY_SUBSCRIPTIONS_MAX_BYTES);
	const vapidSource = createLegacySourceClaim$1(stateRoot, stateDir, detected.vapidKeysPath, LEGACY_VAPID_KEYS_MAX_BYTES);
	await subscriptionsSource.recover("interrupted Web Push doctor claim conflicts with its source");
	await vapidSource.recover("interrupted Web Push doctor claim conflicts with its source");
	const sources = [];
	let subscriptions = /* @__PURE__ */ new Map();
	let vapidKeys = null;
	if (await subscriptionsSource.exists()) {
		const snapshot = await subscriptionsSource.read();
		subscriptions = parseLegacySubscriptions(snapshot.raw);
		sources.push({
			claim: subscriptionsSource,
			snapshot
		});
	}
	if (await vapidSource.exists()) {
		const snapshot = await vapidSource.read();
		vapidKeys = parseLegacyVapidKeys(snapshot.raw, env);
		sources.push({
			claim: vapidSource,
			snapshot
		});
	}
	return {
		subscriptions,
		vapidKeys,
		sources
	};
}
async function assertSourcesUnchanged(sources) {
	for (const { claim, snapshot } of sources) if (!legacyMigrationSourceSnapshotsMatch(await claim.read(), snapshot)) throw new Error("legacy Web Push source changed after doctor loaded it");
}
function mergedSubscription(params) {
	const { existing, legacy } = params;
	const createdAtMs = Math.min(existing.createdAtMs, legacy.createdAtMs);
	if (existing.updatedAtMs === legacy.updatedAtMs) {
		const normalizedExisting = {
			...existing,
			createdAtMs
		};
		const normalizedLegacy = {
			...legacy,
			createdAtMs
		};
		if (!webPushSubscriptionsEqual(normalizedExisting, normalizedLegacy)) throw new Error("Web Push subscription diverges at the same timestamp");
		return normalizedExisting;
	}
	return {
		...existing.updatedAtMs > legacy.updatedAtMs ? existing : legacy,
		createdAtMs
	};
}
function findSubscriptionById(db, subscriptionId) {
	return executeSqliteQueryTakeFirstSync(db, getNodeSqliteKysely(db).selectFrom("web_push_subscriptions").selectAll().where("subscription_id", "=", subscriptionId));
}
function writeSubscription(db, endpointHash, subscription) {
	const row = webPushSubscriptionToRow({
		endpointHash,
		subscription
	});
	executeSqliteQuerySync(db, getNodeSqliteKysely(db).insertInto("web_push_subscriptions").values(row).onConflict((conflict) => conflict.column("endpoint_hash").doUpdateSet({
		subscription_id: row.subscription_id,
		endpoint: row.endpoint,
		p256dh: row.p256dh,
		auth: row.auth,
		created_at_ms: row.created_at_ms,
		updated_at_ms: row.updated_at_ms
	})));
}
function migrateIntoDatabase(params) {
	let importedSubscriptions = 0;
	let importedVapidKeys = false;
	runOpenClawStateWriteTransaction(({ db }) => {
		ensureWebPushSubscriptionBindingColumns(db);
		const webPushDb = getNodeSqliteKysely(db);
		const expectedSubscriptions = /* @__PURE__ */ new Map();
		for (const [endpointHash, legacySubscription] of params.legacy.subscriptions) {
			const existingRow = executeSqliteQueryTakeFirstSync(db, webPushDb.selectFrom("web_push_subscriptions").selectAll().where("endpoint_hash", "=", endpointHash));
			if (existingRow && existingRow.endpoint !== legacySubscription.endpoint) throw new Error("Web Push endpoint hash collision during legacy import");
			const existing = existingRow ? webPushSubscriptionFromRow(existingRow) : null;
			const expected = existing ? mergedSubscription({
				existing,
				legacy: legacySubscription
			}) : legacySubscription;
			const conflictingIdRow = findSubscriptionById(db, expected.subscriptionId);
			if (conflictingIdRow && conflictingIdRow.endpoint_hash !== endpointHash) throw new Error("Web Push subscription id conflicts with another endpoint");
			if (!existing || !webPushSubscriptionsEqual(existing, expected)) {
				writeSubscription(db, endpointHash, expected);
				importedSubscriptions += 1;
			}
			expectedSubscriptions.set(endpointHash, expected);
		}
		let expectedVapidKeys = null;
		if (params.legacy.vapidKeys) {
			const existingVapidRow = executeSqliteQueryTakeFirstSync(db, webPushDb.selectFrom("config_machine_state").select("value_json").where("state_key", "=", WEB_PUSH_VAPID_STATE_KEY));
			if (existingVapidRow) {
				const existingVapidKeys = JSON.parse(existingVapidRow.value_json);
				if (existingVapidKeys.publicKey !== params.legacy.vapidKeys.publicKey || existingVapidKeys.privateKey !== params.legacy.vapidKeys.privateKey) throw new Error("legacy Web Push VAPID identity conflicts with SQLite");
				expectedVapidKeys = existingVapidKeys;
			} else {
				executeSqliteQuerySync(db, webPushDb.insertInto("config_machine_state").values({
					state_key: WEB_PUSH_VAPID_STATE_KEY,
					value_json: JSON.stringify(params.legacy.vapidKeys),
					updated_at_ms: params.nowMs
				}));
				expectedVapidKeys = params.legacy.vapidKeys;
				importedVapidKeys = true;
			}
		}
		for (const [endpointHash, expected] of expectedSubscriptions) {
			const row = executeSqliteQueryTakeFirstSync(db, webPushDb.selectFrom("web_push_subscriptions").selectAll().where("endpoint_hash", "=", endpointHash));
			if (!row || !webPushSubscriptionsEqual(webPushSubscriptionFromRow(row), expected)) throw new Error("SQLite verification failed for a Web Push subscription");
		}
		if (expectedVapidKeys) {
			const row = executeSqliteQueryTakeFirstSync(db, webPushDb.selectFrom("config_machine_state").select("value_json").where("state_key", "=", WEB_PUSH_VAPID_STATE_KEY));
			const persisted = row ? JSON.parse(row.value_json) : void 0;
			if (!persisted || persisted.publicKey !== expectedVapidKeys.publicKey || persisted.privateKey !== expectedVapidKeys.privateKey || persisted.subject !== expectedVapidKeys.subject) throw new Error("SQLite verification failed for the Web Push VAPID identity");
		}
	}, { env: {
		...process.env,
		OPENCLAW_STATE_DIR: params.stateDir
	} });
	return {
		importedSubscriptions,
		importedVapidKeys
	};
}
async function removeClaimedSources(params) {
	for (const claim of params.claimed) if (await claim.exists()) throw new Error(`legacy Web Push source reappeared during import: ${claim.sourcePath}`);
	for (const claim of params.claimed) await claim.remove({
		removeSource: params.removeSource,
		skipSourceCheck: true
	});
}
async function migrateLegacyWebPushWithExclusiveStateOwnership(params) {
	const changes = [];
	const warnings = [];
	const notices = [];
	if (!params.detected.hasLegacy) return {
		changes,
		warnings
	};
	let legacy;
	try {
		legacy = await readLegacyState(params.stateRoot, params.stateDir, params.detected, params.env);
	} catch (error) {
		warnings.push(`Failed reading legacy Web Push state: ${String(error)}`);
		return {
			changes,
			warnings
		};
	}
	let claimed;
	try {
		params.beforeVerify?.();
		await assertSourcesUnchanged(legacy.sources);
		await claimLegacyMigrationSourceClaims(legacy.sources, {
			beforeClaim: params.beforeClaim,
			mismatchMessage: "legacy Web Push source changed before doctor could claim it"
		});
		claimed = legacy.sources.map(({ claim }) => claim);
	} catch (error) {
		warnings.push(`Failed migrating legacy Web Push state: ${String(error)}`);
		return {
			changes,
			warnings
		};
	}
	let result;
	try {
		result = migrateIntoDatabase({
			stateDir: params.stateDir,
			legacy,
			nowMs: Date.now()
		});
	} catch (error) {
		const restoreErrors = await restoreLegacyMigrationSourceClaims(claimed);
		warnings.push(`Failed migrating legacy Web Push state: ${String(error)}${restoreErrors.length > 0 ? `; restore failures: ${restoreErrors.join("; ")}` : ""}`);
		return {
			changes,
			warnings
		};
	}
	try {
		await removeClaimedSources({
			claimed,
			removeSource: params.removeSource
		});
	} catch (error) {
		warnings.push(`Web Push state is in SQLite, but legacy cleanup failed: ${String(error)}`);
		return {
			changes,
			warnings
		};
	}
	changes.push(`Migrated ${result.importedSubscriptions} Web Push subscription${result.importedSubscriptions === 1 ? "" : "s"} to SQLite.`);
	if (result.importedVapidKeys) changes.push("Migrated the Web Push VAPID identity to SQLite.");
	notices.push("Removed retired Web Push JSON state after verified SQLite import.");
	return {
		changes,
		warnings,
		notices
	};
}
async function migrateLegacyWebPush(params) {
	if (!params.detected.hasLegacy) return {
		changes: [],
		warnings: []
	};
	return await withLegacyMigrationStateLock({
		stateDir: params.stateDir,
		env: params.env,
		label: "legacy Web Push state",
		releaseLabel: "Web Push",
		errorLabel: "Failed reading legacy Web Push state",
		run: async (env) => {
			const stateRoot = await root(params.stateDir, {
				hardlinks: "reject",
				maxBytes: LEGACY_SUBSCRIPTIONS_MAX_BYTES,
				symlinks: "reject"
			});
			return await migrateLegacyWebPushWithExclusiveStateOwnership({
				...params,
				env,
				stateRoot
			});
		}
	});
}
//#endregion
//#region src/infra/state-migrations.workspace-setup-receipts.ts
function resolveWorkspaceMigrationSourceKey(source) {
	return resolveLegacyMigrationSourceKey(`workspace-${source.kind}`, source.sourcePath, source.workspaceKey);
}
function readReceipt(source, env) {
	const receipt = readLegacyMigrationReceipt(resolveWorkspaceMigrationSourceKey(source), env);
	const archivePath = receipt ? safeParseJsonRecord(receipt.reportJson)?.archivePath : void 0;
	return receipt ? {
		sourceKey: receipt.sourceKey,
		sha256: receipt.sourceSha256,
		removedSource: receipt.removedSource,
		...typeof archivePath === "string" ? { archivePath } : {}
	} : null;
}
//#endregion
//#region src/infra/state-migrations.workspace-setup-store.ts
const MIGRATION_KIND = WORKSPACE_LEGACY_STATE_MIGRATION_KIND;
function parseIsoTimestamp(value, field) {
	if (value === void 0) return;
	if (typeof value !== "string" || value.length === 0) throw new Error(`legacy workspace setup ${field} is invalid`);
	const parsed = new Date(value);
	if (!Number.isFinite(parsed.getTime()) || parsed.toISOString() !== value) throw new Error(`legacy workspace setup ${field} is invalid`);
	return value;
}
function parseSetup(raw) {
	let value;
	try {
		value = JSON.parse(raw);
	} catch {
		throw new Error("legacy workspace setup contains invalid JSON");
	}
	if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("legacy workspace setup is not an object");
	const record = value;
	const allowed = /* @__PURE__ */ new Set([
		"version",
		"bootstrapSeededAt",
		"setupCompletedAt",
		"onboardingCompletedAt"
	]);
	if (Object.keys(record).some((key) => !allowed.has(key))) throw new Error("legacy workspace setup has an unexpected field");
	if (record.version !== void 0 && record.version !== 1) throw new Error("legacy workspace setup has an unsupported version");
	const bootstrapSeededAt = parseIsoTimestamp(record.bootstrapSeededAt, "bootstrap timestamp");
	const setupCompletedAt = parseIsoTimestamp(record.setupCompletedAt, "completion timestamp");
	const onboardingCompletedAt = parseIsoTimestamp(record.onboardingCompletedAt, "legacy completion timestamp");
	if (setupCompletedAt && onboardingCompletedAt && setupCompletedAt !== onboardingCompletedAt) throw new Error("legacy workspace setup has conflicting completion timestamps");
	const parsed = {
		...bootstrapSeededAt ? { bootstrapSeededAt } : {},
		...setupCompletedAt ?? onboardingCompletedAt ? { setupCompletedAt: setupCompletedAt ?? onboardingCompletedAt } : {}
	};
	return {
		kind: "setup",
		value: parsed,
		recordCount: Number(Boolean(parsed.bootstrapSeededAt)) + Number(Boolean(parsed.setupCompletedAt))
	};
}
function parseAttestation(snapshot) {
	const lines = snapshot.raw.split(/\r?\n/);
	if (lines.at(-1) === "") lines.pop();
	if (lines[0] !== "openclaw-workspace-attestation:v1" || lines.length < 2) throw new Error("legacy workspace attestation has an invalid header");
	parseIsoTimestamp(lines[1], "attestation timestamp");
	const generatedHashes = /* @__PURE__ */ new Map();
	for (const line of lines.slice(2)) {
		const match = /^generated:([^:]+):([a-f0-9]{64})$/.exec(line);
		if (!match?.[1] || !match[2] || !isSafeWorkspaceAttestationFilename(match[1])) throw new Error("legacy workspace attestation has an invalid generated hash");
		if (generatedHashes.has(match[1])) throw new Error("legacy workspace attestation has a duplicate generated hash");
		generatedHashes.set(match[1], match[2]);
	}
	const attestedAtMs = Math.trunc(snapshot.mtimeMs);
	if (!Number.isSafeInteger(attestedAtMs) || attestedAtMs < 0) throw new Error("legacy workspace attestation has an invalid modification time");
	return {
		kind: "attestation",
		value: {
			attestedAtMs,
			generatedHashes
		},
		recordCount: 1 + generatedHashes.size
	};
}
function parseSource(source, snapshot) {
	return source.kind === "setup" ? parseSetup(snapshot.raw) : parseAttestation(snapshot);
}
function mapsEqual(left, right) {
	if (left.size !== right.size) return false;
	for (const [key, value] of left) if (right.get(key) !== value) return false;
	return true;
}
function canonicalFingerprint(value) {
	return createHash("sha256").update(JSON.stringify(value)).digest("hex");
}
function setupFingerprint(params) {
	return canonicalFingerprint({
		kind: "setup",
		workspacePath: params.workspace_path,
		version: 1,
		bootstrapSeededAt: params.bootstrap_seeded_at,
		setupCompletedAt: params.setup_completed_at
	});
}
function attestationFingerprint(params) {
	return canonicalFingerprint({
		kind: "attestation",
		attestedAtMs: params.attestedAtMs,
		generatedHashes: [...params.generatedHashes.entries()].toSorted(([left], [right]) => left.localeCompare(right))
	});
}
function receiptPreservesAuthority(receipt, expectedFingerprint) {
	const report = receipt ? safeParseJsonRecord(receipt.reportJson) : void 0;
	return report?.authoritative === true && report.canonicalFingerprint === expectedFingerprint;
}
function findMigrationAuthority(params) {
	const rows = executeSqliteQuerySync(params.db, params.kysely.selectFrom("migration_sources").select("report_json").where("migration_kind", "=", MIGRATION_KIND).where("target_table", "=", params.source.kind === "setup" ? "workspace_setup_state" : "workspace_attestations")).rows;
	let bestPriority = null;
	for (const row of rows) {
		if (!row.report_json) continue;
		try {
			const report = JSON.parse(row.report_json);
			if (report.workspaceKey !== params.source.workspaceKey || report.sourceKind !== params.source.kind || report.canonicalFingerprint !== params.fingerprint || report.authoritative !== true || typeof report.sourcePriority !== "number" || !Number.isSafeInteger(report.sourcePriority) || report.sourcePriority < 0) continue;
			bestPriority = bestPriority === null ? report.sourcePriority : Math.min(bestPriority, report.sourcePriority);
		} catch {}
	}
	return bestPriority === null ? null : { priority: bestPriority };
}
function canonicalCoversParsedSource(params) {
	const { db } = openOpenClawStateDatabase({ env: params.env });
	return runSqliteDeferredTransactionSync(db, () => {
		const kysely = getNodeSqliteKysely(db);
		if (params.source.kind === "setup" && params.parsed.kind === "setup") {
			const row = executeSqliteQueryTakeFirstSync(db, kysely.selectFrom("workspace_setup_state").selectAll().where("workspace_key", "=", params.source.workspaceKey));
			return row?.version === 1 && row.workspace_path === params.source.workspaceDir;
		}
		if (params.source.kind !== "attestation" || params.parsed.kind !== "attestation") return false;
		const row = executeSqliteQueryTakeFirstSync(db, kysely.selectFrom("workspace_setup_state").select("attested_at_ms").where("workspace_key", "=", params.source.workspaceKey));
		if (!row || row.attested_at_ms == null) return false;
		if (row.attested_at_ms > params.parsed.value.attestedAtMs) return true;
		if (row.attested_at_ms < params.parsed.value.attestedAtMs) return false;
		const hashes = new Map(executeSqliteQuerySync(db, kysely.selectFrom("workspace_generated_bootstrap_hashes").select(["filename", "sha256"]).where("workspace_key", "=", params.source.workspaceKey)).rows.map((hashRow) => [hashRow.filename, hashRow.sha256]));
		if (mapsEqual(hashes, params.parsed.value.generatedHashes)) return true;
		const fingerprint = attestationFingerprint({
			attestedAtMs: row.attested_at_ms,
			generatedHashes: hashes
		});
		const authority = findMigrationAuthority({
			db,
			kysely,
			source: params.source,
			fingerprint
		});
		return Boolean(authority && authority.priority <= params.source.priority);
	});
}
function importAndRecordReceipt(params) {
	const key = resolveWorkspaceMigrationSourceKey(params.source);
	const runId = `${key}:${params.snapshot.sha256.slice(0, 16)}`;
	const now = Date.now();
	return runOpenClawStateWriteTransaction((database) => {
		const { db } = database;
		const kysely = getNodeSqliteKysely(db);
		const existingReceipt = readLegacyMigrationReceiptFromDatabase(db, key);
		if (existingReceipt && (existingReceipt.sourceSha256 !== params.previousReceipt?.sha256 || existingReceipt.removedSource !== params.previousReceipt?.removedSource)) throw new Error("workspace migration receipt appeared concurrently; retry Doctor");
		let imported = false;
		const differences = [];
		let resolution;
		let verifiedFingerprint;
		if (params.parsed.kind === "setup") {
			if (!params.source.workspaceDir) throw new Error("legacy workspace setup has no workspace path");
			const existing = executeSqliteQueryTakeFirstSync(db, kysely.selectFrom("workspace_setup_state").selectAll().where("workspace_key", "=", params.source.workspaceKey));
			if (existing && existing.version != null) {
				if (existing.workspace_path !== params.source.workspaceDir || existing.version !== 1) throw new Error("legacy workspace setup conflicts with canonical SQLite state");
				const existingFingerprint = setupFingerprint(existing);
				for (const [milestone, canonical] of [["bootstrapSeededAt", existing.bootstrap_seeded_at], ["setupCompletedAt", existing.setup_completed_at]]) {
					const legacy = params.parsed.value[milestone];
					if (legacy !== void 0 && legacy !== canonical) differences.push(`${milestone} legacy=${JSON.stringify(legacy)} canonical=${JSON.stringify(canonical)}`);
				}
				resolution = differences.length > 0 ? "superseded" : "verified";
				verifiedFingerprint = existingFingerprint;
			} else {
				if (existing?.workspace_path != null && existing.workspace_path !== params.source.workspaceDir) throw new Error("legacy workspace setup conflicts with canonical SQLite state");
				const setupColumns = {
					workspace_path: params.source.workspaceDir,
					version: 1,
					bootstrap_seeded_at: params.parsed.value.bootstrapSeededAt ?? null,
					setup_completed_at: params.parsed.value.setupCompletedAt ?? null,
					updated_at: now
				};
				executeSqliteQuerySync(db, kysely.insertInto("workspace_setup_state").values({
					workspace_key: params.source.workspaceKey,
					...setupColumns
				}).onConflict((conflict) => conflict.column("workspace_key").doUpdateSet(setupColumns)));
				imported = true;
				resolution = existing ? "merged" : "inserted";
				verifiedFingerprint = setupFingerprint(setupColumns);
			}
			const verified = executeSqliteQueryTakeFirstSync(db, kysely.selectFrom("workspace_setup_state").selectAll().where("workspace_key", "=", params.source.workspaceKey));
			const actualFingerprint = verified && verified.workspace_path != null ? setupFingerprint(verified) : null;
			if (!verified || actualFingerprint !== verifiedFingerprint) throw new Error("SQLite verification failed for workspace setup state");
		} else {
			const parsedAttestation = params.parsed.value;
			const insertGeneratedHashes = () => {
				const hashes = [...parsedAttestation.generatedHashes.entries()].toSorted(([a], [b]) => a.localeCompare(b));
				if (hashes.length > 0) executeSqliteQuerySync(db, kysely.insertInto("workspace_generated_bootstrap_hashes").values(hashes.map(([filename, sha256]) => ({
					workspace_key: params.source.workspaceKey,
					filename,
					sha256
				}))));
			};
			const incomingFingerprint = attestationFingerprint({
				attestedAtMs: parsedAttestation.attestedAtMs,
				generatedHashes: parsedAttestation.generatedHashes
			});
			const existingRow = executeSqliteQueryTakeFirstSync(db, kysely.selectFrom("workspace_setup_state").selectAll().where("workspace_key", "=", params.source.workspaceKey));
			const existing = existingRow && existingRow.attested_at_ms != null ? { attested_at_ms: existingRow.attested_at_ms } : null;
			if (existing) {
				const rows = executeSqliteQuerySync(db, kysely.selectFrom("workspace_generated_bootstrap_hashes").select(["filename", "sha256"]).where("workspace_key", "=", params.source.workspaceKey)).rows;
				const existingHashes = new Map(rows.map((row) => [row.filename, row.sha256]));
				const existingFingerprint = attestationFingerprint({
					attestedAtMs: existing.attested_at_ms,
					generatedHashes: existingHashes
				});
				const replaceExistingAttestation = () => {
					executeSqliteQuerySync(db, kysely.updateTable("workspace_setup_state").set({
						attested_at_ms: parsedAttestation.attestedAtMs,
						attestation_updated_at_ms: now
					}).where("workspace_key", "=", params.source.workspaceKey));
					executeSqliteQuerySync(db, kysely.deleteFrom("workspace_generated_bootstrap_hashes").where("workspace_key", "=", params.source.workspaceKey));
					insertGeneratedHashes();
				};
				if (existing.attested_at_ms === parsedAttestation.attestedAtMs && mapsEqual(existingHashes, parsedAttestation.generatedHashes)) {
					resolution = "verified";
					verifiedFingerprint = existingFingerprint;
				} else if (existing.attested_at_ms > parsedAttestation.attestedAtMs) {
					resolution = "superseded";
					verifiedFingerprint = existingFingerprint;
				} else if (existing.attested_at_ms === parsedAttestation.attestedAtMs) {
					const authority = findMigrationAuthority({
						db,
						kysely,
						source: params.source,
						fingerprint: existingFingerprint
					});
					if (!authority) throw new Error("legacy workspace attestation conflicts with canonical SQLite state");
					if (params.source.priority < authority.priority) {
						replaceExistingAttestation();
						imported = true;
						resolution = "replaced";
						verifiedFingerprint = incomingFingerprint;
					} else {
						resolution = "superseded";
						verifiedFingerprint = existingFingerprint;
					}
				} else {
					replaceExistingAttestation();
					imported = true;
					resolution = "replaced";
					verifiedFingerprint = incomingFingerprint;
				}
			} else {
				executeSqliteQuerySync(db, kysely.insertInto("workspace_setup_state").values({
					workspace_key: params.source.workspaceKey,
					workspace_path: params.source.workspaceDir ?? null,
					attested_at_ms: parsedAttestation.attestedAtMs,
					attestation_updated_at_ms: now
				}).onConflict((conflict) => conflict.column("workspace_key").doUpdateSet({
					attested_at_ms: parsedAttestation.attestedAtMs,
					attestation_updated_at_ms: now
				})));
				insertGeneratedHashes();
				imported = true;
				resolution = "inserted";
				verifiedFingerprint = incomingFingerprint;
			}
			const verifiedRow = executeSqliteQueryTakeFirstSync(db, kysely.selectFrom("workspace_setup_state").select("attested_at_ms").where("workspace_key", "=", params.source.workspaceKey));
			const verified = verifiedRow && verifiedRow.attested_at_ms != null ? { attested_at_ms: verifiedRow.attested_at_ms } : null;
			const verifiedHashes = new Map(executeSqliteQuerySync(db, kysely.selectFrom("workspace_generated_bootstrap_hashes").select(["filename", "sha256"]).where("workspace_key", "=", params.source.workspaceKey)).rows.map((row) => [row.filename, row.sha256]));
			const actualFingerprint = verified ? attestationFingerprint({
				attestedAtMs: verified.attested_at_ms,
				generatedHashes: verifiedHashes
			}) : null;
			if (!verified || actualFingerprint !== verifiedFingerprint) throw new Error("SQLite verification failed for workspace attestation state");
		}
		if (params.source.workspaceDir) registerWorkspaceStateAliasesInTransaction({
			database,
			workspaceDirs: [params.source.workspaceDir, params.source.workspaceAliasPath ?? params.source.workspaceDir],
			identity: {
				workspaceKey: params.source.workspaceKey,
				workspacePath: params.source.workspaceDir
			},
			updatedAtMs: now
		});
		const targetTable = params.parsed.kind === "setup" ? "workspace_setup_state" : "workspace_attestations";
		const reportJson = JSON.stringify({
			source: MIGRATION_KIND,
			sourceKind: params.parsed.kind,
			target: targetTable,
			workspaceKey: params.source.workspaceKey,
			sourceSha256: params.snapshot.sha256,
			sourceRecordCount: params.parsed.recordCount,
			sourcePriority: params.source.priority,
			canonicalFingerprint: verifiedFingerprint,
			authoritative: resolution === "inserted" || resolution === "replaced" || receiptPreservesAuthority(existingReceipt, verifiedFingerprint),
			resolution,
			imported,
			...params.archivePath ? {
				archivePath: params.archivePath,
				differences
			} : {}
		});
		recordLegacyMigrationReceipt(db, {
			sourceKey: key,
			migrationKind: MIGRATION_KIND,
			sourcePath: params.source.sourcePath,
			targetTable,
			sourceSha256: params.snapshot.sha256,
			sourceSizeBytes: params.snapshot.size,
			sourceRecordCount: params.parsed.recordCount,
			runId,
			now,
			reportJson,
			upsert: existingReceipt !== null
		});
		return {
			sourceKey: key,
			imported,
			differences
		};
	}, { env: params.env });
}
//#endregion
//#region src/infra/state-migrations.workspace-setup.ts
const SETUP_MAX_BYTES = 65536;
const CLAIM_SUFFIX = WORKSPACE_DOCTOR_CLAIM_SUFFIX;
const utf8Decoder = new TextDecoder$1("utf-8", { fatal: true });
async function readBoundedRegularFile(params) {
	const opened = await params.sourceRoot.open(params.relativePath, {
		hardlinks: "reject",
		symlinks: "reject"
	});
	try {
		const before = opened.stat;
		if (!before.isFile() || before.nlink !== 1 || !Number.isSafeInteger(before.size) || before.size < 0 || before.size > params.maxBytes) throw new Error("legacy workspace source is not a safe regular file");
		const buffer = Buffer.alloc(before.size);
		let offset = 0;
		while (offset < buffer.length) {
			const { bytesRead } = await opened.handle.read(buffer, offset, buffer.length - offset, offset);
			if (bytesRead === 0) throw new Error("legacy workspace source ended unexpectedly");
			offset += bytesRead;
		}
		const after = await opened.handle.stat();
		if (!after.isFile() || after.nlink !== 1 || after.dev !== before.dev || after.ino !== before.ino || after.size !== before.size || after.mtimeMs !== before.mtimeMs || after.ctimeMs !== before.ctimeMs || offset !== after.size) throw new Error("legacy workspace source changed while reading");
		let raw;
		try {
			raw = utf8Decoder.decode(buffer);
		} catch {
			throw new Error("legacy workspace source is not valid UTF-8");
		}
		return {
			sourcePath: params.sourcePath,
			dev: after.dev,
			ino: after.ino,
			mtimeMs: after.mtimeMs,
			sha256: createHash("sha256").update(buffer).digest("hex"),
			size: after.size,
			raw,
			buffer
		};
	} finally {
		await opened[Symbol.asyncDispose]();
	}
}
async function archiveWorkspaceSetupSource(sourceRoot, source, snapshot, existingArchivePath) {
	const archivePath = existingArchivePath ?? `${source.sourcePath}.migrated.${snapshot.sha256}.${randomUUID()}`;
	const relativePath = path.relative(source.rootDir, archivePath);
	if (!existingArchivePath) await sourceRoot.create(relativePath, snapshot.buffer, { mode: 384 });
	if ((await readBoundedRegularFile({
		sourceRoot,
		relativePath,
		sourcePath: archivePath,
		maxBytes: SETUP_MAX_BYTES
	})).sha256 !== snapshot.sha256) throw new Error(`workspace setup backup differs from the claimed source: ${archivePath}`);
	return archivePath;
}
function createLegacySourceClaim(sourceRoot, source) {
	return new LegacyMigrationSourceClaim({
		stateRoot: sourceRoot,
		stateDir: source.rootDir,
		sourcePath: source.sourcePath,
		label: "workspace",
		claimSuffix: CLAIM_SUFFIX,
		formatError: formatErrorMessage,
		readSnapshot: (sourcePath) => readBoundedRegularFile({
			sourceRoot,
			relativePath: sourcePath === source.sourcePath ? source.relativePath : `${source.relativePath}${CLAIM_SUFFIX}`,
			sourcePath,
			maxBytes: source.kind === "setup" ? SETUP_MAX_BYTES : LEGACY_WORKSPACE_ATTESTATION_MAX_BYTES
		})
	});
}
function createLegacySource(params) {
	const rootDir = path.resolve(params.rootDir);
	const sourcePath = path.resolve(params.sourcePath);
	const relativePath = path.relative(rootDir, sourcePath);
	if (!relativePath || relativePath === ".." || relativePath.startsWith(`..${path.sep}`) || path.isAbsolute(relativePath)) throw new Error("legacy workspace source is outside its migration root");
	return {
		...params,
		rootDir,
		relativePath,
		sourcePath
	};
}
function listOrphanAttestationSources(params) {
	const sources = [];
	const stateDirs = [.../* @__PURE__ */ new Set([params.stateDir, ...resolveLegacyStateDirs(params.homedir)])];
	for (const [priority, stateDir] of stateDirs.entries()) {
		const attestationDir = path.join(stateDir, LEGACY_WORKSPACE_ATTESTATION_DIRNAME);
		let entries;
		try {
			entries = fs.readdirSync(attestationDir, { withFileTypes: true });
		} catch (error) {
			if (error.code === "ENOENT") continue;
			sources.push({ ...createLegacySource({
				kind: "attestation",
				rootDir: stateDir,
				sourcePath: attestationDir,
				workspaceKey: "unreadable-attestation-directory",
				priority
			}) });
			continue;
		}
		for (const entry of entries) {
			const match = /^([a-f0-9]{64})\.attested(?:\.doctor-importing)?$/.exec(entry.name);
			if (!match?.[1]) continue;
			const sourceName = entry.name.endsWith(CLAIM_SUFFIX) ? entry.name.slice(0, -CLAIM_SUFFIX.length) : entry.name;
			sources.push(createLegacySource({
				kind: "attestation",
				rootDir: stateDir,
				sourcePath: path.join(attestationDir, sourceName),
				workspaceKey: match[1],
				priority
			}));
		}
	}
	return sources;
}
function addLegacyWorkspaceSources(params) {
	const identity = resolveWorkspaceStateIdentity(params.workspaceDir);
	const paths = resolveLegacyWorkspaceSourcePaths(params.workspaceDir, {
		env: params.env,
		homedir: params.homedir
	});
	for (const [priority, sourcePath] of paths.setupStatePaths.entries()) if (legacyMigrationSourceOrClaimMayExist(sourcePath)) params.add(createLegacySource({
		kind: "setup",
		rootDir: sourcePath.endsWith("openclaw-workspace-state.json") ? path.dirname(sourcePath) : path.dirname(path.dirname(sourcePath)),
		sourcePath,
		workspaceKey: identity.workspaceKey,
		workspaceDir: identity.workspacePath,
		workspaceAliasPath: paths.workspacePath,
		priority
	}));
	for (const [priority, sourcePath] of paths.stateDirAttestationPaths.entries()) if (legacyMigrationSourceOrClaimMayExist(sourcePath)) params.add(createLegacySource({
		kind: "attestation",
		rootDir: path.dirname(path.dirname(sourcePath)),
		sourcePath,
		workspaceKey: identity.workspaceKey,
		workspaceDir: identity.workspacePath,
		workspaceAliasPath: paths.workspacePath,
		priority
	}));
	for (const [index, sourcePath] of paths.siblingAttestationPaths.entries()) {
		if (!pathMayExistSync(`${sourcePath}${CLAIM_SUFFIX}`) && !legacyWorkspaceSiblingAttestationMayExist(sourcePath)) continue;
		params.add(createLegacySource({
			kind: "attestation",
			rootDir: path.dirname(sourcePath),
			sourcePath,
			workspaceKey: identity.workspaceKey,
			workspaceDir: identity.workspacePath,
			workspaceAliasPath: paths.workspacePath,
			priority: paths.stateDirAttestationPaths.length + index
		}));
	}
}
/** Detect retired workspace files only when an explicit Doctor flow opts in. */
function detectLegacyWorkspaceState(params) {
	if (params.doctorOnlyStateMigrations !== true) return {
		sources: [],
		hasLegacy: false
	};
	const env = {
		...params.env ?? process.env,
		OPENCLAW_STATE_DIR: params.stateDir
	};
	const homedir = params.homedir ?? os.homedir;
	const byPath = /* @__PURE__ */ new Map();
	const add = (source) => {
		const key = `${source.kind}:${path.resolve(source.sourcePath)}`;
		const existing = byPath.get(key);
		const sourceIsConfigured = source.workspaceDir !== void 0;
		const existingIsConfigured = existing?.workspaceDir !== void 0;
		if (!existing || sourceIsConfigured && !existingIsConfigured || sourceIsConfigured === existingIsConfigured && source.priority < existing.priority) byPath.set(key, source);
	};
	const workspaceDirs = new Set(listWorkspaceStateDirs({
		cfg: params.cfg,
		env,
		homedir,
		stateDir: params.stateDir
	}));
	const sharedWorkspace = params.cfg.agents?.defaults?.workspace?.trim();
	if (sharedWorkspace) workspaceDirs.add(resolveUserPath(sharedWorkspace, env, homedir));
	for (const workspaceDir of workspaceDirs) addLegacyWorkspaceSources({
		workspaceDir,
		env,
		homedir,
		add
	});
	for (const source of listOrphanAttestationSources({
		stateDir: params.stateDir,
		homedir
	})) add(source);
	const sources = [...byPath.values()].toSorted((left, right) => left.priority - right.priority || left.workspaceKey.localeCompare(right.workspaceKey) || left.sourcePath.localeCompare(right.sourcePath));
	return {
		sources,
		hasLegacy: sources.length > 0
	};
}
function formatLegacyWorkspaceReadWarning(source, error) {
	return `Failed reading legacy workspace state at ${source.sourcePath}: ${formatErrorMessage(error)}`;
}
function assertConfiguredWorkspaceIdentity(source) {
	if (!source.workspaceAliasPath) return;
	if (!source.workspaceDir) throw new Error("configured legacy workspace source has no canonical path");
	const current = resolveWorkspaceStateIdentity(source.workspaceAliasPath);
	if (current.workspaceKey !== source.workspaceKey || current.workspacePath !== source.workspaceDir) throw new Error("configured workspace identity changed during Doctor migration");
}
async function cleanupReceiptSource(params) {
	try {
		assertConfiguredWorkspaceIdentity(params.source);
		const sourceClaim = params.sourceClaim;
		const { hasSource, hasClaim } = params;
		if (!hasSource && !hasClaim) {
			if (!params.receipt.removedSource) markLegacyMigrationSourceRemoved(params.receipt.sourceKey, params.env);
			return {
				changes: [],
				warnings: []
			};
		}
		if (hasSource && hasClaim) return {
			changes: [],
			warnings: ["Workspace state is in SQLite, but source and interrupted claim both exist."]
		};
		let snapshot = await sourceClaim.read(hasClaim);
		let claimedByThisRun = false;
		if (hasSource) {
			try {
				snapshot = await sourceClaim.claim({
					snapshot,
					mismatchMessage: "legacy workspace source changed before Doctor could claim it"
				});
			} catch (error) {
				await sourceClaim.restore();
				throw error;
			}
			claimedByThisRun = true;
		}
		const parsed = parseSource(params.source, snapshot);
		if (!params.receipt.sha256 || snapshot.sha256 !== params.receipt.sha256 || !canonicalCoversParsedSource({
			source: params.source,
			parsed,
			env: params.env
		})) {
			if (claimedByThisRun) await sourceClaim.restore();
			return {
				changes: [],
				warnings: ["Workspace state is in SQLite, but the retired source now conflicts."]
			};
		}
		const notices = [];
		if (parsed.kind === "setup") {
			const archivePath = await archiveWorkspaceSetupSource(params.sourceRoot, params.source, snapshot, params.receipt.archivePath);
			if (!params.receipt.archivePath) {
				const imported = importAndRecordReceipt({
					source: params.source,
					snapshot,
					parsed,
					env: params.env,
					previousReceipt: params.receipt,
					archivePath
				});
				notices.push(`Archived legacy workspace setup state at ${archivePath}.`, ...imported.differences);
			}
		}
		const unchanged = await sourceClaim.read(true);
		if (!legacyMigrationSourceSnapshotsMatch(snapshot, unchanged)) {
			if (claimedByThisRun) await sourceClaim.restore();
			throw new Error("legacy workspace claim changed before cleanup");
		}
		assertConfiguredWorkspaceIdentity(params.source);
		await sourceClaim.remove({ skipSourceCheck: true });
		markLegacyMigrationSourceRemoved(params.receipt.sourceKey, params.env);
		return {
			changes: [],
			warnings: [],
			notices: [...notices, "Discarded retired workspace state already covered by its SQLite receipt."]
		};
	} catch (error) {
		return {
			changes: [],
			warnings: [`Workspace state is in SQLite, but legacy cleanup failed at ${params.source.sourcePath}: ${formatErrorMessage(error)}`]
		};
	}
}
async function migrateOneSource(params) {
	let sourceClaim;
	let sourceRoot;
	try {
		assertConfiguredWorkspaceIdentity(params.source);
		sourceRoot = await root(params.source.rootDir, {
			hardlinks: "reject",
			symlinks: "reject"
		});
		sourceClaim = createLegacySourceClaim(sourceRoot, params.source);
	} catch (error) {
		return {
			changes: [],
			warnings: [formatLegacyWorkspaceReadWarning(params.source, error)]
		};
	}
	const receipt = readReceipt(params.source, params.env);
	let hasSource;
	let hasClaim;
	try {
		hasSource = await sourceClaim.exists();
		hasClaim = await sourceClaim.exists(true);
	} catch (error) {
		return {
			changes: [],
			warnings: [formatLegacyWorkspaceReadWarning(params.source, error)]
		};
	}
	if (receipt && !(receipt.removedSource && hasSource !== hasClaim)) return cleanupReceiptSource({
		sourceRoot,
		sourceClaim,
		source: params.source,
		receipt,
		env: params.env,
		hasSource,
		hasClaim
	});
	if (hasSource && hasClaim) return {
		changes: [],
		warnings: ["Failed migrating legacy workspace state: source and interrupted claim both exist."]
	};
	if (!hasSource && !hasClaim) return {
		changes: [],
		warnings: []
	};
	let operation = `reading legacy workspace state at ${params.source.sourcePath}`;
	let claimAttempted = false;
	let imported;
	let archivePath;
	try {
		let snapshot = await sourceClaim.read(!hasSource);
		const discardEmptyAttestation = params.source.kind === "attestation" && path.basename(path.dirname(params.source.sourcePath)) === "workspace-attestations" && snapshot.size === 0;
		const parsed = discardEmptyAttestation ? void 0 : parseSource(params.source, snapshot);
		operation = discardEmptyAttestation ? `discarding empty reserved workspace attestation at ${params.source.sourcePath}` : "migrating legacy workspace state";
		if (hasSource) {
			claimAttempted = true;
			snapshot = await sourceClaim.claim({
				snapshot,
				beforeClaim: () => {
					params.beforeClaim?.(params.source);
					assertConfiguredWorkspaceIdentity(params.source);
				},
				mismatchMessage: "legacy workspace source changed before Doctor could claim it"
			});
		}
		if (parsed) {
			if (parsed.kind === "setup") archivePath = await archiveWorkspaceSetupSource(sourceRoot, params.source, snapshot);
			assertConfiguredWorkspaceIdentity(params.source);
			imported = importAndRecordReceipt({
				source: params.source,
				snapshot,
				parsed,
				env: params.env,
				previousReceipt: receipt ?? void 0,
				archivePath
			});
		}
		if (await sourceClaim.exists()) throw new Error("legacy workspace source reappeared during import");
		const unchanged = await sourceClaim.read(true);
		if (!legacyMigrationSourceSnapshotsMatch(snapshot, unchanged)) throw new Error("legacy workspace claim changed after import");
		assertConfiguredWorkspaceIdentity(params.source);
		await sourceClaim.remove({
			removeSource: params.removeSource,
			skipSourceCheck: true
		});
		if (imported) markLegacyMigrationSourceRemoved(imported.sourceKey, params.env);
	} catch (error) {
		if (imported) return {
			changes: [],
			warnings: [`Workspace state is in SQLite, but legacy cleanup failed at ${params.source.sourcePath}: ${formatErrorMessage(error)}`]
		};
		const restoreError = claimAttempted ? await sourceClaim.restore() : null;
		return {
			changes: [],
			warnings: [`Failed ${operation}: ${formatErrorMessage(error)}${restoreError ? `; restore failure: ${restoreError}` : ""}`]
		};
	}
	if (!imported) return {
		changes: [`Discarded empty reserved workspace attestation at ${params.source.sourcePath}.`],
		warnings: []
	};
	const label = params.source.kind === "setup" ? "workspace setup state" : "workspace attestation";
	return {
		changes: [imported.imported ? `Migrated ${label} to SQLite.` : `Verified canonical SQLite ${label}.`],
		warnings: [],
		notices: [
			...archivePath ? [`Archived legacy workspace setup state at ${archivePath}.`] : [],
			...imported.differences,
			"Removed retired workspace state after verified SQLite import."
		]
	};
}
/** Import retired workspace files while excluding Gateways that can recreate them. */
async function migrateLegacyWorkspaceState(params) {
	const detected = params.detected;
	if (!detected?.hasLegacy) return {
		changes: [],
		warnings: []
	};
	return await withLegacyMigrationStateLock({
		stateDir: params.stateDir,
		env: params.env,
		label: "legacy workspace state",
		releaseLabel: "Workspace",
		formatAcquireError: formatErrorMessage,
		run: async (env) => {
			const changes = [];
			const warnings = [];
			const notices = [];
			for (const source of detected.sources) {
				const result = await migrateOneSource({
					source,
					env,
					...params.beforeClaim ? { beforeClaim: params.beforeClaim } : {},
					...params.removeSource ? { removeSource: params.removeSource } : {}
				});
				changes.push(...result.changes);
				warnings.push(...result.warnings);
				notices.push(...result.notices ?? []);
			}
			return notices.length > 0 ? {
				changes,
				warnings,
				notices
			} : {
				changes,
				warnings
			};
		}
	});
}
//#endregion
//#region src/infra/state-migrations.doctor.ts
function describeStateSchemaMigration(migration) {
	switch (migration.kind) {
		case "agent-databases-composite-primary-key": return "agent database registry primary key → agent_id,path";
		case "audit-events-v2": return "audit event ledger → versioned message lifecycle schema";
		case "commitments-retirement-v7": return "retired commitments storage → discarded rows, table, and indexes";
		case "worker-placement-execution-mode-v8": return "cloud worker placements → execution-mode claims";
		case "agent-databases-relative-paths-v9": return "agent database registry paths → state-relative storage";
		case "state-table-retirement-v10": return "retired shared-state tables → removed tables and indexes";
		case "state-table-retirement-v11": return "retired skill curator tables → removed tables and indexes";
		case "singleton-state-foldin-v12": return "singleton state tables → shared configuration state";
		case "state-consolidation-v13": return "cron jobs and subagent runs → canonical JSON storage";
		case "creator-namespace-v14": return "historical cron creators → unknown source attribution";
		case "conversation-binding-targets-v15": return "conversation bindings → exact target keys without agent/session projections";
		case "operator-approvals-system-agent": return "operator approvals → OpenClaw system changes";
		case "session-watch-cursor-provenance-v4": return "session watch cursors → provenance column";
		case "strict-tables-v3": return "tables → SQLite STRICT typing";
	}
	return migration.kind;
}
const autoMigrateChecked = /* @__PURE__ */ new Set();
const DEFERRED_LEGACY_OWNER_MESSAGE = "Deferred legacy agent/session migration: select an agent owner";
function tryResolveDoctorStateMigrationAgentId(cfg) {
	const agentId = tryResolveAmbientOwnerAgentId(cfg);
	return agentId && listAgentIds(cfg).includes(agentId) ? agentId : void 0;
}
function tryResolveDoctorSessionMigrationAgentId(cfg) {
	return tryResolveDoctorStateMigrationAgentId(cfg) ?? (!isPerAgentSessionStoreConfig(cfg.session?.store) ? resolveSessionStoreCompatibilityAgentId(cfg) : void 0);
}
function resolveConcreteBindingAccountId(value) {
	if (typeof value !== "string") return;
	const accountId = value.trim();
	return accountId && accountId !== "*" ? accountId : void 0;
}
async function detectManagedWorktreeStateMigration(params) {
	const rawRoot = path.join(params.stateDir, "worktrees");
	const stateEnv = {
		...params.env,
		OPENCLAW_STATE_DIR: params.stateDir
	};
	const databaseExists = migrationFileExists(resolveOpenClawStateSqlitePath(stateEnv));
	const hasCurrentSchema = params.stateSchemaMigrations.length === 0;
	const hasLegacy = params.doctorOnlyStateMigrations === true && hasCurrentSchema && databaseExists && hasLegacyRegistryWorktrees(stateEnv);
	let canonicalRoot;
	try {
		canonicalRoot = await fs$1.realpath(rawRoot);
	} catch (error) {
		if (error.code !== "ENOENT") throw error;
		try {
			canonicalRoot = path.join(await fs$1.realpath(params.stateDir), "worktrees");
		} catch (stateDirError) {
			if (stateDirError.code === "ENOENT") return {
				hasLegacy,
				pathRewrites: []
			};
			throw stateDirError;
		}
	}
	if (rawRoot === canonicalRoot || !hasCurrentSchema || !databaseExists) return {
		hasLegacy,
		pathRewrites: []
	};
	return {
		hasLegacy,
		pathRewrites: listRegistryWorktreesForMigration(stateEnv).flatMap((row) => {
			const fromPath = path.join(rawRoot, row.repoFingerprint, row.name);
			return row.path === fromPath ? [{
				id: row.id,
				fromPath,
				toPath: path.join(canonicalRoot, row.repoFingerprint, row.name)
			}] : [];
		})
	};
}
async function detectLegacyStateMigrations(params) {
	const env = params.env ?? process.env;
	const homedir = params.homedir ?? os.homedir;
	const stateDir = resolveStateDir(env, homedir);
	const oauthDir = resolveOAuthDir(env, stateDir);
	const detectSessionFiles = params.mode !== "automatic";
	const migrationAgentId = tryResolveDoctorStateMigrationAgentId(params.cfg);
	const sessionMigrationAgentId = tryResolveDoctorSessionMigrationAgentId(params.cfg);
	const targetAgentId = migrationAgentId ?? sessionMigrationAgentId ?? "main";
	const rawMainKey = params.cfg.session?.mainKey;
	const targetMainKey = typeof rawMainKey === "string" && rawMainKey.trim().length > 0 ? rawMainKey.trim() : DEFAULT_MAIN_KEY;
	const targetScope = params.cfg.session?.scope;
	const sessionsLegacyDir = path.join(stateDir, "sessions");
	const sessionsLegacyStorePath = path.join(sessionsLegacyDir, "sessions.json");
	const sessionsTargetDir = path.join(stateDir, "agents", targetAgentId, "sessions");
	const sessionsTargetStorePath = path.join(sessionsTargetDir, "sessions.json");
	const pluginConfig = params.pluginDoctorConfig ?? params.cfg;
	const pluginSessionStoreAgentIds = params.pluginSessionStoreAgentIds ?? listPluginDoctorSessionStoreAgentIds({
		config: pluginConfig,
		env,
		pluginIds: collectRelevantDoctorPluginIds(pluginConfig)
	});
	const currentSessionStoreOwnership = detectSessionFiles && sessionMigrationAgentId ? resolveSessionStoreOwnership({
		cfg: params.cfg,
		env,
		stateDir,
		targetAgentId: sessionMigrationAgentId,
		pluginSessionStoreAgentIds
	}) : {
		preserveAmbiguousKeys: true,
		preserveForeignMainAliases: true,
		targetStoreAliases: {
			hasDistinctAliases: false,
			hasFinalSymlink: false,
			hasUnresolvedIdentity: false
		}
	};
	const sessionStoreOwnership = {
		preserveAmbiguousKeys: params.sessionStoreOwnership?.preserveAmbiguousKeys === true || currentSessionStoreOwnership.preserveAmbiguousKeys,
		preserveForeignMainAliases: params.sessionStoreOwnership?.preserveForeignMainAliases === true || currentSessionStoreOwnership.preserveForeignMainAliases,
		targetStoreAliases: mergeSessionStoreAliasPlans(params.sessionStoreOwnership?.targetStoreAliases, currentSessionStoreOwnership.targetStoreAliases)
	};
	const { preserveForeignMainAliases } = sessionStoreOwnership;
	const hasLegacySessions = detectSessionFiles && (migrationFileExists(sessionsLegacyStorePath) || safeReadDir(sessionsLegacyDir).some((e) => e.isFile() && e.name.endsWith(".jsonl")));
	const targetSessionParsed = detectSessionFiles && migrationFileExists(sessionsTargetStorePath) ? readSessionStoreJson5(sessionsTargetStorePath) : {
		store: {},
		ok: true
	};
	const legacySessionSurfaces = detectSessionFiles ? params.legacySessionSurfaces : EMPTY_LEGACY_SESSION_SURFACES;
	const legacyKeys = targetSessionParsed.ok && legacySessionSurfaces.failures.length === 0 ? listLegacySessionKeys({
		store: targetSessionParsed.store,
		agentId: targetAgentId,
		mainKey: targetMainKey,
		scope: targetScope,
		preserveAmbiguousKeys: sessionStoreOwnership.preserveAmbiguousKeys,
		preserveForeignMainAliases,
		legacySessionSurfaces: legacySessionSurfaces.surfaces
	}) : [];
	const hasStaleSessionFiles = targetSessionParsed.ok && Object.values(targetSessionParsed.store).some((entry) => Boolean(resolveStaleLegacySessionFile({
		entry,
		legacyDir: sessionsLegacyDir,
		targetDir: sessionsTargetDir
	})));
	const legacyAgentDir = path.join(stateDir, "agent");
	const targetAgentDir = path.join(stateDir, "agents", targetAgentId, "agent");
	const legacyAgentDirInspection = inspectLegacyAgentDir(legacyAgentDir);
	const hasLegacyAgentDir = legacyAgentDirInspection.status === "payload";
	const pluginStateSidecarPath = resolveLegacyPluginStateSidecarPath(stateDir);
	const hasPluginStateSidecar = migrationFileExists(pluginStateSidecarPath);
	const hasPendingPluginStateSidecarArchive = hasPendingSqliteSidecarArchive(pluginStateSidecarPath, PLUGIN_STATE_SQLITE_SIDECAR_SUFFIXES);
	const pluginInstallIndexPath = resolveLegacyInstalledPluginIndexStorePath({ stateDir });
	const hasPluginInstallIndex = migrationFileExists(pluginInstallIndexPath);
	const debugProxyCaptureSidecar = detectLegacyDebugProxyCaptureSidecar(stateDir, env);
	const stateSchemaMigrations = detectOpenClawStateDatabaseSchemaMigrations({ env: {
		...env,
		OPENCLAW_STATE_DIR: stateDir
	} });
	const worktrees = await detectManagedWorktreeStateMigration({
		env,
		stateDir,
		stateSchemaMigrations,
		doctorOnlyStateMigrations: params.doctorOnlyStateMigrations
	});
	const taskRunsSidecarPath = resolveLegacyTaskRunsSidecarPath(stateDir);
	const flowRunsSidecarPath = resolveLegacyFlowRunsSidecarPath(stateDir);
	const hasPendingTaskRunsSidecarArchive = hasPendingSqliteSidecarArchive(taskRunsSidecarPath, TASK_STATE_SQLITE_SIDECAR_SUFFIXES);
	const hasPendingFlowRunsSidecarArchive = hasPendingSqliteSidecarArchive(flowRunsSidecarPath, TASK_STATE_SQLITE_SIDECAR_SUFFIXES);
	const hasTaskStateSidecars = migrationFileExists(taskRunsSidecarPath) || migrationFileExists(flowRunsSidecarPath) || hasPendingTaskRunsSidecarArchive || hasPendingFlowRunsSidecarArchive;
	const deliveryQueuePaths = {
		outboundPath: resolveLegacyDeliveryQueuePath(stateDir, "delivery-queue"),
		sessionPath: resolveLegacyDeliveryQueuePath(stateDir, "session-delivery-queue")
	};
	const hasDeliveryQueues = listLegacyDeliveryQueueFiles(deliveryQueuePaths.outboundPath).length > 0 || listLegacyDeliveryQueueDeliveredMarkers(deliveryQueuePaths.outboundPath).length > 0 || listLegacyDeliveryQueueFiles(deliveryQueuePaths.sessionPath).length > 0 || listLegacyDeliveryQueueDeliveredMarkers(deliveryQueuePaths.sessionPath).length > 0;
	const voiceWake = {
		triggersPath: resolveLegacyVoiceWakeTriggersPath(stateDir),
		routingPath: resolveLegacyVoiceWakeRoutingPath(stateDir)
	};
	const hasVoiceWake = migrationFileExists(voiceWake.triggersPath) || migrationFileExists(voiceWake.routingPath);
	const updateCheck = { sourcePath: resolveLegacyUpdateCheckPath(stateDir) };
	const hasUpdateCheck = migrationFileExists(updateCheck.sourcePath);
	const configHealth = { sourcePath: resolveLegacyConfigHealthPath(stateDir) };
	const hasConfigHealth = migrationFileExists(configHealth.sourcePath);
	const pluginBindingApprovals = { sourcePath: resolveLegacyPluginBindingApprovalsPath(env, homedir) };
	const hasPluginBindingApprovals = path.resolve(path.dirname(pluginBindingApprovals.sourcePath)) === path.resolve(stateDir) && migrationFileExists(pluginBindingApprovals.sourcePath);
	const currentConversationBindings = { sourcePath: resolveLegacyCurrentConversationBindingsPath(stateDir) };
	const hasCurrentConversationBindings = migrationFileExists(currentConversationBindings.sourcePath);
	const detectDoctorOwnedState = (detect) => detect({
		stateDir,
		doctorOnlyStateMigrations: params.doctorOnlyStateMigrations
	});
	const tuiLastSessions = detectDoctorOwnedState(detectLegacyTuiLastSessions);
	const commitments = await detectLegacyCommitments({
		stateDir,
		env,
		doctorOnlyStateMigrations: params.doctorOnlyStateMigrations
	});
	const auditLogs = detectDoctorOwnedState(detectLegacyAuditLogs);
	const acpReplayLedger = detectDoctorOwnedState(detectLegacyAcpReplayLedger);
	const managedOutgoingImages = detectDoctorOwnedState(detectLegacyManagedOutgoingImages);
	const apns = detectDoctorOwnedState(detectLegacyApnsRegistrations);
	const deviceAuth = detectDoctorOwnedState(detectLegacyDeviceAuth);
	const sharedAuthStore = detectSharedAuthStoreMigration({
		stateDir,
		env,
		doctorOnlyStateMigrations: stateSchemaMigrations.length === 0 && params.doctorOnlyStateMigrations === true
	});
	const deviceIdentity = detectLegacyDeviceIdentity({
		stateDir,
		env,
		doctorOnlyStateMigrations: params.doctorOnlyStateMigrations,
		allowLegacyDeviceIdentityImport: params.allowLegacyDeviceIdentityImport
	});
	const execApprovals = detectDoctorOwnedState(detectLegacyExecApprovals);
	const mcpOauth = detectDoctorOwnedState(detectLegacyMcpOAuthStores);
	const meetingTranscripts = detectLegacyMeetingTranscripts({
		stateDir,
		env,
		doctorOnlyStateMigrations: params.doctorOnlyStateMigrations
	});
	const restartSentinel = detectLegacyRestartSentinel({ stateDir });
	const workspace = detectLegacyWorkspaceState({
		cfg: params.cfg,
		stateDir,
		env,
		homedir,
		doctorOnlyStateMigrations: params.doctorOnlyStateMigrations
	});
	const webPush = detectDoctorOwnedState(detectLegacyWebPush);
	const nodeHost = detectDoctorOwnedState(detectLegacyNodeHostConfig);
	const subagentRegistry = detectDoctorOwnedState(detectLegacySubagentRegistry);
	const rescuePending = detectDoctorOwnedState(detectLegacyRescuePending);
	const channelPairing = detectLegacyChannelPairingState({
		sourceDir: oauthDir,
		configuredChannelIds: Object.keys(params.cfg.channels ?? {}),
		resolveAccounts: () => {
			const configuredChannels = Object.entries(params.cfg.channels ?? {});
			let migrationOwnerConfig = params.cfg;
			if (migrationAgentId && listAgentIds(params.cfg).length > 1 && params.cfg.agents) {
				const agents = structuredClone(params.cfg.agents);
				delete agents.ownership;
				for (const [agentId, entry] of Object.entries(agents.entries ?? {})) entry.default = normalizeAgentId(agentId) === targetAgentId;
				for (const entry of agents.list ?? []) entry.default = normalizeAgentId(entry.id) === targetAgentId;
				migrationOwnerConfig = {
					...params.cfg,
					agents
				};
			}
			const configuredAccountIds = Object.fromEntries(configuredChannels.map(([channelId, value]) => {
				const channelConfig = value && typeof value === "object" && !Array.isArray(value) ? value : void 0;
				const accountIds = [
					...getChannelPlugin(channelId)?.config.listAccountIds(params.cfg) ?? [],
					...channelConfig?.accounts && typeof channelConfig.accounts === "object" && !Array.isArray(channelConfig.accounts) ? Object.keys(channelConfig.accounts) : [],
					...typeof channelConfig?.defaultAccount === "string" ? [channelConfig.defaultAccount] : [],
					...(params.cfg.bindings ?? []).flatMap((binding) => {
						const accountId = binding.match?.channel === channelId ? resolveConcreteBindingAccountId(binding.match.accountId) : void 0;
						return accountId ? [accountId] : [];
					})
				];
				return [channelId, Array.from(new Set(accountIds.map((entry) => entry.trim()).filter(Boolean)))];
			}));
			return {
				defaultAccountIds: Object.fromEntries(configuredChannels.flatMap(([channelId, value]) => {
					const boundAccountId = params.cfg.bindings?.find((binding) => normalizeAgentId(binding.agentId) === targetAgentId && binding.match?.channel === channelId && resolveConcreteBindingAccountId(binding.match.accountId) !== void 0)?.match.accountId;
					const concreteBoundAccountId = resolveConcreteBindingAccountId(boundAccountId);
					if (concreteBoundAccountId) return [[channelId, concreteBoundAccountId]];
					const defaultAccount = value && typeof value === "object" && !Array.isArray(value) ? value.defaultAccount : void 0;
					if (typeof defaultAccount === "string" && defaultAccount.trim()) return [[channelId, defaultAccount.trim()]];
					const plugin = getChannelPlugin(channelId);
					if (plugin) return [[channelId, resolveChannelDefaultAccountId({
						plugin,
						cfg: migrationOwnerConfig
					})]];
					return [[channelId, configuredAccountIds[channelId]?.toSorted()[0] ?? "default"]];
				})),
				accountIds: configuredAccountIds
			};
		}
	});
	const pluginPlanWarnings = [];
	const pluginPlans = stateSchemaMigrations.length > 0 ? [] : await collectPluginDoctorStateMigrationPlans({
		config: pluginConfig,
		env,
		stateDir,
		oauthDir
	}, {
		includeDoctorOnly: params.doctorOnlyStateMigrations === true,
		warnings: pluginPlanWarnings
	});
	const sessionsHaveLegacy = Boolean(sessionMigrationAgentId) && (hasLegacySessions || legacyKeys.length > 0 || hasStaleSessionFiles);
	const agentDirHasLegacy = Boolean(migrationAgentId) && hasLegacyAgentDir;
	const deferredSessions = !sessionMigrationAgentId && (hasLegacySessions || legacyKeys.length > 0 || hasStaleSessionFiles);
	const deferredAgentDir = !migrationAgentId && hasLegacyAgentDir;
	const deferredWarnings = deferredSessions || deferredAgentDir && params.doctorOnlyStateMigrations === true ? [DEFERRED_LEGACY_OWNER_MESSAGE] : [];
	const deferredNotices = deferredAgentDir && params.doctorOnlyStateMigrations !== true ? [DEFERRED_LEGACY_OWNER_MESSAGE] : [];
	const preview = [];
	if (sessionsHaveLegacy && hasLegacySessions) preview.push(`- Sessions: ${sessionsLegacyDir} → ${sessionsTargetDir}`);
	if (sessionsHaveLegacy && legacyKeys.length > 0) preview.push(`- Sessions: canonicalize legacy keys in ${sessionsTargetStorePath}`);
	if (sessionsHaveLegacy && hasStaleSessionFiles) preview.push(`- Sessions: repair migrated transcript paths in ${sessionsTargetStorePath}`);
	if (agentDirHasLegacy) preview.push(`- Agent dir: ${legacyAgentDir} → ${targetAgentDir}`);
	if (hasPluginStateSidecar) preview.push(`- Plugin state sidecar: ${pluginStateSidecarPath} → shared SQLite state`);
	else if (hasPendingPluginStateSidecarArchive) preview.push(`- Plugin state sidecar: finish archive cleanup for ${pluginStateSidecarPath}`);
	if (hasPluginInstallIndex) preview.push(`- Plugin install index: ${pluginInstallIndexPath} → shared SQLite state`);
	if (debugProxyCaptureSidecar.hasLegacy) preview.push(`- Debug proxy capture sidecar: ${debugProxyCaptureSidecar.sourcePath} → shared SQLite state`);
	if (stateSchemaMigrations.length > 0) {
		for (const migration of stateSchemaMigrations) preview.push(`- Shared SQLite schema: ${describeStateSchemaMigration(migration)}`);
		preview.push("- Rerun doctor after shared SQLite schema repair to detect plugin state migrations");
	}
	if (worktrees.hasLegacy) preview.push("- Managed worktrees: discard rows without provisioned-file ledgers");
	if (worktrees.pathRewrites.length > 0) preview.push(`- Managed worktrees: canonicalize ${worktrees.pathRewrites.length} persisted ${worktrees.pathRewrites.length === 1 ? "path" : "paths"} for symlinked state directories`);
	if (migrationFileExists(taskRunsSidecarPath)) preview.push(`- Task registry sidecar: ${taskRunsSidecarPath} → shared SQLite state`);
	else if (hasPendingTaskRunsSidecarArchive) preview.push(`- Task registry sidecar: finish archive cleanup for ${taskRunsSidecarPath}`);
	if (migrationFileExists(flowRunsSidecarPath)) preview.push(`- Task flow sidecar: ${flowRunsSidecarPath} → shared SQLite state`);
	else if (hasPendingFlowRunsSidecarArchive) preview.push(`- Task flow sidecar: finish archive cleanup for ${flowRunsSidecarPath}`);
	const stateMigrationPreviews = [
		[sharedAuthStore.hasLegacy, "- Shared auth store: legacy main-agent rows → shared SQLite state"],
		[hasDeliveryQueues, "- Delivery queues: legacy JSON queue files → shared SQLite state"],
		[hasVoiceWake, "- Voice Wake settings: legacy JSON files → shared SQLite state"],
		[hasUpdateCheck, "- Update-check state: legacy JSON file → shared SQLite state"],
		[hasConfigHealth, "- Config health state: legacy JSON file → shared SQLite state"],
		[hasPluginBindingApprovals, "- Plugin binding approvals: legacy JSON file → shared SQLite state"],
		[hasCurrentConversationBindings, "- Current-conversation bindings: legacy JSON file → shared SQLite state"],
		[tuiLastSessions.hasLegacy, "- TUI last-session pointers: legacy JSON file → shared SQLite state"],
		[commitments.hasLegacy, "- Commitments: discard retired commitments/commitments.json rows without import, archive, or export"],
		...auditLogs.sources.map((source) => [true, `- ${source.label}: legacy JSONL file → shared SQLite state`]),
		[acpReplayLedger.hasLegacy, "- ACP replay ledger: legacy JSON file → shared SQLite state"],
		[managedOutgoingImages.hasLegacy, "- Managed outgoing images: legacy record JSON → shared SQLite state"],
		[apns.hasLegacy, "- APNs registrations: legacy JSON → shared SQLite state"],
		[deviceAuth.hasLegacy, "- Device auth tokens: legacy JSON → shared SQLite state"],
		[deviceIdentity.hasLegacy, "- Primary device identity: legacy JSON → shared SQLite state"],
		[deviceIdentity.hasInvalidCanonical && !deviceIdentity.hasLegacy, "- Primary device identity: invalid SQLite row → new device identity"],
		[execApprovals.hasLegacy, "- Exec approvals: legacy JSON → shared SQLite state"],
		[mcpOauth.hasLegacy, "- MCP OAuth credentials: legacy JSON → shared SQLite state"],
		[meetingTranscripts.hasLegacy, "- Meeting transcripts: legacy JSON/JSONL files → shared SQLite state"],
		[restartSentinel.hasLegacy, "- Restart sentinel: legacy JSON → shared SQLite state"],
		[workspace.hasLegacy, "- Workspace setup and attestations: legacy files → shared SQLite state"],
		[webPush.hasLegacy, "- Web Push subscriptions and VAPID identity: legacy JSON → shared SQLite state"],
		[nodeHost.hasLegacy, "- Node-host config: legacy node.json → shared SQLite state"],
		[subagentRegistry.hasLegacy, "- Subagent runs: discard retired transient subagents/runs.json state"],
		[rescuePending.hasLegacy, "- System-agent rescue approvals: discard retired pending JSON capabilities"],
		[channelPairing.hasLegacy, "- Channel pairing state: legacy JSON files → shared SQLite state"]
	];
	for (const [hasLegacy, message] of stateMigrationPreviews) if (hasLegacy) preview.push(message);
	if (pluginPlans.length > 0) preview.push(...pluginPlans.flatMap((plan) => plan.preview));
	return {
		doctorOnlyStateMigrations: params.doctorOnlyStateMigrations === true,
		targetAgentId,
		targetMainKey,
		targetScope,
		stateDir,
		oauthDir,
		sessions: {
			legacyDir: sessionsLegacyDir,
			legacyStorePath: sessionsLegacyStorePath,
			targetDir: sessionsTargetDir,
			targetStorePath: sessionsTargetStorePath,
			hasLegacy: sessionsHaveLegacy,
			legacyKeys: sessionMigrationAgentId ? legacyKeys : [],
			preserveAmbiguousKeys: sessionStoreOwnership.preserveAmbiguousKeys,
			preserveForeignMainAliases,
			targetStoreAliases: sessionStoreOwnership.targetStoreAliases
		},
		agentDir: {
			legacyDir: legacyAgentDir,
			targetDir: targetAgentDir,
			hasLegacy: agentDirHasLegacy
		},
		pluginPlans: {
			hasLegacy: pluginPlans.length > 0,
			plans: pluginPlans
		},
		pluginStateSidecar: {
			sourcePath: pluginStateSidecarPath,
			hasLegacy: hasPluginStateSidecar || hasPendingPluginStateSidecarArchive
		},
		pluginInstallIndex: {
			sourcePath: pluginInstallIndexPath,
			hasLegacy: hasPluginInstallIndex
		},
		debugProxyCaptureSidecar,
		stateSchema: {
			hasLegacy: stateSchemaMigrations.length > 0,
			preview: stateSchemaMigrations.map((migration) => migration.path)
		},
		sharedAuthStore,
		worktrees,
		taskStateSidecars: {
			taskRunsPath: taskRunsSidecarPath,
			flowRunsPath: flowRunsSidecarPath,
			hasLegacy: hasTaskStateSidecars
		},
		deliveryQueues: {
			...deliveryQueuePaths,
			hasLegacy: hasDeliveryQueues
		},
		voiceWake: {
			...voiceWake,
			hasLegacy: hasVoiceWake
		},
		updateCheck: {
			...updateCheck,
			hasLegacy: hasUpdateCheck
		},
		configHealth: {
			...configHealth,
			hasLegacy: hasConfigHealth
		},
		pluginBindingApprovals: {
			...pluginBindingApprovals,
			hasLegacy: hasPluginBindingApprovals
		},
		currentConversationBindings: {
			...currentConversationBindings,
			hasLegacy: hasCurrentConversationBindings
		},
		tuiLastSessions,
		commitments,
		auditLogs,
		acpReplayLedger,
		managedOutgoingImages,
		apns,
		deviceAuth,
		deviceIdentity,
		execApprovals,
		mcpOauth,
		meetingTranscripts,
		restartSentinel,
		workspace,
		webPush,
		nodeHost,
		subagentRegistry,
		rescuePending,
		channelPairing,
		warnings: [
			...pluginPlanWarnings,
			...legacySessionSurfaces.failures,
			...legacyAgentDirInspection.status === "failed" ? [legacyAgentDirInspection.warning] : [],
			...deferredWarnings
		],
		notices: deferredNotices,
		preview
	};
}
function migrateLegacyStateSchema(detected, env) {
	return repairOpenClawStateDatabaseSchema({ env: {
		...env,
		OPENCLAW_STATE_DIR: detected.stateDir
	} });
}
function buildLegacyStateMigrationSteps(params) {
	const { detected, env } = params;
	const stateDir = detected.stateDir;
	const now = params.now ?? (() => Date.now());
	const isDoctor = params.mode === "doctor";
	const repairSessionFiles = (isDoctor || params.doctorOnlyStateMigrations === true) && !params.skipAgentScopedMigrations;
	const sharedStep = (run, collectNotices = false) => ({
		phase: "shared",
		run,
		collectNotices
	});
	const finalStep = (run, collectNotices = false) => ({
		phase: "final",
		run,
		collectNotices
	});
	const ownerStep = (detection, migrate, phase = "final", collectNotices = true) => ({
		phase,
		collectNotices,
		run: () => migrate({
			detected: detection,
			env,
			stateDir
		})
	});
	const managedWorktreePrelude = [finalStep(() => {
		const stateEnv = {
			...env,
			OPENCLAW_STATE_DIR: stateDir
		};
		const discardedWorktrees = isDoctor && detected.worktrees.hasLegacy ? discardLegacyRegistryWorktrees(stateEnv) : 0;
		const canonicalizedWorktrees = rewriteRegistryWorktreePathsForMigration(stateEnv, detected.worktrees.pathRewrites);
		return {
			changes: [...discardedWorktrees > 0 ? [`Discarded ${discardedWorktrees} legacy managed worktree ${discardedWorktrees === 1 ? "row" : "rows"}; affected worktrees will provision fresh on next use`] : [], ...canonicalizedWorktrees > 0 ? [`Canonicalized ${canonicalizedWorktrees} managed worktree ${canonicalizedWorktrees === 1 ? "path" : "paths"} for symlinked state directories`] : []],
			warnings: []
		};
	})];
	const sharedSteps = [
		ownerStep(detected.sharedAuthStore, migrateSharedAuthStore, "shared"),
		sharedStep(() => migrateLegacyPluginStateSidecar({ stateDir })),
		sharedStep(() => migrateLegacyInstalledPluginIndex({ stateDir }), true),
		ownerStep(detected.debugProxyCaptureSidecar, migrateLegacyDebugProxyCaptureSidecar, "shared", false),
		sharedStep(() => migrateLegacyTaskStateSidecars({ stateDir })),
		sharedStep(() => migrateLegacyDeliveryQueues({ stateDir })),
		ownerStep(detected.voiceWake, migrateLegacyVoiceWakeSettings, "shared"),
		ownerStep(detected.updateCheck, migrateLegacyUpdateCheckState, "shared"),
		ownerStep(detected.configHealth, migrateLegacyConfigHealth, "shared", false),
		ownerStep(detected.pluginBindingApprovals, migrateLegacyPluginBindingApprovals, "shared"),
		ownerStep(detected.currentConversationBindings, migrateLegacyCurrentConversationBindings, "shared")
	];
	const doctorStateSteps = isDoctor ? [
		ownerStep(detected.tuiLastSessions, migrateLegacyTuiLastSessions),
		...detected.commitments ? [ownerStep(detected.commitments, migrateLegacyCommitments)] : [],
		ownerStep(detected.auditLogs, migrateLegacyAuditLogs),
		ownerStep(detected.acpReplayLedger, migrateLegacyAcpReplayLedger),
		ownerStep(detected.managedOutgoingImages, migrateLegacyManagedOutgoingImages),
		ownerStep(detected.apns, migrateLegacyApnsRegistrations),
		ownerStep(detected.deviceAuth, migrateLegacyDeviceAuth),
		finalStep(() => migrateLegacyDeviceIdentity({
			detected: detected.deviceIdentity,
			env,
			stateDir,
			doctorOnlyStateMigrations: params.doctorOnlyStateMigrations
		}), true),
		ownerStep(detected.execApprovals, migrateLegacyExecApprovals),
		ownerStep(detected.mcpOauth, migrateLegacyMcpOAuthStores),
		finalStep(() => migrateLegacyMeetingTranscripts({
			detected: detected.meetingTranscripts,
			env,
			stateDir,
			now
		}), true)
	] : [];
	const doctorFinalSteps = isDoctor ? [
		ownerStep(detected.workspace, migrateLegacyWorkspaceState),
		ownerStep(detected.webPush, migrateLegacyWebPush),
		ownerStep(detected.nodeHost, migrateLegacyNodeHostConfig),
		ownerStep(detected.subagentRegistry, migrateLegacySubagentRegistry),
		ownerStep(detected.rescuePending, discardLegacyRescuePending, "final", false)
	] : [];
	const finalSteps = [
		ownerStep(detected.restartSentinel, migrateLegacyRestartSentinel),
		...doctorFinalSteps,
		finalStep(() => migrateLegacyChannelPairingState({
			detected: detected.channelPairing,
			env: {
				...env,
				OPENCLAW_STATE_DIR: stateDir
			}
		})),
		finalStep(() => isDoctor && detected.stateSchema.hasLegacy ? {
			changes: [],
			warnings: []
		} : runPluginDoctorStateMigrationPlans({
			detected,
			config: params.config,
			env
		}), true)
	];
	if (repairSessionFiles) finalSteps.push(finalStep(() => migrateLegacySessions(detected, now, {
		recoverCorruptTargetStore: params.recoverCorruptTargetStore,
		legacySessionSurfaces: params.legacySessionSurfaces
	})));
	if (!isDoctor) finalSteps.push({
		...finalStep(async () => {
			const result = await migrateLegacyMainSessionKeys({
				cfg: params.sessionConfig ?? params.config,
				env,
				mode: "automatic",
				now
			});
			return {
				changes: result.changes,
				warnings: [],
				notices: result.warnings
			};
		}, true),
		kind: "legacy-main-session-keys"
	});
	if (repairSessionFiles) finalSteps.push({
		...finalStep(() => migrateLegacyAcpSessionMetadata({
			cfg: params.sessionConfig ?? params.config,
			env: isDoctor ? {
				...env,
				OPENCLAW_STATE_DIR: stateDir
			} : env,
			now,
			...isDoctor ? {} : { pluginSessionStoreAgentIds: params.pluginSessionStoreAgentIds },
			legacySessionSurfaces: params.legacySessionSurfaces
		})),
		kind: "acp-session-metadata"
	});
	if (!params.skipAgentScopedMigrations) finalSteps.push(finalStep(() => migrateLegacyAgentDir(detected, now)));
	return [
		...managedWorktreePrelude,
		...sharedSteps,
		...doctorStateSteps,
		...finalSteps
	];
}
async function runLegacyStateMigrationSteps(steps) {
	const sources = [];
	const sharedSources = [];
	const finalSources = [];
	const sharedNoticeSources = [];
	const finalNoticeSources = [];
	for (const step of steps) {
		const result = await step.run();
		sources.push(result);
		(step.phase === "shared" ? sharedSources : finalSources).push(result);
		if (step.collectNotices) (step.phase === "shared" ? sharedNoticeSources : finalNoticeSources).push(result);
	}
	return {
		sources,
		sharedSources,
		finalSources,
		sharedNoticeSources,
		finalNoticeSources
	};
}
async function runLegacyStateMigrations(params) {
	const detected = params.detected;
	const env = params.env ?? process.env;
	const config = params.config ?? {};
	const legacySessionSurfaces = params.legacySessionSurfaces;
	const stateSchema = migrateLegacyStateSchema(detected, env);
	if (detected.stateSchema.hasLegacy && stateSchema.warnings.length > 0) return stateSchema;
	const migrations = await runLegacyStateMigrationSteps(buildLegacyStateMigrationSteps({
		mode: "doctor",
		detected,
		config,
		env,
		now: params.now,
		recoverCorruptTargetStore: params.recoverCorruptTargetStore,
		doctorOnlyStateMigrations: params.doctorOnlyStateMigrations,
		legacySessionSurfaces
	}));
	const notices = mergeNotices([...migrations.sharedNoticeSources, ...migrations.finalNoticeSources]);
	return {
		changes: [...stateSchema.changes, ...migrations.sources.flatMap((source) => source.changes)],
		warnings: [.../* @__PURE__ */ new Set([
			...stateSchema.warnings,
			...detected.warnings,
			...migrations.sources.flatMap((source) => source.warnings)
		])],
		...notices.length > 0 ? { notices } : {}
	};
}
/** Run canonical startup migrations and explicit Doctor-owned file repairs. */
async function autoMigrateLegacyState(params) {
	const env = params.env ?? process.env;
	const homedir = params.homedir ?? os.homedir;
	const migrationMode = params.doctorOnlyStateMigrations === true ? "doctor-repair" : "automatic";
	const initialStateDir = resolveStateDir(env, homedir);
	const checkKey = `${path.resolve(initialStateDir)}\0${migrationMode}`;
	if (autoMigrateChecked.has(checkKey)) return {
		migrated: false,
		skipped: true,
		changes: [],
		warnings: []
	};
	autoMigrateChecked.add(checkKey);
	const stateDirResult = await autoMigrateLegacyStateDir({
		env,
		homedir,
		log: params.log
	});
	const stateDir = resolveStateDir(env, homedir);
	autoMigrateChecked.add(`${path.resolve(stateDir)}\0${migrationMode}`);
	const stateSchemaOptions = { env: {
		...env,
		OPENCLAW_STATE_DIR: stateDir
	} };
	const stateSchema = params.doctorOnlyStateMigrations === true ? repairOpenClawStateDatabaseSchema(stateSchemaOptions) : repairOpenClawStateDatabaseSchemaIfNeeded(stateSchemaOptions);
	if (stateSchema.warnings.length > 0) {
		if (params.doctorOnlyStateMigrations !== true) throw new Error(formatStartupMigrationFailure(stateSchema.warnings));
		return {
			migrated: stateDirResult.migrated || stateSchema.changes.length > 0,
			skipped: false,
			changes: [...stateDirResult.changes, ...stateSchema.changes],
			warnings: [...stateDirResult.warnings, ...stateSchema.warnings],
			...stateDirResult.notices?.length ? { notices: stateDirResult.notices } : {}
		};
	}
	const pluginDoctorConfig = params.pluginDoctorConfig ?? params.cfg;
	const configMachineState = migrateLegacyConfigMachineState({
		config: pluginDoctorConfig,
		env: {
			...env,
			OPENCLAW_STATE_DIR: stateDir
		}
	});
	const agentMigrationOptions = {
		configuredAgentDatabaseTargets: resolveConfiguredAgentDatabaseTargets(params.cfg, { env }),
		env: {
			...env,
			OPENCLAW_STATE_DIR: stateDir
		}
	};
	const mediaPersistence = params.doctorOnlyStateMigrations === true ? await migrateLegacyMediaPersistence(agentMigrationOptions) : {
		changes: [],
		warnings: []
	};
	const transcriptDirectives = mediaPersistence.warnings.length === 0 ? await migrateHistoricalTranscriptDirectives(agentMigrationOptions) : {
		changes: [],
		warnings: []
	};
	if (transcriptDirectives.warnings.length > 0 || mediaPersistence.warnings.length > 0) return {
		migrated: stateDirResult.migrated || stateSchema.changes.length > 0 || configMachineState.changes.length > 0 || transcriptDirectives.changes.length > 0 || mediaPersistence.changes.length > 0,
		skipped: false,
		changes: [
			...stateDirResult.changes,
			...stateSchema.changes,
			...configMachineState.changes,
			...transcriptDirectives.changes,
			...mediaPersistence.changes
		],
		warnings: [
			...stateDirResult.warnings,
			...stateSchema.warnings,
			...transcriptDirectives.warnings,
			...mediaPersistence.warnings
		],
		...stateDirResult.notices?.length ? { notices: stateDirResult.notices } : {}
	};
	const profileWorkspace = params.doctorOnlyStateMigrations === true ? migrateLegacyProfileWorkspace({
		env,
		homedir
	}) : {
		changes: [],
		warnings: []
	};
	const pluginSessionStoreAgentIds = listPluginDoctorSessionStoreAgentIds({
		config: pluginDoctorConfig,
		env,
		pluginIds: collectRelevantDoctorPluginIds(pluginDoctorConfig)
	});
	const legacySessionSurfaces = params.doctorOnlyStateMigrations === true ? params.legacySessionSurfaces ?? (await import("./legacy-session-surfaces-zNTu1u7j.js")).prepareLegacySessionSurfaces({
		config: params.cfg,
		env
	}) : EMPTY_LEGACY_SESSION_SURFACES;
	const ownershipAgentId = params.doctorOnlyStateMigrations === true ? tryResolveDoctorSessionMigrationAgentId(params.cfg) : void 0;
	const sessionStoreOwnership = ownershipAgentId ? resolveSessionStoreOwnership({
		cfg: params.cfg,
		env,
		stateDir,
		targetAgentId: ownershipAgentId,
		pluginSessionStoreAgentIds
	}) : void 0;
	const orphanKeys = params.doctorOnlyStateMigrations === true ? await migrateOrphanedSessionKeys({
		cfg: params.cfg,
		env,
		additionalAgentIds: pluginSessionStoreAgentIds,
		legacySessionSurfaces
	}) : {
		changes: [],
		warnings: []
	};
	const detected = await detectLegacyStateMigrations({
		cfg: params.cfg,
		mode: params.doctorOnlyStateMigrations === true ? "doctor" : "automatic",
		pluginDoctorConfig: params.pluginDoctorConfig,
		pluginSessionStoreAgentIds,
		sessionStoreOwnership,
		env,
		homedir: params.homedir,
		doctorOnlyStateMigrations: params.doctorOnlyStateMigrations,
		allowLegacyDeviceIdentityImport: params.allowLegacyDeviceIdentityImport,
		legacySessionSurfaces
	});
	const deviceAuth = await migrateLegacyDeviceAuth({
		detected: detected.deviceAuth,
		env,
		stateDir: detected.stateDir
	});
	const deviceIdentity = await migrateLegacyDeviceIdentity({
		detected: detected.deviceIdentity,
		env,
		stateDir: detected.stateDir,
		doctorOnlyStateMigrations: params.doctorOnlyStateMigrations,
		allowLegacyDeviceIdentityImport: params.allowLegacyDeviceIdentityImport
	});
	const meetingTranscripts = await migrateLegacyMeetingTranscripts({
		detected: detected.meetingTranscripts,
		env,
		stateDir: detected.stateDir,
		now: params.now
	});
	const hasCustomAgentDir = env.OPENCLAW_AGENT_DIR?.trim() || env.PI_CODING_AGENT_DIR?.trim();
	const migrationSteps = buildLegacyStateMigrationSteps({
		mode: "automatic",
		detected,
		config: pluginDoctorConfig,
		sessionConfig: params.cfg,
		env,
		now: params.now,
		pluginSessionStoreAgentIds,
		recoverCorruptTargetStore: params.recoverCorruptTargetStore,
		skipAgentScopedMigrations: Boolean(hasCustomAgentDir),
		doctorOnlyStateMigrations: params.doctorOnlyStateMigrations,
		legacySessionSurfaces
	});
	const initialMigrationSources = [
		stateDirResult,
		profileWorkspace,
		stateSchema,
		transcriptDirectives,
		mediaPersistence,
		configMachineState,
		orphanKeys
	];
	const initialMigrationWarnings = [
		...initialMigrationSources.slice(0, -1).flatMap((source) => source.warnings),
		...detected.warnings,
		...orphanKeys.warnings
	];
	if (!hasCustomAgentDir && !detected.sessions.hasLegacy && !detected.agentDir.hasLegacy && !detected.pluginPlans?.hasLegacy && !detected.pluginStateSidecar.hasLegacy && !detected.pluginInstallIndex.hasLegacy && !detected.debugProxyCaptureSidecar.hasLegacy && !detected.stateSchema.hasLegacy && !detected.sharedAuthStore.hasLegacy && !detected.worktrees.hasLegacy && detected.worktrees.pathRewrites.length === 0 && !detected.taskStateSidecars.hasLegacy && !detected.deliveryQueues.hasLegacy && !detected.voiceWake.hasLegacy && !detected.updateCheck.hasLegacy && !detected.configHealth.hasLegacy && !detected.pluginBindingApprovals.hasLegacy && !detected.currentConversationBindings.hasLegacy && !detected.deviceAuth.hasLegacy && !detected.restartSentinel?.hasLegacy && !detected.workspace.hasLegacy && !detected.channelPairing.hasLegacy) {
		const alwaysRunSources = [];
		for (const step of migrationSteps) if (step.kind === "legacy-main-session-keys" || step.kind === "acp-session-metadata") alwaysRunSources.push(await step.run());
		const changes = [
			...initialMigrationSources,
			...alwaysRunSources,
			deviceAuth,
			deviceIdentity,
			meetingTranscripts
		].flatMap((source) => source.changes);
		const warnings = [.../* @__PURE__ */ new Set([...initialMigrationWarnings, ...[
			...alwaysRunSources,
			deviceAuth,
			deviceIdentity,
			meetingTranscripts
		].flatMap((source) => source.warnings)])];
		const notices = mergeNotices([
			stateDirResult,
			detected,
			...alwaysRunSources,
			deviceAuth,
			deviceIdentity
		]);
		logStateMigrationResult({
			changes,
			warnings,
			notices
		}, params.log);
		return {
			migrated: stateDirResult.migrated || changes.length > 0,
			skipped: false,
			changes,
			warnings,
			...notices.length > 0 ? { notices } : {}
		};
	}
	const migrations = await runLegacyStateMigrationSteps(migrationSteps);
	const changes = [
		...initialMigrationSources,
		...migrations.sharedSources,
		deviceAuth,
		deviceIdentity,
		...hasCustomAgentDir ? [] : [meetingTranscripts],
		...migrations.finalSources
	].flatMap((source) => source.changes);
	const warnings = [.../* @__PURE__ */ new Set([
		...initialMigrationWarnings,
		...migrations.sharedSources.flatMap((source) => source.warnings),
		...deviceAuth.warnings,
		...deviceIdentity.warnings,
		...hasCustomAgentDir ? [] : meetingTranscripts.warnings,
		...migrations.finalSources.flatMap((source) => source.warnings)
	])];
	const notices = mergeNotices([
		stateDirResult,
		detected,
		...migrations.sharedNoticeSources,
		deviceAuth,
		deviceIdentity,
		meetingTranscripts,
		...migrations.finalNoticeSources
	]);
	logStateMigrationResult({
		changes,
		warnings,
		notices
	}, params.log);
	return {
		migrated: stateDirResult.migrated || changes.length > 0 || meetingTranscripts.changes.length > 0,
		skipped: Boolean(hasCustomAgentDir),
		changes,
		warnings,
		...notices.length > 0 ? { notices } : {}
	};
}
//#endregion
export { autoMigrateLegacyState, detectLegacyStateMigrations, runLegacyStateMigrations };