clawmatrix 0.6.0 → 0.6.2
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/cli/bin/clawmatrix.mjs +78 -2
- package/cli/skills/clawmatrix/SKILL.md +0 -49
- package/package.json +1 -1
- package/src/api.ts +0 -66
- package/src/connection.ts +27 -29
- package/src/health-tracker.ts +67 -9
- package/src/index.ts +0 -285
- package/src/knowledge-sync.ts +7 -1
- package/src/peer-manager.ts +22 -4
- package/src/sentinel.ts +1 -1
package/cli/bin/clawmatrix.mjs
CHANGED
|
@@ -650,6 +650,8 @@ function cmdHelpJson() {
|
|
|
650
650
|
{ name: "peers", args: "", options: [], description: "List known peers (JSON)" },
|
|
651
651
|
{ name: "check", args: "<nodeId>", options: [], description: "Check if a specific node is reachable" },
|
|
652
652
|
{ name: "config", args: "", options: ["--json"], description: "Show local node configuration" },
|
|
653
|
+
{ name: "config read", args: "<path>", options: [], description: "Read a config file under ~/.openclaw/" },
|
|
654
|
+
{ name: "config write", args: "<path> <content|->", options: [], description: "Write a config file under ~/.openclaw/" },
|
|
653
655
|
{ name: "availability", args: "[range]", options: ["--json"], description: "Show node uptime (24h|7d|90d)" },
|
|
654
656
|
// Tools & Execution
|
|
655
657
|
{ name: "tools", args: "[node]", options: ["--json", "-v", "-f <keyword>", "-d <tool>"], description: "List available tools on remote nodes" },
|
|
@@ -677,6 +679,7 @@ function cmdHelpJson() {
|
|
|
677
679
|
{ name: "kb files", args: "", options: ["--json"], description: "List all synced knowledge files" },
|
|
678
680
|
{ name: "kb history", args: "<path>", options: ["--json"], description: "Show change history for a synced file" },
|
|
679
681
|
{ name: "kb blame", args: "<path>", options: ["--json"], description: "Show line-by-line attribution for a synced file" },
|
|
682
|
+
{ name: "kb content", args: "<path>", options: [], description: "Read the content of a synced file" },
|
|
680
683
|
{ name: "board", args: "", options: ["--json"], description: "Kanban board summary" },
|
|
681
684
|
{ name: "board list", args: "", options: ["--json", "--stage <s>", "--label <l>", "--priority <p>", "--node <n>"], description: "List kanban cards" },
|
|
682
685
|
{ name: "board create", args: "<title>", options: ["--desc <text>", "--priority <p>", "--labels <l1,l2>", "--node <n>", "--agent <a>"], description: "Create a new kanban card" },
|
|
@@ -972,7 +975,8 @@ async function cmdKb(args) {
|
|
|
972
975
|
Subcommands:
|
|
973
976
|
files [--json] List all synced files with metadata
|
|
974
977
|
history <path> [--json] Show change history for a synced file
|
|
975
|
-
blame <path> [--json] Show line-by-line attribution for a synced file
|
|
978
|
+
blame <path> [--json] Show line-by-line attribution for a synced file
|
|
979
|
+
content <path> Read the content of a synced file`);
|
|
976
980
|
return;
|
|
977
981
|
}
|
|
978
982
|
|
|
@@ -1043,6 +1047,19 @@ Subcommands:
|
|
|
1043
1047
|
return;
|
|
1044
1048
|
}
|
|
1045
1049
|
|
|
1050
|
+
if (subCmd === "content" || subCmd === "read" || subCmd === "cat") {
|
|
1051
|
+
const filePath = subArgs.filter((a) => !a.startsWith("--"))[0];
|
|
1052
|
+
if (!filePath) { console.error("Usage: clawmatrix kb content <path>"); process.exit(1); }
|
|
1053
|
+
const result = await callGateway("clawmatrix.kb.content", { path: filePath });
|
|
1054
|
+
if (result.content !== undefined) {
|
|
1055
|
+
console.log(result.content);
|
|
1056
|
+
} else {
|
|
1057
|
+
console.error(`Error: ${result.error || "File not found"}`);
|
|
1058
|
+
process.exitCode = 1;
|
|
1059
|
+
}
|
|
1060
|
+
return;
|
|
1061
|
+
}
|
|
1062
|
+
|
|
1046
1063
|
if (subCmd === "blame") {
|
|
1047
1064
|
const filePath = subArgs.filter((a) => !a.startsWith("--"))[0];
|
|
1048
1065
|
if (!filePath) { console.error("Usage: clawmatrix kb blame <path>"); process.exit(1); }
|
|
@@ -1513,6 +1530,65 @@ Subcommands:
|
|
|
1513
1530
|
|
|
1514
1531
|
async function cmdConfig(args) {
|
|
1515
1532
|
const isJson = args.includes("--json");
|
|
1533
|
+
const subCmd = args[0];
|
|
1534
|
+
|
|
1535
|
+
// config read <path> — read a config file under ~/.openclaw/
|
|
1536
|
+
if (subCmd === "read") {
|
|
1537
|
+
const configPath = args[1];
|
|
1538
|
+
if (!configPath) { console.error("Usage: clawmatrix config read <path> (path under ~/.openclaw/)"); process.exit(1); }
|
|
1539
|
+
try {
|
|
1540
|
+
const data = await callGateway("clawmatrix.config.read", { path: configPath });
|
|
1541
|
+
if (data.content !== undefined) {
|
|
1542
|
+
console.log(data.content);
|
|
1543
|
+
} else {
|
|
1544
|
+
console.error(`Error: ${data.error || "Failed to read config"}`);
|
|
1545
|
+
process.exitCode = 1;
|
|
1546
|
+
}
|
|
1547
|
+
} catch (err) {
|
|
1548
|
+
console.error(`Error: ${err.message || String(err)}`);
|
|
1549
|
+
process.exitCode = 1;
|
|
1550
|
+
}
|
|
1551
|
+
return;
|
|
1552
|
+
}
|
|
1553
|
+
|
|
1554
|
+
// config write <path> <content|-> — write a config file under ~/.openclaw/
|
|
1555
|
+
if (subCmd === "write") {
|
|
1556
|
+
const configPath = args[1];
|
|
1557
|
+
let content = args[2];
|
|
1558
|
+
if (!configPath) { console.error("Usage: clawmatrix config write <path> <content|->"); process.exit(1); }
|
|
1559
|
+
if (content === "-" || (!content && !process.stdin.isTTY)) {
|
|
1560
|
+
const chunks = [];
|
|
1561
|
+
for await (const chunk of process.stdin) chunks.push(Buffer.from(chunk));
|
|
1562
|
+
content = Buffer.concat(chunks).toString("utf-8");
|
|
1563
|
+
}
|
|
1564
|
+
if (!content) { console.error("Error: No content provided. Pass content as argument or pipe via stdin."); process.exit(1); }
|
|
1565
|
+
try {
|
|
1566
|
+
const data = await callGateway("clawmatrix.config.write", { path: configPath, content });
|
|
1567
|
+
if (data.success) {
|
|
1568
|
+
console.log(`Written: ${configPath}`);
|
|
1569
|
+
} else {
|
|
1570
|
+
console.error(`Error: ${data.error || "Failed to write config"}`);
|
|
1571
|
+
process.exitCode = 1;
|
|
1572
|
+
}
|
|
1573
|
+
} catch (err) {
|
|
1574
|
+
console.error(`Error: ${err.message || String(err)}`);
|
|
1575
|
+
process.exitCode = 1;
|
|
1576
|
+
}
|
|
1577
|
+
return;
|
|
1578
|
+
}
|
|
1579
|
+
|
|
1580
|
+
if (subCmd === "--help" || subCmd === "-h") {
|
|
1581
|
+
console.log(`Usage: clawmatrix config [subcommand]
|
|
1582
|
+
|
|
1583
|
+
Subcommands:
|
|
1584
|
+
(no args) Show local node configuration summary
|
|
1585
|
+
read <path> Read a config file under ~/.openclaw/
|
|
1586
|
+
write <path> <content|->
|
|
1587
|
+
Write a config file under ~/.openclaw/
|
|
1588
|
+
Options:
|
|
1589
|
+
--json Output as JSON`);
|
|
1590
|
+
return;
|
|
1591
|
+
}
|
|
1516
1592
|
|
|
1517
1593
|
try {
|
|
1518
1594
|
const data = await callGateway("clawmatrix.config.get");
|
|
@@ -1608,7 +1684,7 @@ Cluster:
|
|
|
1608
1684
|
status Show cluster topology and peer status
|
|
1609
1685
|
peers List known peers (JSON)
|
|
1610
1686
|
check <nodeId> Check if a specific node is reachable
|
|
1611
|
-
config
|
|
1687
|
+
config [subcmd] Node config (read|write or show summary)
|
|
1612
1688
|
availability [range] Show node uptime (24h|7d|90d)
|
|
1613
1689
|
|
|
1614
1690
|
Tools & Execution:
|
|
@@ -18,10 +18,7 @@ All commands output LLM-friendly text (no ANSI colors) when stdout is not a TTY.
|
|
|
18
18
|
```bash
|
|
19
19
|
clawmatrix status # Cluster topology: peers, agents, models, tags
|
|
20
20
|
clawmatrix status --json # Structured JSON output
|
|
21
|
-
clawmatrix peers # List known peers (JSON)
|
|
22
21
|
clawmatrix check <nodeId> # Quick reachability check with latency
|
|
23
|
-
clawmatrix config # Show local node configuration
|
|
24
|
-
clawmatrix config --json # Config as JSON
|
|
25
22
|
```
|
|
26
23
|
|
|
27
24
|
Always start with `clawmatrix status` to understand the current topology before performing other operations.
|
|
@@ -58,14 +55,6 @@ clawmatrix models # All cluster models
|
|
|
58
55
|
clawmatrix models --node <nodeId> # Filter by node
|
|
59
56
|
```
|
|
60
57
|
|
|
61
|
-
## Availability
|
|
62
|
-
|
|
63
|
-
```bash
|
|
64
|
-
clawmatrix availability # Node uptime (default: 24h)
|
|
65
|
-
clawmatrix availability 7d # 7-day uptime
|
|
66
|
-
clawmatrix availability 90d --json # 90-day as JSON
|
|
67
|
-
```
|
|
68
|
-
|
|
69
58
|
## Events
|
|
70
59
|
|
|
71
60
|
```bash
|
|
@@ -74,19 +63,6 @@ clawmatrix events --type <type> # Filter by type
|
|
|
74
63
|
clawmatrix events --source <nodeId> # Filter by source node
|
|
75
64
|
clawmatrix events --consume <id1,id2> # Mark events as consumed
|
|
76
65
|
clawmatrix events --all # Include consumed events
|
|
77
|
-
clawmatrix events ingest '[{"type":"x","source":"y","data":{}}]' # Ingest events
|
|
78
|
-
```
|
|
79
|
-
|
|
80
|
-
## Automations
|
|
81
|
-
|
|
82
|
-
```bash
|
|
83
|
-
clawmatrix automations rules # List automation rules
|
|
84
|
-
clawmatrix automations history # Execution history
|
|
85
|
-
clawmatrix automations history --limit 5 # Limit results
|
|
86
|
-
clawmatrix automations run <ruleId> # Manually trigger a rule
|
|
87
|
-
clawmatrix automations replay <execId> # Re-run historical execution
|
|
88
|
-
clawmatrix automations save rules.json # Save rules from JSON file
|
|
89
|
-
cat rules.json | clawmatrix automations save - # Save from stdin
|
|
90
66
|
```
|
|
91
67
|
|
|
92
68
|
## Peer Approval
|
|
@@ -118,31 +94,6 @@ clawmatrix notify "iOS 构建" --action end --task-id <id>
|
|
|
118
94
|
|
|
119
95
|
Options: `--detail <text>`, `--progress <0-100>`, `--action start|update|end`, `--task-id <id>`, `--tool <name>`
|
|
120
96
|
|
|
121
|
-
## Knowledge Sync
|
|
122
|
-
|
|
123
|
-
```bash
|
|
124
|
-
clawmatrix kb files # List synced knowledge files
|
|
125
|
-
clawmatrix kb history <path> # File change history
|
|
126
|
-
clawmatrix kb blame <path> # Line-by-line attribution
|
|
127
|
-
```
|
|
128
|
-
|
|
129
|
-
## Kanban Board
|
|
130
|
-
|
|
131
|
-
```bash
|
|
132
|
-
clawmatrix board # Board summary
|
|
133
|
-
clawmatrix board list # List all cards
|
|
134
|
-
clawmatrix board list --stage in_progress --priority urgent # Filter cards
|
|
135
|
-
clawmatrix board create "Task title" --priority high --labels "bug,p0"
|
|
136
|
-
clawmatrix board get <cardId> # Card details
|
|
137
|
-
clawmatrix board update <cardId> --title "New title" --desc "Updated"
|
|
138
|
-
clawmatrix board claim <cardId> # Claim a card
|
|
139
|
-
clawmatrix board move <cardId> done # Move to stage
|
|
140
|
-
clawmatrix board annotate <cardId> "Progress note" --type progress
|
|
141
|
-
clawmatrix board delete <cardId> # Delete a card
|
|
142
|
-
```
|
|
143
|
-
|
|
144
|
-
Stages: `backlog` → `claimed` → `in_progress` → `review` → `done` → `archived`
|
|
145
|
-
|
|
146
97
|
## Workflow
|
|
147
98
|
|
|
148
99
|
1. Run `clawmatrix status` to see the cluster topology
|
package/package.json
CHANGED
package/src/api.ts
CHANGED
|
@@ -733,32 +733,6 @@ export class ApiHandler {
|
|
|
733
733
|
return resolved;
|
|
734
734
|
}
|
|
735
735
|
|
|
736
|
-
/** Read a config file (for gateway methods). */
|
|
737
|
-
async readConfigFile(configPath: string): Promise<{ success: boolean; content?: string; error?: string }> {
|
|
738
|
-
const resolved = this.resolveConfigPath(configPath);
|
|
739
|
-
if (!resolved) return { success: false, error: "Path must be under ~/.openclaw/" };
|
|
740
|
-
try {
|
|
741
|
-
const content = await readFile(resolved, "utf-8");
|
|
742
|
-
return { success: true, content };
|
|
743
|
-
} catch (err: unknown) {
|
|
744
|
-
if ((err as NodeJS.ErrnoException).code === "ENOENT") return { success: true, content: "{}" };
|
|
745
|
-
return { success: false, error: err instanceof Error ? err.message : "Failed to read config" };
|
|
746
|
-
}
|
|
747
|
-
}
|
|
748
|
-
|
|
749
|
-
/** Write a config file (for gateway methods). */
|
|
750
|
-
async writeConfigFile(configPath: string, content: string): Promise<{ success: boolean; error?: string }> {
|
|
751
|
-
const resolved = this.resolveConfigPath(configPath);
|
|
752
|
-
if (!resolved) return { success: false, error: "Path must be under ~/.openclaw/" };
|
|
753
|
-
try {
|
|
754
|
-
await mkdir(dirname(resolved), { recursive: true });
|
|
755
|
-
await writeFile(resolved, content, "utf-8");
|
|
756
|
-
return { success: true };
|
|
757
|
-
} catch (err) {
|
|
758
|
-
return { success: false, error: err instanceof Error ? err.message : "Failed to write config" };
|
|
759
|
-
}
|
|
760
|
-
}
|
|
761
|
-
|
|
762
736
|
/** GET /api/config/read?path=<configPath> — read a local config file. */
|
|
763
737
|
private async handleConfigRead(req: IncomingMessage, res: ServerResponse) {
|
|
764
738
|
const url = new URL(req.url ?? "/", `http://${req.headers.host ?? "localhost"}`);
|
|
@@ -1609,46 +1583,6 @@ export class ApiHandler {
|
|
|
1609
1583
|
return events.slice(-(opts?.limit ?? 50)).reverse();
|
|
1610
1584
|
}
|
|
1611
1585
|
|
|
1612
|
-
/** Ingest events programmatically (from gateway methods, CLI, etc.). */
|
|
1613
|
-
ingestEvents(items: Array<Record<string, unknown>>): { count: number; ids: string[] } {
|
|
1614
|
-
const ingested: IngestedEvent[] = [];
|
|
1615
|
-
for (const item of items) {
|
|
1616
|
-
if (!item.type) continue;
|
|
1617
|
-
const event: IngestedEvent = {
|
|
1618
|
-
id: nanoid(),
|
|
1619
|
-
source: String(item.source || "unknown"),
|
|
1620
|
-
type: String(item.type),
|
|
1621
|
-
data: (typeof item.data === "object" && item.data !== null ? item.data : { value: item.data ?? item }) as Record<string, unknown>,
|
|
1622
|
-
ts: typeof item.ts === "number" ? Math.min(item.ts as number, Date.now()) : Date.now(),
|
|
1623
|
-
consumed: false,
|
|
1624
|
-
};
|
|
1625
|
-
this.ingestedEvents.push(event);
|
|
1626
|
-
ingested.push(event);
|
|
1627
|
-
try {
|
|
1628
|
-
this.store?.insertIngestedEvent({
|
|
1629
|
-
id: event.id, ts: event.ts, type: event.type, source: event.source,
|
|
1630
|
-
data: JSON.stringify(event.data),
|
|
1631
|
-
});
|
|
1632
|
-
} catch { /* non-fatal */ }
|
|
1633
|
-
}
|
|
1634
|
-
if (this.ingestedEvents.length > MAX_INGESTED_EVENTS) {
|
|
1635
|
-
this.ingestedEvents.splice(0, this.ingestedEvents.length - MAX_INGESTED_EVENTS);
|
|
1636
|
-
}
|
|
1637
|
-
for (const event of ingested) {
|
|
1638
|
-
this.pushSatelliteEvent({
|
|
1639
|
-
ts: event.ts,
|
|
1640
|
-
type: "event_ingested" as SatelliteEvent["type"],
|
|
1641
|
-
data: { id: event.id, source: event.source, type: event.type },
|
|
1642
|
-
});
|
|
1643
|
-
}
|
|
1644
|
-
if (this.automationManager) {
|
|
1645
|
-
for (const event of ingested) {
|
|
1646
|
-
this.automationManager.onEventIngested(event);
|
|
1647
|
-
}
|
|
1648
|
-
}
|
|
1649
|
-
return { count: ingested.length, ids: ingested.map(e => e.id) };
|
|
1650
|
-
}
|
|
1651
|
-
|
|
1652
1586
|
/** Mark events as consumed by id(s). */
|
|
1653
1587
|
consumeEvents(ids: string[]): number {
|
|
1654
1588
|
if (this.store) {
|
package/src/connection.ts
CHANGED
|
@@ -147,7 +147,17 @@ export class Connection extends EventEmitter<ConnectionEvents> {
|
|
|
147
147
|
|
|
148
148
|
/** Bind standard WebSocket event listeners. Call this for outbound connections. */
|
|
149
149
|
bindWebSocket(ws: WebSocket) {
|
|
150
|
-
|
|
150
|
+
// Node.js 22+ built-in WebSocket defaults binaryType to "blob", which
|
|
151
|
+
// onRawMessage cannot handle (Blob is async-only). Set "arraybuffer"
|
|
152
|
+
// so binary frames arrive as ArrayBuffer, then normalize to Buffer below.
|
|
153
|
+
if ("binaryType" in ws) {
|
|
154
|
+
(ws as any).binaryType = "arraybuffer";
|
|
155
|
+
}
|
|
156
|
+
ws.addEventListener("message", (ev) => {
|
|
157
|
+
// Normalize ArrayBuffer → Buffer for consistent handling across runtimes
|
|
158
|
+
const data = ev.data instanceof ArrayBuffer ? Buffer.from(ev.data) : ev.data;
|
|
159
|
+
this.onRawMessage(data);
|
|
160
|
+
});
|
|
151
161
|
ws.addEventListener("close", (ev) => {
|
|
152
162
|
this.close(ev.code, ev.reason);
|
|
153
163
|
});
|
|
@@ -208,11 +218,19 @@ export class Connection extends EventEmitter<ConnectionEvents> {
|
|
|
208
218
|
|
|
209
219
|
/** Send raw data. Buffers sent as binary frames; strings as-is; objects JSON-encoded. */
|
|
210
220
|
private sendRaw(data: unknown) {
|
|
211
|
-
|
|
212
|
-
if (
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
221
|
+
try {
|
|
222
|
+
if (this.transport.readyState === WebSocket.OPEN) {
|
|
223
|
+
if (Buffer.isBuffer(data)) {
|
|
224
|
+
this.transport.send(data);
|
|
225
|
+
} else {
|
|
226
|
+
this.transport.send(typeof data === "string" ? data : JSON.stringify(data));
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
} catch (err) {
|
|
230
|
+
debug("send", `sendRaw failed for ${this.remoteNodeId ?? "unknown"}: ${err}`);
|
|
231
|
+
// Transport is broken — schedule close on next tick to avoid re-entrancy
|
|
232
|
+
if (!this.closed) {
|
|
233
|
+
queueMicrotask(() => this.close(4002, "send failed"));
|
|
216
234
|
}
|
|
217
235
|
}
|
|
218
236
|
}
|
|
@@ -269,25 +287,6 @@ export class Connection extends EventEmitter<ConnectionEvents> {
|
|
|
269
287
|
private async onRawMessage(data: unknown) {
|
|
270
288
|
this.lastReceivedAt = Date.now();
|
|
271
289
|
|
|
272
|
-
// Debug: log data type for unauthenticated connections to diagnose auth issues
|
|
273
|
-
if (!this.authenticated && this.role === "outbound") {
|
|
274
|
-
const dtype = Buffer.isBuffer(data) ? `Buffer(${(data as Buffer).length})` :
|
|
275
|
-
data instanceof ArrayBuffer ? `ArrayBuffer(${data.byteLength})` :
|
|
276
|
-
typeof data === "string" ? `string(${data.length})` :
|
|
277
|
-
`${data?.constructor?.name ?? typeof data}`;
|
|
278
|
-
debug("auth", `onRawMessage[outbound] dataType=${dtype}`);
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
// Normalize non-Buffer binary types to Buffer.
|
|
282
|
-
// Node.js 24+'s built-in WebSocket (undici) delivers binary frames as Blob
|
|
283
|
-
// (binaryType "blob") or ArrayBuffer (binaryType "arraybuffer").
|
|
284
|
-
// The ws package delivers Buffer (binaryType "nodebuffer").
|
|
285
|
-
if (data instanceof ArrayBuffer) {
|
|
286
|
-
data = Buffer.from(data);
|
|
287
|
-
} else if (typeof Blob !== "undefined" && data instanceof Blob) {
|
|
288
|
-
data = Buffer.from(await data.arrayBuffer());
|
|
289
|
-
}
|
|
290
|
-
|
|
291
290
|
let frame: AnyClusterFrame | undefined;
|
|
292
291
|
|
|
293
292
|
// Binary frame (Buffer) — decrypt directly without base64
|
|
@@ -403,7 +402,7 @@ export class Connection extends EventEmitter<ConnectionEvents> {
|
|
|
403
402
|
}
|
|
404
403
|
|
|
405
404
|
if (frame.type === "ping") {
|
|
406
|
-
this.
|
|
405
|
+
this.sendDirect({ type: "pong", from: this.nodeId, timestamp: Date.now() } as AnyClusterFrame);
|
|
407
406
|
return;
|
|
408
407
|
}
|
|
409
408
|
if (frame.type === "pong") {
|
|
@@ -579,7 +578,6 @@ export class Connection extends EventEmitter<ConnectionEvents> {
|
|
|
579
578
|
}
|
|
580
579
|
|
|
581
580
|
// auth_ok (decrypted from binary envelope, or plaintext legacy)
|
|
582
|
-
debug("auth", `Outbound received frame type=${frame.type} (authenticated=${this.authenticated})`);
|
|
583
581
|
if (frame.type === "auth_ok") {
|
|
584
582
|
const ok = frame as AuthOk;
|
|
585
583
|
this.remoteNodeId = ok.payload.nodeId;
|
|
@@ -718,7 +716,7 @@ export class Connection extends EventEmitter<ConnectionEvents> {
|
|
|
718
716
|
return;
|
|
719
717
|
}
|
|
720
718
|
this.lastPingSentAt = Date.now();
|
|
721
|
-
this.
|
|
719
|
+
this.sendDirect({
|
|
722
720
|
type: "ping",
|
|
723
721
|
from: this.nodeId,
|
|
724
722
|
timestamp: this.lastPingSentAt,
|
|
@@ -755,7 +753,7 @@ export class Connection extends EventEmitter<ConnectionEvents> {
|
|
|
755
753
|
close(code = 1000, reason = "normal") {
|
|
756
754
|
if (this.closed) return;
|
|
757
755
|
// Flush any pending batch before closing
|
|
758
|
-
this.flushBatch();
|
|
756
|
+
try { this.flushBatch(); } catch { /* transport may already be dead */ }
|
|
759
757
|
this.closed = true;
|
|
760
758
|
this.clearAuthTimer();
|
|
761
759
|
if (this.heartbeatTimer) {
|
package/src/health-tracker.ts
CHANGED
|
@@ -55,6 +55,10 @@ export class HealthTracker {
|
|
|
55
55
|
private readonly nodeId: string;
|
|
56
56
|
private store: Store | null;
|
|
57
57
|
private logReplicator: LogReplicator | null;
|
|
58
|
+
/** In-memory timestamp of when start() was called (survives store failures). */
|
|
59
|
+
private startedAt: number | null = null;
|
|
60
|
+
/** In-memory tracking of currently connected peers (timestamp of connection). */
|
|
61
|
+
private readonly connectedPeers = new Map<string, number>();
|
|
58
62
|
|
|
59
63
|
constructor(opts: HealthTrackerOptions) {
|
|
60
64
|
this.nodeId = opts.nodeId;
|
|
@@ -66,14 +70,23 @@ export class HealthTracker {
|
|
|
66
70
|
setStore(store: Store, logReplicator?: LogReplicator) {
|
|
67
71
|
this.store = store;
|
|
68
72
|
this.logReplicator = logReplicator ?? this.logReplicator;
|
|
73
|
+
// Flush deferred events that occurred before store was available
|
|
74
|
+
if (this.startedAt !== null) {
|
|
75
|
+
this.recordEvent({ ts: this.startedAt, type: "start" });
|
|
76
|
+
}
|
|
77
|
+
for (const [peerId, ts] of this.connectedPeers) {
|
|
78
|
+
this.recordEvent({ ts, type: "peer_online", peer: peerId, via: "direct" });
|
|
79
|
+
}
|
|
69
80
|
}
|
|
70
81
|
|
|
71
82
|
async start() {
|
|
72
|
-
this.
|
|
83
|
+
this.startedAt = Date.now();
|
|
84
|
+
this.recordEvent({ ts: this.startedAt, type: "start" });
|
|
73
85
|
debug(TAG, `health tracker started for node "${this.nodeId}"`);
|
|
74
86
|
}
|
|
75
87
|
|
|
76
88
|
async stop() {
|
|
89
|
+
this.startedAt = null;
|
|
77
90
|
this.recordEvent({ ts: Date.now(), type: "stop" });
|
|
78
91
|
debug(TAG, "health tracker stopped");
|
|
79
92
|
}
|
|
@@ -98,10 +111,12 @@ export class HealthTracker {
|
|
|
98
111
|
}
|
|
99
112
|
|
|
100
113
|
recordPeerOnline(peerId: string, via: "direct" | "relay") {
|
|
114
|
+
this.connectedPeers.set(peerId, Date.now());
|
|
101
115
|
this.recordEvent({ ts: Date.now(), type: "peer_online", peer: peerId, via });
|
|
102
116
|
}
|
|
103
117
|
|
|
104
118
|
recordPeerOffline(peerId: string, reason?: string) {
|
|
119
|
+
this.connectedPeers.delete(peerId);
|
|
105
120
|
this.recordEvent({ ts: Date.now(), type: "peer_offline", peer: peerId, reason });
|
|
106
121
|
}
|
|
107
122
|
|
|
@@ -143,6 +158,18 @@ export class HealthTracker {
|
|
|
143
158
|
|
|
144
159
|
// Find observation gaps (periods where THIS node was down)
|
|
145
160
|
const selfEvents = this.getEventsForNode(this.nodeId);
|
|
161
|
+
// Inject current in-memory session if not reflected in persisted events.
|
|
162
|
+
// This handles the case where the store wasn't ready when start() was called,
|
|
163
|
+
// or the start event failed to persist for any reason.
|
|
164
|
+
if (this.startedAt !== null) {
|
|
165
|
+
const hasCurrentStart = selfEvents.some(
|
|
166
|
+
(e) => e.type === "start" && e.ts === this.startedAt,
|
|
167
|
+
);
|
|
168
|
+
if (!hasCurrentStart) {
|
|
169
|
+
selfEvents.push({ ts: this.startedAt, type: "start" });
|
|
170
|
+
selfEvents.sort((a, b) => a.ts - b.ts);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
146
173
|
const gaps = this.getObservationGaps(selfEvents, startTs, endTs);
|
|
147
174
|
|
|
148
175
|
// Build timeline for each node (including self)
|
|
@@ -154,15 +181,22 @@ export class HealthTracker {
|
|
|
154
181
|
for (const ev of allEvents) {
|
|
155
182
|
if (ev.peer) everConnected.add(ev.peer);
|
|
156
183
|
}
|
|
184
|
+
// Include currently connected peers (in-memory, survives store failures)
|
|
185
|
+
for (const peerId of this.connectedPeers.keys()) {
|
|
186
|
+
everConnected.add(peerId);
|
|
187
|
+
}
|
|
157
188
|
|
|
158
189
|
const knownNodes = new Set(this.getKnownNodes());
|
|
190
|
+
// Always include self node
|
|
191
|
+
knownNodes.add(this.nodeId);
|
|
159
192
|
// Also include nodes we've observed via peer_online events
|
|
160
193
|
for (const peerId of everConnected) knownNodes.add(peerId);
|
|
161
194
|
|
|
162
195
|
for (const nodeId of knownNodes) {
|
|
163
196
|
if (nodeId !== this.nodeId && !everConnected.has(nodeId)) continue;
|
|
164
197
|
|
|
165
|
-
|
|
198
|
+
// Reuse selfEvents (with injected in-memory start) for the self node
|
|
199
|
+
const events = nodeId === this.nodeId ? selfEvents : this.getEventsForNode(nodeId);
|
|
166
200
|
const timeline = this.buildNodeTimeline(
|
|
167
201
|
nodeId, events, startTs, endTs, bucketMs, bucketCount, gaps,
|
|
168
202
|
);
|
|
@@ -190,7 +224,15 @@ export class HealthTracker {
|
|
|
190
224
|
? this.buildPeerIntervals(nodeId, startTs, endTs)
|
|
191
225
|
: [];
|
|
192
226
|
|
|
193
|
-
if (sorted.length === 0 && peerIntervals.length === 0)
|
|
227
|
+
if (sorted.length === 0 && peerIntervals.length === 0) {
|
|
228
|
+
// For self node with no events (store not initialized), treat as currently online
|
|
229
|
+
if (nodeId === this.nodeId) {
|
|
230
|
+
const now = Date.now();
|
|
231
|
+
const buckets: BucketState[] = Array(bucketCount).fill("up");
|
|
232
|
+
return { nodeId, firstSeen: now, lastSeen: now, buckets, uptimeRatio: 1 };
|
|
233
|
+
}
|
|
234
|
+
return null;
|
|
235
|
+
}
|
|
194
236
|
|
|
195
237
|
// Determine firstSeen/lastSeen from both self-reported events and peer observations
|
|
196
238
|
let firstSeen = Infinity;
|
|
@@ -300,13 +342,29 @@ export class HealthTracker {
|
|
|
300
342
|
startTs: number,
|
|
301
343
|
endTs: number,
|
|
302
344
|
): Array<[number, number]> {
|
|
303
|
-
|
|
345
|
+
const relevantEvents: HealthEvent[] = [];
|
|
346
|
+
|
|
347
|
+
if (this.store) {
|
|
348
|
+
// Collect peer_online/peer_offline events about this node from ALL observers
|
|
349
|
+
const relevantRows = this.store.queryHealth({ peer: targetNodeId });
|
|
350
|
+
for (const r of relevantRows) {
|
|
351
|
+
if (r.type === "peer_online" || r.type === "peer_offline") {
|
|
352
|
+
relevantEvents.push({ ts: r.ts, type: r.type as HealthEvent["type"], peer: r.peer ?? undefined });
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// Inject in-memory connected peer if not reflected in persisted events
|
|
358
|
+
const connectedAt = this.connectedPeers.get(targetNodeId);
|
|
359
|
+
if (connectedAt !== undefined) {
|
|
360
|
+
const hasRecentOnline = relevantEvents.some(
|
|
361
|
+
(e) => e.type === "peer_online" && e.ts === connectedAt,
|
|
362
|
+
);
|
|
363
|
+
if (!hasRecentOnline) {
|
|
364
|
+
relevantEvents.push({ ts: connectedAt, type: "peer_online", peer: targetNodeId });
|
|
365
|
+
}
|
|
366
|
+
}
|
|
304
367
|
|
|
305
|
-
// Collect peer_online/peer_offline events about this node from ALL observers
|
|
306
|
-
const relevantRows = this.store.queryHealth({ peer: targetNodeId });
|
|
307
|
-
const relevantEvents: HealthEvent[] = relevantRows
|
|
308
|
-
.filter(r => r.type === "peer_online" || r.type === "peer_offline")
|
|
309
|
-
.map(r => ({ ts: r.ts, type: r.type as HealthEvent["type"], peer: r.peer ?? undefined }));
|
|
310
368
|
relevantEvents.sort((a, b) => a.ts - b.ts);
|
|
311
369
|
|
|
312
370
|
const intervals: Array<[number, number]> = [];
|
package/src/index.ts
CHANGED
|
@@ -1537,291 +1537,6 @@ const plugin = {
|
|
|
1537
1537
|
},
|
|
1538
1538
|
);
|
|
1539
1539
|
|
|
1540
|
-
// ── Availability gateway method ──────────────────────────────────
|
|
1541
|
-
|
|
1542
|
-
api.registerGatewayMethod(
|
|
1543
|
-
"clawmatrix.availability",
|
|
1544
|
-
({ params, respond }: GatewayRequestHandlerOptions) => {
|
|
1545
|
-
try {
|
|
1546
|
-
const runtime = getClusterRuntime();
|
|
1547
|
-
const { range } = (params ?? {}) as { range?: string };
|
|
1548
|
-
const validRanges = ["24h", "7d", "90d"];
|
|
1549
|
-
const r = validRanges.includes(range ?? "") ? (range as "24h" | "7d" | "90d") : "24h";
|
|
1550
|
-
const result = runtime.healthTracker.getAvailability(r);
|
|
1551
|
-
respond(true, result);
|
|
1552
|
-
} catch {
|
|
1553
|
-
respond(false, { error: "ClawMatrix service not running" });
|
|
1554
|
-
}
|
|
1555
|
-
},
|
|
1556
|
-
);
|
|
1557
|
-
|
|
1558
|
-
// ── Automation gateway methods ──────────────────────────────────
|
|
1559
|
-
|
|
1560
|
-
api.registerGatewayMethod(
|
|
1561
|
-
"clawmatrix.automations.rules",
|
|
1562
|
-
({ respond }: GatewayRequestHandlerOptions) => {
|
|
1563
|
-
try {
|
|
1564
|
-
const runtime = getClusterRuntime();
|
|
1565
|
-
if (!runtime.automationManager) {
|
|
1566
|
-
respond(true, { rules: [] });
|
|
1567
|
-
return;
|
|
1568
|
-
}
|
|
1569
|
-
respond(true, { rules: runtime.automationManager.getRules() });
|
|
1570
|
-
} catch {
|
|
1571
|
-
respond(false, { error: "ClawMatrix service not running" });
|
|
1572
|
-
}
|
|
1573
|
-
},
|
|
1574
|
-
);
|
|
1575
|
-
|
|
1576
|
-
api.registerGatewayMethod(
|
|
1577
|
-
"clawmatrix.automations.save",
|
|
1578
|
-
async ({ params, respond }: GatewayRequestHandlerOptions) => {
|
|
1579
|
-
try {
|
|
1580
|
-
const runtime = getClusterRuntime();
|
|
1581
|
-
if (!runtime.automationManager) {
|
|
1582
|
-
respond(false, { error: "Automations not available" });
|
|
1583
|
-
return;
|
|
1584
|
-
}
|
|
1585
|
-
const { rules } = (params ?? {}) as { rules?: unknown[] };
|
|
1586
|
-
if (!Array.isArray(rules)) {
|
|
1587
|
-
respond(false, { error: "Missing required param: rules (array)" });
|
|
1588
|
-
return;
|
|
1589
|
-
}
|
|
1590
|
-
await runtime.automationManager.saveRules(rules as Parameters<typeof runtime.automationManager.saveRules>[0]);
|
|
1591
|
-
respond(true, { ok: true, count: rules.length });
|
|
1592
|
-
} catch (err) {
|
|
1593
|
-
respond(false, { error: String(err) });
|
|
1594
|
-
}
|
|
1595
|
-
},
|
|
1596
|
-
);
|
|
1597
|
-
|
|
1598
|
-
api.registerGatewayMethod(
|
|
1599
|
-
"clawmatrix.automations.history",
|
|
1600
|
-
({ params, respond }: GatewayRequestHandlerOptions) => {
|
|
1601
|
-
try {
|
|
1602
|
-
const runtime = getClusterRuntime();
|
|
1603
|
-
if (!runtime.automationManager) {
|
|
1604
|
-
respond(true, { executions: [] });
|
|
1605
|
-
return;
|
|
1606
|
-
}
|
|
1607
|
-
const { limit } = (params ?? {}) as { limit?: number };
|
|
1608
|
-
respond(true, { executions: runtime.automationManager.getExecutions(limit ?? 50) });
|
|
1609
|
-
} catch {
|
|
1610
|
-
respond(false, { error: "ClawMatrix service not running" });
|
|
1611
|
-
}
|
|
1612
|
-
},
|
|
1613
|
-
);
|
|
1614
|
-
|
|
1615
|
-
api.registerGatewayMethod(
|
|
1616
|
-
"clawmatrix.automations.run",
|
|
1617
|
-
async ({ params, respond }: GatewayRequestHandlerOptions) => {
|
|
1618
|
-
try {
|
|
1619
|
-
const runtime = getClusterRuntime();
|
|
1620
|
-
if (!runtime.automationManager) {
|
|
1621
|
-
respond(false, { error: "Automations not available" });
|
|
1622
|
-
return;
|
|
1623
|
-
}
|
|
1624
|
-
const { ruleId, event } = (params ?? {}) as { ruleId?: string; event?: Record<string, unknown> };
|
|
1625
|
-
if (!ruleId) {
|
|
1626
|
-
respond(false, { error: "Missing required param: ruleId" });
|
|
1627
|
-
return;
|
|
1628
|
-
}
|
|
1629
|
-
const evt = event ? {
|
|
1630
|
-
id: nanoid(),
|
|
1631
|
-
source: String(event.source || "cli"),
|
|
1632
|
-
type: String(event.type || "manual"),
|
|
1633
|
-
data: (event.data ?? {}) as Record<string, unknown>,
|
|
1634
|
-
ts: typeof event.ts === "number" ? event.ts : Date.now(),
|
|
1635
|
-
consumed: false,
|
|
1636
|
-
} as import("./types.ts").IngestedEvent : undefined;
|
|
1637
|
-
const execution = await runtime.automationManager.runRuleById(ruleId, evt);
|
|
1638
|
-
respond(true, { ok: true, execution });
|
|
1639
|
-
} catch (err) {
|
|
1640
|
-
respond(false, { error: String(err) });
|
|
1641
|
-
}
|
|
1642
|
-
},
|
|
1643
|
-
);
|
|
1644
|
-
|
|
1645
|
-
api.registerGatewayMethod(
|
|
1646
|
-
"clawmatrix.automations.replay",
|
|
1647
|
-
async ({ params, respond }: GatewayRequestHandlerOptions) => {
|
|
1648
|
-
try {
|
|
1649
|
-
const runtime = getClusterRuntime();
|
|
1650
|
-
if (!runtime.automationManager) {
|
|
1651
|
-
respond(false, { error: "Automations not available" });
|
|
1652
|
-
return;
|
|
1653
|
-
}
|
|
1654
|
-
const { executionId } = (params ?? {}) as { executionId?: string };
|
|
1655
|
-
if (!executionId) {
|
|
1656
|
-
respond(false, { error: "Missing required param: executionId" });
|
|
1657
|
-
return;
|
|
1658
|
-
}
|
|
1659
|
-
const execution = await runtime.automationManager.replayExecution(executionId);
|
|
1660
|
-
respond(true, { ok: true, execution });
|
|
1661
|
-
} catch (err) {
|
|
1662
|
-
respond(false, { error: String(err) });
|
|
1663
|
-
}
|
|
1664
|
-
},
|
|
1665
|
-
);
|
|
1666
|
-
|
|
1667
|
-
// ── Events ingest gateway method ──────────────────────────────────
|
|
1668
|
-
|
|
1669
|
-
api.registerGatewayMethod(
|
|
1670
|
-
"clawmatrix.events.ingest",
|
|
1671
|
-
({ params, respond }: GatewayRequestHandlerOptions) => {
|
|
1672
|
-
try {
|
|
1673
|
-
const runtime = getClusterRuntime();
|
|
1674
|
-
if (!runtime.apiHandler) {
|
|
1675
|
-
respond(false, { error: "API handler not available (listen mode required)" });
|
|
1676
|
-
return;
|
|
1677
|
-
}
|
|
1678
|
-
const { events } = (params ?? {}) as { events?: unknown[] };
|
|
1679
|
-
if (!Array.isArray(events) || events.length === 0) {
|
|
1680
|
-
respond(false, { error: "Missing required param: events (non-empty array)" });
|
|
1681
|
-
return;
|
|
1682
|
-
}
|
|
1683
|
-
const result = runtime.apiHandler.ingestEvents(events as Array<Record<string, unknown>>);
|
|
1684
|
-
respond(true, { ok: true, ...result });
|
|
1685
|
-
} catch {
|
|
1686
|
-
respond(false, { error: "ClawMatrix service not running" });
|
|
1687
|
-
}
|
|
1688
|
-
},
|
|
1689
|
-
);
|
|
1690
|
-
|
|
1691
|
-
// ── Board update gateway method ──────────────────────────────────
|
|
1692
|
-
|
|
1693
|
-
api.registerGatewayMethod(
|
|
1694
|
-
"clawmatrix.board.update",
|
|
1695
|
-
({ params, respond }: GatewayRequestHandlerOptions) => {
|
|
1696
|
-
try {
|
|
1697
|
-
const runtime = getClusterRuntime();
|
|
1698
|
-
if (!runtime.kanbanManager) {
|
|
1699
|
-
respond(false, { error: "Kanban not enabled" });
|
|
1700
|
-
return;
|
|
1701
|
-
}
|
|
1702
|
-
const { cardId, ...updates } = (params ?? {}) as {
|
|
1703
|
-
cardId?: string; title?: string; description?: string;
|
|
1704
|
-
priority?: string; targetNode?: string; targetAgent?: string;
|
|
1705
|
-
cwd?: string; labels?: string[];
|
|
1706
|
-
};
|
|
1707
|
-
if (!cardId) {
|
|
1708
|
-
respond(false, { error: "Missing required param: cardId" });
|
|
1709
|
-
return;
|
|
1710
|
-
}
|
|
1711
|
-
const card = runtime.kanbanManager.updateCard(cardId, updates as Parameters<typeof runtime.kanbanManager.updateCard>[1]);
|
|
1712
|
-
if (!card) {
|
|
1713
|
-
respond(false, { error: `Card not found: ${cardId}` });
|
|
1714
|
-
return;
|
|
1715
|
-
}
|
|
1716
|
-
respond(true, card);
|
|
1717
|
-
} catch {
|
|
1718
|
-
respond(false, { error: "ClawMatrix service not running" });
|
|
1719
|
-
}
|
|
1720
|
-
},
|
|
1721
|
-
);
|
|
1722
|
-
|
|
1723
|
-
// ── Config gateway method ──────────────────────────────────────────
|
|
1724
|
-
|
|
1725
|
-
api.registerGatewayMethod(
|
|
1726
|
-
"clawmatrix.config.get",
|
|
1727
|
-
({ respond }: GatewayRequestHandlerOptions) => {
|
|
1728
|
-
try {
|
|
1729
|
-
const runtime = getClusterRuntime();
|
|
1730
|
-
const c = runtime.config;
|
|
1731
|
-
respond(true, {
|
|
1732
|
-
nodeId: c.nodeId,
|
|
1733
|
-
listen: c.listen,
|
|
1734
|
-
tags: c.tags,
|
|
1735
|
-
agents: c.agents.map((a) => ({ id: a.id, model: a.model })),
|
|
1736
|
-
models: c.models.map((m) => ({ id: m.id, provider: m.provider })),
|
|
1737
|
-
e2ee: c.e2ee,
|
|
1738
|
-
toolProxy: c.toolProxy ? { enabled: c.toolProxy.enabled, allow: c.toolProxy.allow, deny: c.toolProxy.deny } : undefined,
|
|
1739
|
-
terminal: c.terminal,
|
|
1740
|
-
acp: c.acp ? { enabled: c.acp.enabled } : undefined,
|
|
1741
|
-
knowledge: c.knowledge ? { enabled: c.knowledge.enabled } : undefined,
|
|
1742
|
-
proxyModels: c.proxyModels?.length ?? 0,
|
|
1743
|
-
peers: c.peers?.length ?? 0,
|
|
1744
|
-
});
|
|
1745
|
-
} catch {
|
|
1746
|
-
respond(false, { error: "ClawMatrix service not running" });
|
|
1747
|
-
}
|
|
1748
|
-
},
|
|
1749
|
-
);
|
|
1750
|
-
|
|
1751
|
-
// ── Config file read/write gateway methods ──────────────────────
|
|
1752
|
-
|
|
1753
|
-
api.registerGatewayMethod(
|
|
1754
|
-
"clawmatrix.config.read",
|
|
1755
|
-
async ({ params, respond }: GatewayRequestHandlerOptions) => {
|
|
1756
|
-
try {
|
|
1757
|
-
const runtime = getClusterRuntime();
|
|
1758
|
-
if (!runtime.apiHandler) {
|
|
1759
|
-
respond(false, { error: "API handler not available" });
|
|
1760
|
-
return;
|
|
1761
|
-
}
|
|
1762
|
-
const { path: configPath } = (params ?? {}) as { path?: string };
|
|
1763
|
-
if (!configPath) {
|
|
1764
|
-
respond(false, { error: "Missing required param: path" });
|
|
1765
|
-
return;
|
|
1766
|
-
}
|
|
1767
|
-
const result = await runtime.apiHandler.readConfigFile(configPath);
|
|
1768
|
-
respond(result.success, result);
|
|
1769
|
-
} catch (err) {
|
|
1770
|
-
respond(false, { error: String(err) });
|
|
1771
|
-
}
|
|
1772
|
-
},
|
|
1773
|
-
);
|
|
1774
|
-
|
|
1775
|
-
api.registerGatewayMethod(
|
|
1776
|
-
"clawmatrix.config.write",
|
|
1777
|
-
async ({ params, respond }: GatewayRequestHandlerOptions) => {
|
|
1778
|
-
try {
|
|
1779
|
-
const runtime = getClusterRuntime();
|
|
1780
|
-
if (!runtime.apiHandler) {
|
|
1781
|
-
respond(false, { error: "API handler not available" });
|
|
1782
|
-
return;
|
|
1783
|
-
}
|
|
1784
|
-
const { path: configPath, content } = (params ?? {}) as { path?: string; content?: string };
|
|
1785
|
-
if (!configPath || typeof content !== "string") {
|
|
1786
|
-
respond(false, { error: "Missing required params: path, content" });
|
|
1787
|
-
return;
|
|
1788
|
-
}
|
|
1789
|
-
const result = await runtime.apiHandler.writeConfigFile(configPath, content);
|
|
1790
|
-
respond(result.success, result);
|
|
1791
|
-
} catch (err) {
|
|
1792
|
-
respond(false, { error: String(err) });
|
|
1793
|
-
}
|
|
1794
|
-
},
|
|
1795
|
-
);
|
|
1796
|
-
|
|
1797
|
-
// ── Knowledge content gateway method ──────────────────────────────
|
|
1798
|
-
|
|
1799
|
-
api.registerGatewayMethod(
|
|
1800
|
-
"clawmatrix.kb.content",
|
|
1801
|
-
({ params, respond }: GatewayRequestHandlerOptions) => {
|
|
1802
|
-
try {
|
|
1803
|
-
const runtime = getClusterRuntime();
|
|
1804
|
-
if (!runtime.knowledgeSync) {
|
|
1805
|
-
respond(false, { error: "Knowledge sync not enabled" });
|
|
1806
|
-
return;
|
|
1807
|
-
}
|
|
1808
|
-
const { path } = (params ?? {}) as { path?: string };
|
|
1809
|
-
if (!path) {
|
|
1810
|
-
respond(false, { error: "Missing required param: path" });
|
|
1811
|
-
return;
|
|
1812
|
-
}
|
|
1813
|
-
const content = runtime.knowledgeSync.getFileContent(path);
|
|
1814
|
-
if (content === null) {
|
|
1815
|
-
respond(false, { error: "File not found or content not yet synced" });
|
|
1816
|
-
return;
|
|
1817
|
-
}
|
|
1818
|
-
respond(true, { path, content });
|
|
1819
|
-
} catch {
|
|
1820
|
-
respond(false, { error: "ClawMatrix service not running" });
|
|
1821
|
-
}
|
|
1822
|
-
},
|
|
1823
|
-
);
|
|
1824
|
-
|
|
1825
1540
|
// Log model selection on each LLM call (fire-and-forget)
|
|
1826
1541
|
api.on("llm_input", (event) => {
|
|
1827
1542
|
api.logger.debug(`[clawmatrix] llm_input: provider=${event.provider} model=${event.model}`);
|
package/src/knowledge-sync.ts
CHANGED
|
@@ -191,6 +191,11 @@ export class KnowledgeSync {
|
|
|
191
191
|
|
|
192
192
|
// ── Public API ─────────────────────────────────────────────────
|
|
193
193
|
|
|
194
|
+
/** The resolved workspace directory that knowledge files are synced under. */
|
|
195
|
+
get workspacePath(): string {
|
|
196
|
+
return this.opts.workspacePath;
|
|
197
|
+
}
|
|
198
|
+
|
|
194
199
|
async start() {
|
|
195
200
|
debug(TAG, `starting knowledge sync: workspace=${this.opts.workspacePath}`);
|
|
196
201
|
|
|
@@ -536,7 +541,7 @@ export class KnowledgeSync {
|
|
|
536
541
|
}
|
|
537
542
|
|
|
538
543
|
/** List all synced files with metadata. */
|
|
539
|
-
listSyncedFiles(): Array<{ path: string; version: number; updatedAt: number; deleted: boolean }> {
|
|
544
|
+
listSyncedFiles(): Array<{ path: string; version: number; updatedAt: number; deleted: boolean; synced: boolean }> {
|
|
540
545
|
const files = this.registry.files;
|
|
541
546
|
if (!files) return [];
|
|
542
547
|
return Object.entries(files).map(([path, meta]) => ({
|
|
@@ -544,6 +549,7 @@ export class KnowledgeSync {
|
|
|
544
549
|
version: meta.version,
|
|
545
550
|
updatedAt: meta.updatedAt,
|
|
546
551
|
deleted: meta.deleted,
|
|
552
|
+
synced: this.fileDocs.has(path) && this.fileDocs.get(path)!.content !== undefined,
|
|
547
553
|
})).filter(f => !f.deleted);
|
|
548
554
|
}
|
|
549
555
|
|
package/src/peer-manager.ts
CHANGED
|
@@ -140,7 +140,19 @@ export class PeerManager extends EventEmitter<PeerManagerEvents> {
|
|
|
140
140
|
super();
|
|
141
141
|
this.config = config;
|
|
142
142
|
this.localDeviceInfo = collectDeviceInfo(openclawVersion, openclawConfig);
|
|
143
|
-
|
|
143
|
+
// Determine initial ACP agents: ClawMatrix config → OpenClaw config fallback.
|
|
144
|
+
// When ACP is enabled via OpenClaw but ClawMatrix has no explicit agents list,
|
|
145
|
+
// use OpenClaw's allowedAgents so auth_ok advertises them before async detection.
|
|
146
|
+
const ocAcp = (openclawConfig as Record<string, any>)?.acp;
|
|
147
|
+
const acpEnabled = config.acp?.enabled || ocAcp?.enabled;
|
|
148
|
+
let acpAgents: { id: string; description: string }[] | undefined;
|
|
149
|
+
if (acpEnabled) {
|
|
150
|
+
if (config.acp?.agents?.length) {
|
|
151
|
+
acpAgents = config.acp.agents;
|
|
152
|
+
} else if (Array.isArray(ocAcp?.allowedAgents) && ocAcp.allowedAgents.length > 0) {
|
|
153
|
+
acpAgents = ocAcp.allowedAgents.map((id: string) => ({ id, description: "" }));
|
|
154
|
+
}
|
|
155
|
+
}
|
|
144
156
|
this.localCapabilities = {
|
|
145
157
|
nodeId: config.nodeId,
|
|
146
158
|
agents: config.agents,
|
|
@@ -707,9 +719,15 @@ export class PeerManager extends EventEmitter<PeerManagerEvents> {
|
|
|
707
719
|
debug("peer", `connectToChannel(${nodeId}): self-connection, will not reconnect`);
|
|
708
720
|
return;
|
|
709
721
|
}
|
|
710
|
-
// Don't reconnect
|
|
711
|
-
|
|
712
|
-
|
|
722
|
+
// Don't reconnect deliberate closures that shouldn't trigger reconnection
|
|
723
|
+
const skipReasons = ["warm-up pruned", "route switch", "replaced by new connection"];
|
|
724
|
+
if (skipReasons.includes(ev.reason)) {
|
|
725
|
+
debug("peer", `connectToChannel(${nodeId}): ${ev.reason}, will not reconnect`);
|
|
726
|
+
return;
|
|
727
|
+
}
|
|
728
|
+
// Don't reconnect when peer approval was denied — retrying is futile
|
|
729
|
+
if (ev.code === 4005) {
|
|
730
|
+
debug("peer", `connectToChannel(${nodeId}): approval denied, will not reconnect`);
|
|
713
731
|
return;
|
|
714
732
|
}
|
|
715
733
|
// Record close code for adaptive backoff
|
package/src/sentinel.ts
CHANGED
|
@@ -177,7 +177,7 @@ function buildCapabilities(): NodeCapabilities {
|
|
|
177
177
|
}
|
|
178
178
|
|
|
179
179
|
function connectToPeer(peer: { nodeId: string; url: string }) {
|
|
180
|
-
const ws = new
|
|
180
|
+
const ws = new WebSocket(peer.url, ["graphql-transport-ws"]);
|
|
181
181
|
const e2eeOpts: ConnectionE2eeOptions = {
|
|
182
182
|
e2ee: config.e2ee,
|
|
183
183
|
compression: config.compression,
|