@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
@@ -594,6 +594,40 @@
594
594
  overflow: hidden;
595
595
  text-overflow: ellipsis;
596
596
  }
597
+ .sidebar-version__relay {
598
+ display: inline-flex;
599
+ align-items: center;
600
+ gap: 6px;
601
+ font-size: 10px;
602
+ line-height: 1.2;
603
+ color: var(--muted);
604
+ white-space: nowrap;
605
+ overflow: hidden;
606
+ text-overflow: ellipsis;
607
+ }
608
+ .sidebar-version__relay::before {
609
+ content: "";
610
+ width: 7px;
611
+ height: 7px;
612
+ border-radius: 999px;
613
+ background: var(--ok);
614
+ box-shadow: 0 0 0 4px color-mix(in srgb, var(--ok) 12%, transparent);
615
+ flex: 0 0 auto;
616
+ }
617
+ .sidebar-version__relay.warn {
618
+ color: color-mix(in srgb, var(--warn) 86%, var(--text));
619
+ }
620
+ .sidebar-version__relay.warn::before {
621
+ background: var(--warn);
622
+ box-shadow: 0 0 0 4px color-mix(in srgb, var(--warn) 12%, transparent);
623
+ }
624
+ .sidebar-version__relay.danger {
625
+ color: color-mix(in srgb, var(--danger) 86%, var(--text));
626
+ }
627
+ .sidebar-version__relay.danger::before {
628
+ background: var(--danger);
629
+ box-shadow: 0 0 0 4px color-mix(in srgb, var(--danger) 12%, transparent);
630
+ }
597
631
  .sidebar-version__actions {
598
632
  display: flex;
599
633
  align-items: center;
@@ -869,6 +903,7 @@
869
903
  .pill.ok { color: var(--ok); border-color: rgba(34, 197, 94, 0.45); background: rgba(34, 197, 94, 0.08); }
870
904
  .pill.warn { color: var(--warn); border-color: rgba(245, 158, 11, 0.45); background: rgba(245, 158, 11, 0.08); }
871
905
  .pill.danger { color: var(--danger); border-color: rgba(239, 68, 68, 0.42); background: rgba(239, 68, 68, 0.08); }
906
+ .pill.ok { color: var(--ok); border-color: color-mix(in srgb, var(--ok) 42%, transparent); background: color-mix(in srgb, var(--ok) 10%, transparent); }
872
907
 
873
908
  .notice {
874
909
  display: none;
@@ -101,6 +101,7 @@ export const appTemplate = String.raw`<div class="app" id="appShell">
101
101
  <span class="sidebar-version__label">Version</span>
102
102
  <span class="sidebar-version__text" id="brandVersion">-</span>
103
103
  <span class="sidebar-version__hint" id="brandUpdateHint">Checking for updates...</span>
104
+ <span class="sidebar-version__relay hidden" id="brandRelayHint">Relay queues are healthy.</span>
104
105
  </div>
105
106
  <div class="sidebar-version__actions">
106
107
  <button class="sidebar-version__btn sidebar-version__btn--ghost hidden" id="brandCheckUpdateBtn" type="button">Check</button>
@@ -86,7 +86,7 @@ export const TRANSLATIONS = {
86
86
  showLess: 'Show Less',
87
87
  showMoreCount: 'Show {count} More',
88
88
  openclawNotInstalled: 'OpenClaw Not Installed Here',
89
- openclawNotRunning: 'OpenClaw Not Running',
89
+ openclawNotRunning: 'OpenClaw Gateway Not Running',
90
90
  openclawSkillLearned: 'Skill Already Learned',
91
91
  silicaClawSkillsInstalled: 'Skills Already Installed',
92
92
  },
@@ -146,6 +146,9 @@ export const TRANSLATIONS = {
146
146
  versionPlatformMac: 'macOS service will restart automatically',
147
147
  versionPlatformLinux: 'Linux service will restart automatically',
148
148
  versionPlatformOther: 'Local service will refresh after the update',
149
+ relayQueuesHealthy: 'Relay queues are healthy.',
150
+ relayQueuesWatch: 'Relay queues need attention.',
151
+ relayQueuesHigh: 'Relay queues are building up.',
149
152
  networkEyebrow: 'Network',
150
153
  connectionSummary: 'Connection',
151
154
  quickActions: 'Broadcast',
@@ -216,6 +219,9 @@ export const TRANSLATIONS = {
216
219
  duplicateWindowSeconds: 'Duplicate Window (seconds)',
217
220
  blockedAgentIds: 'Blocked agent IDs (agent_id, comma separated)',
218
221
  blockedTerms: 'Blocked Terms (comma separated)',
222
+ queueHealthy: 'Healthy',
223
+ queueWatch: 'Watch',
224
+ queueHigh: 'High',
219
225
  },
220
226
  hints: {
221
227
  publicDiscoverySwitch: 'Use Profile -> Public Enabled as the single public visibility switch.',
@@ -585,7 +591,7 @@ export const TRANSLATIONS = {
585
591
  openclawSkillNotInstalled: 'The SilicaClaw broadcast skill is not installed yet.',
586
592
  openclawSkillInstallFailed: 'OpenClaw skill installation failed',
587
593
  openclawRoleBroadcasterOnly: 'This computer is broadcaster-only. Install OpenClaw here to learn broadcasts.',
588
- openclawRoleNotRunning: 'OpenClaw is installed here, but not running yet.',
594
+ openclawRoleNotRunning: 'OpenClaw is configured here, but the gateway is not running yet.',
589
595
  openclawRoleReadyToLearn: 'OpenClaw is installed here. Learn the broadcast skill to keep going.',
590
596
  openclawRoleLearned: 'This computer is ready to learn broadcasts and forward useful updates.',
591
597
  openclawRoleLearningOnly: 'Broadcast learning works, but owner forwarding is not ready yet.',
@@ -688,7 +694,7 @@ export const TRANSLATIONS = {
688
694
  showLess: '收起',
689
695
  showMoreCount: '再显示 {count} 个',
690
696
  openclawNotInstalled: '本机未安装 OpenClaw',
691
- openclawNotRunning: 'OpenClaw 未启动',
697
+ openclawNotRunning: 'OpenClaw 网关未启动',
692
698
  openclawSkillLearned: '技能已学会',
693
699
  silicaClawSkillsInstalled: '技能已全部安装',
694
700
  },
@@ -735,6 +741,9 @@ export const TRANSLATIONS = {
735
741
  versionPlatformMac: 'macOS 服务会自动重启',
736
742
  versionPlatformLinux: 'Linux 服务会自动重启',
737
743
  versionPlatformOther: '更新后本地服务会自动刷新',
744
+ relayQueuesHealthy: 'Relay 队列正常。',
745
+ relayQueuesWatch: 'Relay 队列需要关注。',
746
+ relayQueuesHigh: 'Relay 队列正在堆积。',
738
747
  profileEyebrow: '资料',
739
748
  publicProfile: '公开资料',
740
749
  publicProfileEditor: '公开资料编辑器',
@@ -818,6 +827,9 @@ export const TRANSLATIONS = {
818
827
  duplicateWindowSeconds: '重复消息窗口(秒)',
819
828
  blockedAgentIds: '已屏蔽代理 ID(agent_id,逗号分隔)',
820
829
  blockedTerms: '已屏蔽词(逗号分隔)',
830
+ queueHealthy: '正常',
831
+ queueWatch: '注意',
832
+ queueHigh: '偏高',
821
833
  },
822
834
  hints: {
823
835
  publicDiscoverySwitch: '使用资料 -> Public Enabled 作为唯一的公开可见性开关。',
@@ -1008,7 +1020,7 @@ export const TRANSLATIONS = {
1008
1020
  homeDegraded: '降级',
1009
1021
  homeRunning: '运行中',
1010
1022
  homeStopped: '未启动',
1011
- homeInstalledOnly: '仅检测到安装痕迹',
1023
+ homeInstalledOnly: '已检测到 OpenClaw 配置',
1012
1024
  homeGlobalReady: '全网预览已启用',
1013
1025
  homeNotGlobal: '当前不是全网预览',
1014
1026
  homeBriefNetwork: '网络',
@@ -1020,7 +1032,7 @@ export const TRANSLATIONS = {
1020
1032
  homeBriefActionBroadcast: '保持公开广播,让运行 OpenClaw 的机器可以学习这个代理。',
1021
1033
  homeBriefActionStabilize: '先修复 relay 或广播健康度,再依赖主人转发链路。',
1022
1034
  homeMetaRunning: '已经检测到本机 OpenClaw 进程。',
1023
- homeMetaNotRunning: '要开始学习广播,前提是本机 OpenClaw 正在运行。',
1035
+ homeMetaNotRunning: '要开始学习广播,前提是本机 OpenClaw 网关正在运行。',
1024
1036
  homeMetaGlobal: '当前默认使用全网 relay 预览模式。',
1025
1037
  homeMetaNotGlobal: '这台机器当前还没有走全网 relay 路径。',
1026
1038
  homeMetaPeers: '从本机视角看到 {online} 个在线代理,累计发现 {discovered} 个代理。',
@@ -1187,7 +1199,7 @@ export const TRANSLATIONS = {
1187
1199
  openclawSkillNotInstalled: 'SilicaClaw 广播技能还没有安装。',
1188
1200
  openclawSkillInstallFailed: 'OpenClaw 技能安装失败',
1189
1201
  openclawRoleBroadcasterOnly: '这台电脑当前只能广播。想学习广播,先在这里安装 OpenClaw。',
1190
- openclawRoleNotRunning: '这台电脑已经安装了 OpenClaw,但还没启动。',
1202
+ openclawRoleNotRunning: '这台电脑已经配置了 OpenClaw,但网关还没启动。',
1191
1203
  openclawRoleReadyToLearn: '这台电脑已经装了 OpenClaw。先学习广播技能,再继续。',
1192
1204
  openclawRoleLearned: '这台电脑已经可以学习广播,并转发有用更新。',
1193
1205
  openclawRoleLearningOnly: '已经能学习广播,但主人推送还没准备好。',
@@ -17,7 +17,6 @@ import {
17
17
  PublicProfileSummary,
18
18
  SignedProfileRecord,
19
19
  buildPublicProfileSummary,
20
- buildIndexRecords,
21
20
  cleanupExpiredPresence,
22
21
  createDefaultProfileInput,
23
22
  createEmptyDirectoryState,
@@ -116,6 +115,12 @@ const SOCIAL_MESSAGE_MAX_AGE_MS = Number(process.env.SOCIAL_MESSAGE_MAX_AGE_MS |
116
115
  const SOCIAL_MESSAGE_OBSERVATION_HISTORY_LIMIT = Number(process.env.SOCIAL_MESSAGE_OBSERVATION_HISTORY_LIMIT || 500);
117
116
  const SOCIAL_MESSAGE_REPLAY_WINDOW_MS = Number(process.env.SOCIAL_MESSAGE_REPLAY_WINDOW_MS || 10 * 60_000);
118
117
  const SOCIAL_MESSAGE_REPLAY_MAX_PER_BROADCAST = Number(process.env.SOCIAL_MESSAGE_REPLAY_MAX_PER_BROADCAST || 3);
118
+ const SOCIAL_MESSAGE_REPLAY_REFRESH_INTERVAL_MS = Number(
119
+ process.env.SOCIAL_MESSAGE_REPLAY_REFRESH_INTERVAL_MS || 120_000
120
+ );
121
+ const PROFILE_RELAY_REFRESH_INTERVAL_MS = Number(
122
+ process.env.PROFILE_RELAY_REFRESH_INTERVAL_MS || 120_000
123
+ );
119
124
  const SOCIAL_MESSAGE_BLOCKED_AGENT_IDS = new Set(
120
125
  dedupeStrings(parseListEnv(process.env.SOCIAL_MESSAGE_BLOCKED_AGENT_IDS || ""))
121
126
  );
@@ -450,8 +455,85 @@ function readOpenClawConfiguredGateway(workspaceRoot: string) {
450
455
  } as const;
451
456
  }
452
457
 
458
+ function resolveOpenClawStatusCommand(workspaceRoot: string) {
459
+ const explicitBin = String(process.env.OPENCLAW_BIN || "").trim();
460
+ if (explicitBin) {
461
+ return { cmd: explicitBin, args: ["status"] } as const;
462
+ }
463
+
464
+ const configuredSourceDir = String(process.env.OPENCLAW_SOURCE_DIR || "").trim();
465
+ const defaultSourceDir = defaultOpenClawSourceDir(workspaceRoot);
466
+ const sourceDir = configuredSourceDir || defaultSourceDir;
467
+ const sourceEntry = existingPathOrNull(resolve(sourceDir, "openclaw.mjs"));
468
+ if (sourceEntry) {
469
+ return { cmd: process.execPath, args: [sourceEntry, "status"] } as const;
470
+ }
471
+
472
+ const commandPath = resolveExecutableInPath("openclaw");
473
+ if (commandPath) {
474
+ return { cmd: commandPath, args: ["status"] } as const;
475
+ }
476
+
477
+ return null;
478
+ }
479
+
480
+ function resolveOpenClawGatewayProbeCommand(workspaceRoot: string) {
481
+ const explicitBin = String(process.env.OPENCLAW_BIN || "").trim();
482
+ if (explicitBin) {
483
+ return { cmd: explicitBin, args: ["gateway", "probe"] } as const;
484
+ }
485
+
486
+ const configuredSourceDir = String(process.env.OPENCLAW_SOURCE_DIR || "").trim();
487
+ const defaultSourceDir = defaultOpenClawSourceDir(workspaceRoot);
488
+ const sourceDir = configuredSourceDir || defaultSourceDir;
489
+ const sourceEntry = existingPathOrNull(resolve(sourceDir, "openclaw.mjs"));
490
+ if (sourceEntry) {
491
+ return { cmd: process.execPath, args: [sourceEntry, "gateway", "probe"] } as const;
492
+ }
493
+
494
+ const commandPath = resolveExecutableInPath("openclaw");
495
+ if (commandPath) {
496
+ return { cmd: commandPath, args: ["gateway", "probe"] } as const;
497
+ }
498
+
499
+ return null;
500
+ }
501
+
453
502
  function detectOpenClawRuntime(workspaceRoot: string) {
454
503
  const configuredGateway = readOpenClawConfiguredGateway(workspaceRoot);
504
+ const statusCommand = resolveOpenClawStatusCommand(workspaceRoot);
505
+ const gatewayProbeCommand = resolveOpenClawGatewayProbeCommand(workspaceRoot);
506
+ const statusProbe = statusCommand
507
+ ? spawnSync(statusCommand.cmd, statusCommand.args, {
508
+ encoding: "utf8",
509
+ env: process.env,
510
+ })
511
+ : null;
512
+ const statusStdout = String(statusProbe?.stdout || "");
513
+ const statusStderr = String(statusProbe?.stderr || "");
514
+ const statusLooksConfigured = Boolean(
515
+ statusCommand &&
516
+ statusProbe &&
517
+ statusProbe.status === 0 &&
518
+ (
519
+ /\bChannels\b/i.test(statusStdout) ||
520
+ /\bSessions\b/i.test(statusStdout) ||
521
+ /\bNext steps:\b/i.test(statusStdout)
522
+ )
523
+ );
524
+ const gatewayStatusProbe = gatewayProbeCommand
525
+ ? spawnSync(gatewayProbeCommand.cmd, gatewayProbeCommand.args, {
526
+ encoding: "utf8",
527
+ env: process.env,
528
+ })
529
+ : null;
530
+ const gatewayStatusStdout = String(gatewayStatusProbe?.stdout || "");
531
+ const gatewayStatusStderr = String(gatewayStatusProbe?.stderr || "");
532
+ const gatewayProbeOk = Boolean(
533
+ gatewayProbeCommand &&
534
+ gatewayStatusProbe &&
535
+ gatewayStatusProbe.status === 0
536
+ );
455
537
  const result = spawnSync("ps", ["-Ao", "pid=,ppid=,command="], {
456
538
  encoding: "utf8",
457
539
  });
@@ -535,6 +617,12 @@ function detectOpenClawRuntime(workspaceRoot: string) {
535
617
  const allProcesses = Array.from(combinedProcesses.values());
536
618
  const gatewayReachable = gatewayListeners.length > 0;
537
619
  const detectionNotes = [];
620
+ if (statusProbe && statusProbe.status !== 0) {
621
+ detectionNotes.push(String(statusStderr || "openclaw status failed").trim());
622
+ }
623
+ if (gatewayStatusProbe && gatewayStatusProbe.status !== 0) {
624
+ detectionNotes.push(String(gatewayStatusStderr || "openclaw gateway probe failed").trim());
625
+ }
538
626
  if (result.status !== 0) detectionNotes.push(String(result.stderr || "ps failed").trim());
539
627
  if (gatewayProbe.status !== 0 && gatewayLines.length === 0) {
540
628
  detectionNotes.push(String(gatewayProbe.stderr || "lsof failed").trim());
@@ -543,19 +631,49 @@ function detectOpenClawRuntime(workspaceRoot: string) {
543
631
  const gatewayUrl = `http://${OPENCLAW_GATEWAY_HOST}:${gatewayPort}/`;
544
632
 
545
633
  return {
546
- running: allProcesses.length > 0 || gatewayReachable,
634
+ running: gatewayProbeOk || allProcesses.length > 0 || gatewayReachable,
547
635
  process_count: allProcesses.length,
548
636
  processes: allProcesses.slice(0, 10),
549
637
  detection_error: detectionNotes.filter(Boolean).join(" | ") || null,
550
638
  gateway_url: gatewayUrl,
551
639
  gateway_port: gatewayPort,
552
640
  gateway_reachable: gatewayReachable,
641
+ status_command: statusCommand ? [statusCommand.cmd, ...statusCommand.args].join(" ") : null,
642
+ status_ok: statusLooksConfigured,
643
+ status_summary: statusLooksConfigured
644
+ ? statusStdout
645
+ .split("\n")
646
+ .map((line) => line.trim())
647
+ .filter(Boolean)
648
+ .slice(0, 6)
649
+ .join(" | ")
650
+ : null,
651
+ gateway_probe_command: gatewayProbeCommand ? [gatewayProbeCommand.cmd, ...gatewayProbeCommand.args].join(" ") : null,
652
+ gateway_probe_ok: gatewayProbeOk,
653
+ gateway_probe_summary: gatewayProbeOk
654
+ ? gatewayStatusStdout
655
+ .split("\n")
656
+ .map((line) => line.trim())
657
+ .filter(Boolean)
658
+ .slice(0, 4)
659
+ .join(" | ")
660
+ : null,
553
661
  configured_gateway_url: configuredGateway.gateway_url,
554
662
  configured_gateway_port: configuredGateway.gateway_port,
555
663
  configured_gateway_bind: configuredGateway.gateway_bind,
556
664
  configured_gateway_config_path: configuredGateway.config_path,
557
665
  detection_mode:
558
- processes.length > 0 && gatewayReachable
666
+ gatewayProbeOk
667
+ ? (
668
+ processes.length > 0 && gatewayReachable
669
+ ? "gateway-probe+process+gateway"
670
+ : gatewayReachable
671
+ ? "gateway-probe+gateway"
672
+ : processes.length > 0
673
+ ? "gateway-probe+process"
674
+ : "gateway-probe"
675
+ )
676
+ : processes.length > 0 && gatewayReachable
559
677
  ? "process+gateway"
560
678
  : gatewayReachable
561
679
  ? "gateway"
@@ -828,11 +946,17 @@ type OpenClawBridgeStatus = {
828
946
  gateway_url: string;
829
947
  gateway_port: number;
830
948
  gateway_reachable: boolean;
949
+ status_command: string | null;
950
+ status_ok: boolean;
951
+ status_summary: string | null;
952
+ gateway_probe_command: string | null;
953
+ gateway_probe_ok: boolean;
954
+ gateway_probe_summary: string | null;
831
955
  configured_gateway_url: string;
832
956
  configured_gateway_port: number;
833
957
  configured_gateway_bind: string | null;
834
958
  configured_gateway_config_path: string | null;
835
- detection_mode: "process" | "gateway" | "process+gateway" | "not_running";
959
+ detection_mode: "gateway-probe" | "gateway-probe+process" | "gateway-probe+gateway" | "gateway-probe+process+gateway" | "process" | "gateway" | "process+gateway" | "not_running";
836
960
  };
837
961
  skill_learning: {
838
962
  available: boolean;
@@ -922,6 +1046,10 @@ export class LocalNodeService {
922
1046
  private broadcastCount = 0;
923
1047
  private lastMessageAt = 0;
924
1048
  private lastBroadcastAt = 0;
1049
+ private lastProfileBroadcastAt = 0;
1050
+ private lastProfileBroadcastSignature = "";
1051
+ private lastReplayBroadcastAt = 0;
1052
+ private lastReplayBroadcastSignature = "";
925
1053
  private lastBroadcastErrorAt = 0;
926
1054
  private lastBroadcastError: string | null = null;
927
1055
  private broadcastFailureCount = 0;
@@ -2280,15 +2408,14 @@ export class LocalNodeService {
2280
2408
  profile: this.profile,
2281
2409
  };
2282
2410
  const presenceRecord = signPresence(this.identity, Date.now());
2283
- const indexRecords = buildIndexRecords(this.profile);
2284
- const replayMessages = this.getReplayableSelfSocialMessages();
2411
+ const shouldPublishProfile = this.shouldPublishProfileRecord(profileRecord, reason, presenceRecord.timestamp);
2412
+ const replayMessages = this.getReplayableSelfSocialMessages(reason);
2285
2413
 
2286
2414
  try {
2287
- await this.publish("profile", profileRecord);
2288
- await this.publish("presence", presenceRecord);
2289
- for (const record of indexRecords) {
2290
- await this.publish("index", record);
2415
+ if (shouldPublishProfile) {
2416
+ await this.publish("profile", profileRecord);
2291
2417
  }
2418
+ await this.publish("presence", presenceRecord);
2292
2419
  for (const message of replayMessages) {
2293
2420
  await this.publish(SOCIAL_MESSAGE_TOPIC, message);
2294
2421
  }
@@ -2311,19 +2438,37 @@ export class LocalNodeService {
2311
2438
 
2312
2439
  this.directory = ingestProfileRecord(this.directory, profileRecord);
2313
2440
  this.directory = ingestPresenceRecord(this.directory, presenceRecord);
2314
- for (const record of indexRecords) {
2315
- this.directory = ingestIndexRecord(this.directory, record);
2316
- }
2317
2441
  this.compactCacheInMemory();
2318
2442
  await this.persistCache();
2319
2443
 
2320
2444
  await this.log(
2321
2445
  "info",
2322
- `Broadcast sent (${indexRecords.length} index refs, replayed_messages=${replayMessages.length}, reason=${reason})`
2446
+ `Broadcast sent (${shouldPublishProfile ? "profile + " : ""}presence, replayed_messages=${replayMessages.length}, reason=${reason})`
2323
2447
  );
2324
2448
  return { sent: true, reason };
2325
2449
  }
2326
2450
 
2451
+ private shouldPublishProfileRecord(
2452
+ profileRecord: SignedProfileRecord,
2453
+ reason: string,
2454
+ now = Date.now()
2455
+ ): boolean {
2456
+ if (reason !== "interval") {
2457
+ this.lastProfileBroadcastSignature = profileRecord.profile.signature;
2458
+ this.lastProfileBroadcastAt = now;
2459
+ return true;
2460
+ }
2461
+ const signature = profileRecord.profile.signature;
2462
+ const changedSinceLastPublish = signature !== this.lastProfileBroadcastSignature;
2463
+ const refreshDue = now - this.lastProfileBroadcastAt >= PROFILE_RELAY_REFRESH_INTERVAL_MS;
2464
+ if (!changedSinceLastPublish && !refreshDue) {
2465
+ return false;
2466
+ }
2467
+ this.lastProfileBroadcastSignature = signature;
2468
+ this.lastProfileBroadcastAt = now;
2469
+ return true;
2470
+ }
2471
+
2327
2472
  private async maybeRecoverFromBroadcastFailure(reason: string, errorMessage: string): Promise<void> {
2328
2473
  const recoveryThreshold = 3;
2329
2474
  const recoveryCooldownMs = 60_000;
@@ -3213,18 +3358,32 @@ export class LocalNodeService {
3213
3358
  return this.socialMessages.some((item) => item.message_id === messageId);
3214
3359
  }
3215
3360
 
3216
- private getReplayableSelfSocialMessages(now = Date.now()): SocialMessageRecord[] {
3361
+ private getReplayableSelfSocialMessages(reason = "manual", now = Date.now()): SocialMessageRecord[] {
3217
3362
  const maxCount = Math.max(0, SOCIAL_MESSAGE_REPLAY_MAX_PER_BROADCAST);
3218
3363
  if (!this.identity || maxCount === 0) {
3219
3364
  return [];
3220
3365
  }
3221
- return this.socialMessages
3366
+ const replayable = this.socialMessages
3222
3367
  .filter((item) => (
3223
3368
  item.agent_id === this.identity?.agent_id &&
3224
3369
  now - item.created_at <= SOCIAL_MESSAGE_REPLAY_WINDOW_MS
3225
3370
  ))
3226
3371
  .sort((a, b) => a.created_at - b.created_at)
3227
3372
  .slice(-maxCount);
3373
+ if (!replayable.length) {
3374
+ this.lastReplayBroadcastSignature = "";
3375
+ return [];
3376
+ }
3377
+ const signature = replayable.map((item) => item.message_id).join(",");
3378
+ const isIntervalReplay = reason === "interval";
3379
+ const changedSinceLastReplay = signature !== this.lastReplayBroadcastSignature;
3380
+ const refreshDue = now - this.lastReplayBroadcastAt >= SOCIAL_MESSAGE_REPLAY_REFRESH_INTERVAL_MS;
3381
+ if (isIntervalReplay && !changedSinceLastReplay && !refreshDue) {
3382
+ return [];
3383
+ }
3384
+ this.lastReplayBroadcastSignature = signature;
3385
+ this.lastReplayBroadcastAt = now;
3386
+ return replayable;
3228
3387
  }
3229
3388
 
3230
3389
  private hasRecentDuplicateMessage(agentId: string, body: string, topic: string, now = Date.now()): boolean {
@@ -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()) {
@@ -34,6 +34,10 @@ type RelayPeer = {
34
34
  last_seen_at: number;
35
35
  messages_seen: number;
36
36
  reconnect_attempts: number;
37
+ meta?: {
38
+ signal_queue_size?: number;
39
+ relay_queue_size?: number;
40
+ };
37
41
  };
38
42
 
39
43
  type RelayDiagnostics = {
@@ -227,7 +231,6 @@ export class RelayPreviewAdapter implements NetworkAdapter {
227
231
  try {
228
232
  await this.joinRoom("start");
229
233
  this.started = true;
230
- await this.refreshPeers();
231
234
  await this.pollOnce();
232
235
  this.scheduleNextPoll(this.pollIntervalMs);
233
236
  this.recordDiscovery("signaling_connected", { endpoint: this.activeEndpoint });
@@ -375,8 +378,10 @@ export class RelayPreviewAdapter implements NetworkAdapter {
375
378
  const payload = await this.get(`/peers?room=${encodeURIComponent(this.room)}`);
376
379
  this.lastPeerRefreshAt = Date.now();
377
380
  this.stats.peers_refresh_succeeded += 1;
378
- const peerIds = Array.isArray(payload?.peers) ? payload.peers : [];
379
- this.updatePeersFromList(peerIds);
381
+ const peerItems = Array.isArray(payload?.peer_details) && payload.peer_details.length
382
+ ? payload.peer_details
383
+ : Array.isArray(payload?.peers) ? payload.peers : [];
384
+ this.updatePeersFromList(peerItems);
380
385
  }
381
386
 
382
387
  private onEnvelope(envelope: unknown): void {
@@ -457,9 +462,13 @@ export class RelayPreviewAdapter implements NetworkAdapter {
457
462
 
458
463
  private async joinRoom(reason: string): Promise<void> {
459
464
  this.stats.join_attempted += 1;
460
- await this.post("/join", { room: this.room, peer_id: this.peerId });
465
+ const payload = await this.post("/join", { room: this.room, peer_id: this.peerId });
461
466
  this.lastJoinAt = Date.now();
462
467
  this.stats.join_succeeded += 1;
468
+ if (Array.isArray(payload?.peers)) {
469
+ this.updatePeersFromList(payload.peers);
470
+ this.lastPeerRefreshAt = this.lastJoinAt;
471
+ }
463
472
  this.recordDiscovery("join_ok", { endpoint: this.activeEndpoint, detail: reason });
464
473
  }
465
474
 
@@ -528,13 +537,38 @@ export class RelayPreviewAdapter implements NetworkAdapter {
528
537
  }
529
538
 
530
539
  private updatePeersFromList(values: unknown[]): void {
531
- const peerIds = values.map((value) => String(value || "").trim()).filter(Boolean);
540
+ const parsedPeers: Array<{ peer_id: string; meta?: RelayPeer["meta"] }> = [];
541
+ for (const value of values) {
542
+ if (typeof value === "string") {
543
+ const peerId = String(value || "").trim();
544
+ if (peerId) {
545
+ parsedPeers.push({ peer_id: peerId });
546
+ }
547
+ continue;
548
+ }
549
+ if (value && typeof value === "object") {
550
+ const raw = value as Record<string, unknown>;
551
+ const peerId = String(raw.peer_id || "").trim();
552
+ if (!peerId) {
553
+ continue;
554
+ }
555
+ parsedPeers.push({
556
+ peer_id: peerId,
557
+ meta: {
558
+ signal_queue_size: Number(raw.signal_queue_size ?? 0),
559
+ relay_queue_size: Number(raw.relay_queue_size ?? 0),
560
+ },
561
+ });
562
+ }
563
+ }
564
+ const peerIds = parsedPeers.map((peer) => peer.peer_id);
532
565
  if (!peerIds.includes(this.peerId)) {
533
566
  void this.joinRoom("self_missing_from_peers").catch(() => {});
534
567
  }
535
568
  const now = Date.now();
536
569
  const next = new Map<string, RelayPeer>();
537
- for (const peerId of peerIds) {
570
+ for (const peerInfo of parsedPeers) {
571
+ const peerId = peerInfo.peer_id;
538
572
  if (peerId === this.peerId) continue;
539
573
  const existing = this.peers.get(peerId);
540
574
  if (!existing) {
@@ -547,6 +581,7 @@ export class RelayPreviewAdapter implements NetworkAdapter {
547
581
  last_seen_at: now,
548
582
  messages_seen: existing?.messages_seen ?? 0,
549
583
  reconnect_attempts: existing?.reconnect_attempts ?? 0,
584
+ meta: peerInfo.meta || existing?.meta,
550
585
  });
551
586
  }
552
587
  for (const peerId of this.peers.keys()) {
@@ -1 +1 @@
1
- 2026.3.20-beta.4
1
+ 2026.3.20-beta.6
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "silicaclaw-broadcast",
3
- "version": "2026.3.20-beta.4",
3
+ "version": "2026.3.20-beta.6",
4
4
  "display_name": "SilicaClaw Broadcast",
5
5
  "description": "Official OpenClaw skill for a bounded local SilicaClaw broadcast workflow: read public broadcasts, publish public broadcasts, and optionally forward owner-relevant summaries through OpenClaw's native channel.",
6
6
  "entrypoints": {
@@ -1 +1 @@
1
- 2026.3.20-beta.2
1
+ 2026.3.20-beta.3
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "silicaclaw-owner-push",
3
- "version": "2026.3.20-beta.2",
3
+ "version": "2026.3.20-beta.3",
4
4
  "display_name": "SilicaClaw Owner Push",
5
5
  "description": "Official OpenClaw skill for a bounded local SilicaClaw monitoring workflow: watch public broadcasts, filter owner-relevant updates, and push concise summaries through OpenClaw's native owner channel.",
6
6
  "entrypoints": {