clawmatrix 0.1.16 → 0.1.18
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/BOOTSTRAP.md +123 -121
- package/README.md +76 -71
- package/openclaw.plugin.json +1 -67
- package/package.json +5 -3
- package/src/cluster-service.ts +58 -0
- package/src/config.ts +14 -2
- package/src/index.ts +51 -62
- package/src/knowledge-sync.ts +426 -0
- package/src/tools/cluster-events.ts +8 -8
- package/src/tools/cluster-exec.ts +1 -1
- package/src/tools/cluster-handoff-reply.ts +2 -6
- package/src/tools/cluster-handoff.ts +2 -10
- package/src/tools/cluster-peers.ts +4 -11
- package/src/tools/cluster-read.ts +1 -1
- package/src/tools/cluster-tool.ts +1 -1
- package/src/tools/cluster-write.ts +1 -1
- package/src/types.ts +10 -1
package/openclaw.plugin.json
CHANGED
|
@@ -5,73 +5,7 @@
|
|
|
5
5
|
"providers": ["clawmatrix"],
|
|
6
6
|
"configSchema": {
|
|
7
7
|
"type": "object",
|
|
8
|
-
"
|
|
9
|
-
"nodeId": { "type": "string" },
|
|
10
|
-
"secret": { "type": "string" },
|
|
11
|
-
"listen": { "type": "boolean", "default": false },
|
|
12
|
-
"listenHost": { "type": "string", "default": "0.0.0.0" },
|
|
13
|
-
"listenPort": { "type": "number", "default": 19000 },
|
|
14
|
-
"peers": {
|
|
15
|
-
"type": "array",
|
|
16
|
-
"items": {
|
|
17
|
-
"type": "object",
|
|
18
|
-
"properties": {
|
|
19
|
-
"nodeId": { "type": "string" },
|
|
20
|
-
"url": { "type": "string" }
|
|
21
|
-
},
|
|
22
|
-
"required": ["nodeId", "url"]
|
|
23
|
-
},
|
|
24
|
-
"default": []
|
|
25
|
-
},
|
|
26
|
-
"agents": {
|
|
27
|
-
"type": "array",
|
|
28
|
-
"items": {
|
|
29
|
-
"type": "object",
|
|
30
|
-
"properties": {
|
|
31
|
-
"id": { "type": "string" },
|
|
32
|
-
"description": { "type": "string" },
|
|
33
|
-
"tags": { "type": "array", "items": { "type": "string" }, "default": [] }
|
|
34
|
-
},
|
|
35
|
-
"required": ["id", "description"]
|
|
36
|
-
},
|
|
37
|
-
"default": []
|
|
38
|
-
},
|
|
39
|
-
"models": {
|
|
40
|
-
"type": "array",
|
|
41
|
-
"items": {
|
|
42
|
-
"type": "object",
|
|
43
|
-
"properties": {
|
|
44
|
-
"id": { "type": "string" },
|
|
45
|
-
"provider": { "type": "string" },
|
|
46
|
-
"description": { "type": "string" }
|
|
47
|
-
},
|
|
48
|
-
"required": ["id", "provider"]
|
|
49
|
-
},
|
|
50
|
-
"default": []
|
|
51
|
-
},
|
|
52
|
-
"tags": { "type": "array", "items": { "type": "string" }, "default": [] },
|
|
53
|
-
"proxyPort": { "type": "number", "default": 19001 },
|
|
54
|
-
"toolProxy": {
|
|
55
|
-
"type": "object",
|
|
56
|
-
"properties": {
|
|
57
|
-
"enabled": { "type": "boolean", "default": false },
|
|
58
|
-
"allow": {
|
|
59
|
-
"type": "array",
|
|
60
|
-
"items": { "type": "string" },
|
|
61
|
-
"default": [],
|
|
62
|
-
"description": "Allowed OpenClaw tool names. Use [\"*\"] for all. Empty or [\"*\"] = all allowed."
|
|
63
|
-
},
|
|
64
|
-
"deny": {
|
|
65
|
-
"type": "array",
|
|
66
|
-
"items": { "type": "string" },
|
|
67
|
-
"default": [],
|
|
68
|
-
"description": "Denied OpenClaw tool names (takes precedence over allow)."
|
|
69
|
-
},
|
|
70
|
-
"maxOutputBytes": { "type": "number", "default": 1048576 }
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
},
|
|
74
|
-
"required": ["nodeId", "secret"]
|
|
8
|
+
"additionalProperties": true
|
|
75
9
|
},
|
|
76
10
|
"uiHints": {
|
|
77
11
|
"secret": { "sensitive": true, "label": "Cluster Secret", "help": "Shared secret for HMAC authentication between nodes" },
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "clawmatrix",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.18",
|
|
4
4
|
"description": "Decentralized mesh cluster plugin for OpenClaw — inter-gateway communication, model proxy, task handoff, and tool proxy.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -32,9 +32,11 @@
|
|
|
32
32
|
"release": "bunx bumpp"
|
|
33
33
|
},
|
|
34
34
|
"dependencies": {
|
|
35
|
+
"@automerge/automerge": "^3.2.4",
|
|
36
|
+
"@mariozechner/pi-coding-agent": ">=0.55.0",
|
|
37
|
+
"ignore": "^7.0.5",
|
|
35
38
|
"ws": "^8.19.0",
|
|
36
|
-
"zod": "^4.3.6"
|
|
37
|
-
"@mariozechner/pi-coding-agent": ">=0.55.0"
|
|
39
|
+
"zod": "^4.3.6"
|
|
38
40
|
},
|
|
39
41
|
"devDependencies": {
|
|
40
42
|
"@types/bun": "latest",
|
package/src/cluster-service.ts
CHANGED
|
@@ -4,6 +4,7 @@ import type {
|
|
|
4
4
|
OpenClawConfig,
|
|
5
5
|
PluginLogger,
|
|
6
6
|
} from "openclaw/plugin-sdk";
|
|
7
|
+
import path from "node:path";
|
|
7
8
|
import type { ClawMatrixConfig } from "./config.ts";
|
|
8
9
|
import { spawnProcess } from "./compat.ts";
|
|
9
10
|
import { debug } from "./debug.ts";
|
|
@@ -12,6 +13,7 @@ import { HandoffManager } from "./handoff.ts";
|
|
|
12
13
|
import { ModelProxy } from "./model-proxy.ts";
|
|
13
14
|
import { ToolProxy, type GatewayInfo } from "./tool-proxy.ts";
|
|
14
15
|
import { WebHandler } from "./web.ts";
|
|
16
|
+
import { KnowledgeSync } from "./knowledge-sync.ts";
|
|
15
17
|
import type {
|
|
16
18
|
AnyClusterFrame,
|
|
17
19
|
HandoffRequest,
|
|
@@ -22,6 +24,7 @@ import type {
|
|
|
22
24
|
HandoffStatusResponse,
|
|
23
25
|
HandoffInputRequired,
|
|
24
26
|
HandoffInput,
|
|
27
|
+
KnowledgeSyncFrame,
|
|
25
28
|
ModelRequest,
|
|
26
29
|
ModelResponse,
|
|
27
30
|
ModelStreamChunk,
|
|
@@ -49,12 +52,15 @@ export class ClusterRuntime {
|
|
|
49
52
|
readonly handoffManager: HandoffManager;
|
|
50
53
|
readonly modelProxy: ModelProxy;
|
|
51
54
|
readonly toolProxy: ToolProxy;
|
|
55
|
+
knowledgeSync: KnowledgeSync | null = null;
|
|
52
56
|
webHandler: WebHandler | null = null;
|
|
53
57
|
private logger: PluginLogger;
|
|
58
|
+
private openclawConfig: OpenClawConfig;
|
|
54
59
|
|
|
55
60
|
constructor(config: ClawMatrixConfig, logger: PluginLogger, openclawConfig: OpenClawConfig, openclawVersion?: string) {
|
|
56
61
|
this.config = config;
|
|
57
62
|
this.logger = logger;
|
|
63
|
+
this.openclawConfig = openclawConfig;
|
|
58
64
|
const gatewayInfo = resolveGatewayInfo(openclawConfig);
|
|
59
65
|
this.peerManager = new PeerManager(config, openclawVersion);
|
|
60
66
|
this.handoffManager = new HandoffManager(config, this.peerManager);
|
|
@@ -85,6 +91,34 @@ export class ClusterRuntime {
|
|
|
85
91
|
this.logger.info(`[clawmatrix] Web dashboard enabled on listen port`);
|
|
86
92
|
}
|
|
87
93
|
|
|
94
|
+
// Knowledge sync
|
|
95
|
+
if (this.config.knowledge?.enabled) {
|
|
96
|
+
const workspacePath = this.resolveWorkspacePath();
|
|
97
|
+
if (workspacePath) {
|
|
98
|
+
const stateDir = path.join(path.dirname(workspacePath), ".clawmatrix");
|
|
99
|
+
this.knowledgeSync = new KnowledgeSync({
|
|
100
|
+
workspacePath,
|
|
101
|
+
storePath: path.join(stateDir, "knowledge.automerge"),
|
|
102
|
+
nodeId: this.config.nodeId,
|
|
103
|
+
debounce: this.config.knowledge.debounce ?? 5000,
|
|
104
|
+
peerManager: this.peerManager,
|
|
105
|
+
});
|
|
106
|
+
this.knowledgeSync.start().then(() => {
|
|
107
|
+
this.logger.info(`[clawmatrix] Knowledge sync started: ${workspacePath}`);
|
|
108
|
+
}).catch((err) => {
|
|
109
|
+
this.logger.error(`[clawmatrix] Knowledge sync failed to start: ${err}`);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
// Sync with peers on connect/disconnect
|
|
113
|
+
this.peerManager.on("peerConnected", (nodeId) => {
|
|
114
|
+
this.knowledgeSync?.initPeerSync(nodeId);
|
|
115
|
+
});
|
|
116
|
+
this.peerManager.on("peerDisconnected", (nodeId) => {
|
|
117
|
+
this.knowledgeSync?.removePeerSync(nodeId);
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
88
122
|
// Start subsystems
|
|
89
123
|
this.peerManager.start();
|
|
90
124
|
this.modelProxy.start();
|
|
@@ -97,6 +131,7 @@ export class ClusterRuntime {
|
|
|
97
131
|
}
|
|
98
132
|
|
|
99
133
|
async stop() {
|
|
134
|
+
await this.knowledgeSync?.stop();
|
|
100
135
|
this.webHandler?.destroy();
|
|
101
136
|
this.handoffManager.destroy();
|
|
102
137
|
this.modelProxy.stop();
|
|
@@ -105,6 +140,24 @@ export class ClusterRuntime {
|
|
|
105
140
|
this.logger.info(`[clawmatrix] Node "${this.config.nodeId}" stopped`);
|
|
106
141
|
}
|
|
107
142
|
|
|
143
|
+
private resolveWorkspacePath(): string | null {
|
|
144
|
+
// Read workspace from OpenClaw agent config (first agent or default agent)
|
|
145
|
+
const agents = (this.openclawConfig as Record<string, unknown>).agents as
|
|
146
|
+
| { list?: Array<{ id?: string; default?: boolean; workspace?: string }>; defaults?: { workspace?: string } }
|
|
147
|
+
| undefined;
|
|
148
|
+
if (!agents) return null;
|
|
149
|
+
const list = agents.list ?? [];
|
|
150
|
+
const defaultAgent = list.find((a) => a.default) ?? list[0];
|
|
151
|
+
const workspace = defaultAgent?.workspace ?? agents.defaults?.workspace;
|
|
152
|
+
if (!workspace) return null;
|
|
153
|
+
// Resolve ~ to home directory
|
|
154
|
+
if (workspace.startsWith("~/") || workspace === "~") {
|
|
155
|
+
const home = process.env.HOME ?? process.env.USERPROFILE ?? "";
|
|
156
|
+
return path.resolve(home, workspace.slice(2));
|
|
157
|
+
}
|
|
158
|
+
return path.resolve(workspace);
|
|
159
|
+
}
|
|
160
|
+
|
|
108
161
|
private dispatchFrame(frame: AnyClusterFrame) {
|
|
109
162
|
if (frame.type.startsWith("model_")) {
|
|
110
163
|
debug("dispatch", `${frame.type} id=${frame.id} from=${frame.from}`);
|
|
@@ -161,6 +214,11 @@ export class ClusterRuntime {
|
|
|
161
214
|
case "send":
|
|
162
215
|
this.handleSendMessage(frame as SendMessage);
|
|
163
216
|
break;
|
|
217
|
+
case "knowledge_sync":
|
|
218
|
+
this.knowledgeSync?.handleSyncMessage(frame as KnowledgeSyncFrame).catch((err) => {
|
|
219
|
+
this.logger.error(`[clawmatrix] Knowledge sync error: ${err}`);
|
|
220
|
+
});
|
|
221
|
+
break;
|
|
164
222
|
}
|
|
165
223
|
}
|
|
166
224
|
|
package/src/config.ts
CHANGED
|
@@ -77,6 +77,11 @@ const ProxyModelGroupSchema = z.object({
|
|
|
77
77
|
models: z.array(ProxyModelEntrySchema),
|
|
78
78
|
});
|
|
79
79
|
|
|
80
|
+
const KnowledgeConfigSchema = z.object({
|
|
81
|
+
enabled: z.boolean().default(false),
|
|
82
|
+
debounce: z.number().default(5000),
|
|
83
|
+
}).optional();
|
|
84
|
+
|
|
80
85
|
const WebConfigSchema = z.object({
|
|
81
86
|
enabled: z.boolean().default(false),
|
|
82
87
|
token: z.string().min(8, "web token must be at least 8 characters"),
|
|
@@ -97,6 +102,7 @@ const RawClawMatrixConfigSchema = z.object({
|
|
|
97
102
|
toolProxy: ToolProxyConfigSchema.optional(),
|
|
98
103
|
handoffTimeout: z.number().default(600_000),
|
|
99
104
|
web: WebConfigSchema,
|
|
105
|
+
knowledge: KnowledgeConfigSchema,
|
|
100
106
|
});
|
|
101
107
|
|
|
102
108
|
/** Flat proxy model after group expansion (used internally). */
|
|
@@ -122,9 +128,15 @@ export { RawClawMatrixConfigSchema as ClawMatrixConfigSchema };
|
|
|
122
128
|
export type PeerConfig = z.infer<typeof PeerConfigSchema>;
|
|
123
129
|
export type ToolProxyConfig = z.infer<typeof ToolProxyConfigSchema>;
|
|
124
130
|
|
|
125
|
-
/** Parse and flatten grouped proxyModels into flat array.
|
|
131
|
+
/** Parse and flatten grouped proxyModels into flat array.
|
|
132
|
+
* Uses passthrough() to ignore unknown fields and safeParse to avoid crashing. */
|
|
126
133
|
export function parseConfig(raw: unknown): ClawMatrixConfig {
|
|
127
|
-
const
|
|
134
|
+
const result = RawClawMatrixConfigSchema.passthrough().safeParse(raw);
|
|
135
|
+
if (!result.success) {
|
|
136
|
+
const issues = result.error.issues.map((i) => `${i.path.join(".")}: ${i.message}`).join("; ");
|
|
137
|
+
throw new Error(`ClawMatrix config error: ${issues}`);
|
|
138
|
+
}
|
|
139
|
+
const parsed = result.data;
|
|
128
140
|
|
|
129
141
|
// Flatten proxy model groups
|
|
130
142
|
const proxyModels: ProxyModel[] = [];
|
package/src/index.ts
CHANGED
|
@@ -21,24 +21,20 @@ const plugin = {
|
|
|
21
21
|
|
|
22
22
|
configSchema: {
|
|
23
23
|
safeParse(value: unknown) {
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
return { success: true, data: result.data };
|
|
27
|
-
}
|
|
28
|
-
return {
|
|
29
|
-
success: false,
|
|
30
|
-
error: {
|
|
31
|
-
issues: result.error.issues.map((i) => ({
|
|
32
|
-
path: i.path.map(String),
|
|
33
|
-
message: i.message,
|
|
34
|
-
})),
|
|
35
|
-
},
|
|
36
|
-
};
|
|
24
|
+
// Always pass — validation is handled internally with warnings
|
|
25
|
+
return { success: true, data: value };
|
|
37
26
|
},
|
|
38
27
|
},
|
|
39
28
|
|
|
40
29
|
register(api: OpenClawPluginApi) {
|
|
41
|
-
|
|
30
|
+
let config: ReturnType<typeof parseConfig>;
|
|
31
|
+
try {
|
|
32
|
+
config = parseConfig(api.pluginConfig);
|
|
33
|
+
} catch (err) {
|
|
34
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
35
|
+
console.warn(`[clawmatrix] Config error (plugin disabled): ${message}`);
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
42
38
|
|
|
43
39
|
// Background service: manages mesh connections, WS listener, heartbeat
|
|
44
40
|
api.registerService(createClusterService(config, api.config, api.runtime.version));
|
|
@@ -164,70 +160,63 @@ const plugin = {
|
|
|
164
160
|
// CLI subcommand
|
|
165
161
|
api.registerCli(registerClusterCli, { commands: ["clawmatrix"] });
|
|
166
162
|
|
|
167
|
-
// Inject cluster context into agent prompts
|
|
163
|
+
// Inject cluster context into agent prompts.
|
|
164
|
+
//
|
|
165
|
+
// Minimal-injection strategy:
|
|
166
|
+
// prependSystemContext (cached) = static identity + guidance + peer count.
|
|
167
|
+
// Content is stable across turns → prompt caching works.
|
|
168
|
+
// Peer count is included so the agent knows the cluster exists;
|
|
169
|
+
// detailed topology is on-demand via cluster_peers tool.
|
|
170
|
+
// prependContext (per-turn) = only pending event notifications.
|
|
171
|
+
// Events need proactive push so the agent can react without being asked.
|
|
172
|
+
// Everything else (peer details, satellites) is pull-based via tools.
|
|
173
|
+
|
|
174
|
+
let cachedPeerCount = -1;
|
|
175
|
+
let cachedSystemContext = "";
|
|
176
|
+
|
|
168
177
|
api.on("before_prompt_build", () => {
|
|
169
178
|
try {
|
|
170
179
|
const runtime = getClusterRuntime();
|
|
171
|
-
const
|
|
172
|
-
if (peers.length === 0) return;
|
|
173
|
-
|
|
174
|
-
const lines = [
|
|
175
|
-
`[ClawMatrix Cluster]`,
|
|
176
|
-
`You are on node "${config.nodeId}"${config.tags.length ? ` (tags: ${config.tags.join(", ")})` : ""}.`,
|
|
177
|
-
];
|
|
180
|
+
const peerCount = runtime.peerManager.router.getAllPeers().length;
|
|
178
181
|
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
for (const agent of peer.agents) {
|
|
194
|
-
lines.push(` agent "${agent.id}": ${agent.description}`);
|
|
195
|
-
}
|
|
196
|
-
if (peer.models.length > 0) {
|
|
197
|
-
lines.push(` models: ${peer.models.map((m) => m.id).join(", ")}`);
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
for (const sat of activeSatellites) {
|
|
201
|
-
const age = Math.floor((Date.now() - sat.ts) / 1000);
|
|
202
|
-
const country = sat.country ? `, ${sat.country}` : "";
|
|
203
|
-
lines.push(` - ${sat.nodeId} (satellite${country}, ${age}s ago)`);
|
|
204
|
-
if (sat.tools?.length) {
|
|
205
|
-
lines.push(` tools: ${sat.tools.join(", ")}`);
|
|
182
|
+
// Rebuild system context only when peer count changes
|
|
183
|
+
if (peerCount !== cachedPeerCount) {
|
|
184
|
+
cachedPeerCount = peerCount;
|
|
185
|
+
const lines: string[] = [];
|
|
186
|
+
if (peerCount === 0) {
|
|
187
|
+
lines.push("[ClawMatrix] No peers online. Use cluster_peers to check cluster status.");
|
|
188
|
+
} else {
|
|
189
|
+
lines.push(
|
|
190
|
+
`[ClawMatrix Cluster] node="${config.nodeId}"${config.tags.length ? ` tags=${config.tags.join(",")}` : ""}`,
|
|
191
|
+
...(config.agents.length > 0 ? [`Role: ${config.agents[0]!.description}`] : []),
|
|
192
|
+
`${peerCount} remote peer(s) online. Use cluster_peers to see topology, agents, and models.`,
|
|
193
|
+
"Prefer cluster_exec/read/write for simple ops; cluster_handoff for complex multi-step tasks.",
|
|
194
|
+
"IMPORTANT: Always tell user which remote node you're targeting before calling cluster tools.",
|
|
195
|
+
);
|
|
206
196
|
}
|
|
197
|
+
cachedSystemContext = lines.join("\n");
|
|
207
198
|
}
|
|
208
199
|
|
|
209
|
-
//
|
|
200
|
+
// Per-turn: only push pending events (agent must react proactively)
|
|
210
201
|
const pendingEvents = runtime.webHandler?.getUnconsumedEvents(5) ?? [];
|
|
202
|
+
let prependContext: string | undefined;
|
|
211
203
|
if (pendingEvents.length > 0) {
|
|
212
|
-
|
|
204
|
+
const evtLines = ["Pending events (use cluster_events to query details or consume):"];
|
|
213
205
|
for (const evt of pendingEvents) {
|
|
214
206
|
const age = Math.floor((Date.now() - evt.ts) / 1000);
|
|
215
207
|
const dataStr = Object.entries(evt.data)
|
|
216
|
-
.map(([k, v]) => `${k}
|
|
217
|
-
.join(",
|
|
208
|
+
.map(([k, v]) => `${k}:${typeof v === "string" ? v : JSON.stringify(v)}`)
|
|
209
|
+
.join(",");
|
|
218
210
|
const truncated = dataStr.length > 120 ? dataStr.slice(0, 120) + "…" : dataStr;
|
|
219
|
-
|
|
211
|
+
evtLines.push(` [${evt.type}] ${evt.source} (${age}s,id:${evt.id}): ${truncated}`);
|
|
220
212
|
}
|
|
221
|
-
|
|
213
|
+
prependContext = evtLines.join("\n");
|
|
222
214
|
}
|
|
223
215
|
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
);
|
|
229
|
-
|
|
230
|
-
return { prependSystemContext: lines.join("\n") };
|
|
216
|
+
return {
|
|
217
|
+
prependSystemContext: cachedSystemContext,
|
|
218
|
+
...(prependContext ? { prependContext } : {}),
|
|
219
|
+
};
|
|
231
220
|
} catch {
|
|
232
221
|
return;
|
|
233
222
|
}
|