diff --git a/scripts/e2e/parallels-linux-smoke.sh b/scripts/e2e/parallels-linux-smoke.sh index d56a3a96d27..0ef8110b772 100644 --- a/scripts/e2e/parallels-linux-smoke.sh +++ b/scripts/e2e/parallels-linux-smoke.sh @@ -106,7 +106,7 @@ Options: --provider Provider auth/model lane. Default: openai --model Override the model used for the agent-turn smoke. - Default: openai/gpt-5.5 for the OpenAI lane + Default: openai/gpt-5.4 for the OpenAI lane --api-key-env Host env var name for provider API key. Default: OPENAI_API_KEY for openai, ANTHROPIC_API_KEY for anthropic --openai-api-key-env Alias for --api-key-env (backward compatible) @@ -209,7 +209,7 @@ case "$PROVIDER" in openai) AUTH_CHOICE="openai-api-key" AUTH_KEY_FLAG="openai-api-key" - [[ "$MODEL_ID_EXPLICIT" -eq 1 ]] || MODEL_ID="${OPENCLAW_PARALLELS_OPENAI_MODEL:-openai/gpt-5.5}" + [[ "$MODEL_ID_EXPLICIT" -eq 1 ]] || MODEL_ID="${OPENCLAW_PARALLELS_OPENAI_MODEL:-openai/gpt-5.4}" [[ -n "$API_KEY_ENV" ]] || API_KEY_ENV="OPENAI_API_KEY" ;; anthropic) diff --git a/scripts/e2e/parallels-macos-smoke.sh b/scripts/e2e/parallels-macos-smoke.sh index 53a7f873153..ad755b3f58b 100644 --- a/scripts/e2e/parallels-macos-smoke.sh +++ b/scripts/e2e/parallels-macos-smoke.sh @@ -144,7 +144,7 @@ Options: --provider Provider auth/model lane. Default: openai --model Override the model used for the agent-turn smoke. - Default: openai/gpt-5.5 for the OpenAI lane + Default: openai/gpt-5.4 for the OpenAI lane --api-key-env Host env var name for provider API key. Default: OPENAI_API_KEY for openai, ANTHROPIC_API_KEY for anthropic --openai-api-key-env Alias for --api-key-env (backward compatible) @@ -266,7 +266,7 @@ case "$PROVIDER" in openai) AUTH_CHOICE="openai-api-key" AUTH_KEY_FLAG="openai-api-key" - [[ "$MODEL_ID_EXPLICIT" -eq 1 ]] || MODEL_ID="${OPENCLAW_PARALLELS_OPENAI_MODEL:-openai/gpt-5.5}" + [[ "$MODEL_ID_EXPLICIT" -eq 1 ]] || MODEL_ID="${OPENCLAW_PARALLELS_OPENAI_MODEL:-openai/gpt-5.4}" [[ -n "$API_KEY_ENV" ]] || API_KEY_ENV="OPENAI_API_KEY" ;; anthropic) diff --git a/scripts/e2e/parallels-npm-update-smoke.sh b/scripts/e2e/parallels-npm-update-smoke.sh index d37e8376f65..98989a8e2fe 100755 --- a/scripts/e2e/parallels-npm-update-smoke.sh +++ b/scripts/e2e/parallels-npm-update-smoke.sh @@ -122,7 +122,7 @@ Options: --provider Provider auth/model lane. Default: openai --model Override the model used for agent-turn smoke checks. - Default: openai/gpt-5.5 for the OpenAI lane + Default: openai/gpt-5.4 for the OpenAI lane --api-key-env Host env var name for provider API key. Default: OPENAI_API_KEY for openai, ANTHROPIC_API_KEY for anthropic --openai-api-key-env Alias for --api-key-env (backward compatible) @@ -214,7 +214,7 @@ case "$PROVIDER" in openai) AUTH_CHOICE="openai-api-key" AUTH_KEY_FLAG="openai-api-key" - [[ "$MODEL_ID_EXPLICIT" -eq 1 ]] || MODEL_ID="${OPENCLAW_PARALLELS_OPENAI_MODEL:-openai/gpt-5.5}" + [[ "$MODEL_ID_EXPLICIT" -eq 1 ]] || MODEL_ID="${OPENCLAW_PARALLELS_OPENAI_MODEL:-openai/gpt-5.4}" [[ -n "$API_KEY_ENV" ]] || API_KEY_ENV="OPENAI_API_KEY" ;; anthropic) diff --git a/scripts/e2e/parallels-windows-smoke.sh b/scripts/e2e/parallels-windows-smoke.sh index ca6a4bab805..ff391ee1ccd 100644 --- a/scripts/e2e/parallels-windows-smoke.sh +++ b/scripts/e2e/parallels-windows-smoke.sh @@ -140,7 +140,7 @@ Options: --provider Provider auth/model lane. Default: openai --model Override the model used for the agent-turn smoke. - Default: openai/gpt-5.5 for the OpenAI lane + Default: openai/gpt-5.4 for the OpenAI lane --api-key-env Host env var name for provider API key. Default: OPENAI_API_KEY for openai, ANTHROPIC_API_KEY for anthropic --openai-api-key-env Alias for --api-key-env (backward compatible) @@ -257,7 +257,7 @@ case "$PROVIDER" in openai) AUTH_CHOICE="openai-api-key" AUTH_KEY_FLAG="openai-api-key" - [[ "$MODEL_ID_EXPLICIT" -eq 1 ]] || MODEL_ID="${OPENCLAW_PARALLELS_OPENAI_MODEL:-openai/gpt-5.5}" + [[ "$MODEL_ID_EXPLICIT" -eq 1 ]] || MODEL_ID="${OPENCLAW_PARALLELS_OPENAI_MODEL:-openai/gpt-5.4}" [[ -n "$API_KEY_ENV" ]] || API_KEY_ENV="OPENAI_API_KEY" ;; anthropic) diff --git a/src/cli/update-cli/update-command.ts b/src/cli/update-cli/update-command.ts index 918dbaf894b..e03814943a3 100644 --- a/src/cli/update-cli/update-command.ts +++ b/src/cli/update-cli/update-command.ts @@ -94,6 +94,8 @@ const POST_CORE_UPDATE_ENV = "OPENCLAW_UPDATE_POST_CORE"; const POST_CORE_UPDATE_CHANNEL_ENV = "OPENCLAW_UPDATE_POST_CORE_CHANNEL"; const POST_CORE_UPDATE_REQUESTED_CHANNEL_ENV = "OPENCLAW_UPDATE_POST_CORE_REQUESTED_CHANNEL"; const POST_CORE_UPDATE_RESULT_PATH_ENV = "OPENCLAW_UPDATE_POST_CORE_RESULT_PATH"; +const UPDATE_PARENT_SUPPORTS_DOCTOR_CONFIG_WRITE_ENV = + "OPENCLAW_UPDATE_PARENT_SUPPORTS_DOCTOR_CONFIG_WRITE"; const SERVICE_REFRESH_PATH_ENV_KEYS = [ "OPENCLAW_HOME", "OPENCLAW_STATE_DIR", @@ -569,6 +571,7 @@ async function runPackageInstallUpdate(params: { env: { ...disableUpdatedPackageCompileCacheEnv(process.env), OPENCLAW_UPDATE_IN_PROGRESS: "1", + [UPDATE_PARENT_SUPPORTS_DOCTOR_CONFIG_WRITE_ENV]: "1", }, timeoutMs: params.timeoutMs, progress: params.progress, @@ -1030,6 +1033,7 @@ async function maybeRestartService(params: { defaultRuntime.log(theme.success("Daemon restarted successfully.")); defaultRuntime.log(""); process.env.OPENCLAW_UPDATE_IN_PROGRESS = "1"; + process.env[UPDATE_PARENT_SUPPORTS_DOCTOR_CONFIG_WRITE_ENV] = "1"; try { const interactiveDoctor = process.stdin.isTTY && !params.opts.json && params.opts.yes !== true; @@ -1040,6 +1044,7 @@ async function maybeRestartService(params: { defaultRuntime.log(theme.warn(`Doctor failed: ${String(err)}`)); } finally { delete process.env.OPENCLAW_UPDATE_IN_PROGRESS; + delete process.env[UPDATE_PARENT_SUPPORTS_DOCTOR_CONFIG_WRITE_ENV]; } } } catch (err) { diff --git a/src/flows/doctor-health-contributions.test.ts b/src/flows/doctor-health-contributions.test.ts index bf480246f1e..c076143999d 100644 --- a/src/flows/doctor-health-contributions.test.ts +++ b/src/flows/doctor-health-contributions.test.ts @@ -1,5 +1,8 @@ import { describe, expect, it } from "vitest"; -import { resolveDoctorHealthContributions } from "./doctor-health-contributions.js"; +import { + resolveDoctorHealthContributions, + shouldSkipLegacyUpdateDoctorMetadataWrite, +} from "./doctor-health-contributions.js"; describe("doctor health contributions", () => { it("repairs bundled runtime deps before channel-owned doctor paths can import runtimes", () => { @@ -20,11 +23,47 @@ describe("doctor health contributions", () => { expect(ids.indexOf("doctor:plugin-registry")).toBeGreaterThan(-1); expect(ids.indexOf("doctor:plugin-registry")).toBeLessThan(ids.indexOf("doctor:write-config")); }); - it("checks command owner configuration before final config writes", () => { const ids = resolveDoctorHealthContributions().map((entry) => entry.id); expect(ids.indexOf("doctor:command-owner")).toBeGreaterThan(-1); expect(ids.indexOf("doctor:command-owner")).toBeLessThan(ids.indexOf("doctor:write-config")); }); + + it("skips metadata-only doctor writes under legacy update parents", () => { + expect( + shouldSkipLegacyUpdateDoctorMetadataWrite({ + env: { OPENCLAW_UPDATE_IN_PROGRESS: "1" }, + before: { gateway: { mode: "local" }, meta: { lastTouchedVersion: "2026.4.26" } }, + after: { + gateway: { mode: "local" }, + meta: { lastTouchedVersion: "2026.4.27" }, + wizard: { lastRunCommand: "doctor" }, + }, + }), + ).toBe(true); + }); + + it("keeps real doctor repairs writable during update", () => { + expect( + shouldSkipLegacyUpdateDoctorMetadataWrite({ + env: { OPENCLAW_UPDATE_IN_PROGRESS: "1" }, + before: { gateway: { mode: "local" } }, + after: { gateway: { mode: "remote" } }, + }), + ).toBe(false); + }); + + it("keeps current update parents writable", () => { + expect( + shouldSkipLegacyUpdateDoctorMetadataWrite({ + env: { + OPENCLAW_UPDATE_IN_PROGRESS: "1", + OPENCLAW_UPDATE_PARENT_SUPPORTS_DOCTOR_CONFIG_WRITE: "1", + }, + before: { meta: { lastTouchedVersion: "2026.4.26" } }, + after: { meta: { lastTouchedVersion: "2026.4.27" } }, + }), + ).toBe(false); + }); }); diff --git a/src/flows/doctor-health-contributions.ts b/src/flows/doctor-health-contributions.ts index 0018fdf28be..7fce3615273 100644 --- a/src/flows/doctor-health-contributions.ts +++ b/src/flows/doctor-health-contributions.ts @@ -1,4 +1,5 @@ import fs from "node:fs"; +import { isDeepStrictEqual } from "node:util"; import type { probeGatewayMemoryStatus } from "../commands/doctor-gateway-health.js"; import type { DoctorOptions, DoctorPrompter } from "../commands/doctor-prompter.js"; import type { OpenClawConfig } from "../config/types.openclaw.js"; @@ -39,6 +40,39 @@ export function resolveDoctorMode(cfg: OpenClawConfig): DoctorFlowMode { return cfg.gateway?.mode === "remote" ? "remote" : "local"; } +const UPDATE_PARENT_SUPPORTS_DOCTOR_CONFIG_WRITE_ENV = + "OPENCLAW_UPDATE_PARENT_SUPPORTS_DOCTOR_CONFIG_WRITE"; + +function isTruthyEnvValue(value: string | undefined): boolean { + if (!value) { + return false; + } + const normalized = value.trim().toLowerCase(); + return normalized !== "" && normalized !== "0" && normalized !== "false" && normalized !== "no"; +} + +function omitDoctorWriteMetadata(cfg: OpenClawConfig): OpenClawConfig { + const { meta: _meta, wizard: _wizard, ...rest } = cfg; + return rest; +} + +export function shouldSkipLegacyUpdateDoctorMetadataWrite(params: { + env: NodeJS.ProcessEnv; + before: OpenClawConfig; + after: OpenClawConfig; +}): boolean { + if (!isTruthyEnvValue(params.env.OPENCLAW_UPDATE_IN_PROGRESS)) { + return false; + } + if (isTruthyEnvValue(params.env[UPDATE_PARENT_SUPPORTS_DOCTOR_CONFIG_WRITE_ENV])) { + return false; + } + return isDeepStrictEqual( + omitDoctorWriteMetadata(params.before), + omitDoctorWriteMetadata(params.after), + ); +} + function createDoctorHealthContribution(params: { id: string; label: string; @@ -495,6 +529,16 @@ async function runWriteConfigHealth(ctx: DoctorHealthFlowContext): Promise command: "doctor", mode: resolveDoctorMode(ctx.cfg), }); + if ( + shouldSkipLegacyUpdateDoctorMetadataWrite({ + env: ctx.env, + before: ctx.cfgForPersistence, + after: ctx.cfg, + }) + ) { + ctx.runtime.log("Skipping doctor metadata-only config write during legacy update handoff."); + return; + } await replaceConfigFile({ nextConfig: ctx.cfg, afterWrite: { mode: "auto" }, diff --git a/src/infra/update-runner.ts b/src/infra/update-runner.ts index 04cd8020924..7d30f7cae9e 100644 --- a/src/infra/update-runner.ts +++ b/src/infra/update-runner.ts @@ -161,6 +161,8 @@ const MAX_LOG_CHARS = 8000; const PREFLIGHT_MAX_COMMITS = 10; const DEFAULT_PACKAGE_NAME = "openclaw"; const CORE_PACKAGE_NAMES = new Set([DEFAULT_PACKAGE_NAME]); +const UPDATE_PARENT_SUPPORTS_DOCTOR_CONFIG_WRITE_ENV = + "OPENCLAW_UPDATE_PARENT_SUPPORTS_DOCTOR_CONFIG_WRITE"; const PREFLIGHT_TEMP_PREFIX = process.platform === "win32" ? "ocu-pf-" : "openclaw-update-preflight-"; const PREFLIGHT_WORKTREE_DIRNAME = process.platform === "win32" ? "wt" : "worktree"; @@ -1292,7 +1294,10 @@ export async function runGatewayUpdate(opts: UpdateRunnerOptions = {}): Promise< const doctorNodePath = await resolveStableNodePath(process.execPath); const doctorArgv = [doctorNodePath, doctorEntry, "doctor", "--non-interactive", "--fix"]; const doctorStep = await runStep( - step("openclaw doctor", doctorArgv, gitRoot, { OPENCLAW_UPDATE_IN_PROGRESS: "1" }), + step("openclaw doctor", doctorArgv, gitRoot, { + OPENCLAW_UPDATE_IN_PROGRESS: "1", + [UPDATE_PARENT_SUPPORTS_DOCTOR_CONFIG_WRITE_ENV]: "1", + }), ); steps.push(doctorStep); if (doctorStep.exitCode !== 0) {