@silicaclaw/cli 2026.3.20-4 → 2026.3.20-6

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.
Files changed (27) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/INSTALL.md +2 -2
  3. package/README.md +2 -2
  4. package/VERSION +1 -1
  5. package/apps/local-console/dist/apps/local-console/src/server.d.ts +13 -2
  6. package/apps/local-console/dist/apps/local-console/src/server.js +149 -20
  7. package/apps/local-console/dist/packages/network/src/relayPreview.d.ts +4 -0
  8. package/apps/local-console/dist/packages/network/src/relayPreview.js +37 -6
  9. package/apps/local-console/public/app/app.js +45 -2
  10. package/apps/local-console/public/app/network.js +35 -4
  11. package/apps/local-console/public/app/social.js +1 -0
  12. package/apps/local-console/public/app/styles.css +35 -0
  13. package/apps/local-console/public/app/template.js +1 -0
  14. package/apps/local-console/public/app/translations.js +18 -6
  15. package/apps/local-console/src/server.ts +175 -16
  16. package/node_modules/@silicaclaw/network/dist/packages/network/src/relayPreview.d.ts +4 -0
  17. package/node_modules/@silicaclaw/network/dist/packages/network/src/relayPreview.js +37 -6
  18. package/node_modules/@silicaclaw/network/src/relayPreview.ts +41 -6
  19. package/openclaw-skills/silicaclaw-broadcast/VERSION +1 -1
  20. package/openclaw-skills/silicaclaw-broadcast/manifest.json +1 -1
  21. package/openclaw-skills/silicaclaw-owner-push/VERSION +1 -1
  22. package/openclaw-skills/silicaclaw-owner-push/manifest.json +1 -1
  23. package/openclaw-skills/silicaclaw-owner-push/scripts/owner-push-forwarder.mjs +84 -1
  24. package/package.json +1 -1
  25. package/packages/network/dist/packages/network/src/relayPreview.d.ts +4 -0
  26. package/packages/network/dist/packages/network/src/relayPreview.js +37 -6
  27. package/packages/network/src/relayPreview.ts +41 -6
package/CHANGELOG.md CHANGED
@@ -2,6 +2,18 @@
2
2
 
3
3
  ## v1.0 beta - 2026-03-20
4
4
 
5
+ ### 2026.3.20-6
6
+
7
+ - release build:
8
+ - prepared another fresh latest-channel package build without publishing
9
+ - regenerated the npm tarball through the verified release packing workflow
10
+
11
+ ### 2026.3.20-5
12
+
13
+ - release build:
14
+ - prepared another fresh latest-channel package build without publishing
15
+ - regenerated the npm tarball through the verified release packing workflow
16
+
5
17
  ### 2026.3.20-4
6
18
 
7
19
  - release build:
package/INSTALL.md CHANGED
@@ -211,9 +211,9 @@ npx clawhub publish openclaw-skills/silicaclaw-broadcast \
211
211
  npx clawhub publish openclaw-skills/silicaclaw-owner-push \
212
212
  --slug silicaclaw-owner-push \
213
213
  --name "SilicaClaw Owner Push" \
214
- --version 2026.3.20-beta.2 \
214
+ --version 2026.3.20-beta.3 \
215
215
  --tags latest \
216
- --changelog "Added latest-only owner push behavior with timestamp cursor state so only the newest qualifying broadcast is pushed and older messages are skipped."
216
+ --changelog "Added single-instance lock protection so owner push avoids duplicate notifications when multiple forwarders start at the same time."
217
217
  ```
218
218
 
219
219
  ClawHub expects each skill version to be valid semver, so use the versions from each skill's `manifest.json` and `VERSION`, not the npm CLI version format.
package/README.md CHANGED
@@ -283,9 +283,9 @@ npx clawhub publish openclaw-skills/silicaclaw-broadcast \
283
283
  npx clawhub publish openclaw-skills/silicaclaw-owner-push \
284
284
  --slug silicaclaw-owner-push \
285
285
  --name "SilicaClaw Owner Push" \
286
- --version 2026.3.20-beta.2 \
286
+ --version 2026.3.20-beta.3 \
287
287
  --tags latest \
288
- --changelog "Added latest-only owner push behavior with timestamp cursor state so only the newest qualifying broadcast is pushed and older messages are skipped."
288
+ --changelog "Added single-instance lock protection so owner push avoids duplicate notifications when multiple forwarders start at the same time."
289
289
  ```
290
290
 
291
291
  ClawHub publishes the OpenClaw skill folders, not the npm CLI package.
package/VERSION CHANGED
@@ -1 +1 @@
1
- v2026.3.20-4
1
+ v2026.3.20-6
@@ -72,11 +72,17 @@ type OpenClawBridgeStatus = {
72
72
  gateway_url: string;
73
73
  gateway_port: number;
74
74
  gateway_reachable: boolean;
75
+ status_command: string | null;
76
+ status_ok: boolean;
77
+ status_summary: string | null;
78
+ gateway_probe_command: string | null;
79
+ gateway_probe_ok: boolean;
80
+ gateway_probe_summary: string | null;
75
81
  configured_gateway_url: string;
76
82
  configured_gateway_port: number;
77
83
  configured_gateway_bind: string | null;
78
84
  configured_gateway_config_path: string | null;
79
- detection_mode: "process" | "gateway" | "process+gateway" | "not_running";
85
+ detection_mode: "gateway-probe" | "gateway-probe+process" | "gateway-probe+gateway" | "gateway-probe+process+gateway" | "process" | "gateway" | "process+gateway" | "not_running";
80
86
  };
81
87
  skill_learning: {
82
88
  available: boolean;
@@ -162,6 +168,10 @@ export declare class LocalNodeService {
162
168
  private broadcastCount;
163
169
  private lastMessageAt;
164
170
  private lastBroadcastAt;
171
+ private lastProfileBroadcastAt;
172
+ private lastProfileBroadcastSignature;
173
+ private lastReplayBroadcastAt;
174
+ private lastReplayBroadcastSignature;
165
175
  private lastBroadcastErrorAt;
166
176
  private lastBroadcastError;
167
177
  private broadcastFailureCount;
@@ -632,7 +642,7 @@ export declare class LocalNodeService {
632
642
  openclaw: {
633
643
  detected: boolean;
634
644
  running: boolean;
635
- detection_mode: "process+gateway" | "gateway" | "process" | "not_running";
645
+ detection_mode: "gateway" | "gateway-probe+process+gateway" | "gateway-probe+gateway" | "gateway-probe+process" | "gateway-probe" | "process+gateway" | "process" | "not_running";
636
646
  gateway_url: string;
637
647
  workspace_install_root: string;
638
648
  legacy_install_root: string;
@@ -754,6 +764,7 @@ export declare class LocalNodeService {
754
764
  reason: string;
755
765
  error?: string;
756
766
  }>;
767
+ private shouldPublishProfileRecord;
757
768
  private maybeRecoverFromBroadcastFailure;
758
769
  private hydrateFromDisk;
759
770
  private applySocialConfigOnCurrentState;
@@ -63,6 +63,8 @@ const SOCIAL_MESSAGE_MAX_AGE_MS = Number(process.env.SOCIAL_MESSAGE_MAX_AGE_MS |
63
63
  const SOCIAL_MESSAGE_OBSERVATION_HISTORY_LIMIT = Number(process.env.SOCIAL_MESSAGE_OBSERVATION_HISTORY_LIMIT || 500);
64
64
  const SOCIAL_MESSAGE_REPLAY_WINDOW_MS = Number(process.env.SOCIAL_MESSAGE_REPLAY_WINDOW_MS || 10 * 60_000);
65
65
  const SOCIAL_MESSAGE_REPLAY_MAX_PER_BROADCAST = Number(process.env.SOCIAL_MESSAGE_REPLAY_MAX_PER_BROADCAST || 3);
66
+ const SOCIAL_MESSAGE_REPLAY_REFRESH_INTERVAL_MS = Number(process.env.SOCIAL_MESSAGE_REPLAY_REFRESH_INTERVAL_MS || 120_000);
67
+ const PROFILE_RELAY_REFRESH_INTERVAL_MS = Number(process.env.PROFILE_RELAY_REFRESH_INTERVAL_MS || 120_000);
66
68
  const SOCIAL_MESSAGE_BLOCKED_AGENT_IDS = new Set(dedupeStrings(parseListEnv(process.env.SOCIAL_MESSAGE_BLOCKED_AGENT_IDS || "")));
67
69
  const SOCIAL_MESSAGE_BLOCKED_TERMS = dedupeStrings(parseListEnv(process.env.SOCIAL_MESSAGE_BLOCKED_TERMS || ""))
68
70
  .map((term) => term.trim().toLowerCase())
@@ -387,8 +389,71 @@ function readOpenClawConfiguredGateway(workspaceRoot) {
387
389
  gateway_url: OPENCLAW_GATEWAY_URL,
388
390
  };
389
391
  }
392
+ function resolveOpenClawStatusCommand(workspaceRoot) {
393
+ const explicitBin = String(process.env.OPENCLAW_BIN || "").trim();
394
+ if (explicitBin) {
395
+ return { cmd: explicitBin, args: ["status"] };
396
+ }
397
+ const configuredSourceDir = String(process.env.OPENCLAW_SOURCE_DIR || "").trim();
398
+ const defaultSourceDir = defaultOpenClawSourceDir(workspaceRoot);
399
+ const sourceDir = configuredSourceDir || defaultSourceDir;
400
+ const sourceEntry = existingPathOrNull((0, path_1.resolve)(sourceDir, "openclaw.mjs"));
401
+ if (sourceEntry) {
402
+ return { cmd: process.execPath, args: [sourceEntry, "status"] };
403
+ }
404
+ const commandPath = resolveExecutableInPath("openclaw");
405
+ if (commandPath) {
406
+ return { cmd: commandPath, args: ["status"] };
407
+ }
408
+ return null;
409
+ }
410
+ function resolveOpenClawGatewayProbeCommand(workspaceRoot) {
411
+ const explicitBin = String(process.env.OPENCLAW_BIN || "").trim();
412
+ if (explicitBin) {
413
+ return { cmd: explicitBin, args: ["gateway", "probe"] };
414
+ }
415
+ const configuredSourceDir = String(process.env.OPENCLAW_SOURCE_DIR || "").trim();
416
+ const defaultSourceDir = defaultOpenClawSourceDir(workspaceRoot);
417
+ const sourceDir = configuredSourceDir || defaultSourceDir;
418
+ const sourceEntry = existingPathOrNull((0, path_1.resolve)(sourceDir, "openclaw.mjs"));
419
+ if (sourceEntry) {
420
+ return { cmd: process.execPath, args: [sourceEntry, "gateway", "probe"] };
421
+ }
422
+ const commandPath = resolveExecutableInPath("openclaw");
423
+ if (commandPath) {
424
+ return { cmd: commandPath, args: ["gateway", "probe"] };
425
+ }
426
+ return null;
427
+ }
390
428
  function detectOpenClawRuntime(workspaceRoot) {
391
429
  const configuredGateway = readOpenClawConfiguredGateway(workspaceRoot);
430
+ const statusCommand = resolveOpenClawStatusCommand(workspaceRoot);
431
+ const gatewayProbeCommand = resolveOpenClawGatewayProbeCommand(workspaceRoot);
432
+ const statusProbe = statusCommand
433
+ ? (0, child_process_1.spawnSync)(statusCommand.cmd, statusCommand.args, {
434
+ encoding: "utf8",
435
+ env: process.env,
436
+ })
437
+ : null;
438
+ const statusStdout = String(statusProbe?.stdout || "");
439
+ const statusStderr = String(statusProbe?.stderr || "");
440
+ const statusLooksConfigured = Boolean(statusCommand &&
441
+ statusProbe &&
442
+ statusProbe.status === 0 &&
443
+ (/\bChannels\b/i.test(statusStdout) ||
444
+ /\bSessions\b/i.test(statusStdout) ||
445
+ /\bNext steps:\b/i.test(statusStdout)));
446
+ const gatewayStatusProbe = gatewayProbeCommand
447
+ ? (0, child_process_1.spawnSync)(gatewayProbeCommand.cmd, gatewayProbeCommand.args, {
448
+ encoding: "utf8",
449
+ env: process.env,
450
+ })
451
+ : null;
452
+ const gatewayStatusStdout = String(gatewayStatusProbe?.stdout || "");
453
+ const gatewayStatusStderr = String(gatewayStatusProbe?.stderr || "");
454
+ const gatewayProbeOk = Boolean(gatewayProbeCommand &&
455
+ gatewayStatusProbe &&
456
+ gatewayStatusProbe.status === 0);
392
457
  const result = (0, child_process_1.spawnSync)("ps", ["-Ao", "pid=,ppid=,command="], {
393
458
  encoding: "utf8",
394
459
  });
@@ -471,6 +536,12 @@ function detectOpenClawRuntime(workspaceRoot) {
471
536
  const allProcesses = Array.from(combinedProcesses.values());
472
537
  const gatewayReachable = gatewayListeners.length > 0;
473
538
  const detectionNotes = [];
539
+ if (statusProbe && statusProbe.status !== 0) {
540
+ detectionNotes.push(String(statusStderr || "openclaw status failed").trim());
541
+ }
542
+ if (gatewayStatusProbe && gatewayStatusProbe.status !== 0) {
543
+ detectionNotes.push(String(gatewayStatusStderr || "openclaw gateway probe failed").trim());
544
+ }
474
545
  if (result.status !== 0)
475
546
  detectionNotes.push(String(result.stderr || "ps failed").trim());
476
547
  if (gatewayProbe.status !== 0 && gatewayLines.length === 0) {
@@ -479,24 +550,52 @@ function detectOpenClawRuntime(workspaceRoot) {
479
550
  const gatewayPort = preferredListener?.port || configuredGateway.gateway_port;
480
551
  const gatewayUrl = `http://${OPENCLAW_GATEWAY_HOST}:${gatewayPort}/`;
481
552
  return {
482
- running: allProcesses.length > 0 || gatewayReachable,
553
+ running: gatewayProbeOk || allProcesses.length > 0 || gatewayReachable,
483
554
  process_count: allProcesses.length,
484
555
  processes: allProcesses.slice(0, 10),
485
556
  detection_error: detectionNotes.filter(Boolean).join(" | ") || null,
486
557
  gateway_url: gatewayUrl,
487
558
  gateway_port: gatewayPort,
488
559
  gateway_reachable: gatewayReachable,
560
+ status_command: statusCommand ? [statusCommand.cmd, ...statusCommand.args].join(" ") : null,
561
+ status_ok: statusLooksConfigured,
562
+ status_summary: statusLooksConfigured
563
+ ? statusStdout
564
+ .split("\n")
565
+ .map((line) => line.trim())
566
+ .filter(Boolean)
567
+ .slice(0, 6)
568
+ .join(" | ")
569
+ : null,
570
+ gateway_probe_command: gatewayProbeCommand ? [gatewayProbeCommand.cmd, ...gatewayProbeCommand.args].join(" ") : null,
571
+ gateway_probe_ok: gatewayProbeOk,
572
+ gateway_probe_summary: gatewayProbeOk
573
+ ? gatewayStatusStdout
574
+ .split("\n")
575
+ .map((line) => line.trim())
576
+ .filter(Boolean)
577
+ .slice(0, 4)
578
+ .join(" | ")
579
+ : null,
489
580
  configured_gateway_url: configuredGateway.gateway_url,
490
581
  configured_gateway_port: configuredGateway.gateway_port,
491
582
  configured_gateway_bind: configuredGateway.gateway_bind,
492
583
  configured_gateway_config_path: configuredGateway.config_path,
493
- detection_mode: processes.length > 0 && gatewayReachable
494
- ? "process+gateway"
495
- : gatewayReachable
496
- ? "gateway"
497
- : processes.length > 0
498
- ? "process"
499
- : "not_running",
584
+ detection_mode: gatewayProbeOk
585
+ ? (processes.length > 0 && gatewayReachable
586
+ ? "gateway-probe+process+gateway"
587
+ : gatewayReachable
588
+ ? "gateway-probe+gateway"
589
+ : processes.length > 0
590
+ ? "gateway-probe+process"
591
+ : "gateway-probe")
592
+ : processes.length > 0 && gatewayReachable
593
+ ? "process+gateway"
594
+ : gatewayReachable
595
+ ? "gateway"
596
+ : processes.length > 0
597
+ ? "process"
598
+ : "not_running",
500
599
  };
501
600
  }
502
601
  function detectOpenClawSkillInstallation() {
@@ -706,6 +805,10 @@ class LocalNodeService {
706
805
  broadcastCount = 0;
707
806
  lastMessageAt = 0;
708
807
  lastBroadcastAt = 0;
808
+ lastProfileBroadcastAt = 0;
809
+ lastProfileBroadcastSignature = "";
810
+ lastReplayBroadcastAt = 0;
811
+ lastReplayBroadcastSignature = "";
709
812
  lastBroadcastErrorAt = 0;
710
813
  lastBroadcastError = null;
711
814
  broadcastFailureCount = 0;
@@ -1955,14 +2058,13 @@ class LocalNodeService {
1955
2058
  profile: this.profile,
1956
2059
  };
1957
2060
  const presenceRecord = (0, core_1.signPresence)(this.identity, Date.now());
1958
- const indexRecords = (0, core_1.buildIndexRecords)(this.profile);
1959
- const replayMessages = this.getReplayableSelfSocialMessages();
2061
+ const shouldPublishProfile = this.shouldPublishProfileRecord(profileRecord, reason, presenceRecord.timestamp);
2062
+ const replayMessages = this.getReplayableSelfSocialMessages(reason);
1960
2063
  try {
1961
- await this.publish("profile", profileRecord);
1962
- await this.publish("presence", presenceRecord);
1963
- for (const record of indexRecords) {
1964
- await this.publish("index", record);
2064
+ if (shouldPublishProfile) {
2065
+ await this.publish("profile", profileRecord);
1965
2066
  }
2067
+ await this.publish("presence", presenceRecord);
1966
2068
  for (const message of replayMessages) {
1967
2069
  await this.publish(SOCIAL_MESSAGE_TOPIC, message);
1968
2070
  }
@@ -1984,14 +2086,27 @@ class LocalNodeService {
1984
2086
  this.consecutiveBroadcastFailures = 0;
1985
2087
  this.directory = (0, core_1.ingestProfileRecord)(this.directory, profileRecord);
1986
2088
  this.directory = (0, core_1.ingestPresenceRecord)(this.directory, presenceRecord);
1987
- for (const record of indexRecords) {
1988
- this.directory = (0, core_1.ingestIndexRecord)(this.directory, record);
1989
- }
1990
2089
  this.compactCacheInMemory();
1991
2090
  await this.persistCache();
1992
- await this.log("info", `Broadcast sent (${indexRecords.length} index refs, replayed_messages=${replayMessages.length}, reason=${reason})`);
2091
+ await this.log("info", `Broadcast sent (${shouldPublishProfile ? "profile + " : ""}presence, replayed_messages=${replayMessages.length}, reason=${reason})`);
1993
2092
  return { sent: true, reason };
1994
2093
  }
2094
+ shouldPublishProfileRecord(profileRecord, reason, now = Date.now()) {
2095
+ if (reason !== "interval") {
2096
+ this.lastProfileBroadcastSignature = profileRecord.profile.signature;
2097
+ this.lastProfileBroadcastAt = now;
2098
+ return true;
2099
+ }
2100
+ const signature = profileRecord.profile.signature;
2101
+ const changedSinceLastPublish = signature !== this.lastProfileBroadcastSignature;
2102
+ const refreshDue = now - this.lastProfileBroadcastAt >= PROFILE_RELAY_REFRESH_INTERVAL_MS;
2103
+ if (!changedSinceLastPublish && !refreshDue) {
2104
+ return false;
2105
+ }
2106
+ this.lastProfileBroadcastSignature = signature;
2107
+ this.lastProfileBroadcastAt = now;
2108
+ return true;
2109
+ }
1995
2110
  async maybeRecoverFromBroadcastFailure(reason, errorMessage) {
1996
2111
  const recoveryThreshold = 3;
1997
2112
  const recoveryCooldownMs = 60_000;
@@ -2776,16 +2891,30 @@ class LocalNodeService {
2776
2891
  hasSocialMessage(messageId) {
2777
2892
  return this.socialMessages.some((item) => item.message_id === messageId);
2778
2893
  }
2779
- getReplayableSelfSocialMessages(now = Date.now()) {
2894
+ getReplayableSelfSocialMessages(reason = "manual", now = Date.now()) {
2780
2895
  const maxCount = Math.max(0, SOCIAL_MESSAGE_REPLAY_MAX_PER_BROADCAST);
2781
2896
  if (!this.identity || maxCount === 0) {
2782
2897
  return [];
2783
2898
  }
2784
- return this.socialMessages
2899
+ const replayable = this.socialMessages
2785
2900
  .filter((item) => (item.agent_id === this.identity?.agent_id &&
2786
2901
  now - item.created_at <= SOCIAL_MESSAGE_REPLAY_WINDOW_MS))
2787
2902
  .sort((a, b) => a.created_at - b.created_at)
2788
2903
  .slice(-maxCount);
2904
+ if (!replayable.length) {
2905
+ this.lastReplayBroadcastSignature = "";
2906
+ return [];
2907
+ }
2908
+ const signature = replayable.map((item) => item.message_id).join(",");
2909
+ const isIntervalReplay = reason === "interval";
2910
+ const changedSinceLastReplay = signature !== this.lastReplayBroadcastSignature;
2911
+ const refreshDue = now - this.lastReplayBroadcastAt >= SOCIAL_MESSAGE_REPLAY_REFRESH_INTERVAL_MS;
2912
+ if (isIntervalReplay && !changedSinceLastReplay && !refreshDue) {
2913
+ return [];
2914
+ }
2915
+ this.lastReplayBroadcastSignature = signature;
2916
+ this.lastReplayBroadcastAt = now;
2917
+ return replayable;
2789
2918
  }
2790
2919
  hasRecentDuplicateMessage(agentId, body, topic, now = Date.now()) {
2791
2920
  return this.socialMessages.some((item) => (item.agent_id === agentId &&
@@ -22,6 +22,10 @@ type RelayPeer = {
22
22
  last_seen_at: number;
23
23
  messages_seen: number;
24
24
  reconnect_attempts: number;
25
+ meta?: {
26
+ signal_queue_size?: number;
27
+ relay_queue_size?: number;
28
+ };
25
29
  };
26
30
  type RelayDiagnostics = {
27
31
  adapter: "relay-preview";
@@ -109,7 +109,6 @@ class RelayPreviewAdapter {
109
109
  try {
110
110
  await this.joinRoom("start");
111
111
  this.started = true;
112
- await this.refreshPeers();
113
112
  await this.pollOnce();
114
113
  this.scheduleNextPoll(this.pollIntervalMs);
115
114
  this.recordDiscovery("signaling_connected", { endpoint: this.activeEndpoint });
@@ -258,8 +257,10 @@ class RelayPreviewAdapter {
258
257
  const payload = await this.get(`/peers?room=${encodeURIComponent(this.room)}`);
259
258
  this.lastPeerRefreshAt = Date.now();
260
259
  this.stats.peers_refresh_succeeded += 1;
261
- const peerIds = Array.isArray(payload?.peers) ? payload.peers : [];
262
- this.updatePeersFromList(peerIds);
260
+ const peerItems = Array.isArray(payload?.peer_details) && payload.peer_details.length
261
+ ? payload.peer_details
262
+ : Array.isArray(payload?.peers) ? payload.peers : [];
263
+ this.updatePeersFromList(peerItems);
263
264
  }
264
265
  onEnvelope(envelope) {
265
266
  this.stats.received_total += 1;
@@ -340,9 +341,13 @@ class RelayPreviewAdapter {
340
341
  }
341
342
  async joinRoom(reason) {
342
343
  this.stats.join_attempted += 1;
343
- await this.post("/join", { room: this.room, peer_id: this.peerId });
344
+ const payload = await this.post("/join", { room: this.room, peer_id: this.peerId });
344
345
  this.lastJoinAt = Date.now();
345
346
  this.stats.join_succeeded += 1;
347
+ if (Array.isArray(payload?.peers)) {
348
+ this.updatePeersFromList(payload.peers);
349
+ this.lastPeerRefreshAt = this.lastJoinAt;
350
+ }
346
351
  this.recordDiscovery("join_ok", { endpoint: this.activeEndpoint, detail: reason });
347
352
  }
348
353
  async maybeRefreshJoin(reason) {
@@ -407,13 +412,38 @@ class RelayPreviewAdapter {
407
412
  throw new Error(errors.join(" | "));
408
413
  }
409
414
  updatePeersFromList(values) {
410
- const peerIds = values.map((value) => String(value || "").trim()).filter(Boolean);
415
+ const parsedPeers = [];
416
+ for (const value of values) {
417
+ if (typeof value === "string") {
418
+ const peerId = String(value || "").trim();
419
+ if (peerId) {
420
+ parsedPeers.push({ peer_id: peerId });
421
+ }
422
+ continue;
423
+ }
424
+ if (value && typeof value === "object") {
425
+ const raw = value;
426
+ const peerId = String(raw.peer_id || "").trim();
427
+ if (!peerId) {
428
+ continue;
429
+ }
430
+ parsedPeers.push({
431
+ peer_id: peerId,
432
+ meta: {
433
+ signal_queue_size: Number(raw.signal_queue_size ?? 0),
434
+ relay_queue_size: Number(raw.relay_queue_size ?? 0),
435
+ },
436
+ });
437
+ }
438
+ }
439
+ const peerIds = parsedPeers.map((peer) => peer.peer_id);
411
440
  if (!peerIds.includes(this.peerId)) {
412
441
  void this.joinRoom("self_missing_from_peers").catch(() => { });
413
442
  }
414
443
  const now = Date.now();
415
444
  const next = new Map();
416
- for (const peerId of peerIds) {
445
+ for (const peerInfo of parsedPeers) {
446
+ const peerId = peerInfo.peer_id;
417
447
  if (peerId === this.peerId)
418
448
  continue;
419
449
  const existing = this.peers.get(peerId);
@@ -427,6 +457,7 @@ class RelayPreviewAdapter {
427
457
  last_seen_at: now,
428
458
  messages_seen: existing?.messages_seen ?? 0,
429
459
  reconnect_attempts: existing?.reconnect_attempts ?? 0,
460
+ meta: peerInfo.meta || existing?.meta,
430
461
  });
431
462
  }
432
463
  for (const peerId of this.peers.keys()) {
@@ -72,6 +72,7 @@ const APP_UPDATE_SESSION_KEY = 'silicaclaw_pending_updated_version';
72
72
  document.querySelector('.sidebar-version').title = t('common.version');
73
73
  setText('.sidebar-version__label', t('common.version'));
74
74
  document.getElementById('brandUpdateHint').textContent = t('labels.versionChecking');
75
+ document.getElementById('brandRelayHint').textContent = t('labels.relayQueuesHealthy');
75
76
  document.getElementById('brandCheckUpdateBtn').textContent = t('actions.checkUpdate');
76
77
  document.getElementById('brandUpdateBtn').textContent = t('actions.updateNow');
77
78
  document.getElementById('integrationStatusBar').textContent = t('social.barStatus', {
@@ -325,6 +326,8 @@ const APP_UPDATE_SESSION_KEY = 'silicaclaw_pending_updated_version';
325
326
  } = shell;
326
327
  let appUpdatePollTimer = null;
327
328
  let appUpdateCheckInFlight = false;
329
+ let relayQueueCheckInFlight = false;
330
+ let lastRelayQueueCheckAt = 0;
328
331
 
329
332
  function setAppUpdateUi({
330
333
  hint,
@@ -359,6 +362,45 @@ const APP_UPDATE_SESSION_KEY = 'silicaclaw_pending_updated_version';
359
362
  return t('labels.versionPlatformOther');
360
363
  }
361
364
 
365
+ function setRelayQueueUi({ hint = '', tone = 'ok', visible = false }) {
366
+ const hintEl = document.getElementById('brandRelayHint');
367
+ if (!hintEl) return;
368
+ hintEl.textContent = hint;
369
+ hintEl.classList.toggle('hidden', !visible || !hint);
370
+ hintEl.classList.remove('warn', 'danger');
371
+ if (tone === 'warn' || tone === 'danger') {
372
+ hintEl.classList.add(tone);
373
+ }
374
+ }
375
+
376
+ async function refreshRelayQueueStatus({ force = false } = {}) {
377
+ const now = Date.now();
378
+ if (relayQueueCheckInFlight) return null;
379
+ if (!force && now - lastRelayQueueCheckAt < 15_000) return null;
380
+ relayQueueCheckInFlight = true;
381
+ try {
382
+ const result = await api('/api/peers');
383
+ const peers = result.data || {};
384
+ const peerItems = Array.isArray(peers.items) ? peers.items : [];
385
+ const relayQueueMax = peerItems.reduce((max, peer) => Math.max(max, Number(peer?.meta?.relay_queue_size || 0)), 0);
386
+ const signalQueueMax = peerItems.reduce((max, peer) => Math.max(max, Number(peer?.meta?.signal_queue_size || 0)), 0);
387
+ const queueMax = Math.max(relayQueueMax, signalQueueMax);
388
+ if (queueMax >= 100) {
389
+ setRelayQueueUi({ hint: t('labels.relayQueuesHigh'), tone: 'danger', visible: true });
390
+ } else if (queueMax >= 20) {
391
+ setRelayQueueUi({ hint: t('labels.relayQueuesWatch'), tone: 'warn', visible: true });
392
+ } else {
393
+ setRelayQueueUi({ hint: t('labels.relayQueuesHealthy'), tone: 'ok', visible: true });
394
+ }
395
+ lastRelayQueueCheckAt = now;
396
+ return { relayQueueMax, signalQueueMax };
397
+ } catch (_error) {
398
+ return null;
399
+ } finally {
400
+ relayQueueCheckInFlight = false;
401
+ }
402
+ }
403
+
362
404
  async function refreshAppUpdateStatus({ silent = false } = {}) {
363
405
  if (appUpdateCheckInFlight) return null;
364
406
  appUpdateCheckInFlight = true;
@@ -646,7 +688,7 @@ const APP_UPDATE_SESSION_KEY = 'silicaclaw_pending_updated_version';
646
688
  let autoRefreshInFlight = false;
647
689
 
648
690
  async function refreshActiveView() {
649
- const tasks = [refreshPublicProfilePreview()];
691
+ const tasks = [refreshPublicProfilePreview(), refreshRelayQueueStatus()];
650
692
  if (activeTab === 'overview') {
651
693
  tasks.push(refreshOverview(), refreshMessages(), refreshSocial());
652
694
  } else if (activeTab === 'agent') {
@@ -682,7 +724,7 @@ const APP_UPDATE_SESSION_KEY = 'silicaclaw_pending_updated_version';
682
724
  }
683
725
 
684
726
  async function refreshAll() {
685
- const tasks = [refreshOverview(), refreshNetwork(), refreshSocial(), refreshSkills(), refreshPublicProfilePreview(), refreshMessages()];
727
+ const tasks = [refreshOverview(), refreshNetwork(), refreshSocial(), refreshSkills(), refreshPublicProfilePreview(), refreshMessages(), refreshRelayQueueStatus({ force: true })];
686
728
  if (activeTab === 'network') {
687
729
  tasks.push(refreshPeers(), refreshDiscovery(), refreshLogs());
688
730
  }
@@ -756,6 +798,7 @@ const APP_UPDATE_SESSION_KEY = 'silicaclaw_pending_updated_version';
756
798
  if (!document.hidden) {
757
799
  refreshAuto().catch(() => {});
758
800
  refreshAppUpdateStatus({ silent: true }).catch(() => {});
801
+ refreshRelayQueueStatus({ force: true }).catch(() => {});
759
802
  }
760
803
  });
761
804
  setInterval(refreshAuto, 4000);
@@ -17,6 +17,18 @@ export function createNetworkController({
17
17
  let lastPeersRenderKey = "";
18
18
  let lastDiscoveryRenderKey = "";
19
19
 
20
+ function queueState(value) {
21
+ const n = Number(value || 0);
22
+ if (n >= 100) return { tone: "danger", label: t("labels.queueHigh") };
23
+ if (n >= 20) return { tone: "warn", label: t("labels.queueWatch") };
24
+ return { tone: "ok", label: t("labels.queueHealthy") };
25
+ }
26
+
27
+ function queueBadge(value) {
28
+ const state = queueState(value);
29
+ return `<span class="pill ${state.tone}">${Number(value || 0)} · ${state.label}</span>`;
30
+ }
31
+
20
32
  async function refreshNetwork() {
21
33
  const [cfg, sts, rtp] = await Promise.all([api("/api/network/config"), api("/api/network/stats"), api("/api/runtime/paths")]);
22
34
  const c = cfg.data;
@@ -183,6 +195,11 @@ export function createNetworkController({
183
195
  const peers = peerRes.data || {};
184
196
  const ds = statsRes.data?.adapter_discovery_stats || {};
185
197
  const summary = peers.diagnostics_summary || {};
198
+ const peerItems = Array.isArray(peers.items) ? peers.items : [];
199
+ const relayQueueTotal = peerItems.reduce((sum, peer) => sum + Number(peer?.meta?.relay_queue_size || 0), 0);
200
+ const relayQueueMax = peerItems.reduce((max, peer) => Math.max(max, Number(peer?.meta?.relay_queue_size || 0)), 0);
201
+ const signalQueueTotal = peerItems.reduce((sum, peer) => sum + Number(peer?.meta?.signal_queue_size || 0), 0);
202
+ const signalQueueMax = peerItems.reduce((max, peer) => Math.max(max, Number(peer?.meta?.signal_queue_size || 0)), 0);
186
203
  const peerCardsHtml = [
187
204
  [t("network.total"), peers.total || 0],
188
205
  [t("overview.online"), peers.online || 0],
@@ -198,6 +215,10 @@ export function createNetworkController({
198
215
  [t("network.seedPeers"), summary.seed_peers_count ?? 0],
199
216
  [t("network.discoveryEvents"), summary.discovery_events_total ?? 0],
200
217
  [t("network.activeWebrtcPeers"), summary.active_webrtc_peers ?? "-"],
218
+ ["Relay queue", queueBadge(relayQueueTotal)],
219
+ ["Max relay queue", queueBadge(relayQueueMax)],
220
+ ["Signal queue", queueBadge(signalQueueTotal)],
221
+ ["Max signal queue", queueBadge(signalQueueMax)],
201
222
  [t("network.observeCalls"), ds.observe_calls || 0],
202
223
  [t("network.heartbeats"), ds.heartbeat_sent || 0],
203
224
  [t("network.peersAdded"), ds.peers_added || 0],
@@ -213,9 +234,9 @@ export function createNetworkController({
213
234
  ? `<div class="empty-state">${t("network.noPeersDiscovered")}</div>`
214
235
  : `
215
236
  <table class="table">
216
- <thead><tr><th>${t("network.peer")}</th><th>${t("network.status")}</th><th>${t("network.lastSeen")}</th><th>${t("network.staleSince")}</th><th>${t("network.messages")}</th><th>${t("network.firstSeen")}</th><th>${t("network.meta")}</th></tr></thead>
237
+ <thead><tr><th>${t("network.peer")}</th><th>${t("network.status")}</th><th>${t("network.lastSeen")}</th><th>${t("network.staleSince")}</th><th>${t("network.messages")}</th><th>${t("network.firstSeen")}</th><th>Relay Q</th><th>Signal Q</th><th>${t("network.meta")}</th></tr></thead>
217
238
  <tbody>
218
- ${peers.items.map((peer) => `
239
+ ${peerItems.map((peer) => `
219
240
  <tr>
220
241
  <td class="mono">${shortId(peer.peer_id)}</td>
221
242
  <td class="${peer.status === "online" ? "online" : peer.status === "offline" ? "offline" : "stale"}">${peerStatusText(peer.status)}</td>
@@ -223,6 +244,8 @@ export function createNetworkController({
223
244
  <td>${peer.stale_since_at ? ago(peer.stale_since_at) : "-"}</td>
224
245
  <td>${peer.messages_seen || 0}</td>
225
246
  <td>${new Date(peer.first_seen_at).toLocaleTimeString()}</td>
247
+ <td>${queueBadge(Number(peer?.meta?.relay_queue_size || 0))}</td>
248
+ <td>${queueBadge(Number(peer?.meta?.signal_queue_size || 0))}</td>
226
249
  <td class="mono">${peer.meta ? JSON.stringify(peer.meta) : "-"}</td>
227
250
  </tr>
228
251
  `).join("")}
@@ -248,14 +271,22 @@ export function createNetworkController({
248
271
  peers_added: ds.peers_added || 0,
249
272
  peers_removed: ds.peers_removed || 0,
250
273
  },
251
- items: Array.isArray(peers.items)
252
- ? peers.items.map((peer) => [
274
+ queues: {
275
+ relay_total: relayQueueTotal,
276
+ relay_max: relayQueueMax,
277
+ signal_total: signalQueueTotal,
278
+ signal_max: signalQueueMax,
279
+ },
280
+ items: peerItems
281
+ ? peerItems.map((peer) => [
253
282
  peer.peer_id,
254
283
  peer.status || "",
255
284
  peer.last_seen_at || 0,
256
285
  peer.stale_since_at || 0,
257
286
  peer.messages_seen || 0,
258
287
  peer.first_seen_at || 0,
288
+ Number(peer?.meta?.relay_queue_size || 0),
289
+ Number(peer?.meta?.signal_queue_size || 0),
259
290
  peer.meta ? JSON.stringify(peer.meta) : "",
260
291
  ])
261
292
  : [],
@@ -446,6 +446,7 @@ export function createSocialController({
446
446
  [t("social.ownerForwardReady"), ownerDelivery.ready ? t("common.yes") : t("common.no")],
447
447
  [t("social.ownerForwardCommand"), ownerDelivery.forward_command_configured ? t("common.yes") : t("common.no")],
448
448
  [t("social.openclawDetectionMode"), bridge.openclaw_runtime?.detection_mode || "-"],
449
+ ["Gateway probe", bridge.openclaw_runtime?.gateway_probe_ok ? t("common.yes") : t("common.no")],
449
450
  [t("social.openclawGateway"), bridge.openclaw_runtime?.gateway_url || "-"],
450
451
  [t("social.installMode"), skillLearning.install_mode || "-"],
451
452
  [t("social.installedPath"), skillInstalled ? installedSkillPath : "-"],