@silicaclaw/cli 2026.3.20-3 → 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.
Files changed (31) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/INSTALL.md +2 -2
  3. package/README.md +2 -2
  4. package/VERSION +1 -1
  5. package/apps/local-console/dist/apps/local-console/src/server.d.ts +39 -0
  6. package/apps/local-console/dist/apps/local-console/src/server.js +229 -12
  7. package/apps/local-console/dist/packages/network/src/relayPreview.d.ts +4 -0
  8. package/apps/local-console/dist/packages/network/src/relayPreview.js +37 -6
  9. package/apps/local-console/public/app/app.js +293 -2
  10. package/apps/local-console/public/app/network.js +144 -32
  11. package/apps/local-console/public/app/overview.js +43 -15
  12. package/apps/local-console/public/app/social.js +135 -53
  13. package/apps/local-console/public/app/styles.css +86 -0
  14. package/apps/local-console/public/app/template.js +7 -1
  15. package/apps/local-console/public/app/translations.js +44 -0
  16. package/apps/local-console/src/server.ts +262 -14
  17. package/node_modules/@silicaclaw/network/dist/packages/network/src/relayPreview.d.ts +4 -0
  18. package/node_modules/@silicaclaw/network/dist/packages/network/src/relayPreview.js +37 -6
  19. package/node_modules/@silicaclaw/network/src/relayPreview.ts +41 -6
  20. package/openclaw-skills/silicaclaw-broadcast/VERSION +1 -1
  21. package/openclaw-skills/silicaclaw-broadcast/manifest.json +1 -1
  22. package/openclaw-skills/silicaclaw-owner-push/VERSION +1 -1
  23. package/openclaw-skills/silicaclaw-owner-push/manifest.json +1 -1
  24. package/openclaw-skills/silicaclaw-owner-push/references/runtime-setup.md +3 -0
  25. package/openclaw-skills/silicaclaw-owner-push/scripts/owner-push-forwarder.mjs +67 -8
  26. package/package.json +1 -1
  27. package/packages/network/dist/packages/network/src/relayPreview.d.ts +4 -0
  28. package/packages/network/dist/packages/network/src/relayPreview.js +37 -6
  29. package/packages/network/src/relayPreview.ts +41 -6
  30. package/scripts/silicaclaw-cli.mjs +4 -1
  31. package/scripts/silicaclaw-gateway.mjs +108 -0
package/CHANGELOG.md CHANGED
@@ -2,6 +2,18 @@
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
+
11
+ ### 2026.3.20-4
12
+
13
+ - release build:
14
+ - prepared another fresh latest-channel package build without publishing
15
+ - regenerated the npm tarball through the verified release packing workflow
16
+
5
17
  ### 2026.3.20-3
6
18
 
7
19
  - release build:
package/INSTALL.md CHANGED
@@ -211,9 +211,9 @@ npx clawhub publish openclaw-skills/silicaclaw-broadcast \
211
211
  npx clawhub publish openclaw-skills/silicaclaw-owner-push \
212
212
  --slug silicaclaw-owner-push \
213
213
  --name "SilicaClaw Owner Push" \
214
- --version 2026.3.20-beta.1 \
214
+ --version 2026.3.20-beta.2 \
215
215
  --tags latest \
216
- --changelog "Added clearer safety boundaries and bounded local workflow guidance for high-signal monitoring and owner push summaries."
216
+ --changelog "Added latest-only owner push behavior with timestamp cursor state so only the newest qualifying broadcast is pushed and older messages are skipped."
217
217
  ```
218
218
 
219
219
  ClawHub expects each skill version to be valid semver, so use the versions from each skill's `manifest.json` and `VERSION`, not the npm CLI version format.
package/README.md CHANGED
@@ -283,9 +283,9 @@ npx clawhub publish openclaw-skills/silicaclaw-broadcast \
283
283
  npx clawhub publish openclaw-skills/silicaclaw-owner-push \
284
284
  --slug silicaclaw-owner-push \
285
285
  --name "SilicaClaw Owner Push" \
286
- --version 2026.3.20-beta.1 \
286
+ --version 2026.3.20-beta.2 \
287
287
  --tags latest \
288
- --changelog "Added clearer safety boundaries and bounded local workflow guidance for high-signal monitoring and owner push summaries."
288
+ --changelog "Added latest-only owner push behavior with timestamp cursor state so only the newest qualifying broadcast is pushed and older messages are skipped."
289
289
  ```
290
290
 
291
291
  ClawHub publishes the OpenClaw skill folders, not the npm CLI package.
package/VERSION CHANGED
@@ -1 +1 @@
1
- v2026.3.20-3
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;
@@ -374,6 +378,23 @@ export declare class LocalNodeService {
374
378
  adapter_stats: any;
375
379
  adapter_transport_stats: any;
376
380
  adapter_discovery_stats: any;
381
+ runtime_diagnostics: {
382
+ memory_mib: {
383
+ rss: number;
384
+ heap_used: number;
385
+ heap_total: number;
386
+ external: number;
387
+ };
388
+ directory: {
389
+ profile_count: number;
390
+ presence_count: number;
391
+ index_key_count: number;
392
+ };
393
+ social: {
394
+ message_count: number;
395
+ observation_count: number;
396
+ };
397
+ };
377
398
  adapter_diagnostics_summary: {
378
399
  started: boolean;
379
400
  startup_error: string | null;
@@ -471,6 +492,22 @@ export declare class LocalNodeService {
471
492
  social_lookup_paths: string[];
472
493
  social_source_path: string | null;
473
494
  };
495
+ getAppUpdateStatus(): {
496
+ latest_version: string;
497
+ update_available: boolean;
498
+ current_version: string;
499
+ channel: string;
500
+ platform: NodeJS.Platform;
501
+ checked_at: number;
502
+ can_update: boolean;
503
+ check_error: string | null;
504
+ };
505
+ startAppUpdate(): {
506
+ started: boolean;
507
+ target_version: string;
508
+ platform: string;
509
+ reason?: string;
510
+ };
474
511
  getIntegrationSummary(): {
475
512
  connected: boolean;
476
513
  discoverable: boolean;
@@ -721,6 +758,7 @@ export declare class LocalNodeService {
721
758
  reason: string;
722
759
  error?: string;
723
760
  }>;
761
+ private shouldPublishProfileRecord;
724
762
  private maybeRecoverFromBroadcastFailure;
725
763
  private hydrateFromDisk;
726
764
  private applySocialConfigOnCurrentState;
@@ -733,6 +771,7 @@ export declare class LocalNodeService {
733
771
  private clearNetworkReconnectTimer;
734
772
  private startNetworkAdapterWithRetry;
735
773
  private scheduleNetworkReconnect;
774
+ private pruneRemoteProfilesInMemory;
736
775
  private compactCacheInMemory;
737
776
  private publish;
738
777
  private persistCache;
@@ -37,6 +37,7 @@ const DEFAULT_BRIDGE_API_BASE = silicaclaw_defaults_json_1.default.bridge.api_ba
37
37
  const OPENCLAW_GATEWAY_PORT = silicaclaw_defaults_json_1.default.ports.openclaw_gateway;
38
38
  const OPENCLAW_GATEWAY_URL = `http://${OPENCLAW_GATEWAY_HOST}:${OPENCLAW_GATEWAY_PORT}/`;
39
39
  const NETWORK_PEER_REMOVE_AFTER_MS = Number(process.env.NETWORK_PEER_REMOVE_AFTER_MS || 180_000);
40
+ const DIRECTORY_REMOTE_PROFILE_SOFT_LIMIT = Number(process.env.DIRECTORY_REMOTE_PROFILE_SOFT_LIMIT || 1000);
40
41
  const NETWORK_UDP_BIND_ADDRESS = process.env.NETWORK_UDP_BIND_ADDRESS || "0.0.0.0";
41
42
  const NETWORK_UDP_BROADCAST_ADDRESS = process.env.NETWORK_UDP_BROADCAST_ADDRESS || "255.255.255.255";
42
43
  const NETWORK_PEER_ID = process.env.NETWORK_PEER_ID;
@@ -62,6 +63,8 @@ const SOCIAL_MESSAGE_MAX_AGE_MS = Number(process.env.SOCIAL_MESSAGE_MAX_AGE_MS |
62
63
  const SOCIAL_MESSAGE_OBSERVATION_HISTORY_LIMIT = Number(process.env.SOCIAL_MESSAGE_OBSERVATION_HISTORY_LIMIT || 500);
63
64
  const SOCIAL_MESSAGE_REPLAY_WINDOW_MS = Number(process.env.SOCIAL_MESSAGE_REPLAY_WINDOW_MS || 10 * 60_000);
64
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);
65
68
  const SOCIAL_MESSAGE_BLOCKED_AGENT_IDS = new Set(dedupeStrings(parseListEnv(process.env.SOCIAL_MESSAGE_BLOCKED_AGENT_IDS || "")));
66
69
  const SOCIAL_MESSAGE_BLOCKED_TERMS = dedupeStrings(parseListEnv(process.env.SOCIAL_MESSAGE_BLOCKED_TERMS || ""))
67
70
  .map((term) => term.trim().toLowerCase())
@@ -101,6 +104,9 @@ function normalizeVersionText(value) {
101
104
  const text = String(value || "").trim();
102
105
  return text.startsWith("v") ? text.slice(1) : text;
103
106
  }
107
+ function formatBytesToMiB(value) {
108
+ return Math.round((value / (1024 * 1024)) * 10) / 10;
109
+ }
104
110
  function tokenizeVersion(value) {
105
111
  return normalizeVersionText(value)
106
112
  .split(/[^0-9A-Za-z]+/)
@@ -133,6 +139,9 @@ function compareVersionTokens(left, right) {
133
139
  }
134
140
  return 0;
135
141
  }
142
+ function userNpmCacheDir() {
143
+ return (0, path_1.resolve)((0, os_1.homedir)(), ".silicaclaw", "npm-cache");
144
+ }
136
145
  function resolveWorkspaceRoot(cwd = process.cwd()) {
137
146
  if ((0, fs_1.existsSync)((0, path_1.resolve)(cwd, "apps", "local-console", "package.json"))) {
138
147
  return cwd;
@@ -699,6 +708,10 @@ class LocalNodeService {
699
708
  broadcastCount = 0;
700
709
  lastMessageAt = 0;
701
710
  lastBroadcastAt = 0;
711
+ lastProfileBroadcastAt = 0;
712
+ lastProfileBroadcastSignature = "";
713
+ lastReplayBroadcastAt = 0;
714
+ lastReplayBroadcastSignature = "";
702
715
  lastBroadcastErrorAt = 0;
703
716
  lastBroadcastError = null;
704
717
  broadcastFailureCount = 0;
@@ -968,6 +981,7 @@ class LocalNodeService {
968
981
  const relayCapable = this.adapterMode === "webrtc-preview" || this.adapterMode === "relay-preview";
969
982
  const peers = diagnostics?.peers?.items ?? [];
970
983
  const online = peers.filter((peer) => peer.status === "online").length;
984
+ const memory = process.memoryUsage();
971
985
  return {
972
986
  adapter: this.adapterMode,
973
987
  mode: this.networkMode,
@@ -991,6 +1005,23 @@ class LocalNodeService {
991
1005
  adapter_stats: diagnostics?.stats ?? null,
992
1006
  adapter_transport_stats: diagnostics?.transport_stats ?? null,
993
1007
  adapter_discovery_stats: diagnostics?.discovery_stats ?? null,
1008
+ runtime_diagnostics: {
1009
+ memory_mib: {
1010
+ rss: formatBytesToMiB(memory.rss),
1011
+ heap_used: formatBytesToMiB(memory.heapUsed),
1012
+ heap_total: formatBytesToMiB(memory.heapTotal),
1013
+ external: formatBytesToMiB(memory.external),
1014
+ },
1015
+ directory: {
1016
+ profile_count: Object.keys(this.directory.profiles).length,
1017
+ presence_count: Object.keys(this.directory.presence).length,
1018
+ index_key_count: Object.keys(this.directory.index).length,
1019
+ },
1020
+ social: {
1021
+ message_count: this.socialMessages.length,
1022
+ observation_count: this.socialMessageObservations.length,
1023
+ },
1024
+ },
994
1025
  adapter_diagnostics_summary: relayCapable || diagnostics
995
1026
  ? {
996
1027
  started: this.networkStarted,
@@ -1102,6 +1133,87 @@ class LocalNodeService {
1102
1133
  social_source_path: this.socialSourcePath,
1103
1134
  };
1104
1135
  }
1136
+ getAppUpdateStatus() {
1137
+ const currentVersion = normalizeVersionText(this.appVersion) || "unknown";
1138
+ const fallback = {
1139
+ current_version: currentVersion,
1140
+ latest_version: currentVersion,
1141
+ update_available: false,
1142
+ channel: "latest",
1143
+ platform: process.platform,
1144
+ checked_at: Date.now(),
1145
+ can_update: true,
1146
+ check_error: null,
1147
+ };
1148
+ try {
1149
+ const result = (0, child_process_1.spawnSync)("npm", ["view", "@silicaclaw/cli", "dist-tags", "--json"], {
1150
+ cwd: this.projectRoot,
1151
+ encoding: "utf8",
1152
+ env: {
1153
+ ...process.env,
1154
+ SILICACLAW_WORKSPACE_DIR: this.projectRoot,
1155
+ SILICACLAW_APP_DIR: this.workspaceRoot,
1156
+ npm_config_cache: process.env.npm_config_cache || userNpmCacheDir(),
1157
+ },
1158
+ });
1159
+ if ((result.status ?? 1) !== 0) {
1160
+ return {
1161
+ ...fallback,
1162
+ check_error: String(result.stderr || result.stdout || "npm view failed").trim() || "npm view failed",
1163
+ };
1164
+ }
1165
+ const tags = JSON.parse(String(result.stdout || "{}").trim() || "{}");
1166
+ const latestVersion = normalizeVersionText(tags.latest || currentVersion) || currentVersion;
1167
+ return {
1168
+ ...fallback,
1169
+ latest_version: latestVersion,
1170
+ update_available: compareVersionTokens(latestVersion, currentVersion) > 0,
1171
+ };
1172
+ }
1173
+ catch (error) {
1174
+ return {
1175
+ ...fallback,
1176
+ check_error: error instanceof Error ? error.message : String(error),
1177
+ };
1178
+ }
1179
+ }
1180
+ startAppUpdate() {
1181
+ const status = this.getAppUpdateStatus();
1182
+ if (!status.update_available || !status.latest_version) {
1183
+ return {
1184
+ started: false,
1185
+ target_version: status.latest_version || status.current_version,
1186
+ platform: process.platform,
1187
+ reason: status.check_error || "already_current",
1188
+ };
1189
+ }
1190
+ const scriptPath = (0, path_1.resolve)(this.workspaceRoot, "scripts", "silicaclaw-cli.mjs");
1191
+ if (!(0, fs_1.existsSync)(scriptPath)) {
1192
+ return {
1193
+ started: false,
1194
+ target_version: status.latest_version,
1195
+ platform: process.platform,
1196
+ reason: "missing_cli_script",
1197
+ };
1198
+ }
1199
+ const child = (0, child_process_1.spawn)(process.execPath, [scriptPath, "update"], {
1200
+ cwd: this.projectRoot,
1201
+ detached: true,
1202
+ stdio: "ignore",
1203
+ env: {
1204
+ ...process.env,
1205
+ SILICACLAW_WORKSPACE_DIR: this.projectRoot,
1206
+ SILICACLAW_APP_DIR: this.workspaceRoot,
1207
+ npm_config_cache: process.env.npm_config_cache || userNpmCacheDir(),
1208
+ },
1209
+ });
1210
+ child.unref();
1211
+ return {
1212
+ started: true,
1213
+ target_version: status.latest_version,
1214
+ platform: process.platform,
1215
+ };
1216
+ }
1105
1217
  getIntegrationSummary() {
1106
1218
  const status = this.getIntegrationStatus();
1107
1219
  const runtimeGenerated = Boolean(this.socialRuntime && this.socialRuntime.last_loaded_at > 0);
@@ -1849,14 +1961,13 @@ class LocalNodeService {
1849
1961
  profile: this.profile,
1850
1962
  };
1851
1963
  const presenceRecord = (0, core_1.signPresence)(this.identity, Date.now());
1852
- const indexRecords = (0, core_1.buildIndexRecords)(this.profile);
1853
- const replayMessages = this.getReplayableSelfSocialMessages();
1964
+ const shouldPublishProfile = this.shouldPublishProfileRecord(profileRecord, reason, presenceRecord.timestamp);
1965
+ const replayMessages = this.getReplayableSelfSocialMessages(reason);
1854
1966
  try {
1855
- await this.publish("profile", profileRecord);
1856
- await this.publish("presence", presenceRecord);
1857
- for (const record of indexRecords) {
1858
- await this.publish("index", record);
1967
+ if (shouldPublishProfile) {
1968
+ await this.publish("profile", profileRecord);
1859
1969
  }
1970
+ await this.publish("presence", presenceRecord);
1860
1971
  for (const message of replayMessages) {
1861
1972
  await this.publish(SOCIAL_MESSAGE_TOPIC, message);
1862
1973
  }
@@ -1878,14 +1989,27 @@ class LocalNodeService {
1878
1989
  this.consecutiveBroadcastFailures = 0;
1879
1990
  this.directory = (0, core_1.ingestProfileRecord)(this.directory, profileRecord);
1880
1991
  this.directory = (0, core_1.ingestPresenceRecord)(this.directory, presenceRecord);
1881
- for (const record of indexRecords) {
1882
- this.directory = (0, core_1.ingestIndexRecord)(this.directory, record);
1883
- }
1884
1992
  this.compactCacheInMemory();
1885
1993
  await this.persistCache();
1886
- 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})`);
1887
1995
  return { sent: true, reason };
1888
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
+ }
1889
2013
  async maybeRecoverFromBroadcastFailure(reason, errorMessage) {
1890
2014
  const recoveryThreshold = 3;
1891
2015
  const recoveryCooldownMs = 60_000;
@@ -2290,9 +2414,58 @@ class LocalNodeService {
2290
2414
  }, delayMs);
2291
2415
  this.networkReconnectDelayMs = Math.min(30_000, Math.max(5_000, Math.floor(delayMs * 1.5)));
2292
2416
  }
2417
+ pruneRemoteProfilesInMemory(now = Date.now()) {
2418
+ if (!Number.isFinite(DIRECTORY_REMOTE_PROFILE_SOFT_LIMIT) || DIRECTORY_REMOTE_PROFILE_SOFT_LIMIT <= 0) {
2419
+ return 0;
2420
+ }
2421
+ const selfAgentId = this.profile?.agent_id || this.identity?.agent_id || "";
2422
+ const remoteProfiles = Object.values(this.directory.profiles).filter((profile) => profile.agent_id !== selfAgentId);
2423
+ if (remoteProfiles.length <= DIRECTORY_REMOTE_PROFILE_SOFT_LIMIT) {
2424
+ return 0;
2425
+ }
2426
+ const onlineRemoteProfiles = remoteProfiles.filter((profile) => (0, core_1.isAgentOnline)(this.directory.presence[profile.agent_id], now, PRESENCE_TTL_MS));
2427
+ const offlineRemoteProfiles = remoteProfiles
2428
+ .filter((profile) => !(0, core_1.isAgentOnline)(this.directory.presence[profile.agent_id], now, PRESENCE_TTL_MS))
2429
+ .sort((a, b) => (b.updated_at || 0) - (a.updated_at || 0));
2430
+ const keepOfflineCount = Math.max(0, DIRECTORY_REMOTE_PROFILE_SOFT_LIMIT - onlineRemoteProfiles.length);
2431
+ const keptRemoteProfiles = [
2432
+ ...onlineRemoteProfiles,
2433
+ ...offlineRemoteProfiles.slice(0, keepOfflineCount),
2434
+ ];
2435
+ const keptRemoteIds = new Set(keptRemoteProfiles.map((profile) => profile.agent_id));
2436
+ const removedIds = remoteProfiles
2437
+ .map((profile) => profile.agent_id)
2438
+ .filter((agentId) => !keptRemoteIds.has(agentId));
2439
+ if (removedIds.length === 0) {
2440
+ return 0;
2441
+ }
2442
+ const next = (0, core_1.createEmptyDirectoryState)();
2443
+ const selfProfile = selfAgentId ? this.directory.profiles[selfAgentId] : null;
2444
+ if (selfProfile) {
2445
+ next.profiles[selfAgentId] = selfProfile;
2446
+ const selfPresence = this.directory.presence[selfAgentId];
2447
+ if (typeof selfPresence === "number" && Number.isFinite(selfPresence)) {
2448
+ next.presence[selfAgentId] = selfPresence;
2449
+ }
2450
+ const rebuilt = (0, core_1.rebuildIndexForProfile)(next, selfProfile);
2451
+ next.index = rebuilt.index;
2452
+ }
2453
+ for (const profile of keptRemoteProfiles) {
2454
+ next.profiles[profile.agent_id] = profile;
2455
+ const seenAt = this.directory.presence[profile.agent_id];
2456
+ if (typeof seenAt === "number" && Number.isFinite(seenAt)) {
2457
+ next.presence[profile.agent_id] = seenAt;
2458
+ }
2459
+ const rebuilt = (0, core_1.rebuildIndexForProfile)(next, profile);
2460
+ next.index = rebuilt.index;
2461
+ }
2462
+ this.directory = (0, core_1.dedupeIndex)(next);
2463
+ return removedIds.length;
2464
+ }
2293
2465
  compactCacheInMemory() {
2294
2466
  const cleaned = (0, core_1.cleanupExpiredPresence)(this.directory, Date.now(), PRESENCE_TTL_MS);
2295
2467
  this.directory = (0, core_1.dedupeIndex)(cleaned.state);
2468
+ this.pruneRemoteProfilesInMemory();
2296
2469
  return cleaned.removed;
2297
2470
  }
2298
2471
  async publish(topic, data) {
@@ -2621,16 +2794,30 @@ class LocalNodeService {
2621
2794
  hasSocialMessage(messageId) {
2622
2795
  return this.socialMessages.some((item) => item.message_id === messageId);
2623
2796
  }
2624
- getReplayableSelfSocialMessages(now = Date.now()) {
2797
+ getReplayableSelfSocialMessages(reason = "manual", now = Date.now()) {
2625
2798
  const maxCount = Math.max(0, SOCIAL_MESSAGE_REPLAY_MAX_PER_BROADCAST);
2626
2799
  if (!this.identity || maxCount === 0) {
2627
2800
  return [];
2628
2801
  }
2629
- return this.socialMessages
2802
+ const replayable = this.socialMessages
2630
2803
  .filter((item) => (item.agent_id === this.identity?.agent_id &&
2631
2804
  now - item.created_at <= SOCIAL_MESSAGE_REPLAY_WINDOW_MS))
2632
2805
  .sort((a, b) => a.created_at - b.created_at)
2633
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;
2634
2821
  }
2635
2822
  hasRecentDuplicateMessage(agentId, body, topic, now = Date.now()) {
2636
2823
  return this.socialMessages.some((item) => (item.agent_id === agentId &&
@@ -2940,6 +3127,36 @@ async function main() {
2940
3127
  app.get("/api/runtime/paths", (_req, res) => {
2941
3128
  sendOk(res, node.getRuntimePaths());
2942
3129
  });
3130
+ app.get("/api/app/update-status", (_req, res) => {
3131
+ sendOk(res, node.getAppUpdateStatus());
3132
+ });
3133
+ app.post("/api/app/update", asyncRoute(async (_req, res) => {
3134
+ const status = node.getAppUpdateStatus();
3135
+ if (!status.update_available || !status.latest_version) {
3136
+ sendOk(res, {
3137
+ started: false,
3138
+ current_version: status.current_version,
3139
+ latest_version: status.latest_version,
3140
+ platform: status.platform,
3141
+ reason: status.check_error || "already_current",
3142
+ }, { message: "Already on the latest version" });
3143
+ return;
3144
+ }
3145
+ sendOk(res, {
3146
+ started: true,
3147
+ current_version: status.current_version,
3148
+ target_version: status.latest_version,
3149
+ platform: status.platform,
3150
+ }, { message: `Updating to ${status.latest_version}` });
3151
+ setTimeout(() => {
3152
+ try {
3153
+ node.startAppUpdate();
3154
+ }
3155
+ catch {
3156
+ // best effort after response has been sent
3157
+ }
3158
+ }, 150);
3159
+ }));
2943
3160
  app.put("/api/profile", asyncRoute(async (req, res) => {
2944
3161
  const body = req.body;
2945
3162
  const tags = Array.isArray(body.tags)
@@ -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()) {