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.
@@ -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 Show local node configuration
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. You can invoke any tool available on remote nodes — including getting device location, battery status, running shell commands, reading/writing files, and more.
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
- All commands output LLM-friendly text (no ANSI colors) when stdout is not a TTY.
8
+ Run `clawmatrix --help` or `clawmatrix <command> --help` for detailed usage.
9
9
 
10
- ## Quick Start
10
+ ## Capabilities
11
11
 
12
- 1. Run `clawmatrix status` to see connected nodes and their capabilities
13
- 2. Run `clawmatrix tools <nodeId>` to see what tools a node offers
14
- 3. Run `clawmatrix call <nodeId> <tool> '<params>'` to invoke a tool
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
- ## Cluster Status
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
- ```bash
19
- clawmatrix status # Cluster topology: peers, agents, models, tags
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
- Always start with `clawmatrix status` to understand the current topology before performing other operations.
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
- ## Remote Tools
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
- ```bash
32
- clawmatrix tools # List all remote tools (compact)
33
- clawmatrix tools <nodeId> # Tools on a specific node
34
- clawmatrix tools --describe <tool> # Full usage and parameter schema
35
- clawmatrix tools --filter <keyword> # Search by name or description
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
- Use `--describe` to understand a tool's parameters before calling it.
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
- ## Invoke Tools
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
- ```bash
43
- clawmatrix call <nodeId> <tool> '<json-params>' # Single tool invocation
44
- clawmatrix call <nodeId> <tool> '<json-params>' -t 30000 # With timeout (ms)
45
- ```
48
+ **Config Management**
49
+ - View runtime config summary
50
+ - Read/write config files under `~/.openclaw/` (for remote node configuration)
46
51
 
47
- ```bash
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
- Batch supports stdin: `echo '<json>' | clawmatrix batch <nodeId>`
53
-
54
- ## Models
55
-
56
- ```bash
57
- clawmatrix models # All cluster models
58
- clawmatrix models --node <nodeId> # Filter by node
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clawmatrix",
3
- "version": "0.6.0",
3
+ "version": "0.6.1",
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/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) {
@@ -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.recordEvent({ ts: Date.now(), type: "start" });
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
- const events = this.getEventsForNode(nodeId);
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) return null;
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
- if (!this.store) return [];
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]> = [];
@@ -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
- const acpAgents = config.acp?.enabled ? config.acp.agents : undefined;
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,