Skip to content

Commit c96a12d

Browse files
BKF-Gittysteipete
andauthored
fix(update): surface plugin channel fallbacks (#81422)
* fix: surface plugin update channel fallbacks * fix: clarify dry-run plugin fallback output * fix: preserve failed plugin fallback metadata * chore: mark compatibility aliases deprecated * chore: fix channel runtime lint directive --------- Co-authored-by: Peter Steinberger <steipete@gmail.com>
1 parent 28a2e79 commit c96a12d

10 files changed

Lines changed: 281 additions & 10 deletions

File tree

src/agents/embedded-agent-runner/run/preemptive-compaction.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -223,7 +223,11 @@ export function estimateRenderedLlmBoundaryTokenPressure(params: {
223223
return Math.max(0, Math.ceil((systemTokens + promptTokens) * SAFETY_MARGIN));
224224
}
225225

226-
/** Backward-compatible alias for callers that still name this a pre-prompt estimate. */
226+
/**
227+
* Backward-compatible alias for callers that still name this a pre-prompt estimate.
228+
*
229+
* @deprecated Use estimateLlmBoundaryTokenPressure.
230+
*/
227231
export function estimatePrePromptTokens(params: {
228232
messages: AgentMessage[];
229233
systemPrompt?: string;

src/cli/plugins-update-outcomes.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@ import { theme } from "../../packages/terminal-core/src/theme.js";
44
type PluginUpdateCliOutcome = {
55
status: string;
66
message: string;
7+
channelFallback?: {
8+
message: string;
9+
};
710
};
811

912
/** Log update outcomes with severity styling and report whether any errors occurred. */
@@ -16,13 +19,22 @@ export function logPluginUpdateOutcomes(params: {
1619
if (outcome.status === "error") {
1720
hasErrors = true;
1821
params.log(theme.error(outcome.message));
22+
if (outcome.channelFallback) {
23+
params.log(theme.warn(outcome.channelFallback.message));
24+
}
1925
continue;
2026
}
2127
if (outcome.status === "skipped") {
2228
params.log(theme.warn(outcome.message));
29+
if (outcome.channelFallback) {
30+
params.log(theme.warn(outcome.channelFallback.message));
31+
}
2332
continue;
2433
}
2534
params.log(outcome.message);
35+
if (outcome.channelFallback) {
36+
params.log(theme.warn(outcome.channelFallback.message));
37+
}
2638
}
2739
return { hasErrors };
2840
}

src/cli/update-cli.test.ts

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1580,6 +1580,49 @@ describe("update-cli", () => {
15801580
expect(npmPluginUpdateCall()?.timeoutMs).toBe(1_800_000);
15811581
});
15821582

1583+
it("prints plugin channel fallbacks near the post-core plugin summary", async () => {
1584+
updateNpmInstalledPlugins.mockResolvedValueOnce({
1585+
changed: false,
1586+
config: baseConfig,
1587+
outcomes: [
1588+
{
1589+
pluginId: "lossless-claw",
1590+
status: "updated",
1591+
message: "Updated lossless-claw: 1.0.0 -> 1.0.1.",
1592+
channelFallback: {
1593+
requestedSpec: "lossless-claw@beta",
1594+
usedSpec: "lossless-claw",
1595+
requestedLabel: "@beta",
1596+
usedLabel: "@latest",
1597+
reason: "unavailable",
1598+
message:
1599+
"plugin channel fallback: lossless-claw used @latest because @beta was unavailable",
1600+
},
1601+
},
1602+
],
1603+
});
1604+
1605+
await withEnvAsync(
1606+
{
1607+
OPENCLAW_UPDATE_POST_CORE: "1",
1608+
OPENCLAW_UPDATE_POST_CORE_CHANNEL: "beta",
1609+
},
1610+
async () => {
1611+
await updateCommand({ restart: false });
1612+
},
1613+
);
1614+
1615+
const logs = vi.mocked(runtimeCapture.log).mock.calls.map((call) => String(call[0]));
1616+
expect(logs.some((line) => line.includes("npm plugins: 1 updated, 0 unchanged."))).toBe(true);
1617+
expect(
1618+
logs.some((line) =>
1619+
line.includes(
1620+
"plugin channel fallback: lossless-claw used @latest because @beta was unavailable",
1621+
),
1622+
),
1623+
).toBe(true);
1624+
});
1625+
15831626
it("uses a fail-closed integrity policy for post-core plugin updates", async () => {
15841627
await withEnvAsync(
15851628
{

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

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -560,6 +560,20 @@ function createGuidedPostUpdatePluginOutcome(outcome: PluginUpdateOutcome): {
560560
};
561561
}
562562

563+
function collectPluginChannelFallbackMessages(outcomes: readonly PluginUpdateOutcome[]): string[] {
564+
const seen = new Set<string>();
565+
const messages: string[] = [];
566+
for (const outcome of outcomes) {
567+
const message = outcome.channelFallback?.message;
568+
if (!message || seen.has(message)) {
569+
continue;
570+
}
571+
seen.add(message);
572+
messages.push(message);
573+
}
574+
return messages;
575+
}
576+
563577
function isDisabledAfterFailureOutcome(outcome: PluginUpdateOutcome): boolean {
564578
return outcome.status === "skipped" && outcome.message.includes("after plugin update failure");
565579
}
@@ -1923,6 +1937,10 @@ export async function updatePluginsAfterCoreUpdate(params: {
19231937
defaultRuntime.log(theme.muted(`npm plugins: ${parts.join(", ")}.`));
19241938
}
19251939

1940+
for (const message of collectPluginChannelFallbackMessages(pluginUpdateOutcomes)) {
1941+
defaultRuntime.log(theme.warn(message));
1942+
}
1943+
19261944
for (const outcome of pluginUpdateOutcomes) {
19271945
if (outcome.status !== "error") {
19281946
continue;

src/infra/channel-runtime-context.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,8 +49,8 @@ export function registerChannelRuntimeContext(
4949
});
5050
}
5151

52-
// oxlint-disable-next-line typescript/no-unnecessary-type-parameters -- Runtime context values are caller-typed by key.
5352
/** Reads a channel-scoped runtime context from the current runtime registry. */
53+
// oxlint-disable-next-line typescript/no-unnecessary-type-parameters -- Runtime context values are caller-typed by key.
5454
export function getChannelRuntimeContext<T = unknown>(
5555
params: ChannelRuntimeContextKey & {
5656
channelRuntime?: ChannelRuntimeSurface;

src/infra/home-dir.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -129,7 +129,11 @@ export function resolveHomeRelativePath(
129129
return path.resolve(trimmed);
130130
}
131131

132-
/** Backward-compatible alias for resolving user paths against the effective home. */
132+
/**
133+
* Backward-compatible alias for resolving user paths against the effective home.
134+
*
135+
* @deprecated Use resolveHomeRelativePath.
136+
*/
133137
export function resolveUserPath(
134138
input: string,
135139
env: NodeJS.ProcessEnv = process.env,

src/infra/update-runner.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,14 @@ export type UpdateRunResult = {
8686
message: string;
8787
currentVersion?: string;
8888
nextVersion?: string;
89+
channelFallback?: {
90+
requestedSpec: string;
91+
usedSpec: string;
92+
requestedLabel: string;
93+
usedLabel: string;
94+
reason: "unavailable" | "failed";
95+
message: string;
96+
};
8997
}>;
9098
};
9199
integrityDrifts: Array<{

src/media/audio.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,11 @@ export function isVoiceMessageCompatibleAudio(opts: {
3535
return VOICE_MESSAGE_AUDIO_EXTENSIONS.has(ext);
3636
}
3737

38-
/** Backward-compatible alias for voice-message audio compatibility checks. */
38+
/**
39+
* Backward-compatible alias for voice-message audio compatibility checks.
40+
*
41+
* @deprecated Use isVoiceMessageCompatibleAudio.
42+
*/
3943
export function isVoiceCompatibleAudio(opts: {
4044
contentType?: string | null;
4145
fileName?: string | null;

src/plugins/update.test.ts

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2188,6 +2188,52 @@ describe("updateNpmInstalledPlugins", () => {
21882188
expect(result.outcomes[0]?.message).toBe(
21892189
"Updated openclaw-codex-app-server: unknown -> 0.2.6. (warning: beta channel fallback used openclaw-codex-app-server because openclaw-codex-app-server@beta could not be used).",
21902190
);
2191+
expect(result.outcomes[0]?.channelFallback).toEqual({
2192+
requestedSpec: "openclaw-codex-app-server@beta",
2193+
usedSpec: "openclaw-codex-app-server",
2194+
requestedLabel: "@beta",
2195+
usedLabel: "@latest",
2196+
reason: "unavailable",
2197+
message:
2198+
"plugin channel fallback: openclaw-codex-app-server used @latest because @beta was unavailable",
2199+
});
2200+
});
2201+
2202+
it("reports npm beta fallback as tentative during dry-run checks", async () => {
2203+
installPluginFromNpmSpecMock
2204+
.mockResolvedValueOnce({
2205+
ok: false,
2206+
error:
2207+
"npm ERR! code ETARGET\nnpm ERR! No matching version found for openclaw-codex-app-server@beta.",
2208+
})
2209+
.mockResolvedValueOnce(
2210+
createSuccessfulNpmUpdateResult({
2211+
pluginId: "openclaw-codex-app-server",
2212+
targetDir: "/tmp/openclaw-codex-app-server",
2213+
version: "0.2.6",
2214+
npmResolution: {
2215+
name: "openclaw-codex-app-server",
2216+
version: "0.2.6",
2217+
resolvedSpec: "openclaw-codex-app-server@0.2.6",
2218+
},
2219+
}),
2220+
);
2221+
2222+
const result = await updateNpmInstalledPlugins({
2223+
config: createCodexAppServerInstallConfig({
2224+
spec: "openclaw-codex-app-server",
2225+
}),
2226+
pluginIds: ["openclaw-codex-app-server"],
2227+
updateChannel: "beta",
2228+
dryRun: true,
2229+
});
2230+
2231+
expect(result.outcomes[0]?.message).toBe(
2232+
"Would update openclaw-codex-app-server: unknown -> 0.2.6. (warning: beta channel fallback would use openclaw-codex-app-server because openclaw-codex-app-server@beta could not be used).",
2233+
);
2234+
expect(result.outcomes[0]?.channelFallback?.message).toBe(
2235+
"plugin channel fallback: openclaw-codex-app-server would use @latest because @beta was unavailable",
2236+
);
21912237
});
21922238

21932239
it("falls back to the default npm spec when the beta package exists but is invalid", async () => {
@@ -2233,6 +2279,12 @@ describe("updateNpmInstalledPlugins", () => {
22332279
expect(result.outcomes[0]?.message).toBe(
22342280
"Updated openclaw-codex-app-server: unknown -> 0.2.6. (warning: beta channel fallback used openclaw-codex-app-server because openclaw-codex-app-server@beta could not be used).",
22352281
);
2282+
expect(result.outcomes[0]?.channelFallback).toMatchObject({
2283+
requestedLabel: "@beta",
2284+
usedLabel: "@latest",
2285+
reason: "failed",
2286+
message: "plugin channel fallback: openclaw-codex-app-server used @latest after @beta failed",
2287+
});
22362288
});
22372289

22382290
it("reports the fallback npm spec when beta fallback also fails", async () => {
@@ -2262,6 +2314,55 @@ describe("updateNpmInstalledPlugins", () => {
22622314
status: "error",
22632315
message:
22642316
"Failed to update openclaw-codex-app-server: npm package not found for openclaw-codex-app-server.",
2317+
channelFallback: {
2318+
requestedSpec: "openclaw-codex-app-server@beta",
2319+
usedSpec: "openclaw-codex-app-server",
2320+
requestedLabel: "@beta",
2321+
usedLabel: "@latest",
2322+
reason: "failed",
2323+
message:
2324+
"plugin channel fallback: openclaw-codex-app-server used @latest after @beta failed",
2325+
},
2326+
},
2327+
]);
2328+
});
2329+
2330+
it("keeps fallback metadata when a dry-run beta fallback also fails", async () => {
2331+
installPluginFromNpmSpecMock
2332+
.mockResolvedValueOnce({
2333+
ok: false,
2334+
error: "Installed plugin package uses a TypeScript entry without compiled runtime output.",
2335+
})
2336+
.mockResolvedValueOnce({
2337+
ok: false,
2338+
code: "npm_package_not_found",
2339+
error: "npm package not found",
2340+
});
2341+
2342+
const result = await updateNpmInstalledPlugins({
2343+
config: createCodexAppServerInstallConfig({
2344+
spec: "openclaw-codex-app-server",
2345+
}),
2346+
pluginIds: ["openclaw-codex-app-server"],
2347+
updateChannel: "beta",
2348+
dryRun: true,
2349+
});
2350+
2351+
expect(result.outcomes).toEqual([
2352+
{
2353+
pluginId: "openclaw-codex-app-server",
2354+
status: "error",
2355+
message:
2356+
"Failed to check openclaw-codex-app-server: npm package not found for openclaw-codex-app-server.",
2357+
channelFallback: {
2358+
requestedSpec: "openclaw-codex-app-server@beta",
2359+
usedSpec: "openclaw-codex-app-server",
2360+
requestedLabel: "@beta",
2361+
usedLabel: "@latest",
2362+
reason: "failed",
2363+
message:
2364+
"plugin channel fallback: openclaw-codex-app-server would use @latest after @beta failed",
2365+
},
22652366
},
22662367
]);
22672368
});

0 commit comments

Comments
 (0)