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.
@@ -5,73 +5,7 @@
5
5
  "providers": ["clawmatrix"],
6
6
  "configSchema": {
7
7
  "type": "object",
8
- "properties": {
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.16",
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",
@@ -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 parsed = RawClawMatrixConfigSchema.parse(raw);
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
- const result = ClawMatrixConfigSchema.safeParse(value);
25
- if (result.success) {
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
- const config = parseConfig(api.pluginConfig);
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 peers = runtime.peerManager.router.getAllPeers();
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
- if (config.agents.length > 0) {
180
- const localAgent = config.agents[0]!;
181
- lines.push(`Your role: ${localAgent.description}`);
182
- }
183
-
184
- // Satellite nodes (via WebHandler on relay, or gossiped via peer_sync)
185
- const satellites = runtime.webHandler?.getSatelliteContexts() ?? runtime.peerManager.satelliteContexts;
186
- const activeSatellites = satellites.filter(s => Date.now() - s.ts < 600_000);
187
-
188
- lines.push("", "Remote nodes in the cluster:");
189
- for (const peer of peers) {
190
- const status = peer.connection?.isOpen ? "connected" : "via relay";
191
- const tags = peer.tags.length ? ` [${peer.tags.join(", ")}]` : "";
192
- lines.push(` - ${peer.nodeId} (${status})${tags}`);
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
- // Unconsumed events from external sources (Shortcuts automations, etc.)
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
- lines.push("", "Pending events:");
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}: ${typeof v === "string" ? v : JSON.stringify(v)}`)
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
- lines.push(` - [${evt.type}] ${evt.source} (${age}s ago, id:${evt.id}): ${truncated}`);
211
+ evtLines.push(` [${evt.type}] ${evt.source} (${age}s,id:${evt.id}): ${truncated}`);
220
212
  }
221
- lines.push("Use cluster_events for details or to mark consumed.");
213
+ prependContext = evtLines.join("\n");
222
214
  }
223
215
 
224
- lines.push(
225
- "",
226
- "Prefer cluster_exec/read/write for simple ops; cluster_handoff for complex multi-step tasks.",
227
- "IMPORTANT: Always tell user which remote node you're targeting before calling cluster tools.",
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
  }