@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.
- package/CHANGELOG.md +12 -0
- package/INSTALL.md +2 -2
- package/README.md +2 -2
- package/VERSION +1 -1
- package/apps/local-console/dist/apps/local-console/src/server.d.ts +39 -0
- package/apps/local-console/dist/apps/local-console/src/server.js +229 -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 +293 -2
- package/apps/local-console/public/app/network.js +144 -32
- package/apps/local-console/public/app/overview.js +43 -15
- package/apps/local-console/public/app/social.js +135 -53
- package/apps/local-console/public/app/styles.css +86 -0
- package/apps/local-console/public/app/template.js +7 -1
- package/apps/local-console/public/app/translations.js +44 -0
- package/apps/local-console/src/server.ts +262 -14
- 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/openclaw-skills/silicaclaw-owner-push/VERSION +1 -1
- package/openclaw-skills/silicaclaw-owner-push/manifest.json +1 -1
- package/openclaw-skills/silicaclaw-owner-push/references/runtime-setup.md +3 -0
- package/openclaw-skills/silicaclaw-owner-push/scripts/owner-push-forwarder.mjs +67 -8
- 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/scripts/silicaclaw-cli.mjs +4 -1
- 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.
|
|
214
|
+
--version 2026.3.20-beta.2 \
|
|
215
215
|
--tags latest \
|
|
216
|
-
--changelog "Added
|
|
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.
|
|
286
|
+
--version 2026.3.20-beta.2 \
|
|
287
287
|
--tags latest \
|
|
288
|
-
--changelog "Added
|
|
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-
|
|
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
|
|
1853
|
-
const replayMessages = this.getReplayableSelfSocialMessages();
|
|
1964
|
+
const shouldPublishProfile = this.shouldPublishProfileRecord(profileRecord, reason, presenceRecord.timestamp);
|
|
1965
|
+
const replayMessages = this.getReplayableSelfSocialMessages(reason);
|
|
1854
1966
|
try {
|
|
1855
|
-
|
|
1856
|
-
|
|
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 (${
|
|
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
|
-
|
|
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)
|
|
@@ -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()) {
|