clawmatrix 0.6.0 → 0.6.1
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 +42 -135
- package/package.json +1 -1
- package/src/api.ts +0 -66
- package/src/health-tracker.ts +67 -9
- package/src/peer-manager.ts +13 -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:
|
|
@@ -3,151 +3,58 @@ name: clawmatrix
|
|
|
3
3
|
description: Use the clawmatrix CLI to interact with remote devices (phones, computers, servers) in a mesh cluster — run tools, check location, get battery status, read/write files, execute commands, and more on any connected node.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
-
Use the `clawmatrix` CLI to interact with the ClawMatrix mesh cluster. Remote nodes can be phones (iPhone/Android), computers, or servers.
|
|
6
|
+
Use the `clawmatrix` CLI to interact with the ClawMatrix mesh cluster. Remote nodes can be phones (iPhone/Android), computers, or servers.
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
Run `clawmatrix --help` or `clawmatrix <command> --help` for detailed usage.
|
|
9
9
|
|
|
10
|
-
##
|
|
10
|
+
## Capabilities
|
|
11
11
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
12
|
+
**Cluster & Discovery**
|
|
13
|
+
- View cluster topology, peer status, node config, and uptime/availability
|
|
14
|
+
- Check reachability of specific nodes with latency
|
|
15
|
+
- Approve/deny/revoke peer connections
|
|
15
16
|
|
|
16
|
-
|
|
17
|
+
**Remote Tool Execution**
|
|
18
|
+
- Discover tools across all nodes (filter by keyword, describe params)
|
|
19
|
+
- Invoke single tools or batch multiple in one round-trip
|
|
20
|
+
- Tools include device-specific capabilities: location, battery, camera, clipboard, contacts, calendar, health data, HomeKit, etc.
|
|
17
21
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
clawmatrix status --json # Structured JSON output
|
|
21
|
-
clawmatrix peers # List known peers (JSON)
|
|
22
|
-
clawmatrix check <nodeId> # Quick reachability check with latency
|
|
23
|
-
clawmatrix config # Show local node configuration
|
|
24
|
-
clawmatrix config --json # Config as JSON
|
|
25
|
-
```
|
|
22
|
+
**Models**
|
|
23
|
+
- List all LLM models available across the cluster, filterable by node
|
|
26
24
|
|
|
27
|
-
|
|
25
|
+
**Agent Delegation**
|
|
26
|
+
- `handoff` — delegate tasks to remote agents with streaming output and failover
|
|
27
|
+
- `send` — fire-and-forget messages to remote nodes
|
|
28
|
+
- `acp` — manage persistent coding agent sessions (Claude Code, Codex, Gemini) with prompt/resume/cancel/close
|
|
28
29
|
|
|
29
|
-
|
|
30
|
+
**Events & Automations**
|
|
31
|
+
- Query, consume, and ingest events from external sources (iOS Shortcuts, webhooks, etc.)
|
|
32
|
+
- Manage automation rules: list, create/save, manually trigger, replay historical executions
|
|
30
33
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
```
|
|
34
|
+
**Infrastructure**
|
|
35
|
+
- `diagnostic` — sentinel-based diagnostics (works even when gateway is down)
|
|
36
|
+
- `terminal` — interactive PTY sessions on remote nodes
|
|
37
|
+
- `transfer` — push/pull files up to 100MB between nodes with integrity verification
|
|
38
|
+
- `notify` — push Dynamic Island / Live Activity notifications to iOS devices
|
|
37
39
|
|
|
38
|
-
|
|
40
|
+
**Knowledge Sync (CRDT)**
|
|
41
|
+
- List synced workspace files, view change history, line-by-line blame, read file content
|
|
42
|
+
- All knowledge is CRDT-based and mesh-synced across nodes
|
|
39
43
|
|
|
40
|
-
|
|
44
|
+
**Kanban Board**
|
|
45
|
+
- Distributed task board: create, list, get, update, claim, move, annotate, delete cards
|
|
46
|
+
- Filter by stage/priority/label/node; stages flow from `backlog` → `done` → `archived`
|
|
41
47
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
```
|
|
48
|
+
**Config Management**
|
|
49
|
+
- View runtime config summary
|
|
50
|
+
- Read/write config files under `~/.openclaw/` (for remote node configuration)
|
|
46
51
|
|
|
47
|
-
|
|
48
|
-
clawmatrix batch <nodeId> '[{"tool":"t1","params":{}},{"tool":"t2","params":{}}]'
|
|
49
|
-
clawmatrix batch <nodeId> --no-stop-on-error '[...]' # Continue on failure
|
|
50
|
-
```
|
|
52
|
+
## Important Notes
|
|
51
53
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
```
|
|
60
|
-
|
|
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
|
-
## Events
|
|
70
|
-
|
|
71
|
-
```bash
|
|
72
|
-
clawmatrix events # Unconsumed events
|
|
73
|
-
clawmatrix events --type <type> # Filter by type
|
|
74
|
-
clawmatrix events --source <nodeId> # Filter by source node
|
|
75
|
-
clawmatrix events --consume <id1,id2> # Mark events as consumed
|
|
76
|
-
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
|
-
```
|
|
91
|
-
|
|
92
|
-
## Peer Approval
|
|
93
|
-
|
|
94
|
-
```bash
|
|
95
|
-
clawmatrix approve <approvalId> # Approve a pending peer
|
|
96
|
-
clawmatrix deny <approvalId> # Deny a pending peer
|
|
97
|
-
clawmatrix approval list # List pending/approved/denied peers
|
|
98
|
-
clawmatrix approval revoke <nodeId> # Revoke an approved peer
|
|
99
|
-
```
|
|
100
|
-
|
|
101
|
-
## Notifications (Dynamic Island / Live Activity)
|
|
102
|
-
|
|
103
|
-
Push progress notifications to the user's iPhone via `clawmatrix notify`. This triggers the Dynamic Island and lock screen Live Activity.
|
|
104
|
-
|
|
105
|
-
**Use this proactively when running long tasks** (iOS builds, large refactors, test suites, batch operations) so the user can track progress without watching the terminal.
|
|
106
|
-
|
|
107
|
-
```bash
|
|
108
|
-
# Start a notification
|
|
109
|
-
clawmatrix notify "iOS 构建" --detail "正在编译..."
|
|
110
|
-
# Returns: {"taskId":"<id>", "action":"start", "targets":1}
|
|
111
|
-
|
|
112
|
-
# Update progress
|
|
113
|
-
clawmatrix notify "iOS 构建" --action update --task-id <id> --detail "链接中..." --progress 80
|
|
114
|
-
|
|
115
|
-
# End (success)
|
|
116
|
-
clawmatrix notify "iOS 构建" --action end --task-id <id>
|
|
117
|
-
```
|
|
118
|
-
|
|
119
|
-
Options: `--detail <text>`, `--progress <0-100>`, `--action start|update|end`, `--task-id <id>`, `--tool <name>`
|
|
120
|
-
|
|
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
|
-
## Workflow
|
|
147
|
-
|
|
148
|
-
1. Run `clawmatrix status` to see the cluster topology
|
|
149
|
-
2. Use `clawmatrix tools --filter <keyword>` to find relevant tools
|
|
150
|
-
3. Use `clawmatrix tools --describe <tool>` to check parameters
|
|
151
|
-
4. Use `clawmatrix call` or `clawmatrix batch` to invoke tools
|
|
152
|
-
5. If a call fails, run `clawmatrix check <nodeId>` to verify connectivity
|
|
153
|
-
6. For long-running tasks, use `clawmatrix notify` to push progress to the user's phone
|
|
54
|
+
- **Always start with `clawmatrix status`** to understand the cluster topology before other operations
|
|
55
|
+
- **Use `clawmatrix tools --describe <tool>`** to check a tool's parameter schema before calling it
|
|
56
|
+
- **Use `clawmatrix notify` proactively during long tasks** (builds, tests, large refactors) so the user can track progress on their phone without watching the terminal
|
|
57
|
+
- Output is **LLM-optimized**: no ANSI colors and compact format when stdout is not a TTY; add `--json` for structured output
|
|
58
|
+
- Target nodes by exact `nodeId` or by tag expression `tags:<tag>`
|
|
59
|
+
- Most commands support `--json` for structured output
|
|
60
|
+
- `batch` and `automations save` support stdin for piping data
|
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/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/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,
|