mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-18 15:24:46 +00:00
fix(update): use post-doctor plugin records
This commit is contained in:
@@ -31,6 +31,7 @@ Docs: https://docs.openclaw.ai
|
||||
- Agents/OpenAI streams: yield via `setTimeout(0)` instead of `setImmediate` between bursty Responses chunks so abort timers can fire during the yield, keeping cancel-on-timeout responsive on hot streams. Refs #82462.
|
||||
- CLI/config: send SecretRef diagnostics to stderr so JSON command stdout remains parseable.
|
||||
- CLI/plugins: ship the bundled memory CLI as a package entry so package-installed `openclaw memory` commands register correctly.
|
||||
- CLI/update: defer doctor-time plugin package installs during package swaps and seed post-core repair from the updated install registry, preventing duplicate reinstall failures.
|
||||
- Feishu: detect SecretRef top-level credentials as a configured default account instead of treating object-backed app secrets as missing.
|
||||
- CLI/completion: resolve concrete PowerShell profile paths and reload commands during setup and doctor completion installation. Fixes #44296. (#83059) Thanks @yu-xin-c.
|
||||
- Providers/Google: preserve and recover Gemini 3 tool-call thought signatures during native replay so function-calling turns no longer fail with missing `thought_signature` 400s. Fixes #72879. (#80358) Thanks @abnershang.
|
||||
|
||||
@@ -1176,6 +1176,44 @@ describe("update-cli", () => {
|
||||
expect(updateCall?.skipIds?.has("demo")).toBe(true);
|
||||
});
|
||||
|
||||
it("post-core resume mode prefers post-doctor disk install records over the stale parent snapshot", async () => {
|
||||
const resultDir = createCaseDir("openclaw-post-core-disk-records");
|
||||
const recordsPath = path.join(resultDir, "plugin-install-records.json");
|
||||
await fs.mkdir(resultDir, { recursive: true });
|
||||
await fs.writeFile(
|
||||
recordsPath,
|
||||
`${JSON.stringify({
|
||||
stale: {
|
||||
source: "npm",
|
||||
spec: "@openclaw/stale@1.0.0",
|
||||
installPath: "/tmp/stale-plugin",
|
||||
},
|
||||
})}\n`,
|
||||
"utf-8",
|
||||
);
|
||||
const postDoctorRecords = {
|
||||
codex: {
|
||||
source: "npm",
|
||||
spec: "@openclaw/codex@2026.5.17",
|
||||
installPath: "/tmp/codex-plugin",
|
||||
},
|
||||
} satisfies Record<string, PluginInstallRecord>;
|
||||
loadInstalledPluginIndexInstallRecords.mockResolvedValueOnce(postDoctorRecords);
|
||||
|
||||
await withEnvAsync(
|
||||
{
|
||||
OPENCLAW_UPDATE_POST_CORE: "1",
|
||||
OPENCLAW_UPDATE_POST_CORE_CHANNEL: "stable",
|
||||
OPENCLAW_UPDATE_POST_CORE_INSTALL_RECORDS_PATH: recordsPath,
|
||||
},
|
||||
async () => {
|
||||
await updateCommand({ json: true, restart: false });
|
||||
},
|
||||
);
|
||||
|
||||
expect(syncPluginCall()?.config?.plugins?.installs).toEqual(postDoctorRecords);
|
||||
});
|
||||
|
||||
it("post-core resume mode persists the requested update channel with the updated process", async () => {
|
||||
vi.mocked(readConfigFileSnapshot).mockResolvedValue({
|
||||
...baseSnapshot,
|
||||
|
||||
@@ -9,7 +9,10 @@ import {
|
||||
ensureCompletionCacheExists,
|
||||
} from "../../commands/doctor-completion.js";
|
||||
import { doctorCommand } from "../../commands/doctor.js";
|
||||
import { UPDATE_DEFER_CONFIGURED_PLUGIN_INSTALL_REPAIR_ENV } from "../../commands/doctor/shared/update-phase.js";
|
||||
import {
|
||||
UPDATE_DEFER_CONFIGURED_PLUGIN_INSTALL_REPAIR_ENV,
|
||||
UPDATE_PARENT_SUPPORTS_DOCTOR_CONFIG_WRITE_ENV,
|
||||
} from "../../commands/doctor/shared/update-phase.js";
|
||||
import { createPreUpdateConfigSnapshot } from "../../config/backup-rotation.js";
|
||||
import {
|
||||
assertConfigWriteAllowedInCurrentMode,
|
||||
@@ -144,8 +147,6 @@ const POST_CORE_UPDATE_SOURCE_CONFIG_PATH_ENV = "OPENCLAW_UPDATE_POST_CORE_SOURC
|
||||
const POST_CORE_UPDATE_STARTED_AT_ENV = "OPENCLAW_UPDATE_POST_CORE_STARTED_AT_MS";
|
||||
const POST_CORE_UPDATE_RESULT_POLL_MS = 100;
|
||||
const PRE_UPDATE_CONFIG_SNAPSHOT_MAX_AGE_MS = 6 * 60 * 60 * 1000;
|
||||
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",
|
||||
@@ -2733,6 +2734,14 @@ export async function updateCommand(opts: UpdateCommandOptions): Promise<void> {
|
||||
postCoreConfigSnapshot,
|
||||
preUpdateSourceConfig,
|
||||
);
|
||||
const parentPluginInstallRecords =
|
||||
await readPostCorePluginInstallRecordsFile(postCoreInstallRecordsPath);
|
||||
// The updated doctor may have repaired plugin installs before this fresh process resumed.
|
||||
const currentPluginInstallRecords = await loadInstalledPluginIndexInstallRecords();
|
||||
const pluginInstallRecords =
|
||||
Object.keys(currentPluginInstallRecords).length > 0
|
||||
? currentPluginInstallRecords
|
||||
: parentPluginInstallRecords;
|
||||
|
||||
const pluginUpdate = await runPostCorePluginUpdate({
|
||||
root,
|
||||
@@ -2742,7 +2751,7 @@ export async function updateCommand(opts: UpdateCommandOptions): Promise<void> {
|
||||
restoredAuthoredChannels: restoredPostCoreConfig.authoredChannels,
|
||||
opts,
|
||||
timeoutMs: updateStepTimeoutMs,
|
||||
pluginInstallRecords: await readPostCorePluginInstallRecordsFile(postCoreInstallRecordsPath),
|
||||
pluginInstallRecords,
|
||||
});
|
||||
if (process.env[POST_CORE_UPDATE_RESULT_PATH_ENV]) {
|
||||
await writePostCorePluginUpdateResultFile(
|
||||
|
||||
@@ -374,6 +374,42 @@ describe("configured plugin install release step", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("defers package-manager plugin repair when an older updater supports post-doctor config writes", async () => {
|
||||
mocks.repairMissingPluginInstallsForIds.mockResolvedValue({
|
||||
changes: [],
|
||||
warnings: [],
|
||||
});
|
||||
|
||||
const { maybeRunConfiguredPluginInstallReleaseStep } =
|
||||
await import("./release-configured-plugin-installs.js");
|
||||
const result = await maybeRunConfiguredPluginInstallReleaseStep({
|
||||
cfg: {
|
||||
plugins: {
|
||||
entries: {
|
||||
discord: { enabled: true },
|
||||
},
|
||||
},
|
||||
},
|
||||
currentVersion: "2026.5.2-beta.1",
|
||||
touchedVersion: "2026.5.1",
|
||||
env: {
|
||||
OPENCLAW_UPDATE_IN_PROGRESS: "1",
|
||||
OPENCLAW_UPDATE_PARENT_SUPPORTS_DOCTOR_CONFIG_WRITE: "1",
|
||||
},
|
||||
});
|
||||
|
||||
expect(readOnlyMissingPluginInstallRepairCall().env).toEqual({
|
||||
OPENCLAW_UPDATE_IN_PROGRESS: "1",
|
||||
OPENCLAW_UPDATE_PARENT_SUPPORTS_DOCTOR_CONFIG_WRITE: "1",
|
||||
});
|
||||
expect(result).toEqual({
|
||||
changes: [],
|
||||
warnings: [],
|
||||
completed: false,
|
||||
touchedConfig: false,
|
||||
});
|
||||
});
|
||||
|
||||
it("repairs missing configured installs even when a prior update doctor touched config", async () => {
|
||||
mocks.repairMissingPluginInstallsForIds.mockResolvedValue({
|
||||
changes: ['Installed missing configured plugin "discord".'],
|
||||
|
||||
@@ -2,6 +2,7 @@ import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
UPDATE_DEFER_CONFIGURED_PLUGIN_INSTALL_REPAIR_ENV,
|
||||
UPDATE_IN_PROGRESS_ENV,
|
||||
UPDATE_PARENT_SUPPORTS_DOCTOR_CONFIG_WRITE_ENV,
|
||||
UPDATE_POST_CORE_CONVERGENCE_ENV,
|
||||
isLegacyPackageUpdateDoctorPass,
|
||||
isPostCoreConvergencePass,
|
||||
@@ -46,6 +47,12 @@ describe("update-phase env helpers", () => {
|
||||
[UPDATE_DEFER_CONFIGURED_PLUGIN_INSTALL_REPAIR_ENV]: "1",
|
||||
}),
|
||||
).toBe(true);
|
||||
expect(
|
||||
shouldDeferConfiguredPluginInstallRepair({
|
||||
[UPDATE_IN_PROGRESS_ENV]: "1",
|
||||
[UPDATE_PARENT_SUPPORTS_DOCTOR_CONFIG_WRITE_ENV]: "1",
|
||||
}),
|
||||
).toBe(true);
|
||||
expect(
|
||||
shouldDeferConfiguredPluginInstallRepair({
|
||||
[UPDATE_IN_PROGRESS_ENV]: "1",
|
||||
@@ -67,6 +74,12 @@ describe("update-phase env helpers", () => {
|
||||
[UPDATE_DEFER_CONFIGURED_PLUGIN_INSTALL_REPAIR_ENV]: "1",
|
||||
}),
|
||||
).toBe(false);
|
||||
expect(
|
||||
isLegacyPackageUpdateDoctorPass({
|
||||
[UPDATE_IN_PROGRESS_ENV]: "1",
|
||||
[UPDATE_PARENT_SUPPORTS_DOCTOR_CONFIG_WRITE_ENV]: "1",
|
||||
}),
|
||||
).toBe(false);
|
||||
expect(
|
||||
isLegacyPackageUpdateDoctorPass({
|
||||
[UPDATE_IN_PROGRESS_ENV]: "1",
|
||||
|
||||
@@ -4,6 +4,8 @@ export const UPDATE_IN_PROGRESS_ENV = "OPENCLAW_UPDATE_IN_PROGRESS";
|
||||
export const UPDATE_POST_CORE_CONVERGENCE_ENV = "OPENCLAW_UPDATE_POST_CORE_CONVERGENCE";
|
||||
export const UPDATE_DEFER_CONFIGURED_PLUGIN_INSTALL_REPAIR_ENV =
|
||||
"OPENCLAW_UPDATE_DEFER_CONFIGURED_PLUGIN_INSTALL_REPAIR";
|
||||
export const UPDATE_PARENT_SUPPORTS_DOCTOR_CONFIG_WRITE_ENV =
|
||||
"OPENCLAW_UPDATE_PARENT_SUPPORTS_DOCTOR_CONFIG_WRITE";
|
||||
|
||||
/**
|
||||
* True iff the caller is the doctor pass that runs WHILE the core package
|
||||
@@ -45,7 +47,8 @@ export function isUpdatePackageSwapInProgress(env: NodeJS.ProcessEnv): boolean {
|
||||
export function shouldDeferConfiguredPluginInstallRepair(env: NodeJS.ProcessEnv): boolean {
|
||||
return (
|
||||
isUpdatePackageSwapInProgress(env) &&
|
||||
isTruthyEnvValue(env[UPDATE_DEFER_CONFIGURED_PLUGIN_INSTALL_REPAIR_ENV])
|
||||
(isTruthyEnvValue(env[UPDATE_DEFER_CONFIGURED_PLUGIN_INSTALL_REPAIR_ENV]) ||
|
||||
isTruthyEnvValue(env[UPDATE_PARENT_SUPPORTS_DOCTOR_CONFIG_WRITE_ENV]))
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user