clawmatrix 0.1.16 → 0.1.17

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clawmatrix",
3
- "version": "0.1.16",
3
+ "version": "0.1.17",
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",
package/src/index.ts CHANGED
@@ -164,70 +164,63 @@ const plugin = {
164
164
  // CLI subcommand
165
165
  api.registerCli(registerClusterCli, { commands: ["clawmatrix"] });
166
166
 
167
- // Inject cluster context into agent prompts
167
+ // Inject cluster context into agent prompts.
168
+ //
169
+ // Minimal-injection strategy:
170
+ // prependSystemContext (cached) = static identity + guidance + peer count.
171
+ // Content is stable across turns → prompt caching works.
172
+ // Peer count is included so the agent knows the cluster exists;
173
+ // detailed topology is on-demand via cluster_peers tool.
174
+ // prependContext (per-turn) = only pending event notifications.
175
+ // Events need proactive push so the agent can react without being asked.
176
+ // Everything else (peer details, satellites) is pull-based via tools.
177
+
178
+ let cachedPeerCount = -1;
179
+ let cachedSystemContext = "";
180
+
168
181
  api.on("before_prompt_build", () => {
169
182
  try {
170
183
  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
- ];
178
-
179
- if (config.agents.length > 0) {
180
- const localAgent = config.agents[0]!;
181
- lines.push(`Your role: ${localAgent.description}`);
182
- }
184
+ const peerCount = runtime.peerManager.router.getAllPeers().length;
183
185
 
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(", ")}`);
186
+ // Rebuild system context only when peer count changes
187
+ if (peerCount !== cachedPeerCount) {
188
+ cachedPeerCount = peerCount;
189
+ const lines: string[] = [];
190
+ if (peerCount === 0) {
191
+ lines.push("[ClawMatrix] No peers online. Use cluster_peers to check cluster status.");
192
+ } else {
193
+ lines.push(
194
+ `[ClawMatrix Cluster] node="${config.nodeId}"${config.tags.length ? ` tags=${config.tags.join(",")}` : ""}`,
195
+ ...(config.agents.length > 0 ? [`Role: ${config.agents[0]!.description}`] : []),
196
+ `${peerCount} remote peer(s) online. Use cluster_peers to see topology, agents, and models.`,
197
+ "Prefer cluster_exec/read/write for simple ops; cluster_handoff for complex multi-step tasks.",
198
+ "IMPORTANT: Always tell user which remote node you're targeting before calling cluster tools.",
199
+ );
206
200
  }
201
+ cachedSystemContext = lines.join("\n");
207
202
  }
208
203
 
209
- // Unconsumed events from external sources (Shortcuts automations, etc.)
204
+ // Per-turn: only push pending events (agent must react proactively)
210
205
  const pendingEvents = runtime.webHandler?.getUnconsumedEvents(5) ?? [];
206
+ let prependContext: string | undefined;
211
207
  if (pendingEvents.length > 0) {
212
- lines.push("", "Pending events:");
208
+ const evtLines = ["Pending events (use cluster_events to query details or consume):"];
213
209
  for (const evt of pendingEvents) {
214
210
  const age = Math.floor((Date.now() - evt.ts) / 1000);
215
211
  const dataStr = Object.entries(evt.data)
216
- .map(([k, v]) => `${k}: ${typeof v === "string" ? v : JSON.stringify(v)}`)
217
- .join(", ");
212
+ .map(([k, v]) => `${k}:${typeof v === "string" ? v : JSON.stringify(v)}`)
213
+ .join(",");
218
214
  const truncated = dataStr.length > 120 ? dataStr.slice(0, 120) + "…" : dataStr;
219
- lines.push(` - [${evt.type}] ${evt.source} (${age}s ago, id:${evt.id}): ${truncated}`);
215
+ evtLines.push(` [${evt.type}] ${evt.source} (${age}s,id:${evt.id}): ${truncated}`);
220
216
  }
221
- lines.push("Use cluster_events for details or to mark consumed.");
217
+ prependContext = evtLines.join("\n");
222
218
  }
223
219
 
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") };
220
+ return {
221
+ prependSystemContext: cachedSystemContext,
222
+ ...(prependContext ? { prependContext } : {}),
223
+ };
231
224
  } catch {
232
225
  return;
233
226
  }
@@ -13,32 +13,32 @@ export function createClusterEventsTool(): AnyAgentTool {
13
13
  action: {
14
14
  type: "string",
15
15
  enum: ["query", "consume"],
16
- description: '"query" to list events, "consume" to mark events as processed',
16
+ description: '"query" to list events (type/source/unconsumed/since/limit filters apply), "consume" to mark as processed (requires ids)',
17
17
  },
18
18
  type: {
19
19
  type: "string",
20
- description: 'Filter by event type (e.g. "message_received", "call_missed"). Only for action=query.',
20
+ description: 'Filter by event type (e.g. "message_received")',
21
21
  },
22
22
  source: {
23
23
  type: "string",
24
- description: 'Filter by source (e.g. "shortcuts", "iphone"). Only for action=query.',
24
+ description: 'Filter by source (e.g. "shortcuts")',
25
25
  },
26
26
  unconsumed: {
27
27
  type: "boolean",
28
- description: "Only return unconsumed events. Default true. Only for action=query.",
28
+ description: "Only unconsumed events (default true)",
29
29
  },
30
30
  since: {
31
31
  type: "number",
32
- description: "Only return events after this unix timestamp (ms). Only for action=query.",
32
+ description: "Events after this unix timestamp (ms)",
33
33
  },
34
34
  limit: {
35
35
  type: "number",
36
- description: "Max events to return. Default 20. Only for action=query.",
36
+ description: "Max events to return (default 20)",
37
37
  },
38
38
  ids: {
39
39
  type: "array",
40
40
  items: { type: "string" },
41
- description: "Event IDs to mark as consumed. Required for action=consume.",
41
+ description: "Event IDs to consume",
42
42
  },
43
43
  },
44
44
  required: ["action"],
@@ -105,7 +105,7 @@ export function createClusterEventsTool(): AnyAgentTool {
105
105
  }));
106
106
 
107
107
  return {
108
- content: [{ type: "text" as const, text: JSON.stringify(summary, null, 2) }],
108
+ content: [{ type: "text" as const, text: JSON.stringify(summary) }],
109
109
  details: { events: summary },
110
110
  };
111
111
  } catch (err) {
@@ -53,7 +53,7 @@ export function createClusterExecTool(): AnyAgentTool {
53
53
  content: [
54
54
  {
55
55
  type: "text" as const,
56
- text: JSON.stringify(result, null, 2),
56
+ text: JSON.stringify(result),
57
57
  },
58
58
  ],
59
59
  details: result,
@@ -34,7 +34,7 @@ export function createClusterHandoffReplyTool(): AnyAgentTool {
34
34
  content: [
35
35
  {
36
36
  type: "text" as const,
37
- text: `Remote agent needs more information.\n\nHandoff ID: ${result.handoffId}\nQuestion: ${result.result}\n\nUse cluster_handoff_reply again to respond.`,
37
+ text: `Input required (handoff_id:${result.handoffId}): ${result.result}`,
38
38
  },
39
39
  ],
40
40
  details: result,
@@ -52,11 +52,7 @@ export function createClusterHandoffReplyTool(): AnyAgentTool {
52
52
  content: [
53
53
  {
54
54
  type: "text" as const,
55
- text: JSON.stringify(
56
- { nodeId: result.nodeId, agent: result.agent, result: result.result },
57
- null,
58
- 2,
59
- ),
55
+ text: JSON.stringify({ nodeId: result.nodeId, agent: result.agent, result: result.result }),
60
56
  },
61
57
  ],
62
58
  details: result,
@@ -42,7 +42,7 @@ export function createClusterHandoffTool(): AnyAgentTool {
42
42
  content: [
43
43
  {
44
44
  type: "text" as const,
45
- text: `Remote agent needs more information before continuing.\n\nHandoff ID: ${result.handoffId}\nQuestion: ${result.result}\n\nUse cluster_handoff_reply tool to respond.`,
45
+ text: `Input required (handoff_id:${result.handoffId}): ${result.result}`,
46
46
  },
47
47
  ],
48
48
  details: result,
@@ -65,15 +65,7 @@ export function createClusterHandoffTool(): AnyAgentTool {
65
65
  content: [
66
66
  {
67
67
  type: "text" as const,
68
- text: JSON.stringify(
69
- {
70
- nodeId: result.nodeId,
71
- agent: result.agent,
72
- result: result.result,
73
- },
74
- null,
75
- 2,
76
- ),
68
+ text: JSON.stringify({ nodeId: result.nodeId, agent: result.agent, result: result.result }),
77
69
  },
78
70
  ],
79
71
  details: result,
@@ -21,36 +21,29 @@ export function createClusterPeersTool(): AnyAgentTool {
21
21
  description: a.description,
22
22
  tags: a.tags,
23
23
  })),
24
- models: entry.models.map((m) => ({
25
- id: m.id,
26
- provider: m.provider,
27
- })),
24
+ models: entry.models.map((m) => m.id),
28
25
  tags: entry.tags,
29
26
  tools: entry.toolProxy?.enabled ? (entry.toolProxy.allow ?? []) : [],
30
27
  status: entry.connection?.isOpen ? "connected" : "unreachable",
31
28
  latencyMs: entry.latencyMs,
32
29
  }));
33
30
 
34
- // Include satellite nodes
31
+ // Include satellite nodes (minimal fields — no agents/models)
35
32
  const satellites = runtime.webHandler?.getSatelliteContexts() ?? runtime.peerManager.satelliteContexts;
36
33
  for (const sat of satellites) {
37
34
  if (Date.now() - sat.ts >= 600_000) continue;
38
35
  peers.push({
39
36
  nodeId: sat.nodeId,
40
- agents: [],
41
- models: [],
42
- tags: [],
43
37
  tools: sat.tools ?? [],
44
38
  status: "satellite",
45
- latencyMs: undefined,
46
- });
39
+ } as (typeof peers)[number]);
47
40
  }
48
41
 
49
42
  return {
50
43
  content: [
51
44
  {
52
45
  type: "text" as const,
53
- text: JSON.stringify(peers, null, 2),
46
+ text: JSON.stringify(peers),
54
47
  },
55
48
  ],
56
49
  details: peers,
@@ -30,7 +30,7 @@ export function createClusterReadTool(): AnyAgentTool {
30
30
  content: [
31
31
  {
32
32
  type: "text" as const,
33
- text: JSON.stringify(result, null, 2),
33
+ text: JSON.stringify(result),
34
34
  },
35
35
  ],
36
36
  details: result,
@@ -39,7 +39,7 @@ export function createClusterToolTool(): AnyAgentTool {
39
39
  content: [
40
40
  {
41
41
  type: "text" as const,
42
- text: JSON.stringify(result, null, 2),
42
+ text: JSON.stringify(result),
43
43
  },
44
44
  ],
45
45
  details: result,
@@ -42,7 +42,7 @@ export function createClusterWriteTool(): AnyAgentTool {
42
42
  content: [
43
43
  {
44
44
  type: "text" as const,
45
- text: JSON.stringify(result, null, 2),
45
+ text: JSON.stringify(result),
46
46
  },
47
47
  ],
48
48
  details: result,