Download src/cli/update-cli/update-command-fresh-doctor.ts from SaylorTwift/openclaw: direct link, hf CLI and curl.
- Browser
- Download file 13.6 kB
-
https://huggingface.co/SaylorTwift/openclaw/resolve/main/src/cli/update-cli/update-command-fresh-doctor.ts
- Command line
-
hf download hf://SaylorTwift/openclaw/src/cli/update-cli/update-command-fresh-doctor.ts
-
curl -L -o update-command-fresh-doctor.ts https://huggingface.co/SaylorTwift/openclaw/resolve/main/src/cli/update-cli/update-command-fresh-doctor.ts
13.6 kB
| // Runs post-plugin convergence checks without retaining pre-update plugin modules. | |
| import os from "node:os"; | |
| import { isRecord } from "@openclaw/normalization-core/record-coerce"; | |
| import { | |
| UPDATE_DEFER_CONFIGURED_PLUGIN_INSTALL_REPAIR_ENV, | |
| UPDATE_PARENT_SUPPORTS_DOCTOR_CONFIG_WRITE_ENV, | |
| UPDATE_POST_CORE_CONVERGENCE_ENV, | |
| } from "../../commands/doctor/shared/update-phase.js"; | |
| import { readConfigFileSnapshot } from "../../config/config.js"; | |
| import { resolveStateDir } from "../../config/paths.js"; | |
| import type { ConfigFileSnapshot } from "../../config/types.openclaw.js"; | |
| import { resolveGatewayInstallEntrypoint } from "../../daemon/gateway-entrypoint.js"; | |
| import { resolveAggregateSqliteInspectionTimeoutMs } from "../../infra/sqlite-readonly-worker.js"; | |
| import { collectStateDatabasePaths } from "../../infra/update-candidate-state.js"; | |
| import { readUpdateStateDatabaseSizes } from "../../infra/update-candidate-state.sizes.js"; | |
| import { UPDATE_RUN_ID_ENV } from "../../infra/update-control-plane-sentinel.js"; | |
| import { hasDeferredUpdateModelRetirement } from "../../infra/update-deferred-model-retirement.js"; | |
| import { | |
| consumeUpdatePostInstallDoctorResult, | |
| createUpdatePostInstallDoctorResultPath, | |
| UPDATE_POST_INSTALL_DOCTOR_ADVISORY_EXIT_CODE, | |
| UPDATE_POST_INSTALL_DOCTOR_RESULT_PATH_ENV, | |
| UpdateDoctorError, | |
| type UpdatePostInstallDoctorResult, | |
| } from "../../infra/update-doctor-result.js"; | |
| import { | |
| createUpdateFailureFact, | |
| type UpdateFailureFact, | |
| } from "../../infra/update-failure-facts.js"; | |
| import { buildUpdateDoctorEnv } from "../../infra/update-runner-doctor.js"; | |
| import { redactSupportString } from "../../logging/diagnostic-support-redaction.js"; | |
| import { formatCommandOutput } from "../../process/command-error.js"; | |
| import { isPlainCommandExitFailure, runExec } from "../../process/exec.js"; | |
| import { defaultRuntime } from "../../runtime.js"; | |
| import { truncateUtf8Prefix, truncateUtf8Suffix } from "../../utils/utf8-truncate.js"; | |
| import { resolveNodeRunner } from "./shared.js"; | |
| import type { PostCorePluginUpdateResult } from "./update-command-plugins.js"; | |
| import { applyPostPluginUpdateReadiness } from "./update-command-post-plugin-readiness.js"; | |
| import { | |
| applyPostPluginConfigValidation, | |
| POST_PLUGIN_DOCTOR_EXECUTION_FAILED_REASON, | |
| } from "./update-command-post-plugin-validation.js"; | |
| import { | |
| disableUpdatedPackageCompileCacheEnv, | |
| stripGatewayServiceMarkerEnv, | |
| } from "./update-command-service-env.js"; | |
| import { captureUpdateFinalizationDoctorOutput } from "./update-finalization-output.js"; | |
| type UpdateDoctorPhase = "pre-plugin" | "post-plugin"; | |
| export async function withPrePluginUpdateDoctorEnv<T>(run: () => Promise<T>): Promise<T> { | |
| const previousValues = [ | |
| "OPENCLAW_UPDATE_IN_PROGRESS", | |
| UPDATE_DEFER_CONFIGURED_PLUGIN_INSTALL_REPAIR_ENV, | |
| UPDATE_PARENT_SUPPORTS_DOCTOR_CONFIG_WRITE_ENV, | |
| UPDATE_POST_CORE_CONVERGENCE_ENV, | |
| ].map((key) => [key, process.env[key]] as const); | |
| process.env.OPENCLAW_UPDATE_IN_PROGRESS = "1"; | |
| process.env[UPDATE_DEFER_CONFIGURED_PLUGIN_INSTALL_REPAIR_ENV] = "1"; | |
| process.env[UPDATE_PARENT_SUPPORTS_DOCTOR_CONFIG_WRITE_ENV] = "1"; | |
| delete process.env[UPDATE_POST_CORE_CONVERGENCE_ENV]; | |
| try { | |
| return await run(); | |
| } finally { | |
| for (const [key, value] of previousValues) { | |
| if (value === undefined) { | |
| delete process.env[key]; | |
| } else { | |
| process.env[key] = value; | |
| } | |
| } | |
| } | |
| } | |
| async function withNormalConfigValidation<T>(run: () => Promise<T>): Promise<T> { | |
| const previousUpdateInProgress = process.env.OPENCLAW_UPDATE_IN_PROGRESS; | |
| process.env.OPENCLAW_UPDATE_IN_PROGRESS = "0"; | |
| try { | |
| return await run(); | |
| } finally { | |
| if (previousUpdateInProgress === undefined) { | |
| delete process.env.OPENCLAW_UPDATE_IN_PROGRESS; | |
| } else { | |
| process.env.OPENCLAW_UPDATE_IN_PROGRESS = previousUpdateInProgress; | |
| } | |
| } | |
| } | |
| function createPostPluginDoctorExecutionFailure( | |
| pluginUpdate: PostCorePluginUpdateResult, | |
| reason: string, | |
| failureFacts?: UpdateFailureFact[], | |
| ): PostCorePluginUpdateResult { | |
| return { | |
| ...pluginUpdate, | |
| status: "error", | |
| reason: POST_PLUGIN_DOCTOR_EXECUTION_FAILED_REASON, | |
| ...(failureFacts?.length ? { failureFacts } : {}), | |
| warnings: [ | |
| ...(pluginUpdate.warnings ?? []), | |
| { | |
| reason, | |
| message: "Updated plugin migrations could not be run in a fresh process.", | |
| guidance: ["Run `openclaw update repair` to retry post-update plugin repair."], | |
| }, | |
| ], | |
| }; | |
| } | |
| export async function runUpdateFinalizationDoctorInFreshProcess(params: { | |
| phase: UpdateDoctorPhase; | |
| root: string; | |
| runId?: string; | |
| yes: boolean; | |
| json: boolean; | |
| workspaceSuggestions?: boolean; | |
| timeoutMs?: number; | |
| nodeRunner?: string; | |
| entryPath?: string; | |
| onWarnings?: (warnings: string[]) => void; | |
| }): Promise<void> { | |
| const entryPath = params.entryPath ?? (await resolveGatewayInstallEntrypoint(params.root)); | |
| if (!entryPath) { | |
| throw new Error("Updated OpenClaw entrypoint not found for post-plugin doctor"); | |
| } | |
| const args = [ | |
| entryPath, | |
| "doctor", | |
| "--repair", | |
| "--non-interactive", | |
| ...(params.workspaceSuggestions ? [] : ["--no-workspace-suggestions"]), | |
| ...(params.yes ? ["--yes"] : []), | |
| ]; | |
| const baseEnv = stripGatewayServiceMarkerEnv(disableUpdatedPackageCompileCacheEnv(process.env)); | |
| delete baseEnv[UPDATE_POST_CORE_CONVERGENCE_ENV]; | |
| const doctorResultPath = createUpdatePostInstallDoctorResultPath(); | |
| let doctorResult: UpdatePostInstallDoctorResult | null = null; | |
| let result: { stdout?: unknown; stderr?: unknown } | undefined; | |
| try { | |
| result = await runExec(params.nodeRunner ?? resolveNodeRunner(), args, { | |
| cwd: params.root, | |
| timeoutMs: params.timeoutMs, | |
| maxBuffer: 4 * 1024 * 1024, | |
| logOutput: false, | |
| onOutputChunk: captureUpdateFinalizationDoctorOutput(params.phase), | |
| baseEnv, | |
| env: { | |
| [UPDATE_POST_INSTALL_DOCTOR_RESULT_PATH_ENV]: doctorResultPath, | |
| ...(params.runId ? { [UPDATE_RUN_ID_ENV]: params.runId } : {}), | |
| // The outer updater owns service refresh and activation after every | |
| // migration finishes; a fresh Doctor must not resume its parked service. | |
| ...buildUpdateDoctorEnv({ | |
| allowGatewayServiceRepair: false, | |
| allowGatewayActivation: false, | |
| deferConfiguredPluginInstallRepair: true, | |
| }), | |
| ...(params.phase === "post-plugin" ? { [UPDATE_POST_CORE_CONVERGENCE_ENV]: "1" } : {}), | |
| }, | |
| }); | |
| } catch (error) { | |
| doctorResult = await consumeUpdatePostInstallDoctorResult(doctorResultPath); | |
| if (isRecord(error)) { | |
| result = error; | |
| // Enabling the existing result channel gives deferred plugin repair its | |
| // advisory exit code. Convergence below still owns that repair. | |
| if ( | |
| error.exitCode === UPDATE_POST_INSTALL_DOCTOR_ADVISORY_EXIT_CODE && | |
| isPlainCommandExitFailure({ ...error, failed: error.failed === true }) && | |
| doctorResult?.status === "advisory" | |
| ) { | |
| return; | |
| } | |
| } | |
| const exitCode = isRecord(error) && typeof error.exitCode === "number" ? error.exitCode : null; | |
| const redaction = { env: process.env, stateDir: resolveStateDir() }; | |
| const failureFacts = doctorResult?.failureFacts?.length | |
| ? doctorResult.failureFacts | |
| : [ | |
| createUpdateFailureFact({ | |
| check: "doctor", | |
| code: "doctor-failed", | |
| message: | |
| typeof result?.stderr === "string" && result.stderr.trim() | |
| ? result.stderr | |
| : error instanceof Error | |
| ? error.message | |
| : String(error), | |
| }), | |
| ]; | |
| const details = (["stderr", "stdout"] as const).flatMap((stream) => { | |
| const output = result?.[stream]; | |
| if (typeof output !== "string" || !output.trim()) { | |
| return []; | |
| } | |
| // Execa's message starts with full argv. Keep both actual diagnostics before | |
| // the bounded update handoff, without cutting a credential before redaction. | |
| const redacted = redactSupportString(output, redaction, { | |
| maxLength: Number.MAX_SAFE_INTEGER, | |
| }); | |
| const formatted = formatCommandOutput(redacted, 384); | |
| let excerpt = formatted; | |
| if (Buffer.byteLength(redacted) > 384 || Buffer.byteLength(formatted) > 384) { | |
| const beginning = formatCommandOutput(truncateUtf8Prefix(redacted, 256), 256); | |
| excerpt = `${truncateUtf8Prefix(beginning, 256)}\n...\n${truncateUtf8Suffix(formatted, 123)}`; | |
| } | |
| return excerpt ? [`${stream}: ${excerpt}`] : []; | |
| }); | |
| if (details.length > 0) { | |
| throw new UpdateDoctorError( | |
| `Updated ${params.phase} Doctor failed:\n${details.join("\n")}`, | |
| failureFacts, | |
| { cause: error, exitCode }, | |
| ); | |
| } | |
| throw new UpdateDoctorError( | |
| error instanceof Error ? error.message : String(error), | |
| failureFacts, | |
| { cause: error, exitCode }, | |
| ); | |
| } finally { | |
| doctorResult ??= await consumeUpdatePostInstallDoctorResult(doctorResultPath); | |
| if (doctorResult?.warnings?.length) { | |
| params.onWarnings?.(doctorResult.warnings); | |
| } | |
| // Clack writes directly to the child's stdout. Preserve diagnostics on either | |
| // exit path without letting them share the parent's JSON result stream. | |
| if (typeof result?.stdout === "string" && result.stdout.trim()) { | |
| defaultRuntime[params.json ? "error" : "log"](result.stdout.trimEnd()); | |
| } | |
| if (typeof result?.stderr === "string" && result.stderr.trim()) { | |
| defaultRuntime.error(result.stderr.trimEnd()); | |
| } | |
| } | |
| } | |
| async function validatePostPluginConfigInFreshProcess(params: { | |
| root: string; | |
| timeoutMs: number; | |
| entryPath: string; | |
| nodeRunner?: string; | |
| }): Promise<boolean> { | |
| try { | |
| await runExec( | |
| params.nodeRunner ?? resolveNodeRunner(), | |
| [params.entryPath, "config", "validate", "--json"], | |
| { | |
| cwd: params.root, | |
| timeoutMs: params.timeoutMs, | |
| maxBuffer: 4 * 1024 * 1024, | |
| logOutput: false, | |
| baseEnv: stripGatewayServiceMarkerEnv(disableUpdatedPackageCompileCacheEnv(process.env)), | |
| env: { OPENCLAW_UPDATE_IN_PROGRESS: "0" }, | |
| }, | |
| ); | |
| return true; | |
| } catch { | |
| return false; | |
| } | |
| } | |
| export async function completePostCorePluginUpdate(params: { | |
| root: string; | |
| runId?: string; | |
| pluginUpdate: PostCorePluginUpdateResult; | |
| freshDoctorRequired: boolean; | |
| yes: boolean; | |
| json: boolean; | |
| timeoutMs?: number; | |
| nodeRunner?: string; | |
| beforeDoctor?: () => Promise<void>; | |
| onWarnings?: (warnings: string[]) => void; | |
| }): Promise<{ | |
| pluginUpdate: PostCorePluginUpdateResult; | |
| configSnapshot: ConfigFileSnapshot; | |
| }> { | |
| let pluginUpdate = params.pluginUpdate; | |
| let entryPath: string | undefined; | |
| let freshConfigValid: boolean | undefined; | |
| if (pluginUpdate.status !== "error") { | |
| try { | |
| entryPath = await resolveGatewayInstallEntrypoint(params.root); | |
| if (!entryPath) { | |
| throw new Error("Updated OpenClaw entrypoint not found for post-plugin doctor"); | |
| } | |
| if (params.freshDoctorRequired || hasDeferredUpdateModelRetirement()) { | |
| await params.beforeDoctor?.(); | |
| await runUpdateFinalizationDoctorInFreshProcess({ | |
| ...params, | |
| entryPath, | |
| phase: "post-plugin", | |
| }); | |
| } | |
| } catch (err) { | |
| pluginUpdate = createPostPluginDoctorExecutionFailure( | |
| params.pluginUpdate, | |
| String(err), | |
| err instanceof UpdateDoctorError ? err.failureFacts : undefined, | |
| ); | |
| freshConfigValid = false; | |
| } | |
| } | |
| // Only the target runtime may write state after a version switch: observing | |
| // config here could migrate its database back to the parent's newer schema. | |
| const configSnapshot = await withNormalConfigValidation(() => | |
| readConfigFileSnapshot({ observe: false }), | |
| ); | |
| if (entryPath) { | |
| let checkTimeoutMs = params.timeoutMs; | |
| if (checkTimeoutMs === undefined) { | |
| // Doctor can grow shared and agent stores. Measure once after its writes settle. | |
| const env = { ...process.env }; | |
| const databases = await collectStateDatabasePaths( | |
| { stateDir: resolveStateDir(env), config: configSnapshot.sourceConfig, env }, | |
| { includeUnconfiguredAgents: false }, | |
| ); | |
| checkTimeoutMs = resolveAggregateSqliteInspectionTimeoutMs( | |
| "post-plugin checks", | |
| await readUpdateStateDatabaseSizes( | |
| Array.from(databases.values(), (database) => database.spellings[0]), | |
| { nodeRunner: process.execPath, sourceEnv: env, stagingRoot: os.tmpdir() }, | |
| ), | |
| ); | |
| } | |
| // No authored file is a valid unconfigured install, not an invalid config. | |
| // Existing files still need the target schema; every install needs readiness. | |
| freshConfigValid = | |
| (!configSnapshot.exists && configSnapshot.valid) || | |
| (await validatePostPluginConfigInFreshProcess({ | |
| ...params, | |
| entryPath, | |
| timeoutMs: checkTimeoutMs, | |
| })); | |
| if (freshConfigValid) { | |
| pluginUpdate = await applyPostPluginUpdateReadiness({ | |
| root: params.root, | |
| entryPath, | |
| pluginUpdate, | |
| timeoutMs: checkTimeoutMs, | |
| ...(params.nodeRunner ? { nodeRunner: params.nodeRunner } : {}), | |
| }); | |
| } | |
| } | |
| // Strict validity belongs to the target runtime even when no plugin changed. | |
| // The parent may retain the previous schema; its snapshot is best-effort context. | |
| pluginUpdate = applyPostPluginConfigValidation( | |
| pluginUpdate, | |
| freshConfigValid ?? configSnapshot.valid, | |
| ); | |
| return { pluginUpdate, configSnapshot }; | |
| } | |