openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
2,244 lines • 106 kB
JavaScript
import { c as normalizeOptionalLowercaseString, l as normalizeOptionalString } from "./string-coerce-1VuiLaaR.mjs";
import { s as asFiniteNumber } from "./number-coercion-CuVhdCBs.mjs";
import { r as createLazyRuntimeModule } from "./lazy-runtime-CJrzFzqU.mjs";
import { n as isTruthyEnvValue } from "./env-BBJTTIuE.mjs";
import { c as isRecord } from "./record-coerce-TIsquoVR.mjs";
import { n as sanitizeForLog } from "./ansi-BeFJ0Uu7.mjs";
import { r as createInvalidConfigError } from "./io.invalid-config-BzeD3bi5.mjs";
import { a as coerceSecretRef, l as isLegacySecretRefWithoutProvider, m as parseLegacySecretRefEnvMarker, s as hasLegacySecretRefExtraFields } from "./types.secrets-DsPRn9Dz.mjs";
import { d as resolveHomeDir } from "./utils-DgZn2dLu.mjs";
import { r as resolveStateDir } from "./state-dir-BHJORNly.mjs";
import { S as resolveIsConfigReadOnly, m as resolveConfigPath, p as resolveCanonicalConfigPath, t as CONFIG_PATH } from "./paths-DGIwWbpu.mjs";
import { t as formatErrorMessage } from "./errors-O33D01XW.mjs";
import { t as formatCliCommand } from "./command-format-DwEdrDKL.mjs";
import { t as createSubsystemLogger } from "./subsystem-I-uqdx4f.mjs";
import { c as resolveOpenClawStateSqlitePath } from "./openclaw-state-db.paths-DfifnSGp.mjs";
import { i as resolveUpdateRehearsalRoot } from "./update-rehearsal-paths-DTVdt0cq.mjs";
import { r as hasCommandProcessCleanupError } from "./exec-result-s7WOth0N.mjs";
import { r as cloneEnvWithPlatformSemantics, y as resolveFutureConfigActionBlock } from "./config-env-vars-DjPHKieF.mjs";
import { m as withOpenClawStateDatabaseReadSnapshot, o as withArtifactPreservingStateReads } from "./openclaw-state-db-readonly-CC57AD_q.mjs";
import { r as assertOpenClawStateWriteAllowedAtPath } from "./openclaw-state-ownership-BDcUGGMF.mjs";
import { r as normalizeProviderId } from "./provider-id-D1VImdKk.mjs";
import { d as resolveAgentWorkspaceDir } from "./agent-scope-config-CDYpzA0r.mjs";
import "./session-key-C09ow4h0.mjs";
import { t as DEFAULT_ACCOUNT_ID } from "./account-id-DbRsdayc.mjs";
import { a as readAgentRosterProperty, l as tryResolveSoleAgentId, n as listAgentEntries } from "./agent-roster-BXD1uMJD.mjs";
import { i as hashConfigRaw, s as parseConfigJson5, t as coerceConfig } from "./io.read-helpers-Bvdjf9tk.mjs";
import { r as UPDATE_ENVIRONMENT_FAILURE_REASONS } from "./update-outcome-BHX5xsqd.mjs";
import { m as shouldSkipLegacyUpdateDoctorConfigWrite, p as shouldDeferConfiguredPluginInstallRepair } from "./update-phase-BClSJ17n.mjs";
import { c as readDeferredPluginMigrations, i as mergeDeferredPluginMigration, r as formatDeferredPluginMigration, t as DeferredPluginMigrationConflictError, u as recordDeferredPluginMigrations } from "./deferred-plugin-migrations-by1ypkxx.mjs";
import { C as setActiveDegradedPlugins } from "./discovery-Dma3bVNk.mjs";
import { r as getOfficialExternalPluginCatalogEntry } from "./official-external-plugin-catalog-Dcv926GC.mjs";
import { i as loadInstalledPluginIndexInstallRecordsSync } from "./installed-plugin-index-record-reader-BP4r74yH.mjs";
import { o as getOfficialExternalPluginCatalogManifest } from "./official-external-plugin-catalog-source-B7ojvzQW.mjs";
import { h as resolveNormalizedProviderModelMaxTokens } from "./validation-core-C5DCruFP.mjs";
import { c as recoverConfigFromLastKnownGoodCore, l as createConfigIoContext, n as prepareConfigRecoveryFromContext } from "./io.factory-0ZgSoWaa.mjs";
import { f as readRecentConfigAuditRecords } from "./io.audit-BXzMurur.mjs";
import "./agent-scope-CG17uXFn.mjs";
import { n as loadPluginManifestRegistryForPluginRegistry } from "./plugin-registry-contributions-Dzd0IlT8.mjs";
import { a as resolveDeferredPluginMigrationConfigPaths, i as preserveDeferredPluginMigrationConfig } from "./deferred-plugin-migration-config-Db8uALnk.mjs";
import { d as migrateLegacyWebFetchConfig, f as migrateLegacyWebSearchConfig, h as LEGACY_AGENT_ROSTER_RULES, n as findLegacyConfigRuleIssues, p as migrateLegacyXSearchConfig, s as normalizeLegacyTalkConfig, u as stripRetiredTuningKnobs } from "./legacy-BEdDMq-_.mjs";
import { i as tryGetLegacyDefaultAgentId, r as retainLegacyDefaultAgentId } from "./legacy.default-agent-owner-CiZjlN5C.mjs";
import { r as resolveLegacyAgentRosterOwner } from "./legacy.roster-D0lEOcPT.mjs";
import { i as isBlockedLegacyCodexModelRef } from "./codex-route-model-ref-DcSSGtAw.mjs";
import "./legacy-config-record-shared-C4da1YWQ.mjs";
import { t as classifyOtelGrpcMigrationOwnership } from "./include-migration-ownership-jJubBWXv.mjs";
import { a as mergeModelRefMapEntries, i as isLegacyModelsAddCodexMetadataModel, n as modelEntryWithRuntimePolicy, s as rewriteModelRefs } from "./legacy-config-migrations.runtime.models-DCS4mjdk.mjs";
import { a as HeartbeatSchema } from "./zod-schema.agent-runtime-cX-pI-Mt.mjs";
import { n as discoverConfigSecretTargets } from "./target-registry-query-djbCpoM3.mjs";
import { i as setPathExistingStrict } from "./path-utils-DQXhHs4r.mjs";
import "./target-registry-C9njyDoW.mjs";
import "./env-vars-Bcb14_jq.mjs";
import { t as configIncludeOwnsAgentRoster } from "./agent-roster-provenance-CHtmWQjc.mjs";
import { h as readCurrentConfigForResolution, y as recoverConfigFromJsonRootSuffix } from "./io.runtime-BsUXSsD4.mjs";
import "./io-BYWYXJXl.mjs";
import "./plugin-registry-CtFzlfK3.mjs";
import { _ as recordAgentDatabaseAdmissions, l as evaluateAgentDatabaseAdmissions, m as listAgentDatabaseAdmissionRefusals, r as canIsolateAgentDatabase } from "./agent-database-admission-dWNS6Rcs.mjs";
import "./installed-plugin-index-records-D8YKQ3gZ.mjs";
import { a as runPluginSetupConfigMigrations } from "./setup-registry-BwvtiOQK.mjs";
import { t as DEFAULT_GOOGLE_API_BASE_URL } from "./google-api-base-url-DVjll0O0.mjs";
import { t as findRetiredConfigUpgradeRequirement } from "./retired-config-formats-yKg5AICE.mjs";
import { t as note } from "./note-Byxgt4MB.mjs";
import { n as sanitizeDoctorNote, t as emitDoctorNotes } from "./emit-notes-CPyL1dzc.mjs";
import { a as migrateLegacyConfig, c as noteDoctorConfigPreflightIssues, d as noteMcpOriginWarning, f as noteMediaCliModelWarnings, h as noteSandboxOriginProxyWarning, l as noteDoctorHookConfigWarnings, m as noteOpencodeProviderOverrides, n as applyUnknownConfigKeyStep, o as applyLegacyDoctorMigrations, p as noteMissingDefaultAgentOwner, r as prepareDoctorConfigReferenceSource, s as applyChannelDoctorCompatibilityMigrations, t as applyLegacyCompatibilityStep, u as noteImplicitFallbackClobberWarnings } from "./config-flow-steps-Ck9lV3Gm.mjs";
import { t as listDoctorConfiguredChannelIds } from "./configured-channel-ids-DbRqmqAQ.mjs";
import { r as callGateway } from "./call-B7pNY6_o.mjs";
import { t as getAgentDatabaseStartupAdmission } from "./agent-database-startup-n_PGS5Sj.mjs";
import { n as resolveCronJobsStorePathFromConfig } from "./paths-C7ju4So0.mjs";
import { i as migrateLegacyCommandOwners } from "./doctor-command-owner-ZwiTw8zO.mjs";
import { r as loadSetupChannelPluginFromManifestRecord } from "./read-only-BUz6WPkw.mjs";
import { t as pruneBindingsForMissingAgents } from "./legacy-config-binding-repair-DLRN5d7r.mjs";
import { r as withProgress } from "./progress-UTM8yfZa.mjs";
import { t as createPluginCapabilityConsentPrompter } from "./plugin-capability-consent-B7boFiKO.mjs";
import { t as resolveSingleAccountPromotion } from "./setup-promotion-helpers-CZR3vCRJ.mjs";
import { n as commitAutomaticConfigRepair, r as planAutomaticConfigRepair, t as canPlanAutomaticConfigRepair } from "./automatic-config-repair-Pise_uHu.mjs";
import { n as assertNoRetiredStateFiles } from "./state-migrations.retired-files-Dlsbs5FH.mjs";
import { r as listReferencedLegacyOAuthSidecarPaths, t as assertNoRetiredOAuthSidecarsBeforeConfigRecovery } from "./doctor-auth-legacy-paths-D2U_pugg.mjs";
import { t as listRetiredCronStateFiles } from "./state-migrations.retired-cron-files-BfwwvNnu.mjs";
import { t as readRetiredDeferredPluginMigrationWarnings } from "./deferred-plugin-migration-warnings-Bi1cLcKC.mjs";
import { c as throwIfDoctorStateMigrationRefused, t as DoctorStateMigrationRefusalError } from "./state-migrations.messages-CXxBVY0a.mjs";
import { c as measureDoctorConfigPreflightStep, n as runDoctorPluginConvergence, r as assertPreflightConfigUnchanged, s as readConfigPreflightSnapshot } from "./doctor-config-preflight-plugin-verification-Bvpmk9Xt.mjs";
import { n as createDoctorPluginMetadataSnapshotScope } from "./plugin-metadata-snapshot-scope-DQBp81Gl.mjs";
import { t as inspectPluginMigrationAvailability } from "./plugin-migration-availability-CaB0UgqO.mjs";
import { n as createWorkspaceAliasMigrationRepair } from "./doctor-workspace-alias-C5obhIxU.mjs";
import { r as cronCodexRuntimePolicyTargetKey } from "./store-migration-Boav2T0U.mjs";
import { t as applyDoctorConfigMutation } from "./config-mutation-state-T3qeDQIp.mjs";
import { t as migrateLegacyRuntimeModelRef } from "./legacy-runtime-model-providers-DkyokuxH.mjs";
import { t as canWriteDoctorInclude } from "./roster-include-write-C1wglSCL.mjs";
import { existsSync } from "node:fs";
import path from "node:path";
import fs$1 from "node:fs/promises";
import { homedir } from "node:os";
import { isDeepStrictEqual } from "node:util";
//#region src/commands/doctor-config-preflight-plugin-index.ts
/** Returns true during updater-managed config rewrites where plugin validation may be stale. */
function shouldSkipPluginValidationForDoctorConfigPreflight(env = process.env) {
return isTruthyEnvValue(env.OPENCLAW_UPDATE_IN_PROGRESS);
}
/** One preflight owns completion; each read still checks the current update phase. */
function createDoctorRehearsalSnapshotPreparation(report) {
let completed = false;
const prepareSnapshot = async (snapshot) => {
if (completed) return;
const { completeUpdateCandidatePluginRehearsal } = await import("./update-candidate-plugin-repair-BCX2_maY.mjs");
const result = await completeUpdateCandidatePluginRehearsal({
config: snapshot.sourceConfig ?? snapshot.config ?? {},
env: process.env,
installRecords: loadInstalledPluginIndexInstallRecordsSync({ env: process.env })
});
completed = true;
report({
changes: result.copiedFiles > 0 ? [`Update rehearsal: copied ${result.copiedFiles} missing plugin dependency files.`] : [],
warnings: result.warnings
});
};
return (enabled) => enabled && resolveUpdateRehearsalRoot(process.env) && process.env.OPENCLAW_UPDATE_IN_PROGRESS === "1" ? prepareSnapshot : void 0;
}
//#endregion
//#region src/commands/doctor/shared/config-recovery.ts
const transformDoctorRecoveryCandidate = (params) => {
const ownership = classifyOtelGrpcMigrationOwnership({
snapshot: {
path: params.configPath,
includeProvenance: params.includeProvenance
},
authoredConfig: params.candidate.parsed,
resolvedConfig: params.resolvedConfig
});
if (ownership && ownership.kind !== "direct") throw createInvalidConfigError(params.configPath, ownership.kind === "resolved-only" ? "candidate migration cannot persist an env-resolved diagnostics.otel.protocol repair" : "candidate migration requires an include-owned diagnostics.otel.protocol repair");
const migration = applyLegacyDoctorMigrations(params.candidate.parsed, {
sourceConfigBeforeMigrations: params.resolvedConfig,
context: {
authoredRaw: params.candidate.parsed,
resolvedRaw: params.resolvedConfig
}
});
return migration.next ? preserveDeferredPluginMigrationConfig({
sourceConfig: params.candidate.parsed,
nextConfig: migration.next,
pending: params.deferredPluginMigrations
}) : params.candidate.parsed;
};
function prepareDoctorConfigRecoverySnapshot(options, snapshot) {
return prepareConfigRecoveryFromContext(createConfigIoContext(options, transformDoctorRecoveryCandidate), snapshot);
}
function recoverDoctorConfigFromLastKnownGood(params) {
const context = createConfigIoContext({ configPath: params.snapshot.path }, transformDoctorRecoveryCandidate);
return recoverConfigFromLastKnownGoodCore({
...params,
deps: context.deps,
prepareCandidate: context.prepareRecoveryBackupCandidate
});
}
//#endregion
//#region src/commands/doctor-config-preflight-legacy-config.ts
function createDoctorConfigRepairPlanner(params) {
const planScopedConfigRepair = (snapshot) => {
return params.runWithPluginMetadataSnapshot({ config: snapshot.sourceConfig ?? snapshot.config ?? {} }, () => planAutomaticConfigRepair(snapshot));
};
const planAdmittedConfigRepair = (snapshot, prepared = null) => (params.options.repairPrefixedConfig === true || params.stateMigrationsRequested && params.options.migrateLegacyConfig !== false) && canPlanAutomaticConfigRepair(snapshot) && !params.skipLegacyParentConfigWrite && (params.options.repairPrefixedConfig === true || !shouldSkipPluginValidationForDoctorConfigPreflight()) && !resolveIsConfigReadOnly(process.env) && !resolveFutureConfigActionBlock({
action: "normalize legacy config",
snapshot
}) ? prepared ?? planScopedConfigRepair(snapshot) : null;
return {
planScopedConfigRepair,
planAdmittedConfigRepair
};
}
async function migrateLegacyDoctorConfig(params) {
if (!params.enabled || resolveIsConfigReadOnly(process.env)) return;
const changes = await params.measure("legacy-config-migration", maybeMigrateLegacyConfig);
if (changes.length > 0) note(changes.map((entry) => `- ${entry}`).join("\n"), "Doctor changes");
}
/** Repair active legacy bytes before considering an older backup. */
async function prepareDoctorConfigRecovery(params) {
let snapshotRead = params.snapshotRead;
let snapshot = snapshotRead.snapshot;
const assertSupportedConfig = (config) => {
const retired = findRetiredConfigUpgradeRequirement(config);
if (retired) throw new Error(`${retired.message} ${retired.nextAction}`);
assertNoRetiredStateFiles("OAuth credential sidecars", listReferencedLegacyOAuthSidecarPaths(process.env, coerceConfig(config)));
};
assertSupportedConfig(snapshot.sourceConfigBeforeMigrations ?? snapshot.sourceConfig);
assertNoRetiredStateFiles("Cron state", await listRetiredCronStateFiles(resolveCronJobsStorePathFromConfig(snapshot.sourceConfigBeforeMigrations ?? snapshot.sourceConfig ?? snapshot.config)));
let activeConfigRepair = null;
const recoveryEnabled = params.enabled && !resolveFutureConfigActionBlock({
action: "recover config",
snapshot
});
if (recoveryEnabled && snapshot.valid) {
const recovery = await prepareDoctorConfigRecoverySnapshot({ configPath: snapshot.path }, snapshot);
if (recovery) {
await recovery.apply();
snapshotRead = await params.readSnapshot();
snapshot = snapshotRead.snapshot;
}
}
if (recoveryEnabled && snapshot.exists && !snapshot.valid) {
activeConfigRepair = typeof snapshot.raw === "string" && parseConfigJson5(snapshot.raw).ok ? params.planRepair(snapshot) : null;
let configRepaired = false;
if (!activeConfigRepair && await recoverConfigFromJsonRootSuffix(snapshot, assertSupportedConfig)) {
note("Removed non-JSON prefix from openclaw.json.", "Config");
configRepaired = true;
} else if (!activeConfigRepair && await recoverDoctorConfigFromLastKnownGood({
snapshot,
reason: "doctor-invalid-config"
})) {
note("Restored openclaw.json from last-known-good; original saved as .clobbered.*.", "Config");
configRepaired = true;
}
if (configRepaired) {
snapshotRead = await params.readSnapshot();
snapshot = snapshotRead.snapshot;
}
if (!snapshot.valid && typeof snapshot.raw === "string" && !parseConfigJson5(snapshot.raw).ok) throw new Error(`Config at ${snapshot.path} is not parseable and cannot be repaired automatically. The file remains unchanged. Inspect the exact parse error with ${formatCliCommand("openclaw config validate")}, then hand-edit the file; or move it aside and run ${formatCliCommand("openclaw onboard")} to generate a fresh config.`);
}
return {
snapshotRead,
activeConfigRepair
};
}
async function maybeMigrateLegacyConfig() {
const changes = [];
const home = resolveHomeDir();
if (!home) return changes;
const targetPath = resolveCanonicalConfigPath();
const targetDir = path.dirname(targetPath);
try {
await fs$1.access(targetPath);
return changes;
} catch {}
const legacyPath = path.join(home, ".clawdbot", "clawdbot.json");
try {
await fs$1.access(legacyPath);
} catch {
return changes;
}
await fs$1.mkdir(targetDir, { recursive: true });
try {
await fs$1.copyFile(legacyPath, targetPath, fs$1.constants.COPYFILE_EXCL);
changes.push(`Migrated legacy config: ${legacyPath} -> ${targetPath}`);
} catch (error) {
if ((error && typeof error === "object" && "code" in error ? error.code : void 0) !== "EEXIST") throw new Error(`Failed to migrate legacy config ${legacyPath} -> ${targetPath}: ${formatErrorMessage(error)}`, { cause: error });
}
return changes;
}
//#endregion
//#region src/commands/doctor-config-preflight-migrations.ts
/** Settle package repairs before state migrations select their plugin owners. */
async function prepareDoctorMigrationPlugins(params) {
const convergence = await runDoctorPluginConvergence(params);
setActiveDegradedPlugins(convergence.quarantinedPlugins);
await params.onDeferredPlugins(convergence.deferredPlugins ?? [], convergence.migrationInspection);
const refreshed = await params.readRefreshedSnapshot();
assertPreflightConfigUnchanged(params.snapshotRead.snapshot, refreshed.snapshot);
return refreshed;
}
async function assertDoctorPreflightMigrationsComplete(params) {
const scopedRefusals = params.stepReceipts.filter((receipt) => receipt.outcome === "refused" && (receipt.refusal?.code === "agent-database-ownership-mismatch" || receipt.refusal?.code === "blocked-by-agent-database-refusal") && receipt.refusedAgentDatabasePaths?.length);
const admissions = scopedRefusals.length > 0 ? getAgentDatabaseStartupAdmission() ? listAgentDatabaseAdmissionRefusals() : await evaluateAgentDatabaseAdmissions(params.cfg) : [];
if (scopedRefusals.length > 0) recordAgentDatabaseAdmissions(admissions);
const isolatedPaths = new Set(admissions.filter((refusal) => refusal.code !== "agent-database-ownership-mismatch" || canIsolateAgentDatabase(params.cfg, refusal.agentId)).flatMap((refusal) => refusal.paths.map((pathname) => path.resolve(pathname))));
for (const receipt of scopedRefusals) if (receipt.refusedAgentDatabasePaths?.every((pathname) => isolatedPaths.has(path.resolve(pathname)))) receipt.outcome = "warning";
try {
throwIfDoctorStateMigrationRefused(params.stepReceipts);
} catch (error) {
if (error instanceof DoctorStateMigrationRefusalError) {
const { assertConfiguredWorkspaceStateReady } = await import("./workspace-state-dirs--SUCclNd.mjs");
try {
await assertConfiguredWorkspaceStateReady({
cfg: params.cfg,
operation: "doctor"
});
} catch (workspaceError) {
params.report({
changes: [],
warnings: [String(workspaceError)]
});
}
}
throw error;
}
}
function noteStateMigrationResult(result) {
for (const key of [
"changes",
"notices",
"warnings"
]) if (result[key]?.length) note(result[key].map((entry) => `- ${entry}`).join("\n"), `Doctor ${key}`);
}
//#endregion
//#region src/commands/doctor-config-preflight-plugin-migrations.ts
/** One preflight retains unavailable owners until their migration reports completion. */
function createDoctorPluginMigrationPreparation(params) {
const previousById = /* @__PURE__ */ new Map();
let deferred = [];
let expectedPending = [];
let refreshSnapshot = false;
let previousLoaded = false;
const loadPrevious = async (snapshot) => {
if (previousLoaded) return;
const env = cloneEnvWithPlatformSemantics(params.env());
if (!(snapshot?.exists ?? existsSync(resolveConfigPath(env)))) return;
deferred = await withArtifactPreservingStateReads(() => withOpenClawStateDatabaseReadSnapshot(async () => readDeferredPluginMigrations({ env }), { env }));
expectedPending = structuredClone(deferred);
for (const entry of deferred) previousById.set(entry.pluginId, entry);
previousLoaded = true;
};
let prepared = false;
const completedIds = /* @__PURE__ */ new Set();
const reported = /* @__PURE__ */ new Map();
let statelessPluginIds = /* @__PURE__ */ new Set();
let runtimePluginAliases = /* @__PURE__ */ new Set();
const inspectedStatelessPluginIds = /* @__PURE__ */ new Set();
const learn = (inspection) => {
if (!inspection) return;
statelessPluginIds = new Set(inspection.statelessPluginIds);
runtimePluginAliases = new Set(inspection.runtimePluginAliases);
for (const pluginId of inspection.requiredPluginIds) {
const pending = previousById.get(pluginId);
if (pending) previousById.set(pluginId, {
...pending,
requiresStateMigration: true
});
}
for (const pluginId of inspection.inspectionRequiredPluginIds) {
const pending = previousById.get(pluginId);
if (pending) previousById.set(pluginId, {
...pending,
requiresDoctorInspection: true
});
}
};
const retain = (pending) => mergeDeferredPluginMigration(previousById.get(pending.pluginId), pending);
const remember = () => {
for (const pending of deferred) previousById.set(pending.pluginId, pending);
};
const prepare = async (snapshot) => {
await loadPrevious(snapshot);
if (!snapshot.exists) return [...previousById.values()];
if (!prepared && params.enabled) {
const availability = await inspectPluginMigrationAvailability({
cfg: snapshot.sourceConfig,
env: params.env(),
retainedPluginIds: [...previousById.keys()],
deferInstallation: shouldDeferConfiguredPluginInstallRepair(params.env())
});
learn(availability);
deferred = availability.pending.map(retain);
remember();
prepared = true;
}
return [...previousById.values()];
};
const reportPending = (plugin) => {
const warning = formatDeferredPluginMigration(plugin, params.env());
const previous = reported.get(plugin.pluginId);
if (previous?.warnings[0] === warning) return;
params.report({
changes: [],
warnings: [warning],
warningDisposition: "recoverable",
outcome: "deferred"
});
if (previous) {
previous.warnings = [warning];
return;
}
const receipt = {
id: `plugin:${plugin.pluginId}`,
phase: "final",
source: [{
kind: "owner",
id: plugin.pluginId
}],
target: [{
kind: "owner",
id: plugin.pluginId
}],
requiredness: "conditional",
reversibility: "checkpoint-required",
outcome: "deferred",
changes: [],
warnings: [warning]
};
reported.set(plugin.pluginId, receipt);
params.recordReceipt(receipt);
};
const persistPending = async (pending, resolvedPluginIds) => {
try {
const committed = await recordDeferredPluginMigrations({
env: params.env(),
pending,
...resolvedPluginIds ? { resolvedPluginIds } : {},
expectedPending
});
if (committed) expectedPending = structuredClone(committed);
return true;
} catch (error) {
if (!(error instanceof DeferredPluginMigrationConflictError)) throw error;
expectedPending = structuredClone(error.pending);
previousById.clear();
deferred = error.pending;
completedIds.clear();
statelessPluginIds.clear();
runtimePluginAliases.clear();
inspectedStatelessPluginIds.clear();
refreshSnapshot = true;
for (const plugin of deferred) {
previousById.set(plugin.pluginId, plugin);
reportPending(plugin);
}
return false;
}
};
return {
deferred: () => deferred,
retainedPluginIds: () => [...previousById.keys()],
prepare,
snapshotOptions: async () => {
await loadPrevious();
return {
preparePluginMigrations: !prepared && params.enabled ? prepare : void 0,
deferredPluginMigrations: [...previousById.values()]
};
},
async migrate(config) {
const { autoMigrateLegacyPluginDoctorState } = await import("./state-migrations.plugin-doctor-BbPe8x7T.mjs");
params.report(await params.measure("plugin-doctor-migrations", () => params.runWithPluginMetadataSnapshot({ config }, () => autoMigrateLegacyPluginDoctorState({
config,
env: params.env(),
log: params.log,
...params.doctorOnlyStateMigrations ? { doctorOnlyStateMigrations: true } : {}
}))));
},
async converged(pending, snapshot, metadata, inspection) {
learn(inspection);
deferred = pending.map((plugin) => retain(Object.assign(resolveDeferredPluginMigrationConfigPaths({
config: snapshot.sourceConfigBeforeMigrations ?? snapshot.sourceConfig,
pluginId: plugin.pluginId,
compatibilityMigrationPaths: metadata?.plugins.find((record) => record.id === plugin.pluginId)?.configContracts?.compatibilityMigrationPaths
}), plugin)));
remember();
if (!await persistPending([...previousById.values()])) return;
for (const plugin of deferred) reportPending(plugin);
},
observe(result) {
for (const pluginId of result.requiredPluginIds ?? []) {
const pending = previousById.get(pluginId);
if (pending) previousById.set(pluginId, {
...pending,
requiresStateMigration: true
});
}
for (const pluginId of result.statelessPluginIds ?? []) inspectedStatelessPluginIds.add(pluginId);
for (const pluginId of result.completedPluginIds ?? []) completedIds.add(pluginId);
},
async complete() {
if (!params.enabled) return false;
const unavailableIds = new Set(deferred.map((plugin) => plugin.pluginId));
const resolvedPluginIds = [...previousById.values()].filter((plugin) => {
if (completedIds.has(plugin.pluginId)) return true;
if (plugin.requiresStateMigration || unavailableIds.has(plugin.pluginId)) return false;
if (inspectedStatelessPluginIds.has(plugin.pluginId)) return true;
if (plugin.requiresDoctorInspection) return false;
return statelessPluginIds.has(plugin.pluginId) || runtimePluginAliases.has(plugin.pluginId) && !plugin.validationExcludedPaths?.length && (plugin.configPaths ?? []).every((segments) => segments.length === 2 && segments[0] === "session" && segments[1] === "store");
}).map((plugin) => plugin.pluginId);
const resolvedIds = new Set(resolvedPluginIds);
const pending = [...previousById.values()].filter((plugin) => !resolvedIds.has(plugin.pluginId)).map((plugin) => unavailableIds.has(plugin.pluginId) ? plugin : Object.assign(plugin, {
reason: "The installed plugin has not confirmed that its saved data and settings are ready for this version. If Doctor cannot finish the upgrade, report this warning to the plugin maintainer.",
command: "openclaw doctor --fix"
}));
if (resolvedPluginIds.length === 0 && pending.length === 0) return refreshSnapshot;
if (!await persistPending(pending, resolvedPluginIds)) return true;
for (const pluginId of resolvedPluginIds) previousById.delete(pluginId);
for (const plugin of pending) {
previousById.set(plugin.pluginId, plugin);
reportPending(plugin);
}
return refreshSnapshot || resolvedPluginIds.length > 0;
}
};
}
//#endregion
//#region src/commands/doctor-config-preflight.cron.ts
/** Preserve a retired partition selector even when other config cannot drive core migrations. */
async function migrateRetainedStore(params) {
const cfg = retainStoreConfig(params.config);
if (!cfg) return;
const { repairLegacyCronStoreWithoutPrompt } = await params.measure("cron-repair-import", () => import("./legacy-repair-B3Id5yeg.mjs"));
params.report(await params.measure("cron-repair", () => repairLegacyCronStoreWithoutPrompt({
cfg,
migrateCodexModelRefs: false
})));
const { migrateLegacyConfigMachineState } = await import("./state-migrations.config-machine-state-BGq4_GMW.mjs");
params.report(migrateLegacyConfigMachineState({
config: params.config,
env: params.env
}));
}
/** Restores retired cron migration inputs that canonical config migration intentionally strips. */
function withLegacyConfig(config, legacyConfig) {
const legacyCron = legacyConfig?.cron;
if (!legacyCron || !Object.hasOwn(legacyCron, "store") && !Object.hasOwn(legacyCron, "webhook")) return config;
return {
...config,
cron: {
...config.cron,
...Object.hasOwn(legacyCron, "store") ? { store: legacyCron.store } : {},
...Object.hasOwn(legacyCron, "webhook") ? { webhook: legacyCron.webhook } : {}
}
};
}
/** Isolates the trusted partition selector from a partially valid legacy config. */
function retainStoreConfig(config) {
const cron = config?.cron;
if (typeof cron?.store !== "string" || !cron.store.trim()) return;
return { cron: {
store: cron.store,
...Object.hasOwn(cron, "webhook") ? { webhook: cron.webhook } : {}
} };
}
//#endregion
//#region src/commands/doctor-update-run.ts
async function noteStaleUpdateRuns(options = {}) {
const warnings = /* @__PURE__ */ new Set();
const reportReconciliationError = (error) => {
if (hasCommandProcessCleanupError(error)) throw error;
warnings.add(`Update history reconciliation could not complete: ${String(error)}`);
};
try {
await inspectStaleUpdateRuns(options, reportReconciliationError);
} catch (error) {
reportReconciliationError(error);
}
for (const warning of warnings) note(warning, "Update history");
if (warnings.size && options.migrateState !== false) try {
await recordUpdateHistoryWarning([...warnings].join("\n"));
} catch (error) {
if (hasCommandProcessCleanupError(error)) throw error;
note(`Update history warning could not be saved: ${String(error)}`, "Update history");
}
}
async function recordUpdateHistoryWarning(detail) {
const [{ runExistingOpenClawStateWriteTransaction }, { executeSqliteQuerySync, executeSqliteQueryTakeFirstSync, getNodeSqliteKysely }, { decodeRun }, { encodeRun }, { updateRunLedgerSchema, upsertStep }] = await Promise.all([
import("./openclaw-state-db-existing-write-D14qTgJt.mjs"),
import("./kysely-sync-CoevGH3_.mjs"),
import("./update-run-read.kernel-NF8yjkI7.mjs"),
import("./update-run-codec-BtYThXHP.mjs"),
import("./update-run-write-QAXTZ_hR.mjs")
]);
runExistingOpenClawStateWriteTransaction(({ db }) => {
const latest = executeSqliteQueryTakeFirstSync(db, getNodeSqliteKysely(db).selectFrom("update_runs").selectAll().orderBy("created_at_ms", "desc").orderBy("run_id", "desc").limit(1));
if (!latest) return;
const record = decodeRun(latest);
upsertStep(record, {
step: "warning:update-history-reconciliation",
status: "completed",
detail
});
executeSqliteQuerySync(db, getNodeSqliteKysely(db).updateTable("update_runs").set({ steps_json: encodeRun(record, {}).steps_json }).where("run_id", "=", record.runId));
}, {}, {
schemaSql: updateRunLedgerSchema,
operationLabel: "update.history.warning"
});
}
async function inspectStaleUpdateRuns(options, reportReconciliationError) {
const [{ staleUpdateRunGuidance }, { listUpdateRunsAsync }, { renderUpdateRunReport }, { updateRunWarningMessages }, { readInstalledUpdateCandidate, reconcileInterruptedUpdateRuns }, { isAcknowledgedAbandonedUpdateRun }, { readInterruptedUpdateCandidateAsync }] = await Promise.all([
import("./update-run-activity-C6JhZrSC.mjs"),
import("./update-run-reader-C-3VhFcB.mjs"),
import("./update-run-report-S-g6zEkF.mjs"),
import("./update-run-step-CGy-iIV_.mjs"),
import("./update-run-interruption-z4WlzDhN.mjs"),
import("./update-run-record-D0bE4SF0.mjs"),
import("./update-run-interruption-worker-CPCqEDrt.mjs")
]);
const discovered = await withArtifactPreservingStateReads(() => withOpenClawStateDatabaseReadSnapshot(async () => {
let candidate;
if (options.migrateState !== false) try {
candidate = await readInterruptedUpdateCandidateAsync({});
} catch (error) {
reportReconciliationError(error);
}
return {
candidate,
active: await listUpdateRunsAsync({
active: true,
limit: 100
}),
history: await listUpdateRunsAsync({ limit: 100 })
};
}));
let { active, history } = discovered;
let reconciled = [];
if (discovered.candidate) try {
reconciled = await reconcileInterruptedUpdateRuns({ candidate: discovered.candidate });
for (const run of reconciled) note(`Update ${run.runId}: recorded succeeded after verifying the installed and serving candidate build ${run.after.buildId}; its updater exited before recording completion.`, "Update history");
} catch (error) {
reportReconciliationError(error);
}
if (reconciled.length) {
active = await listUpdateRunsAsync({
active: true,
limit: 100
});
history = await listUpdateRunsAsync({ limit: 100 });
}
for (const run of active) {
const guidance = staleUpdateRunGuidance(run);
if (guidance) note(`Update ${run.runId}: ${guidance}`, "Update history");
}
for (const run of history) if (run.status === "failed" && run.reason === "abandoned" && !isAcknowledgedAbandonedUpdateRun(run)) {
const reason = readInstalledUpdateCandidate(run) ? "the recorded candidate has not been verified as installed and serving" : "the target build was not recorded, so current version equality cannot prove this update completed";
note(`Update ${run.runId} remains abandoned: ${reason}. Run \`openclaw update repair\` to repair the installation and reconcile its history.`, "Update history");
}
const [latest] = history;
if (latest) {
if (latest.status === "failed" && latest.reason && (latest.reason === "update-activation-timeout" || UPDATE_ENVIRONMENT_FAILURE_REASONS.has(latest.reason))) note(`Update ${latest.runId}: ${renderUpdateRunReport(latest).markdown}`, "Update history");
const retiredWarnings = await readRetiredDeferredPluginMigrationWarnings(latest.steps.map((step) => step.detail));
const warnings = updateRunWarningMessages(latest.steps.filter((step) => !retiredWarnings.has(step.detail ?? "")));
if (warnings.length) note(`Recorded warnings from update ${latest.runId} (a later repair may have resolved them):\n${warnings.slice(-3).join("\n")}`, "Update history");
}
}
//#endregion
//#region src/commands/doctor/shared/legacy-config-state-migration-input.ts
function resolveStateMigrationConfigInput(params) {
const pluginDoctorConfig = params.snapshot.sourceConfig ?? params.snapshot.config ?? params.snapshot.parsed;
const sourceConfigBeforeMigrations = params.snapshot.sourceConfigBeforeMigrations ?? pluginDoctorConfig;
if (params.postConvergenceConfig) return {
cfg: params.postConvergenceConfig,
sourceConfigBeforeMigrations,
...pluginDoctorConfig ? { pluginDoctorConfig } : {}
};
if (params.snapshot.valid) return params.snapshot.legacyIssues.length > 0 && pluginDoctorConfig !== void 0 ? {
cfg: params.baseConfig,
pluginDoctorConfig,
sourceConfigBeforeMigrations
} : {
cfg: params.baseConfig,
sourceConfigBeforeMigrations
};
const migrationSource = pluginDoctorConfig ?? params.snapshot.parsed;
if (params.snapshot.legacyIssues.length === 0 || migrationSource === void 0) return null;
const migrated = migrateLegacyConfig(migrationSource, { sourceConfigBeforeMigrations: params.snapshot.sourceConfigBeforeMigrations });
if (!migrated.config || migrated.partiallyValid) return { pluginDoctorConfig: pluginDoctorConfig ?? migrationSource };
return {
cfg: migrated.config,
sourceConfigBeforeMigrations,
...pluginDoctorConfig ? { pluginDoctorConfig } : {}
};
}
//#endregion
//#region src/commands/doctor-config-preflight.ts
/** Config preflight for Doctor: legacy migration, recovery, and snapshot loading. */
const loadState = createLazyRuntimeModule(() => import("./state-migrations.state-dir-DovHCDPq.mjs"));
const loadCronRepair = createLazyRuntimeModule(() => import("./legacy-repair-B3Id5yeg.mjs"));
/** Preserve retired state inputs before the main Doctor config repair flow. */
async function runDoctorConfigPreflight(options = {}) {
if (options.migrateState !== false && options.doctorOnlyStateMigrations === true) {
const { withSqliteReadOnlyWorkerScope } = await import("./sqlite-readonly-worker-DPlfD0t8.mjs");
return await withSqliteReadOnlyWorkerScope(() => runDoctorConfigPreflightOperation(options));
}
return await runDoctorConfigPreflightOperation(options);
}
async function runDoctorConfigPreflightOperation(options) {
assertNoRetiredOAuthSidecarsBeforeConfigRecovery({ env: process.env });
const { config: inspectionConfig, env: inspectionEnv } = readCurrentConfigForResolution();
assertNoRetiredStateFiles("OAuth credential sidecars", listReferencedLegacyOAuthSidecarPaths(inspectionEnv, inspectionConfig));
const stateMigrationsRequested = options.migrateState !== false;
const skipLegacyParentConfigWrite = shouldSkipLegacyUpdateDoctorConfigWrite(process.env);
const measurePreflightStep = (name, run) => measureDoctorConfigPreflightStep(name, run, options.measure);
if (stateMigrationsRequested) await measurePreflightStep("state-write-admission", () => assertOpenClawStateWriteAllowedAtPath({
databasePath: resolveOpenClawStateSqlitePath(process.env),
env: process.env,
recoverOrphanedSidecars: true
}));
await measurePreflightStep("stale-update-runs", () => noteStaleUpdateRuns({ migrateState: stateMigrationsRequested }));
let modelBillingRouteMigrationSource;
const cronCodexRuntimePolicyTargets = [];
const stateMigrationStepReceipts = [];
let postSessionPluginMigration;
let postSessionPluginMigrationPlanBound = false;
let doctorMediaPersistenceAttempted = false;
let configSnapshotRead;
const pluginMigrations = createDoctorPluginMigrationPreparation({
enabled: stateMigrationsRequested,
env: () => process.env,
report: (result) => noteDoctorStateMigrationResult(result),
recordReceipt: (receipt) => stateMigrationStepReceipts.push(receipt),
measure: measurePreflightStep,
runWithPluginMetadataSnapshot: (scope, run) => pluginMetadata.run(scope, run),
doctorOnlyStateMigrations: options.doctorOnlyStateMigrations === true
});
const pluginMetadata = createDoctorPluginMetadataSnapshotScope({
getBaseSnapshot: () => configSnapshotRead?.pluginMetadataSnapshot,
env: process.env,
getDeferredPluginIds: () => pluginMigrations.deferred().map((pending) => pending.pluginId)
});
const noteDoctorStateMigrationResult = (result) => {
pluginMigrations.observe(result);
noteStateMigrationResult(result);
};
const getSnapshotPreparation = createDoctorRehearsalSnapshotPreparation(noteDoctorStateMigrationResult);
const { planScopedConfigRepair, planAdmittedConfigRepair } = createDoctorConfigRepairPlanner({
options,
stateMigrationsRequested,
skipLegacyParentConfigWrite,
runWithPluginMetadataSnapshot: pluginMetadata.run
});
const readConfigSnapshotForPreflight = async (allowCurrentPluginMetadata = true) => await measurePreflightStep("config-snapshot", async () => readConfigPreflightSnapshot({
purpose: "doctor",
allowCurrentPluginMetadata,
includePluginMetadata: options.preparePluginMetadataSnapshot === true,
measure: options.measure,
observe: options.observe,
preparePluginMetadataSnapshot: options.preparePluginMetadataSnapshot === true,
skipPluginValidation: shouldSkipPluginValidationForDoctorConfigPreflight(),
prepareSnapshot: getSnapshotPreparation(options.doctorOnlyStateMigrations === true),
...await pluginMigrations.snapshotOptions()
}));
const stateDirMigrations = stateMigrationsRequested ? await measurePreflightStep("state-migrations-import", loadState) : void 0;
if (stateDirMigrations) noteDoctorStateMigrationResult(await measurePreflightStep("state-dir-migrations", () => stateDirMigrations.autoMigrateLegacyStateDir({ env: process.env })));
await migrateLegacyDoctorConfig({
enabled: options.migrateLegacyConfig !== false,
measure: measurePreflightStep
});
configSnapshotRead = await readConfigSnapshotForPreflight(!stateDirMigrations);
const recovery = await prepareDoctorConfigRecovery({
enabled: options.repairPrefixedConfig === true && !skipLegacyParentConfigWrite,
snapshotRead: configSnapshotRead,
planRepair: planScopedConfigRepair,
readSnapshot: () => readConfigSnapshotForPreflight(false)
});
configSnapshotRead = recovery.snapshotRead;
let snapshot = configSnapshotRead.snapshot;
const rosterMigrationSource = snapshot.sourceConfigBeforeMigrations ?? snapshot.sourceConfig;
const activeConfigRepair = recovery.activeConfigRepair;
noteDoctorConfigPreflightIssues(snapshot, {
invalidConfigNote: options.invalidConfigNote,
activeRepair: activeConfigRepair !== null
});
let baseConfig = snapshot.sourceConfig ?? snapshot.config ?? {};
let automaticConfigRepair = planAdmittedConfigRepair(snapshot, activeConfigRepair);
if (options.doctorOnlyStateMigrations === true && stateDirMigrations) {
const { prepareLegacyStateDatabaseSchema } = await import("./state-migrations.doctor-BHhZCBGE.mjs");
const receipt = await measurePreflightStep("state-schema", () => prepareLegacyStateDatabaseSchema(process.env));
if (receipt.outcome !== "skipped") {
stateMigrationStepReceipts.push(receipt);
noteDoctorStateMigrationResult({
changes: receipt.changes,
warnings: receipt.warnings,
notices: receipt.notices
});
throwIfDoctorStateMigrationRefused(stateMigrationStepReceipts);
}
}
if (options.invocationPurpose === "doctor" || options.doctorOnlyStateMigrations === true) {
const { repairLegacySessionEntryStates } = await import("./doctor-session-delivery-state-DzKJ_thf.mjs");
const repair = options.doctorOnlyStateMigrations === true && stateDirMigrations !== void 0;
const entryState = await measurePreflightStep("session-entry-state", () => repairLegacySessionEntryStates({
apply: repair,
cfg: automaticConfigRepair?.config ?? baseConfig,
env: process.env,
deferSchemaRepair: repair
}));
if (!repair && entryState.found > 0) {
const { SessionStoreMigrationRequiredError } = await import("./migration-required-BVCOqvE2.mjs");
throw new SessionStoreMigrationRequiredError(`Found ${entryState.found} session rows with legacy entry state. Run openclaw doctor --fix before further session inspection; no rows were changed.`);
}
}
let postConvergenceStateConfig;
if (stateDirMigrations) {
const refreshed = await prepareDoctorMigrationPlugins({
cfg: automaticConfigRepair?.config ?? baseConfig,
env: process.env,
retainedPluginIds: pluginMigrations.retainedPluginIds(),
measure: options.measure,
snapshotRead: {
...configSnapshotRead,
snapshot
},
readRefreshedSnapshot: () => readConfigSnapshotForPreflight(false),
onDeferredPlugins: (pending, inspection) => pluginMigrations.converged(pending, snapshot, configSnapshotRead?.pluginMetadataSnapshot, inspection)
});
configSnapshotRead = refreshed;
pluginMetadata.invalidate();
snapshot = refreshed.snapshot;
baseConfig = snapshot.sourceConfig ?? snapshot.config ?? {};
automaticConfigRepair = planAdmittedConfigRepair(snapshot);
postConvergenceStateConfig = automaticConfigRepair?.snapshot.config;
}
const stateMigrationInput = resolveStateMigrationConfigInput({
snapshot,
baseConfig,
postConvergenceConfig: postConvergenceStateConfig
});
const rosterMigrationOwnerId = stateMigrationInput?.cfg ? tryGetLegacyDefaultAgentId(stateMigrationInput.cfg) : void 0;
if (stateDirMigrations) {
if (options.doctorOnlyStateMigrations === true && !stateMigrationInput?.cfg) {
const { detectLegacyExecApprovals, migrateLegacyExecApprovals } = await import("./state-migrations.exec-approvals-DMnMX8tw.mjs");
const stateDir = resolveStateDir(process.env);
noteDoctorStateMigrationResult(await measurePreflightStep("exec-approvals-migration", () => migrateLegacyExecApprovals({
detected: detectLegacyExecApprovals({
stateDir,
doctorOnlyStateMigrations: true
}),
stateDir,
env: process.env
})));
}
if (stateMigrationInput) {
if (stateMigrationInput.cfg) {
const { autoMigrateLegacyState } = await import("./state-migrations.doctor-BHhZCBGE.mjs");
const migrationConfig = stateMigrationInput.cfg;
const pluginDoctorConfig = stateMigrationInput.pluginDoctorConfig;
const { collectCronCodexRuntimePolicyTargetsReadOnly, repairLegacyCronStoreWithoutPrompt } = await measurePreflightStep("cron-repair-import", loadCronRepair);
noteDoctorStateMigrationResult(await measurePreflightStep("cron-repair", () => repairLegacyCronStoreWithoutPrompt({
cfg: withLegacyConfig(migrationConfig, pluginDoctorConfig),
migrateCodexModelRefs: false
})));
if (options.repairPrefixedConfig === true) {
const cronCodexPlan = await measurePreflightStep("cron-policy-scan", () => collectCronCodexRuntimePolicyTargetsReadOnly({ cfg: migrationConfig }));
cronCodexRuntimePolicyTargets.push(...cronCodexPlan.targets);
noteDoctorStateMigrationResult({
changes: [],
warnings: cronCodexPlan.warnings
});
}
const legacyStateResult = await measurePreflightStep("legacy-state-migrations", () => pluginMetadata.run({ config: pluginDoctorConfig ?? migrationConfig }, () => autoMigrateLegacyState({
cfg: migrationConfig,
sourceConfigBeforeMigrations: stateMigrationInput.sourceConfigBeforeMigrations,
...pluginDoctorConfig ? { pluginDoctorConfig } : {},
configIncludedPaths: snapshot.includedPaths ?? [],
env: process.env,
doctorOnlyStateMigrations: options.doctorOnlyStateMigrations,
invocationPurpose: options.invocationPurpose,
...options.agentDatabaseMigrationDiscovery ? { agentDatabaseMigrationDiscovery: options.agentDatabaseMigrationDiscovery } : {},
beforeWorkspaceStateMigration: options.beforeWorkspaceStateMigration,
onStepReceipt: (receipt) => stateMigrationStepReceipts.push(receipt)
})));
postSessionPluginMigration = legacyStateResult.postSessionPluginMigration;
postSessionPluginMigrationPlanBound = options.doctorOnlyStateMigrations === true;
doctorMediaPersistenceAttempted = options.doctorOnlyStateMigrations === true;
noteDoctorStateMigrationResult(legacyStateResult);
if (options.doctorOnlyStateMigrations === true) await assertDoctorPreflightMigrationsComplete({
cfg: migrationConfig,
stepReceipts: stateMigrationStepReceipts,
report: noteDoctorStateMigrationResult
});
} else if (stateMigrationInput.pluginDoctorConfig) {
const pluginDoctorConfig = stateMigrationInput.pluginDoctorConfig;
await migrateRetainedStore({
config: pluginDoctorConfig,
env: process.env,
measure: measurePreflightStep,
report: noteDoctorStateMigrationResult
});
await pluginMigrations.migrate(pluginDoctorConfig);
}
}
}
if (stateDirMigrations && options.doctorOnlyStateMigrations === true && !doctorMediaPersistenceAttempted) {
const { migrateLegacyMediaPersistence } = await import("./state-migrations.media-persistence-D4F3y_PJ.mjs");
noteDoctorStateMigrationResult(await measurePreflightStep("media-persistence-migration", () => migrateLegacyMediaPersistence({ env: process.env })));
const { repairLegacySessionEntryStates } = await import("./doctor-session-delivery-state-DzKJ_thf.mjs");
await repairLegacySessionEntryStates({
apply: true,
cfg: automaticConfigRepair?.config ?? baseConfig,
env: process.env
});
}
if (await pluginMigrations.complete()) {
configSnapshotRead = await readConfigSnapshotForPreflight(false);
snapshot = configSnapshotRead.snapshot;
baseConfig = snapshot.sourceConfig ?? snapshot.config ?? {};
automaticConfigRepair = planAdmittedConfigRepair(snapshot);
}
if (automaticConfigRepair && !skipLegacyParentConfigWrite) {
modelBillingRouteMigrationSource ??= snapshot.sourceConfigBeforeMigrations ?? snapshot.sourceConfig;
await measurePreflightStep("automatic-config-repair", () => pluginMetadata.run({ config: automaticConfigRepair.config }, () => commitAutomaticConfigRepair(automaticConfigRepair, snapshot)));
note(`Migrated legacy config keys in the active openclaw.json:\n${automaticConfigRepair.changes.map((entry) => `- ${entry}`).join("\n")}`, "Doctor changes");
configSnapshotRead = await readConfigSnapshotForPreflight(false);
snapshot = configSnapshotRead.snapshot;
baseConfig = snapshot.sourceConfig ?? snapshot.config ?? {};
}
const deferredPluginMigrations = pluginMigrations.deferred();
return {
snapshot,
baseConfig,
rosterMigrationSource,
...rosterMigrationOwnerId ? { rosterMigrationOwnerId } : {},
...deferredPluginMigrations.length > 0 ? { deferredPluginMigrations } : {},
...modelBillingRouteMigrationSource ? { modelBillingRouteMigrationSource } : {},
...configSnapshotRead.pluginMetadataSnapshot ? { pluginMetadataSnapshot: configSnapshotRead.pluginMetadataSnapshot } : {},
...cronCodexRuntimePolicyTargets.length > 0 ? { cronCodexRuntimePolicyTargets } : {},
...stateMigrationStepReceipts.length > 0 ? { stateMigrationStepReceipts } : {},
...postSessionPluginMigration ? { postSessionPluginMigration } : {},
...postSessionPluginMigrationPlanBound ? { postSessionPluginMigrationPlanBound: true } : {}
};
}
//#endregion
//#region src/commands/doctor/changes-panel-sink.ts
function createDoctorChangesPanelSink(shouldRepair) {
const pending = [];
return {
emit: (changeLines, options = {}) => {
if (changeLines.length === 0) return;
const body = changeLines.join("\n");
const message = options.sanitize ? sanitizeDoctorNote(body) : body;
if (shouldRepair) {
pending.push(message);
return;
}
note(message, "Doctor changes preview");
},
drain: () => pending.splice(0)
};
}
//#endregion
//#region src/commands/doctor/finalize-config-flow.ts
/** Decide whether doctor should write the repaired candidate config or only print hints. */
async function finalizeDoctorConfigFlow(params) {
const confirmedConfigSource = {
path: params.snapshot.path,
hash: params.snapshot.hash ?? hashConfigRaw(params.snapshot.raw)
};
let cfg = params.cfg;
let shouldWriteConfig = params.shouldRepair && params.pendingChanges;
if (!params.shouldRepair && params.pendingChanges) {
shouldWriteConfig = await params.confirm({
message: "Apply recommended config repairs now?",
initialValue: true
});
if (shouldWriteConfig) cfg = params.candidate;
else if (params.fixHints.length > 0) params.note(params.fixHints.join("\n"), "Doctor");
}
return {
cfg,
shouldWriteConfig,
confirmedConfigSource
};
}
//#endregion
//#region src/commands/doctor/shared/config-migration-result.ts
/** Preserve migration provenance before repairs, then prepare the post-write health handoff. */
function prepareDoctorConfigMigrationResult(preflight, snapshot) {
const sourceVersion = snapshot.sourceConfig.meta?.lastTouchedVersion;
const sourceLastTouchedVersion = typeof sourceVersion === "string" ? sourceVersion : void 0;
const billingRouteSource = preflight.modelBillingRouteMigrationSource ?? snapshot.sourceConfigBeforeMigrations ?? snapshot.sourceConfig;
return async (params) => {
let modelBillingRouteWarnings = [];
if ((params.shouldWriteConfig || preflight.modelBillingRouteMigrationSource) && (!isDeepStrictEqual(billingRouteSource.agents, params.cfg.agents) || !isDeepStrictEqual(billingRouteSource.models, params.cfg.models))) {
const { collectModelBillingRouteMigrationWarnings } = await import("./model-billing-route-migration-DDESUzls.mjs");
modelBillingRouteWarnings = params.runWithCurrentPluginMetadata(params.cfg, () => collectModelBillingRouteMigrationWarnings({
before: billingRouteSource,
after: params.cfg,
metadataSnapshot: params.metadataSnapshot
}));
}
let receipts = preflight.stateMigrationStepReceipts;
let postSession = preflight.postSessionPluginMigration;
const planBound = preflight.postSessionPluginMigrationPlanBound;
if (planBound && params.pluginInventoryChanged && postSession) {
const { preparePostSessionPluginMigration } = await import("./state-migrations.plugin-plan-BOk6qV7D.mjs");
const { resolveLivePluginDoctorStateMigrationInventory } = await import("./doctor-contract-registry-BWlChrnb.mjs");
postSession = params.runWithCurrentPluginMetadata(params.cfg, () => preparePostSessionPluginMigration({
mode: "doctor",
inventory: resolveLivePluginDoctorStateMigrationInventory({
config: params.cfg,
env: process.env
})
}));
if (postSession.step.refusal) {
const { createLegacyStateMigrationStepReceipt } = await import("./state-migrations.messages-CHAbDmBr.mjs");
receipts = [...receipts ?? [], createLegacyStateMigrationStepReceipt(postSession.step, {
changes: [],
warnings: [postSession.step.refusal.message]
})];
postSession = void 0;
}
}
return {
...sourceLastTouchedVersion ? { sourceLastTouchedVersion } : {},
...modelBillingRouteWarnings.length > 0 ? { modelBillingRouteWarnings } : {},
...params.metadataSnapshot ? { pluginMetadataSnapshot: params.metadataSnapshot } : {},
...receipts ? { stateMigrationStepReceipts: receipts } : {},
...postSession ? { postSessionPluginMigration: postSession } : {},
...planBound ? { postSessionPluginMigrationPlanBound: true } : {}
};
};
}
//#endregion
//#region src/channels/plugins/setup-promotion-discovery.ts
function resolveDiscoveredChannelSetupPromotionSurface(channelKey, cfg) {
const registry = loadPluginManifestRegistryForPluginRegistry({
config: cfg,
includeDisabled: true
});
if (registry.plugins.some((plugin) => plugin.channels.includes(channelKey) && plugin.packageManifest?.setupFeatures?.configPromotion === "preserve-root")) return { configPromotion: "preserve-root" };
if (getOfficialExternalPluginCatalogManifest(getOfficialExternalPluginCatalogEntry(channelKey) ?? {})?.setupFeatures?.configPromotion === "preserve-root") return { configPromotion: "preserve-root" };
const owner = registry.plugins.find((plugin) => plugin.channels.includes(channelKey) && plugin.packageManifest?.setupFeatures?.configPromotion === true);
if (!owner) return null;
const { plugin } = loadSetupChannelPluginFromManifestRecord({
record: owner,
channelId: channelKey,
env: process.env
});
const setup = plugin?.setupContract ?? plugin?.setup;
return setup && typeof setup === "object" ? setup : null;
}
//#endregion
//#region src/commands/doctor/shared/legacy-mistral-model-cost.ts
const MISTRAL_MODEL_CACHE_READ_COST_BY_ID = {
"codestral-latest": .03,
"devstral-medium-latest": .04,
"magistral-small": .05,
"mistral-large-latest": .05,
"mistral-medium-2508": .04,
"mistral-medium-3-5": .15,
"mistral-small-latest": .01,
"pixtral-large-latest": .2
};
function normalizeLegacyMistralModelCost(params) {
const cost = params.model.cost;
if (!isRecord(cost) || cost.cacheRead !== 0) return params.model;
const normalizedCacheRead = MISTRAL_MODEL_CACHE_READ_COST_BY_ID[params.modelId.toLowerCase()];
if (normalizedCacheRead === void 0) return params.model;
params.changes.push(`Normalized models.providers.${sanitizeForLog(params.providerId)}.models[${params.index}].cost.cacheRead (0 → ${normalizedCacheRead}) for Mistral prompt-cache billing.`);
return {
...params.model,
cost: {
...cost,
cacheRead: normalizedCacheRead
}
};
}
//#endregion
//#region src/commands/doctor/shared/legacy-config-core-normalizers.ts
const log = createSubsystemLogger("doctor");
/** Migrate legacy browser/Chrome relay config to current browser profile settings. */
function normalizeLegacyBrowserConfig(cfg, changes) {
const rawBrowser = cfg.browser;
if (!isRecord(rawBrowser)) return cfg;
const browser = structuredClone(rawBrowser);
let browserChanged = false;
const rawProfiles = browser.profiles;
if (isRecord(rawProfiles)) {
const profiles = { ...rawProfiles };
let profilesChanged = false;
for (const [profileName, rawProfile] of Object.entries(rawProfiles)) {
if (!isRecord(rawProfile)) continue;
if ((normalizeOptionalString(rawProfile.driver) ?? "") !== "extension" || !normalizeOptionalString(rawProfile.cdpUrl)) continue;
const nextProfile = { ...rawProfile };
delete nextProfile.cdpUrl;
profiles[profileName] = nextProfile;
profilesChanged = true;
changes.push(`Removed browser.profiles.${profileName}.cdpUrl (extension driver profiles own their relay endpoint).`);
}
if (profilesChanged) {
browser.profiles = profiles;
browserChanged = true;
}
}
if (!browserChanged) return cfg;
return {
...cfg,
browser
};
}
/** Seed an empty account map without changing an existing account set or route. */
function seedMissingDefaultAccountsFromSingleAccountBase(cfg, changes) {
const channels = cfg.channels;
if (!channels) return cfg;
let channelsChanged = false;
const nextChannels = { ...channels };
for (const [channelId, rawChannel] of Object.entries(channels)) {
if (!isRecord(rawChannel)) continue;
const rawAccounts = rawChannel.accounts;
if (!isRecord(rawAccounts)) continue;
if (Object.keys(rawAccounts).length > 0) continue;
const promotion = resolveSingleAccountPromotion({
channelKey: channelId,
channel: rawChannel,
resolveBundledSurface: (key) => resolveDiscoveredChannelSetupPromotionSurface(key, cfg)
});
if (promotion.kind === "preserve-root") continue;
if (promotion.shouldDeferPromotion) {
log.debug(`Deferring channels.${channelId} single-account promotion until its plugin declares uncovered root keys.`);
continue;
}
const keysToMove = promotion.keysToMove;
if (keysToMove.length === 0) continue;
const defaultAccount = {};
for (const key of keysToMove) {
const value = rawChannel[key];
defaultAccount[key] = value && typeof value === "object" ? structuredClone(value) : value;
}
const nextChannel = { ...rawChannel };
for (const key of keysToMove) delete nextChannel[key];
nextChannel.accounts = { [DEFAULT_ACCOUNT_ID]: defaultAccount };
nextChannels[channelId] = nextChannel;
channelsChanged = true;
changes.push(`Moved channels.${channelId} single-account top-level values into channels.${channelId}.accounts.default.`);
}
if (!channelsChanged) return cfg;
return {
...cfg,
channels: nextChannels
};
}
const LEGACY_CODEX_CLI_RUNTIME_ID = "codex-cli";
const CODEX_APP_SERVER_RUNTIME_ID = "codex";
function migrateUnblockedLegacyRuntimeModelRef(modelRef, blockedModelIdentities) {
return isBlockedLegacyCodexModelRef({
modelRef,
blockedModelIdentities
}) ? null : migrateLegacyRuntimeModelRef(modelRef);
}
function mergeModelEntry(legacyEntry, currentEntry) {
if (!isRecord(legacyEntry) || !isRecord(currentEntry)) return currentEntry ?? legacyEntry;
return mergeModelRefMapEntries(currentEntry, legacyEntry, "models").value;
}
function normalizeLegacyCodexCliAgentRuntimePolicy(raw) {
if (!isRecord(raw)) return {
value: raw,
changed: false
};
if (normalizeOptionalLowercaseString(raw.id) !== LEGACY_CODEX_CLI_RUNTIME_ID) return {
value: raw,
changed: false
};
return {
value: {
...raw,
id: CODEX_APP_SERVER_RUNTIME_ID
},
changed: true
};
}
function normalizeLegacyRuntimeAgentModelConfig(raw, blockedModelIdentities) {
const selectedRefs = [];
const migrate = (ref) => {
const migrated = typeof ref === "string" ? migrateUnblockedLegacyRuntimeModelRef(ref, blockedModelIdentities) : null;
if (!migrated) return ref;
selectedRefs.push({
ref: migrated.ref,
runtime: migrated.runtime
});
return migrated.ref;
};
if (typeof raw === "string") {
const value = migrate(raw);
return selectedRefs.length > 0 ? {
value,
changed: true,
selectedRuntime: selectedRefs[0].runtime,
selectedRefs
} : {
value: raw,
changed: false,
selectedRefs: []
};
}
if (!isRecord(raw)) return {
value: raw,
changed: false,
selectedRefs: []
};
const next = { ...raw };
if (typeof raw.primary === "string") next.primary = migrate(raw.primary);
if (Array.isArray(raw.fallbacks)) next.fallbacks = raw.fallbacks.map(migrate);
if (selectedRefs.length === 0) return {
value: raw,
changed: false,
selectedRefs: []
};
return {
value: next,
changed: true,
selectedRuntime: selectedRefs[0].runtime,
selectedRefs
};
}
function normalizeLegacyRuntimeAllowlistModels(rawModels, blockedModelIdentities) {
if (!isRecord(rawModels)) return {
value: rawModels,
changed: false
};
let changed = false;
const next = {};
const legacyEntries = [];
for (const [rawKey, entry] of Object.entries(rawModels)) {
const migrated = migrateUnblockedLegacyRuntimeModelRef(rawKey, blockedModelIdentities);
if (migrated) {
changed = true;
legacyEntries.push({
migratedKey: migrated.ref,
entry,
runtime: migrated.runtime
});
continue;
}
next[rawKey] = mergeModelEntry(entry, next[rawKey]);
}
for (const { migratedKey, entry, runtime } of legacyEntries) next[migratedKey] = modelEntryWithRuntimePolicy(mergeModelEntry(entry, next[migratedKey]), runtime).entry;
return {
value: next,
changed
};
}
function normalizeLegacyRuntimeModelPolicy(rawPolicy, blockedModelIdentities) {
if (!isRecord(rawPolicy) || !Array.isArray(rawPolicy.allow)) return {
value: rawPolicy,
runtimes: /* @__PURE__ */ new Set(),
selectedRefs: []
};
const runtimes = /* @__PURE__ */ new Set();
const selectedRefs = [];
const allow = rawPolicy.allow.map((entry) => {
if (typeof entry !== "string") return entry;
const migrated = migrateUnblockedLegacyRuntimeModelRef(entry, blockedModelIdentities);
if (!migrated) return entry;
runtimes.add(migrated.runtime);
selectedRefs.push({
ref: migrated.ref,
runtime: migrated.runtime
});
return migrated.ref;
});
return {
value: runtimes.size > 0 ? {
...rawPolicy,
allow
} : rawPolicy,
runtimes,
selectedRefs
};
}
function ensureSelectedModelRuntimePolicies(rawModels, selectedRefs) {
if (selectedRefs.length === 0) return {
value: rawModels,
changed: false
};
const next = isRecord(rawModels) ? { ...rawModels } : {};
let changed = false;
for (const { ref, runtime } of selectedRefs) {
const current = next[ref];
const updated = modelEntryWithRuntimePolicy(current, runtime);
if (!updated.changed) continue;
next[ref] = updated.entry;
changed = true;
}
return {
value: next,
changed
};
}
function normalizeLegacyCodexCliRuntimePinsInModels(rawModels, path, changes) {
if (!isRecord(rawModels)) return {
value: rawModels,
changed: false
};
let changed = false;
const next = { ...rawModels };
for (const [modelRef, rawEntry] of Object.entries(rawModels)) {
if (!isRecord(rawEntry)) continue;
const runtime = normalizeLegacyCodexCliAgentRuntimePolicy(rawEntry.agentRuntime);
if (!runtime.changed) continue;
next[modelRef] = {
...rawEntry,
agentRuntime: runtime.value
};
changed = true;
changes.push(`Moved ${path}.${sanitizeForLog(modelRef)} agentRuntime.id from codex-cli to codex.`);
}
return {
value: next,
changed
};
}
function normalizeLegacyRuntimeAgentContainer(raw, path, changes, blockedModelIdentities) {
let changed = false;
const next = { ...raw };
const model = normalizeLegacyRuntimeAgentModelConfig(raw.model, blockedModelIdentities);
if (model.changed) {
next.model = model.value;
changed = true;
const runtimeSuffix = model.selectedRuntime ? ` and selected ${model.selectedRuntime} runtime` : "";
changes.push(`Moved ${path}.model legacy runtime primary refs to canonical provider refs${runtimeSuffix}.`);
}
const modelPolicy = normalizeLegacyRuntimeModelPolicy(raw.modelPolicy, blockedModelIdentities);
const models = normalizeLegacyRuntimeAllowlistModels(raw.models, blockedModelIdentities);
if (models.changed) {
next.models = models.value;
changed = true;
changes.push(`Moved ${path}.models legacy runtime keys to canonical provider keys.`);
}
const policyRuntimes = ensureSelectedModelRuntimePolicies(next.models, modelPolicy.selectedRefs);
if (policyRuntimes.changed) {
next.models = policyRuntimes.value;
changed = true;
changes.push(`Preserved runtime policy for ${path}.modelPolicy.allow entries.`);
}
if (model.selectedRuntime) {
const modelRuntimes = ensureSelectedModelRuntimePolicies(next.models, model.selectedRefs);
if (modelRuntimes.changed) {
next.models = modelRuntimes.value;
changed = true;
changes.push(`Selected ${model.selectedRuntime} runtime for ${path}.models entries.`);
}
}
for (const key of ["heartbeat", "subagents"]) {
const execution = raw[key];
if (!isRecord(execution)) continue;
const selection = normalizeLegacyRuntimeAgentModelConfig(execution.model, blockedModelIdentities);
if (!selection.changed) continue;
next[key] = {
...execution,
model: selection.value
};
const runtimes = ensureSelectedModelRuntimePolicies(next.models, selection.selectedRefs);
if (runtimes.changed) next.models = runtimes.value;
changed = true;
changes.push(`Moved ${path}.${key}.model to canonical refs with model runtime policy.`);
}
if (modelPolicy.runtimes.size > 0) {
next.modelPolicy = modelPolicy.value;
changed = true;
changes.push(`Moved ${path}.modelPolicy.allow legacy runtime refs to canonical provider refs.`);
}
const codexCliRuntimePins = normalizeLegacyCodexCliRuntimePinsInModels(next.models, `${path}.models`, changes);
if (codexCliRuntimePins.changed) {
next.models = codexCliRuntimePins.value;
changed = true;
}
return {
value: next,
changed
};
}
function normalizeLegacyCodexCliProviderRuntimePins(cfg, changes) {
return normalizeModelProviders(cfg, (rawProvider, providerId) => {
let provider = rawProvider;
const providerRuntime = normalizeLegacyCodexCliAgentRuntimePolicy(rawProvider.agentRuntime);
if (providerRuntime.changed) {
provider = {
...provider,
agentRuntime: providerRuntime.value
};
changes.push(`Moved models.providers.${sanitizeForLog(providerId)} agentRuntime.id from codex-cli to codex.`);
}
return normalizeProviderModels(provider, (entry, index) => {
const runtime = normalizeLegacyCodexCliAgentRuntimePolicy(entry.agentRuntime);
if (!runtime.changed) return entry;
const modelId = normalizeOptionalString(entry.id) ?? `[${index}]`;
changes.push(`Moved models.providers.${sanitizeForLog(providerId)}.models.${sanitizeForLog(modelId)} agentRuntime.id from codex-cli to codex.`);
return {
...entry,
agentRuntime: runtime.value
};
});
});
}
/** Move legacy runtime-tagged model/provider refs onto current agentRuntime policy fields. */
function normalizeLegacyRuntimeModelRefs(cfg, changes, blockedModelIdentities) {
const cfgWithProviders = normalizeLegacyCodexCliProviderRuntimePins(cfg, changes);
const rewriteRemainingSlots = (config) => rewriteModelRefs(config, "config", changes, (modelRef) => {
const migrated = migrateUnblockedLegacyRuntimeModelRef(modelRef, blockedModelIdentities);
return migrated && (migrated.legacyProvider === "codex-cli" || migrated.legacyProvider === "claude-cli" || migrated.legacyProvider === "google-gemini-cli") ? migrated.ref : null;
}).value;
const rawAgents = cfgWithProviders.agents;
if (!isRecord(rawAgents)) return rewriteRemainingSlots(cfgWithProviders);
let changed = false;
const nextAgents = { ...rawAgents };
if (isRecord(rawAgents.defaults)) {
const defaults = normalizeLegacyRuntimeAgentContainer(rawAgents.defaults, "agents.defaults", changes, blockedModelIdentities);
if (defaults.changed) {
nextAgents.defaults = defaults.value;
changed = true;
}
}
if (Array.isArray(rawAgents.list)) {
const nextList = rawAgents.list.map((entry, index) => {
if (!isRecord(entry)) return entry;
const agentId = normalizeOptionalString(entry.id);
const agent = normalizeLegacyRuntimeAgentContainer(entry, agentId ? `agents.list.${sanitizeForLog(agentId)}` : `agents.list[${index}]`, changes, blockedModelIdentities);
if (agent.changed) {
changed = true;
return agent.value;
}
return entry;
});
if (changed) nextAgents.list = nextList;
}
if (isRecord(rawAgents.entries)) {
let nextEntries;
for (const [agentId, entry] of Object.entries(rawAgents.entries)) {
if (!isRecord(entry)) continue;
const agent = normalizeLegacyRuntimeAgentContainer(entry, `agents.entries.${sanitizeForLog(agentId)}`, changes, blockedModelIdentities);
if (agent.changed) (nextEntries ??= { ...rawAgents.entries })[agentId] = agent.value;
}
if (nextEntries) {
nextAgents.entries = nextEntries;
changed = true;
}
}
return rewriteRemainingSlots(changed ? {
...cfgWithProviders,
agents: nextAgents
} : cfgWithProviders);
}
/** Add missing metadata source markers to legacy OpenAI Codex model catalog entries. */
function normalizeLegacyOpenAICodexModelsAddMetadata(cfg, changes) {
return normalizeModelProviders(cfg, (rawProvider, providerId) => {
if (normalizeProviderId(providerId) !== "openai-codex") return rawProvider;
return normalizeProviderModels(rawProvider, (model) => {
if ("metadataSource" in model || !isLegacyModelsAddCodexMetadataModel({
provider: providerId,
model
})) return model;
const safeProviderId = sanitizeForLog(providerId);
const safeModelId = sanitizeForLog(normalizeOptionalString(model.id) ?? "unknown");
changes.push(`Marked models.providers.${safeProviderId}.models.${safeModelId} as /models add metadata so official OpenAI Codex metadata can override it.`);
return Object.assign({}, model, { metadataSource: "models-add" });
});
});
}
function normalizeProviderModels(provider, normalize) {
if (!Array.isArray(provider.models)) return provider;
let changed = false;
const models = provider.models.map((model, index) => {
const next = isRecord(model) ? normalize(model, index) : model;
changed ||= next !== model;
return next;
});
return changed ? {
...provider,
models
} : provider;
}
function normalizeModelProviders(cfg, normalize) {
const providers = cfg.models?.providers;
if (!isRecord(providers)) return cfg;
let nextProviders;
for (const [providerId, provider] of Object.entries(providers)) {
if (!isRecord(provider)) continue;
const next = normalize(provider, providerId);
if (next !== provider) (nextProviders ??= { ...providers })[providerId] = next;
}
return nextProviders ? {
...cfg,
models: {
...cfg.models,
providers: nextProviders
}
} : cfg;
}
/** Rename legacy OpenAI API identifiers to the current completion/chat API ids. */
function normalizeLegacyOpenAIModelProviderApi(cfg, changes) {
return normalizeModelProviders(cfg, (rawProvider, providerId) => {
let provider = rawProvider;
if (provider.api === "openai") {
provider = {
...provider,
api: "openai-completions"
};
changes.push(`Moved models.providers.${sanitizeForLog(providerId)}.api "openai" → "openai-completions".`);
}
return normalizeProviderModels(provider, (model, index) => {
if (model.api !== "openai") return model;
changes.push(`Moved models.providers.${sanitizeForLog(providerId)}.models[${index}].api "openai" → "openai-completions".`);
return {
...model,
api: "openai-completions"
};
});
});
}
/** Remove retired bundled nano-banana skill config after migrating image generation models. */
function normalizeLegacyNanoBananaSkill(cfg, changes) {
const NANO_BANANA_SKILL_KEY = "nano-banana-pro";
const NANO_BANANA_MODEL = "google/gemini-3-pro-image-preview";
const rawSkills = cfg.skills;
if (!isRecord(rawSkills)) return cfg;
let next = cfg;
let skillsChanged = false;
const skills = structuredClone(rawSkills);
if (Array.isArray(skills.allowBundled)) {
const allowBundled = skills.allowBundled.filter((value) => typeof value !== "string" || value.trim() !== NANO_BANANA_SKILL_KEY);
if (allowBundled.length !== skills.allowBundled.length) {
if (allowBundled.length === 0) {
delete skills.allowBundled;
changes.push(`Removed skills.allowBundled entry for ${NANO_BANANA_SKILL_KEY}.`);
} else {
skills.allowBundled = allowBundled;
changes.push(`Removed ${NANO_BANANA_SKILL_KEY} from skills.allowBundled.`);
}
skillsChanged = true;
}
}
const rawEntries = skills.entries;
if (!isRecord(rawEntries) || !isRecord(rawEntries[NANO_BANANA_SKILL_KEY])) {
if (!skillsChanged) return cfg;
return {
...cfg,
skills
};
}
const rawLegacyEntry = rawEntries[NANO_BANANA_SKILL_KEY];
if (next.agents?.defaults?.mediaModels?.image === void 0) {
next = {
...next,
agents: {
...next.agents,
defaults: {
...next.agents?.defaults,
mediaModels: {
...next.agents?.defaults?.mediaModels,
image: { primary: NANO_BANANA_MODEL }
}
}
}
};
changes.push(`Moved skills.entries.${NANO_BANANA_SKILL_KEY} → agents.defaults.mediaModels.image.primary (${NANO_BANANA_MODEL}).`);
}
const legacyEnv = isRecord(rawLegacyEntry.env) ? rawLegacyEntry.env : void 0;
const legacyEnvApiKey = normalizeOptionalString(legacyEnv?.GEMINI_API_KEY) ?? "";
const legacyApiKey = legacyEnvApiKey || (typeof rawLegacyEntry.apiKey === "string" ? normalizeOptionalString(rawLegacyEntry.apiKey) : rawLegacyEntry.apiKey && isRecord(rawLegacyEntry.apiKey) ? structuredClone(rawLegacyEntry.apiKey) : void 0);
const rawModels = isRecord(next.models) ? structuredClone(next.models) : {};
const rawProviders = isRecord(rawModels.providers) ? { ...rawModels.providers } : {};
const rawGoogle = isRecord(rawProviders.google) ? { ...rawProviders.google } : {};
if (!(rawGoogle.apiKey !== void 0) && legacyApiKey) {
rawGoogle.apiKey = legacyApiKey;
if (!rawGoogle.baseUrl) rawGoogle.baseUrl = DEFAULT_GOOGLE_API_BASE_URL;
if (!Array.isArray(rawGoogle.models)) rawGoogle.models = [];
rawProviders.google = rawGoogle;
rawModels.providers = rawProviders;
next = {
...next,
models: rawModels
};
changes.push(`Moved skills.entries.${NANO_BANANA_SKILL_KEY}.${legacyEnvApiKey ? "env.GEMINI_API_KEY" : "apiKey"} → models.providers.google.apiKey.`);
}
const entries = { ...rawEntries };
delete entries[NANO_BANANA_SKILL_KEY];
if (Object.keys(entries).length === 0) delete skills.entries;
else skills.entries = entries;
changes.push(`Removed legacy skills.entries.${NANO_BANANA_SKILL_KEY}.`);
if (Object.keys(skills).length === 0) {
const { skills: _ignored, ...rest } = next;
return rest;
}
return {
...next,
skills
};
}
function normalizeConfiguredPositiveInteger(value) {
if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) return;
return Math.floor(value);
}
function resolveConfiguredOllamaModelNumCtxBudget(params) {
if (normalizeConfiguredPositiveInteger(params.model.contextTokens) !== void 0) return;
const modelContextWindow = normalizeConfiguredPositiveInteger(params.model.contextWindow);
const providerContextWindow = normalizeConfiguredPositiveInteger(params.provider.contextWindow);
if (modelContextWindow !== void 0 || providerContextWindow !== void 0) return modelContextWindow ?? (params.providerNumCtxApplies ? void 0 : providerContextWindow);
return normalizeConfiguredPositiveInteger(params.model.maxTokens) ?? (params.providerNumCtxApplies ? void 0 : normalizeConfiguredPositiveInteger(params.provider.maxTokens));
}
function isNativeOllamaProviderConfig(provider) {
return normalizeOptionalLowercaseString(provider.api) === "ollama";
}
function isNativeOllamaModelConfig(provider, model) {
return (normalizeOptionalLowercaseString(model.api) || normalizeOptionalLowercaseString(provider.api)) === "ollama";
}
function hasConfiguredOllamaProviderNumCtx(provider) {
const rawParams = provider.params;
return isRecord(rawParams) && Object.hasOwn(rawParams, "num_ctx");
}
function applyLegacyOllamaProviderNumCtxParams(params) {
if (!isNativeOllamaProviderConfig(params.provider)) return params.provider;
const rawParams = params.provider.params;
if (rawParams !== void 0 && !isRecord(rawParams)) return params.provider;
if (rawParams && Object.hasOwn(rawParams, "num_ctx")) return params.provider;
const numCtx = normalizeConfiguredPositiveInteger(params.provider.contextWindow) ?? normalizeConfiguredPositiveInteger(params.provider.maxTokens);
if (numCtx === void 0) return params.provider;
params.changes.push(`Set models.providers.${sanitizeForLog(params.providerId)}.params.num_ctx to ${numCtx} for native Ollama compatibility.`);
return {
...params.provider,
params: rawParams ? {
...rawParams,
num_ctx: numCtx
} : { num_ctx: numCtx }
};
}
/** Seed native Ollama num_ctx params from legacy context-token budgets. */
function normalizeLegacyOllamaNativeNumCtxParams(cfg, changes) {
return normalizeModelProviders(cfg, (rawProvider, providerId) => {
const rawModels = rawProvider.models;
if (!Array.isArray(rawModels)) return rawProvider;
const provider = rawModels.some((model) => isRecord(model) && normalizeConfiguredPositiveInteger(model.contextTokens) !== void 0) ? rawProvider : applyLegacyOllamaProviderNumCtxParams({
providerId,
provider: rawProvider,
changes
});
const providerNumCtxApplies = isNativeOllamaProviderConfig(provider) && hasConfiguredOllamaProviderNumCtx(provider);
return normalizeProviderModels(provider, (model, index) => {
if (!isNativeOllamaModelConfig(provider, model)) return model;
const rawParams = model.params;
if (rawParams !== void 0 && !isRecord(rawParams)) return model;
if (rawParams && Object.hasOwn(rawParams, "num_ctx")) return model;
const numCtx = resolveConfiguredOllamaModelNumCtxBudget({
model,
provider,
providerNumCtxApplies
});
if (numCtx === void 0) return model;
changes.push(`Set models.providers.${sanitizeForLog(providerId)}.models[${index}].params.num_ctx to ${numCtx} for native Ollama compatibility.`);
return Object.assign({}, model, { params: rawParams ? {
...rawParams,
num_ctx: numCtx
} : { num_ctx: numCtx } });
});
});
}
/** Normalize stale Mistral model defaults such as prompt-cache read cost. */
function normalizeLegacyMistralModelDefaults(cfg, changes) {
return normalizeModelProviders(cfg, (rawProvider, providerId) => {
if (normalizeProviderId(providerId) !== "mistral") return rawProvider;
return normalizeProviderModels(rawProvider, (model, index) => {
const modelId = normalizeOptionalString(model.id) ?? "";
if (!modelId) return model;
let nextModel = model;
const contextWindow = asFiniteNumber(model.contextWindow) ?? null;
const maxTokens = asFiniteNumber(model.maxTokens) ?? null;
if (contextWindow !== null && maxTokens !== null) {
const normalizedMaxTokens = resolveNormalizedProviderModelMaxTokens({
providerId,
modelId,
contextWindow,
rawMaxTokens: maxTokens
});
if (normalizedMaxTokens !== maxTokens) {
nextModel = Object.assign({}, nextModel, { maxTokens: normalizedMaxTokens });
changes.push(`Normalized models.providers.${providerId}.models[${index}].maxTokens (${maxTokens} → ${normalizedMaxTokens}) to avoid Mistral context-window rejects.`);
}
}
return normalizeLegacyMistralModelCost({
providerId,
model: nextModel,
modelId,
index,
changes
});
});
});
}
//#endregion
//#region src/commands/doctor/shared/legacy-config-compatibility-base.ts
/** Run common compatibility migrations before caller-specific setup/channel passes. */
function normalizeBaseCompatibilityConfigValues(cfg, changes, afterBrowser, blockedModelIdentities) {
let next = seedMissingDefaultAccountsFromSingleAccountBase(cfg, changes);
next = normalizeLegacyBrowserConfig(next, changes);
next = afterBrowser ? afterBrowser(next) : next;
for (const migrate of [
migrateLegacyWebSearchConfig,
migrateLegacyWebFetchConfig,
migrateLegacyXSearchConfig
]) {
const migrated = migrate(next);
if (migrated.changes.length === 0) continue;
next = migrated.config;
changes.push(...migrated.changes);
}
next = normalizeLegacyNanoBananaSkill(next, changes);
next = normalizeLegacyTalkConfig(next, changes);
next = normalizeLegacyOpenAIModelProviderApi(next, changes);
next = normalizeLegacyRuntimeModelRefs(next, changes, blockedModelIdentities);
next = normalizeLegacyOllamaNativeNumCtxParams(next, changes);
return normalizeLegacyMistralModelDefaults(next, changes);
}
//#endregion
//#region src/commands/doctor/shared/legacy-secret-inputs.ts
/** Normalize only registered credential paths before config validation and backup/write. */
function migrateLegacySecretInputs(config) {
let next;
const changes = [];
for (const target of discoverConfigSecretTargets(config)) {
const original = target.value;
const providerless = isLegacySecretRefWithoutProvider(original);
const ref = providerless ? coerceSecretRef(target.value, config.secrets?.defaults) : parseLegacySecretRefEnvMarker(target.value, config.secrets?.defaults?.env);
if (!ref) continue;
next ??= structuredClone(config);
if (setPathExistingStrict(next, target.pathSegments, ref)) {
changes.push(providerless ? `Added provider ${ref.provider} to ${target.path} SecretRef.` : `Moved ${target.path} ${String(target.value).trim()} marker → structured env SecretRef.`);
if (hasLegacySecretRefExtraFields(original)) changes.push(`Canonicalized ${target.path} SecretRef to source/provider/id; Doctor preserves removed fields in the original config backup before writing the repair.`);
}
}
return {
config: next ?? config,
changes
};
}
//#endregion
//#region src/commands/doctor/shared/reserved-mcp-server-name-migrate.ts
const RESERVED_MCP_SERVER_NAME = "__proto__";
function resolveMcpServers(raw, nodeHost) {
if (!isRecord(raw)) return;
const owner = nodeHost ? raw.nodeHost : raw;
if (!isRecord(owner)) return;
const mcp = isRecord(owner.mcp) ? owner.mcp : void 0;
return isRecord(mcp?.servers) ? mcp.servers : void 0;
}
/** Drop reserved MCP server names before canonical config validation runs. */
function migrateReservedMcpServerNames(cfg, sourceRaw = cfg) {
const locations = [{
path: "mcp.servers",
nodeHost: false
}, {
path: "nodeHost.mcp.servers",
nodeHost: true
}].filter(({ nodeHost }) => [sourceRaw, cfg].some((value) => Object.hasOwn(resolveMcpServers(value, nodeHost) ?? {}, RESERVED_MCP_SERVER_NAME)));
if (locations.length === 0) return {
config: cfg,
changes: []
};
const next = structuredClone(cfg);
const changes = [];
for (const { path, nodeHost } of locations) {
const servers = resolveMcpServers(next, nodeHost);
if (servers) delete servers[RESERVED_MCP_SERVER_NAME];
changes.push(`Dropped MCP server "${RESERVED_MCP_SERVER_NAME}" from ${path} because the name is reserved; re-add it under a different name.`);
}
return {
config: next,
changes
};
}
//#endregion
//#region src/commands/doctor/shared/legacy-config-core-migrate.ts
function repairAgentRoster(cfg, repair) {
const roster = readAgentRosterProperty(cfg);
const values = roster?.value;
if (!roster || !isRecord(values) && !Array.isArray(values)) return cfg;
if (Array.isArray(values) !== (roster.kind === "list")) return cfg;
let changed = false;
const entries = Object.entries(values).map(([key, agent]) => {
const path = roster.kind === "entries" ? `agents.entries.${key}` : `agents.list[${key}]`;
const next = isRecord(agent) ? repair(agent, path) : agent;
changed ||= next !== agent;
return [key, next];
});
return changed ? {
...cfg,
agents: {
...cfg.agents,
[roster.kind]: roster.kind === "entries" ? Object.fromEntries(entries) : entries.map(([, agent]) => agent)
}
} : cfg;
}
function repairInvalidHeartbeatActiveHours(cfg, changes) {
const repairHeartbeat = (heartbeat, path) => {
if (!isRecord(heartbeat) || !Object.hasOwn(heartbeat, "activeHours")) return heartbeat;
if (HeartbeatSchema.safeParse({ activeHours: heartbeat.activeHours }).success) return heartbeat;
const { activeHours: _activeHours, ...rest } = heartbeat;
changes.push(`Removed invalid ${path}.activeHours; heartbeats will use unrestricted hours until it is reconfigured.`);
return rest;
};
const defaultsHeartbeat = repairHeartbeat(cfg.agents?.defaults?.heartbeat, "agents.defaults.heartbeat");
const next = repairAgentRoster(cfg, (agent, path) => {
const heartbeat = repairHeartbeat(agent.heartbeat, `${path}.heartbeat`);
return heartbeat === agent.heartbeat ? agent : {
...agent,
heartbeat
};
});
if (defaultsHeartbeat === cfg.agents?.defaults?.heartbeat) return next;
return {
...next,
agents: {
...next.agents,
defaults: {
...next.agents?.defaults,
heartbeat: defaultsHeartbeat
}
}
};
}
function repairNullAgentWorkspaces(cfg, changes) {
let repaired = 0;
const next = repairAgentRoster(cfg, (agent) => {
if (agent.workspace === null) {
repaired += 1;
const { workspace: _workspace, ...rest } = agent;
return rest;
}
return agent;
});
if (repaired === 0) return cfg;
changes.push(`Removed null workspace value${repaired === 1 ? "" : "s"} from agents.${readAgentRosterProperty(cfg)?.kind} entr${repaired === 1 ? "y" : "ies"}.`);
return next;
}
/** Normalize pre-admission config through core, plugin setup, channel, and secret-ref migrations. */
function normalizeCompatibilityConfigValues(raw, options = {}) {
if (!isRecord(raw)) throw new TypeError("Compatibility config normalization requires an object");
const changes = [];
const warnings = [];
const reservedMcpServerNames = migrateReservedMcpServerNames(raw, options.sourceRaw);
changes.push(...reservedMcpServerNames.changes);
let next = normalizeBaseCompatibilityConfigValues(reservedMcpServerNames.config, changes, (config) => {
const setupMigration = runPluginSetupConfigMigrations({ config });
warnings.push(...setupMigration.warnings ?? []);
changes.push(...setupMigration.changes);
return setupMigration.config;
}, options.blockedModelIdentities);
const tuningCandidate = structuredClone(next);
if (stripRetiredTuningKnobs(tuningCandidate, changes)) next = tuningCandidate;
const channelMigrations = applyChannelDoctorCompatibilityMigrations(next, { historicalWebhookListeners: true });
warnings.push(...channelMigrations.warnings ?? []);
if (channelMigrations.changes.length > 0) {
next = channelMigrations.next;
changes.push(...channelMigrations.changes);
}
const secretRefMarkers = migrateLegacySecretInputs(next);
if (secretRefMarkers.changes.length > 0) {
next = secretRefMarkers.config;
changes.push(...secretRefMarkers.changes);
}
next = normalizeLegacyOpenAICodexModelsAddMetadata(next, changes);
next = repairInvalidHeartbeatActiveHours(next, changes);
next = repairNullAgentWorkspaces(next, changes);
next = migrateLegacyCommandOwners(next, changes);
next = pruneBindingsForMissingAgents(next, changes);
return {
config: next,
changes,
...warnings.length ? { warnings } : {}
};
}
//#endregion
//#region src/commands/doctor-config-flow.ts
async function refreshGatewayAuthStateAfterAuthProfileRepair() {
for (const request of [{
method: "secrets.reload",
params: {}
}, {
method: "models.authStatus",
params: { refresh: true }
}]) try {
await callGateway({
...request,
timeoutMs: 3e3
});
} catch {}
}
async function loadAndMaybeMigrateDoctorConfig(params) {
const shouldRepair = params.options.repair === true || params.options.yes === true;
const preflight = await withProgress({
label: "Checking OpenClaw state…",
enabled: params.options.nonInteractive !== true && params.options.json !== true,
delayMs: 200
}, (progress) => runDoctorConfigPreflight({
observe: false,
invocationPurpose: "doctor",
repairPrefixedConfig: shouldRepair,
doctorOnlyStateMigrations: shouldRepair,
preparePluginMetadataSnapshot: true,
...params.agentDatabaseMigrationDiscovery ? { agentDatabaseMigrationDiscovery: params.agentDatabaseMigrationDiscovery } : {},
beforeWorkspaceStateMigration: createWorkspaceAliasMigrationRepair(params.prompter, progress.done),
measure: async (name, run) => {
progress.setLabel(`${name.slice(name.lastIndexOf(".") + 1).replaceAll("-", " ")}…`);
return await run();
}
}));
const { snapshot, baseConfig: baseCfg } = preflight;
const referenceSource = prepareDoctorConfigReferenceSource(snapshot);
const pluginMetadataSnapshotState = { current: preflight.pluginMetadataSnapshot };
const { createDoctorPluginMetadataSnapshotScope } = await import("./plugin-metadata-snapshot-scope-DXzxnOjt.mjs");
const pluginMetadataSnapshotScope = createDoctorPluginMetadataSnapshotScope({
getBaseSnapshot: () => pluginMetadataSnapshotState.current,
env: process.env,
getDeferredPluginIds: () => preflight.deferredPluginMigrations?.map((pending) => pending.pluginId) ?? []
});
const runWithPluginMetadataSnapshot = pluginMetadataSnapshotScope.run;
const invalidatePluginMetadataSnapshot = () => {
pluginMetadataSnapshotState.current = void 0;
pluginMetadataSnapshotScope.invalidate();
};
const runWithCurrentPluginMetadata = (config, run) => {
const soleAgentId = tryResolveSoleAgentId(config);
return runWithPluginMetadataSnapshot({
config,
workspaceDir: soleAgentId ? resolveAgentWorkspaceDir(config, soleAgentId) : void 0
}, run);
};
let state = {
cfg: baseCfg,
candidate: structuredClone(baseCfg),
pendingChanges: false,
fixHints: []
};
const explicitSetPaths = [];
let shouldRepairCronCodexModelRefsAfterConfigWrite = false;
let openAICodexAuthProfileIdMap;
let modelRetirementRepairRan = false;
let retiredModelRefConfig;
const doctorFixCommand = formatCliCommand("openclaw doctor --fix");
const changesPanelSink = createDoctorChangesPanelSink(shouldRepair);
const configRepairWarnings = [];
const applyConfigMutation = (mutation, options) => {
changesPanelSink.emit(mutation.changes, options.sanitize ? { sanitize: true } : {});
if (options.emitWarnings && mutation.warnings?.length) {
emitDoctorNotes({
note,
warningNotes: mutation.warnings
});
configRepairWarnings.push(...mutation.warnings);
}
state = applyDoctorConfigMutation({
state,
mutation,
shouldRepair,
fixHint: options.fixHint
});
};
const finalizeMigrationResult = prepareDoctorConfigMigrationResult(preflight, snapshot);
const sourceRosterConfig = snapshot.sourceConfigBeforeMigrations ?? snapshot.sourceConfig;
const rosterMigrationNeeded = findLegacyConfigRuleIssues(sourceRosterConfig, LEGACY_AGENT_ROSTER_RULES).length > 0 || listAgentEntries(sourceRosterConfig).length === 0 || baseCfg.agents?.ownership === void 0 && listAgentEntries(baseCfg).length > 1;
const legacyStep = runWithCurrentPluginMetadata(state.candidate, () => applyLegacyCompatibilityStep({
snapshot,
state,
shouldRepair,
doctorFixCommand
}));
state = legacyStep.state;
const legacyDefaultAgentId = preflight.rosterMigrationOwnerId ?? tryGetLegacyDefaultAgentId(state.candidate) ?? resolveLegacyAgentRosterOwner(preflight.rosterMigrationSource ?? sourceRosterConfig);
if (legacyDefaultAgentId) {
retainLegacyDefaultAgentId(state.cfg, legacyDefaultAgentId);
retainLegacyDefaultAgentId(state.candidate, legacyDefaultAgentId);
}
const includeOwnsRoster = configIncludeOwnsAgentRoster(snapshot);
const persistCanonicalAgentRoster = snapshot.exists && rosterMigrationNeeded && !includeOwnsRoster;
if (persistCanonicalAgentRoster) {
applyConfigMutation({
config: state.candidate,
changes: ["Prepared the canonical agent roster for persistence."]
}, { fixHint: `Run "${doctorFixCommand}" to persist the explicit agent roster.` });
if (state.candidate.agents?.ownership === "explicit") explicitSetPaths.push(["agents", "ownership"]);
}
const { prepareSessionStoreOwnerRecovery } = await import("./doctor-session-store-owner-CoHtW2Xv.mjs");
const sessionStoreOwnerRecovery = await prepareSessionStoreOwnerRecovery({
config: state.candidate,
snapshot,
prompter: params.prompter
});
applyConfigMutation(sessionStoreOwnerRecovery, {
fixHint: `Run "${doctorFixCommand}" to review session-store ownership recovery.`,
sanitize: true,
emitWarnings: true
});
const { collectBlockedLegacyOpenAICodexProviderPlan } = await import("./legacy-config-migrations.runtime.models-DIK5NRhH.mjs");
const blockedCodexProviderPlan = collectBlockedLegacyOpenAICodexProviderPlan(state.candidate);
const blockedCodexModelIdentities = new Set(blockedCodexProviderPlan.blockedModelIdentities);
if (preflight.cronCodexRuntimePolicyTargets?.length) {
const { repairCronCodexRuntimePolicies } = await import("./runtime-policy-migration-DvDWEXUk.mjs");
const cronRuntimeRepair = repairCronCodexRuntimePolicies({
cfg: state.candidate,
targets: preflight.cronCodexRuntimePolicyTargets,
blockedModelIdentities: blockedCodexModelIdentities
});
applyConfigMutation(cronRuntimeRepair, {
fixHint: `Run "${doctorFixCommand}" to preserve migrated cron runtime policy.`,
emitWarnings: true
});
const blockedTargets = new Set(cronRuntimeRepair.blockedTargets.map(cronCodexRuntimePolicyTargetKey));
shouldRepairCronCodexModelRefsAfterConfigWrite = preflight.cronCodexRuntimePolicyTargets.some((target) => !blockedTargets.has(cronCodexRuntimePolicyTargetKey(target)));
}
const pluginLegacyIssues = await (async () => {
if (snapshot.parsed === snapshot.sourceConfig) return [];
const { findDoctorLegacyConfigIssues } = await import("./legacy-config-issues-Cg6VC_-R.mjs");
return runWithCurrentPluginMetadata(state.candidate, () => findDoctorLegacyConfigIssues(snapshot.parsed, snapshot.parsed));
})();
const seenLegacyIssues = new Set(snapshot.legacyIssues.map((issue) => `${issue.path}:${issue.message}`));
const pluginIssueLines = pluginLegacyIssues.filter((issue) => {
const key = `${issue.path}:${issue.message}`;
if (seenLegacyIssues.has(key)) return false;
seenLegacyIssues.add(key);
return true;
}).map((issue) => `- ${issue.path}: ${issue.message}`);
const legacyIssueLines = [...legacyStep.issueLines, ...pluginIssueLines];
if (pluginIssueLines.length > 0 && !shouldRepair && !state.fixHints.includes(`Run "${doctorFixCommand}" to migrate legacy config keys.`)) state.fixHints.push(`Run "${doctorFixCommand}" to migrate legacy config keys.`);
if (legacyIssueLines.length > 0) note(legacyIssueLines.join("\n"), "Legacy config keys detected");
changesPanelSink.emit(legacyStep.changeLines);
const { MODEL_METADATA_CORRUPTION_AUDIT_LIMIT, repairGeneratedModelMetadataCorruption } = await import("./model-metadata-corruption-repair-B1GKvBzX.mjs");
applyConfigMutation(runWithCurrentPluginMetadata(state.candidate, () => repairGeneratedModelMetadataCorruption({
config: state.candidate,
authoredRoot: snapshot.parsed,
configPath: snapshot.path,
currentHash: hashConfigRaw(snapshot.raw),
auditRecords: readRecentConfigAuditRecords({
env: process.env,
homedir,
limit: MODEL_METADATA_CORRUPTION_AUDIT_LIMIT
})
})), {
fixHint: `Run "${doctorFixCommand}" to remove audit-proven generated model metadata.`,
emitWarnings: true
});
noteDoctorHookConfigWarnings(state.cfg, snapshot.path);
applyConfigMutation(runWithCurrentPluginMetadata(state.candidate, () => normalizeCompatibilityConfigValues(state.candidate, {
blockedModelIdentities: blockedCodexModelIdentities,
sourceRaw: snapshot.parsed
})), {
fixHint: `Run "${doctorFixCommand}" to apply these changes.`,
emitWarnings: true
});
const { repairUnownedChannelAccountBindings } = await import("./legacy-config-binding-repair-BeyW45Wl.mjs");
applyConfigMutation(runWithCurrentPluginMetadata(state.candidate, () => repairUnownedChannelAccountBindings({
config: state.candidate,
sourceConfigBeforeMigrations: snapshot.sourceConfigBeforeMigrations
})), {
fixHint: `Run "${doctorFixCommand}" to preserve channel account ownership.`,
emitWarnings: true
});
const { prepareTailscaleConfigMigration } = await import("./doctor-tailscale-pOCUYQ3t.mjs");
applyConfigMutation(await prepareTailscaleConfigMigration({
cfg: state.candidate,
env: process.env
}), {
fixHint: `Run "${doctorFixCommand}" to apply safe Tailscale configuration migrations.`,
emitWarnings: true
});
const { prepareRetiredPhoneControlCleanup } = await import("./doctor-retired-phone-control-B7TieL3q.mjs");
const retiredPhoneControlCleanup = await prepareRetiredPhoneControlCleanup({
cfg: state.candidate,
env: process.env
});
applyConfigMutation({
config: retiredPhoneControlCleanup.config,
changes: retiredPhoneControlCleanup.configChanges,
warnings: retiredPhoneControlCleanup.warnings
}, {
fixHint: `Run "${doctorFixCommand}" to retire Phone Control lease configuration.`,
emitWarnings: true
});
if (retiredPhoneControlCleanup.cleanupPending && !shouldRepair) note(`Retired Phone Control lease state remains. Run "${doctorFixCommand}" to archive it.`, "Legacy state detected");
const { recoverInstalledPluginConfigIds } = await import("./installed-plugin-id-recovery-l7pcSmXO.mjs");
const installedPluginRecovery = await recoverInstalledPluginConfigIds(state.candidate, process.env);
applyConfigMutation({
...installedPluginRecovery,
warnings: installedPluginRecovery.notices
}, {
fixHint: `Run "${doctorFixCommand}" to apply these changes.`,
emitWarnings: true
});
if (referenceSource) referenceSource.installedPluginIdRecovery = installedPluginRecovery.recovery;
const pluginActivationSourceConfig = state.candidate;
const { collectCodexPluginActivationWarnings } = await import("./codex-plugin-activation-warning-cn_i_J1A.mjs");
emitDoctorNotes({
note,
warningNotes: collectCodexPluginActivationWarnings(pluginActivationSourceConfig)
});
const { applyPluginAutoEnable } = await import("./plugin-auto-enable-Bw5N3luC.mjs");
applyConfigMutation(runWithCurrentPluginMetadata(state.candidate, () => applyPluginAutoEnable({
config: state.candidate,
env: process.env
})), { fixHint: `Run "${doctorFixCommand}" to apply these changes.` });
if (!shouldRepair) {
const { repairStaleAgentModelRefs } = await import("./stale-agent-model-ref-repair-R1evxWug.mjs");
const staleAgentModelRepair = runWithCurrentPluginMetadata(state.candidate, () => repairStaleAgentModelRefs(state.candidate, { env: process.env }));
retiredModelRefConfig = staleAgentModelRepair.retiredModelRefConfig;
applyConfigMutation(staleAgentModelRepair, {
fixHint: `Run "${doctorFixCommand}" to remove stale agent model references.`,
sanitize: true,
emitWarnings: true
});
}
const [{ collectPluginToolAllowlistWarnings }, { collectGitHubUpgradeWarnings }, { normalizePluginsConfig }] = await Promise.all([
import("./plugin-tool-allowlist-warnings-C9jO6LhG.mjs"),
import("./github-preview-upgrade-DnWfUUgI.mjs"),
import("./config-state-CNnS6siP.mjs")
]);
const pluginWarnings = [...runWithCurrentPluginMetadata(state.candidate, () => collectPluginToolAllowlistWarnings({
cfg: state.candidate,
env: process.env
})), ...collectGitHubUpgradeWarnings(normalizePluginsConfig(state.candidate.plugins))];
if (pluginWarnings.length > 0) note(sanitizeDoctorNote(pluginWarnings.join("\n")), "Doctor warnings");
const hasConfiguredChannels = listDoctorConfiguredChannelIds(state.candidate, { configEntryPolicy: "raw" }).length > 0;
let collectMutableAllowlistWarnings;
if (hasConfiguredChannels) {
const channelDoctor = await import("./channel-doctor-BovHUh0d.mjs");
collectMutableAllowlistWarnings = channelDoctor.collectChannelDoctorMutableAllowlistWarnings;
const channelDoctorSequence = await runWithCurrentPluginMetadata(state.candidate, () => channelDoctor.runChannelDoctorConfigSequences({
cfg: state.candidate,
env: process.env,
shouldRepair
}));
emitDoctorNotes({
note,
changeNotes: channelDoctorSequence.changeNotes,
infoNotes: channelDoctorSequence.infoNotes,
warningNotes: channelDoctorSequence.warningNotes
});
const staleChannelCleanups = await runWithCurrentPluginMetadata(state.candidate, () => channelDoctor.collectChannelDoctorStaleConfigMutations(state.candidate, { env: process.env }));
for (const staleCleanup of staleChannelCleanups) applyConfigMutation(staleCleanup, {
fixHint: `Run "${doctorFixCommand}" to remove stale channel plugin references.`,
sanitize: true,
emitWarnings: true
});
}
const { repairHooksTokenReuseGatewayAuth } = await import("./hooks-token-reuse-repair-Bw4Qi1Xi.mjs");
applyConfigMutation(await repairHooksTokenReuseGatewayAuth(state.candidate, process.env), { fixHint: `Run "${doctorFixCommand}" to rotate hooks.token away from Gateway auth.` });
if (shouldRepair) {
const { runDoctorRepairSequence } = await import("./repair-sequencing-D4BOALnn.mjs");
const prompter = params.prompter;
const repairSequence = await runDoctorRepairSequence({
state,
installedPluginIdRecovery: installedPluginRecovery.recovery,
doctorFixCommand,
env: process.env,
blockedCodexProviderPlan,
pluginMetadataSnapshotState,
runWithPluginMetadataSnapshot,
...prompter ? { onCapabilityConsent: createPluginCapabilityConsentPrompter({
note: async (message, title) => note(message, title),
confirm: (confirmation) => prompter.confirmRuntimeRepair({
...confirmation,
requiresInteractiveConfirmation: true
})
}) } : {}
});
state = repairSequence.state;
if (referenceSource) referenceSource.installedPluginIdRecovery = new Map([...installedPluginRecovery.recovery, ...repairSequence.installedPluginIdRecovery]);
pluginMetadataSnapshotState.current = repairSequence.pluginMetadataSnapshot;
openAICodexAuthProfileIdMap = repairSequence.openAICodexAuthProfileIdMap;
retiredModelRefConfig = repairSequence.retiredModelRefConfig;
modelRetirementRepairRan = repairSequence.modelRetirementRepairRan;
if (repairSequence.authProfilesRepaired) await refreshGatewayAuthStateAfterAuthProfileRepair();
emitDoctorNotes({
note,
changeNotes: repairSequence.changeNotes,
warningNotes: repairSequence.warningNotes
});
for (const configChange of repairSequence.configChangeNotes ?? []) changesPanelSink.emit([configChange]);
} else {
const { collectDoctorPreviewNotes } = await import("./preview-warnings-Fe0kPmLO.mjs");
const previewNotes = await runWithCurrentPluginMetadata(state.candidate, () => collectDoctorPreviewNotes({
cfg: state.candidate,
activationSourceConfig: pluginActivationSourceConfig,
doctorFixCommand,
env: process.env,
allowExec: params.options.allowExec === true,
blockedCodexProviderPlan
}));
emitDoctorNotes({
note,
infoNotes: previewNotes.infoNotes,
warningNotes: previewNotes.warningNotes
});
}
const mutableAllowlistWarnings = collectMutableAllowlistWarnings ? await runWithCurrentPluginMetadata(state.candidate, () => collectMutableAllowlistWarnings({
cfg: state.candidate,
env: process.env
})) : [];
if (mutableAllowlistWarnings.length > 0) note(sanitizeDoctorNote(mutableAllowlistWarnings.join("\n")), "Doctor warnings");
const unknownStep = applyUnknownConfigKeyStep({
state,
shouldRepair,
doctorFixCommand
});
state = unknownStep.state;
if (unknownStep.removed.length > 0 || unknownStep.repairs.length > 0) {
const lines = [...unknownStep.removed.map((pathLocal) => `- ${pathLocal}`), ...unknownStep.repairs.map((change) => `- ${change}`)];
if (shouldRepair) changesPanelSink.emit(lines);
else note(lines.join("\n"), "Unknown config keys");
}
if (unknownStep.warnings.length > 0) note(unknownStep.warnings.join("\n"), "Doctor warnings");
const finalized = await finalizeDoctorConfigFlow({
...state,
snapshot,
shouldRepair,
confirm: params.confirm,
note
});
const cfg = finalized.cfg;
const shouldWriteConfig = finalized.shouldWriteConfig && legacyStep.blocksWrite !== true;
const includeBoundaryWrite = shouldWriteConfig && canWriteDoctorInclude(snapshot, cfg, {
persistCanonicalAgentRoster,
explicitSetPaths
}, referenceSource?.installedPluginIdRecovery);
const configuredOpencodePluginIds = [cfg.models?.providers?.opencode || cfg.models?.providers?.["opencode-zen"] ? "opencode" : void 0, cfg.models?.providers?.["opencode-go"] ? "opencode-go" : void 0].filter((pluginId) => pluginId !== void 0);
let activeOpencodePluginIds = [];
if (configuredOpencodePluginIds.length > 0) {
const { resolveEnabledProviderPluginIds } = await import("./providers-C0g6uigZ.mjs");
activeOpencodePluginIds = runWithCurrentPluginMetadata(cfg, () => resolveEnabledProviderPluginIds({
config: cfg,
onlyPluginIds: configuredOpencodePluginIds
}));
}
noteOpencodeProviderOverrides(cfg, {
opencodePluginActive: activeOpencodePluginIds.includes("opencode"),
opencodeGoPluginActive: activeOpencodePluginIds.includes("opencode-go")
});
noteImplicitFallbackClobberWarnings(cfg);
noteSandboxOriginProxyWarning(cfg);
noteMcpOriginWarning(cfg);
noteMediaCliModelWarnings(cfg);
noteMissingDefaultAgentOwner(cfg);
const migrationResult = await finalizeMigrationResult({
cfg,
shouldWriteConfig,
pluginInventoryChanged: pluginMetadataSnapshotState.inventoryChanged,
metadataSnapshot: pluginMetadataSnapshotState.current,
runWithCurrentPluginMetadata
});
const pendingChangePanels = changesPanelSink.drain();
return {
...finalized,
...shouldWriteConfig && sessionStoreOwnerRecovery.changes.length > 0 ? { confirmedConfigSource: {
path: snapshot.path,
hash: snapshot.hash ?? hashConfigRaw(snapshot.raw)
} } : {},
...referenceSource ? { referenceSource } : {},
path: snapshot.path ?? CONFIG_PATH,
shouldWriteConfig,
...configRepairWarnings.length ? { warnings: [...new Set(configRepairWarnings)] } : {},
...shouldWriteConfig && pendingChangePanels.length > 0 ? { pendingChangePanels } : {},
sourceConfigValid: snapshot.valid,
...legacyStep.partiallyValid === true ? { skipPluginValidationOnWrite: true } : {},
...shouldWriteConfig && explicitSetPaths.length > 0 ? { explicitSetPaths } : {},
...shouldWriteConfig && persistCanonicalAgentRoster ? { persistCanonicalAgentRoster: true } : {},
...includeBoundaryWrite ? { skipWizardMetadataForIncludeWrite: true } : {},
...shouldRepairCronCodexModelRefsAfterConfigWrite ? { shouldRepairCronCodexModelRefsAfterConfigWrite: true } : {},
...shouldRepair && retiredPhoneControlCleanup.cleanupPending && retiredPhoneControlCleanup.cleanupSafe ? { retiredPhoneControlStateCleanupPending: true } : {},
...blockedCodexProviderPlan.blockedModelIdentities.length > 0 ? { blockedCodexModelIdentities: blockedCodexProviderPlan.blockedModelIdentities } : {},
...openAICodexAuthProfileIdMap ? { openAICodexAuthProfileIdMap } : {},
...retiredModelRefConfig ? { retiredModelRefConfig } : {},
modelRetirementRepairRan: modelRetirementRepairRan && !legacyStep.blocksWrite && (shouldWriteConfig || snapshot.valid),
...migrationResult,
runWithPluginMetadataSnapshot,
invalidatePluginMetadataSnapshot
};
}
//#endregion
export { loadAndMaybeMigrateDoctorConfig };