@silicaclaw/cli 2026.3.20-4 → 2026.3.20-5

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/CHANGELOG.md CHANGED
@@ -2,6 +2,12 @@
2
2
 
3
3
  ## v1.0 beta - 2026-03-20
4
4
 
5
+ ### 2026.3.20-5
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
+
5
11
  ### 2026.3.20-4
6
12
 
7
13
  - release build:
package/VERSION CHANGED
@@ -1 +1 @@
1
- v2026.3.20-4
1
+ v2026.3.20-5
@@ -162,6 +162,10 @@ export declare class LocalNodeService {
162
162
  private broadcastCount;
163
163
  private lastMessageAt;
164
164
  private lastBroadcastAt;
165
+ private lastProfileBroadcastAt;
166
+ private lastProfileBroadcastSignature;
167
+ private lastReplayBroadcastAt;
168
+ private lastReplayBroadcastSignature;
165
169
  private lastBroadcastErrorAt;
166
170
  private lastBroadcastError;
167
171
  private broadcastFailureCount;
@@ -754,6 +758,7 @@ export declare class LocalNodeService {
754
758
  reason: string;
755
759
  error?: string;
756
760
  }>;
761
+ private shouldPublishProfileRecord;
757
762
  private maybeRecoverFromBroadcastFailure;
758
763
  private hydrateFromDisk;
759
764
  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())
@@ -706,6 +708,10 @@ class LocalNodeService {
706
708
  broadcastCount = 0;
707
709
  lastMessageAt = 0;
708
710
  lastBroadcastAt = 0;
711
+ lastProfileBroadcastAt = 0;
712
+ lastProfileBroadcastSignature = "";
713
+ lastReplayBroadcastAt = 0;
714
+ lastReplayBroadcastSignature = "";
709
715
  lastBroadcastErrorAt = 0;
710
716
  lastBroadcastError = null;
711
717
  broadcastFailureCount = 0;
@@ -1955,14 +1961,13 @@ class LocalNodeService {
1955
1961
  profile: this.profile,
1956
1962
  };
1957
1963
  const presenceRecord = (0, core_1.signPresence)(this.identity, Date.now());
1958
- const indexRecords = (0, core_1.buildIndexRecords)(this.profile);
1959
- const replayMessages = this.getReplayableSelfSocialMessages();
1964
+ const shouldPublishProfile = this.shouldPublishProfileRecord(profileRecord, reason, presenceRecord.timestamp);
1965
+ const replayMessages = this.getReplayableSelfSocialMessages(reason);
1960
1966
  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);
1967
+ if (shouldPublishProfile) {
1968
+ await this.publish("profile", profileRecord);
1965
1969
  }
1970
+ await this.publish("presence", presenceRecord);
1966
1971
  for (const message of replayMessages) {
1967
1972
  await this.publish(SOCIAL_MESSAGE_TOPIC, message);
1968
1973
  }
@@ -1984,14 +1989,27 @@ class LocalNodeService {
1984
1989
  this.consecutiveBroadcastFailures = 0;
1985
1990
  this.directory = (0, core_1.ingestProfileRecord)(this.directory, profileRecord);
1986
1991
  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
1992
  this.compactCacheInMemory();
1991
1993
  await this.persistCache();
1992
- await this.log("info", `Broadcast sent (${indexRecords.length} index refs, replayed_messages=${replayMessages.length}, reason=${reason})`);
1994
+ await this.log("info", `Broadcast sent (${shouldPublishProfile ? "profile + " : ""}presence, replayed_messages=${replayMessages.length}, reason=${reason})`);
1993
1995
  return { sent: true, reason };
1994
1996
  }
1997
+ shouldPublishProfileRecord(profileRecord, reason, now = Date.now()) {
1998
+ if (reason !== "interval") {
1999
+ this.lastProfileBroadcastSignature = profileRecord.profile.signature;
2000
+ this.lastProfileBroadcastAt = now;
2001
+ return true;
2002
+ }
2003
+ const signature = profileRecord.profile.signature;
2004
+ const changedSinceLastPublish = signature !== this.lastProfileBroadcastSignature;
2005
+ const refreshDue = now - this.lastProfileBroadcastAt >= PROFILE_RELAY_REFRESH_INTERVAL_MS;
2006
+ if (!changedSinceLastPublish && !refreshDue) {
2007
+ return false;
2008
+ }
2009
+ this.lastProfileBroadcastSignature = signature;
2010
+ this.lastProfileBroadcastAt = now;
2011
+ return true;
2012
+ }
1995
2013
  async maybeRecoverFromBroadcastFailure(reason, errorMessage) {
1996
2014
  const recoveryThreshold = 3;
1997
2015
  const recoveryCooldownMs = 60_000;
@@ -2776,16 +2794,30 @@ class LocalNodeService {
2776
2794
  hasSocialMessage(messageId) {
2777
2795
  return this.socialMessages.some((item) => item.message_id === messageId);
2778
2796
  }
2779
- getReplayableSelfSocialMessages(now = Date.now()) {
2797
+ getReplayableSelfSocialMessages(reason = "manual", now = Date.now()) {
2780
2798
  const maxCount = Math.max(0, SOCIAL_MESSAGE_REPLAY_MAX_PER_BROADCAST);
2781
2799
  if (!this.identity || maxCount === 0) {
2782
2800
  return [];
2783
2801
  }
2784
- return this.socialMessages
2802
+ const replayable = this.socialMessages
2785
2803
  .filter((item) => (item.agent_id === this.identity?.agent_id &&
2786
2804
  now - item.created_at <= SOCIAL_MESSAGE_REPLAY_WINDOW_MS))
2787
2805
  .sort((a, b) => a.created_at - b.created_at)
2788
2806
  .slice(-maxCount);
2807
+ if (!replayable.length) {
2808
+ this.lastReplayBroadcastSignature = "";
2809
+ return [];
2810
+ }
2811
+ const signature = replayable.map((item) => item.message_id).join(",");
2812
+ const isIntervalReplay = reason === "interval";
2813
+ const changedSinceLastReplay = signature !== this.lastReplayBroadcastSignature;
2814
+ const refreshDue = now - this.lastReplayBroadcastAt >= SOCIAL_MESSAGE_REPLAY_REFRESH_INTERVAL_MS;
2815
+ if (isIntervalReplay && !changedSinceLastReplay && !refreshDue) {
2816
+ return [];
2817
+ }
2818
+ this.lastReplayBroadcastSignature = signature;
2819
+ this.lastReplayBroadcastAt = now;
2820
+ return replayable;
2789
2821
  }
2790
2822
  hasRecentDuplicateMessage(agentId, body, topic, now = Date.now()) {
2791
2823
  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
  : [],
@@ -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>
@@ -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.',
@@ -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 作为唯一的公开可见性开关。',
@@ -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
  );
@@ -922,6 +927,10 @@ export class LocalNodeService {
922
927
  private broadcastCount = 0;
923
928
  private lastMessageAt = 0;
924
929
  private lastBroadcastAt = 0;
930
+ private lastProfileBroadcastAt = 0;
931
+ private lastProfileBroadcastSignature = "";
932
+ private lastReplayBroadcastAt = 0;
933
+ private lastReplayBroadcastSignature = "";
925
934
  private lastBroadcastErrorAt = 0;
926
935
  private lastBroadcastError: string | null = null;
927
936
  private broadcastFailureCount = 0;
@@ -2280,15 +2289,14 @@ export class LocalNodeService {
2280
2289
  profile: this.profile,
2281
2290
  };
2282
2291
  const presenceRecord = signPresence(this.identity, Date.now());
2283
- const indexRecords = buildIndexRecords(this.profile);
2284
- const replayMessages = this.getReplayableSelfSocialMessages();
2292
+ const shouldPublishProfile = this.shouldPublishProfileRecord(profileRecord, reason, presenceRecord.timestamp);
2293
+ const replayMessages = this.getReplayableSelfSocialMessages(reason);
2285
2294
 
2286
2295
  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);
2296
+ if (shouldPublishProfile) {
2297
+ await this.publish("profile", profileRecord);
2291
2298
  }
2299
+ await this.publish("presence", presenceRecord);
2292
2300
  for (const message of replayMessages) {
2293
2301
  await this.publish(SOCIAL_MESSAGE_TOPIC, message);
2294
2302
  }
@@ -2311,19 +2319,37 @@ export class LocalNodeService {
2311
2319
 
2312
2320
  this.directory = ingestProfileRecord(this.directory, profileRecord);
2313
2321
  this.directory = ingestPresenceRecord(this.directory, presenceRecord);
2314
- for (const record of indexRecords) {
2315
- this.directory = ingestIndexRecord(this.directory, record);
2316
- }
2317
2322
  this.compactCacheInMemory();
2318
2323
  await this.persistCache();
2319
2324
 
2320
2325
  await this.log(
2321
2326
  "info",
2322
- `Broadcast sent (${indexRecords.length} index refs, replayed_messages=${replayMessages.length}, reason=${reason})`
2327
+ `Broadcast sent (${shouldPublishProfile ? "profile + " : ""}presence, replayed_messages=${replayMessages.length}, reason=${reason})`
2323
2328
  );
2324
2329
  return { sent: true, reason };
2325
2330
  }
2326
2331
 
2332
+ private shouldPublishProfileRecord(
2333
+ profileRecord: SignedProfileRecord,
2334
+ reason: string,
2335
+ now = Date.now()
2336
+ ): boolean {
2337
+ if (reason !== "interval") {
2338
+ this.lastProfileBroadcastSignature = profileRecord.profile.signature;
2339
+ this.lastProfileBroadcastAt = now;
2340
+ return true;
2341
+ }
2342
+ const signature = profileRecord.profile.signature;
2343
+ const changedSinceLastPublish = signature !== this.lastProfileBroadcastSignature;
2344
+ const refreshDue = now - this.lastProfileBroadcastAt >= PROFILE_RELAY_REFRESH_INTERVAL_MS;
2345
+ if (!changedSinceLastPublish && !refreshDue) {
2346
+ return false;
2347
+ }
2348
+ this.lastProfileBroadcastSignature = signature;
2349
+ this.lastProfileBroadcastAt = now;
2350
+ return true;
2351
+ }
2352
+
2327
2353
  private async maybeRecoverFromBroadcastFailure(reason: string, errorMessage: string): Promise<void> {
2328
2354
  const recoveryThreshold = 3;
2329
2355
  const recoveryCooldownMs = 60_000;
@@ -3213,18 +3239,32 @@ export class LocalNodeService {
3213
3239
  return this.socialMessages.some((item) => item.message_id === messageId);
3214
3240
  }
3215
3241
 
3216
- private getReplayableSelfSocialMessages(now = Date.now()): SocialMessageRecord[] {
3242
+ private getReplayableSelfSocialMessages(reason = "manual", now = Date.now()): SocialMessageRecord[] {
3217
3243
  const maxCount = Math.max(0, SOCIAL_MESSAGE_REPLAY_MAX_PER_BROADCAST);
3218
3244
  if (!this.identity || maxCount === 0) {
3219
3245
  return [];
3220
3246
  }
3221
- return this.socialMessages
3247
+ const replayable = this.socialMessages
3222
3248
  .filter((item) => (
3223
3249
  item.agent_id === this.identity?.agent_id &&
3224
3250
  now - item.created_at <= SOCIAL_MESSAGE_REPLAY_WINDOW_MS
3225
3251
  ))
3226
3252
  .sort((a, b) => a.created_at - b.created_at)
3227
3253
  .slice(-maxCount);
3254
+ if (!replayable.length) {
3255
+ this.lastReplayBroadcastSignature = "";
3256
+ return [];
3257
+ }
3258
+ const signature = replayable.map((item) => item.message_id).join(",");
3259
+ const isIntervalReplay = reason === "interval";
3260
+ const changedSinceLastReplay = signature !== this.lastReplayBroadcastSignature;
3261
+ const refreshDue = now - this.lastReplayBroadcastAt >= SOCIAL_MESSAGE_REPLAY_REFRESH_INTERVAL_MS;
3262
+ if (isIntervalReplay && !changedSinceLastReplay && !refreshDue) {
3263
+ return [];
3264
+ }
3265
+ this.lastReplayBroadcastSignature = signature;
3266
+ this.lastReplayBroadcastAt = now;
3267
+ return replayable;
3228
3268
  }
3229
3269
 
3230
3270
  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.5
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "silicaclaw-broadcast",
3
- "version": "2026.3.20-beta.4",
3
+ "version": "2026.3.20-beta.5",
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": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@silicaclaw/cli",
3
- "version": "2026.3.20-4",
3
+ "version": "2026.3.20-5",
4
4
  "private": false,
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -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()) {