Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
19 changes: 17 additions & 2 deletions src/core/custom-steps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
19 changes: 19 additions & 0 deletions tests/registry-shared.test.ts
Original file line number Diff line number Diff line change
@@ -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();
});
});
Loading