From f322bd558eea175ac5772bfaacb183def2e15bf6 Mon Sep 17 00:00:00 2001 From: pallaoro Date: Wed, 16 Sep 2026 22:59:25 +0200 Subject: [PATCH] Share one step registry per process, whatever path ClawFlow was imported from Node caches ES modules by resolved path, so a plugin importing ClawFlow from src/index.ts and a gateway running dist/index.js got two registries. Steps registered through the first never reached the engine, and flows failed validation with "Unknown node type". House plugins have flipped between the two paths more than once: agent-tools was fixed in July, openclaw-email was still wrong in September. The default registry now lives on globalThis under a registered symbol, so every module instance in the process reads and writes the same one. The test loads the module twice under different specifiers and checks a step registered through one is visible through the other. --- package-lock.json | 4 ++-- package.json | 2 +- src/core/custom-steps.ts | 19 +++++++++++++++++-- tests/registry-shared.test.ts | 19 +++++++++++++++++++ 4 files changed, 39 insertions(+), 5 deletions(-) create mode 100644 tests/registry-shared.test.ts diff --git a/package-lock.json b/package-lock.json index 924c847..2c709ad 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@clawnify/clawflow", - "version": "1.6.4", + "version": "1.6.5", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@clawnify/clawflow", - "version": "1.6.4", + "version": "1.6.5", "license": "MIT", "dependencies": { "croner": "^10.0.1" diff --git a/package.json b/package.json index 5c41693..9ca0c20 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@clawnify/clawflow", - "version": "1.6.4", + "version": "1.6.5", "description": "The n8n for agents. A declarative, AI-native workflow format that agents can read, write, and run.", "type": "module", "main": "./dist/index.js", diff --git a/src/core/custom-steps.ts b/src/core/custom-steps.ts index a6a07f1..32bc586 100644 --- a/src/core/custom-steps.ts +++ b/src/core/custom-steps.ts @@ -141,8 +141,23 @@ export class StepRegistry { } } -/** Module-level default registry shared by host applications. */ -export const defaultRegistry = new StepRegistry(); +/** + * Process-wide default registry shared by host applications. + * + * Held on `globalThis` under a registered symbol, not as a plain module + * singleton. Node caches ES modules by resolved path, so a plugin importing + * ClawFlow from `src/index.ts` and a gateway running `dist/index.js` used to + * get two registries: the plugin's steps landed in one the engine never + * read, and flows failed validation with "Unknown node type". House plugins + * flipped between the two paths more than once (agent-tools fixed in + * July, openclaw-email still wrong in September). With one registry per + * process the import path no longer matters, and neither does a second + * copy of the package on disk. + */ +const REGISTRY_KEY = Symbol.for("@clawnify/clawflow/step-registry"); +type RegistryHolder = { [REGISTRY_KEY]?: StepRegistry }; +const holder = globalThis as unknown as RegistryHolder; +export const defaultRegistry: StepRegistry = holder[REGISTRY_KEY] ?? (holder[REGISTRY_KEY] = new StepRegistry()); /** * Register a custom step type on the default registry. Call once at startup. diff --git a/tests/registry-shared.test.ts b/tests/registry-shared.test.ts new file mode 100644 index 0000000..e3187e8 --- /dev/null +++ b/tests/registry-shared.test.ts @@ -0,0 +1,19 @@ +import { describe, it } from "node:test"; +import assert from "node:assert/strict"; + +// Two imports of the same module file under different specifiers are two +// module instances in Node — exactly what a plugin importing clawflow from +// src/ while the gateway runs dist/ produces. The default registry must be +// the same object in both. +describe("default step registry", () => { + it("is shared across separately loaded module instances", async () => { + const a = await import("../src/core/custom-steps.js?instance=a"); + const b = await import("../src/core/custom-steps.js?instance=b"); + assert.notEqual(a, b, "test setup: expected two module instances"); + assert.equal(a.defaultRegistry, b.defaultRegistry); + + a.registerStepType({ name: "shared_probe_step", allowedKeys: [], run: () => 1 }); + assert.ok(b.defaultRegistry.has("shared_probe_step"), "a step registered through one instance is visible through the other"); + a.defaultRegistry.clear(); + }); +});