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(); + }); +});