Skip to content

Commit c425801

Browse files
committed
fix(update): narrow corrupt plugin warnings
1 parent 18eeeba commit c425801

6 files changed

Lines changed: 78 additions & 80 deletions

File tree

docs/cli/update.md

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -38,8 +38,9 @@ openclaw --update
3838
- `--tag <dist-tag|version|spec>`: override the package target for this update only. For package installs, `main` maps to `github:openclaw/openclaw#main`.
3939
- `--dry-run`: preview planned update actions (channel/tag/target/restart flow) without writing config, installing, syncing plugins, or restarting.
4040
- `--json`: print machine-readable `UpdateRunResult` JSON, including
41-
`postUpdate.plugins.integrityDrifts` when npm plugin artifact drift is
42-
detected during post-update plugin sync.
41+
`postUpdate.plugins.warnings` when corrupt or unloadable managed plugins need
42+
repair after the core update succeeds, and `postUpdate.plugins.integrityDrifts`
43+
when npm plugin artifact drift is detected during post-update plugin sync.
4344
- `--timeout <seconds>`: per-step timeout (default is 1800s).
4445
- `--yes`: skip confirmation prompts (for example downgrade confirmation).
4546

@@ -177,7 +178,7 @@ If an exact pinned npm plugin update resolves to an artifact whose integrity dif
177178
</Warning>
178179

179180
<Note>
180-
Post-update plugin sync failures fail the update result and stop restart follow-up work. Fix the plugin install or update error, then rerun `openclaw update`.
181+
Post-update plugin sync failures that are scoped to a managed plugin are reported as warnings after the core update succeeds. The JSON result keeps the top-level update `status: "ok"` and reports `postUpdate.plugins.status: "warning"` with `openclaw doctor --fix` and `openclaw plugins inspect <id> --runtime --json` guidance. Unexpected updater or sync exceptions still fail the update result. Fix the plugin install or update error, then rerun `openclaw doctor --fix` or `openclaw update`.
181182

182183
When the updated Gateway starts, plugin loading is verify-only: startup does not run package managers or mutate dependency trees. Package-manager `update.run` restarts bypass the normal idle deferral and restart cooldown after the package tree has been swapped, so the old process cannot keep lazy-loading removed chunks.
183184

docs/help/testing-updates-plugins.md

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -172,7 +172,7 @@ targets the shipped npm package instead.
172172
Release checks call Package Acceptance with the package/update/restart/plugin set:
173173

174174
```text
175-
doctor-switch update-channel-switch upgrade-survivor published-upgrade-survivor update-restart-auth plugins-offline plugin-update
175+
doctor-switch update-channel-switch update-corrupt-plugin upgrade-survivor published-upgrade-survivor update-restart-auth plugins-offline plugin-update
176176
```
177177

178178
When release soak is enabled, they also pass:
@@ -183,10 +183,10 @@ published_upgrade_survivor_scenarios=reported-issues
183183
telegram_mode=mock-openai
184184
```
185185

186-
This keeps package migration, update channel switching, stale plugin dependency
187-
cleanup, offline plugin coverage, plugin update behavior, and Telegram package
188-
QA on the same resolved artifact without making the default release package gate
189-
walk every published release.
186+
This keeps package migration, update channel switching, corrupt managed-plugin
187+
tolerance, stale plugin dependency cleanup, offline plugin coverage, plugin
188+
update behavior, and Telegram package QA on the same resolved artifact without
189+
making the default release package gate walk every published release.
190190

191191
`last-stable-4` resolves to the four latest stable npm-published OpenClaw
192192
releases. Release package acceptance pins `2026.4.23` as the first plugin-update

scripts/e2e/lib/plugin-update/probe.mjs

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -121,16 +121,20 @@ function assertCorruptUpdate(updateJsonPath, pluginId) {
121121
if (!plugins) {
122122
throw new Error(`missing postUpdate.plugins in update output: ${JSON.stringify(payload)}`);
123123
}
124-
if (plugins.status !== "ok") {
125-
throw new Error(`expected post-update plugin status ok, got ${JSON.stringify(plugins.status)}`);
124+
if (plugins.status !== "warning") {
125+
throw new Error(
126+
`expected post-update plugin status warning, got ${JSON.stringify(plugins.status)}`,
127+
);
126128
}
127129
assertCorruptPluginDetails(plugins, pluginId);
128130
}
129131

130132
function assertCorruptPluginResult(pluginJsonPath, pluginId) {
131133
const plugins = readJson(pluginJsonPath);
132-
if (plugins.status !== "ok") {
133-
throw new Error(`expected post-update plugin status ok, got ${JSON.stringify(plugins.status)}`);
134+
if (plugins.status !== "warning") {
135+
throw new Error(
136+
`expected post-update plugin status warning, got ${JSON.stringify(plugins.status)}`,
137+
);
134138
}
135139
assertCorruptPluginDetails(plugins, pluginId);
136140
}

src/cli/update-cli.test.ts

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1039,7 +1039,7 @@ describe("update-cli", () => {
10391039
action: "aborted",
10401040
},
10411041
]);
1042-
expect(jsonOutput?.postUpdate?.plugins?.status).toBe("ok");
1042+
expect(jsonOutput?.postUpdate?.plugins?.status).toBe("warning");
10431043
expect(jsonOutput?.postUpdate?.plugins?.warnings?.[0]).toMatchObject({
10441044
pluginId: "demo",
10451045
guidance: [
@@ -1107,7 +1107,7 @@ describe("update-cli", () => {
11071107
| UpdateRunResult
11081108
| undefined;
11091109
expect(jsonOutput?.status).toBe("ok");
1110-
expect(jsonOutput?.postUpdate?.plugins?.status).toBe("ok");
1110+
expect(jsonOutput?.postUpdate?.plugins?.status).toBe("warning");
11111111
expect(jsonOutput?.postUpdate?.plugins?.warnings?.[0]).toMatchObject({
11121112
pluginId: "demo",
11131113
});
@@ -1154,6 +1154,22 @@ describe("update-cli", () => {
11541154
expect(logs).toContain("Run openclaw plugins inspect demo --runtime --json for details.");
11551155
});
11561156

1157+
it("fails unexpected post-core plugin sync exceptions", async () => {
1158+
syncPluginsForUpdateChannel.mockRejectedValueOnce(new Error("plugin sync invariant broke"));
1159+
1160+
await expect(updateCommand({ json: true, restart: false })).rejects.toThrow(
1161+
"plugin sync invariant broke",
1162+
);
1163+
});
1164+
1165+
it("fails unexpected post-core npm update exceptions", async () => {
1166+
updateNpmInstalledPlugins.mockRejectedValueOnce(new Error("npm update invariant broke"));
1167+
1168+
await expect(updateCommand({ json: true, restart: false })).rejects.toThrow(
1169+
"npm update invariant broke",
1170+
);
1171+
});
1172+
11571173
it("preserves fresh-process plugin warning details in parent json output", async () => {
11581174
setupUpdatedRootRefresh();
11591175
spawn.mockImplementationOnce((_node, _argv, options) => {
@@ -1167,7 +1183,7 @@ describe("update-cli", () => {
11671183
await fs.writeFile(
11681184
resultPath,
11691185
JSON.stringify({
1170-
status: "ok",
1186+
status: "warning",
11711187
changed: false,
11721188
warnings: [
11731189
{

src/cli/update-cli/update-command.ts

Lines changed: 41 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -266,21 +266,23 @@ function createPostUpdatePluginWarning(params: {
266266
};
267267
}
268268

269-
function addPostUpdatePluginGuidance(outcome: PluginUpdateOutcome): PluginUpdateOutcome {
269+
function createGuidedPostUpdatePluginOutcome(outcome: PluginUpdateOutcome): {
270+
outcome: PluginUpdateOutcome;
271+
warning?: PostUpdatePluginWarning;
272+
} {
270273
if (outcome.status !== "error") {
271-
return outcome;
272-
}
273-
const guidance = [
274-
POST_UPDATE_PLUGIN_REPAIR_GUIDANCE,
275-
formatPostUpdatePluginInspectGuidance(outcome.pluginId),
276-
];
277-
const missing = guidance.filter((entry) => !outcome.message.includes(entry));
278-
if (missing.length === 0) {
279-
return outcome;
274+
return { outcome };
280275
}
276+
const warning = createPostUpdatePluginWarning({
277+
...(outcome.pluginId && outcome.pluginId !== "unknown" ? { pluginId: outcome.pluginId } : {}),
278+
reason: outcome.message,
279+
});
281280
return {
282-
...outcome,
283-
message: `${outcome.message} ${missing.join(" ")}`,
281+
outcome: {
282+
...outcome,
283+
message: warning.message,
284+
},
285+
warning,
284286
};
285287
}
286288

@@ -1125,25 +1127,6 @@ async function updatePluginsAfterCoreUpdate(params: {
11251127
workspaceDir: params.root,
11261128
}),
11271129
logger: pluginLogger,
1128-
}).catch((error: unknown) => {
1129-
const warning = createPostUpdatePluginWarning({
1130-
reason: error instanceof Error ? error.message : String(error),
1131-
});
1132-
warnings.push(warning);
1133-
if (!params.opts.json) {
1134-
defaultRuntime.log(theme.warn(warning.message));
1135-
}
1136-
return {
1137-
config: syncConfig,
1138-
changed: false,
1139-
summary: {
1140-
switchedToBundled: [],
1141-
switchedToClawHub: [],
1142-
switchedToNpm: [],
1143-
warnings: [warning.message],
1144-
errors: [],
1145-
},
1146-
};
11471130
});
11481131
for (const error of syncResult.summary.errors) {
11491132
warnings.push(createPostUpdatePluginWarning({ reason: error }));
@@ -1218,41 +1201,16 @@ async function updatePluginsAfterCoreUpdate(params: {
12181201
skipDisabledPlugins: true,
12191202
logger: pluginLogger,
12201203
onIntegrityDrift: onPluginIntegrityDrift,
1221-
}).catch((error: unknown) => {
1222-
const warning = createPostUpdatePluginWarning({
1223-
reason: error instanceof Error ? error.message : String(error),
1224-
});
1225-
warnings.push(warning);
1226-
if (!params.opts.json) {
1227-
defaultRuntime.log(theme.warn(warning.message));
1228-
}
1229-
return {
1230-
config: pluginConfig,
1231-
changed: false,
1232-
outcomes: [
1233-
{
1234-
pluginId: "unknown",
1235-
status: "error" as const,
1236-
message: warning.message,
1237-
},
1238-
],
1239-
};
12401204
});
12411205
pluginConfig = npmResult.config;
12421206
pluginsChanged ||= npmResult.changed;
12431207
npmPluginsChanged ||= npmResult.changed;
1244-
const guidedNpmOutcomes = npmResult.outcomes.map(addPostUpdatePluginGuidance);
1245-
pluginUpdateOutcomes.push(...guidedNpmOutcomes);
1246-
for (const outcome of guidedNpmOutcomes) {
1247-
if (outcome.status !== "error") {
1248-
continue;
1208+
for (const rawOutcome of npmResult.outcomes) {
1209+
const guided = createGuidedPostUpdatePluginOutcome(rawOutcome);
1210+
pluginUpdateOutcomes.push(guided.outcome);
1211+
if (guided.warning) {
1212+
warnings.push(guided.warning);
12491213
}
1250-
warnings.push(
1251-
createPostUpdatePluginWarning({
1252-
pluginId: outcome.pluginId,
1253-
reason: outcome.message,
1254-
}),
1255-
);
12561214
}
12571215

12581216
const remainingMissingPayloads = await collectMissingPluginInstallPayloads({
@@ -1297,7 +1255,7 @@ async function updatePluginsAfterCoreUpdate(params: {
12971255

12981256
if (params.opts.json) {
12991257
return {
1300-
status: "ok",
1258+
status: warnings.length > 0 ? "warning" : "ok",
13011259
changed: pluginsChanged,
13021260
warnings,
13031261
sync: {
@@ -1367,7 +1325,7 @@ async function updatePluginsAfterCoreUpdate(params: {
13671325
}
13681326

13691327
return {
1370-
status: "ok",
1328+
status: warnings.length > 0 ? "warning" : "ok",
13711329
changed: pluginsChanged,
13721330
warnings,
13731331
sync: {
@@ -1719,7 +1677,10 @@ async function readPostCorePluginUpdateResultFile(
17191677
if (
17201678
parsed &&
17211679
typeof parsed === "object" &&
1722-
(parsed.status === "ok" || parsed.status === "skipped" || parsed.status === "error")
1680+
(parsed.status === "ok" ||
1681+
parsed.status === "warning" ||
1682+
parsed.status === "skipped" ||
1683+
parsed.status === "error")
17231684
) {
17241685
return parsed;
17251686
}
@@ -2338,13 +2299,29 @@ export async function updateCommand(opts: UpdateCommandOptions): Promise<void> {
23382299
const resultWithPostUpdate: UpdateRunResult = postCorePluginUpdate
23392300
? {
23402301
...result,
2302+
status: postCorePluginUpdate.status === "error" ? "error" : result.status,
2303+
...(postCorePluginUpdate.status === "error" ? { reason: "post-update-plugins" } : {}),
23412304
postUpdate: {
23422305
...result.postUpdate,
23432306
plugins: postCorePluginUpdate,
23442307
},
23452308
}
23462309
: result;
23472310

2311+
if (postCorePluginUpdate?.status === "error") {
2312+
if (opts.json) {
2313+
defaultRuntime.writeJson(resultWithPostUpdate);
2314+
} else {
2315+
defaultRuntime.error(theme.error("Update failed during plugin post-update sync."));
2316+
}
2317+
await maybeRestartServiceAfterFailedPackageUpdate({
2318+
prePackageServiceStop,
2319+
jsonMode: Boolean(opts.json),
2320+
});
2321+
defaultRuntime.exit(1);
2322+
return;
2323+
}
2324+
23482325
let restartScriptPath: string | null = null;
23492326
let refreshGatewayServiceEnv = false;
23502327
let gatewayServiceEnv: NodeJS.ProcessEnv | undefined;

src/infra/update-runner.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ export type UpdateRunResult = {
5858
durationMs: number;
5959
postUpdate?: {
6060
plugins?: {
61-
status: "ok" | "skipped" | "error";
61+
status: "ok" | "warning" | "skipped" | "error";
6262
reason?: string;
6363
changed: boolean;
6464
warnings?: Array<{

0 commit comments

Comments
 (0)