Skip to content

Commit 98af517

Browse files
BSG2000CopilotBSG2000clawsweeper[bot]Takhoffman
authored
fix(channels): hint at when bundled channel module is missing (#76974)
Summary: - The PR adds a bundled-channel load-error formatter, wires it into the bundled-channel warning paths, adds focused tests, and updates the changelog. - Reproducibility: yes. source-level: current main logs bundled-channel load failures with bare `formatErrorMe ... cause`. The contributor's terminal proof demonstrates the same wrapped-error shape before and after the PR. Automerge notes: - PR branch already contained follow-up commit before automerge: fix(channels): walk error cause chain to detect missing bundled modules - PR branch already contained follow-up commit before automerge: docs(changelog): add Unreleased Fixes entry - PR branch already contained follow-up commit before automerge: Merge remote-tracking branch 'origin/main' into fix/bundled-channel-l… - PR branch already contained follow-up commit before automerge: Merge branch 'main' into fix/bundled-channel-load-doctor-hint Validation: - ClawSweeper review passed for head 416a8a2. - Required merge gates passed before the squash merge. Prepared head SHA: 416a8a2 Review: #76974 (comment) Co-authored-by: BSG2000 <github@hsu.hamburg> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: BSG2000 <BSG2000@users.noreply.github.com> Co-authored-by: BSG2000 <thomas.krohnfuss@stud.th-luebeck.de> Co-authored-by: Thomas Krohnfuß <BSG2000@users.noreply.github.com> Co-authored-by: clawsweeper[bot] <274271284+clawsweeper[bot]@users.noreply.github.com> Approved-by: takhoffman Co-authored-by: takhoffman <781889+takhoffman@users.noreply.github.com>
1 parent 0212188 commit 98af517

3 files changed

Lines changed: 90 additions & 8 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -254,6 +254,7 @@ Docs: https://docs.openclaw.ai
254254
- Agents: guard final-delivery fresh session routing against mismatched logical sessions before reusing recovered delivery context. (#83928) Thanks @joshavant.
255255
- Media: prevent image metadata probing from invoking external decoder delegates on unrecognized image bytes, and stop fallback chaining after real processing errors.
256256
- Media: install Sharp with the root package and fall back to sips, Windows native imaging, ImageMagick, GraphicsMagick, or ffmpeg for image resizing/conversion when Sharp is unavailable. Fixes #83401. Thanks @scotthuang.
257+
- Channels/bundled: append `openclaw doctor --fix` guidance to the bundled-channel load warnings emitted on `ERR_MODULE_NOT_FOUND` / `MODULE_NOT_FOUND` (including those wrapped on `.cause` by the native-require loader), so users hitting unstaged plugin runtime deps (e.g. `nostr-tools`) see an actionable repair hint instead of a bare module-not-found warning. (#76974) Thanks @BSG2000.
257258
- Telegram: deliver generated media completions back into forum topics by preserving topic IDs across requester-agent handoff. (#83556) Thanks @fuller-stack-dev.
258259
- Gateway: defer update-check startup until after readiness so package update checks no longer block sidecar-ready startup, while preserving update broadcasts and shutdown cleanup. (#83520) Thanks @samzong.
259260
- Telegram: keep `/btw` and read-only status commands from aborting active runs, and avoid retaining raw update payloads in timed-out spool tombstones. Refs #83272.
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import { describe, it, expect } from "vitest";
2+
import { describeBundledChannelLoadError } from "./bundled.js";
3+
4+
describe("describeBundledChannelLoadError", () => {
5+
it("appends the doctor --fix hint for a top-level MODULE_NOT_FOUND error", () => {
6+
const err = Object.assign(new Error("Cannot find module 'nostr-tools'"), {
7+
code: "MODULE_NOT_FOUND",
8+
});
9+
const detail = describeBundledChannelLoadError(err, "nostr");
10+
expect(detail).toContain("Cannot find module 'nostr-tools'");
11+
expect(detail).toContain("openclaw doctor --fix");
12+
expect(detail).toContain("channel nostr");
13+
});
14+
15+
it("appends the doctor --fix hint for a top-level ERR_MODULE_NOT_FOUND error", () => {
16+
const err = Object.assign(new Error("Cannot find package '@larksuiteoapi/node-sdk'"), {
17+
code: "ERR_MODULE_NOT_FOUND",
18+
});
19+
expect(describeBundledChannelLoadError(err, "feishu")).toContain("openclaw doctor --fix");
20+
});
21+
22+
it("appends the doctor --fix hint when the missing-module code is on a nested cause (native require wrap)", () => {
23+
// Mirrors src/channels/plugins/module-loader.ts which wraps require failures
24+
// in `new Error(..., { cause: error })`. The MODULE_NOT_FOUND code only
25+
// exists on the cause, not on the outer wrapper.
26+
const inner = Object.assign(new Error("Cannot find module 'discord.js'"), {
27+
code: "MODULE_NOT_FOUND",
28+
});
29+
const wrapped = new Error(
30+
"failed to load channel plugin module with native require: /plugins/discord/index.js",
31+
{ cause: inner },
32+
);
33+
const detail = describeBundledChannelLoadError(wrapped, "discord");
34+
expect(detail).toContain("openclaw doctor --fix");
35+
expect(detail).toContain("channel discord");
36+
// The detail should still surface the underlying message via the existing
37+
// formatErrorMessage cause traversal.
38+
expect(detail).toContain("Cannot find module 'discord.js'");
39+
});
40+
41+
it("returns the bare detail when the error is unrelated", () => {
42+
const detail = describeBundledChannelLoadError(new TypeError("boom"), "whatsapp");
43+
expect(detail).not.toContain("openclaw doctor --fix");
44+
expect(detail).toContain("boom");
45+
});
46+
47+
it("does not loop on a self-referential cause chain", () => {
48+
const err = new Error("outer") as Error & { cause?: unknown };
49+
err.cause = err;
50+
expect(() => describeBundledChannelLoadError(err, "msteams")).not.toThrow();
51+
});
52+
});

src/channels/plugins/bundled.ts

Lines changed: 37 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import path from "node:path";
22
import type { OpenClawConfig } from "../../config/types.openclaw.js";
3-
import { formatErrorMessage } from "../../infra/errors.js";
3+
import { extractErrorCode, formatErrorMessage } from "../../infra/errors.js";
44
import { isPathInside } from "../../infra/path-guards.js";
55
import { createSubsystemLogger } from "../../logging/subsystem.js";
66
import type {
@@ -241,6 +241,35 @@ function loadGeneratedBundledChannelModule(params: {
241241
}
242242
}
243243

244+
// Walk the `.cause` chain looking for a Node-style "module not found" code.
245+
// Native-require failures inside `module-loader.ts` rewrap the original Node
246+
// error in a new Error with `{ cause }`, so the missing-module code lives on
247+
// the cause rather than the top-level error.
248+
function findMissingModuleCodeInChain(error: unknown): string | undefined {
249+
const seen = new Set<unknown>();
250+
let current: unknown = error;
251+
while (current && !seen.has(current)) {
252+
seen.add(current);
253+
const code = extractErrorCode(current);
254+
if (code === "ERR_MODULE_NOT_FOUND" || code === "MODULE_NOT_FOUND") {
255+
return code;
256+
}
257+
if (typeof current !== "object") {
258+
return undefined;
259+
}
260+
current = (current as { cause?: unknown }).cause;
261+
}
262+
return undefined;
263+
}
264+
265+
export function describeBundledChannelLoadError(error: unknown, channelId: string): string {
266+
const detail = formatErrorMessage(error);
267+
if (findMissingModuleCodeInChain(error) !== undefined) {
268+
return `${detail} (run \`openclaw doctor --fix\` to install missing bundled runtime dependencies for channel ${channelId})`;
269+
}
270+
return detail;
271+
}
272+
244273
function loadGeneratedBundledChannelEntry(params: {
245274
rootScope: BundledChannelRootScope;
246275
metadata: BundledChannelPluginMetadata;
@@ -264,7 +293,7 @@ function loadGeneratedBundledChannelEntry(params: {
264293
entry,
265294
};
266295
} catch (error) {
267-
const detail = formatErrorMessage(error);
296+
const detail = describeBundledChannelLoadError(error, params.metadata.manifest.id);
268297
log.warn(`[channels] failed to load bundled channel ${params.metadata.manifest.id}: ${detail}`);
269298
return null;
270299
}
@@ -293,7 +322,7 @@ function loadGeneratedBundledChannelSetupEntry(params: {
293322
}
294323
return setupEntry;
295324
} catch (error) {
296-
const detail = formatErrorMessage(error);
325+
const detail = describeBundledChannelLoadError(error, params.metadata.manifest.id);
297326
log.warn(
298327
`[channels] failed to load bundled channel setup entry ${params.metadata.manifest.id}: ${detail}`,
299328
);
@@ -573,7 +602,7 @@ function getBundledChannelPluginForRoot(
573602
loadContext.lazyPluginsById.set(id, normalizedPlugin);
574603
return normalizedPlugin;
575604
} catch (error) {
576-
const detail = formatErrorMessage(error);
605+
const detail = describeBundledChannelLoadError(error, id);
577606
log.warn(`[channels] failed to load bundled channel ${id}: ${detail}`);
578607
loadContext.lazyPluginsById.set(id, null);
579608
return undefined;
@@ -601,7 +630,7 @@ function getBundledChannelSecretsForRoot(
601630
loadContext.lazySecretsById.set(id, secrets ?? null);
602631
return secrets;
603632
} catch (error) {
604-
const detail = formatErrorMessage(error);
633+
const detail = describeBundledChannelLoadError(error, id);
605634
log.warn(`[channels] failed to load bundled channel secrets ${id}: ${detail}`);
606635
loadContext.lazySecretsById.set(id, null);
607636
return undefined;
@@ -626,7 +655,7 @@ function getBundledChannelAccountInspectorForRoot(
626655
loadContext.lazyAccountInspectorsById.set(id, inspector);
627656
return inspector;
628657
} catch (error) {
629-
const detail = formatErrorMessage(error);
658+
const detail = describeBundledChannelLoadError(error, id);
630659
log.warn(`[channels] failed to load bundled channel account inspector ${id}: ${detail}`);
631660
loadContext.lazyAccountInspectorsById.set(id, null);
632661
return undefined;
@@ -654,7 +683,7 @@ function getBundledChannelSetupPluginForRoot(
654683
loadContext.lazySetupPluginsById.set(id, plugin);
655684
return plugin;
656685
} catch (error) {
657-
const detail = formatErrorMessage(error);
686+
const detail = describeBundledChannelLoadError(error, id);
658687
log.warn(`[channels] failed to load bundled channel setup ${id}: ${detail}`);
659688
loadContext.lazySetupPluginsById.set(id, null);
660689
return undefined;
@@ -682,7 +711,7 @@ function getBundledChannelSetupSecretsForRoot(
682711
loadContext.lazySetupSecretsById.set(id, secrets ?? null);
683712
return secrets;
684713
} catch (error) {
685-
const detail = formatErrorMessage(error);
714+
const detail = describeBundledChannelLoadError(error, id);
686715
log.warn(`[channels] failed to load bundled channel setup secrets ${id}: ${detail}`);
687716
loadContext.lazySetupSecretsById.set(id, null);
688717
return undefined;

0 commit comments

Comments
 (0)