UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

5,299 lines 218 kB
import { c as isRecord } from "./record-coerce-DItp3I4t.js";
import { c as normalizeOptionalLowercaseString, l as normalizeOptionalString, o as normalizeLowercaseStringOrEmpty, p as normalizeStringifiedOptionalString } from "./string-coerce-CIXf7egm.js";
import { y as uniqueStrings } from "./string-normalization-DsCfAx8q.js";
import { t as isBlockedObjectKey } from "./prototype-keys-CuYw53fZ.js";
import { n as normalizeAgentId } from "./agent-id-CeT3w4ap.js";
import { r as normalizeProviderId } from "./provider-id-DMd-TDFp.js";
import { n as DEFAULT_GATEWAY_PORT } from "./paths-D2sRr1a_.js";
import { n as normalizeAccountId } from "./account-id-CETVCrTz.js";
import { t as parseDurationMs } from "./parse-duration-CuuCHKpt.js";
import { t as normalizeExactAllowedHost } from "./exact-hostname-B5MIU7_E.js";
import { a as cloneRecord, c as visitAgentConfigScopes, l as visitAgentEntries, o as deleteRetiredPath, s as hasOwnKey, t as LEGACY_CONFIG_MIGRATIONS_RUNTIME_MODELS, u as visitChannelEntries } from "./legacy-config-migrations.runtime.models-Ak4QjGte.js";
import { c as ensureRecord, d as mergeMissing, l as getRecord, s as defineLegacyConfigMigration, u as mapLegacyAudioTranscription } from "./legacy-config-migrations.runtime.models.catalog-ChsrCTYM.js";
import { n as normalizeToolProviderPolicyKey, t as isCanonicalToolProviderPolicyKey } from "./provider-tool-policy-BKZP2YeG.js";
import { t as DEFAULT_SANDBOX_BROWSER_NETWORK } from "./browser-network-BG6jdrVm.js";
import { r as isKnownCoreToolId } from "./tool-catalog-79RBtNnN.js";
import { a as expandToolGroups, u as resolveToolProfilePolicy } from "./tool-policy-shared-DIyS0iQC.js";
import { o as isToolAllowedByPolicyName } from "./tool-policy-match-TnFxBs5z.js";
import { l as mergeAlsoAllowPolicy } from "./tool-policy-Wi0C45cX.js";
import { u as normalizeStaticProviderModelId } from "./model-ref-shared-Dz7QU0Lx.js";
import { a as resolveLegacyFirstAgentWorkspacePin, r as projectLegacyAgentRosterEntries } from "./legacy.roster-CQ9vDbV2.js";
import { a as resolveGatewayPortWithDefault, i as isGatewayNonLoopbackBindMode, r as hasConfiguredControlUiAllowedOrigins, t as buildDefaultControlUiAllowedOrigins } from "./gateway-control-ui-origins-dIyw_yYs.js";
import { i as resolveOpenClawMcpTransportAlias, n as isKnownCliMcpTypeAlias, t as canonicalizeConfiguredMcpServer } from "./mcp-config-normalize-CWrDNtye.js";
import { n as TASK_SUGGESTION_TOOL_NAME_MIGRATION, o as migrateLegacyToolNamePolicies, r as findLegacyToolNamePaths, t as IMAGE_INSPECTION_TOOL_NAME_MIGRATION } from "./legacy-tool-name-migration-C7jUPJK3.js";
import fs from "node:fs";
import path from "node:path";
import os from "node:os";
//#region src/commands/doctor/shared/legacy-config-migrations.audio.ts
function applyLegacyAudioTranscriptionModel(params) {
	const mapped = mapLegacyAudioTranscription(params.source);
	if (!mapped) {
		params.changes.push(params.invalidMessage);
		return;
	}
	const tools = ensureRecord(params.raw, "tools");
	const media = ensureRecord(tools, "media");
	const mediaAudio = ensureRecord(media, "audio");
	const models = Array.isArray(media.models) ? media.models : [];
	const isAudioCompatible = (value) => {
		const model = getRecord(value);
		return model !== null && (!Array.isArray(model.capabilities) || model.capabilities.includes("audio"));
	};
	if (!(Array.isArray(mediaAudio.models) && mediaAudio.models.some(isAudioCompatible) || models.some(isAudioCompatible))) {
		mediaAudio.enabled = true;
		mediaAudio.preferredModel = typeof mapped.command === "string" ? `cli:${mapped.command}` : void 0;
		media.models = [...models, {
			...mapped,
			capabilities: ["audio"]
		}];
		params.changes.push(params.movedMessage);
		return;
	}
	params.changes.push(params.alreadySetMessage);
}
/** Legacy config migration specs for audio/tool media config. */
const LEGACY_CONFIG_MIGRATIONS_AUDIO = [defineLegacyConfigMigration({
	id: "audio.transcription-v2",
	describe: "Move audio.transcription to tools.media.models",
	legacyRules: [{
		path: ["audio", "transcription"],
		message: "Use a capability-tagged tools.media.models entry instead."
	}],
	apply: (raw, changes) => {
		const audio = getRecord(raw.audio);
		if (audio?.transcription === void 0) return;
		applyLegacyAudioTranscriptionModel({
			raw,
			source: audio.transcription,
			changes,
			movedMessage: "Moved audio.transcription → tools.media.models.",
			alreadySetMessage: "Removed audio.transcription (tools.media.models already set).",
			invalidMessage: "Removed audio.transcription (invalid or empty command)."
		});
		delete audio.transcription;
		if (Object.keys(audio).length === 0) delete raw.audio;
		else raw.audio = audio;
	}
})];
//#endregion
//#region src/commands/doctor/shared/legacy-config-migrations.channels.ts
function cleanupEmptyRecord(parent, key) {
	const value = getRecord(parent[key]);
	if (value && Object.keys(value).length === 0) delete parent[key];
}
function resolveCompatibleDefaultGroupEntry(section) {
	const existingGroups = section.groups;
	if (existingGroups !== void 0 && !getRecord(existingGroups)) return null;
	const groups = getRecord(existingGroups) ?? {};
	const existingEntry = groups["*"];
	if (existingEntry !== void 0 && !getRecord(existingEntry)) return null;
	return {
		groups,
		entry: getRecord(existingEntry) ?? {}
	};
}
function migrateChannelDefaultRequireMention(params) {
	const defaultGroupEntry = resolveCompatibleDefaultGroupEntry(params.section);
	if (!defaultGroupEntry) {
		params.changes.push(`Removed ${params.legacyPath} (channels.${params.channelId}.groups has an incompatible shape; fix remaining issues manually).`);
		return false;
	}
	const { groups, entry } = defaultGroupEntry;
	if (entry.requireMention === void 0) {
		entry.requireMention = params.requireMention;
		groups["*"] = entry;
		params.section.groups = groups;
		params.changes.push(`Moved ${params.legacyPath} → channels.${params.channelId}.groups."*".requireMention.`);
		return true;
	}
	params.changes.push(`Removed ${params.legacyPath} (channels.${params.channelId}.groups."*" already set).`);
	return false;
}
function migrateRoutingAllowFrom(raw, changes) {
	const routing = getRecord(raw.routing);
	if (!routing || routing.allowFrom === void 0) return;
	const channels = getRecord(raw.channels);
	const whatsapp = getRecord(channels?.whatsapp);
	if (!channels || !whatsapp) {
		delete routing.allowFrom;
		cleanupEmptyRecord(raw, "routing");
		changes.push("Removed routing.allowFrom (channels.whatsapp not configured).");
		return;
	}
	if (whatsapp.allowFrom === void 0) {
		whatsapp.allowFrom = routing.allowFrom;
		changes.push("Moved routing.allowFrom → channels.whatsapp.allowFrom.");
	} else changes.push("Removed routing.allowFrom (channels.whatsapp.allowFrom already set).");
	delete routing.allowFrom;
	channels.whatsapp = whatsapp;
	raw.channels = channels;
	cleanupEmptyRecord(raw, "routing");
}
function migrateRoutingGroupChatMessages(params) {
	const migrateMessageGroupField = (field) => {
		const value = params.groupChat[field];
		if (value === void 0) return;
		const messages = ensureRecord(params.raw, "messages");
		const messagesGroup = ensureRecord(messages, "groupChat");
		if (messagesGroup[field] === void 0) {
			messagesGroup[field] = value;
			params.changes.push(`Moved routing.groupChat.${field} → messages.groupChat.${field}.`);
		} else params.changes.push(`Removed routing.groupChat.${field} (messages.groupChat.${field} already set).`);
		delete params.groupChat[field];
	};
	migrateMessageGroupField("historyLimit");
	migrateMessageGroupField("mentionPatterns");
	if (Object.keys(params.groupChat).length === 0) delete params.routing.groupChat;
	else params.routing.groupChat = params.groupChat;
}
function migrateRoutingGroupChatRequireMention(params) {
	const requireMention = params.groupChat.requireMention;
	if (requireMention === void 0) return;
	const channels = getRecord(params.raw.channels);
	let matchedChannel = false;
	if (channels) {
		for (const channelId of [
			"whatsapp",
			"telegram",
			"imessage"
		]) {
			const section = getRecord(channels[channelId]);
			if (!section) continue;
			matchedChannel = true;
			migrateChannelDefaultRequireMention({
				section,
				channelId,
				legacyPath: "routing.groupChat.requireMention",
				requireMention,
				changes: params.changes
			});
			channels[channelId] = section;
		}
		params.raw.channels = channels;
	}
	if (!matchedChannel) params.changes.push("Removed routing.groupChat.requireMention (no configured WhatsApp, Telegram, or iMessage channel found).");
	delete params.groupChat.requireMention;
}
function migrateRoutingGroupChat(raw, changes) {
	const routing = getRecord(raw.routing);
	const groupChat = getRecord(routing?.groupChat);
	if (!routing || !groupChat) return;
	migrateRoutingGroupChatRequireMention({
		raw,
		groupChat,
		changes
	});
	migrateRoutingGroupChatMessages({
		raw,
		routing,
		groupChat,
		changes
	});
	cleanupEmptyRecord(raw, "routing");
}
function migrateTelegramRequireMention(raw, changes) {
	const channels = getRecord(raw.channels);
	const telegram = getRecord(channels?.telegram);
	if (!channels || !telegram || telegram.requireMention === void 0) return;
	migrateChannelDefaultRequireMention({
		section: telegram,
		channelId: "telegram",
		legacyPath: "channels.telegram.requireMention",
		requireMention: telegram.requireMention,
		changes
	});
	delete telegram.requireMention;
	channels.telegram = telegram;
	raw.channels = channels;
}
function hasLegacyFeishuAccountBotName(value) {
	const accounts = getRecord(value);
	if (!accounts) return false;
	return Object.values(accounts).some((entry) => {
		const account = getRecord(entry);
		return Boolean(account && hasOwnKey(account, "botName"));
	});
}
function migrateFeishuAccountBotName(raw, changes) {
	const channels = getRecord(raw.channels);
	const feishu = getRecord(channels?.feishu);
	const accounts = getRecord(feishu?.accounts);
	if (!channels || !feishu || !accounts) return;
	for (const [accountId, accountRaw] of Object.entries(accounts)) {
		const account = getRecord(accountRaw);
		if (!account || !hasOwnKey(account, "botName")) continue;
		const legacyPath = `channels.feishu.accounts.${accountId}.botName`;
		const currentPath = `channels.feishu.accounts.${accountId}.name`;
		if (account.name === void 0) {
			account.name = account.botName;
			changes.push(`Moved ${legacyPath} → ${currentPath}.`);
		} else changes.push(`Removed ${legacyPath} (${currentPath} already set).`);
		delete account.botName;
		accounts[accountId] = account;
	}
	feishu.accounts = accounts;
	channels.feishu = feishu;
	raw.channels = channels;
}
function hasLegacyThreadBindingTtl(value) {
	const threadBindings = getRecord(value);
	return Boolean(threadBindings && hasOwnKey(threadBindings, "ttlHours"));
}
function hasLegacyThreadBindingSpawnSplit(value) {
	const threadBindings = getRecord(value);
	return Boolean(threadBindings && (hasOwnKey(threadBindings, "spawnSubagentSessions") || hasOwnKey(threadBindings, "spawnAcpSessions")));
}
function migrateThreadBindingsTtlHoursForPath(params) {
	const threadBindings = getRecord(params.owner.threadBindings);
	if (!threadBindings || !hasOwnKey(threadBindings, "ttlHours")) return false;
	const hadIdleHours = threadBindings.idleHours !== void 0;
	if (!hadIdleHours) threadBindings.idleHours = threadBindings.ttlHours;
	delete threadBindings.ttlHours;
	params.owner.threadBindings = threadBindings;
	if (hadIdleHours) params.changes.push(`Removed ${params.pathPrefix}.threadBindings.ttlHours (${params.pathPrefix}.threadBindings.idleHours already set).`);
	else params.changes.push(`Moved ${params.pathPrefix}.threadBindings.ttlHours → ${params.pathPrefix}.threadBindings.idleHours.`);
	return true;
}
function resolveMigratedSpawnSessions(threadBindings) {
	const subagent = threadBindings.spawnSubagentSessions;
	const acp = threadBindings.spawnAcpSessions;
	const subagentBool = typeof subagent === "boolean" ? subagent : void 0;
	const acpBool = typeof acp === "boolean" ? acp : void 0;
	if (subagentBool === void 0) return acpBool;
	if (acpBool === void 0) return subagentBool;
	return subagentBool && acpBool;
}
function migrateThreadBindingsSpawnSessionsForPath(params) {
	const threadBindings = getRecord(params.owner.threadBindings);
	if (!threadBindings || !hasLegacyThreadBindingSpawnSplit(threadBindings)) return false;
	const hadSpawnSessions = threadBindings.spawnSessions !== void 0;
	const resolved = resolveMigratedSpawnSessions(threadBindings);
	const oldSubagent = threadBindings.spawnSubagentSessions;
	const oldAcp = threadBindings.spawnAcpSessions;
	delete threadBindings.spawnSubagentSessions;
	delete threadBindings.spawnAcpSessions;
	if (!hadSpawnSessions && resolved !== void 0) threadBindings.spawnSessions = resolved;
	params.owner.threadBindings = threadBindings;
	if (hadSpawnSessions) params.changes.push(`Removed deprecated ${params.pathPrefix}.threadBindings.spawnSubagentSessions/spawnAcpSessions (${params.pathPrefix}.threadBindings.spawnSessions already set).`);
	else if (typeof oldSubagent === "boolean" && typeof oldAcp === "boolean" && oldSubagent !== oldAcp) params.changes.push(`Collapsed conflicting ${params.pathPrefix}.threadBindings.spawnSubagentSessions/spawnAcpSessions → ${params.pathPrefix}.threadBindings.spawnSessions (${String(resolved)}).`);
	else params.changes.push(`Moved ${params.pathPrefix}.threadBindings.spawnSubagentSessions/spawnAcpSessions → ${params.pathPrefix}.threadBindings.spawnSessions (${String(resolved)}).`);
	return true;
}
function migrateThreadBindingsForPath(params) {
	migrateThreadBindingsTtlHoursForPath(params);
	migrateThreadBindingsSpawnSessionsForPath(params);
}
function hasLegacyThreadBindingInAnyChannel(value, matcher) {
	const channels = getRecord(value);
	if (!channels) return false;
	return Object.values(channels).some((entry) => {
		const channel = getRecord(entry);
		if (!channel) return false;
		return matcher(channel.threadBindings) || Object.values(getRecord(channel.accounts) ?? {}).some((account) => matcher(getRecord(account)?.threadBindings));
	});
}
const THREAD_BINDING_RULES = [
	{
		path: ["session", "threadBindings"],
		message: "session.threadBindings.ttlHours was renamed to session.threadBindings.idleHours. Run \"openclaw doctor --fix\".",
		match: (value) => hasLegacyThreadBindingTtl(value)
	},
	{
		path: ["channels"],
		message: "channels.<id>.threadBindings.ttlHours was renamed to channels.<id>.threadBindings.idleHours. Run \"openclaw doctor --fix\".",
		match: (value) => hasLegacyThreadBindingInAnyChannel(value, hasLegacyThreadBindingTtl)
	},
	{
		path: ["session", "threadBindings"],
		message: "session.threadBindings.spawnSubagentSessions/spawnAcpSessions were replaced by session.threadBindings.spawnSessions. Run \"openclaw doctor --fix\".",
		match: (value) => hasLegacyThreadBindingSpawnSplit(value)
	},
	{
		path: ["channels"],
		message: "channels.<id>.threadBindings.spawnSubagentSessions/spawnAcpSessions were replaced by channels.<id>.threadBindings.spawnSessions. Run \"openclaw doctor --fix\".",
		match: (value) => hasLegacyThreadBindingInAnyChannel(value, hasLegacyThreadBindingSpawnSplit)
	}
];
const GROUP_ROUTING_RULES = [
	{
		path: ["routing", "allowFrom"],
		message: "routing.allowFrom was removed; use channels.whatsapp.allowFrom instead. Run \"openclaw doctor --fix\"."
	},
	{
		path: [
			"routing",
			"groupChat",
			"requireMention"
		],
		message: "routing.groupChat.requireMention was removed; use channels.<channel>.groups.\"*\".requireMention instead. Run \"openclaw doctor --fix\"."
	},
	{
		path: [
			"routing",
			"groupChat",
			"historyLimit"
		],
		message: "routing.groupChat.historyLimit was moved; use messages.groupChat.historyLimit instead. Run \"openclaw doctor --fix\"."
	},
	{
		path: [
			"routing",
			"groupChat",
			"mentionPatterns"
		],
		message: "routing.groupChat.mentionPatterns was moved; use messages.groupChat.mentionPatterns instead. Run \"openclaw doctor --fix\"."
	},
	{
		path: [
			"channels",
			"telegram",
			"requireMention"
		],
		message: "channels.telegram.requireMention was removed; use channels.telegram.groups.\"*\".requireMention instead. Run \"openclaw doctor --fix\"."
	}
];
const FEISHU_ACCOUNT_RULES = [{
	path: [
		"channels",
		"feishu",
		"accounts"
	],
	message: "channels.feishu.accounts.<id>.botName was renamed to channels.feishu.accounts.<id>.name. Run \"openclaw doctor --fix\".",
	match: (value) => hasLegacyFeishuAccountBotName(value)
}];
const WEBCHAT_CHANNEL_RULES = [{
	path: ["channels", "webchat"],
	message: "channels.webchat is retired. Run \"openclaw doctor --fix\"."
}];
function migrateRetiredWebchatChannelConfig(raw, changes) {
	const channels = getRecord(raw.channels);
	if (!channels || !hasOwnKey(channels, "webchat")) return;
	delete channels.webchat;
	raw.channels = channels;
	cleanupEmptyRecord(raw, "channels");
	changes.push("Removed retired channels.webchat config.");
}
/** Legacy config migration specs for channel-owned compatibility keys. */
const LEGACY_CONFIG_MIGRATIONS_CHANNELS = [
	defineLegacyConfigMigration({
		id: "channels.webchat-remove",
		describe: "Remove retired WebChat channel config",
		legacyRules: WEBCHAT_CHANNEL_RULES,
		apply: migrateRetiredWebchatChannelConfig
	}),
	defineLegacyConfigMigration({
		id: "legacy-group-routing->channel-groups",
		describe: "Move legacy routing group chat settings to current channel group and messages config",
		legacyRules: GROUP_ROUTING_RULES,
		apply: (raw, changes) => {
			migrateRoutingAllowFrom(raw, changes);
			migrateRoutingGroupChat(raw, changes);
			migrateTelegramRequireMention(raw, changes);
		}
	}),
	defineLegacyConfigMigration({
		id: "feishu.accounts.botName->name",
		describe: "Move legacy Feishu account botName config to account name",
		legacyRules: FEISHU_ACCOUNT_RULES,
		apply: migrateFeishuAccountBotName
	}),
	defineLegacyConfigMigration({
		id: "thread-bindings.ttlHours->idleHours",
		describe: "Move legacy threadBindings.ttlHours keys to threadBindings.idleHours (session + channel configs)",
		legacyRules: THREAD_BINDING_RULES,
		apply: (raw, changes) => {
			const session = getRecord(raw.session);
			if (session) migrateThreadBindingsForPath({
				owner: session,
				pathPrefix: "session",
				changes
			});
			const channels = getRecord(raw.channels);
			if (!channels) return;
			for (const channelId of Object.keys(channels)) visitChannelEntries(raw, channelId, (owner, pathPrefix) => {
				migrateThreadBindingsForPath({
					owner,
					pathPrefix,
					changes
				});
			});
		}
	})
];
//#endregion
//#region src/commands/doctor/shared/legacy-config-migrations.qqbot-account.ts
function hasEnvironmentValue(name) {
	return Boolean(process.env[name]?.trim());
}
function shouldCreateEnvironmentOnlyQQBotConfig(raw) {
	const channels = getRecord(raw.channels);
	return Boolean((raw.channels === void 0 || channels) && !getRecord(channels?.qqbot) && hasEnvironmentValue("QQBOT_APP_ID") && hasEnvironmentValue("QQBOT_CLIENT_SECRET"));
}
function listQQBotConfigEntries(qqbot) {
	const rootSnapshot = structuredClone(qqbot);
	const entries = [{
		entry: qqbot,
		path: "channels.qqbot"
	}];
	const accounts = getRecord(qqbot.accounts);
	if (!accounts) return entries;
	for (const [accountId, accountValue] of Object.entries(accounts)) {
		const account = getRecord(accountValue);
		if (account) entries.push({
			entry: account,
			path: `channels.qqbot.accounts.${accountId}`,
			aliasSuffix: accountId,
			inheritedEntry: accountId === "default" ? rootSnapshot : void 0
		});
	}
	return entries;
}
function migrateDefaultAccount(qqbot, changes) {
	const accounts = getRecord(qqbot.accounts);
	const configuredDefaultAccount = typeof qqbot.defaultAccount === "string" ? qqbot.defaultAccount.trim() : "";
	const normalizedDefaultAccount = configuredDefaultAccount.toLowerCase();
	const defaultAccount = getRecord(accounts?.default);
	if (configuredDefaultAccount && normalizedDefaultAccount !== "default") {
		const selectedAccountId = normalizedDefaultAccount;
		const selectedAccount = getRecord(accounts?.[selectedAccountId]);
		if (!selectedAccount) {
			delete qqbot.defaultAccount;
			changes.push(`Removed invalid channels.qqbot.defaultAccount=${configuredDefaultAccount}; the bundled plugin already fell back to its normal account selection order.`);
			return;
		}
		if (qqbot.appId || hasEnvironmentValue("QQBOT_APP_ID") || defaultAccount) return;
		const reorderedAccounts = { [selectedAccountId]: selectedAccount };
		for (const [accountId, account] of Object.entries(accounts ?? {})) if (accountId !== selectedAccountId && !isBlockedObjectKey(accountId)) reorderedAccounts[accountId] = account;
		if (Object.keys(reorderedAccounts)[0] !== selectedAccountId) return;
		qqbot.accounts = reorderedAccounts;
		delete qqbot.defaultAccount;
		changes.push(`Moved channels.qqbot.accounts.${selectedAccountId} to the first account position and removed defaultAccount so Tencent QQBot 2.0 preserves the selected named default.`);
		return;
	}
	if (!accounts || !defaultAccount) {
		if (hasOwnKey(qqbot, "defaultAccount")) {
			delete qqbot.defaultAccount;
			changes.push("Removed channels.qqbot.defaultAccount=default because Tencent QQBot 2.0 selects the root account directly.");
		}
		return;
	}
	for (const [key, value] of Object.entries(defaultAccount)) if (key !== "accounts" && !isBlockedObjectKey(key)) qqbot[key] = value;
	delete accounts.default;
	if (Object.keys(accounts).length === 0) delete qqbot.accounts;
	delete qqbot.defaultAccount;
	changes.push("Moved channels.qqbot.accounts.default overrides to channels.qqbot for Tencent QQBot 2.0 default-account resolution.");
}
function normalizeProviderAliasSegment(value) {
	return value.trim().toLowerCase().replace(/[^a-z0-9_-]+/g, "-").replace(/^-+|-+$/g, "") || "account";
}
function isMatchingFileProvider(value, filePath) {
	const provider = getRecord(value);
	return Boolean(provider && provider.source === "file" && provider.path === filePath && provider.mode === "singleValue");
}
function allocateFileProviderAlias(params) {
	let secrets = getRecord(params.raw.secrets);
	if (!secrets) {
		if (params.raw.secrets !== void 0) return;
		secrets = {};
		params.raw.secrets = secrets;
	}
	let providers = getRecord(secrets.providers);
	if (!providers) {
		if (secrets.providers !== void 0) return;
		providers = {};
		secrets.providers = providers;
	}
	const base = `qqbot${params.aliasSuffix ? `-${normalizeProviderAliasSegment(params.aliasSuffix)}` : ""}-client-secret`.slice(0, 60).replace(/-+$/g, "");
	for (let index = 1; index <= 999; index += 1) {
		const alias = index === 1 ? base : `${base.slice(0, 60 - String(index).length)}-${index}`;
		const existing = providers[alias];
		if (existing === void 0) {
			providers[alias] = {
				source: "file",
				path: params.filePath,
				mode: "singleValue"
			};
			return alias;
		}
		if (isMatchingFileProvider(existing, params.filePath)) return alias;
	}
}
function migrateClientSecretFile(params) {
	if (!hasOwnKey(params.entry, "clientSecretFile")) return;
	if (params.entry.clientSecret !== void 0) {
		delete params.entry.clientSecretFile;
		params.changes.push(`Removed ${params.path}.clientSecretFile (${params.path}.clientSecret already set).`);
		return;
	}
	const filePath = typeof params.entry.clientSecretFile === "string" ? params.entry.clientSecretFile.trim() : "";
	if (!filePath) {
		params.entry.enabled = false;
		delete params.entry.clientSecretFile;
		params.changes.push(`Removed invalid ${params.path}.clientSecretFile and disabled this QQBot account.`);
		return;
	}
	const provider = allocateFileProviderAlias({
		raw: params.raw,
		filePath,
		aliasSuffix: params.aliasSuffix
	});
	if (!provider) {
		params.entry.enabled = false;
		params.changes.push(`Disabled ${params.path} because its clientSecretFile could not be migrated while secrets.providers has an incompatible shape.`);
		return;
	}
	params.entry.clientSecret = {
		source: "file",
		provider,
		id: "value"
	};
	delete params.entry.clientSecretFile;
	params.changes.push(`Moved ${params.path}.clientSecretFile → ${params.path}.clientSecret using file provider ${provider}.`);
}
//#endregion
//#region src/commands/doctor/shared/legacy-config-migrations.qqbot.ts
const APPROVALS_DISABLED_SENTINEL = "openclaw:approval-disabled";
function hasQQBotEntryMatching(value, predicate) {
	const qqbot = getRecord(value);
	return Boolean(qqbot && listQQBotConfigEntries(qqbot).some(({ entry, inheritedEntry }) => predicate(entry, inheritedEntry)));
}
function normalizeIds(value) {
	if (!Array.isArray(value)) return [];
	return [...new Set(value.filter((item) => ["string", "number"].includes(typeof item)).map((item) => String(item).trim()).filter(Boolean))];
}
function normalizeLegacyAllowFrom(value) {
	return [...new Set(normalizeIds(value).map((id) => {
		const unprefixed = id.replace(/^qqbot:/i, "");
		if (unprefixed === "*" || unprefixed === APPROVALS_DISABLED_SENTINEL) return unprefixed;
		return unprefixed.toUpperCase();
	}))];
}
function resolveLegacyQQBotCommandsAllowFrom(raw) {
	const commands = getRecord(raw.commands);
	const allowFrom = getRecord(commands?.allowFrom);
	if (!allowFrom) return;
	if (Array.isArray(allowFrom.qqbot)) return normalizeLegacyAllowFrom(allowFrom.qqbot);
	return Array.isArray(allowFrom["*"]) ? normalizeLegacyAllowFrom(allowFrom["*"]) : void 0;
}
function hasConfiguredFilter(value) {
	return Array.isArray(value) ? value.length > 0 : value !== void 0;
}
function migrateExecApprovals(params) {
	const hasOwnLegacyConfig = hasOwnKey(params.entry, "execApprovals");
	const hasLegacyConfig = hasOwnLegacyConfig || params.inheritedEntry?.execApprovals !== void 0;
	const hasOwnPolicyOverride = hasOwnLegacyConfig || hasOwnKey(params.entry, "allowFrom") || hasOwnKey(params.entry, "dmPolicy");
	if (params.inheritedEntry && !hasOwnPolicyOverride) return;
	const legacy = getRecord(hasOwnLegacyConfig ? params.entry.execApprovals : params.inheritedEntry?.execApprovals);
	const allowFromValue = hasOwnKey(params.entry, "allowFrom") ? params.entry.allowFrom : params.inheritedEntry?.allowFrom;
	const dmPolicy = hasOwnKey(params.entry, "dmPolicy") ? params.entry.dmPolicy : params.inheritedEntry?.dmPolicy;
	const existingAllowFrom = normalizeLegacyAllowFrom(allowFromValue);
	const explicitApprovers = normalizeLegacyAllowFrom(legacy?.approvers);
	const allowFromWasOpen = existingAllowFrom.length === 0 || existingAllowFrom.includes("*");
	const preserveOpenDm = dmPolicy === "open" || dmPolicy === void 0 && allowFromWasOpen || dmPolicy === "allowlist" && existingAllowFrom.includes("*");
	if (!hasLegacyConfig) {
		if (params.commandsAllowFrom !== void 0) {
			const commandApprovers = params.commandsAllowFrom.filter((id) => id !== "*");
			const restrictiveChatAllowFrom = new Set(existingAllowFrom.filter((id) => id !== "*"));
			const safeApprovers = existingAllowFrom.length > 0 && !existingAllowFrom.includes("*") ? commandApprovers.filter((id) => restrictiveChatAllowFrom.has(id)) : commandApprovers;
			const nextAllowFrom = safeApprovers.length > 0 ? safeApprovers : [APPROVALS_DISABLED_SENTINEL];
			const needsOpenDm = preserveOpenDm && dmPolicy !== "open";
			if (existingAllowFrom.length === nextAllowFrom.length && existingAllowFrom.every((id, index) => id === nextAllowFrom[index]) && !needsOpenDm) return;
			params.entry.allowFrom = nextAllowFrom;
			if (needsOpenDm) params.entry.dmPolicy = "open";
			params.changes.push(`Secured ${params.path}.allowFrom for Tencent QQBot 2.0 native approvals using the previous commands.allowFrom operator list${safeApprovers.length > 0 ? " intersected with restrictive chat access" : "; no safely representable operator remained, so approvals were locked"}.`);
			return;
		}
		if (!allowFromWasOpen) return;
		const explicitAllowFrom = existingAllowFrom.filter((id) => id !== "*");
		params.entry.allowFrom = explicitAllowFrom.length > 0 ? explicitAllowFrom : [APPROVALS_DISABLED_SENTINEL];
		if (preserveOpenDm) params.entry.dmPolicy = "open";
		params.changes.push(`Secured ${params.path}.allowFrom for Tencent QQBot 2.0 native approvals; wildcard/empty approval access was replaced with ${explicitAllowFrom.length > 0 ? "the existing explicit IDs" : "a non-matching marker"} while preserving open DM access separately.`);
		return;
	}
	const hasUnsupportedPolicy = !legacy || legacy.enabled === false || hasConfiguredFilter(legacy.agentFilter) || hasConfiguredFilter(legacy.sessionFilter) || legacy.target !== void 0;
	let nextAllowFrom;
	let reason;
	if (hasUnsupportedPolicy || explicitApprovers.includes("*")) {
		nextAllowFrom = [APPROVALS_DISABLED_SENTINEL];
		reason = "the Tencent 2.0 plugin cannot represent the previous approval policy, so native approval actions were locked";
	} else if (explicitApprovers.length > 0) {
		const restrictiveAllowFrom = new Set(existingAllowFrom.filter((id) => id !== "*"));
		nextAllowFrom = dmPolicy === "allowlist" && existingAllowFrom.length === 0 ? [] : existingAllowFrom.length > 0 && !existingAllowFrom.includes("*") ? explicitApprovers.filter((id) => restrictiveAllowFrom.has(id)) : explicitApprovers;
		if (nextAllowFrom.length === 0) {
			nextAllowFrom = [APPROVALS_DISABLED_SENTINEL];
			reason = "the approval and chat allowlists did not overlap, so native approval actions were locked";
		} else reason = "approval access was intersected with the existing chat allowlist";
	} else if (existingAllowFrom.length > 0 && !existingAllowFrom.includes("*")) {
		nextAllowFrom = existingAllowFrom;
		reason = "the existing restrictive chat allowlist remains the approval allowlist";
	} else {
		nextAllowFrom = [APPROVALS_DISABLED_SENTINEL];
		reason = "the previous same-chat or wildcard policy has no safe Tencent 2.0 representation, so native approval actions were locked";
	}
	if (preserveOpenDm && !nextAllowFrom.includes("*")) params.entry.dmPolicy = "open";
	params.entry.allowFrom = nextAllowFrom;
	delete params.entry.execApprovals;
	params.changes.push(`Moved ${params.path}.execApprovals → ${params.path}.allowFrom; ${reason}. Review chat access before re-enabling broader approval access.`);
}
function migrateAllowFrom(params) {
	const current = normalizeIds(params.entry.allowFrom);
	const normalized = normalizeLegacyAllowFrom(params.entry.allowFrom);
	if (current.every((id, index) => id === normalized[index])) return;
	params.entry.allowFrom = normalized;
	params.changes.push(`Normalized ${params.path}.allowFrom QQBot-prefixed IDs for Tencent QQBot 2.0.`);
}
function hasLegacyStreamingTransport(entry) {
	const streaming = getRecord(entry.streaming);
	return Boolean(streaming && (hasOwnKey(streaming, "nativeTransport") || hasOwnKey(streaming, "c2cStreamApi")));
}
function migrateStreamingTransport(params) {
	const streaming = getRecord(params.entry.streaming);
	if (!streaming || !hasLegacyStreamingTransport(params.entry)) return;
	const transport = typeof streaming.nativeTransport === "boolean" ? streaming.nativeTransport : typeof streaming.c2cStreamApi === "boolean" ? streaming.c2cStreamApi : void 0;
	delete streaming.nativeTransport;
	delete streaming.c2cStreamApi;
	if (transport !== void 0) streaming.mode = transport ? "partial" : "off";
	params.changes.push(`Removed unsupported ${params.path}.streaming native transport keys for Tencent QQBot 2.0${transport === void 0 ? "" : ` and set mode=${String(streaming.mode)}`}.`);
}
function mapTencentToolPolicy(value) {
	const policy = getRecord(value);
	const allow = Array.isArray(policy?.allow) ? policy.allow.map(String) : void 0;
	const deny = Array.isArray(policy?.deny) ? policy.deny.map(String) : void 0;
	const allowsAll = !allow || allow.length === 0 || allow.includes("*");
	if (allowsAll && (!deny || deny.length === 0)) return "full";
	if (deny?.length === 1 && deny[0] === "*") return "none";
	if (allowsAll && deny?.length === 3 && [
		"exec",
		"read",
		"write"
	].every((tool) => deny.includes(tool))) return "restricted";
	return "none";
}
function mostRestrictiveTencentToolPolicy(first, second) {
	const rank = {
		none: 0,
		restricted: 1,
		full: 2
	};
	const normalizedFirst = first === "full" || first === "restricted" || first === "none" ? first : "none";
	return rank[normalizedFirst] <= rank[second] ? normalizedFirst : second;
}
function migrateGroupTools(params) {
	const groups = getRecord(params.entry.groups);
	if (!groups) return;
	for (const [groupId, groupValue] of Object.entries(groups)) {
		const group = getRecord(groupValue);
		if (!group || !hasOwnKey(group, "tools") && !hasOwnKey(group, "toolsBySender")) continue;
		const groupPath = `${params.path}.groups.${groupId}`;
		const migratedPolicy = hasOwnKey(group, "toolsBySender") ? "none" : mapTencentToolPolicy(group.tools);
		group.toolPolicy = group.toolPolicy === void 0 ? migratedPolicy : mostRestrictiveTencentToolPolicy(group.toolPolicy, migratedPolicy);
		params.changes.push(`Moved ${groupPath}.tools policy → ${groupPath}.toolPolicy=${String(group.toolPolicy)} for Tencent QQBot 2.0, preserving the most restrictive configured policy.`);
		delete group.tools;
		if (hasOwnKey(group, "toolsBySender")) {
			delete group.toolsBySender;
			params.changes.push(`Removed ${groupPath}.toolsBySender; Tencent QQBot 2.0 cannot represent sender-specific tool policy, so the group policy was not broadened.`);
		}
	}
}
function hasLegacyGroupCommandLevel(entry) {
	const groups = getRecord(entry.groups);
	return Boolean(groups && Object.values(groups).some((groupValue) => {
		const group = getRecord(groupValue);
		return Boolean(group && hasOwnKey(group, "commandLevel"));
	}));
}
function migrateGroupCommandLevels(params) {
	const groups = getRecord(params.entry.groups);
	if (!groups) {
		const inheritedGroups = getRecord(params.inheritedEntry?.groups);
		if (Boolean(inheritedGroups && Object.values(inheritedGroups).some((groupValue) => {
			const group = getRecord(groupValue);
			return Boolean(group && hasOwnKey(group, "commandLevel") && group.commandLevel !== "all");
		})) && params.entry.groupPolicy !== void 0 && params.entry.groupPolicy !== "disabled") {
			params.entry.groupPolicy = "disabled";
			params.changes.push(`Set ${params.path}.groupPolicy=disabled because this default account overrides the root lock while inheriting a safety/strict group command policy that Tencent QQBot 2.0 cannot represent.`);
		}
		return;
	}
	let requiresLock = false;
	for (const [groupId, groupValue] of Object.entries(groups)) {
		const group = getRecord(groupValue);
		if (!group || !hasOwnKey(group, "commandLevel")) continue;
		const commandLevel = group.commandLevel;
		if (commandLevel !== "all") requiresLock = true;
		delete group.commandLevel;
		params.changes.push(`Removed unsupported ${params.path}.groups.${groupId}.commandLevel=${String(commandLevel)} for Tencent QQBot 2.0.`);
	}
	if (!requiresLock) return;
	params.entry.groupPolicy = "disabled";
	params.changes.push(`Set ${params.path}.groupPolicy=disabled because Tencent QQBot 2.0 cannot represent a previous safety/strict group command policy. Review the account before re-enabling group access.`);
}
const LEGACY_CONFIG_MIGRATIONS_QQBOT = [defineLegacyConfigMigration({
	id: "qqbot.tencent-2.0-compatibility",
	describe: "Migrate bundled QQBot config to Tencent QQBot 2.0 canonical fields",
	legacyRules: [
		{
			path: [],
			message: "Environment-only QQBot credentials need a safe Tencent QQBot 2.0 config shell. Run \"openclaw doctor --fix\".",
			match: (_value, root) => shouldCreateEnvironmentOnlyQQBotConfig(root)
		},
		{
			path: ["channels", "qqbot"],
			message: "QQBot defaultAccount/accounts.default must migrate to Tencent QQBot 2.0 account selection. Run \"openclaw doctor --fix\".",
			match: (value) => {
				const qqbot = getRecord(value);
				return Boolean(qqbot && (hasOwnKey(qqbot, "defaultAccount") || getRecord(getRecord(qqbot.accounts)?.default)));
			}
		},
		{
			path: ["channels", "qqbot"],
			message: "QQBot clientSecretFile must migrate to a file-backed SecretRef for Tencent QQBot 2.0. Run \"openclaw doctor --fix\".",
			match: (value) => hasQQBotEntryMatching(value, (entry) => hasOwnKey(entry, "clientSecretFile"))
		},
		{
			path: ["channels", "qqbot"],
			message: "QQBot wildcard/empty allowFrom must be separated from Tencent QQBot 2.0 native approval access. Run \"openclaw doctor --fix\".",
			match: (value) => hasQQBotEntryMatching(value, (entry, inheritedEntry) => {
				if (hasOwnKey(entry, "execApprovals")) return false;
				const allowFrom = normalizeLegacyAllowFrom(hasOwnKey(entry, "allowFrom") ? entry.allowFrom : inheritedEntry?.allowFrom);
				return allowFrom.length === 0 || allowFrom.includes("*");
			})
		},
		{
			path: ["channels", "qqbot"],
			message: "QQBot chat allowFrom must be reconciled with the previous commands.allowFrom approval operators for Tencent QQBot 2.0. Run \"openclaw doctor --fix\".",
			match: (value, root) => {
				const commandsAllowFrom = resolveLegacyQQBotCommandsAllowFrom(root);
				if (commandsAllowFrom === void 0) return false;
				const commandApprovers = new Set(commandsAllowFrom.filter((id) => id !== "*"));
				return hasQQBotEntryMatching(value, (entry, inheritedEntry) => {
					if (hasOwnKey(entry, "execApprovals") || !hasOwnKey(entry, "allowFrom") && inheritedEntry?.execApprovals !== void 0) return false;
					return normalizeLegacyAllowFrom(hasOwnKey(entry, "allowFrom") ? entry.allowFrom : inheritedEntry?.allowFrom).filter((id) => id !== "*" && id !== APPROVALS_DISABLED_SENTINEL).some((id) => !commandApprovers.has(id));
				});
			}
		},
		{
			path: ["channels", "qqbot"],
			message: "QQBot groups.*.commandLevel must migrate before Tencent QQBot 2.0 can safely handle group commands. Run \"openclaw doctor --fix\".",
			match: (value) => hasQQBotEntryMatching(value, hasLegacyGroupCommandLevel)
		},
		{
			path: ["channels", "qqbot"],
			message: "QQBot streaming.nativeTransport/c2cStreamApi must migrate to Tencent QQBot 2.0 streaming.mode. Run \"openclaw doctor --fix\".",
			match: (value) => hasQQBotEntryMatching(value, hasLegacyStreamingTransport)
		},
		{
			path: ["channels", "qqbot"],
			message: "QQBot allowFrom IDs must migrate to Tencent QQBot 2.0 canonical uppercase OpenIDs. Run \"openclaw doctor --fix\".",
			match: (value) => hasQQBotEntryMatching(value, (entry) => {
				const current = normalizeIds(entry.allowFrom);
				const normalized = normalizeLegacyAllowFrom(entry.allowFrom);
				return current.length !== normalized.length || current.some((id, index) => id !== normalized[index]);
			})
		},
		{
			path: ["channels", "qqbot"],
			message: "QQBot execApprovals must migrate to Tencent QQBot 2.0 allowFrom semantics. Run \"openclaw doctor --fix\".",
			match: (value) => hasQQBotEntryMatching(value, (entry) => hasOwnKey(entry, "execApprovals"))
		},
		{
			path: ["channels", "qqbot"],
			message: "QQBot group tools policies must migrate to Tencent QQBot 2.0 toolPolicy. Run \"openclaw doctor --fix\".",
			match: (value) => hasQQBotEntryMatching(value, (entry) => {
				const groups = getRecord(entry.groups);
				return Boolean(groups && Object.values(groups).some((groupValue) => {
					const group = getRecord(groupValue);
					return Boolean(group && (hasOwnKey(group, "tools") || hasOwnKey(group, "toolsBySender")));
				}));
			})
		}
	],
	apply: (raw, changes) => {
		let channels = getRecord(raw.channels);
		let qqbot = getRecord(channels?.qqbot);
		if (!qqbot && shouldCreateEnvironmentOnlyQQBotConfig(raw)) {
			channels ??= {};
			raw.channels = channels;
			qqbot = {
				enabled: true,
				dmPolicy: "open",
				allowFrom: [APPROVALS_DISABLED_SENTINEL]
			};
			channels.qqbot = qqbot;
			changes.push("Created channels.qqbot for environment-only Tencent QQBot 2.0 credentials with native approvals locked; no credential value was copied into config.");
		}
		if (!qqbot) return;
		migrateDefaultAccount(qqbot, changes);
		const commandsAllowFrom = resolveLegacyQQBotCommandsAllowFrom(raw);
		for (const item of listQQBotConfigEntries(qqbot)) {
			migrateClientSecretFile({
				raw,
				changes,
				...item
			});
			migrateExecApprovals({
				changes,
				commandsAllowFrom,
				...item
			});
			migrateAllowFrom({
				changes,
				...item
			});
			migrateStreamingTransport({
				changes,
				...item
			});
			migrateGroupTools({
				changes,
				...item
			});
			migrateGroupCommandLevels({
				changes,
				...item
			});
		}
	}
})];
//#endregion
//#region src/commands/doctor/shared/legacy-config-migrations.queue.ts
const RETIRED_QUEUE_MODES = /* @__PURE__ */ new Set([
	"queue",
	"steer-backlog",
	"steer+backlog"
]);
function isRetiredQueueMode(value) {
	return typeof value === "string" && RETIRED_QUEUE_MODES.has(value);
}
function hasRetiredQueueModeByChannel(value) {
	const byChannel = getRecord(value);
	return Boolean(byChannel && Object.values(byChannel).some(isRetiredQueueMode));
}
function migrateQueueMode(params) {
	const value = params.owner[params.key];
	if (!isRetiredQueueMode(value)) return false;
	const replacement = value === "queue" ? "steer" : "followup";
	params.owner[params.key] = replacement;
	params.changes.push(`Moved deprecated ${params.path} "${value}" → "${replacement}"; use "steer" for default active-run steering.`);
	return true;
}
/** Legacy config migration specs for message queue mode compatibility. */
const LEGACY_CONFIG_MIGRATIONS_QUEUE = [defineLegacyConfigMigration({
	id: "messages.queue.retired-steering-modes",
	describe: "Move retired messages.queue modes to followup mode",
	legacyRules: [{
		path: [
			"messages",
			"queue",
			"mode"
		],
		message: "messages.queue.mode uses a retired queue mode; use steer, followup, collect, or interrupt. Run \"openclaw doctor --fix\".",
		match: isRetiredQueueMode
	}, {
		path: [
			"messages",
			"queue",
			"byChannel"
		],
		message: "messages.queue.byChannel contains a retired queue mode; use steer, followup, collect, or interrupt. Run \"openclaw doctor --fix\".",
		match: hasRetiredQueueModeByChannel
	}],
	apply: (raw, changes) => {
		const queue = getRecord(getRecord(raw.messages)?.queue);
		if (!queue) return;
		migrateQueueMode({
			owner: queue,
			key: "mode",
			path: "messages.queue.mode",
			changes
		});
		const byChannel = getRecord(queue.byChannel);
		if (byChannel) {
			for (const [channelId, _value] of Object.entries(byChannel)) migrateQueueMode({
				owner: byChannel,
				key: channelId,
				path: `messages.queue.byChannel.${channelId}`,
				changes
			});
			queue.byChannel = byChannel;
		}
	}
})];
//#endregion
//#region src/commands/doctor/shared/legacy-runtime-model-policy.ts
/** Select canonical refs owned by a provider, preserving config order and duplicates. */
function selectedCanonicalModelRefsForRuntimePolicy(rawModel, provider) {
	const refs = [];
	const addRef = (rawRef) => {
		if (typeof rawRef !== "string") return;
		const ref = rawRef.trim();
		const slash = ref.indexOf("/");
		if (slash <= 0 || slash >= ref.length - 1 || normalizeProviderId(ref.slice(0, slash)) !== normalizeProviderId(provider)) return;
		refs.push(ref);
	};
	if (typeof rawModel === "string") {
		addRef(rawModel);
		return refs;
	}
	if (!isRecord(rawModel)) return refs;
	addRef(rawModel.primary);
	if (Array.isArray(rawModel.fallbacks)) for (const fallback of rawModel.fallbacks) addRef(fallback);
	return refs;
}
/** Add runtime policy unless the model entry already selects an explicit non-auto runtime. */
function modelEntryWithRuntimePolicy(entry, runtime) {
	const next = isRecord(entry) ? { ...entry } : {};
	const currentRuntime = isRecord(next.agentRuntime) ? next.agentRuntime : void 0;
	const currentRuntimeId = normalizeOptionalLowercaseString(currentRuntime?.id);
	if (currentRuntimeId && currentRuntimeId !== "auto") return {
		changed: false,
		entry: next
	};
	next.agentRuntime = {
		...currentRuntime,
		id: runtime
	};
	return {
		changed: true,
		entry: next
	};
}
//#endregion
//#region src/commands/doctor/shared/legacy-runtime-model-providers.ts
const LEGACY_RUNTIME_MODEL_PROVIDER_ALIASES = [
	{
		legacyProvider: "codex",
		provider: "openai",
		runtime: "codex",
		cli: false,
		requiresRuntimePolicy: true
	},
	{
		legacyProvider: "codex-cli",
		provider: "openai",
		runtime: "codex",
		cli: false,
		requiresRuntimePolicy: true
	},
	{
		legacyProvider: "claude-cli",
		provider: "anthropic",
		runtime: "claude-cli",
		cli: true,
		requiresRuntimePolicy: true
	},
	{
		legacyProvider: "google-gemini-cli",
		provider: "google",
		runtime: "google-gemini-cli",
		cli: true,
		requiresRuntimePolicy: true
	}
];
function normalizeLegacyRuntimeProviderId(provider) {
	const normalized = provider.trim().toLowerCase();
	return normalized === "anthropic-cli" ? "claude-cli" : normalizeProviderId(normalized);
}
const LEGACY_ALIAS_BY_PROVIDER = new Map(LEGACY_RUNTIME_MODEL_PROVIDER_ALIASES.map((entry) => [normalizeLegacyRuntimeProviderId(entry.legacyProvider), entry]));
/** List legacy model-provider aliases that doctor can migrate to provider/runtime policy. */
function listLegacyRuntimeModelProviderAliases() {
	return LEGACY_RUNTIME_MODEL_PROVIDER_ALIASES;
}
/** Return true when a legacy provider alias requires writing explicit runtime policy. */
function legacyRuntimeModelAliasRequiresRuntimePolicy(provider) {
	return LEGACY_ALIAS_BY_PROVIDER.get(normalizeLegacyRuntimeProviderId(provider))?.requiresRuntimePolicy === true;
}
function resolveLegacyRuntimeModelProviderAlias(provider) {
	return LEGACY_ALIAS_BY_PROVIDER.get(normalizeLegacyRuntimeProviderId(provider));
}
/** Rewrite a legacy runtime-encoded model ref to canonical provider/model plus runtime intent. */
function migrateLegacyRuntimeModelRef(raw) {
	const trimmed = raw.trim();
	const slash = trimmed.indexOf("/");
	if (slash <= 0 || slash >= trimmed.length - 1) return null;
	const alias = resolveLegacyRuntimeModelProviderAlias(trimmed.slice(0, slash));
	if (!alias) return null;
	const rawModel = trimmed.slice(slash + 1).trim();
	const model = normalizeStaticProviderModelId(alias.provider, rawModel);
	if (!model) return null;
	return {
		ref: `${alias.provider}/${model}`,
		legacyProvider: alias.legacyProvider,
		provider: alias.provider,
		model,
		runtime: alias.runtime,
		cli: alias.cli
	};
}
//#endregion
//#region src/commands/doctor/shared/legacy-config-migrations.runtime.agents.ts
const CHANNEL_HEARTBEAT_KEYS = /* @__PURE__ */ new Set([
	"showOk",
	"showAlerts",
	"useIndicator"
]);
const LEGACY_MEMORY_SEARCH_FIELD_MAPPINGS = [
	{
		legacyKey: "chunkSize",
		parentKey: "chunking",
		canonicalKey: "tokens"
	},
	{
		legacyKey: "chunkOverlap",
		parentKey: "chunking",
		canonicalKey: "overlap"
	},
	{
		legacyKey: "maxResults",
		parentKey: "query",
		canonicalKey: "maxResults"
	}
];
const MEMORY_SEARCH_RULE = {
	path: ["memorySearch"],
	message: "top-level memorySearch was moved; use memory.search instead. Run \"openclaw doctor --fix\"."
};
const AGENT_MEMORY_SEARCH_OWNER_RULES = [{
	path: [
		"agents",
		"defaults",
		"memorySearch"
	],
	message: "agents.defaults.memorySearch moved to memory.search. Run \"openclaw doctor --fix\"."
}, {
	path: ["agents"],
	message: "agents.entries.*.memorySearch moved to agents.entries.*.memory.search. Run \"openclaw doctor --fix\".",
	match: (value) => someAgentEntry(value, (agent) => agent.memorySearch !== void 0)
}];
const LEGACY_MEMORY_SEARCH_AUTO_PROVIDER_RULES = [
	{
		path: ["memorySearch", "provider"],
		message: "memorySearch.provider = \"auto\" is legacy; use \"openai\" explicitly. Run \"openclaw doctor --fix\".",
		match: isLegacyMemorySearchAutoProvider
	},
	{
		path: [
			"memory",
			"search",
			"provider"
		],
		message: "memory.search.provider = \"auto\" is legacy; use \"openai\" explicitly. Run \"openclaw doctor --fix\".",
		match: isLegacyMemorySearchAutoProvider
	},
	{
		path: ["agents"],
		message: "agents.entries.*.memorySearch.provider = \"auto\" is legacy; use \"openai\" explicitly. Run \"openclaw doctor --fix\".",
		match: (value) => someAgentEntry(value, (agent) => isLegacyMemorySearchAutoProvider(getAgentMemorySearchRecord(agent)?.provider))
	}
];
const LEGACY_MEMORY_SEARCH_STORE_PATH_RULES = [
	{
		path: [
			"memorySearch",
			"store",
			"path"
		],
		message: "memorySearch.store.path is legacy; memory indexes now live in each agent database. Run \"openclaw doctor --fix\"."
	},
	{
		path: [
			"memory",
			"search",
			"store",
			"path"
		],
		message: "memory.search.store.path is legacy; memory indexes now live in each agent database. Run \"openclaw doctor --fix\"."
	},
	{
		path: ["agents"],
		message: "agents.entries.*.memorySearch.store.path is legacy; memory indexes now live in each agent database. Run \"openclaw doctor --fix\".",
		match: (value) => someAgentEntry(value, (agent) => hasMemorySearchStorePath(getAgentMemorySearchRecord(agent)))
	}
];
const LEGACY_MEMORY_SEARCH_FLAT_KEY_RULES = [{
	path: ["memory", "search"],
	message: "memory.search uses legacy flat chunkSize, chunkOverlap, or maxResults fields. Run \"openclaw doctor --fix\".",
	match: hasLegacyMemorySearchFlatKeys
}, {
	path: ["agents"],
	message: "agents.entries.*.memorySearch uses legacy flat chunkSize, chunkOverlap, or maxResults fields. Run \"openclaw doctor --fix\".",
	match: (value) => someAgentEntry(value, (agent) => hasLegacyMemorySearchFlatKeys(getAgentMemorySearchRecord(agent)))
}];
function hasLegacyMemorySearchFlatKeys(value) {
	const memorySearch = getRecord(value);
	return Boolean(memorySearch && LEGACY_MEMORY_SEARCH_FIELD_MAPPINGS.some(({ legacyKey }) => Object.hasOwn(memorySearch, legacyKey)));
}
function getAgentMemorySearchRecord(agent) {
	return getRecord(agent.memorySearch) ?? getRecord(getRecord(agent.memory)?.search);
}
function someAgentEntry(value, predicate) {
	let matched = false;
	visitAgentEntries({ agents: value }, (agent) => {
		matched ||= predicate(agent);
	});
	return matched;
}
const HEARTBEAT_RULE = {
	path: ["heartbeat"],
	message: "top-level heartbeat is not a valid config path; use agents.defaults.heartbeat (cadence/target/model settings) or channels.defaults.heartbeat (showOk/showAlerts/useIndicator)."
};
const LEGACY_SANDBOX_SCOPE_RULES = [{
	path: [
		"agents",
		"defaults",
		"sandbox"
	],
	message: "agents.defaults.sandbox.perSession is legacy; use agents.defaults.sandbox.scope instead. Run \"openclaw doctor --fix\".",
	match: (value) => hasLegacySandboxPerSession(value)
}, {
	path: ["agents"],
	message: "agents.entries.*.sandbox.perSession is legacy; use agents.entries.*.sandbox.scope instead. Run \"openclaw doctor --fix\".",
	match: (value) => someAgentEntry(value, (agent) => hasLegacySandboxPerSession(agent.sandbox))
}];
const UNSUPPORTED_SANDBOX_BROWSER_NETWORK_RULES = [{
	path: [
		"agents",
		"defaults",
		"sandbox",
		"browser",
		"network"
	],
	message: "agents.defaults.sandbox.browser.network = \"none\" cannot expose the browser control port. Run \"openclaw doctor --fix\" to disable the sidecar and restore the dedicated browser network.",
	match: isUnsupportedSandboxBrowserNetwork
}, {
	path: ["agents"],
	message: "agents.entries.*.sandbox.browser.network = \"none\" cannot expose the browser control port. Run \"openclaw doctor --fix\" to disable the affected sidecar and restore the dedicated browser network.",
	match: (value) => someAgentEntry(value, (agent) => isUnsupportedSandboxBrowserNetwork(getSandboxBrowserConfig(agent)?.network))
}];
const LEGACY_AGENT_RUNTIME_POLICY_RULES = [
	{
		path: [
			"agents",
			"defaults",
			"agentRuntime",
			"fallback"
		],
		message: "agents.defaults.agentRuntime is ignored; set models.providers.<provider>.agentRuntime or a model-scoped agentRuntime instead. Run \"openclaw doctor --fix\"."
	},
	{
		path: [
			"agents",
			"defaults",
			"embeddedHarness"
		],
		message: "agents.defaults.embeddedHarness is legacy and ignored; set provider/model runtime policy instead. Run \"openclaw doctor --fix\".",
		match: (value) => getRecord(value) !== null
	},
	{
		path: [
			"agents",
			"defaults",
			"agentRuntime"
		],
		message: "agents.defaults.agentRuntime is ignored; set models.providers.<provider>.agentRuntime or a model-scoped agentRuntime instead. Run \"openclaw doctor --fix\".",
		match: (value) => getRecord(value) !== null
	},
	{
		path: ["agents"],
		message: "agents.entries.*.agentRuntime is ignored; set provider/model runtime policy instead. Run \"openclaw doctor --fix\".",
		match: (value) => someAgentEntry(value, (agent) => getRecord(agent.agentRuntime) !== null)
	},
	{
		path: ["agents"],
		message: "agents.entries.*.embeddedHarness is legacy and ignored; set provider/model runtime policy instead. Run \"openclaw doctor --fix\".",
		match: (value) => someAgentEntry(value, (agent) => getRecord(agent.embeddedHarness) !== null)
	}
];
const DEPRECATED_EMBEDDED_AGENT_KEY_RULES = [{
	path: [
		"agents",
		"defaults",
		"embeddedPi"
	],
	message: "agents.defaults.embeddedPi is legacy; use agents.defaults.embeddedAgent instead. Run \"openclaw doctor --fix\".",
	match: (value) => getRecord(value) !== null
}, {
	path: ["agents"],
	message: "agents.entries.*.embeddedPi is legacy; use agents.entries.*.embeddedAgent instead. Run \"openclaw doctor --fix\".",
	match: (value) => someAgentEntry(value, (agent) => getRecord(agent.embeddedPi) !== null)
}];
const LEGACY_AGENT_LLM_TIMEOUT_RULES = [{
	path: [
		"agents",
		"defaults",
		"llm"
	],
	message: "agents.defaults.llm is legacy; use models.providers.<id>.timeoutSeconds for slow model/provider timeouts. Run \"openclaw doctor --fix\".",
	match: (value) => getRecord(value) !== null
}];
const IGNORED_AGENT_MODEL_TIMEOUT_RULES = [
	{
		path: [
			"agents",
			"defaults",
			"model"
		],
		message: "agents.defaults.model.timeoutMs is ignored; agent model config only selects primary/fallback models. Run \"openclaw doctor --fix\" to remove it.",
		match: (value) => hasOwnTimeoutMs(value)
	},
	{
		path: [
			"agents",
			"defaults",
			"subagents",
			"model"
		],
		message: "agents.defaults.subagents.model.timeoutMs is ignored; subagent model config only selects primary/fallback models. Run \"openclaw doctor --fix\" to remove it.",
		match: (value) => hasOwnTimeoutMs(value)
	},
	{
		path: ["agents"],
		message: "agents.entries.*.model.timeoutMs and agents.entries.*.subagents.model.timeoutMs are ignored; agent model config only selects primary/fallback models. Run \"openclaw doctor --fix\" to remove them.",
		match: (value) => someAgentEntry(value, (agent) => hasOwnTimeoutMs(agent.model) || hasOwnTimeoutMs(getRecord(agent.subagents)?.model))
	}
];
const PROFILE_CONFIGURED_TOOL_SECTION_RULES = [{
	path: ["tools"],
	message: "tools.profile filters explicit configured-section tool grants; run \"openclaw doctor --fix\" to rewrite the explicit grants into a valid allowlist.",
	match: (value) => toolProfileConfiguredSectionsNeedExplicitRepair(value)
}, {
	path: ["agents"],
	message: "agents.entries.*.tools.profile filters explicit configured-section tool grants; run \"openclaw doctor --fix\" to rewrite the explicit grants into a valid allowlist.",
	match: (value, root) => {
		const globalTools = getRecord(root.tools);
		const inheritedProfile = typeof globalTools?.profile === "string" ? globalTools.profile : void 0;
		const inheritedAlsoAllow = readToolPolicyGrantList(globalTools, "alsoAllow");
		return someAgentEntry(value, (agent) => {
			const agentTools = getRecord(agent.tools);
			return toolProfileConfiguredSectionsNeedExplicitRepair(agentTools, inheritedProfile, inheritedAlsoAllow, collectEffectiveConfiguredToolSectionGrants(globalTools, agentTools), getRecord(globalTools?.byProvider));
		});
	}
}];
const SILENT_REPLY_LEGACY_RULES = [
	{
		path: [
			"agents",
			"defaults",
			"silentReplyRewrite"
		],
		message: "agents.defaults.silentReplyRewrite was removed; exact NO_REPLY is no longer rewritten to visible fallback text. Run \"openclaw doctor --fix\" to remove it."
	},
	{
		path: [
			"agents",
			"defaults",
			"silentReply"
		],
		message: "agents.defaults.silentReply.direct was removed; direct chats never receive NO_REPLY prompt guidance. Run \"openclaw doctor --fix\" to remove it.",
		match: (value) => Object.hasOwn(getRecord(value) ?? {}, "direct")
	},
	{
		path: ["surfaces"],
		message: "surfaces.*.silentReplyRewrite was removed; exact NO_REPLY is no longer rewritten to visible fallback text. Run \"openclaw doctor --fix\" to remove it.",
		match: (value) => hasSurfaceSilentReplyRewrite(value)
	},
	{
		path: ["surfaces"],
		message: "surfaces.*.silentReply.direct was removed; direct chats never receive NO_REPLY prompt guidance. Run \"openclaw doctor --fix\" to remove it.",
		match: (value) => hasSurfaceSilentReplyDirect(value)
	}
];
const SYSTEM_PROMPT_OVERRIDE_LEGACY_RULES = [{
	path: [
		"agents",
		"defaults",
		"systemPromptOverride"
	],
	message: "agents.defaults.systemPromptOverride was removed; OpenClaw owns the generated system prompt. Run \"openclaw doctor --fix\" to remove it."
}, {
	path: ["agents"],
	message: "agents.entries.*.systemPromptOverride was removed; OpenClaw owns the generated system prompt. Run \"openclaw doctor --fix\" to remove it.",
	match: (value) => someAgentEntry(value, (agent) => Object.hasOwn(agent, "systemPromptOverride"))
}];
function splitLegacyHeartbeat(legacyHeartbeat) {
	const agentHeartbeat = {};
	const channelHeartbeat = {};
	for (const [key, value] of Object.entries(legacyHeartbeat)) {
		if (isBlockedObjectKey(key)) continue;
		if (CHANNEL_HEARTBEAT_KEYS.has(key)) {
			channelHeartbeat[key] = value;
			continue;
		}
		agentHeartbeat[key] = value;
	}
	return {
		agentHeartbeat: Object.keys(agentHeartbeat).length > 0 ? agentHeartbeat : null,
		channelHeartbeat: Object.keys(channelHeartbeat).length > 0 ? channelHeartbeat : null
	};
}
function mergeLegacyIntoDefaults(params) {
	const root = ensureRecord(params.raw, params.rootKey);
	const defaults = ensureRecord(root, "defaults");
	const existing = getRecord(defaults[params.fieldKey]);
	if (!existing) {
		defaults[params.fieldKey] = params.legacyValue;
		params.changes.push(params.movedMessage);
	} else {
		const merged = structuredClone(existing);
		mergeMissing(merged, params.legacyValue);
		defaults[params.fieldKey] = merged;
		params.changes.push(params.mergedMessage);
	}
	root.defaults = defaults;
	params.raw[params.rootKey] = root;
}
function hasLegacySandboxPerSession(value) {
	const sandbox = getRecord(value);
	return Boolean(sandbox && Object.hasOwn(sandbox, "perSession"));
}
function hasOwnTimeoutMs(value) {
	const record = getRecord(value);
	return Boolean(record && Object.hasOwn(record, "timeoutMs"));
}
function migrateLegacyEmbeddedAgentKey(container, pathLabel, changes) {
	const legacy = getRecord(container.embeddedPi);
	if (!legacy) return;
	const existing = getRecord(container.embeddedAgent);
	if (!existing) {
		container.embeddedAgent = legacy;
		changes.push(`Moved ${pathLabel}.embeddedPi → ${pathLabel}.embeddedAgent.`);
	} else {
		const merged = structuredClone(existing);
		mergeMissing(merged, legacy);
		container.embeddedAgent = merged;
		changes.push(`Merged ${pathLabel}.embeddedPi → ${pathLabel}.embeddedAgent (filled missing fields from legacy; kept explicit embeddedAgent values).`);
	}
	delete container.embeddedPi;
}
function isLegacyMemorySearchAutoProvider(value) {
	return typeof value === "string" && value.trim().toLowerCase() === "auto";
}
function hasMemorySearchStorePath(value) {
	return typeof getRecord(getRecord(value)?.store)?.path === "string";
}
function migrateLegacyMemorySearchFlatKeys(memorySearch, pathLabel, changes) {
	if (!memorySearch) return;
	for (const { legacyKey, parentKey, canonicalKey } of LEGACY_MEMORY_SEARCH_FIELD_MAPPINGS) {
		if (!Object.hasOwn(memorySearch, legacyKey)) continue;
		const legacyValue = memorySearch[legacyKey];
		if (memorySearch[parentKey] === void 0) {
			memorySearch[parentKey] = { [canonicalKey]: legacyValue };
			changes.push(`Moved ${pathLabel}.${legacyKey} → ${pathLabel}.${parentKey}.${canonicalKey}.`);
			delete memorySearch[legacyKey];
			continue;
		}
		const canonicalParent = getRecord(memorySearch[parentKey]);
		if (!canonicalParent) changes.push(`Removed ${pathLabel}.${legacyKey} (${pathLabel}.${parentKey} already set).`);
		else if (canonicalParent[canonicalKey] === void 0) {
			canonicalParent[canonicalKey] = legacyValue;
			changes.push(`Moved ${pathLabel}.${legacyKey} → ${pathLabel}.${parentKey}.${canonicalKey}.`);
		} else changes.push(`Removed ${pathLabel}.${legacyKey} (${pathLabel}.${parentKey}.${canonicalKey} already set).`);
		delete memorySearch[legacyKey];
	}
}
function removeLegacyMemorySearchStorePath(memorySearch, pathLabel, changes) {
	const store = getRecord(memorySearch?.store);
	if (!store || typeof store.path !== "string") return;
	delete store.path;
	changes.push(`Removed ${pathLabel}.store.path; memory indexes now use each agent database.`);
}
function rewriteLegacyMemorySearchAutoProvider(memorySearch, pathLabel, changes) {
	if (!memorySearch || !isLegacyMemorySearchAutoProvider(memorySearch.provider)) return;
	memorySearch.provider = "openai";
	changes.push(`Moved ${pathLabel}.provider from legacy "auto" to "openai".`);
}
function migrateCanonicalMemorySearches(raw, changes, migrateMemorySearch) {
	migrateMemorySearch(getRecord(getRecord(raw.memory)?.search), "memory.search", changes);
	visitAgentEntries(raw, (agent, path) => {
		migrateMemorySearch(getRecord(getRecord(agent.memory)?.search), `${path}.memory.search`, changes);
	});
}
function migrateLegacySandboxPerSession(sandbox, pathLabel, changes) {
	if (!Object.hasOwn(sandbox, "perSession")) return;
	const rawPerSession = sandbox.perSession;
	if (typeof rawPerSession !== "boolean") return;
	if (sandbox.scope === void 0) {
		sandbox.scope = rawPerSession ? "session" : "shared";
		changes.push(`Moved ${pathLabel}.perSession → ${pathLabel}.scope (${String(sandbox.scope)}).`);
	} else changes.push(`Removed ${pathLabel}.perSession (${pathLabel}.scope already set).`);
	delete sandbox.perSession;
}
function getSandboxBrowserConfig(container) {
	return getRecord(getRecord(getRecord(container)?.sandbox)?.browser);
}
function isUnsupportedSandboxBrowserNetwork(value) {
	return normalizeOptionalLowercaseString(value) === "none";
}
function migrateExplicitUnsupportedSandboxBrowserNetwork(browser, pathLabel, changes) {
	if (!isUnsupportedSandboxBrowserNetwork(browser.network)) return;
	browser.enabled = false;
	browser.network = DEFAULT_SANDBOX_BROWSER_NETWORK;
	changes.push(`Disabled ${pathLabel} and moved its unsupported network "none" → "${DEFAULT_SANDBOX_BROWSER_NETWORK}".`);
}
function migrateAgentBrowserInheritedFromUnsupportedDefault(params) {
	const browser = getSandboxBrowserConfig(params.agent);
	if (!browser) return;
	const hasExplicitNetwork = typeof browser.network === "string";
	if (normalizeOptionalLowercaseString(browser.network) === "none") {
		migrateExplicitUnsupportedSandboxBrowserNetwork(browser, params.pathLabel, params.changes);
		return;
	}
	if (!hasExplicitNetwork && browser.enabled === true) {
		browser.enabled = false;
		params.changes.push(`Disabled ${params.pathLabel} because it inherited unsupported browser network "none".`);
		return;
	}
	if (hasExplicitNetwork && browser.enabled === void 0 && params.defaultBrowserEnabled) {
		browser.enabled = true;
		params.changes.push(`Set ${params.pathLabel}.enabled to true to preserve its explicit supported network while disabling the unsupported default browser network.`);
	}
}
function migrateUnsupportedSandboxBrowserNetworks(raw, changes) {
	const agents = getRecord(raw.agents);
	const defaultBrowser = getSandboxBrowserConfig(getRecord(agents?.defaults));
	const defaultNetworkUnsupported = isUnsupportedSandboxBrowserNetwork(defaultBrowser?.network);
	const defaultBrowserEnabled = defaultBrowser?.enabled === true;
	visitAgentEntries(raw, (agent, path) => {
		const pathLabel = `${path}.sandbox.browser`;
		if (defaultNetworkUnsupported) {
			migrateAgentBrowserInheritedFromUnsupportedDefault({
				agent,
				pathLabel,
				defaultBrowserEnabled,
				changes
			});
			return;
		}
		const browser = getSandboxBrowserConfig(agent);
		if (browser) migrateExplicitUnsupportedSandboxBrowserNetwork(browser, pathLabel, changes);
	});
	if (defaultBrowser) migrateExplicitUnsupportedSandboxBrowserNetwork(defaultBrowser, "agents.defaults.sandbox.browser", changes);
}
function removeLegacyAgentRuntimePolicy(container, pathLabel, changes) {
	if (getRecord(container.embeddedHarness) !== null) {
		delete container.embeddedHarness;
		changes.push(`Removed ${pathLabel}.embeddedHarness; runtime is now provider/model scoped.`);
	}
	if (getRecord(container.agentRuntime) !== null) {
		preserveLegacyWholeAgentRuntimePolicy(container, pathLabel, changes);
		delete container.agentRuntime;
		changes.push(`Removed ${pathLabel}.agentRuntime; runtime is now provider/model scoped.`);
	}
}
function resolveLegacyAgentRuntimeIntent(raw) {
	const record = getRecord(raw);
	if (!record) return;
	const runtime = typeof record.id === "string" ? record.id.trim().toLowerCase() : "";
	if (!runtime || runtime === "auto" || runtime === "openclaw") return;
	const alias = listLegacyRuntimeModelProviderAliases().find((entry) => entry.cli && normalizeProviderId(entry.runtime) === runtime);
	return alias ? {
		provider: alias.provider,
		runtime: alias.runtime
	} : void 0;
}
function preserveLegacyWholeAgentRuntimePolicy(container, pathLabel, changes) {
	const intent = resolveLegacyAgentRuntimeIntent(container.agentRuntime);
	if (!intent) return;
	const selectedRefs = selectedCanonicalModelRefsForRuntimePolicy(container.model, intent.provider);
	if (selectedRefs.length === 0) return;
	const currentModels = getRecord(container.models);
	const nextModels = currentModels ? { ...currentModels } : {};
	let changed = false;
	for (const ref of selectedRefs) {
		const updated = modelEntryWithRuntimePolicy(nextModels[ref], intent.runtime);
		if (!updated.changed) continue;
		nextModels[ref] = updated.entry;
		changed = true;
	}
	if (!changed) return;
	container.models = nextModels;
	changes.push(`Moved ${pathLabel}.agentRuntime.id ${intent.runtime} to matching ${intent.provider} model runtime policy.`);
}
function removeIgnoredAgentModelTimeouts(agent, pathLabel, changes) {
	for (const [suffix, model] of [["model", agent.model], ["subagents.model", getRecord(agent.subagents)?.model]]) {
		const modelRecord = getRecord(model);
		if (!modelRecord || !Object.hasOwn(modelRecord, "timeoutMs")) continue;
		delete modelRecord.timeoutMs;
		changes.push(`Removed ${pathLabel}.${suffix}.timeoutMs; agent model config only selects models.`);
	}
}
function hasOwnRecordProperty(value, key) {
	const record = getRecord(value);
	return Boolean(record && Object.hasOwn(record, key));
}
function hasSurfaceSilentReplyRewrite(value) {
	const surfaces = getRecord(value);
	if (!surfaces) return false;
	return Object.entries(surfaces).some(([surfaceId, surface]) => !isBlockedObjectKey(surfaceId) && hasOwnRecordProperty(surface, "silentReplyRewrite"));
}
function hasSurfaceSilentReplyDirect(value) {
	const surfaces = getRecord(value);
	if (!surfaces) return false;
	return Object.values(surfaces).some((surface) => Object.hasOwn(getRecord(getRecord(surface)?.silentReply) ?? {}, "direct"));
}
function removeLegacySilentReplyConfig(raw, changes) {
	const defaults = getRecord(getRecord(raw.agents)?.defaults);
	const defaultSilentReply = getRecord(defaults?.silentReply);
	if (defaultSilentReply && Object.hasOwn(defaultSilentReply, "direct")) {
		delete defaultSilentReply.direct;
		changes.push("Removed agents.defaults.silentReply.direct; direct chats never use NO_REPLY.");
	}
	if (defaults && hasOwnRecordProperty(defaults, "silentReplyRewrite")) {
		delete defaults.silentReplyRewrite;
		changes.push("Removed agents.defaults.silentReplyRewrite.");
	}
	const surfaces = getRecord(raw.surfaces);
	if (!surfaces) return;
	for (const [surfaceId, surfaceValue] of Object.entries(surfaces)) {
		if (isBlockedObjectKey(surfaceId)) continue;
		const surface = getRecord(surfaceValue);
		if (!surface) continue;
		const silentReply = getRecord(surface.silentReply);
		if (silentReply && Object.hasOwn(silentReply, "direct")) {
			delete silentReply.direct;
			changes.push(`Removed surfaces.${surfaceId}.silentReply.direct; direct chats never use NO_REPLY.`);
		}
		if (hasOwnRecordProperty(surface, "silentReplyRewrite")) {
			delete surface.silentReplyRewrite;
			changes.push(`Removed surfaces.${surfaceId}.silentReplyRewrite.`);
		}
	}
}
function removeLegacySystemPromptOverride(raw, changes) {
	visitAgentConfigScopes(raw, (agent, path) => {
		if (Object.hasOwn(agent, "systemPromptOverride")) {
			delete agent.systemPromptOverride;
			changes.push(`Removed ${path}.systemPromptOverride.`);
		}
	});
}
const CONFIGURED_TOOL_SECTION_GRANTS = [{
	key: "exec",
	grants: ["exec", "process"]
}, {
	key: "fs",
	grants: [
		"read",
		"write",
		"edit"
	]
}];
function readToolPolicyGrantList(value, key) {
	return readOwnToolPolicyGrantList(value, key) ?? [];
}
function readOwnToolPolicyGrantList(value, key) {
	const tools = getRecord(value);
	return Array.isArray(tools?.[key]) ? tools[key].filter((entry) => typeof entry === "string") : void 0;
}
function resolveToolProfileForMigration(tools, inheritedProfile) {
	return typeof tools.profile === "string" ? tools.profile : inheritedProfile;
}
function collectProfileConfiguredSectionRepairGrants(params) {
	const tools = getRecord(params.value);
	if (!tools) return [];
	const profile = resolveToolProfileForMigration(tools, params.inheritedProfile);
	if (!profile || profile === "full") return [];
	const ownAllow = readToolPolicyGrantList(tools, "allow");
	if (ownAllow.length === 0) return [];
	const explicitAlsoAllow = readOwnToolPolicyGrantList(tools, "alsoAllow");
	const explicitPolicy = { allow: uniqueStrings([...ownAllow, ...explicitAlsoAllow ?? []]) };
	const profilePolicy = mergeAlsoAllowPolicy(resolveToolProfilePolicy(profile), explicitAlsoAllow ?? params.inheritedAlsoAllow ?? []);
	return uniqueStrings(params.configuredGrants.filter((toolName) => isToolAllowedByPolicyName(toolName, explicitPolicy) && (!isToolAllowedByPolicyName(toolName, profilePolicy) || (explicitAlsoAllow ? isToolAllowedByPolicyName(toolName, { allow: explicitAlsoAllow }) : false))));
}
function toolProfileConfiguredSectionsNeedExplicitRepair(value, inheritedProfile, inheritedAlsoAllow, configuredGrantsOverride, inheritedByProvider) {
	const tools = getRecord(value);
	if (!tools) return false;
	const configuredGrants = configuredGrantsOverride ?? collectConfiguredToolSectionGrants(tools);
	return collectProfileConfiguredSectionRepairGrants({
		value,
		inheritedProfile,
		inheritedAlsoAllow,
		configuredGrants
	}).length > 0 || byProviderToolProfilesNeedConfiguredSectionMigration(tools, configuredGrants, readOwnToolPolicyGrantList(tools, "alsoAllow") ?? inheritedAlsoAllow, inheritedByProvider);
}
function collectConfiguredToolSectionGrants(tools) {
	const grants = [];
	for (const section of CONFIGURED_TOOL_SECTION_GRANTS) if (getRecord(tools[section.key])) grants.push(...section.grants);
	return uniqueStrings(grants);
}
function collectEffectiveConfiguredToolSectionGrants(inheritedTools, tools) {
	const includeInheritedSections = typeof tools?.profile !== "string";
	return uniqueStrings([...includeInheritedSections && inheritedTools ? collectConfiguredToolSectionGrants(inheritedTools) : [], ...tools ? collectConfiguredToolSectionGrants(tools) : []]);
}
function resolveProfileBoundAllowGrants(params) {
	const explicitAlsoAllow = readOwnToolPolicyGrantList(params.tools, "alsoAllow");
	const profilePolicy = mergeAlsoAllowPolicy(resolveToolProfilePolicy(params.profile), explicitAlsoAllow ?? params.inheritedAlsoAllow ?? []);
	const profileAllow = expandToolGroups(profilePolicy?.allow);
	const coreAllow = profileAllow.includes("*") ? expandToolGroups(params.allow) : profileAllow.filter((toolName) => isToolAllowedByPolicyName(toolName, { allow: params.allow }));
	const pluginAllow = expandToolGroups(params.allow).filter((entry) => {
		if (entry === "*" || isKnownCoreToolId(entry)) return false;
		return !profileAllow.some((toolName) => isToolAllowedByPolicyName(toolName, { allow: [entry] }));
	});
	return uniqueStrings([
		...coreAllow,
		...pluginAllow,
		...params.configuredGrants
	]);
}
function byProviderToolProfilesNeedConfiguredSectionMigration(tools, configuredGrants, inheritedAlsoAllow, inheritedByProvider) {
	const byProvider = getRecord(tools.byProvider);
	if (Boolean(byProvider && Object.entries(byProvider).some(([providerKey, policy]) => {
		const inheritedProviderPolicy = resolveInheritedProviderPolicy(inheritedByProvider, providerKey);
		const inheritedProviderProfile = typeof inheritedProviderPolicy?.profile === "string" ? inheritedProviderPolicy.profile : void 0;
		if (!(typeof getRecord(policy)?.profile === "string" || Boolean(inheritedProviderProfile))) return false;
		return collectProfileConfiguredSectionRepairGrants({
			value: policy,
			inheritedProfile: inheritedProviderProfile,
			inheritedAlsoAllow: readOwnToolPolicyGrantList(inheritedProviderPolicy, "alsoAllow") ?? inheritedAlsoAllow,
			configuredGrants
		}).length > 0;
	}))) return true;
	const localConfiguredGrants = collectConfiguredToolSectionGrants(tools);
	if (localConfiguredGrants.length === 0) return false;
	const handledProviders = new Set(Object.keys(byProvider ?? {}).map((providerKey) => normalizeToolProviderPolicyKey(providerKey)));
	return listInheritedProviderPoliciesWithProfiles(inheritedByProvider).some((inheritedProvider) => !handledProviders.has(inheritedProvider.normalizedKey) && collectProfileConfiguredSectionRepairGrants({
		value: {},
		inheritedProfile: inheritedProvider.profile,
		inheritedAlsoAllow: readOwnToolPolicyGrantList(inheritedProvider.policy, "alsoAllow"),
		configuredGrants: localConfiguredGrants
	}).length > 0);
}
function addProfileConfiguredSectionGrants(value, pathLabel, changes, inheritedProfile, inheritedAlsoAllow, configuredGrantsOverride, materializeProfile = true) {
	const tools = getRecord(value);
	if (!tools || !materializeProfile) return;
	const profile = resolveToolProfileForMigration(tools, inheritedProfile);
	if (!profile) return;
	const repairGrants = collectProfileConfiguredSectionRepairGrants({
		value: tools,
		inheritedProfile,
		inheritedAlsoAllow,
		configuredGrants: configuredGrantsOverride ?? collectConfiguredToolSectionGrants(tools)
	});
	const allow = readToolPolicyGrantList(tools, "allow");
	if (repairGrants.length === 0 || allow.length === 0 || profile === "full") return;
	const ownAlsoAllow = readOwnToolPolicyGrantList(tools, "alsoAllow");
	tools.allow = resolveProfileBoundAllowGrants({
		tools,
		profile,
		allow: uniqueStrings([...allow, ...ownAlsoAllow ?? []]),
		inheritedAlsoAllow,
		configuredGrants: repairGrants
	});
	changes.push(`Replaced ${pathLabel}.allow entries with profile "${profile}" grants plus explicit configured-section grants.`);
	if (ownAlsoAllow) {
		delete tools.alsoAllow;
		changes.push(`Merged ${pathLabel}.alsoAllow into ${pathLabel}.allow.`);
	}
	tools.profile = "full";
	changes.push(`Set ${pathLabel}.profile to "full" so ${pathLabel}.allow controls explicit configured-section grants directly.`);
}
function addByProviderProfileConfiguredSectionGrants(value, pathLabel, changes, configuredGrantsOverride, inheritedProfile, inheritedByProvider) {
	const tools = getRecord(value);
	if (!tools) return;
	const configuredGrants = configuredGrantsOverride ?? collectConfiguredToolSectionGrants(tools);
	if (configuredGrants.length === 0) return;
	const byProvider = getRecord(tools.byProvider);
	const handledProviders = /* @__PURE__ */ new Set();
	for (const [providerKey, providerPolicy] of Object.entries(byProvider ?? {})) {
		if (isBlockedObjectKey(providerKey)) continue;
		addHandledProviderPolicyKey(handledProviders, providerKey);
		const inheritedProviderPolicy = resolveInheritedProviderPolicy(inheritedByProvider, providerKey);
		const ownsProviderProfile = typeof getRecord(providerPolicy)?.profile === "string";
		const inheritedProviderProfile = typeof inheritedProviderPolicy?.profile === "string" ? inheritedProviderPolicy.profile : void 0;
		const providerInheritedProfile = inheritedProviderProfile ?? inheritedProfile;
		const providerInheritedAlsoAllow = readOwnToolPolicyGrantList(inheritedProviderPolicy, "alsoAllow");
		addProfileConfiguredSectionGrants(providerPolicy, `${pathLabel}.byProvider.${providerKey}`, changes, providerInheritedProfile, providerInheritedAlsoAllow, configuredGrants, ownsProviderProfile || Boolean(inheritedProviderProfile));
	}
	const localConfiguredGrants = collectConfiguredToolSectionGrants(tools);
	if (localConfiguredGrants.length === 0) return;
	for (const inheritedProvider of listInheritedProviderPoliciesWithProfiles(inheritedByProvider)) {
		if (handledProviders.has(inheritedProvider.normalizedKey)) continue;
		const providerPolicy = {};
		const changeCount = changes.length;
		addProfileConfiguredSectionGrants(providerPolicy, `${pathLabel}.byProvider.${inheritedProvider.key}`, changes, inheritedProvider.profile, readOwnToolPolicyGrantList(inheritedProvider.policy, "alsoAllow"), localConfiguredGrants);
		if (changes.length > changeCount) {
			if (!getRecord(tools.byProvider)) tools.byProvider = {};
			getRecord(tools.byProvider)[inheritedProvider.key] = providerPolicy;
			addHandledProviderPolicyKey(handledProviders, inheritedProvider.normalizedKey);
		}
	}
}
function addHandledProviderPolicyKey(handledProviders, providerKey) {
	handledProviders.add(normalizeToolProviderPolicyKey(providerKey));
}
function buildInheritedProviderPolicyLookup(inheritedByProvider) {
	const lookup = /* @__PURE__ */ new Map();
	for (const [key, value] of Object.entries(inheritedByProvider ?? {})) {
		if (isBlockedObjectKey(key)) continue;
		const policy = getRecord(value);
		if (!policy) continue;
		const normalized = normalizeToolProviderPolicyKey(key);
		if (!normalized) continue;
		const canonical = isCanonicalToolProviderPolicyKey(key);
		const existing = lookup.get(normalized);
		if (!existing || canonical && !existing.canonical) lookup.set(normalized, {
			key,
			policy,
			canonical
		});
	}
	return lookup;
}
function resolveInheritedProviderPolicy(inheritedByProvider, providerKey) {
	const lookup = buildInheritedProviderPolicyLookup(inheritedByProvider);
	const normalized = normalizeToolProviderPolicyKey(providerKey);
	const slashIndex = normalized.indexOf("/");
	const candidates = slashIndex > 0 ? [normalized, normalized.slice(0, slashIndex)] : [normalized];
	for (const candidate of candidates) {
		const match = lookup.get(candidate);
		if (match) return match.policy;
	}
	return null;
}
function listInheritedProviderPoliciesWithProfiles(inheritedByProvider) {
	const entries = [];
	for (const [normalizedKey, match] of buildInheritedProviderPolicyLookup(inheritedByProvider)) {
		if (typeof match.policy.profile !== "string") continue;
		entries.push({
			key: match.key,
			normalizedKey,
			policy: match.policy,
			profile: match.policy.profile
		});
	}
	return entries;
}
function bindingMatchHasLegacyDmPeerKind(binding) {
	const match = getRecord(getRecord(binding)?.match);
	const peer = getRecord(match?.peer);
	return peer !== null && peer.kind === "dm";
}
/** Legacy config migration specs for agent/runtime-owned config keys. */
const LEGACY_CONFIG_MIGRATIONS_RUNTIME_AGENTS = [
	defineLegacyConfigMigration({
		id: "bindings.match.peer.kind.dm-to-direct",
		describe: "Move deprecated bindings match.peer.kind dm values to direct",
		legacyRules: [{
			path: ["bindings"],
			message: "bindings[].match.peer.kind uses the retired \"dm\" alias; use \"direct\". Run \"openclaw doctor --fix\".",
			match: (value) => Array.isArray(value) && value.some((binding) => bindingMatchHasLegacyDmPeerKind(binding))
		}],
		apply: (raw, changes) => {
			if (!Array.isArray(raw.bindings)) return;
			let migrated = 0;
			for (const binding of raw.bindings) {
				const match = getRecord(getRecord(binding)?.match);
				const peer = getRecord(match?.peer);
				if (peer !== null && peer.kind === "dm") {
					peer.kind = "direct";
					migrated += 1;
				}
			}
			if (migrated > 0) changes.push(`Moved deprecated bindings[].match.peer.kind "dm" → "direct" for ${migrated} binding${migrated === 1 ? "" : "s"}.`);
		}
	}),
	defineLegacyConfigMigration({
		id: "tools.profile-configured-sections-alsoAllow",
		describe: "Repair explicit configured-section tool grants filtered by profiles",
		legacyRules: PROFILE_CONFIGURED_TOOL_SECTION_RULES,
		apply: (raw, changes) => {
			const globalTools = getRecord(raw.tools);
			const inheritedProfile = typeof globalTools?.profile === "string" ? globalTools.profile : void 0;
			const inheritedAlsoAllow = readToolPolicyGrantList(globalTools, "alsoAllow");
			addProfileConfiguredSectionGrants(raw.tools, "tools", changes);
			addByProviderProfileConfiguredSectionGrants(raw.tools, "tools", changes, void 0, inheritedProfile);
			visitAgentEntries(raw, (agent, path) => {
				const agentTools = getRecord(agent.tools);
				const configuredGrants = collectEffectiveConfiguredToolSectionGrants(globalTools, agentTools);
				addProfileConfiguredSectionGrants(agentTools, `${path}.tools`, changes, inheritedProfile, inheritedAlsoAllow, configuredGrants);
				addByProviderProfileConfiguredSectionGrants(agentTools, `${path}.tools`, changes, configuredGrants, resolveToolProfileForMigration(agentTools ?? {}, inheritedProfile), getRecord(globalTools?.byProvider));
			});
		}
	}),
	defineLegacyConfigMigration({
		id: "silentReplyRewrite-removed",
		describe: "Remove legacy silent reply rewrite and direct-chat silent reply config",
		legacyRules: SILENT_REPLY_LEGACY_RULES,
		apply: removeLegacySilentReplyConfig
	}),
	defineLegacyConfigMigration({
		id: "agents.systemPromptOverride-removed",
		describe: "Remove legacy agent system prompt override config",
		legacyRules: SYSTEM_PROMPT_OVERRIDE_LEGACY_RULES,
		apply: removeLegacySystemPromptOverride
	}),
	defineLegacyConfigMigration({
		id: "agents.defaults.llm->models.providers.timeoutSeconds",
		describe: "Remove legacy agents.defaults.llm timeout config",
		legacyRules: LEGACY_AGENT_LLM_TIMEOUT_RULES,
		apply: (raw, changes) => {
			const defaults = getRecord(getRecord(raw.agents)?.defaults);
			if (!defaults || getRecord(defaults.llm) === null) return;
			delete defaults.llm;
			changes.push("Removed agents.defaults.llm; model idle timeout now follows models.providers.<id>.timeoutSeconds within the agent/run timeout ceiling.");
		}
	}),
	defineLegacyConfigMigration({
		id: "agents.model.timeoutMs-ignored",
		describe: "Remove ignored timeoutMs keys from agent model selection config",
		legacyRules: IGNORED_AGENT_MODEL_TIMEOUT_RULES,
		apply: (raw, changes) => visitAgentConfigScopes(raw, (agent, path) => removeIgnoredAgentModelTimeouts(agent, path, changes))
	}),
	defineLegacyConfigMigration({
		id: "agents.embeddedPi->embeddedAgent",
		describe: "Move legacy embedded agent config key to embeddedAgent",
		legacyRules: DEPRECATED_EMBEDDED_AGENT_KEY_RULES,
		apply: (raw, changes) => visitAgentConfigScopes(raw, (agent, path) => migrateLegacyEmbeddedAgentKey(agent, path, changes))
	}),
	defineLegacyConfigMigration({
		id: "agents.agentRuntime-ignored",
		describe: "Remove ignored agent-wide runtime policy",
		legacyRules: LEGACY_AGENT_RUNTIME_POLICY_RULES,
		apply: (raw, changes) => visitAgentConfigScopes(raw, (agent, path) => removeLegacyAgentRuntimePolicy(agent, path, changes))
	}),
	defineLegacyConfigMigration({
		id: "agents.sandbox.perSession->scope",
		describe: "Move legacy agent sandbox perSession aliases to sandbox.scope",
		legacyRules: LEGACY_SANDBOX_SCOPE_RULES,
		apply: (raw, changes) => visitAgentConfigScopes(raw, (agent, pathLabel) => {
			const sandbox = getRecord(agent.sandbox);
			if (sandbox) migrateLegacySandboxPerSession(sandbox, `${pathLabel}.sandbox`, changes);
		})
	}),
	defineLegacyConfigMigration({
		id: "agents.sandbox.browser.network-none",
		describe: "Disable sandbox browser sidecars that use unsupported network mode none",
		legacyRules: UNSUPPORTED_SANDBOX_BROWSER_NETWORK_RULES,
		apply: migrateUnsupportedSandboxBrowserNetworks
	}),
	defineLegacyConfigMigration({
		id: "memorySearch->memory.search",
		describe: "Move memory search config to its canonical memory owner",
		legacyRules: [MEMORY_SEARCH_RULE, ...AGENT_MEMORY_SEARCH_OWNER_RULES],
		apply: (raw, changes) => {
			const agents = getRecord(raw.agents);
			const defaults = getRecord(agents?.defaults);
			const legacyDefaults = getRecord(defaults?.memorySearch);
			const legacyTopLevel = getRecord(raw.memorySearch);
			const memory = getRecord(raw.memory);
			const canonical = getRecord(memory?.search);
			if (legacyDefaults || legacyTopLevel) {
				const target = structuredClone(canonical ?? {});
				if (legacyDefaults) {
					mergeMissing(target, legacyDefaults);
					delete defaults.memorySearch;
				}
				if (legacyTopLevel) {
					mergeMissing(target, legacyTopLevel);
					delete raw.memorySearch;
				}
				ensureRecord(raw, "memory").search = target;
				changes.push(canonical ? "Merged legacy memorySearch defaults → memory.search (kept explicit memory.search values)." : "Moved legacy memorySearch defaults → memory.search.");
			}
			visitAgentEntries(raw, (agent, path) => {
				const legacy = getRecord(agent.memorySearch);
				if (!legacy) return;
				const agentMemory = ensureRecord(agent, "memory");
				const existing = getRecord(agentMemory.search);
				const target = structuredClone(existing ?? {});
				mergeMissing(target, legacy);
				agentMemory.search = target;
				delete agent.memorySearch;
				changes.push(existing ? `Merged ${path}.memorySearch → ${path}.memory.search (kept explicit memory.search values).` : `Moved ${path}.memorySearch → ${path}.memory.search.`);
			});
		}
	}),
	defineLegacyConfigMigration({
		id: "memorySearch.flat-fields->nested-fields",
		describe: "Move legacy flat memory search fields to canonical nested fields",
		legacyRules: LEGACY_MEMORY_SEARCH_FLAT_KEY_RULES,
		apply: (raw, changes) => migrateCanonicalMemorySearches(raw, changes, migrateLegacyMemorySearchFlatKeys)
	}),
	defineLegacyConfigMigration({
		id: "memorySearch.provider-auto->openai",
		describe: "Rewrite legacy memorySearch provider \"auto\" to \"openai\"",
		legacyRules: LEGACY_MEMORY_SEARCH_AUTO_PROVIDER_RULES,
		apply: (raw, changes) => migrateCanonicalMemorySearches(raw, changes, rewriteLegacyMemorySearchAutoProvider)
	}),
	defineLegacyConfigMigration({
		id: "memorySearch.store.path->agent-database",
		describe: "Remove legacy memory search sidecar index paths",
		legacyRules: LEGACY_MEMORY_SEARCH_STORE_PATH_RULES,
		apply: (raw, changes) => migrateCanonicalMemorySearches(raw, changes, removeLegacyMemorySearchStorePath)
	}),
	defineLegacyConfigMigration({
		id: "session.typingMode->agents.defaults.typingMode",
		describe: "Move session typing mode to agent defaults",
		legacyRules: [{
			path: ["session", "typingMode"],
			message: "session.typingMode moved to agents.defaults.typingMode. Run \"openclaw doctor --fix\"."
		}],
		apply: (raw, changes) => {
			const session = getRecord(raw.session);
			if (!session || !Object.hasOwn(session, "typingMode")) return;
			const defaults = ensureRecord(ensureRecord(raw, "agents"), "defaults");
			const replacedDefault = defaults.typingMode !== void 0;
			defaults.typingMode = session.typingMode;
			changes.push(replacedDefault ? "Moved session.typingMode → agents.defaults.typingMode (replaced the previously shadowed agent default)." : "Moved session.typingMode → agents.defaults.typingMode.");
			delete session.typingMode;
		}
	}),
	defineLegacyConfigMigration({
		id: "heartbeat->agents.defaults.heartbeat",
		describe: "Move top-level heartbeat to agents.defaults.heartbeat/channels.defaults.heartbeat",
		legacyRules: [HEARTBEAT_RULE],
		apply: (raw, changes) => {
			const legacyHeartbeat = getRecord(raw.heartbeat);
			if (!legacyHeartbeat) return;
			const { agentHeartbeat, channelHeartbeat } = splitLegacyHeartbeat(legacyHeartbeat);
			if (agentHeartbeat) mergeLegacyIntoDefaults({
				raw,
				rootKey: "agents",
				fieldKey: "heartbeat",
				legacyValue: agentHeartbeat,
				changes,
				movedMessage: "Moved heartbeat → agents.defaults.heartbeat.",
				mergedMessage: "Merged heartbeat → agents.defaults.heartbeat (filled missing fields from legacy; kept explicit agents.defaults values)."
			});
			if (channelHeartbeat) mergeLegacyIntoDefaults({
				raw,
				rootKey: "channels",
				fieldKey: "heartbeat",
				legacyValue: channelHeartbeat,
				changes,
				movedMessage: "Moved heartbeat visibility → channels.defaults.heartbeat.",
				mergedMessage: "Merged heartbeat visibility → channels.defaults.heartbeat (filled missing fields from legacy; kept explicit channels.defaults values)."
			});
			if (!agentHeartbeat && !channelHeartbeat) changes.push("Removed empty top-level heartbeat.");
			delete raw.heartbeat;
		}
	})
];
//#endregion
//#region src/commands/doctor/shared/legacy-config-migrations.runtime.cli-backends.ts
const CLI_BACKENDS_PLUGIN_GUIDE = "https://docs.openclaw.ai/plugins/cli-backend-plugins";
const LEGACY_CONFIG_MIGRATIONS_RUNTIME_CLI_BACKENDS = [defineLegacyConfigMigration({
	id: "agents.defaults.cliBackends-plugin-registration",
	describe: "Remove CLI backend adapter config now owned by plugins",
	legacyRules: [{
		path: [
			"agents",
			"defaults",
			"cliBackends"
		],
		message: `CLI backend adapters now register through plugins; see ${CLI_BACKENDS_PLUGIN_GUIDE}`
	}],
	apply: (raw, changes) => {
		const defaults = getRecord(getRecord(raw.agents)?.defaults);
		if (!defaults || !Object.hasOwn(defaults, "cliBackends")) return;
		delete defaults.cliBackends;
		changes.push(`Removed agents.defaults.cliBackends; CLI backend adapters now register through plugins (${CLI_BACKENDS_PLUGIN_GUIDE}).`);
	}
})];
const LEGACY_CONFIG_MIGRATIONS_RUNTIME_CRON = [defineLegacyConfigMigration({
	id: "cron.webhook-remove",
	describe: "Remove retired global cron webhook fallback",
	legacyRules: [{
		path: ["cron", "webhook"],
		message: "cron.webhook was retired after per-job delivery migration. Run \"openclaw doctor --fix\"."
	}],
	apply: (raw, changes) => {
		const cron = getRecord(raw.cron);
		if (!cron || !Object.hasOwn(cron, "webhook")) return;
		delete cron.webhook;
		changes.push("Removed retired cron.webhook after stored jobs migrated to per-job delivery.");
	}
}), defineLegacyConfigMigration({
	id: "cron.runLog-remove",
	describe: "Remove retired cron run-log retention config",
	legacyRules: [{
		path: ["cron", "runLog"],
		message: "cron.runLog is retired; run history now has fixed per-job retention. Run \"openclaw doctor --fix\"."
	}],
	apply: (raw, changes) => {
		const cron = getRecord(raw.cron);
		if (!cron || !Object.hasOwn(cron, "runLog")) return;
		delete cron.runLog;
		if (Object.keys(cron).length > 0) raw.cron = cron;
		else delete raw.cron;
		changes.push("Removed retired cron.runLog config; cron history now keeps 2000 runs per job.");
	}
})];
//#endregion
//#region src/commands/doctor/shared/legacy-config-migrations.runtime.diagnostics.ts
const UNSUPPORTED_OTEL_GRPC_PROTOCOL_RULE = {
	path: [
		"diagnostics",
		"otel",
		"protocol"
	],
	message: "diagnostics.otel.protocol = \"grpc\" is no longer accepted because gRPC export is not implemented. Run \"openclaw doctor --fix\", then configure an OTLP/HTTP collector before re-enabling telemetry.",
	match: (value) => value === "grpc"
};
function hasLegacyGrpcOtlpSignals(otel) {
	const logsExporter = typeof otel.logsExporter === "string" ? otel.logsExporter : void 0;
	return otel.traces !== false || otel.metrics !== false || otel.logs === true && logsExporter !== "stdout";
}
/** Legacy config migration specs for diagnostics runtime config. */
const LEGACY_CONFIG_MIGRATIONS_RUNTIME_DIAGNOSTICS = [defineLegacyConfigMigration({
	id: "diagnostics.otel.grpc-protocol",
	describe: "Remove unsupported diagnostics.otel.protocol grpc configs",
	legacyRules: [UNSUPPORTED_OTEL_GRPC_PROTOCOL_RULE],
	apply: (raw, changes, context) => {
		const otel = getRecord(getRecord(raw.diagnostics)?.otel);
		const resolvedRoot = getRecord(context?.resolvedRaw ?? raw);
		const resolvedOtel = getRecord(getRecord(resolvedRoot?.diagnostics)?.otel);
		if (!otel || resolvedOtel?.protocol !== "grpc") return;
		delete otel.protocol;
		changes.push("Removed unsupported diagnostics.otel.protocol \"grpc\"; use \"http/protobuf\" with an OTLP/HTTP collector.");
		if (resolvedOtel.enabled === true && hasLegacyGrpcOtlpSignals(resolvedOtel)) {
			otel.enabled = false;
			changes.push("Disabled diagnostics.otel.enabled because legacy grpc configs with OTLP signals cannot export telemetry; re-enable it after choosing an OTLP/HTTP collector.");
		}
	}
})];
//#endregion
//#region src/commands/doctor/shared/legacy-config-migrations.runtime.entries.ts
function migrateAgentEntries(raw, changes) {
	const agents = getRecord(raw.agents);
	if (!agents || !Array.isArray(agents.list)) return;
	if (getRecord(agents.entries)) {
		delete agents.list;
		changes.push("Removed agents.list because canonical agents.entries is already set.");
		return;
	}
	const projected = projectLegacyAgentRosterEntries(agents.list);
	changes.push(...projected.diagnostics);
	const orderedEntries = projected.entries.map(({ config }) => config);
	const workspace = resolveLegacyFirstAgentWorkspacePin(agents, orderedEntries);
	if (workspace !== void 0) orderedEntries[0].workspace = workspace;
	agents.entries = Object.fromEntries(projected.entries.map(({ id, config }) => [id, config]));
	delete agents.list;
	changes.push("Moved agents.list → keyed agents.entries.");
}
const LEGACY_CONFIG_MIGRATIONS_RUNTIME_ENTRIES = [defineLegacyConfigMigration({
	id: "runtime.agents-entries",
	describe: "Move agent arrays to keyed entries",
	legacyRules: [{
		path: ["agents", "list"],
		message: "agents.list moved to keyed agents.entries. Run \"openclaw doctor --fix\"."
	}],
	apply: migrateAgentEntries
})];
//#endregion
//#region src/commands/doctor/shared/legacy-config-migrations.runtime.gateway.ts
const GATEWAY_PORT_OOB_RULE = {
	path: ["gateway", "port"],
	message: "gateway.port is outside the valid TCP range (1–65535) and will be removed to avoid startup failure. Run \"openclaw doctor --fix\".",
	match: (value) => typeof value === "number" && (value < 1 || value > 65535)
};
const GATEWAY_BIND_RULE = {
	path: ["gateway", "bind"],
	message: "gateway.bind host aliases (for example 0.0.0.0/localhost) are legacy; use bind modes (lan/loopback/custom/tailnet/auto) instead. Run \"openclaw doctor --fix\".",
	match: (value) => isLegacyGatewayBindHostAlias(value),
	requireSourceLiteral: true
};
const GATEWAY_WEBCHAT_RULE = {
	path: ["gateway", "webchat"],
	message: "gateway.webchat is retired. Run \"openclaw doctor --fix\"."
};
const GATEWAY_TAILSCALE_RESET_ON_EXIT_RULE = {
	path: [
		"gateway",
		"tailscale",
		"resetOnExit"
	],
	message: "gateway.tailscale.resetOnExit is retired because managed routes now follow the Gateway lifecycle automatically. Run \"openclaw doctor --fix\".",
	match: (value) => typeof value === "boolean"
};
const GATEWAY_TAILSCALE_SERVICE_NAME_RULE = {
	path: [
		"gateway",
		"tailscale",
		"serviceName"
	],
	message: "gateway.tailscale.serviceName is retired because named Services require persistent background routes that cannot follow the Gateway lifecycle. Run \"openclaw doctor --fix\"."
};
const CONTROL_UI_DEVICE_AUTH_MIGRATION_RULE = {
	path: [
		"gateway",
		"controlUi",
		"dangerouslyDisableDeviceAuth"
	],
	message: "gateway.controlUi.dangerouslyDisableDeviceAuth is retired and ignored. Control UI browsers pair through the normal device flow; run \"openclaw doctor --fix\" to remove the legacy key.",
	match: (value) => typeof value === "boolean"
};
const CONTROL_UI_TOOL_TITLES_RULE = {
	path: [
		"gateway",
		"controlUi",
		"toolTitles"
	],
	message: "gateway.controlUi.toolTitles is retired. Tool activity uses agent-provided descriptions automatically, without utility-model calls. Run \"openclaw doctor --fix\" to remove it."
};
const LEGACY_GATEWAY_BIND_HOST_ALIASES = /* @__PURE__ */ new Map([
	["0.0.0.0", "lan"],
	["::", "lan"],
	["[::]", "lan"],
	["*", "lan"],
	["127.0.0.1", "loopback"],
	["localhost", "loopback"],
	["::1", "loopback"],
	["[::1]", "loopback"]
]);
function isLegacyGatewayBindHostAlias(value) {
	return normalizeLegacyGatewayBindHostAlias(value) !== null;
}
function normalizeLegacyGatewayBindHostAlias(value) {
	const normalized = normalizeOptionalLowercaseString(value);
	return normalized ? LEGACY_GATEWAY_BIND_HOST_ALIASES.get(normalized) ?? null : null;
}
function escapeControlForLog(value) {
	return value.replace(/\r/g, "\\r").replace(/\n/g, "\\n").replace(/\t/g, "\\t");
}
/** Legacy config migration specs for gateway runtime config. */
const LEGACY_CONFIG_MIGRATIONS_RUNTIME_GATEWAY = [
	defineLegacyConfigMigration({
		id: "gateway.control-ui-tool-titles-remove",
		describe: "Remove the retired Control UI tool-title preference",
		legacyRules: [CONTROL_UI_TOOL_TITLES_RULE],
		apply: (raw, changes) => {
			const controlUi = getRecord(getRecord(raw.gateway)?.controlUi);
			if (!controlUi || !Object.hasOwn(controlUi, "toolTitles")) return;
			delete controlUi.toolTitles;
			changes.push("Removed retired gateway.controlUi.toolTitles; tool activity descriptions are automatic and make no utility-model calls.");
		}
	}),
	defineLegacyConfigMigration({
		id: "gateway.tailscale.service-name-remove",
		describe: "Disable managed ingress and remove the retired Tailscale Service name",
		legacyRules: [GATEWAY_TAILSCALE_SERVICE_NAME_RULE],
		apply: (raw, changes) => {
			const gateway = getRecord(raw.gateway);
			const tailscale = getRecord(gateway?.tailscale);
			if (!gateway || !tailscale || !Object.hasOwn(tailscale, "serviceName")) return;
			const wasManagedService = tailscale.mode === "serve";
			delete tailscale.serviceName;
			if (wasManagedService) tailscale.mode = "off";
			gateway.tailscale = tailscale;
			raw.gateway = gateway;
			changes.push(wasManagedService ? "Removed gateway.tailscale.serviceName and set gateway.tailscale.mode=off because named Services cannot use lifecycle-owned routes. Inspect the retained Service route, then run `tailscale serve clear <service-name>`; set gateway.tailscale.mode=serve to use device Serve instead." : "Removed retired gateway.tailscale.serviceName; the current Tailscale mode is unchanged because named Services applied only to Serve.");
		}
	}),
	defineLegacyConfigMigration({
		id: "gateway.tailscale.reset-on-exit-remove",
		describe: "Remove the retired Tailscale route-cleanup preference",
		legacyRules: [GATEWAY_TAILSCALE_RESET_ON_EXIT_RULE],
		apply: (raw, changes) => {
			const gateway = getRecord(raw.gateway);
			const tailscale = getRecord(gateway?.tailscale);
			if (!gateway || !tailscale || !Object.hasOwn(tailscale, "resetOnExit")) return;
			const cleanupWasEnabled = tailscale.resetOnExit === true;
			delete tailscale.resetOnExit;
			gateway.tailscale = tailscale;
			raw.gateway = gateway;
			changes.push(cleanupWasEnabled ? "Removed gateway.tailscale.resetOnExit; managed Tailscale routes now end automatically with the Gateway lifecycle." : "Removed retired gateway.tailscale.resetOnExit config.");
		}
	}),
	defineLegacyConfigMigration({
		id: "gateway.control-ui-device-auth-bypass->pairing-migration",
		describe: "Remove the retired Control UI device-auth bypass",
		legacyRules: [CONTROL_UI_DEVICE_AUTH_MIGRATION_RULE],
		apply: (raw, changes) => {
			const gateway = getRecord(raw.gateway);
			const controlUi = getRecord(gateway?.controlUi);
			if (!controlUi || !Object.hasOwn(controlUi, "dangerouslyDisableDeviceAuth")) return;
			delete controlUi.dangerouslyDisableDeviceAuth;
			changes.push("Removed retired gateway.controlUi.dangerouslyDisableDeviceAuth legacy config.");
		}
	}),
	defineLegacyConfigMigration({
		id: "gateway.webchat-remove",
		describe: "Remove retired WebChat gateway config",
		legacyRules: [GATEWAY_WEBCHAT_RULE],
		apply: (raw, changes) => {
			const gateway = getRecord(raw.gateway);
			if (!gateway || !Object.hasOwn(gateway, "webchat")) return;
			delete gateway.webchat;
			if (Object.keys(gateway).length > 0) raw.gateway = gateway;
			else delete raw.gateway;
			changes.push("Removed retired gateway.webchat config.");
		}
	}),
	defineLegacyConfigMigration({
		id: "gateway.port-oob-repair",
		describe: "Remove out-of-range gateway.port to avoid post-schema-tightening startup failures",
		legacyRules: [GATEWAY_PORT_OOB_RULE],
		apply: (raw, changes) => {
			const gateway = getRecord(raw.gateway);
			if (!gateway || !Object.hasOwn(gateway, "port")) return;
			const port = gateway.port;
			if (typeof port !== "number" || port >= 1 && port <= 65535) return;
			delete gateway.port;
			if (Object.keys(gateway).length > 0) raw.gateway = gateway;
			else delete raw.gateway;
			changes.push(`Removed out-of-range gateway.port (${String(port)}). Valid TCP ports are 1–65535; the gateway will use the default port ${DEFAULT_GATEWAY_PORT}.`);
		}
	}),
	defineLegacyConfigMigration({
		id: "gateway.controlUi.allowedOrigins-seed-for-non-loopback",
		describe: "Seed gateway.controlUi.allowedOrigins for existing non-loopback gateway installs",
		apply: (raw, changes) => {
			const gateway = getRecord(raw.gateway);
			if (!gateway) return;
			const bind = normalizeLegacyGatewayBindHostAlias(gateway.bind) ?? gateway.bind;
			if (!isGatewayNonLoopbackBindMode(bind)) return;
			const controlUi = getRecord(gateway.controlUi) ?? {};
			if (hasConfiguredControlUiAllowedOrigins({
				allowedOrigins: controlUi.allowedOrigins,
				dangerouslyAllowHostHeaderOriginFallback: controlUi.dangerouslyAllowHostHeaderOriginFallback
			})) return;
			const port = resolveGatewayPortWithDefault(gateway.port, DEFAULT_GATEWAY_PORT);
			const origins = buildDefaultControlUiAllowedOrigins({
				port,
				bind,
				customBindHost: typeof gateway.customBindHost === "string" ? gateway.customBindHost : void 0
			});
			gateway.controlUi = {
				...controlUi,
				allowedOrigins: origins
			};
			raw.gateway = gateway;
			changes.push(`Seeded gateway.controlUi.allowedOrigins ${JSON.stringify(origins)} for bind=${bind}. Required since v2026.2.26. Add other machine origins to gateway.controlUi.allowedOrigins if needed.`);
		}
	}),
	defineLegacyConfigMigration({
		id: "gateway.bind.host-alias->bind-mode",
		describe: "Normalize gateway.bind host aliases to supported bind modes",
		legacyRules: [GATEWAY_BIND_RULE],
		apply: (raw, changes) => {
			const gateway = getRecord(raw.gateway);
			if (!gateway) return;
			const bindRaw = gateway.bind;
			if (typeof bindRaw !== "string") return;
			const normalized = normalizeOptionalLowercaseString(bindRaw);
			if (!normalized) return;
			const mapped = normalizeLegacyGatewayBindHostAlias(bindRaw);
			if (!mapped || normalized === mapped) return;
			gateway.bind = mapped;
			raw.gateway = gateway;
			changes.push(`Normalized gateway.bind "${escapeControlForLog(bindRaw)}" → "${mapped}".`);
		}
	})
];
//#endregion
//#region src/commands/doctor/shared/legacy-config-migrations.runtime.mcp.ts
const MCP_SERVER_TYPE_RULE = {
	path: ["mcp", "servers"],
	message: "mcp.servers entries use OpenClaw transport names; CLI-native type aliases are legacy here. Run \"openclaw doctor --fix\".",
	match: (value) => isRecord(value) && Object.values(value).some((server) => isRecord(server) && isKnownCliMcpTypeAlias(server.type))
};
const MCP_SERVER_DISABLED_RULES = [["mcp", "servers"], [
	"nodeHost",
	"mcp",
	"servers"
]].map((path) => ({
	path,
	message: `${path.join(".")} entries use the unsupported "disabled" key; use "enabled" with the inverse boolean value. Run "openclaw doctor --fix" to migrate it.`,
	match: (value) => isRecord(value) && Object.values(value).some((server) => isRecord(server) && typeof server.disabled === "boolean")
}));
const MCP_SERVER_TIMEOUT_ALIASES_RULES = [["mcp", "servers"], [
	"nodeHost",
	"mcp",
	"servers"
]].map((path) => ({
	path,
	message: `${path.join(".")} timeout aliases were retired; use connectionTimeoutMs and requestTimeoutMs. Run "openclaw doctor --fix".`,
	match: (value) => isRecord(value) && Object.values(value).some((server) => isRecord(server) && [
		"connectTimeout",
		"connect_timeout",
		"timeout"
	].some((key) => Object.hasOwn(server, key)))
}));
function hasMcpServerLegacyAliases(server) {
	const codex = isRecord(server.codex) ? server.codex : void 0;
	return Object.hasOwn(server, "workingDirectory") || [
		"supports_parallel_tool_calls",
		"ssl_verify",
		"client_cert",
		"client_key"
	].some((key) => Object.hasOwn(server, key)) || Boolean(codex && Object.hasOwn(codex, "default_tools_approval_mode"));
}
const MCP_SERVER_ALIASES_RULES = [["mcp", "servers"], [
	"nodeHost",
	"mcp",
	"servers"
]].map((path) => ({
	path,
	message: `${path.join(".")} legacy aliases were retired; use camelCase spellings and cwd. Run "openclaw doctor --fix".`,
	match: (value) => isRecord(value) && Object.values(value).some((server) => isRecord(server) && hasMcpServerLegacyAliases(server))
}));
function migrateMcpServerAliases(servers, pathPrefix, changes) {
	if (!isRecord(servers)) return;
	for (const [serverName, value] of Object.entries(servers)) {
		if (!isRecord(value)) continue;
		if (!hasMcpServerLegacyAliases(value)) continue;
		const normalized = canonicalizeConfiguredMcpServer(value);
		if (JSON.stringify(normalized) === JSON.stringify(value)) continue;
		servers[serverName] = normalized;
		changes.push(`Canonicalized legacy aliases in ${pathPrefix}.${serverName}.`);
	}
}
function migrateMcpServerTimeoutAliases(servers, pathPrefix, changes) {
	if (!isRecord(servers)) return;
	for (const [serverName, server] of Object.entries(servers)) {
		if (!isRecord(server)) continue;
		for (const [alias, canonical] of [
			["connectTimeout", "connectionTimeoutMs"],
			["connect_timeout", "connectionTimeoutMs"],
			["timeout", "requestTimeoutMs"]
		]) {
			if (!Object.hasOwn(server, alias)) continue;
			const value = server[alias];
			if (server[canonical] === void 0 && typeof value === "number" && value > 0 && Number.isFinite(value * 1e3)) {
				server[canonical] = value * 1e3;
				changes.push(`Moved ${pathPrefix}.${serverName}.${alias} → ${canonical} (${value * 1e3} ms).`);
			} else changes.push(`Removed ${pathPrefix}.${serverName}.${alias} (${canonical} already set or alias invalid).`);
			delete server[alias];
		}
	}
}
function migrateMcpServerDisabledFlags(servers, pathPrefix, changes) {
	if (!isRecord(servers)) return;
	for (const [serverName, rawServer] of Object.entries(servers)) {
		if (!isRecord(rawServer) || typeof rawServer.disabled !== "boolean") continue;
		const disabled = rawServer.disabled;
		if (typeof rawServer.enabled !== "boolean") {
			rawServer.enabled = !disabled;
			changes.push(`Moved ${pathPrefix}.${serverName}.disabled ${disabled} → enabled ${!disabled}.`);
		} else changes.push(`Removed ${pathPrefix}.${serverName}.disabled ${disabled} because enabled is already set to ${rawServer.enabled}.`);
		delete rawServer.disabled;
	}
}
/** Legacy config migration specs for MCP server config compatibility. */
const LEGACY_CONFIG_MIGRATIONS_RUNTIME_MCP = [defineLegacyConfigMigration({
	id: "mcp.servers.canonicalize",
	describe: "Normalize legacy MCP server config",
	legacyRules: [
		...MCP_SERVER_DISABLED_RULES,
		MCP_SERVER_TYPE_RULE,
		...MCP_SERVER_TIMEOUT_ALIASES_RULES,
		...MCP_SERVER_ALIASES_RULES
	],
	apply: (raw, changes) => {
		const mcp = isRecord(raw.mcp) ? raw.mcp : void 0;
		migrateMcpServerDisabledFlags(mcp?.servers, "mcp.servers", changes);
		migrateMcpServerTimeoutAliases(mcp?.servers, "mcp.servers", changes);
		migrateMcpServerAliases(mcp?.servers, "mcp.servers", changes);
		const nodeHost = isRecord(raw.nodeHost) ? raw.nodeHost : void 0;
		const nodeHostMcp = isRecord(nodeHost?.mcp) ? nodeHost.mcp : void 0;
		migrateMcpServerDisabledFlags(nodeHostMcp?.servers, "nodeHost.mcp.servers", changes);
		migrateMcpServerTimeoutAliases(nodeHostMcp?.servers, "nodeHost.mcp.servers", changes);
		migrateMcpServerAliases(nodeHostMcp?.servers, "nodeHost.mcp.servers", changes);
		const servers = isRecord(mcp?.servers) ? mcp?.servers : void 0;
		if (!servers) return;
		for (const [serverName, rawServer] of Object.entries(servers)) {
			if (!isRecord(rawServer) || !isKnownCliMcpTypeAlias(rawServer.type)) continue;
			const rawType = typeof rawServer.type === "string" ? rawServer.type : "";
			const alias = resolveOpenClawMcpTransportAlias(rawServer.type);
			if (typeof rawServer.transport !== "string" && alias) {
				rawServer.transport = alias;
				changes.push(`Moved mcp.servers.${serverName}.type "${rawType}" → transport "${alias}".`);
			} else if (typeof rawServer.transport === "string") changes.push(`Removed mcp.servers.${serverName}.type (transport "${rawServer.transport}" already set).`);
			else changes.push(`Removed mcp.servers.${serverName}.type "${rawType}".`);
			delete rawServer.type;
		}
	}
})];
//#endregion
//#region src/commands/doctor/shared/legacy-web-tools-migrate.ts
const DANGEROUS_RECORD_KEYS = /* @__PURE__ */ new Set([
	"__proto__",
	"prototype",
	"constructor"
]);
const LEGACY_WEB_SEARCH_OWNERS = /* @__PURE__ */ new Map([
	["brave", "brave"],
	["duckduckgo", "duckduckgo"],
	["exa", "exa"],
	["firecrawl", "firecrawl"],
	["firecrawl-free", "firecrawl"],
	["gemini", "google"],
	["grok", "xai"],
	["kimi", "moonshot"],
	["minimax", "minimax"],
	["ollama", "ollama"],
	["parallel", "parallel"],
	["parallel-free", "parallel"],
	["perplexity", "perplexity"],
	["searxng", "searxng"],
	["tavily", "tavily"]
]);
const NON_MIGRATED_SEARCH_PROVIDERS = /* @__PURE__ */ new Set([
	"firecrawl-free",
	"parallel",
	"parallel-free",
	"tavily"
]);
const RETIRED_GROK_SEARCH_MODELS = /* @__PURE__ */ new Set([
	"grok-4-1-fast",
	"grok-4-1-fast-reasoning",
	"grok-4-fast",
	"grok-4-fast-reasoning",
	"grok-4-0709"
]);
const RETIRED_GROK_CODE_MODELS = /* @__PURE__ */ new Set([
	"grok-code-fast-1",
	"grok-code-fast",
	"grok-code-fast-1-0825"
]);
const RETIRED_X_SEARCH_MODELS = /* @__PURE__ */ new Set([
	"grok-4-1-fast-non-reasoning",
	"grok-4-fast-non-reasoning",
	"grok-3"
]);
function legacySearchProviderIds() {
	return [...LEGACY_WEB_SEARCH_OWNERS.keys()].filter((providerId) => !NON_MIGRATED_SEARCH_PROVIDERS.has(providerId)).toSorted((left, right) => left.localeCompare(right));
}
function resolveWebSlot(raw, slot) {
	if (!isRecord(raw) || !isRecord(raw.tools) || !isRecord(raw.tools.web)) return;
	const value = raw.tools.web[slot];
	return isRecord(value) ? value : void 0;
}
function retainedSource(source, removedRecordKeys) {
	const retained = {};
	for (const [key, value] of Object.entries(source)) {
		if (DANGEROUS_RECORD_KEYS.has(key) || removedRecordKeys.has(key) && isRecord(value)) continue;
		retained[key] = value;
	}
	return retained;
}
function applyPluginMove(root, move, changes) {
	const entries = ensureRecord(ensureRecord(root, "plugins"), "entries");
	const entry = ensureRecord(entries, move.pluginId);
	const activated = entry.enabled === void 0;
	if (activated) entry.enabled = true;
	const config = ensureRecord(entry, "config");
	const existingValue = config[move.configKey];
	const existingWasRecord = isRecord(existingValue);
	const existing = cloneRecord(existingWasRecord ? existingValue : void 0);
	if (!existingWasRecord) {
		config[move.configKey] = cloneRecord(move.payload);
		changes.push(`Moved ${move.legacyPath} → ${move.targetPath}.`);
	} else if (move.mergeMode === "own-api-key") {
		if (!hasOwnKey(existing, "apiKey")) {
			existing.apiKey = move.payload.apiKey;
			config[move.configKey] = existing;
			changes.push(`Merged ${move.legacyPath} → ${move.targetPath} (filled missing plugin auth).`);
		} else changes.push(`Removed ${move.legacyPath} (${move.targetPath} already set).`);
	} else {
		const merged = cloneRecord(existing);
		mergeMissing(merged, move.payload);
		config[move.configKey] = merged;
		if (JSON.stringify(merged) !== JSON.stringify(existing) || activated) changes.push(`Merged ${move.legacyPath} → ${move.targetPath} (filled missing fields from legacy; kept explicit plugin config values).`);
		else changes.push(`Removed ${move.legacyPath} (${move.targetPath} already set).`);
	}
	return activated;
}
function migrateLegacyWebSlot(raw, slot, prepare) {
	const source = resolveWebSlot(raw, slot);
	const prepared = source ? prepare(source) : null;
	if (!isRecord(raw) || !prepared) return {
		config: raw,
		changes: []
	};
	const nextRoot = structuredClone(raw);
	const web = ensureRecord(ensureRecord(nextRoot, "tools"), "web");
	if (prepared.deleteSource) delete web[slot];
	else web[slot] = prepared.retained;
	const changes = [];
	for (const step of prepared.steps) {
		if ("message" in step) {
			changes.push(step.message);
			continue;
		}
		if (applyPluginMove(nextRoot, step.move, changes) && step.move.activatedMessage) changes.push(step.move.activatedMessage);
	}
	return {
		config: nextRoot,
		changes
	};
}
function resolveGrokModelTarget(model, xSearch) {
	if (typeof model !== "string") return;
	const normalized = model.trim().toLowerCase();
	if ((xSearch ? RETIRED_X_SEARCH_MODELS : RETIRED_GROK_SEARCH_MODELS).has(normalized)) return "grok-4.3";
	return RETIRED_GROK_CODE_MODELS.has(normalized) ? "grok-build-0.1" : void 0;
}
function searchMove(providerId, payload, paths) {
	const pluginId = LEGACY_WEB_SEARCH_OWNERS.get(providerId) ?? providerId;
	return {
		pluginId,
		configKey: "webSearch",
		payload,
		legacyPath: paths?.legacyPath ?? `tools.web.search.${providerId}`,
		targetPath: paths?.targetPath ?? `plugins.entries.${pluginId}.config.webSearch`
	};
}
function prepareWebSearch(source) {
	const providerIds = legacySearchProviderIds();
	if (!hasOwnKey(source, "apiKey") && !providerIds.some((id) => isRecord(source[id]))) return null;
	const retained = retainedSource(source, /* @__PURE__ */ new Set(["apiKey", ...providerIds]));
	delete retained.apiKey;
	const steps = [];
	const braveRecord = isRecord(source.brave) ? cloneRecord(source.brave) : void 0;
	const bravePayload = cloneRecord(braveRecord);
	if (hasOwnKey(source, "apiKey")) bravePayload.apiKey = source.apiKey;
	if (Object.keys(bravePayload).length > 0) {
		const hasGlobalApiKey = hasOwnKey(source, "apiKey");
		steps.push({ move: searchMove("brave", bravePayload, hasGlobalApiKey ? {
			legacyPath: "tools.web.search.apiKey",
			targetPath: braveRecord ? "plugins.entries.brave.config.webSearch" : "plugins.entries.brave.config.webSearch.apiKey"
		} : void 0) });
	}
	for (const providerId of providerIds) {
		if (providerId === "brave" || !isRecord(source[providerId])) continue;
		const payload = cloneRecord(source[providerId]);
		if (Object.keys(payload).length === 0) continue;
		if (providerId === "grok") {
			const modelTarget = resolveGrokModelTarget(payload.model, false);
			if (modelTarget) {
				steps.push({ message: `Updated tools.web.search.grok.model from ${JSON.stringify(payload.model)} to ${JSON.stringify(modelTarget)}.` });
				payload.model = modelTarget;
			}
		}
		steps.push({ move: searchMove(providerId, payload) });
	}
	return {
		retained,
		steps
	};
}
function prepareWebFetch(source) {
	if (!isRecord(source.firecrawl)) return null;
	const payload = cloneRecord(source.firecrawl);
	delete payload.enabled;
	return {
		retained: retainedSource(source, /* @__PURE__ */ new Set(["firecrawl"])),
		steps: Object.keys(payload).length > 0 ? [{ move: {
			pluginId: "firecrawl",
			configKey: "webFetch",
			payload,
			legacyPath: "tools.web.fetch.firecrawl",
			targetPath: "plugins.entries.firecrawl.config.webFetch"
		} }] : [{ message: "Removed empty tools.web.fetch.firecrawl." }]
	};
}
/** Resolve a supported replacement for a retired legacy X search model. */
function resolveLegacyXSearchModelTarget(model) {
	return resolveGrokModelTarget(model, true);
}
function prepareXSearch(source) {
	const hasAuth = hasOwnKey(source, "apiKey");
	const modelTarget = resolveLegacyXSearchModelTarget(source.model);
	if (!hasAuth && !modelTarget) return null;
	const retained = cloneRecord(source);
	const steps = [];
	if (hasAuth) delete retained.apiKey;
	if (modelTarget) {
		steps.push({ message: `Updated tools.web.x_search.model from ${JSON.stringify(source.model)} to ${JSON.stringify(modelTarget)}.` });
		retained.model = modelTarget;
	}
	if (hasAuth) steps.push({ move: {
		pluginId: "xai",
		configKey: "webSearch",
		payload: { apiKey: source.apiKey },
		legacyPath: "tools.web.x_search.apiKey",
		targetPath: "plugins.entries.xai.config.webSearch.apiKey",
		mergeMode: "own-api-key",
		...Object.keys(retained).length === 0 ? { activatedMessage: "Removed empty tools.web.x_search." } : {}
	} });
	return {
		retained,
		deleteSource: Object.keys(retained).length === 0,
		steps
	};
}
/** List legacy tools.web.search provider config paths present in raw config. */
function listLegacyWebSearchConfigPaths(raw) {
	const source = resolveWebSlot(raw, "search");
	if (!source) return [];
	const paths = hasOwnKey(source, "apiKey") ? ["tools.web.search.apiKey"] : [];
	for (const providerId of legacySearchProviderIds()) if (isRecord(source[providerId])) paths.push(...Object.keys(source[providerId]).map((key) => `tools.web.search.${providerId}.${key}`));
	return paths;
}
/** Move legacy web-search provider config into provider plugin entries. */
function migrateLegacyWebSearchConfig(raw) {
	return migrateLegacyWebSlot(raw, "search", prepareWebSearch);
}
/** Move legacy Firecrawl web-fetch config into plugin-owned config. */
function migrateLegacyWebFetchConfig(raw) {
	return migrateLegacyWebSlot(raw, "fetch", prepareWebFetch);
}
/** Move legacy X search auth and repair retired legacy model defaults. */
function migrateLegacyXSearchConfig(raw) {
	return migrateLegacyWebSlot(raw, "x_search", prepareXSearch);
}
//#endregion
//#region src/commands/doctor/shared/legacy-config-migrations.runtime.providers.ts
const LEGACY_OPENAI_CODEX_PLUGIN_ID = "openai-codex";
const OPENAI_PLUGIN_ID = "openai";
const LEGACY_CODEX_SUPERVISOR_PLUGIN_ID = "codex-supervisor";
const CODEX_PLUGIN_ID = "codex";
function normalizePluginIdForMigration(value) {
	return typeof value === "string" ? value.trim().toLowerCase() : void 0;
}
const X_SEARCH_RULE = {
	path: [
		"tools",
		"web",
		"x_search",
		"apiKey"
	],
	message: "tools.web.x_search.apiKey moved to the xAI plugin; use plugins.entries.xai.config.webSearch.apiKey instead. Run \"openclaw doctor --fix\"."
};
const X_SEARCH_MODEL_RULE = {
	path: [
		"tools",
		"web",
		"x_search",
		"model"
	],
	message: "tools.web.x_search.model uses a retired xAI model; run \"openclaw doctor --fix\" to repair it.",
	requireSourceLiteral: true,
	match: (value) => resolveLegacyXSearchModelTarget(value) !== void 0
};
function rewritePluginIdList(value, legacyPluginId, replacementPluginId) {
	if (!Array.isArray(value)) return {
		next: value,
		changed: false
	};
	let changed = false;
	const seen = /* @__PURE__ */ new Set();
	const next = [];
	for (const entry of value) {
		const matchesLegacy = normalizePluginIdForMigration(entry) === legacyPluginId;
		if (matchesLegacy && replacementPluginId === void 0) {
			changed = true;
			continue;
		}
		const replacement = matchesLegacy ? replacementPluginId : entry;
		if (replacement !== entry) changed = true;
		if (typeof replacement === "string") {
			const normalizedReplacement = normalizePluginIdForMigration(replacement) ?? replacement;
			if (seen.has(normalizedReplacement)) {
				changed = true;
				continue;
			}
			seen.add(normalizedReplacement);
		}
		next.push(replacement);
	}
	return {
		next,
		changed
	};
}
function rewritePluginSlots(value) {
	if (!isRecord(value)) return false;
	let changed = false;
	for (const [slot, pluginId] of Object.entries(value)) if (pluginId === LEGACY_OPENAI_CODEX_PLUGIN_ID) {
		value[slot] = OPENAI_PLUGIN_ID;
		changed = true;
	}
	return changed;
}
function rewritePluginEntries(value) {
	if (!isRecord(value) || !(LEGACY_OPENAI_CODEX_PLUGIN_ID in value)) return false;
	if (!(OPENAI_PLUGIN_ID in value)) value[OPENAI_PLUGIN_ID] = value[LEGACY_OPENAI_CODEX_PLUGIN_ID];
	delete value[LEGACY_OPENAI_CODEX_PLUGIN_ID];
	return true;
}
function rewriteLegacyOpenAICodexPluginPolicy(raw) {
	const plugins = isRecord(raw.plugins) ? raw.plugins : void 0;
	if (!plugins) return [];
	const changes = [];
	for (const key of ["allow", "deny"]) {
		const rewritten = rewritePluginIdList(plugins[key], LEGACY_OPENAI_CODEX_PLUGIN_ID, OPENAI_PLUGIN_ID);
		if (rewritten.changed) {
			plugins[key] = rewritten.next;
			changes.push(`Rewrote plugins.${key} openai-codex references to openai.`);
		}
	}
	if (rewritePluginEntries(plugins.entries)) changes.push("Rewrote plugins.entries.openai-codex to plugins.entries.openai.");
	if (rewritePluginSlots(plugins.slots)) changes.push("Rewrote plugins.slots openai-codex references to openai.");
	return changes;
}
function migrateLegacyCodexSupervisorEntry(entries, legacySupervisorDenied) {
	const legacyEntryKey = Object.keys(entries).find((key) => normalizePluginIdForMigration(key) === LEGACY_CODEX_SUPERVISOR_PLUGIN_ID);
	if (!legacyEntryKey) return null;
	const rawLegacyEntry = entries[legacyEntryKey];
	if (!isRecord(rawLegacyEntry)) {
		delete entries[legacyEntryKey];
		return "removed-invalid";
	}
	const legacyEntry = rawLegacyEntry;
	const migratedEnabled = legacyEntry.enabled === true && !legacySupervisorDenied;
	const codexEntryKey = Object.keys(entries).find((key) => normalizePluginIdForMigration(key) === CODEX_PLUGIN_ID) ?? CODEX_PLUGIN_ID;
	const rawCodexEntry = entries[codexEntryKey];
	let codexEntry;
	if (isRecord(rawCodexEntry)) codexEntry = rawCodexEntry;
	else {
		codexEntry = {};
		entries[codexEntryKey] = codexEntry;
	}
	if (migratedEnabled && codexEntry.enabled === void 0) codexEntry.enabled = true;
	const codexConfig = isRecord(codexEntry.config) ? codexEntry.config : {};
	codexEntry.config = codexConfig;
	const supervision = isRecord(codexConfig.supervision) ? codexConfig.supervision : {};
	codexConfig.supervision = supervision;
	const legacyConfig = isRecord(legacyEntry.config) ? legacyEntry.config : void 0;
	const migratedSupervision = { enabled: migratedEnabled };
	if (Array.isArray(legacyConfig?.endpoints)) migratedSupervision.endpoints = legacyConfig.endpoints;
	if (typeof legacyConfig?.allowRawTranscripts === "boolean") migratedSupervision.allowRawTranscripts = legacyConfig.allowRawTranscripts;
	if (typeof legacyConfig?.allowWriteControls === "boolean") migratedSupervision.allowWriteControls = legacyConfig.allowWriteControls;
	mergeMissing(supervision, migratedSupervision);
	delete entries[legacyEntryKey];
	return "migrated";
}
function migrateLegacyCodexSupervisorPlugin(raw) {
	const plugins = isRecord(raw.plugins) ? raw.plugins : void 0;
	if (!plugins) return [];
	const changes = [];
	const legacySupervisorDenied = Array.isArray(plugins.deny) && plugins.deny.some((entry) => normalizePluginIdForMigration(entry) === LEGACY_CODEX_SUPERVISOR_PLUGIN_ID);
	const entries = isRecord(plugins.entries) ? plugins.entries : void 0;
	const entryMigration = entries ? migrateLegacyCodexSupervisorEntry(entries, legacySupervisorDenied) : null;
	if (entryMigration === "migrated") changes.push("Moved plugins.entries.codex-supervisor to plugins.entries.codex.config.supervision.");
	else if (entryMigration === "removed-invalid") changes.push("Removed invalid plugins.entries.codex-supervisor config.");
	const rewrittenAllow = rewritePluginIdList(plugins.allow, LEGACY_CODEX_SUPERVISOR_PLUGIN_ID, CODEX_PLUGIN_ID);
	if (rewrittenAllow.changed) {
		plugins.allow = rewrittenAllow.next;
		changes.push("Rewrote plugins.allow codex-supervisor references to codex.");
	}
	const rewrittenDeny = rewritePluginIdList(plugins.deny, LEGACY_CODEX_SUPERVISOR_PLUGIN_ID);
	if (rewrittenDeny.changed) {
		plugins.deny = rewrittenDeny.next;
		changes.push("Removed plugins.deny codex-supervisor references.");
	}
	return changes;
}
/** Legacy config migration specs for provider/plugin runtime config compatibility. */
const LEGACY_CONFIG_MIGRATIONS_RUNTIME_PROVIDERS = [
	defineLegacyConfigMigration({
		id: "plugins.codex-supervisor->plugins.codex.config.supervision",
		describe: "Move retired Codex Supervisor config into the Codex plugin",
		legacyRules: [{
			path: ["plugins"],
			message: "plugins.entries.codex-supervisor and related plugin policy references are retired; use plugins.entries.codex.config.supervision. Run \"openclaw doctor --fix\".",
			requireSourceLiteral: true,
			match: (_value, root) => migrateLegacyCodexSupervisorPlugin(structuredClone(root)).length > 0
		}],
		apply: (raw, changes) => {
			changes.push(...migrateLegacyCodexSupervisorPlugin(raw));
		}
	}),
	defineLegacyConfigMigration({
		id: "plugins.openai-codex->plugins.openai",
		describe: "Rewrite retired OpenAI Codex plugin policy ids",
		legacyRules: [{
			path: ["plugins"],
			message: "plugins.openai-codex references are retired; use the openai plugin id. Run \"openclaw doctor --fix\".",
			requireSourceLiteral: true,
			match: (_value, root) => rewriteLegacyOpenAICodexPluginPolicy(structuredClone(root)).length > 0
		}],
		apply: (raw, changes) => {
			changes.push(...rewriteLegacyOpenAICodexPluginPolicy(raw));
		}
	}),
	defineLegacyConfigMigration({
		id: "tools.web.x_search.apiKey->plugins.entries.xai.config.webSearch.apiKey",
		describe: "Move legacy x_search auth and repair retired xAI model defaults",
		legacyRules: [X_SEARCH_RULE, X_SEARCH_MODEL_RULE],
		apply: (raw, changes) => {
			const migrated = migrateLegacyXSearchConfig(raw);
			if (!migrated.changes.length) return;
			for (const key of Object.keys(raw)) delete raw[key];
			Object.assign(raw, migrated.config);
			changes.push(...migrated.changes);
		}
	})
];
//#endregion
//#region src/commands/doctor/shared/legacy-config-migrations.runtime.config-tranche.ts
function stripRetiredPresentationPrefs(raw, changes) {
	const prefs = getRecord(getRecord(raw.ui)?.prefs);
	if (!prefs) return;
	const removed = [
		"chatMessageMaxWidth",
		"textScale",
		"sidebarLiveActivity",
		"showAdvancedSettings"
	].filter((key) => {
		if (!Object.hasOwn(prefs, key)) return false;
		delete prefs[key];
		return true;
	});
	if (removed.length > 0) changes.push(`Removed browser-local ui.prefs keys: ${removed.map((key) => `ui.prefs.${key}`).join(", ")}.`);
	const ui = getRecord(raw.ui);
	if (Object.keys(prefs).length === 0) delete ui?.prefs;
	if (ui && Object.keys(ui).length === 0) delete raw.ui;
}
function stripRetiredAgentConfig(raw, changes) {
	const agents = getRecord(raw.agents);
	const defaults = getRecord(agents?.defaults);
	let removedContextLimits = false;
	const stripContextLimits = (owner) => {
		for (const key of ["memoryGetDefaultLines", "toolResultMaxChars"]) removedContextLimits = deleteRetiredPath(owner, ["contextLimits", key]) || removedContextLimits;
	};
	if (defaults) stripContextLimits(defaults);
	let removedTypingOverride = false;
	visitAgentEntries(raw, (entry) => {
		if (Object.hasOwn(entry, "typingIntervalSeconds")) {
			delete entry.typingIntervalSeconds;
			removedTypingOverride = true;
		}
		stripContextLimits(entry);
	});
	if (removedTypingOverride) changes.push("Removed per-agent typingIntervalSeconds overrides; agents.defaults.typingIntervalSeconds now applies to every agent.");
	if (removedContextLimits) changes.push("Removed contextLimits.memoryGetDefaultLines/toolResultMaxChars overrides; canonical memory and context-window caps now apply.");
}
function readLegacyDebounce(path, owner, accountId) {
	if (!Object.hasOwn(owner, "debounceMs")) return null;
	const raw = owner.debounceMs;
	delete owner.debounceMs;
	return {
		path,
		...accountId ? { accountId } : {},
		...typeof raw === "number" && Number.isInteger(raw) && raw >= 0 ? { value: raw } : {}
	};
}
function migrateWhatsAppDebounce(raw, changes) {
	const whatsapp = getRecord(getRecord(raw.channels)?.whatsapp);
	if (!whatsapp) return;
	const sources = [];
	const rootSource = readLegacyDebounce("channels.whatsapp.debounceMs", whatsapp);
	if (rootSource) sources.push(rootSource);
	const accounts = getRecord(whatsapp.accounts);
	if (accounts) for (const accountId of Object.keys(accounts).toSorted()) {
		const account = getRecord(accounts[accountId]);
		if (!account) continue;
		const source = readLegacyDebounce(`channels.whatsapp.accounts.${accountId}.debounceMs`, account, normalizeAccountId(accountId));
		if (source) sources.push(source);
	}
	if (sources.length === 0) return;
	const validSources = sources.filter((source) => source.value !== void 0);
	if (validSources.length === 0) {
		changes.push(`Removed invalid WhatsApp debounce values: ${sources.map((source) => source.path).join(", ")}.`);
		return;
	}
	const inbound = ensureRecord(ensureRecord(raw, "messages"), "inbound");
	const byChannel = ensureRecord(inbound, "byChannel");
	if (byChannel.whatsapp !== void 0) {
		changes.push(`Removed ${sources.map((source) => source.path).join(", ")} (messages.inbound.byChannel.whatsapp already set).`);
		return;
	}
	const configuredDefaultAccount = normalizeAccountId(typeof whatsapp.defaultAccount === "string" ? whatsapp.defaultAccount : void 0);
	const selected = validSources.find((source) => source.accountId === configuredDefaultAccount) ?? validSources.find((source) => source.accountId === "default") ?? validSources.find((source) => source.path === "channels.whatsapp.debounceMs") ?? validSources[0];
	if (!selected) return;
	byChannel.whatsapp = selected.value;
	if (new Set(validSources.map((source) => source.value)).size === 1 && validSources.length === sources.length) changes.push(`Moved ${sources.map((source) => source.path).join(", ")} → messages.inbound.byChannel.whatsapp.`);
	else changes.push(`Collapsed conflicting WhatsApp debounce values into messages.inbound.byChannel.whatsapp using ${selected.path} (${selected.value} ms); account-specific debounce is no longer supported.`);
}
function migrateConfigTranche(raw, changes) {
	stripRetiredPresentationPrefs(raw, changes);
	if (deleteRetiredPath(raw, [
		"skills",
		"load",
		"watchDebounceMs"
	])) changes.push("Removed skills.load.watchDebounceMs; the watcher now uses the 250 ms default.");
	stripRetiredAgentConfig(raw, changes);
	migrateWhatsAppDebounce(raw, changes);
}
function hasConfigTrancheLegacyKeys(root) {
	const changes = [];
	migrateConfigTranche(structuredClone(root), changes);
	return changes.length > 0;
}
//#endregion
//#region src/commands/doctor/shared/legacy-config-migrations.runtime.retired-media.ts
function moveVoice(owner, path, changes) {
	if (!Object.hasOwn(owner, "voice")) return;
	if (owner.speakerVoice === void 0) {
		owner.speakerVoice = owner.voice;
		changes.push(`Moved ${path}.voice → ${path}.speakerVoice.`);
	} else changes.push(`Removed ${path}.voice (${path}.speakerVoice already set).`);
	delete owner.voice;
}
function migrateDiscordVoice(channels, changes) {
	const discord = getRecord(channels.discord);
	if (!discord) return;
	const migrateEntry = (entry, path) => {
		const realtime = getRecord(getRecord(entry.voice)?.realtime);
		if (realtime) moveVoice(realtime, `${path}.voice.realtime`, changes);
	};
	migrateEntry(discord, "channels.discord");
	const accounts = getRecord(discord.accounts);
	if (accounts) for (const [accountId, value] of Object.entries(accounts)) {
		const account = getRecord(value);
		if (account) migrateEntry(account, `channels.discord.accounts.${accountId}`);
	}
}
function hasDiscordRealtimeVoice(value) {
	const discord = getRecord(value);
	if (!discord) return false;
	const hasAlias = (entry) => {
		const realtime = getRecord(getRecord(getRecord(entry)?.voice)?.realtime);
		return realtime ? Object.hasOwn(realtime, "voice") : false;
	};
	if (hasAlias(discord)) return true;
	const accounts = getRecord(discord.accounts);
	return accounts ? Object.values(accounts).some(hasAlias) : false;
}
function mapDeepgram(value) {
	const mapped = {};
	if (typeof value.detectLanguage === "boolean") mapped.detect_language = value.detectLanguage;
	if (typeof value.punctuate === "boolean") mapped.punctuate = value.punctuate;
	if (typeof value.smartFormat === "boolean") mapped.smart_format = value.smartFormat;
	return mapped;
}
function migrateDeepgramOwner(owner, path, changes) {
	const legacy = getRecord(owner.deepgram);
	if (!legacy) return;
	const providerOptions = getRecord(owner.providerOptions) ?? {};
	const canonical = getRecord(providerOptions.deepgram) ?? {};
	providerOptions.deepgram = {
		...mapDeepgram(legacy),
		...canonical
	};
	owner.providerOptions = providerOptions;
	delete owner.deepgram;
	changes.push(`Moved ${path}.deepgram → ${path}.providerOptions.deepgram.`);
}
function migrateMediaDeepgram(raw, changes) {
	const media = getRecord(getRecord(raw.tools)?.media);
	if (!media) return;
	const migrateModels = (models, path) => {
		if (!Array.isArray(models)) return;
		models.forEach((value, index) => {
			const model = getRecord(value);
			if (model) migrateDeepgramOwner(model, `${path}[${index}]`, changes);
		});
	};
	migrateModels(media.models, "tools.media.models");
	for (const capability of [
		"audio",
		"image",
		"video"
	]) {
		const entry = getRecord(media[capability]);
		if (!entry) continue;
		migrateDeepgramOwner(entry, `tools.media.${capability}`, changes);
		migrateModels(entry.models, `tools.media.${capability}.models`);
	}
}
function hasMediaDeepgram(value) {
	const media = getRecord(value);
	if (!media) return false;
	const hasAlias = (entry) => {
		const owner = getRecord(entry);
		return owner ? Object.hasOwn(owner, "deepgram") : false;
	};
	const modelsHaveAlias = (models) => Array.isArray(models) && models.some(hasAlias);
	if (modelsHaveAlias(media.models)) return true;
	return [
		"audio",
		"image",
		"video"
	].some((capability) => {
		const entry = getRecord(media[capability]);
		return entry ? hasAlias(entry) || modelsHaveAlias(entry.models) : false;
	});
}
const RETIRED_TUNING_PATHS = [
	["systemAgent"],
	["marketplaces"],
	[
		"cli",
		"banner",
		"taglineMode"
	],
	["commitments"],
	["auth", "cooldowns"],
	["secrets", "resolution"],
	["browser", "remoteCdpTimeoutMs"],
	["browser", "remoteCdpHandshakeTimeoutMs"],
	["browser", "localLaunchTimeoutMs"],
	["browser", "localCdpReadyTimeoutMs"],
	["browser", "actionTimeoutMs"],
	["browser", "cdpPortRangeStart"],
	[
		"browser",
		"tabCleanup",
		"idleMinutes"
	],
	[
		"browser",
		"tabCleanup",
		"maxTabsPerSession"
	],
	[
		"browser",
		"tabCleanup",
		"sweepMinutes"
	],
	[
		"tools",
		"loopDetection",
		"genericRepeat"
	],
	[
		"tools",
		"loopDetection",
		"knownPollNoProgress"
	],
	[
		"tools",
		"loopDetection",
		"pingPong"
	],
	[
		"tools",
		"loopDetection",
		"windowSize"
	],
	[
		"tools",
		"loopDetection",
		"historySize"
	],
	[
		"tools",
		"loopDetection",
		"warningThreshold"
	],
	[
		"tools",
		"loopDetection",
		"unknownToolThreshold"
	],
	[
		"tools",
		"loopDetection",
		"criticalThreshold"
	],
	[
		"tools",
		"loopDetection",
		"globalCircuitBreakerThreshold"
	],
	[
		"tools",
		"loopDetection",
		"detectors"
	],
	[
		"tools",
		"loopDetection",
		"postCompactionGuard"
	],
	["gateway", "handshakeTimeoutMs"],
	["gateway", "channelHealthCheckMinutes"],
	["gateway", "channelStaleEventThresholdMinutes"],
	["gateway", "channelMaxRestartsPerHour"],
	[
		"gateway",
		"reload",
		"debounceMs"
	],
	[
		"gateway",
		"reload",
		"deferralTimeoutMs"
	],
	[
		"gateway",
		"http",
		"endpoints",
		"chatCompletions",
		"maxBodyBytes"
	],
	[
		"gateway",
		"http",
		"endpoints",
		"chatCompletions",
		"maxImageParts"
	],
	[
		"gateway",
		"http",
		"endpoints",
		"chatCompletions",
		"maxTotalImageBytes"
	],
	[
		"gateway",
		"http",
		"endpoints",
		"responses",
		"maxBodyBytes"
	],
	["session", "typingIntervalSeconds"],
	["session", "writeLock"],
	[
		"session",
		"agentToAgent",
		"maxPingPongTurns"
	],
	["cron", "maxConcurrentRuns"],
	[
		"cron",
		"triggers",
		"minIntervalMs"
	],
	["cron", "retry"],
	["diagnostics", "stuckSessionWarnMs"],
	["diagnostics", "stuckSessionAbortMs"],
	["diagnostics", "memoryPressureSnapshot"],
	["diagnostics", "memoryPressureBundle"],
	["web", "heartbeatSeconds"],
	["web", "reconnect"],
	["web", "whatsapp"],
	[
		"messages",
		"queue",
		"debounceMs"
	],
	[
		"messages",
		"statusReactions",
		"timing"
	],
	[
		"acp",
		"stream",
		"coalesceIdleMs"
	],
	[
		"acp",
		"stream",
		"maxChunkChars"
	],
	[
		"acp",
		"stream",
		"maxOutputChars"
	],
	[
		"acp",
		"stream",
		"maxSessionUpdateChars"
	],
	[
		"acp",
		"stream",
		"hiddenBoundarySeparator"
	],
	["acp", "maxConcurrentSessions"],
	[
		"acp",
		"runtime",
		"ttlMinutes"
	],
	["mcp", "sessionIdleTtlMs"],
	["worktrees"],
	["transcripts", "maxUtterances"],
	["hooks", "maxBodyBytes"],
	[
		"update",
		"auto",
		"stableDelayHours"
	],
	[
		"update",
		"auto",
		"stableJitterHours"
	],
	[
		"update",
		"auto",
		"betaCheckIntervalHours"
	],
	[
		"memory",
		"search",
		"chunking"
	],
	[
		"memory",
		"search",
		"sync",
		"watchDebounceMs"
	],
	[
		"memory",
		"search",
		"sync",
		"intervalMinutes"
	],
	[
		"memory",
		"search",
		"query",
		"hybrid",
		"vectorWeight"
	],
	[
		"memory",
		"search",
		"query",
		"hybrid",
		"textWeight"
	],
	[
		"memory",
		"search",
		"query",
		"hybrid",
		"candidateMultiplier"
	],
	[
		"memory",
		"search",
		"query",
		"hybrid",
		"mmr",
		"lambda"
	],
	[
		"memory",
		"search",
		"query",
		"hybrid",
		"temporalDecay",
		"halfLifeDays"
	],
	[
		"memory",
		"search",
		"cache",
		"maxEntries"
	],
	[
		"channels",
		"*",
		"streaming",
		"progress",
		"render"
	],
	[
		"channels",
		"*",
		"accounts",
		"*",
		"streaming",
		"progress",
		"render"
	]
];
const RETIRED_AGENT_TUNING_PATHS = [
	["compaction", "reserveTokens"],
	["compaction", "reserveTokensFloor"],
	["compaction", "maxHistoryShare"],
	["contextPruning", "keepLastAssistants"],
	["contextPruning", "softTrimRatio"],
	["contextPruning", "hardClearRatio"],
	["contextPruning", "minPrunableToolChars"],
	["contextPruning", "softTrim"],
	[
		"memory",
		"search",
		"chunking"
	],
	[
		"memory",
		"search",
		"sync",
		"watchDebounceMs"
	],
	[
		"memory",
		"search",
		"sync",
		"intervalMinutes"
	],
	[
		"memory",
		"search",
		"query",
		"hybrid",
		"vectorWeight"
	],
	[
		"memory",
		"search",
		"query",
		"hybrid",
		"textWeight"
	],
	[
		"memory",
		"search",
		"query",
		"hybrid",
		"candidateMultiplier"
	],
	[
		"memory",
		"search",
		"query",
		"hybrid",
		"mmr",
		"lambda"
	],
	[
		"memory",
		"search",
		"query",
		"hybrid",
		"temporalDecay",
		"halfLifeDays"
	],
	[
		"memory",
		"search",
		"cache",
		"maxEntries"
	],
	[
		"cliBackends",
		"*",
		"reliability",
		"outputLimits"
	],
	[
		"cliBackends",
		"*",
		"reliability",
		"watchdog",
		"fresh",
		"noOutputTimeoutMs"
	],
	[
		"cliBackends",
		"*",
		"reliability",
		"watchdog",
		"resume",
		"noOutputTimeoutMs"
	],
	["runRetries"],
	[
		"tools",
		"loopDetection",
		"genericRepeat"
	],
	[
		"tools",
		"loopDetection",
		"knownPollNoProgress"
	],
	[
		"tools",
		"loopDetection",
		"pingPong"
	],
	[
		"tools",
		"loopDetection",
		"windowSize"
	],
	[
		"tools",
		"loopDetection",
		"historySize"
	],
	[
		"tools",
		"loopDetection",
		"warningThreshold"
	],
	[
		"tools",
		"loopDetection",
		"unknownToolThreshold"
	],
	[
		"tools",
		"loopDetection",
		"criticalThreshold"
	],
	[
		"tools",
		"loopDetection",
		"globalCircuitBreakerThreshold"
	],
	[
		"tools",
		"loopDetection",
		"detectors"
	],
	[
		"tools",
		"loopDetection",
		"postCompactionGuard"
	]
];
function stripRetiredTuningKnobs(raw) {
	let changed = false;
	for (const path of RETIRED_TUNING_PATHS) changed = deleteRetiredPath(raw, path) || changed;
	visitAgentConfigScopes(raw, (agent) => {
		for (const path of RETIRED_AGENT_TUNING_PATHS) changed = deleteRetiredPath(agent, path) || changed;
	});
	return changed;
}
const MEDIA_CAPABILITIES = [
	"image",
	"audio",
	"video"
];
function stableConfigValue(value) {
	if (Array.isArray(value)) return value.map(stableConfigValue);
	const record = getRecord(value);
	if (!record) return value;
	return Object.fromEntries(Object.keys(record).toSorted().map((key) => [key, stableConfigValue(record[key])]));
}
function mediaModelSignature(model) {
	const { capabilities: _capabilities, ...rest } = model;
	return JSON.stringify(stableConfigValue(rest));
}
function scopeLegacyMediaModel(model, capability) {
	if (Array.isArray(model.capabilities) && !model.capabilities.some((value) => value === capability)) return;
	return {
		...model,
		capabilities: [capability]
	};
}
function hasLegacyMediaCapabilityConfig(value) {
	const media = getRecord(value);
	return MEDIA_CAPABILITIES.some((capability) => {
		const config = getRecord(media?.[capability]);
		return Array.isArray(config?.models);
	});
}
function consolidateMediaCapabilityConfig(raw, changes) {
	const media = getRecord(getRecord(raw.tools)?.media);
	if (!media) return;
	const sharedModels = Array.isArray(media.models) ? media.models.filter((value) => getRecord(value) !== void 0) : [];
	const migratedModels = [];
	let changed = false;
	for (const capability of MEDIA_CAPABILITIES) {
		const config = getRecord(media[capability]);
		if (!config) continue;
		const legacyModels = Array.isArray(config.models) ? config.models.filter((value) => getRecord(value) !== void 0) : [];
		const migratedBySignature = /* @__PURE__ */ new Map();
		const eligibleLegacyModels = legacyModels.flatMap((legacyModel) => {
			const scoped = scopeLegacyMediaModel(legacyModel, capability);
			return scoped ? [scoped] : [];
		});
		for (const migrated of eligibleLegacyModels) {
			const signature = mediaModelSignature(migrated);
			if (migratedBySignature.get(signature)) continue;
			migratedBySignature.set(signature, migrated);
			migratedModels.push(migrated);
		}
		if (Object.hasOwn(config, "models")) {
			delete config.models;
			changed = true;
		}
		if (Object.keys(config).length === 0) delete media[capability];
		changed = changed || legacyModels.length > 0;
	}
	const canonicalModels = [...migratedModels, ...sharedModels];
	if (canonicalModels.length > 0) media.models = canonicalModels;
	if (changed) changes.push("Consolidated tools.media image/audio/video model settings into capability-tagged tools.media.models entries.");
}
//#endregion
//#region packages/memory-host-sdk/src/host/config-utils.ts
/** Trim and deduplicate configured extra-memory roots without losing pattern identity. */
function normalizeConfiguredMemoryExtraPaths(extraPaths) {
	const normalized = /* @__PURE__ */ new Map();
	for (const entry of extraPaths ?? []) {
		const configuredPath = (typeof entry === "string" ? entry : entry.path).trim();
		const pattern = typeof entry === "string" ? "" : entry.pattern?.trim() || "";
		if (configuredPath) normalized.set(`${configuredPath}\0${pattern}`, pattern ? {
			path: configuredPath,
			pattern
		} : configuredPath);
	}
	return Array.from(normalized.values());
}
function resolveRememberAcrossConversations(cfg, agentId) {
	const defaults = cfg.memory?.search;
	const explicit = (resolveAgentConfig(cfg, agentId)?.memory?.search)?.rememberAcrossConversations ?? defaults?.rememberAcrossConversations;
	if (explicit !== void 0) return explicit;
	return (cfg.session?.dmScope === void 0 || cfg.session.dmScope === "main") && !cfg.bindings?.some((binding) => {
		if (!binding || typeof binding !== "object") return false;
		const session = binding.session;
		return Boolean(session) && typeof session === "object" && session.dmScope !== void 0;
	});
}
/** Root memory filename used in agent workspaces. */
const MEMORY_HOST_ROOT_FILENAME = "MEMORY.md";
const DEFAULT_AGENT_ID = "main";
const LEGACY_STATE_DIRNAMES = [".clawdbot"];
const NEW_STATE_DIRNAME = ".openclaw";
/** Treat shell-placeholder home values as absent. */
function normalizeHomeValue(value) {
	const trimmed = normalizeOptionalString(value);
	if (!trimmed || trimmed === "undefined" || trimmed === "null") return;
	return trimmed;
}
/** Resolve the underlying OS home before applying OpenClaw-specific overrides. */
function resolveRawOsHomeDir(env, homedir) {
	return normalizeHomeValue(env.HOME) ?? normalizeHomeValue(env.USERPROFILE) ?? normalizeHomeValue(homedir());
}
/** Resolve OPENCLAW_HOME or the OS home, falling back to cwd for hermetic tests. */
function resolveRequiredHomeDir(env = process.env, homedir = os.homedir) {
	const explicitHome = normalizeHomeValue(env.OPENCLAW_HOME);
	const rawHome = explicitHome ? explicitHome.replace(/^~(?=$|[\\/])/, () => resolveRawOsHomeDir(env, homedir) ?? "") : resolveRawOsHomeDir(env, homedir);
	return rawHome ? path.resolve(rawHome) : path.resolve(process.cwd());
}
/** Resolve standalone memory-host paths without importing core home-directory policy. */
function resolveMemoryHostUserPath(input, env = process.env, homedir = os.homedir) {
	const trimmed = input.trim();
	if (!trimmed) return trimmed;
	if (trimmed.startsWith("~")) return path.resolve(trimmed.replace(/^~(?=$|[\\/])/, () => resolveRequiredHomeDir(env, homedir)));
	return path.resolve(trimmed);
}
/** Return legacy state roots in priority order. */
function legacyStateDirs(homedir) {
	return LEGACY_STATE_DIRNAMES.map((dir) => path.join(homedir(), dir));
}
function isFastTestRuntimeEnv(env) {
	return (env.VITEST === "true" || env.VITEST === "1" || env.VITEST_POOL_ID !== void 0 || env.VITEST_WORKER_ID !== void 0 || env.NODE_ENV === "test" || env !== process.env && (process.env.VITEST === "true" || process.env.VITEST === "1" || process.env.VITEST_POOL_ID !== void 0 || process.env.VITEST_WORKER_ID !== void 0 || false)) && env.OPENCLAW_TEST_FAST === "1";
}
/** Resolve the current state root while preserving shipped legacy installs when present. */
function resolveStateDir(env = process.env, homedir = os.homedir) {
	const override = env.OPENCLAW_STATE_DIR?.trim();
	if (override) return resolveMemoryHostUserPath(override, env, homedir);
	const effectiveHome = () => resolveRequiredHomeDir(env, homedir);
	const nextDir = path.join(effectiveHome(), NEW_STATE_DIRNAME);
	if (isFastTestRuntimeEnv(env) || fs.existsSync(nextDir)) return nextDir;
	return legacyStateDirs(effectiveHome).find((dir) => {
		try {
			return fs.existsSync(dir);
		} catch {
			return false;
		}
	}) ?? nextDir;
}
/** Resolve the default agent workspace, partitioned by OPENCLAW_PROFILE when set. */
function resolveDefaultAgentWorkspaceDir(env = process.env) {
	const workspaceDir = env.OPENCLAW_WORKSPACE_DIR?.trim();
	if (workspaceDir) return resolveMemoryHostUserPath(workspaceDir, env);
	if (env.OPENCLAW_STATE_DIR?.trim()) return path.join(resolveStateDir(env), "workspace");
	const home = resolveRequiredHomeDir(env, os.homedir);
	const profile = env.OPENCLAW_PROFILE?.trim();
	if (profile && normalizeLowercaseStringOrEmpty(profile) !== "default") return path.join(resolveStateDir(env), "workspace");
	return path.join(home, ".openclaw", "workspace");
}
/** Return configured agent entries after dropping nullish placeholders. */
function listAgentEntries(cfg) {
	if (cfg.agents?.entries) return Object.entries(cfg.agents.entries).map(([id, entry]) => Object.assign({ id }, entry));
	return Array.isArray(cfg.agents?.list) ? cfg.agents.list.filter((entry) => Boolean(entry)) : [];
}
/** Resolve the default agent id from explicit default marker or first agent entry. */
function resolveDefaultAgentId(cfg) {
	const agents = listAgentEntries(cfg);
	if (agents.length === 0) return DEFAULT_AGENT_ID;
	const chosen = (agents.find((agent) => agent.default) ?? agents[0])?.id;
	return normalizeAgentId(chosen || DEFAULT_AGENT_ID);
}
/** Find one agent config by canonical id. */
function resolveAgentConfig(cfg, agentId) {
	const id = normalizeAgentId(agentId);
	return listAgentEntries(cfg).find((entry) => normalizeAgentId(entry.id) === id);
}
/** Remove null bytes before paths are handed to filesystem APIs. */
function stripNullBytes(value) {
	return value.replaceAll("\0", "");
}
/** Resolve the workspace directory for an agent id and config defaults. */
function resolveMemoryHostAgentWorkspaceDir(cfg, agentId, env = process.env) {
	const id = normalizeAgentId(agentId);
	const configured = resolveAgentConfig(cfg, id)?.workspace?.trim();
	if (configured) return stripNullBytes(resolveMemoryHostUserPath(configured, env));
	const fallback = cfg.agents?.defaults?.workspace?.trim();
	if (id === resolveDefaultAgentId(cfg)) return stripNullBytes(fallback ? resolveMemoryHostUserPath(fallback, env) : resolveDefaultAgentWorkspaceDir(env));
	if (fallback) return stripNullBytes(path.join(resolveMemoryHostUserPath(fallback, env), id));
	return stripNullBytes(path.join(resolveStateDir(env), `workspace-${id}`));
}
/** Resolve context limits for an agent with defaults fallback. */
function resolveMemoryHostAgentContextLimits(cfg, agentId) {
	const defaults = cfg?.agents?.defaults?.contextLimits;
	if (!cfg || !agentId) return defaults;
	return resolveAgentConfig(cfg, agentId)?.contextLimits ?? defaults;
}
/** Resolve enabled memory search config plus deduplicated extra paths for an agent. */
function resolveMemoryHostSearchPathConfig(cfg, agentId) {
	const defaults = cfg.memory?.search;
	const overrides = resolveAgentConfig(cfg, agentId)?.memory?.search;
	const enabled = overrides?.enabled ?? defaults?.enabled ?? true;
	if (!enabled) return null;
	const extraPaths = normalizeConfiguredMemoryExtraPaths([...defaults?.extraPaths ?? [], ...overrides?.extraPaths ?? []]);
	return {
		enabled,
		rememberAcrossConversations: resolveRememberAcrossConversations(cfg, agentId),
		extraPaths
	};
}
//#endregion
//#region src/commands/doctor/shared/legacy-config-migrations.runtime.retired-memory-qmd.ts
const rule$2 = (path, message, match) => ({
	path,
	message: `${message} Run "openclaw doctor --fix".`,
	...match ? { match } : {}
});
function hasRetiredAgentMemoryQmd(value) {
	const memory = getRecord(getRecord(value)?.memory);
	const search = getRecord(memory?.search);
	return Boolean(search && Object.hasOwn(search, "qmd"));
}
function readRetiredQmdExternalPaths(value) {
	if (!Array.isArray(value)) return [];
	const paths = [];
	for (const candidate of value) {
		const entry = getRecord(candidate);
		const path = typeof entry?.path === "string" ? entry.path.trim() : "";
		if (!path) continue;
		const pattern = typeof entry?.pattern === "string" ? entry.pattern.trim() : "";
		paths.push({
			path,
			...pattern ? { pattern } : {}
		});
	}
	return paths;
}
function migrateRetiredQmdExternalPaths(params) {
	if (params.entries.length === 0) return;
	const memory = ensureRecord(params.scope, "memory");
	const search = ensureRecord(memory, "search");
	const existingPaths = normalizeConfiguredMemoryExtraPaths(Array.isArray(search.extraPaths) ? search.extraPaths.filter((entry) => typeof entry === "string" || typeof getRecord(entry)?.path === "string") : []);
	const nextPaths = [...existingPaths];
	const entryKey = (entry) => typeof entry === "string" ? `${entry}\0` : `${entry.path}\0${entry.pattern?.trim() ?? ""}`;
	const seen = new Set(existingPaths.map(entryKey));
	let added = 0;
	for (const entry of params.entries) {
		const nextEntry = entry.pattern ? entry : entry.path;
		const key = entryKey(nextEntry);
		if (seen.has(key)) continue;
		seen.add(key);
		nextPaths.push(nextEntry);
		added += 1;
	}
	if (added > 0) {
		search.extraPaths = nextPaths;
		params.changes.push(`Migrated ${added} external QMD path${added === 1 ? "" : "s"} from ${params.sourcePath} → ${params.targetPath}.`);
	}
}
function migrateRetiredQmdSessionIndexing(qmd, scope, sourcePath, changes, targetPath = sourcePath === "memory.qmd" ? "memory.search" : sourcePath.slice(0, -4)) {
	if (getRecord(qmd?.sessions)?.enabled !== true) return;
	const search = ensureRecord(ensureRecord(scope, "memory"), "search");
	const experimental = getRecord(search.experimental);
	let changed = false;
	if ((experimental || search.experimental === void 0) && experimental?.sessionMemory === void 0) {
		ensureRecord(search, "experimental").sessionMemory = true;
		changed = true;
	}
	if (search.sources === void 0 || Array.isArray(search.sources) && search.sources.length === 0) {
		search.sources = ["memory", "sessions"];
		changed = true;
	} else if (Array.isArray(search.sources) && !search.sources.includes("sessions")) {
		search.sources.push("sessions");
		changed = true;
	}
	if (changed) changes.push(`Migrated ${sourcePath}.sessions.enabled → ${targetPath}.experimental.sessionMemory and ${targetPath}.sources.`);
}
function migrateRetiredMemoryQmd(raw, changes) {
	const memory = getRecord(raw.memory);
	const search = getRecord(memory?.search);
	const qmd = getRecord(memory?.qmd);
	const searchQmd = getRecord(search?.qmd);
	migrateRetiredQmdSessionIndexing(qmd, raw, "memory.qmd", changes);
	migrateRetiredQmdSessionIndexing(searchQmd, raw, "memory.search.qmd", changes);
	migrateRetiredQmdExternalPaths({
		changes,
		entries: [...readRetiredQmdExternalPaths(qmd?.paths), ...readRetiredQmdExternalPaths(searchQmd?.extraCollections)],
		scope: raw,
		sourcePath: "memory.qmd.paths and memory.search.qmd.extraCollections",
		targetPath: "memory.search.extraPaths"
	});
	let removed = false;
	for (const path of [
		["memory", "backend"],
		["memory", "qmd"],
		[
			"memory",
			"search",
			"qmd"
		]
	]) removed = deleteRetiredPath(raw, path) || removed;
	visitAgentConfigScopes(raw, (scope, scopePath) => {
		const agentSearch = getRecord(getRecord(scope.memory)?.search);
		const agentSearchQmd = getRecord(agentSearch?.qmd);
		const isAgentDefaults = scopePath === "agents.defaults" && agentSearch?.qmd !== void 0;
		const targetScope = isAgentDefaults ? raw : scope;
		const targetPath = isAgentDefaults ? "memory.search" : `${scopePath}.memory.search`;
		if (isAgentDefaults && agentSearch) {
			removed = deleteRetiredPath(scope, [
				"memory",
				"search",
				"qmd"
			]) || removed;
			mergeMissing(ensureRecord(ensureRecord(raw, "memory"), "search"), agentSearch);
			delete scope.memory;
		}
		migrateRetiredQmdSessionIndexing(agentSearchQmd, targetScope, `${scopePath}.memory.search.qmd`, changes, targetPath);
		migrateRetiredQmdExternalPaths({
			changes,
			entries: readRetiredQmdExternalPaths(agentSearchQmd?.extraCollections),
			scope: targetScope,
			sourcePath: `${scopePath}.memory.search.qmd.extraCollections`,
			targetPath: `${targetPath}.extraPaths`
		});
		if (!isAgentDefaults) removed = deleteRetiredPath(scope, [
			"memory",
			"search",
			"qmd"
		]) || removed;
	});
	if (removed) changes.push("Removed retired QMD memory configuration; builtin memory is now the only memory engine.");
}
const LEGACY_CONFIG_MIGRATION_RUNTIME_MEMORY_QMD = defineLegacyConfigMigration({
	id: "runtime.memory-qmd-retired",
	describe: "Remove retired QMD memory configuration",
	legacyRules: [
		rule$2(["memory", "backend"], "memory.backend is retired; builtin memory is now the only memory engine."),
		rule$2(["memory", "qmd"], "memory.qmd is retired because the QMD memory backend was removed; configured external paths migrate to memory.search.extraPaths."),
		rule$2([
			"memory",
			"search",
			"qmd"
		], "memory.search.qmd is retired because the QMD memory backend was removed; configured external collections migrate to memory.search.extraPaths."),
		rule$2([
			"agents",
			"defaults",
			"memory",
			"search",
			"qmd"
		], "agents.defaults.memory.search.qmd is retired because the QMD memory backend was removed; configured external collections migrate to memory.search.extraPaths."),
		rule$2(["agents", "entries"], "agents.entries.*.memory.search.qmd is retired because the QMD memory backend was removed; configured external collections migrate to the matching agent memory.search.extraPaths.", (value) => {
			const entries = getRecord(value);
			return entries ? Object.values(entries).some(hasRetiredAgentMemoryQmd) : false;
		}),
		rule$2(["agents", "list"], "agents.list.*.memory.search.qmd is retired because the QMD memory backend was removed; configured external collections migrate to the matching agent memory.search.extraPaths.", (value) => Array.isArray(value) && value.some(hasRetiredAgentMemoryQmd))
	],
	apply: migrateRetiredMemoryQmd
});
//#endregion
//#region src/commands/doctor/shared/legacy-config-migrations.runtime.tier-eval.ts
const TIER_EVAL_RETIRED_ROOT_PATHS = [
	[
		"cloudWorkers",
		"profiles",
		"*",
		"lifetime"
	],
	["meta", "lastTouchedAt"],
	[
		"hooks",
		"internal",
		"installs"
	],
	["cron", "store"],
	["plugins", "bundledDiscovery"],
	["tts", "prefsPath"],
	["logging", "redactSensitive"],
	["commands", "useAccessGroups"],
	[
		"gateway",
		"controlUi",
		"allowInsecureAuth"
	],
	[
		"memory",
		"search",
		"remote",
		"nonBatchConcurrency"
	],
	[
		"memory",
		"search",
		"remote",
		"batch",
		"wait"
	],
	[
		"memory",
		"search",
		"remote",
		"batch",
		"concurrency"
	],
	[
		"memory",
		"search",
		"remote",
		"batch",
		"pollIntervalMs"
	],
	[
		"memory",
		"search",
		"remote",
		"batch",
		"timeoutMinutes"
	],
	[
		"memory",
		"search",
		"local",
		"contextSize"
	],
	[
		"memory",
		"search",
		"local",
		"modelCacheDir"
	],
	[
		"memory",
		"search",
		"store",
		"driver"
	],
	[
		"memory",
		"search",
		"sync"
	],
	[
		"memory",
		"search",
		"query",
		"hybrid"
	]
];
const TIER_EVAL_RETIRED_AGENT_PATHS = [
	["groupChat", "visibleReplies"],
	[
		"memory",
		"search",
		"remote",
		"nonBatchConcurrency"
	],
	[
		"memory",
		"search",
		"remote",
		"batch",
		"wait"
	],
	[
		"memory",
		"search",
		"remote",
		"batch",
		"concurrency"
	],
	[
		"memory",
		"search",
		"remote",
		"batch",
		"pollIntervalMs"
	],
	[
		"memory",
		"search",
		"remote",
		"batch",
		"timeoutMinutes"
	],
	[
		"memory",
		"search",
		"local",
		"contextSize"
	],
	[
		"memory",
		"search",
		"local",
		"modelCacheDir"
	],
	[
		"memory",
		"search",
		"store",
		"driver"
	],
	[
		"memory",
		"search",
		"sync"
	],
	[
		"memory",
		"search",
		"query",
		"hybrid"
	],
	["heartbeat", "ackMaxChars"],
	["heartbeat", "includeReasoning"],
	["heartbeat", "includeSystemPromptSection"],
	["heartbeat", "skipWhenBusy"],
	["heartbeat", "suppressToolErrorWarnings"]
];
function resolveConfiguredExecPolicy(scope) {
	const exec = getRecord(getRecord(scope.tools)?.exec);
	if (!exec) return;
	switch (exec.mode) {
		case "deny": return {
			security: "deny",
			ask: "off"
		};
		case "allowlist": return {
			security: "allowlist",
			ask: "off"
		};
		case "ask":
		case "auto": return {
			security: "allowlist",
			ask: "on-miss"
		};
		case "full": return {
			security: "full",
			ask: "off"
		};
	}
	const security = exec.security === "deny" || exec.security === "allowlist" || exec.security === "full" ? exec.security : void 0;
	const ask = exec.ask === "on-miss" || exec.ask === "always" || exec.ask === "off" ? exec.ask : void 0;
	return security && ask ? {
		security,
		ask
	} : void 0;
}
function migrateExecMode(scope, path, changes, inheritedPolicy) {
	const exec = getRecord(getRecord(scope.tools)?.exec);
	if (!exec || !Object.hasOwn(exec, "security") && !Object.hasOwn(exec, "ask")) return;
	if (exec.mode !== void 0) {
		changes.push(`Removed ${path}.tools.exec.security/ask (${path}.tools.exec.mode already set).`);
		delete exec.security;
		delete exec.ask;
		return;
	}
	const securityValid = exec.security === "deny" || exec.security === "allowlist" || exec.security === "full";
	const askValid = exec.ask === "on-miss" || exec.ask === "always" || exec.ask === "off";
	if (Object.hasOwn(exec, "security") && !securityValid || Object.hasOwn(exec, "ask") && !askValid) return;
	const security = securityValid ? exec.security : inheritedPolicy?.security;
	const ask = askValid ? exec.ask : inheritedPolicy?.ask;
	if (!security || !ask) return;
	if (ask === "always" || security === "full" && ask === "on-miss") return;
	exec.mode = security === "deny" ? "deny" : security === "allowlist" && ask === "off" ? "allowlist" : security === "full" ? "full" : "ask";
	changes.push(`Moved ${path}.tools.exec.security/ask → ${path}.tools.exec.mode.`);
	delete exec.security;
	delete exec.ask;
}
function migrateCliBackendSessionArgs(scope, path, changes) {
	const backends = getRecord(scope.cliBackends);
	if (!backends) return;
	for (const [backendId, value] of Object.entries(backends)) {
		const backend = getRecord(value);
		if (!backend || !Object.hasOwn(backend, "sessionArg")) continue;
		if (backend.sessionArgs === void 0 && typeof backend.sessionArg === "string") {
			backend.sessionArgs = [backend.sessionArg, "{sessionId}"];
			changes.push(`Moved ${path}.cliBackends.${backendId}.sessionArg → ${path}.cliBackends.${backendId}.sessionArgs.`);
		} else changes.push(`Removed ${path}.cliBackends.${backendId}.sessionArg (sessionArgs already set).`);
		delete backend.sessionArg;
	}
}
function migrateSignalEndpoint(entry, path, changes, inherited) {
	if (!Object.hasOwn(entry, "httpHost") && !Object.hasOwn(entry, "httpPort")) return;
	if (entry.httpUrl === void 0 && typeof inherited?.httpUrl === "string") {
		delete entry.httpHost;
		delete entry.httpPort;
		changes.push(`Removed ${path}.httpHost/httpPort (inherited httpUrl already set).`);
		return;
	}
	if (entry.httpUrl === void 0) {
		const rawHost = typeof (entry.httpHost ?? inherited?.httpHost) === "string" && String(entry.httpHost ?? inherited?.httpHost).trim() ? String(entry.httpHost ?? inherited?.httpHost).trim() : "127.0.0.1";
		const host = rawHost.includes(":") && !rawHost.startsWith("[") ? `[${rawHost}]` : rawHost;
		const effectivePort = entry.httpPort ?? inherited?.httpPort;
		entry.httpUrl = `http://${host}:${typeof effectivePort === "number" ? effectivePort : 8080}`;
		if (entry.autoStart === void 0) entry.autoStart = true;
		changes.push(`Moved ${path}.httpHost/httpPort → ${path}.httpUrl.`);
	} else changes.push(`Removed ${path}.httpHost/httpPort (${path}.httpUrl already set).`);
	delete entry.httpHost;
	delete entry.httpPort;
}
function migrateChannelAliases(raw, changes) {
	const signal = getRecord(getRecord(raw.channels)?.signal);
	if (signal) {
		const inherited = {
			httpUrl: signal.httpUrl,
			httpHost: signal.httpHost,
			httpPort: signal.httpPort
		};
		migrateSignalEndpoint(signal, "channels.signal", changes);
		const accounts = getRecord(signal.accounts);
		if (accounts) for (const [accountId, value] of Object.entries(accounts)) {
			const account = getRecord(value);
			if (account) migrateSignalEndpoint(account, `channels.signal.accounts.${accountId}`, changes, inherited);
		}
	}
	visitChannelEntries(raw, "googlechat", (entry, path) => {
		if (!Object.hasOwn(entry, "serviceAccountRef")) return;
		if (entry.serviceAccount !== void 0) {
			changes.push(`Moved ${path}.serviceAccountRef → ${path}.serviceAccount (SecretRef precedence preserved).`);
			entry.serviceAccount = entry.serviceAccountRef;
			delete entry.serviceAccountRef;
			return;
		}
		entry.serviceAccount = entry.serviceAccountRef;
		delete entry.serviceAccountRef;
		changes.push(`Moved ${path}.serviceAccountRef → ${path}.serviceAccount.`);
	});
}
const RESPONSE_PREFIX_CHANNELS = /* @__PURE__ */ new Set([
	"buzz",
	"clickclack",
	"discord",
	"feishu",
	"googlechat",
	"imessage",
	"irc",
	"matrix",
	"mattermost",
	"msteams",
	"nextcloud-talk",
	"qa-channel",
	"signal",
	"slack",
	"telegram",
	"tlon",
	"twitch",
	"whatsapp",
	"zalo",
	"zalouser",
	"line"
]);
function migrateMessagesResponsePrefix(raw, changes) {
	const messages = getRecord(raw.messages);
	if (!messages || !Object.hasOwn(messages, "responsePrefix")) return;
	const channels = getRecord(raw.channels);
	const configuredChannels = channels ? Object.entries(channels).filter((entry) => entry[0] !== "defaults" && Boolean(getRecord(entry[1]))) : [];
	const supported = configuredChannels.filter(([channelId]) => RESPONSE_PREFIX_CHANNELS.has(channelId));
	const unsupported = configuredChannels.map(([channelId]) => channelId).filter((channelId) => !RESPONSE_PREFIX_CHANNELS.has(channelId));
	let copied = false;
	for (const [, channel] of supported) if (channel.responsePrefix === void 0) {
		channel.responsePrefix = messages.responsePrefix;
		copied = true;
	}
	if (copied) changes.push(`Copied messages.responsePrefix to supported channel blocks while retaining the implicit/custom fallback${unsupported.length > 0 ? ` for: ${unsupported.join(", ")}` : ""}.`);
}
function migratePresenceEnabled(raw, changes) {
	let changed = false;
	const wideArea = getRecord(getRecord(raw.discovery)?.wideArea);
	if (wideArea && Object.hasOwn(wideArea, "enabled")) {
		if (wideArea.enabled === false && typeof wideArea.domain === "string" && wideArea.domain.trim()) {
			delete wideArea.enabled;
			delete wideArea.domain;
			changes.push("Removed disabled discovery.wideArea activation fields; domain presence now enables wide-area discovery.");
			changed = true;
		} else {
			delete wideArea.enabled;
			changed = true;
		}
	}
	return changed;
}
function migrateWebEnabled(raw, changes) {
	const web = getRecord(raw.web);
	if (!web) return false;
	if (Object.hasOwn(web, "enabled")) {
		const whatsapp = ensureRecord(ensureRecord(raw, "channels"), "whatsapp");
		if (web.enabled === false && whatsapp.enabled === true) changes.push("Removed web.enabled=false (channels.whatsapp.enabled already set).");
		if (whatsapp.enabled === void 0) {
			whatsapp.enabled = web.enabled;
			changes.push("Moved web.enabled → channels.whatsapp.enabled.");
		}
	}
	delete raw.web;
	return true;
}
function stripPromptsFromTtsConfig(ttsValue, path, changes) {
	const tts = getRecord(ttsValue);
	const personas = getRecord(tts?.personas);
	if (personas) for (const [personaId, personaValue] of Object.entries(personas)) {
		const persona = getRecord(personaValue);
		if (persona && Object.hasOwn(persona, "prompt")) {
			delete persona.prompt;
			changes.push(`Removed ${path}.personas.${personaId}.prompt; move custom shaping into a speech provider prepareSynthesis implementation.`);
		}
	}
}
function stripTtsPersonaPrompts(raw, changes) {
	stripPromptsFromTtsConfig(raw.tts, "tts", changes);
	visitAgentConfigScopes(raw, (scope, path) => {
		stripPromptsFromTtsConfig(scope.tts, `${path}.tts`, changes);
	});
	const channels = getRecord(raw.channels);
	if (!channels) return;
	for (const channelId of Object.keys(channels)) visitChannelEntries(raw, channelId, (entry, path) => {
		stripPromptsFromTtsConfig(entry.tts, `${path}.tts`, changes);
		stripPromptsFromTtsConfig(getRecord(entry.voice)?.tts, `${path}.voice.tts`, changes);
	});
}
function stripCompactionInstructionConfig(scope, path, changes) {
	const compaction = getRecord(scope.compaction);
	if (!compaction) return;
	let stripped = false;
	for (const key of ["customInstructions", "identifierInstructions"]) if (Object.hasOwn(compaction, key)) {
		delete compaction[key];
		stripped = true;
	}
	const memoryFlush = getRecord(compaction.memoryFlush);
	if (memoryFlush) {
		for (const key of ["prompt", "systemPrompt"]) if (Object.hasOwn(memoryFlush, key)) {
			delete memoryFlush[key];
			stripped = true;
		}
	}
	if (compaction.identifierPolicy === "custom") {
		compaction.identifierPolicy = "strict";
		stripped = true;
	}
	if (stripped) changes.push(`Removed ${path}.compaction custom prompt instructions; use a compaction provider summarize() implementation and before_prompt_build hooks.`);
}
function migrateTierEvalTranche(raw, changes) {
	const initialChangeCount = changes.length;
	let stripped = false;
	stripTtsPersonaPrompts(raw, changes);
	stripped = migratePresenceEnabled(raw, changes) || stripped;
	migrateChannelAliases(raw, changes);
	const session = getRecord(raw.session);
	if (session && Object.hasOwn(session, "idleMinutes")) {
		const reset = getRecord(session.reset) ?? { mode: "idle" };
		if (reset.idleMinutes === void 0) {
			reset.idleMinutes = session.idleMinutes;
			session.reset = reset;
			changes.push("Moved session.idleMinutes → session.reset.idleMinutes.");
		}
		delete session.idleMinutes;
		stripped = true;
	}
	const inheritedExecPolicy = resolveConfiguredExecPolicy(raw);
	migrateExecMode(raw, "root", changes);
	visitAgentConfigScopes(raw, (scope, path) => {
		stripCompactionInstructionConfig(scope, path, changes);
		if (path !== "agents.defaults") migrateExecMode(scope, path, changes, inheritedExecPolicy);
		migrateCliBackendSessionArgs(scope, path, changes);
		for (const retiredPath of TIER_EVAL_RETIRED_AGENT_PATHS) stripped = deleteRetiredPath(scope, retiredPath) || stripped;
	});
	stripped = migrateWebEnabled(raw, changes) || stripped;
	migrateMessagesResponsePrefix(raw, changes);
	for (const retiredPath of TIER_EVAL_RETIRED_ROOT_PATHS) stripped = deleteRetiredPath(raw, retiredPath) || stripped;
	const secrets = getRecord(raw.secrets);
	const providers = getRecord(secrets?.providers);
	if (providers) for (const provider of Object.values(providers)) {
		const entry = getRecord(provider);
		if (entry) {
			stripped = Object.hasOwn(entry, "allowInsecurePath") || Object.hasOwn(entry, "allowSymlinkCommand") || stripped;
			delete entry.allowInsecurePath;
			delete entry.allowSymlinkCommand;
		}
	}
	const installExec = getRecord(getRecord(getRecord(raw.security)?.installPolicy)?.exec);
	if (installExec) {
		stripped = Object.hasOwn(installExec, "allowInsecurePath") || Object.hasOwn(installExec, "allowSymlinkCommand") || stripped;
		delete installExec.allowInsecurePath;
		delete installExec.allowSymlinkCommand;
	}
	if (stripped || changes.length > initialChangeCount) changes.push("Applied tier-eval tranche retirements; canonical settings and built-in defaults now apply.");
}
//#endregion
//#region src/commands/doctor/shared/legacy-config-migrations.runtime.retired.ts
const rule$1 = (path, message, match) => ({
	path,
	message: `${message} Run "openclaw doctor --fix".`,
	...match ? { match } : {}
});
function moveKey(owner, legacyKey, canonicalKey, path, changes) {
	if (!owner || !Object.hasOwn(owner, legacyKey)) return;
	if (owner[canonicalKey] === void 0) {
		owner[canonicalKey] = owner[legacyKey];
		changes.push(`Moved ${path}.${legacyKey} → ${path}.${canonicalKey}.`);
	} else changes.push(`Removed ${path}.${legacyKey} (${path}.${canonicalKey} already set).`);
	delete owner[legacyKey];
}
function migrateMessageCrossContext(raw, changes) {
	const globalMessage = getRecord(getRecord(raw.tools)?.message);
	const globalBypass = globalMessage?.allowCrossContextSend;
	const globalCrossContext = getRecord(globalMessage?.crossContext);
	const migrate = (message, path, agent) => {
		if (!message) return;
		const legacy = message.allowCrossContextSend;
		const inheritedBypass = agent && globalBypass === true;
		if (legacy === void 0 && !inheritedBypass) return;
		const crossContext = getRecord(message.crossContext) ?? {};
		if ((legacy ?? (agent ? globalBypass : void 0)) === true) message.crossContext = {
			...crossContext,
			allowWithinProvider: true,
			allowAcrossProviders: true
		};
		else if (inheritedBypass) message.crossContext = {
			...crossContext,
			allowWithinProvider: (crossContext.allowWithinProvider ?? globalCrossContext?.allowWithinProvider) !== false,
			allowAcrossProviders: (crossContext.allowAcrossProviders ?? globalCrossContext?.allowAcrossProviders) === true
		};
		delete message.allowCrossContextSend;
		changes.push(`Moved ${path}.allowCrossContextSend → ${path}.crossContext.`);
	};
	visitAgentConfigScopes(raw, (scope, path) => {
		if (path !== "agents.defaults") migrate(getRecord(getRecord(scope.tools)?.message), `${path}.tools.message`, true);
	});
	migrate(globalMessage, "tools.message", false);
}
function migrateTruncateAfterCompaction(raw, changes) {
	const compaction = getRecord(getRecord(getRecord(raw.agents)?.defaults)?.compaction);
	if (!compaction || !Object.hasOwn(compaction, "truncateAfterCompaction")) return;
	if (compaction.truncateAfterCompaction === false && Object.hasOwn(compaction, "maxActiveTranscriptBytes")) {
		delete compaction.maxActiveTranscriptBytes;
		changes.push("Removed maxActiveTranscriptBytes to preserve truncateAfterCompaction: false.");
	}
	delete compaction.truncateAfterCompaction;
	changes.push("Removed retired agents.defaults.compaction.truncateAfterCompaction.");
}
function migrateFinalLayoutRenames(raw, changes) {
	const agents = getRecord(raw.agents);
	const defaults = getRecord(agents?.defaults);
	moveKey(defaults, "pdfMaxBytesMb", "pdfMaxMb", "agents.defaults", changes);
	if (defaults) {
		const mediaModels = getRecord(defaults.mediaModels) ?? {};
		for (const [legacyKey, canonicalKey] of [
			["imageGenerationModel", "image"],
			["videoGenerationModel", "video"],
			["musicGenerationModel", "music"]
		]) {
			if (!Object.hasOwn(defaults, legacyKey)) continue;
			if (mediaModels[canonicalKey] === void 0) {
				mediaModels[canonicalKey] = defaults[legacyKey];
				changes.push(`Moved agents.defaults.${legacyKey} → agents.defaults.mediaModels.${canonicalKey}.`);
			} else changes.push(`Removed agents.defaults.${legacyKey} (agents.defaults.mediaModels.${canonicalKey} already set).`);
			delete defaults[legacyKey];
		}
		if (Object.keys(mediaModels).length > 0) defaults.mediaModels = mediaModels;
	}
	visitAgentConfigScopes(raw, (scope, path) => {
		moveKey(getRecord(getRecord(scope.tools)?.exec), "timeoutSec", "timeoutSeconds", `${path}.tools.exec`, changes);
		moveKey(getRecord(getRecord(scope.sandbox)?.browser), "enableNoVnc", "noVncEnabled", `${path}.sandbox.browser`, changes);
	});
	moveKey(getRecord(getRecord(raw.tools)?.exec), "timeoutSec", "timeoutSeconds", "tools.exec", changes);
	const env = getRecord(raw.env);
	if (env) {
		const vars = getRecord(env.vars) ?? {};
		let moved = false;
		for (const [key, value] of Object.entries(env)) {
			if (key === "vars" || key === "shellEnv" || typeof value !== "string") continue;
			if (vars[key] === void 0) {
				vars[key] = value;
				changes.push(`Moved env.${key} → env.vars.${key}.`);
			} else changes.push(`Removed env.${key} (env.vars.${key} already set).`);
			delete env[key];
			moved = true;
		}
		if (moved) env.vars = vars;
	}
	const browser = getRecord(raw.browser);
	const ssrfPolicy = getRecord(browser?.ssrfPolicy);
	if (ssrfPolicy && Array.isArray(ssrfPolicy.hostnameAllowlist)) {
		const canonical = Array.isArray(ssrfPolicy.allowedHostnames) ? ssrfPolicy.allowedHostnames : [];
		ssrfPolicy.allowedHostnames = [...new Set([...canonical, ...ssrfPolicy.hostnameAllowlist].filter((value) => typeof value === "string"))];
		delete ssrfPolicy.hostnameAllowlist;
		changes.push("Merged browser.ssrfPolicy.hostnameAllowlist → allowedHostnames.");
	}
	const legacyMedia = getRecord(raw.media);
	if (legacyMedia) {
		const attachments = ensureRecord(raw, "attachments");
		mergeMissing(attachments, legacyMedia);
		delete raw.media;
		changes.push("Moved media → attachments.");
	}
	const audit = getRecord(raw.audit);
	if (audit) {
		const logging = ensureRecord(raw, "logging");
		const canonicalAudit = getRecord(logging.audit) ?? {};
		mergeMissing(canonicalAudit, audit);
		logging.audit = canonicalAudit;
		delete raw.audit;
		changes.push("Moved audit → logging.audit.");
	}
	const nodes = getRecord(getRecord(raw.gateway)?.nodes);
	if (nodes) {
		const skills = getRecord(nodes.skills);
		if (skills && Object.hasOwn(skills, "enabled")) {
			if (nodes.allowSkills === void 0) nodes.allowSkills = skills.enabled;
			delete nodes.skills;
			changes.push("Moved gateway.nodes.skills.enabled → gateway.nodes.allowSkills.");
		}
		const commands = getRecord(nodes.commands) ?? {};
		if (Object.hasOwn(nodes, "allowCommands")) {
			if (commands.allow === void 0) commands.allow = nodes.allowCommands;
			delete nodes.allowCommands;
			changes.push("Moved gateway.nodes.allowCommands → gateway.nodes.commands.allow.");
		}
		if (Object.hasOwn(nodes, "denyCommands")) {
			if (commands.deny === void 0) commands.deny = nodes.denyCommands;
			delete nodes.denyCommands;
			changes.push("Moved gateway.nodes.denyCommands → gateway.nodes.commands.deny.");
		}
		if (Object.keys(commands).length > 0) nodes.commands = commands;
	}
	visitChannelEntries(raw, "slack", (entry, path) => {
		moveKey(entry, "identity", "postAs", path, changes);
	});
}
function migrateFinalLayoutKills(raw, changes) {
	const defaults = getRecord(getRecord(raw.agents)?.defaults);
	if (defaults && Object.hasOwn(defaults, "promptOverlays")) {
		const personality = getRecord(getRecord(defaults.promptOverlays)?.gpt5)?.personality;
		if (personality !== void 0) {
			const openaiConfig = ensureRecord(ensureRecord(ensureRecord(ensureRecord(raw, "plugins"), "entries"), "openai"), "config");
			if (openaiConfig.personality === void 0) {
				openaiConfig.personality = personality;
				changes.push("Moved agents.defaults.promptOverlays.gpt5.personality → plugins.entries.openai.config.personality.");
			} else changes.push("Removed agents.defaults.promptOverlays.gpt5.personality (plugins.entries.openai.config.personality already set).");
		} else changes.push("Removed agents.defaults.promptOverlays; built-in behavior now applies.");
		delete defaults.promptOverlays;
	}
	for (const key of [
		"envelopeTimestamp",
		"envelopeElapsed",
		"envelopeTimezone",
		"timeFormat",
		"bootstrapPromptTruncationWarning",
		"mediaGenerationAutoProviderFallback"
	]) if (defaults && Object.hasOwn(defaults, key)) {
		delete defaults[key];
		changes.push(`Removed agents.defaults.${key}; built-in behavior now applies.`);
	}
	const diagnostics = getRecord(raw.diagnostics);
	const otel = getRecord(diagnostics?.otel);
	const captureContent = getRecord(otel?.captureContent);
	if (otel && captureContent) {
		otel.captureContent = typeof captureContent.enabled === "boolean" ? captureContent.enabled : Object.entries(captureContent).some(([key, value]) => key !== "enabled" && value === true);
		changes.push("Collapsed diagnostics.otel.captureContent to a boolean.");
	}
	const cacheTrace = getRecord(diagnostics?.cacheTrace);
	if (cacheTrace && (Object.keys(cacheTrace).some((key) => key !== "enabled") || cacheTrace.enabled !== void 0 && typeof cacheTrace.enabled !== "boolean")) {
		diagnostics.cacheTrace = { enabled: cacheTrace.enabled === true };
		changes.push("Removed diagnostics.cacheTrace detail fields; only enabled remains.");
	}
	const attachments = getRecord(raw.attachments);
	if (attachments && Object.hasOwn(attachments, "preserveFilenames")) {
		delete attachments.preserveFilenames;
		changes.push("Removed attachments.preserveFilenames; temp-safe names now always apply.");
	}
	const browser = getRecord(raw.browser);
	if (browser && Object.hasOwn(browser, "color")) {
		delete browser.color;
		changes.push("Removed browser.color; the built-in color now applies.");
	}
	const profiles = getRecord(browser?.profiles);
	if (profiles) for (const [profileId, value] of Object.entries(profiles)) {
		const profile = getRecord(value);
		if (profile && Object.hasOwn(profile, "color")) {
			delete profile.color;
			changes.push(`Removed browser.profiles.${profileId}.color.`);
		}
	}
	visitChannelEntries(raw, "discord", (entry, path) => {
		const autoPresence = getRecord(entry.autoPresence);
		for (const key of [
			"healthyText",
			"degradedText",
			"exhaustedText"
		]) if (autoPresence && Object.hasOwn(autoPresence, key)) {
			delete autoPresence[key];
			changes.push(`Removed ${path}.autoPresence.${key}.`);
		}
		const components = getRecord(getRecord(entry.ui)?.components);
		if (components && Object.hasOwn(components, "accentColor")) {
			delete components.accentColor;
			changes.push(`Removed ${path}.ui.components.accentColor.`);
			const ui = getRecord(entry.ui);
			if (Object.keys(components).length === 0 && ui) delete ui.components;
			if (ui && Object.keys(ui).length === 0) delete entry.ui;
		}
	});
	const messages = getRecord(raw.messages);
	const statusReactions = getRecord(messages?.statusReactions);
	if (statusReactions && Object.hasOwn(statusReactions, "emojis")) {
		delete statusReactions.emojis;
		changes.push("Removed messages.statusReactions.emojis; curated defaults now apply.");
	}
	if (messages && Object.hasOwn(messages, "removeAckAfterReply")) {
		delete messages.removeAckAfterReply;
		changes.push("Removed messages.removeAckAfterReply; acknowledgements are retained.");
	}
	visitChannelEntries(raw, "whatsapp", (entry, path) => {
		moveKey(entry, "messagePrefix", "responsePrefix", path, changes);
	});
	visitChannelEntries(raw, "slack", (entry, path) => {
		const socketMode = getRecord(entry.socketMode);
		for (const key of [
			"clientPingTimeout",
			"serverPingTimeout",
			"pingPongLoggingEnabled"
		]) if (socketMode && Object.hasOwn(socketMode, key)) {
			delete socketMode[key];
			changes.push(`Removed ${path}.socketMode.${key}.`);
		}
		if (socketMode && Object.keys(socketMode).length === 0) delete entry.socketMode;
	});
	visitChannelEntries(raw, "imessage", (entry, path) => {
		if (Object.hasOwn(entry, "coalesceSameSenderDms")) {
			delete entry.coalesceSameSenderDms;
			changes.push(`Removed ${path}.coalesceSameSenderDms.`);
		}
	});
	const commands = getRecord(raw.commands);
	for (const key of ["ownerDisplay", "ownerDisplaySecret"]) if (commands && Object.hasOwn(commands, key)) {
		delete commands[key];
		changes.push(`Removed commands.${key}; owner ids now render raw.`);
	}
	const cron = getRecord(raw.cron);
	const failureDestination = getRecord(cron?.failureDestination);
	if (cron && failureDestination) {
		const failureAlert = getRecord(cron.failureAlert) ?? {};
		mergeMissing(failureAlert, failureDestination);
		cron.failureAlert = failureAlert;
		delete cron.failureDestination;
		changes.push("Merged cron.failureDestination → cron.failureAlert.");
	}
	const gateway = getRecord(raw.gateway);
	const reload = getRecord(gateway?.reload);
	if (reload?.mode === "restart" || reload?.mode === "hot") {
		reload.mode = "hybrid";
		changes.push("Mapped gateway.reload.mode to hybrid.");
	}
	const logging = getRecord(raw.logging);
	if (logging?.consoleStyle === "compact") {
		logging.consoleStyle = "pretty";
		changes.push("Mapped logging.consoleStyle compact → pretty.");
	}
	const controlUi = getRecord(gateway?.controlUi);
	if (controlUi && Object.hasOwn(controlUi, "chatMessageMaxWidth")) {
		delete controlUi.chatMessageMaxWidth;
		changes.push("Removed gateway.controlUi.chatMessageMaxWidth; chat width is now browser-local.");
	}
}
function removeUiAssistantIdentity(raw, changes) {
	const ui = getRecord(raw.ui);
	if (!ui || !Object.hasOwn(ui, "assistant")) return;
	delete ui.assistant;
	if (Object.keys(ui).length === 0) delete raw.ui;
	changes.push("Removed retired ui.assistant; configure agents.list[].identity instead.");
}
const LEGACY_CONFIG_MIGRATIONS_RUNTIME_RETIRED = [
	LEGACY_CONFIG_MIGRATION_RUNTIME_MEMORY_QMD,
	defineLegacyConfigMigration({
		id: "runtime.messages-suppress-tool-errors",
		describe: "Remove retired tool failure warning suppression",
		legacyRules: [rule$1(["messages", "suppressToolErrors"], "messages.suppressToolErrors is retired; tool failure warnings now appear only when a run ends without a reply.")],
		apply: (raw, changes) => {
			const messages = getRecord(raw.messages);
			if (!messages || !Object.hasOwn(messages, "suppressToolErrors")) return;
			delete messages.suppressToolErrors;
			changes.push("Removed messages.suppressToolErrors (tool failure warnings now appear only when a run ends without a reply).");
		}
	}),
	defineLegacyConfigMigration({
		id: "runtime.retired-internal-hook-handlers",
		describe: "Remove retired internal hook handler registrations",
		legacyRules: [{
			path: [
				"hooks",
				"internal",
				"handlers"
			],
			message: "hooks.internal.handlers is retired. Move each module to a managed/workspace hook directory with HOOK.md + handler file before running \"openclaw doctor --fix\"; the fix removes retired registrations and does not materialize executable files."
		}],
		apply: (raw, changes) => {
			const internal = getRecord(getRecord(raw.hooks)?.internal);
			if (!internal || !Object.hasOwn(internal, "handlers")) return;
			delete internal.handlers;
			changes.push("Removed retired hooks.internal.handlers registrations; hook files must be migrated separately.");
			const entries = getRecord(internal.entries);
			const extraDirs = getRecord(internal.load)?.extraDirs;
			const hasNamedEntries = Boolean(entries && Object.keys(entries).length > 0);
			const hasExtraDirs = Array.isArray(extraDirs) && extraDirs.some((dir) => typeof dir === "string" && dir.trim().length > 0);
			if (internal.enabled === true && !hasNamedEntries && !hasExtraDirs) {
				delete internal.enabled;
				changes.push("Removed legacy-only hooks.internal.enabled to avoid enabling broad hook discovery.");
			}
		}
	}),
	defineLegacyConfigMigration({
		id: "runtime.doctor-tier-eval-tranche",
		describe: "Consolidate approved tier-eval configuration surfaces",
		legacyRules: [rule$1([], "Approved tier-eval configuration surfaces were consolidated.", (_value, root) => {
			const changes = [];
			migrateTierEvalTranche(structuredClone(root), changes);
			return changes.length > 0;
		})],
		apply: migrateTierEvalTranche
	}),
	defineLegacyConfigMigration({
		id: "runtime.final-layout-polish",
		describe: "Normalize final configuration layout names",
		legacyRules: [rule$1([], "Final layout aliases were retired.", (_value, root) => {
			const changes = [];
			migrateFinalLayoutRenames(structuredClone(root), changes);
			return changes.length > 0;
		})],
		apply: migrateFinalLayoutRenames
	}),
	defineLegacyConfigMigration({
		id: "runtime.final-layout-kills",
		describe: "Remove final layout tuning knobs",
		legacyRules: [rule$1([], "Final layout tuning knobs were retired.", (_value, root) => {
			const changes = [];
			migrateFinalLayoutKills(structuredClone(root), changes);
			return changes.length > 0;
		})],
		apply: migrateFinalLayoutKills
	}),
	defineLegacyConfigMigration({
		id: "runtime.media-models-consolidation",
		describe: "Consolidate per-capability media model configuration",
		legacyRules: [rule$1(["tools", "media"], "Per-capability media model settings moved to capability-tagged tools.media.models entries.", hasLegacyMediaCapabilityConfig)],
		apply: (raw, changes) => {
			migrateMediaDeepgram(raw, changes);
			consolidateMediaCapabilityConfig(raw, changes);
		}
	}),
	defineLegacyConfigMigration({
		id: "runtime.config-tranche",
		describe: "Migrate retired config-tranche options",
		legacyRules: [rule$1([], "Presentation-only preferences and duplicate tuning options moved to canonical defaults.", (_value, root) => hasConfigTrancheLegacyKeys(root))],
		apply: migrateConfigTranche
	}),
	defineLegacyConfigMigration({
		id: "runtime.tuning-knobs-purge",
		describe: "Remove retired runtime tuning knobs",
		legacyRules: [rule$1([], "Numeric runtime tuning knobs were retired and now use built-in defaults.", (_value, root) => stripRetiredTuningKnobs(structuredClone(root)))],
		apply: (raw, changes) => {
			if (stripRetiredTuningKnobs(raw)) changes.push("Removed retired runtime tuning knobs; built-in defaults now apply.");
		}
	}),
	defineLegacyConfigMigration({
		id: "runtime.ui-assistant-identity",
		describe: "Remove the retired UI assistant identity override",
		legacyRules: [rule$1(["ui", "assistant"], "ui.assistant was retired; use agents.list[].identity instead.")],
		apply: removeUiAssistantIdentity
	}),
	defineLegacyConfigMigration({
		id: "runtime.retired-config-keys",
		describe: "Migrate retired root and tool config keys",
		legacyRules: [
			rule$1(["tui"], "tui was retired and is ignored."),
			rule$1(["commands", "modelsWrite"], "commands.modelsWrite was retired and is ignored."),
			rule$1(["messages", "messagePrefix"], "messages.messagePrefix moved to channels.whatsapp.responsePrefix."),
			rule$1([
				"tools",
				"media",
				"asyncCompletion"
			], "tools.media.asyncCompletion.directSend was retired and is ignored."),
			rule$1([
				"tools",
				"message",
				"allowCrossContextSend"
			], "tools.message.allowCrossContextSend moved to tools.message.crossContext."),
			rule$1(["agents"], "Per-agent tools.message.allowCrossContextSend moved to tools.message.crossContext on the same agent.", (value) => {
				let found = false;
				visitAgentConfigScopes({ agents: value }, (scope, path) => {
					found ||= path !== "agents.defaults" && Object.hasOwn(getRecord(getRecord(scope.tools)?.message) ?? {}, "allowCrossContextSend");
				});
				return found;
			}),
			rule$1(["tools", "experimental"], "tools.experimental.planTool moved to tools.updatePlan."),
			rule$1([
				"talk",
				"realtime",
				"voice"
			], "talk.realtime.voice moved to talk.realtime.speakerVoice."),
			rule$1(["channels", "discord"], "Discord realtime voice aliases moved to speakerVoice.", hasDiscordRealtimeVoice),
			rule$1(["tools", "media"], "Legacy Deepgram options moved to providerOptions.deepgram.", hasMediaDeepgram),
			rule$1([
				"agents",
				"defaults",
				"compaction",
				"truncateAfterCompaction"
			], "agents.defaults.compaction.truncateAfterCompaction is retired; byte-triggered compaction now opts in via maxActiveTranscriptBytes alone.")
		],
		apply: (raw, changes) => {
			migrateTruncateAfterCompaction(raw, changes);
			if (Object.hasOwn(raw, "tui")) {
				delete raw.tui;
				changes.push("Removed retired tui config; the footer uses the default compact display.");
			}
			const commands = getRecord(raw.commands);
			if (commands && Object.hasOwn(commands, "modelsWrite")) {
				delete commands.modelsWrite;
				changes.push("Removed retired commands.modelsWrite.");
			}
			const messages = getRecord(raw.messages);
			if (messages && Object.hasOwn(messages, "messagePrefix")) {
				const whatsapp = ensureRecord(ensureRecord(raw, "channels"), "whatsapp");
				if (whatsapp.responsePrefix === void 0) {
					whatsapp.responsePrefix = messages.messagePrefix;
					changes.push("Moved messages.messagePrefix → channels.whatsapp.responsePrefix.");
				} else changes.push("Removed messages.messagePrefix (channels.whatsapp.responsePrefix already set).");
				delete messages.messagePrefix;
			}
			const media = getRecord(getRecord(raw.tools)?.media);
			if (media && Object.hasOwn(media, "asyncCompletion")) {
				delete media.asyncCompletion;
				changes.push("Removed retired tools.media.asyncCompletion.directSend.");
			}
			migrateMessageCrossContext(raw, changes);
			const tools = getRecord(raw.tools);
			const experimentalTools = getRecord(tools?.experimental);
			if (tools && experimentalTools) {
				if (Object.hasOwn(experimentalTools, "planTool") && tools.updatePlan === void 0) {
					tools.updatePlan = experimentalTools.planTool;
					changes.push("Moved tools.experimental.planTool → tools.updatePlan.");
				} else changes.push("Removed tools.experimental; tools.updatePlan now owns the switch.");
				delete tools.experimental;
			}
			const talkRealtime = getRecord(getRecord(raw.talk)?.realtime);
			if (talkRealtime) moveVoice(talkRealtime, "talk.realtime", changes);
			const channels = getRecord(raw.channels);
			if (channels) migrateDiscordVoice(channels, changes);
			migrateMediaDeepgram(raw, changes);
		}
	})
];
//#endregion
//#region src/commands/doctor/shared/legacy-config-migrations.runtime.secrets-egress.ts
const HOST_KEYS = ["bypassHosts", "allowedHosts"];
const rule = (path, message, match) => ({
	path,
	message: `${message} Run "openclaw doctor --fix".`,
	...match ? { match } : {}
});
function isValidExactHostname(value) {
	try {
		normalizeExactAllowedHost(value);
		return true;
	} catch {
		return false;
	}
}
const LEGACY_CONFIG_MIGRATION_RUNTIME_SECRETS_EGRESS = defineLegacyConfigMigration({
	id: "runtime.secrets-egress-proxy-hosts",
	describe: "Drop unusable secret egress proxy host entries",
	legacyRules: HOST_KEYS.map((key) => rule([
		"secrets",
		"egressProxy",
		key
	], `secrets.egressProxy.${key} contains entries that are not usable hostnames.`, (value) => Array.isArray(value) && value.some((entry) => typeof entry !== "string" || !isValidExactHostname(entry)))),
	apply(raw, changes) {
		const egressProxy = getRecord(getRecord(raw.secrets)?.egressProxy);
		if (!egressProxy) return;
		for (const key of HOST_KEYS) {
			const hosts = egressProxy[key];
			if (!Array.isArray(hosts)) continue;
			const validHosts = hosts.filter((entry) => typeof entry === "string" && isValidExactHostname(entry));
			if (validHosts.length === hosts.length) continue;
			const invalidHosts = hosts.filter((entry) => typeof entry !== "string" || !isValidExactHostname(entry));
			if (validHosts.length > 0) egressProxy[key] = validHosts;
			else delete egressProxy[key];
			changes.push(`Removed unusable secrets.egressProxy.${key} entries: ${invalidHosts.map((entry) => JSON.stringify(entry)).join(", ")}.`);
		}
	}
});
//#endregion
//#region src/commands/doctor/shared/legacy-config-migrations.runtime.session.ts
function hasLegacyRotateBytes(value) {
	const maintenance = getRecord(value);
	return Boolean(maintenance && Object.hasOwn(maintenance, "rotateBytes"));
}
function hasLegacyParentForkMaxTokens(value) {
	const session = getRecord(value);
	return Boolean(session && Object.hasOwn(session, "parentForkMaxTokens"));
}
/** Match only parser-valid values that resolve to an unsafe zero-duration cutoff. */
function isZeroDuration(val) {
	if (val === false) return false;
	const normalized = normalizeStringifiedOptionalString(val);
	if (!normalized) return false;
	try {
		return parseDurationMs(normalized, { defaultUnit: "d" }) <= 0;
	} catch {
		return false;
	}
}
function hasZeroDuration(raw, key) {
	const maintenance = getRecord(raw);
	if (!maintenance || !Object.hasOwn(maintenance, key)) return false;
	return isZeroDuration(maintenance[key]);
}
const LEGACY_SESSION_MAINTENANCE_ROTATE_BYTES_RULE = {
	path: ["session", "maintenance"],
	message: "session.maintenance.rotateBytes is deprecated and ignored; run \"openclaw doctor --fix\" to remove it.",
	match: hasLegacyRotateBytes
};
const LEGACY_SESSION_PARENT_FORK_MAX_TOKENS_RULE = {
	path: ["session"],
	message: "session.parentForkMaxTokens was removed; parent fork sizing is automatic. Run \"openclaw doctor --fix\" to remove it.",
	match: hasLegacyParentForkMaxTokens
};
/** Legacy config migration specs for session runtime config compatibility. */
const LEGACY_CONFIG_MIGRATIONS_RUNTIME_SESSION = [
	defineLegacyConfigMigration({
		id: "session.canonical-aliases",
		describe: "Move session aliases to canonical keys",
		legacyRules: [{
			path: [
				"session",
				"maintenance",
				"pruneDays"
			],
			message: "session.maintenance.pruneDays was renamed to pruneAfter. Run \"openclaw doctor --fix\"."
		}, {
			path: [
				"session",
				"resetByType",
				"dm"
			],
			message: "session.resetByType.dm was renamed to direct. Run \"openclaw doctor --fix\"."
		}],
		apply: (raw, changes) => {
			const session = getRecord(raw.session);
			const maintenance = getRecord(session?.maintenance);
			if (maintenance && Object.hasOwn(maintenance, "pruneDays")) {
				if (maintenance.pruneAfter === void 0) {
					maintenance.pruneAfter = maintenance.pruneDays;
					changes.push("Moved session.maintenance.pruneDays → session.maintenance.pruneAfter.");
				} else changes.push("Removed session.maintenance.pruneDays (pruneAfter already set).");
				delete maintenance.pruneDays;
			}
			const resetByType = getRecord(session?.resetByType);
			if (resetByType && Object.hasOwn(resetByType, "dm")) {
				if (resetByType.direct === void 0) {
					resetByType.direct = resetByType.dm;
					changes.push("Moved session.resetByType.dm → session.resetByType.direct.");
				} else changes.push("Removed session.resetByType.dm (direct already set).");
				delete resetByType.dm;
			}
		}
	}),
	defineLegacyConfigMigration({
		id: "session.maintenance.rotateBytes",
		describe: "Remove deprecated session.maintenance.rotateBytes",
		legacyRules: [LEGACY_SESSION_MAINTENANCE_ROTATE_BYTES_RULE],
		apply: (raw, changes) => {
			const maintenance = getRecord(getRecord(raw.session)?.maintenance);
			if (!maintenance || !Object.hasOwn(maintenance, "rotateBytes")) return;
			delete maintenance.rotateBytes;
			changes.push("Removed deprecated session.maintenance.rotateBytes.");
		}
	}),
	defineLegacyConfigMigration({
		id: "session.parentForkMaxTokens",
		describe: "Remove legacy session.parentForkMaxTokens",
		legacyRules: [LEGACY_SESSION_PARENT_FORK_MAX_TOKENS_RULE],
		apply: (raw, changes) => {
			const session = getRecord(raw.session);
			if (!session || !Object.hasOwn(session, "parentForkMaxTokens")) return;
			delete session.parentForkMaxTokens;
			changes.push("Removed session.parentForkMaxTokens; parent fork sizing is automatic.");
		}
	}),
	defineLegacyConfigMigration({
		id: "session.maintenance.zero-duration-retention",
		describe: "Remove zero-duration session maintenance values so documented defaults apply",
		legacyRules: [{
			path: ["session", "maintenance"],
			message: "session.maintenance.pruneAfter is a zero duration — this causes immediate deletion of eligible stale/non-preserved session entries. Run \"openclaw doctor --fix\" to remove it so the documented 30d default applies.",
			match: (raw) => hasZeroDuration(raw, "pruneAfter")
		}, {
			path: ["session", "maintenance"],
			message: "session.maintenance.resetArchiveRetention is a zero duration — this causes immediate deletion of all reset transcript archives. Run \"openclaw doctor --fix\" to remove it so the keep-by-default archive retention applies.",
			match: (raw) => hasZeroDuration(raw, "resetArchiveRetention")
		}],
		apply: (raw, changes) => {
			const maintenance = getRecord(getRecord(raw.session)?.maintenance);
			if (!maintenance) return;
			for (const key of ["resetArchiveRetention", "pruneAfter"]) {
				if (!Object.hasOwn(maintenance, key)) continue;
				const val = maintenance[key];
				if (!isZeroDuration(val)) continue;
				const label = String(val);
				const fieldPath = key === "resetArchiveRetention" ? "session.maintenance.resetArchiveRetention" : "session.maintenance.pruneAfter";
				delete maintenance[key];
				const outcome = key === "resetArchiveRetention" ? "keep-by-default archive retention applies" : "30d session-pruning default applies";
				changes.push(`Removed ${fieldPath} "${label}" (zero duration); ${outcome}.`);
			}
		}
	})
];
//#endregion
//#region src/commands/doctor/shared/legacy-config-migrations.runtime.skills.ts
const LEGACY_CONFIG_MIGRATIONS_RUNTIME_SKILLS = [defineLegacyConfigMigration({
	id: "skills.workshop.autonomous.enabled->mode",
	describe: "Migrate Skill Workshop autonomy to its three-position mode.",
	legacyRules: [{
		path: [
			"skills",
			"workshop",
			"autonomous",
			"enabled"
		],
		message: "skills.workshop.autonomous.enabled is retired; use skills.workshop.autonomous.mode. Run \"openclaw doctor --fix\"."
	}],
	apply: (raw, changes) => {
		const autonomous = getRecord(getRecord(getRecord(raw.skills)?.workshop)?.autonomous);
		if (!autonomous || !Object.hasOwn(autonomous, "enabled")) return;
		if (autonomous.mode === void 0) {
			const mode = autonomous.enabled === false ? "off" : "propose";
			autonomous.mode = mode;
			changes.push(`Mapped skills.workshop.autonomous.enabled to mode: "${mode}".`);
		} else changes.push("Removed skills.workshop.autonomous.enabled because autonomous.mode is already set.");
		delete autonomous.enabled;
	}
})];
const LEGACY_CONFIG_MIGRATIONS_RUNTIME_SYSTEM_AGENT = [defineLegacyConfigMigration({
	id: "crestodian-retired",
	describe: "Remove retired system-agent config",
	legacyRules: [{
		path: ["crestodian"],
		message: "crestodian config was retired; system-agent rescue now uses built-in policy. Run \"openclaw doctor --fix\" to remove it."
	}],
	apply: (raw, changes) => {
		if (!Object.hasOwn(raw, "crestodian")) return;
		delete raw.crestodian;
		changes.push("Removed retired crestodian config; system-agent rescue uses built-in policy.");
	}
})];
//#endregion
//#region src/commands/doctor/shared/legacy-config-migrations.runtime.tool-names.ts
const TOOL_POLICY_ROOTS = [
	"tools",
	"agents",
	"channels",
	"gateway"
];
const LEGACY_CONFIG_MIGRATIONS_RUNTIME_TOOL_NAMES = [{
	id: "tools.suggest-task-name",
	describe: "Migrate the task-suggestion tool in persisted tool policies",
	migration: TASK_SUGGESTION_TOOL_NAME_MIGRATION
}, {
	id: "tools.view-image-name",
	describe: "Migrate the image inspection tool in persisted tool policies",
	migration: IMAGE_INSPECTION_TOOL_NAME_MIGRATION
}].map((migration) => defineLegacyConfigMigration({
	id: migration.id,
	describe: migration.describe,
	legacyRules: TOOL_POLICY_ROOTS.map((root) => ({
		path: [root],
		message: `Tool policies still rely on legacy ${migration.migration.legacyName} coverage; run "openclaw doctor --fix" to preserve equivalent ${migration.migration.canonicalName} access.`,
		match: (value) => findLegacyToolNamePaths(value, migration.migration, [root]).length > 0
	})),
	apply: (raw, changes) => {
		if (!isRecord(raw)) return;
		const paths = TOOL_POLICY_ROOTS.flatMap((root) => migrateLegacyToolNamePolicies(raw[root], migration.migration, [root]));
		if (paths.length === 0) return;
		changes.push(`Migrated legacy ${migration.migration.legacyName} policy coverage to ${migration.migration.canonicalName} in ${paths.join(", ")}.`);
	}
}));
//#endregion
//#region src/commands/doctor/shared/legacy-config-migrations.runtime.tts.ts
const LEGACY_TTS_PROVIDER_KEYS = [
	"openai",
	"elevenlabs",
	"microsoft",
	"edge"
];
const LEGACY_TTS_PLUGIN_IDS = /* @__PURE__ */ new Set(["voice-call"]);
const CHANNEL_ROOT_TTS_UNSUPPORTED_IDS = /* @__PURE__ */ new Set(["discord"]);
function isLegacyEdgeProviderId(value) {
	return typeof value === "string" && value.trim().toLowerCase() === "edge";
}
function hasLegacyTtsProviderKeys(value) {
	const tts = getRecord(value);
	if (!tts) return false;
	if (isLegacyEdgeProviderId(tts.provider)) return true;
	if (LEGACY_TTS_PROVIDER_KEYS.some((key) => Object.hasOwn(tts, key))) return true;
	const providers = getRecord(tts.providers);
	return Boolean(providers && Object.hasOwn(providers, "edge"));
}
function hasLegacyTtsEnabled(value) {
	return typeof getRecord(value)?.enabled === "boolean";
}
function hasLegacySpeakerSelectionKeys(value) {
	const config = getRecord(value);
	if (!config) return false;
	return Object.hasOwn(config, "voice") || Object.hasOwn(config, "voiceName") || Object.hasOwn(config, "voiceId");
}
function hasLegacyTtsSpeakerSelection(value) {
	for (const [config] of visitLegacyTtsSpeakerConfigs(value, "")) if (hasLegacySpeakerSelectionKeys(config)) return true;
	return false;
}
function hasLegacyTtsInLocations(raw, matcher) {
	for (const [tts] of visitKnownTtsConfigLocations(raw)) if (matcher(tts)) return true;
	return false;
}
function supportsChannelRootTtsMigration(channelId) {
	return !CHANNEL_ROOT_TTS_UNSUPPORTED_IDS.has(channelId.trim().toLowerCase());
}
function getOrCreateTtsProviders(tts) {
	const providers = getRecord(tts.providers) ?? {};
	tts.providers = providers;
	return providers;
}
function mergeLegacyTtsProviderConfig(tts, legacyKey, providerId, source = "tts") {
	const legacyOwner = source === "providers" ? getRecord(tts.providers) : tts;
	const legacyValue = getRecord(legacyOwner?.[legacyKey]);
	if (!legacyOwner || !legacyValue) return false;
	const providers = source === "providers" ? legacyOwner : getOrCreateTtsProviders(tts);
	const existing = getRecord(providers[providerId]) ?? {};
	const merged = structuredClone(existing);
	mergeMissing(merged, legacyValue);
	providers[providerId] = merged;
	delete legacyOwner[legacyKey];
	return true;
}
function migrateLegacyTtsConfig(tts, pathLabel, changes) {
	if (!tts) return;
	if (isLegacyEdgeProviderId(tts.provider)) {
		tts.provider = "microsoft";
		changes.push(`Moved ${pathLabel}.provider "edge" → "microsoft".`);
	}
	for (const [legacyKey, providerId, source] of [
		[
			"openai",
			"openai",
			"tts"
		],
		[
			"elevenlabs",
			"elevenlabs",
			"tts"
		],
		[
			"microsoft",
			"microsoft",
			"tts"
		],
		[
			"edge",
			"microsoft",
			"providers"
		],
		[
			"edge",
			"microsoft",
			"tts"
		]
	]) {
		if (!mergeLegacyTtsProviderConfig(tts, legacyKey, providerId, source)) continue;
		const sourcePath = source === "providers" ? `${pathLabel}.providers.${legacyKey}` : `${pathLabel}.${legacyKey}`;
		changes.push(`Moved ${sourcePath} → ${pathLabel}.providers.${providerId}.`);
	}
}
function migrateLegacyTtsEnabled(tts, pathLabel, changes) {
	if (!tts || typeof tts.enabled !== "boolean") return;
	const nextAuto = tts.enabled ? "always" : "off";
	delete tts.enabled;
	if (typeof tts.auto === "string" && tts.auto.trim()) {
		changes.push(`Removed ${pathLabel}.enabled because ${pathLabel}.auto is already set.`);
		return;
	}
	tts.auto = nextAuto;
	changes.push(`Moved ${pathLabel}.enabled → ${pathLabel}.auto "${nextAuto}".`);
}
function migrateLegacySpeakerSelectionConfig(providerConfig, pathLabel, changes) {
	for (const [legacyKey, canonicalKey] of [
		["voice", "speakerVoice"],
		["voiceName", "speakerVoice"],
		["voiceId", "speakerVoiceId"]
	]) {
		if (!Object.hasOwn(providerConfig, legacyKey)) continue;
		if (providerConfig[canonicalKey] === void 0) {
			providerConfig[canonicalKey] = providerConfig[legacyKey];
			changes.push(`Moved ${pathLabel}.${legacyKey} → ${pathLabel}.${canonicalKey}.`);
		} else changes.push(`Removed ${pathLabel}.${legacyKey} because ${pathLabel}.${canonicalKey} is already set.`);
		delete providerConfig[legacyKey];
	}
}
function migrateLegacyTtsSpeakerSelection(tts, pathLabel, changes) {
	for (const [config, path] of visitLegacyTtsSpeakerConfigs(tts, pathLabel)) migrateLegacySpeakerSelectionConfig(config, path, changes);
}
function* visitLegacySpeakerSelectionScope(value, pathLabel) {
	const scope = getRecord(value);
	if (!scope) return;
	for (const [providerId, providerValue] of Object.entries(getRecord(scope.providers) ?? {})) {
		const config = getRecord(providerValue);
		if (!isBlockedObjectKey(providerId) && config) yield [config, `${pathLabel}.providers.${providerId}`];
	}
	for (const providerId of LEGACY_TTS_PROVIDER_KEYS) {
		const config = getRecord(scope[providerId]);
		if (config) yield [config, `${pathLabel}.${providerId}`];
	}
}
function* visitLegacyTtsSpeakerConfigs(value, pathLabel) {
	const tts = getRecord(value);
	yield* visitLegacySpeakerSelectionScope(tts, pathLabel);
	for (const [personaId, persona] of Object.entries(getRecord(tts?.personas) ?? {})) if (!isBlockedObjectKey(personaId)) yield* visitLegacySpeakerSelectionScope(persona, `${pathLabel}.personas.${personaId}`);
}
function* visitKnownTtsConfigLocations(raw) {
	yield [getRecord(raw.tts), "tts"];
	const agentTts = [];
	visitAgentEntries(raw, (entry, path) => agentTts.push([getRecord(entry.tts), `${path}.tts`]));
	yield* agentTts;
	const channels = getRecord(raw.channels);
	for (const [channelId, channelValue] of Object.entries(channels ?? {})) {
		if (isBlockedObjectKey(channelId)) continue;
		const channel = getRecord(channelValue);
		const migrateRootTts = supportsChannelRootTtsMigration(channelId);
		if (migrateRootTts) yield [getRecord(channel?.tts), `channels.${channelId}.tts`];
		yield [getRecord(getRecord(channel?.voice)?.tts), `channels.${channelId}.voice.tts`];
		for (const [accountId, accountValue] of Object.entries(getRecord(channel?.accounts) ?? {})) {
			if (isBlockedObjectKey(accountId)) continue;
			const account = getRecord(accountValue);
			if (migrateRootTts) yield [getRecord(account?.tts), `channels.${channelId}.accounts.${accountId}.tts`];
			yield [getRecord(getRecord(account?.voice)?.tts), `channels.${channelId}.accounts.${accountId}.voice.tts`];
		}
	}
	const pluginEntries = getRecord(getRecord(raw.plugins)?.entries);
	for (const [pluginId, entryValue] of Object.entries(pluginEntries ?? {})) if (!isBlockedObjectKey(pluginId) && LEGACY_TTS_PLUGIN_IDS.has(pluginId)) yield [getRecord(getRecord(getRecord(entryValue)?.config)?.tts), `plugins.entries.${pluginId}.config.tts`];
}
/** Legacy config migration specs for TTS runtime compatibility. */
const LEGACY_CONFIG_MIGRATIONS_RUNTIME_TTS = [
	defineLegacyConfigMigration({
		id: "tts.top-level-owner",
		describe: "Move messages.tts to top-level tts",
		legacyRules: [{
			path: ["messages", "tts"],
			message: "messages.tts moved to top-level tts. Run \"openclaw doctor --fix\"."
		}],
		apply: (raw, changes) => {
			const messages = getRecord(raw.messages);
			if (!messages || !Object.hasOwn(messages, "tts")) return;
			const legacy = getRecord(messages.tts);
			if (!legacy) {
				delete messages.tts;
				changes.push("Removed messages.tts (invalid value).");
				return;
			}
			const legacyRealtime = getRecord(legacy.realtime);
			if (legacyRealtime) {
				const legacyVoice = legacyRealtime.speakerVoice ?? legacyRealtime.voice;
				const talk = getRecord(raw.talk) ?? {};
				const talkRealtime = getRecord(talk.realtime) ?? {};
				if (legacyVoice !== void 0 && talkRealtime.speakerVoice === void 0) {
					talkRealtime.speakerVoice = legacyVoice;
					talk.realtime = talkRealtime;
					raw.talk = talk;
					changes.push("Moved messages.tts.realtime voice → talk.realtime.speakerVoice.");
				} else changes.push("Removed messages.tts.realtime (talk.realtime already configured).");
				delete legacy.realtime;
			}
			const canonical = getRecord(raw.tts) ?? {};
			mergeMissing(canonical, legacy);
			raw.tts = canonical;
			delete messages.tts;
			changes.push("Moved messages.tts to top-level tts.");
		}
	}),
	defineLegacyConfigMigration({
		id: "tts.providers-generic-shape",
		describe: "Move legacy bundled TTS config keys into tts.providers",
		legacyRules: [{
			path: ["tts"],
			message: "tts legacy provider aliases/keys are legacy; use provider: \"microsoft\" and tts.providers.<provider>. Run \"openclaw doctor --fix\".",
			match: (value) => hasLegacyTtsProviderKeys(value)
		}, {
			path: ["plugins", "entries"],
			message: "plugins.entries.voice-call.config.tts legacy provider aliases/keys are legacy; use provider: \"microsoft\" and plugins.entries.voice-call.config.tts.providers.<provider>. Run \"openclaw doctor --fix\".",
			match: (value) => hasLegacyTtsInLocations({ plugins: { entries: value } }, hasLegacyTtsProviderKeys)
		}],
		apply: (raw, changes) => {
			for (const [tts, pathLabel] of visitKnownTtsConfigLocations({
				tts: raw.tts,
				plugins: raw.plugins
			})) migrateLegacyTtsConfig(tts, pathLabel, changes);
		}
	}),
	defineLegacyConfigMigration({
		id: "tts.speaker-selection-keys",
		describe: "Move TTS speaker selection keys to speakerVoice/speakerVoiceId",
		legacyRules: [
			{
				path: ["tts"],
				message: "tts speaker selection fields voice/voiceName/voiceId are legacy; use speakerVoice or speakerVoiceId. Run \"openclaw doctor --fix\".",
				match: (value) => hasLegacyTtsSpeakerSelection(value)
			},
			{
				path: ["agents"],
				message: "agents.entries.*.tts speaker selection fields voice/voiceName/voiceId are legacy; use speakerVoice or speakerVoiceId. Run \"openclaw doctor --fix\".",
				match: (value) => hasLegacyTtsInLocations({ agents: value }, hasLegacyTtsSpeakerSelection)
			},
			{
				path: ["channels"],
				message: "supported channel TTS speaker selection fields voice/voiceName/voiceId are legacy; use speakerVoice or speakerVoiceId. Run \"openclaw doctor --fix\".",
				match: (value) => hasLegacyTtsInLocations({ channels: value }, hasLegacyTtsSpeakerSelection)
			},
			{
				path: ["plugins", "entries"],
				message: "plugins.entries.voice-call.config.tts speaker selection fields voice/voiceName/voiceId are legacy; use speakerVoice or speakerVoiceId. Run \"openclaw doctor --fix\".",
				match: (value) => hasLegacyTtsInLocations({ plugins: { entries: value } }, hasLegacyTtsSpeakerSelection)
			}
		],
		apply: (raw, changes) => {
			for (const [tts, pathLabel] of visitKnownTtsConfigLocations(raw)) migrateLegacyTtsSpeakerSelection(tts, pathLabel, changes);
		}
	}),
	defineLegacyConfigMigration({
		id: "tts.enabled-auto-mode",
		describe: "Move legacy TTS enabled toggles to auto mode",
		legacyRules: [
			{
				path: ["tts"],
				message: "tts.enabled is legacy; use tts.auto. Run \"openclaw doctor --fix\".",
				match: (value) => hasLegacyTtsEnabled(value)
			},
			{
				path: ["agents"],
				message: "agents.entries.*.tts.enabled is legacy; use agents.entries.*.tts.auto. Run \"openclaw doctor --fix\".",
				match: (value) => hasLegacyTtsInLocations({ agents: value }, hasLegacyTtsEnabled)
			},
			{
				path: ["channels"],
				message: "supported channel TTS enabled fields are legacy; use the same TTS block auto field. Run \"openclaw doctor --fix\".",
				match: (value) => hasLegacyTtsInLocations({ channels: value }, hasLegacyTtsEnabled)
			},
			{
				path: ["plugins", "entries"],
				message: "plugins.entries.voice-call.config.tts.enabled is legacy; use plugins.entries.voice-call.config.tts.auto. Run \"openclaw doctor --fix\".",
				match: (value) => hasLegacyTtsInLocations({ plugins: { entries: value } }, hasLegacyTtsEnabled)
			}
		],
		apply: (raw, changes) => {
			for (const [tts, pathLabel] of visitKnownTtsConfigLocations(raw)) migrateLegacyTtsEnabled(tts, pathLabel, changes);
		}
	})
];
//#endregion
//#region src/commands/doctor/shared/legacy-config-migrations.runtime.ts
/** Ordered runtime legacy config migrations applied by doctor. */
const LEGACY_CONFIG_MIGRATIONS_RUNTIME = [
	...LEGACY_CONFIG_MIGRATIONS_RUNTIME_AGENTS,
	...LEGACY_CONFIG_MIGRATIONS_RUNTIME_CLI_BACKENDS,
	...LEGACY_CONFIG_MIGRATIONS_RUNTIME_CRON,
	...LEGACY_CONFIG_MIGRATIONS_RUNTIME_DIAGNOSTICS,
	...LEGACY_CONFIG_MIGRATIONS_RUNTIME_GATEWAY,
	...LEGACY_CONFIG_MIGRATIONS_RUNTIME_MCP,
	...LEGACY_CONFIG_MIGRATIONS_RUNTIME_MODELS,
	...LEGACY_CONFIG_MIGRATIONS_RUNTIME_PROVIDERS,
	...LEGACY_CONFIG_MIGRATIONS_RUNTIME_RETIRED,
	LEGACY_CONFIG_MIGRATION_RUNTIME_SECRETS_EGRESS,
	...LEGACY_CONFIG_MIGRATIONS_RUNTIME_SESSION,
	...LEGACY_CONFIG_MIGRATIONS_RUNTIME_SKILLS,
	...LEGACY_CONFIG_MIGRATIONS_RUNTIME_SYSTEM_AGENT,
	...LEGACY_CONFIG_MIGRATIONS_RUNTIME_TOOL_NAMES,
	...LEGACY_CONFIG_MIGRATIONS_RUNTIME_TTS,
	...LEGACY_CONFIG_MIGRATIONS_RUNTIME_ENTRIES
];
//#endregion
//#region src/commands/doctor/shared/legacy-config-migrations.web-search.ts
const LEGACY_WEB_SEARCH_RULES = [{
	path: [
		"tools",
		"web",
		"search"
	],
	message: "tools.web.search provider-owned config moved to plugins.entries.<plugin>.config.webSearch. Run \"openclaw doctor --fix\".",
	match: (_value, root) => listLegacyWebSearchConfigPaths(root).length > 0,
	requireSourceLiteral: true
}];
function replaceRootRecord(target, replacement) {
	for (const key of Object.keys(target)) delete target[key];
	Object.assign(target, replacement);
}
/** Legacy config migration specs for web-search provider config. */
const LEGACY_CONFIG_MIGRATIONS_WEB_SEARCH = [defineLegacyConfigMigration({
	id: "tools.web.search-provider-config->plugins.entries",
	describe: "Move legacy tools.web.search provider-owned config into plugins.entries.<plugin>.config.webSearch",
	legacyRules: LEGACY_WEB_SEARCH_RULES,
	apply: (raw, changes) => {
		const migrated = migrateLegacyWebSearchConfig(raw);
		if (migrated.changes.length === 0) return;
		replaceRootRecord(raw, migrated.config);
		changes.push(...migrated.changes);
	}
})];
//#endregion
//#region src/commands/doctor/shared/legacy-config-migrations.ts
const LEGACY_CONFIG_MIGRATION_SPECS = [
	...LEGACY_CONFIG_MIGRATIONS_CHANNELS,
	...LEGACY_CONFIG_MIGRATIONS_QQBOT,
	...LEGACY_CONFIG_MIGRATIONS_AUDIO,
	...LEGACY_CONFIG_MIGRATIONS_QUEUE,
	...LEGACY_CONFIG_MIGRATIONS_RUNTIME,
	...LEGACY_CONFIG_MIGRATIONS_WEB_SEARCH
];
/** Ordered legacy migrations without their preview-only rule metadata. */
const LEGACY_CONFIG_MIGRATIONS = LEGACY_CONFIG_MIGRATION_SPECS.map(({ legacyRules: _legacyRules, ...migration }) => migration);
/** Aggregated legacy config rules used for doctor preview issue detection. */
const LEGACY_CONFIG_MIGRATION_RULES = LEGACY_CONFIG_MIGRATION_SPECS.flatMap((migration) => migration.legacyRules ?? []);
//#endregion
//#region src/config/legacy.ts
function getPathValue(root, path) {
	let cursor = root;
	for (const key of path) {
		if (!cursor || typeof cursor !== "object") return;
		cursor = cursor[key];
	}
	return cursor;
}
/** Finds legacy config issues using built-in rules plus optional caller rules. */
function findLegacyConfigIssues(raw, sourceRaw, extraRules = [], _touchedPaths) {
	if (!raw || typeof raw !== "object") return [];
	const root = raw;
	const sourceRoot = sourceRaw && typeof sourceRaw === "object" ? sourceRaw : root;
	const issues = [];
	for (const rule of [...LEGACY_CONFIG_MIGRATION_RULES, ...extraRules]) {
		const cursor = getPathValue(root, rule.path);
		if (cursor !== void 0 && (!rule.match || rule.match(cursor, root))) {
			if (rule.requireSourceLiteral) {
				const sourceCursor = getPathValue(sourceRoot, rule.path);
				if (sourceCursor === void 0) continue;
				if (rule.match && !rule.match(sourceCursor, sourceRoot)) continue;
			}
			issues.push({
				path: rule.path.join("."),
				message: rule.message
			});
		}
	}
	return issues;
}
//#endregion
export { selectedCanonicalModelRefsForRuntimePolicy as _, resolveMemoryHostAgentContextLimits as a, resolveRememberAcrossConversations as c, migrateLegacyWebSearchConfig as d, migrateLegacyXSearchConfig as f, modelEntryWithRuntimePolicy as g, migrateLegacyRuntimeModelRef as h, normalizeConfiguredMemoryExtraPaths as i, stripRetiredTuningKnobs as l, listLegacyRuntimeModelProviderAliases as m, LEGACY_CONFIG_MIGRATIONS as n, resolveMemoryHostAgentWorkspaceDir as o, legacyRuntimeModelAliasRequiresRuntimePolicy as p, MEMORY_HOST_ROOT_FILENAME as r, resolveMemoryHostSearchPathConfig as s, findLegacyConfigIssues as t, migrateLegacyWebFetchConfig as u };