@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 +6 -0
- package/VERSION +1 -1
- package/apps/local-console/dist/apps/local-console/src/server.d.ts +5 -0
- package/apps/local-console/dist/apps/local-console/src/server.js +44 -12
- package/apps/local-console/dist/packages/network/src/relayPreview.d.ts +4 -0
- package/apps/local-console/dist/packages/network/src/relayPreview.js +37 -6
- package/apps/local-console/public/app/app.js +45 -2
- package/apps/local-console/public/app/network.js +35 -4
- package/apps/local-console/public/app/styles.css +35 -0
- package/apps/local-console/public/app/template.js +1 -0
- package/apps/local-console/public/app/translations.js +12 -0
- package/apps/local-console/src/server.ts +53 -13
- package/node_modules/@silicaclaw/network/dist/packages/network/src/relayPreview.d.ts +4 -0
- package/node_modules/@silicaclaw/network/dist/packages/network/src/relayPreview.js +37 -6
- package/node_modules/@silicaclaw/network/src/relayPreview.ts +41 -6
- package/openclaw-skills/silicaclaw-broadcast/VERSION +1 -1
- package/openclaw-skills/silicaclaw-broadcast/manifest.json +1 -1
- package/package.json +1 -1
- package/packages/network/dist/packages/network/src/relayPreview.d.ts +4 -0
- package/packages/network/dist/packages/network/src/relayPreview.js +37 -6
- package/packages/network/src/relayPreview.ts +41 -6
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-
|
|
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
|
|
1959
|
-
const replayMessages = this.getReplayableSelfSocialMessages();
|
|
1964
|
+
const shouldPublishProfile = this.shouldPublishProfileRecord(profileRecord, reason, presenceRecord.timestamp);
|
|
1965
|
+
const replayMessages = this.getReplayableSelfSocialMessages(reason);
|
|
1960
1966
|
try {
|
|
1961
|
-
|
|
1962
|
-
|
|
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 (${
|
|
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
|
-
|
|
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 &&
|
|
@@ -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
|
|
262
|
-
|
|
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
|
|
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
|
|
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
|
-
${
|
|
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
|
-
|
|
252
|
-
|
|
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
|
|
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
|
-
|
|
2288
|
-
|
|
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 (${
|
|
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
|
-
|
|
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 {
|
|
@@ -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
|
|
262
|
-
|
|
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
|
|
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
|
|
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
|
|
379
|
-
|
|
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
|
|
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
|
|
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.
|
|
1
|
+
2026.3.20-beta.5
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "silicaclaw-broadcast",
|
|
3
|
-
"version": "2026.3.20-beta.
|
|
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
|
@@ -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
|
|
262
|
-
|
|
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
|
|
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
|
|
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
|
|
379
|
-
|
|
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
|
|
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
|
|
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()) {
|