clawmatrix 0.2.11 → 0.4.0
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/LICENSE +27 -0
- package/README.md +123 -12
- package/cli/bin/clawmatrix.mjs +1006 -0
- package/cli/package.json +27 -0
- package/cli/skills/clawmatrix/SKILL.md +104 -0
- package/openclaw.plugin.json +1 -0
- package/package.json +3 -1
- package/src/acp-proxy.ts +820 -96
- package/src/cluster-service.ts +186 -16
- package/src/compat.ts +0 -6
- package/src/config.ts +8 -5
- package/src/connection.ts +61 -55
- package/src/e2e/helpers.ts +1 -5
- package/src/file-transfer.ts +64 -14
- package/src/handoff.ts +21 -8
- package/src/health-tracker.ts +40 -11
- package/src/index.ts +686 -14
- package/src/knowledge-sync.ts +62 -10
- package/src/model-proxy.ts +40 -10
- package/src/peer-manager.ts +114 -17
- package/src/rate-limiter.ts +16 -10
- package/src/router.ts +115 -33
- package/src/sentinel-manager.ts +51 -0
- package/src/sentinel.ts +13 -3
- package/src/tool-proxy.ts +52 -6
- package/src/tools/cluster-diagnostic.ts +3 -2
- package/src/tools/cluster-edit.ts +2 -1
- package/src/tools/cluster-events.ts +3 -1
- package/src/tools/cluster-exec.ts +2 -0
- package/src/tools/cluster-handoff.ts +3 -1
- package/src/tools/cluster-notify.ts +132 -0
- package/src/tools/cluster-peers.ts +3 -1
- package/src/tools/cluster-read.ts +4 -1
- package/src/tools/cluster-send.ts +2 -1
- package/src/tools/cluster-terminal.ts +4 -7
- package/src/tools/cluster-tool.ts +2 -2
- package/src/tools/cluster-write.ts +3 -1
- package/src/types.ts +103 -1
- package/src/web.ts +2 -10
- package/src/cli.ts +0 -243
- package/src/web-ui.ts +0 -1622
package/src/handoff.ts
CHANGED
|
@@ -54,16 +54,33 @@ export class HandoffManager {
|
|
|
54
54
|
// Multi-device sync: track which nodes are watching each handoff session (by sessionId)
|
|
55
55
|
private sessionWatchers = new Map<string, Set<string>>();
|
|
56
56
|
|
|
57
|
+
// Pre-built indexes for O(1) agent lookup
|
|
58
|
+
private agentById = new Map<string, ClawMatrixConfig["agents"][number]>();
|
|
59
|
+
private agentsByTag = new Map<string, ClawMatrixConfig["agents"][number]>();
|
|
60
|
+
|
|
57
61
|
constructor(config: ClawMatrixConfig, peerManager: PeerManager, gatewayInfo: GatewayInfo) {
|
|
58
62
|
this.config = config;
|
|
59
63
|
this.peerManager = peerManager;
|
|
60
64
|
this.gatewayInfo = gatewayInfo;
|
|
61
65
|
this.taskActivity = new TaskActivityBroadcaster(config, peerManager);
|
|
62
66
|
|
|
67
|
+
// Build agent indexes
|
|
68
|
+
for (const a of config.agents) {
|
|
69
|
+
this.agentById.set(a.id, a);
|
|
70
|
+
for (const t of a.tags) this.agentsByTag.set(t, a);
|
|
71
|
+
}
|
|
72
|
+
|
|
63
73
|
// Periodically clean up stale input_required entries
|
|
64
74
|
this.staleCleanupTimer = setInterval(() => this.cleanupStale(), STALE_CLEANUP_INTERVAL);
|
|
65
75
|
}
|
|
66
76
|
|
|
77
|
+
private findLocalAgent(target: string): ClawMatrixConfig["agents"][number] | undefined {
|
|
78
|
+
if (target.startsWith("tags:")) {
|
|
79
|
+
return this.agentsByTag.get(target.slice(5));
|
|
80
|
+
}
|
|
81
|
+
return this.agentById.get(target);
|
|
82
|
+
}
|
|
83
|
+
|
|
67
84
|
// ── Multi-device sync helpers ──────────────────────────────────
|
|
68
85
|
|
|
69
86
|
private addSessionWatcher(sessionId: string, nodeId: string) {
|
|
@@ -271,13 +288,7 @@ export class HandoffManager {
|
|
|
271
288
|
const { id, from, payload } = frame;
|
|
272
289
|
|
|
273
290
|
// Find matching local agent
|
|
274
|
-
const agent = this.
|
|
275
|
-
if (payload.target.startsWith("tags:")) {
|
|
276
|
-
const tag = payload.target.slice(5);
|
|
277
|
-
return a.tags.includes(tag);
|
|
278
|
-
}
|
|
279
|
-
return a.id === payload.target;
|
|
280
|
-
});
|
|
291
|
+
const agent = this.findLocalAgent(payload.target);
|
|
281
292
|
|
|
282
293
|
if (!agent) {
|
|
283
294
|
this.peerManager.sendTo(from, {
|
|
@@ -477,6 +488,8 @@ export class HandoffManager {
|
|
|
477
488
|
const decoder = new TextDecoder();
|
|
478
489
|
const chunks: string[] = [];
|
|
479
490
|
let buffer = "";
|
|
491
|
+
// Cache active entry lookup outside the hot loop — stable during the stream
|
|
492
|
+
const activeEntry = this.active.get(handoffId);
|
|
480
493
|
|
|
481
494
|
try {
|
|
482
495
|
while (true) {
|
|
@@ -510,7 +523,6 @@ export class HandoffManager {
|
|
|
510
523
|
|
|
511
524
|
// Broadcast progress to mobile nodes (throttled, detail is just
|
|
512
525
|
// a heartbeat — don't send token-level deltas as they're meaningless fragments)
|
|
513
|
-
const activeEntry = this.active.get(handoffId);
|
|
514
526
|
if (activeEntry) {
|
|
515
527
|
this.taskActivity.broadcast(
|
|
516
528
|
handoffId, "handoff", "progress", activeEntry.agent, activeEntry.startedAt,
|
|
@@ -605,6 +617,7 @@ export class HandoffManager {
|
|
|
605
617
|
|
|
606
618
|
// If canceled during input_required, no runAgentTurn is running to clean up
|
|
607
619
|
if (wasInputRequired) {
|
|
620
|
+
this.sessionWatchers.delete(entry.sessionId);
|
|
608
621
|
this.active.delete(frame.id);
|
|
609
622
|
}
|
|
610
623
|
|
package/src/health-tracker.ts
CHANGED
|
@@ -257,9 +257,18 @@ export class HealthTracker {
|
|
|
257
257
|
// Build timeline for each node (including self)
|
|
258
258
|
const nodes: NodeTimeline[] = [];
|
|
259
259
|
|
|
260
|
+
// Collect nodeIds that have ever had peer_online events (actually connected)
|
|
261
|
+
const everConnected = new Set<string>();
|
|
262
|
+
for (const [, entry] of Object.entries(this.doc.nodes)) {
|
|
263
|
+
for (const ev of entry.events) {
|
|
264
|
+
if (ev.type === "peer_online" && ev.peer) everConnected.add(ev.peer);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
260
268
|
for (const [nodeId, entry] of Object.entries(this.doc.nodes)) {
|
|
261
|
-
//
|
|
262
|
-
|
|
269
|
+
// Remote nodes: must have successfully connected at some point
|
|
270
|
+
if (nodeId !== this.nodeId && !everConnected.has(nodeId)) continue;
|
|
271
|
+
|
|
263
272
|
const timeline = this.buildNodeTimeline(
|
|
264
273
|
nodeId,
|
|
265
274
|
entry,
|
|
@@ -269,7 +278,10 @@ export class HealthTracker {
|
|
|
269
278
|
bucketCount,
|
|
270
279
|
gaps,
|
|
271
280
|
);
|
|
272
|
-
if (timeline)
|
|
281
|
+
if (!timeline) continue;
|
|
282
|
+
// Hide remote nodes that were offline for the entire requested range
|
|
283
|
+
if (nodeId !== this.nodeId && timeline.uptimeRatio === 0) continue;
|
|
284
|
+
nodes.push(timeline);
|
|
273
285
|
}
|
|
274
286
|
|
|
275
287
|
return {
|
|
@@ -345,19 +357,36 @@ export class HealthTracker {
|
|
|
345
357
|
startTs: number,
|
|
346
358
|
endTs: number,
|
|
347
359
|
): Array<[number, number]> {
|
|
348
|
-
const intervals: Array<[number, number]> = [];
|
|
349
|
-
|
|
350
360
|
if (nodeId === this.nodeId) {
|
|
351
361
|
// Self: start/stop events define uptime
|
|
352
|
-
// But we're looking at all nodes' data, so check if this nodeId
|
|
353
|
-
// has start/stop events (each node writes its own start/stop)
|
|
354
362
|
return this.buildSelfIntervals(events, startTs, endTs);
|
|
355
363
|
}
|
|
356
364
|
|
|
357
|
-
// For remote nodes:
|
|
358
|
-
//
|
|
359
|
-
//
|
|
360
|
-
|
|
365
|
+
// For remote nodes: use BOTH self-reported start/stop intervals AND
|
|
366
|
+
// peer_online/peer_offline observations, then merge for best accuracy.
|
|
367
|
+
// Self-reported intervals are the primary signal (the node knows when
|
|
368
|
+
// it was running); peer observations supplement for relay peers or
|
|
369
|
+
// when CRDT sync hasn't propagated the remote node's own events.
|
|
370
|
+
const selfIntervals = this.buildSelfIntervals(events, startTs, endTs);
|
|
371
|
+
const peerIntervals = this.buildPeerIntervals(nodeId, startTs, endTs);
|
|
372
|
+
return this.mergeIntervals([...selfIntervals, ...peerIntervals]);
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
/** Merge overlapping intervals into a sorted, non-overlapping set. */
|
|
376
|
+
private mergeIntervals(intervals: Array<[number, number]>): Array<[number, number]> {
|
|
377
|
+
if (intervals.length <= 1) return intervals;
|
|
378
|
+
intervals.sort((a, b) => a[0] - b[0]);
|
|
379
|
+
const merged: Array<[number, number]> = [intervals[0]!];
|
|
380
|
+
for (let i = 1; i < intervals.length; i++) {
|
|
381
|
+
const prev = merged[merged.length - 1]!;
|
|
382
|
+
const cur = intervals[i]!;
|
|
383
|
+
if (cur[0] <= prev[1]) {
|
|
384
|
+
prev[1] = Math.max(prev[1], cur[1]);
|
|
385
|
+
} else {
|
|
386
|
+
merged.push(cur);
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
return merged;
|
|
361
390
|
}
|
|
362
391
|
|
|
363
392
|
private buildSelfIntervals(
|