refactor(channels): dedupe hook and monitor execution paths

This commit is contained in:
Peter Steinberger
2026-02-22 21:18:53 +00:00
parent 06b0a60bef
commit 2081b3a3c4
19 changed files with 347 additions and 213 deletions

View File

@@ -172,6 +172,21 @@ export function createHookRunner(registry: PluginRegistry, options: HookRunnerOp
return next;
};
const handleHookError = (params: {
hookName: PluginHookName;
pluginId: string;
error: unknown;
}): never | void => {
const msg = `[hooks] ${params.hookName} handler from ${params.pluginId} failed: ${String(
params.error,
)}`;
if (catchErrors) {
logger?.error(msg);
return;
}
throw new Error(msg, { cause: params.error });
};
/**
* Run a hook that doesn't return a value (fire-and-forget style).
* All handlers are executed in parallel for performance.
@@ -192,12 +207,7 @@ export function createHookRunner(registry: PluginRegistry, options: HookRunnerOp
try {
await (hook.handler as (event: unknown, ctx: unknown) => Promise<void>)(event, ctx);
} catch (err) {
const msg = `[hooks] ${hookName} handler from ${hook.pluginId} failed: ${String(err)}`;
if (catchErrors) {
logger?.error(msg);
} else {
throw new Error(msg, { cause: err });
}
handleHookError({ hookName, pluginId: hook.pluginId, error: err });
}
});
@@ -237,12 +247,7 @@ export function createHookRunner(registry: PluginRegistry, options: HookRunnerOp
}
}
} catch (err) {
const msg = `[hooks] ${hookName} handler from ${hook.pluginId} failed: ${String(err)}`;
if (catchErrors) {
logger?.error(msg);
} else {
throw new Error(msg, { cause: err });
}
handleHookError({ hookName, pluginId: hook.pluginId, error: err });
}
}

View File

@@ -0,0 +1,45 @@
import { describe, expect, it } from "vitest";
import { buildNpmResolutionInstallFields, recordPluginInstall } from "./installs.js";
describe("buildNpmResolutionInstallFields", () => {
it("maps npm resolution metadata into install record fields", () => {
const fields = buildNpmResolutionInstallFields({
name: "@openclaw/demo",
version: "1.2.3",
resolvedSpec: "@openclaw/demo@1.2.3",
integrity: "sha512-abc",
shasum: "deadbeef",
resolvedAt: "2026-02-22T00:00:00.000Z",
});
expect(fields).toEqual({
resolvedName: "@openclaw/demo",
resolvedVersion: "1.2.3",
resolvedSpec: "@openclaw/demo@1.2.3",
integrity: "sha512-abc",
shasum: "deadbeef",
resolvedAt: "2026-02-22T00:00:00.000Z",
});
});
it("returns undefined fields when resolution is missing", () => {
expect(buildNpmResolutionInstallFields(undefined)).toEqual({
resolvedName: undefined,
resolvedVersion: undefined,
resolvedSpec: undefined,
integrity: undefined,
shasum: undefined,
resolvedAt: undefined,
});
});
});
describe("recordPluginInstall", () => {
it("stores install metadata for the plugin id", () => {
const next = recordPluginInstall({}, { pluginId: "demo", source: "npm", spec: "demo@latest" });
expect(next.plugins?.installs?.demo).toMatchObject({
source: "npm",
spec: "demo@latest",
});
expect(typeof next.plugins?.installs?.demo?.installedAt).toBe("string");
});
});

View File

@@ -1,8 +1,25 @@
import type { OpenClawConfig } from "../config/config.js";
import type { PluginInstallRecord } from "../config/types.plugins.js";
import type { NpmSpecResolution } from "../infra/install-source-utils.js";
export type PluginInstallUpdate = PluginInstallRecord & { pluginId: string };
export function buildNpmResolutionInstallFields(
resolution?: NpmSpecResolution,
): Pick<
PluginInstallRecord,
"resolvedName" | "resolvedVersion" | "resolvedSpec" | "integrity" | "shasum" | "resolvedAt"
> {
return {
resolvedName: resolution?.name,
resolvedVersion: resolution?.version,
resolvedSpec: resolution?.resolvedSpec,
integrity: resolution?.integrity,
shasum: resolution?.shasum,
resolvedAt: resolution?.resolvedAt,
};
}
export function recordPluginInstall(
cfg: OpenClawConfig,
update: PluginInstallUpdate,

View File

@@ -175,6 +175,31 @@ function createPluginRecord(params: {
};
}
function recordPluginError(params: {
logger: PluginLogger;
registry: PluginRegistry;
record: PluginRecord;
seenIds: Map<string, PluginRecord["origin"]>;
pluginId: string;
origin: PluginRecord["origin"];
error: unknown;
logPrefix: string;
diagnosticMessagePrefix: string;
}) {
const errorText = String(params.error);
params.logger.error(`${params.logPrefix}${errorText}`);
params.record.status = "error";
params.record.error = errorText;
params.registry.plugins.push(params.record);
params.seenIds.set(params.pluginId, params.origin);
params.registry.diagnostics.push({
level: "error",
pluginId: params.record.id,
source: params.record.source,
message: `${params.diagnosticMessagePrefix}${errorText}`,
});
}
function pushDiagnostics(diagnostics: PluginDiagnostic[], append: PluginDiagnostic[]) {
diagnostics.push(...append);
}
@@ -508,16 +533,16 @@ export function loadOpenClawPlugins(options: PluginLoadOptions = {}): PluginRegi
try {
mod = getJiti()(candidate.source) as OpenClawPluginModule;
} catch (err) {
logger.error(`[plugins] ${record.id} failed to load from ${record.source}: ${String(err)}`);
record.status = "error";
record.error = String(err);
registry.plugins.push(record);
seenIds.set(pluginId, candidate.origin);
registry.diagnostics.push({
level: "error",
pluginId: record.id,
source: record.source,
message: `failed to load plugin: ${String(err)}`,
recordPluginError({
logger,
registry,
record,
seenIds,
pluginId,
origin: candidate.origin,
error: err,
logPrefix: `[plugins] ${record.id} failed to load from ${record.source}: `,
diagnosticMessagePrefix: "failed to load plugin: ",
});
continue;
}
@@ -634,18 +659,16 @@ export function loadOpenClawPlugins(options: PluginLoadOptions = {}): PluginRegi
registry.plugins.push(record);
seenIds.set(pluginId, candidate.origin);
} catch (err) {
logger.error(
`[plugins] ${record.id} failed during register from ${record.source}: ${String(err)}`,
);
record.status = "error";
record.error = String(err);
registry.plugins.push(record);
seenIds.set(pluginId, candidate.origin);
registry.diagnostics.push({
level: "error",
pluginId: record.id,
source: record.source,
message: `plugin failed during register: ${String(err)}`,
recordPluginError({
logger,
registry,
record,
seenIds,
pluginId,
origin: candidate.origin,
error: err,
logPrefix: `[plugins] ${record.id} failed during register from ${record.source}: `,
diagnosticMessagePrefix: "plugin failed during register: ",
});
}
}

View File

@@ -4,7 +4,7 @@ import type { UpdateChannel } from "../infra/update-channels.js";
import { resolveUserPath } from "../utils.js";
import { discoverOpenClawPlugins } from "./discovery.js";
import { installPluginFromNpmSpec, resolvePluginInstallDir } from "./install.js";
import { recordPluginInstall } from "./installs.js";
import { buildNpmResolutionInstallFields, recordPluginInstall } from "./installs.js";
import { loadPluginManifest } from "./manifest.js";
export type PluginUpdateLogger = {
@@ -344,12 +344,7 @@ export async function updateNpmInstalledPlugins(params: {
spec: record.spec,
installPath: result.targetDir,
version: nextVersion,
resolvedName: result.npmResolution?.name,
resolvedVersion: result.npmResolution?.version,
resolvedSpec: result.npmResolution?.resolvedSpec,
integrity: result.npmResolution?.integrity,
shasum: result.npmResolution?.shasum,
resolvedAt: result.npmResolution?.resolvedAt,
...buildNpmResolutionInstallFields(result.npmResolution),
});
changed = true;
@@ -473,12 +468,7 @@ export async function syncPluginsForUpdateChannel(params: {
spec,
installPath: result.targetDir,
version: result.version,
resolvedName: result.npmResolution?.name,
resolvedVersion: result.npmResolution?.version,
resolvedSpec: result.npmResolution?.resolvedSpec,
integrity: result.npmResolution?.integrity,
shasum: result.npmResolution?.shasum,
resolvedAt: result.npmResolution?.resolvedAt,
...buildNpmResolutionInstallFields(result.npmResolution),
sourcePath: undefined,
});
summary.switchedToNpm.push(pluginId);