@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
|
@@ -56,6 +56,9 @@ export const TRANSLATIONS = {
|
|
|
56
56
|
},
|
|
57
57
|
actions: {
|
|
58
58
|
broadcastNow: 'Announce Agent Now',
|
|
59
|
+
checkUpdate: 'Check',
|
|
60
|
+
updateNow: 'Update',
|
|
61
|
+
updateNowVersion: 'Update {version}',
|
|
59
62
|
editProfile: 'Edit Profile',
|
|
60
63
|
openNetwork: 'Network',
|
|
61
64
|
openAgent: 'Directory',
|
|
@@ -135,6 +138,17 @@ export const TRANSLATIONS = {
|
|
|
135
138
|
publishStatus: 'Publish Status',
|
|
136
139
|
publicProfilePreview: 'Public Profile Preview',
|
|
137
140
|
goPublic: 'Go public',
|
|
141
|
+
versionChecking: 'Checking for updates...',
|
|
142
|
+
versionCurrent: 'Up to date',
|
|
143
|
+
versionUpdateReady: 'Update {version} ready',
|
|
144
|
+
versionUpdating: 'Updating...',
|
|
145
|
+
versionCheckFailed: 'Could not check updates',
|
|
146
|
+
versionPlatformMac: 'macOS service will restart automatically',
|
|
147
|
+
versionPlatformLinux: 'Linux service will restart automatically',
|
|
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.',
|
|
138
152
|
networkEyebrow: 'Network',
|
|
139
153
|
connectionSummary: 'Connection',
|
|
140
154
|
quickActions: 'Broadcast',
|
|
@@ -205,6 +219,9 @@ export const TRANSLATIONS = {
|
|
|
205
219
|
duplicateWindowSeconds: 'Duplicate Window (seconds)',
|
|
206
220
|
blockedAgentIds: 'Blocked agent IDs (agent_id, comma separated)',
|
|
207
221
|
blockedTerms: 'Blocked Terms (comma separated)',
|
|
222
|
+
queueHealthy: 'Healthy',
|
|
223
|
+
queueWatch: 'Watch',
|
|
224
|
+
queueHigh: 'High',
|
|
208
225
|
},
|
|
209
226
|
hints: {
|
|
210
227
|
publicDiscoverySwitch: 'Use Profile -> Public Enabled as the single public visibility switch.',
|
|
@@ -533,6 +550,11 @@ export const TRANSLATIONS = {
|
|
|
533
550
|
templateCopied: 'Template copied to clipboard.',
|
|
534
551
|
preparingDownload: 'Preparing download...',
|
|
535
552
|
runtimeUpdated: 'Mode updated.',
|
|
553
|
+
appUpdateStarted: 'Update started. SilicaClaw will refresh shortly.',
|
|
554
|
+
appUpdatedTo: 'Updated to {version}.',
|
|
555
|
+
appUpdateLatest: 'Already on the latest version.',
|
|
556
|
+
appUpdateCheckFailed: 'Could not check for updates.',
|
|
557
|
+
appUpdateFailed: 'Could not start the update.',
|
|
536
558
|
copyPreviewFailed: 'Copy preview failed',
|
|
537
559
|
logsRefreshed: 'Logs refreshed',
|
|
538
560
|
crossPreviewEnabled: 'Cross-network preview enabled',
|
|
@@ -642,6 +664,9 @@ export const TRANSLATIONS = {
|
|
|
642
664
|
},
|
|
643
665
|
actions: {
|
|
644
666
|
broadcastNow: '立即公告代理',
|
|
667
|
+
checkUpdate: '检查',
|
|
668
|
+
updateNow: '立即更新',
|
|
669
|
+
updateNowVersion: '更新到 {version}',
|
|
645
670
|
editProfile: '编辑资料',
|
|
646
671
|
openNetwork: '打开网络页',
|
|
647
672
|
openAgent: '打开代理目录',
|
|
@@ -708,6 +733,17 @@ export const TRANSLATIONS = {
|
|
|
708
733
|
discoveredAgents: '公开代理目录',
|
|
709
734
|
onlyShowOnline: '只显示在线',
|
|
710
735
|
nodeSnapshot: '本机代理快照',
|
|
736
|
+
versionChecking: '正在检查更新...',
|
|
737
|
+
versionCurrent: '已是最新版本',
|
|
738
|
+
versionUpdateReady: '可更新到 {version}',
|
|
739
|
+
versionUpdating: '正在更新...',
|
|
740
|
+
versionCheckFailed: '暂时无法检查更新',
|
|
741
|
+
versionPlatformMac: 'macOS 服务会自动重启',
|
|
742
|
+
versionPlatformLinux: 'Linux 服务会自动重启',
|
|
743
|
+
versionPlatformOther: '更新后本地服务会自动刷新',
|
|
744
|
+
relayQueuesHealthy: 'Relay 队列正常。',
|
|
745
|
+
relayQueuesWatch: 'Relay 队列需要关注。',
|
|
746
|
+
relayQueuesHigh: 'Relay 队列正在堆积。',
|
|
711
747
|
profileEyebrow: '资料',
|
|
712
748
|
publicProfile: '公开资料',
|
|
713
749
|
publicProfileEditor: '公开资料编辑器',
|
|
@@ -791,6 +827,9 @@ export const TRANSLATIONS = {
|
|
|
791
827
|
duplicateWindowSeconds: '重复消息窗口(秒)',
|
|
792
828
|
blockedAgentIds: '已屏蔽代理 ID(agent_id,逗号分隔)',
|
|
793
829
|
blockedTerms: '已屏蔽词(逗号分隔)',
|
|
830
|
+
queueHealthy: '正常',
|
|
831
|
+
queueWatch: '注意',
|
|
832
|
+
queueHigh: '偏高',
|
|
794
833
|
},
|
|
795
834
|
hints: {
|
|
796
835
|
publicDiscoverySwitch: '使用资料 -> Public Enabled 作为唯一的公开可见性开关。',
|
|
@@ -1119,6 +1158,11 @@ export const TRANSLATIONS = {
|
|
|
1119
1158
|
templateCopied: '模板已复制到剪贴板。',
|
|
1120
1159
|
preparingDownload: '正在准备下载...',
|
|
1121
1160
|
runtimeUpdated: '模式已更新。',
|
|
1161
|
+
appUpdateStarted: '更新已开始,SilicaClaw 很快会自动刷新。',
|
|
1162
|
+
appUpdatedTo: '已更新到 {version}。',
|
|
1163
|
+
appUpdateLatest: '当前已经是最新版本。',
|
|
1164
|
+
appUpdateCheckFailed: '暂时无法检查更新。',
|
|
1165
|
+
appUpdateFailed: '无法开始更新。',
|
|
1122
1166
|
copyPreviewFailed: '复制预览失败',
|
|
1123
1167
|
logsRefreshed: '日志已刷新',
|
|
1124
1168
|
crossPreviewEnabled: '跨网络预览已启用',
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import express, { NextFunction, Request, Response } from "express";
|
|
2
2
|
import cors from "cors";
|
|
3
|
-
import { execFile, spawnSync } from "child_process";
|
|
3
|
+
import { execFile, spawn, spawnSync } from "child_process";
|
|
4
4
|
import { resolve } from "path";
|
|
5
5
|
import { accessSync, constants, copyFileSync, existsSync, mkdirSync, readFileSync, readdirSync, statSync } from "fs";
|
|
6
6
|
import { createHash } from "crypto";
|
|
@@ -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,
|
|
@@ -90,6 +89,7 @@ const DEFAULT_BRIDGE_API_BASE = defaults.bridge.api_base;
|
|
|
90
89
|
const OPENCLAW_GATEWAY_PORT = defaults.ports.openclaw_gateway;
|
|
91
90
|
const OPENCLAW_GATEWAY_URL = `http://${OPENCLAW_GATEWAY_HOST}:${OPENCLAW_GATEWAY_PORT}/`;
|
|
92
91
|
const NETWORK_PEER_REMOVE_AFTER_MS = Number(process.env.NETWORK_PEER_REMOVE_AFTER_MS || 180_000);
|
|
92
|
+
const DIRECTORY_REMOTE_PROFILE_SOFT_LIMIT = Number(process.env.DIRECTORY_REMOTE_PROFILE_SOFT_LIMIT || 1000);
|
|
93
93
|
const NETWORK_UDP_BIND_ADDRESS = process.env.NETWORK_UDP_BIND_ADDRESS || "0.0.0.0";
|
|
94
94
|
const NETWORK_UDP_BROADCAST_ADDRESS = process.env.NETWORK_UDP_BROADCAST_ADDRESS || "255.255.255.255";
|
|
95
95
|
const NETWORK_PEER_ID = process.env.NETWORK_PEER_ID;
|
|
@@ -115,6 +115,12 @@ const SOCIAL_MESSAGE_MAX_AGE_MS = Number(process.env.SOCIAL_MESSAGE_MAX_AGE_MS |
|
|
|
115
115
|
const SOCIAL_MESSAGE_OBSERVATION_HISTORY_LIMIT = Number(process.env.SOCIAL_MESSAGE_OBSERVATION_HISTORY_LIMIT || 500);
|
|
116
116
|
const SOCIAL_MESSAGE_REPLAY_WINDOW_MS = Number(process.env.SOCIAL_MESSAGE_REPLAY_WINDOW_MS || 10 * 60_000);
|
|
117
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
|
+
);
|
|
118
124
|
const SOCIAL_MESSAGE_BLOCKED_AGENT_IDS = new Set(
|
|
119
125
|
dedupeStrings(parseListEnv(process.env.SOCIAL_MESSAGE_BLOCKED_AGENT_IDS || ""))
|
|
120
126
|
);
|
|
@@ -157,6 +163,10 @@ function normalizeVersionText(value: unknown): string {
|
|
|
157
163
|
return text.startsWith("v") ? text.slice(1) : text;
|
|
158
164
|
}
|
|
159
165
|
|
|
166
|
+
function formatBytesToMiB(value: number): number {
|
|
167
|
+
return Math.round((value / (1024 * 1024)) * 10) / 10;
|
|
168
|
+
}
|
|
169
|
+
|
|
160
170
|
function tokenizeVersion(value: unknown): Array<number | string> {
|
|
161
171
|
return normalizeVersionText(value)
|
|
162
172
|
.split(/[^0-9A-Za-z]+/)
|
|
@@ -186,6 +196,10 @@ function compareVersionTokens(left: unknown, right: unknown): number {
|
|
|
186
196
|
return 0;
|
|
187
197
|
}
|
|
188
198
|
|
|
199
|
+
function userNpmCacheDir(): string {
|
|
200
|
+
return resolve(homedir(), ".silicaclaw", "npm-cache");
|
|
201
|
+
}
|
|
202
|
+
|
|
189
203
|
function resolveWorkspaceRoot(cwd = process.cwd()): string {
|
|
190
204
|
if (existsSync(resolve(cwd, "apps", "local-console", "package.json"))) {
|
|
191
205
|
return cwd;
|
|
@@ -913,6 +927,10 @@ export class LocalNodeService {
|
|
|
913
927
|
private broadcastCount = 0;
|
|
914
928
|
private lastMessageAt = 0;
|
|
915
929
|
private lastBroadcastAt = 0;
|
|
930
|
+
private lastProfileBroadcastAt = 0;
|
|
931
|
+
private lastProfileBroadcastSignature = "";
|
|
932
|
+
private lastReplayBroadcastAt = 0;
|
|
933
|
+
private lastReplayBroadcastSignature = "";
|
|
916
934
|
private lastBroadcastErrorAt = 0;
|
|
917
935
|
private lastBroadcastError: string | null = null;
|
|
918
936
|
private broadcastFailureCount = 0;
|
|
@@ -1201,6 +1219,7 @@ export class LocalNodeService {
|
|
|
1201
1219
|
const relayCapable = this.adapterMode === "webrtc-preview" || this.adapterMode === "relay-preview";
|
|
1202
1220
|
const peers: Array<{ status?: string }> = diagnostics?.peers?.items ?? [];
|
|
1203
1221
|
const online = peers.filter((peer: { status?: string }) => peer.status === "online").length;
|
|
1222
|
+
const memory = process.memoryUsage();
|
|
1204
1223
|
|
|
1205
1224
|
return {
|
|
1206
1225
|
adapter: this.adapterMode,
|
|
@@ -1225,6 +1244,23 @@ export class LocalNodeService {
|
|
|
1225
1244
|
adapter_stats: diagnostics?.stats ?? null,
|
|
1226
1245
|
adapter_transport_stats: diagnostics?.transport_stats ?? null,
|
|
1227
1246
|
adapter_discovery_stats: diagnostics?.discovery_stats ?? null,
|
|
1247
|
+
runtime_diagnostics: {
|
|
1248
|
+
memory_mib: {
|
|
1249
|
+
rss: formatBytesToMiB(memory.rss),
|
|
1250
|
+
heap_used: formatBytesToMiB(memory.heapUsed),
|
|
1251
|
+
heap_total: formatBytesToMiB(memory.heapTotal),
|
|
1252
|
+
external: formatBytesToMiB(memory.external),
|
|
1253
|
+
},
|
|
1254
|
+
directory: {
|
|
1255
|
+
profile_count: Object.keys(this.directory.profiles).length,
|
|
1256
|
+
presence_count: Object.keys(this.directory.presence).length,
|
|
1257
|
+
index_key_count: Object.keys(this.directory.index).length,
|
|
1258
|
+
},
|
|
1259
|
+
social: {
|
|
1260
|
+
message_count: this.socialMessages.length,
|
|
1261
|
+
observation_count: this.socialMessageObservations.length,
|
|
1262
|
+
},
|
|
1263
|
+
},
|
|
1228
1264
|
adapter_diagnostics_summary: relayCapable || diagnostics
|
|
1229
1265
|
? {
|
|
1230
1266
|
started: this.networkStarted,
|
|
@@ -1341,6 +1377,88 @@ export class LocalNodeService {
|
|
|
1341
1377
|
};
|
|
1342
1378
|
}
|
|
1343
1379
|
|
|
1380
|
+
getAppUpdateStatus() {
|
|
1381
|
+
const currentVersion = normalizeVersionText(this.appVersion) || "unknown";
|
|
1382
|
+
const fallback = {
|
|
1383
|
+
current_version: currentVersion,
|
|
1384
|
+
latest_version: currentVersion,
|
|
1385
|
+
update_available: false,
|
|
1386
|
+
channel: "latest",
|
|
1387
|
+
platform: process.platform,
|
|
1388
|
+
checked_at: Date.now(),
|
|
1389
|
+
can_update: true,
|
|
1390
|
+
check_error: null as string | null,
|
|
1391
|
+
};
|
|
1392
|
+
try {
|
|
1393
|
+
const result = spawnSync("npm", ["view", "@silicaclaw/cli", "dist-tags", "--json"], {
|
|
1394
|
+
cwd: this.projectRoot,
|
|
1395
|
+
encoding: "utf8",
|
|
1396
|
+
env: {
|
|
1397
|
+
...process.env,
|
|
1398
|
+
SILICACLAW_WORKSPACE_DIR: this.projectRoot,
|
|
1399
|
+
SILICACLAW_APP_DIR: this.workspaceRoot,
|
|
1400
|
+
npm_config_cache: process.env.npm_config_cache || userNpmCacheDir(),
|
|
1401
|
+
},
|
|
1402
|
+
});
|
|
1403
|
+
if ((result.status ?? 1) !== 0) {
|
|
1404
|
+
return {
|
|
1405
|
+
...fallback,
|
|
1406
|
+
check_error: String(result.stderr || result.stdout || "npm view failed").trim() || "npm view failed",
|
|
1407
|
+
};
|
|
1408
|
+
}
|
|
1409
|
+
const tags = JSON.parse(String(result.stdout || "{}").trim() || "{}") as { latest?: string };
|
|
1410
|
+
const latestVersion = normalizeVersionText(tags.latest || currentVersion) || currentVersion;
|
|
1411
|
+
return {
|
|
1412
|
+
...fallback,
|
|
1413
|
+
latest_version: latestVersion,
|
|
1414
|
+
update_available: compareVersionTokens(latestVersion, currentVersion) > 0,
|
|
1415
|
+
};
|
|
1416
|
+
} catch (error) {
|
|
1417
|
+
return {
|
|
1418
|
+
...fallback,
|
|
1419
|
+
check_error: error instanceof Error ? error.message : String(error),
|
|
1420
|
+
};
|
|
1421
|
+
}
|
|
1422
|
+
}
|
|
1423
|
+
|
|
1424
|
+
startAppUpdate(): { started: boolean; target_version: string; platform: string; reason?: string } {
|
|
1425
|
+
const status = this.getAppUpdateStatus();
|
|
1426
|
+
if (!status.update_available || !status.latest_version) {
|
|
1427
|
+
return {
|
|
1428
|
+
started: false,
|
|
1429
|
+
target_version: status.latest_version || status.current_version,
|
|
1430
|
+
platform: process.platform,
|
|
1431
|
+
reason: status.check_error || "already_current",
|
|
1432
|
+
};
|
|
1433
|
+
}
|
|
1434
|
+
const scriptPath = resolve(this.workspaceRoot, "scripts", "silicaclaw-cli.mjs");
|
|
1435
|
+
if (!existsSync(scriptPath)) {
|
|
1436
|
+
return {
|
|
1437
|
+
started: false,
|
|
1438
|
+
target_version: status.latest_version,
|
|
1439
|
+
platform: process.platform,
|
|
1440
|
+
reason: "missing_cli_script",
|
|
1441
|
+
};
|
|
1442
|
+
}
|
|
1443
|
+
const child = spawn(process.execPath, [scriptPath, "update"], {
|
|
1444
|
+
cwd: this.projectRoot,
|
|
1445
|
+
detached: true,
|
|
1446
|
+
stdio: "ignore",
|
|
1447
|
+
env: {
|
|
1448
|
+
...process.env,
|
|
1449
|
+
SILICACLAW_WORKSPACE_DIR: this.projectRoot,
|
|
1450
|
+
SILICACLAW_APP_DIR: this.workspaceRoot,
|
|
1451
|
+
npm_config_cache: process.env.npm_config_cache || userNpmCacheDir(),
|
|
1452
|
+
},
|
|
1453
|
+
});
|
|
1454
|
+
child.unref();
|
|
1455
|
+
return {
|
|
1456
|
+
started: true,
|
|
1457
|
+
target_version: status.latest_version,
|
|
1458
|
+
platform: process.platform,
|
|
1459
|
+
};
|
|
1460
|
+
}
|
|
1461
|
+
|
|
1344
1462
|
getIntegrationSummary() {
|
|
1345
1463
|
const status = this.getIntegrationStatus();
|
|
1346
1464
|
const runtimeGenerated = Boolean(this.socialRuntime && this.socialRuntime.last_loaded_at > 0);
|
|
@@ -2171,15 +2289,14 @@ export class LocalNodeService {
|
|
|
2171
2289
|
profile: this.profile,
|
|
2172
2290
|
};
|
|
2173
2291
|
const presenceRecord = signPresence(this.identity, Date.now());
|
|
2174
|
-
const
|
|
2175
|
-
const replayMessages = this.getReplayableSelfSocialMessages();
|
|
2292
|
+
const shouldPublishProfile = this.shouldPublishProfileRecord(profileRecord, reason, presenceRecord.timestamp);
|
|
2293
|
+
const replayMessages = this.getReplayableSelfSocialMessages(reason);
|
|
2176
2294
|
|
|
2177
2295
|
try {
|
|
2178
|
-
|
|
2179
|
-
|
|
2180
|
-
for (const record of indexRecords) {
|
|
2181
|
-
await this.publish("index", record);
|
|
2296
|
+
if (shouldPublishProfile) {
|
|
2297
|
+
await this.publish("profile", profileRecord);
|
|
2182
2298
|
}
|
|
2299
|
+
await this.publish("presence", presenceRecord);
|
|
2183
2300
|
for (const message of replayMessages) {
|
|
2184
2301
|
await this.publish(SOCIAL_MESSAGE_TOPIC, message);
|
|
2185
2302
|
}
|
|
@@ -2202,19 +2319,37 @@ export class LocalNodeService {
|
|
|
2202
2319
|
|
|
2203
2320
|
this.directory = ingestProfileRecord(this.directory, profileRecord);
|
|
2204
2321
|
this.directory = ingestPresenceRecord(this.directory, presenceRecord);
|
|
2205
|
-
for (const record of indexRecords) {
|
|
2206
|
-
this.directory = ingestIndexRecord(this.directory, record);
|
|
2207
|
-
}
|
|
2208
2322
|
this.compactCacheInMemory();
|
|
2209
2323
|
await this.persistCache();
|
|
2210
2324
|
|
|
2211
2325
|
await this.log(
|
|
2212
2326
|
"info",
|
|
2213
|
-
`Broadcast sent (${
|
|
2327
|
+
`Broadcast sent (${shouldPublishProfile ? "profile + " : ""}presence, replayed_messages=${replayMessages.length}, reason=${reason})`
|
|
2214
2328
|
);
|
|
2215
2329
|
return { sent: true, reason };
|
|
2216
2330
|
}
|
|
2217
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
|
+
|
|
2218
2353
|
private async maybeRecoverFromBroadcastFailure(reason: string, errorMessage: string): Promise<void> {
|
|
2219
2354
|
const recoveryThreshold = 3;
|
|
2220
2355
|
const recoveryCooldownMs = 60_000;
|
|
@@ -2670,9 +2805,66 @@ export class LocalNodeService {
|
|
|
2670
2805
|
this.networkReconnectDelayMs = Math.min(30_000, Math.max(5_000, Math.floor(delayMs * 1.5)));
|
|
2671
2806
|
}
|
|
2672
2807
|
|
|
2808
|
+
private pruneRemoteProfilesInMemory(now = Date.now()): number {
|
|
2809
|
+
if (!Number.isFinite(DIRECTORY_REMOTE_PROFILE_SOFT_LIMIT) || DIRECTORY_REMOTE_PROFILE_SOFT_LIMIT <= 0) {
|
|
2810
|
+
return 0;
|
|
2811
|
+
}
|
|
2812
|
+
const selfAgentId = this.profile?.agent_id || this.identity?.agent_id || "";
|
|
2813
|
+
const remoteProfiles = Object.values(this.directory.profiles).filter((profile) => profile.agent_id !== selfAgentId);
|
|
2814
|
+
if (remoteProfiles.length <= DIRECTORY_REMOTE_PROFILE_SOFT_LIMIT) {
|
|
2815
|
+
return 0;
|
|
2816
|
+
}
|
|
2817
|
+
|
|
2818
|
+
const onlineRemoteProfiles = remoteProfiles.filter((profile) =>
|
|
2819
|
+
isAgentOnline(this.directory.presence[profile.agent_id], now, PRESENCE_TTL_MS)
|
|
2820
|
+
);
|
|
2821
|
+
const offlineRemoteProfiles = remoteProfiles
|
|
2822
|
+
.filter((profile) => !isAgentOnline(this.directory.presence[profile.agent_id], now, PRESENCE_TTL_MS))
|
|
2823
|
+
.sort((a, b) => (b.updated_at || 0) - (a.updated_at || 0));
|
|
2824
|
+
|
|
2825
|
+
const keepOfflineCount = Math.max(0, DIRECTORY_REMOTE_PROFILE_SOFT_LIMIT - onlineRemoteProfiles.length);
|
|
2826
|
+
const keptRemoteProfiles = [
|
|
2827
|
+
...onlineRemoteProfiles,
|
|
2828
|
+
...offlineRemoteProfiles.slice(0, keepOfflineCount),
|
|
2829
|
+
];
|
|
2830
|
+
const keptRemoteIds = new Set(keptRemoteProfiles.map((profile) => profile.agent_id));
|
|
2831
|
+
const removedIds = remoteProfiles
|
|
2832
|
+
.map((profile) => profile.agent_id)
|
|
2833
|
+
.filter((agentId) => !keptRemoteIds.has(agentId));
|
|
2834
|
+
if (removedIds.length === 0) {
|
|
2835
|
+
return 0;
|
|
2836
|
+
}
|
|
2837
|
+
|
|
2838
|
+
const next = createEmptyDirectoryState();
|
|
2839
|
+
const selfProfile = selfAgentId ? this.directory.profiles[selfAgentId] : null;
|
|
2840
|
+
if (selfProfile) {
|
|
2841
|
+
next.profiles[selfAgentId] = selfProfile;
|
|
2842
|
+
const selfPresence = this.directory.presence[selfAgentId];
|
|
2843
|
+
if (typeof selfPresence === "number" && Number.isFinite(selfPresence)) {
|
|
2844
|
+
next.presence[selfAgentId] = selfPresence;
|
|
2845
|
+
}
|
|
2846
|
+
const rebuilt = rebuildIndexForProfile(next, selfProfile);
|
|
2847
|
+
next.index = rebuilt.index;
|
|
2848
|
+
}
|
|
2849
|
+
|
|
2850
|
+
for (const profile of keptRemoteProfiles) {
|
|
2851
|
+
next.profiles[profile.agent_id] = profile;
|
|
2852
|
+
const seenAt = this.directory.presence[profile.agent_id];
|
|
2853
|
+
if (typeof seenAt === "number" && Number.isFinite(seenAt)) {
|
|
2854
|
+
next.presence[profile.agent_id] = seenAt;
|
|
2855
|
+
}
|
|
2856
|
+
const rebuilt = rebuildIndexForProfile(next, profile);
|
|
2857
|
+
next.index = rebuilt.index;
|
|
2858
|
+
}
|
|
2859
|
+
|
|
2860
|
+
this.directory = dedupeIndex(next);
|
|
2861
|
+
return removedIds.length;
|
|
2862
|
+
}
|
|
2863
|
+
|
|
2673
2864
|
private compactCacheInMemory(): number {
|
|
2674
2865
|
const cleaned = cleanupExpiredPresence(this.directory, Date.now(), PRESENCE_TTL_MS);
|
|
2675
2866
|
this.directory = dedupeIndex(cleaned.state);
|
|
2867
|
+
this.pruneRemoteProfilesInMemory();
|
|
2676
2868
|
return cleaned.removed;
|
|
2677
2869
|
}
|
|
2678
2870
|
|
|
@@ -3047,18 +3239,32 @@ export class LocalNodeService {
|
|
|
3047
3239
|
return this.socialMessages.some((item) => item.message_id === messageId);
|
|
3048
3240
|
}
|
|
3049
3241
|
|
|
3050
|
-
private getReplayableSelfSocialMessages(now = Date.now()): SocialMessageRecord[] {
|
|
3242
|
+
private getReplayableSelfSocialMessages(reason = "manual", now = Date.now()): SocialMessageRecord[] {
|
|
3051
3243
|
const maxCount = Math.max(0, SOCIAL_MESSAGE_REPLAY_MAX_PER_BROADCAST);
|
|
3052
3244
|
if (!this.identity || maxCount === 0) {
|
|
3053
3245
|
return [];
|
|
3054
3246
|
}
|
|
3055
|
-
|
|
3247
|
+
const replayable = this.socialMessages
|
|
3056
3248
|
.filter((item) => (
|
|
3057
3249
|
item.agent_id === this.identity?.agent_id &&
|
|
3058
3250
|
now - item.created_at <= SOCIAL_MESSAGE_REPLAY_WINDOW_MS
|
|
3059
3251
|
))
|
|
3060
3252
|
.sort((a, b) => a.created_at - b.created_at)
|
|
3061
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;
|
|
3062
3268
|
}
|
|
3063
3269
|
|
|
3064
3270
|
private hasRecentDuplicateMessage(agentId: string, body: string, topic: string, now = Date.now()): boolean {
|
|
@@ -3411,6 +3617,48 @@ export async function main() {
|
|
|
3411
3617
|
sendOk(res, node.getRuntimePaths());
|
|
3412
3618
|
});
|
|
3413
3619
|
|
|
3620
|
+
app.get("/api/app/update-status", (_req, res) => {
|
|
3621
|
+
sendOk(res, node.getAppUpdateStatus());
|
|
3622
|
+
});
|
|
3623
|
+
|
|
3624
|
+
app.post(
|
|
3625
|
+
"/api/app/update",
|
|
3626
|
+
asyncRoute(async (_req, res) => {
|
|
3627
|
+
const status = node.getAppUpdateStatus();
|
|
3628
|
+
if (!status.update_available || !status.latest_version) {
|
|
3629
|
+
sendOk(
|
|
3630
|
+
res,
|
|
3631
|
+
{
|
|
3632
|
+
started: false,
|
|
3633
|
+
current_version: status.current_version,
|
|
3634
|
+
latest_version: status.latest_version,
|
|
3635
|
+
platform: status.platform,
|
|
3636
|
+
reason: status.check_error || "already_current",
|
|
3637
|
+
},
|
|
3638
|
+
{ message: "Already on the latest version" }
|
|
3639
|
+
);
|
|
3640
|
+
return;
|
|
3641
|
+
}
|
|
3642
|
+
sendOk(
|
|
3643
|
+
res,
|
|
3644
|
+
{
|
|
3645
|
+
started: true,
|
|
3646
|
+
current_version: status.current_version,
|
|
3647
|
+
target_version: status.latest_version,
|
|
3648
|
+
platform: status.platform,
|
|
3649
|
+
},
|
|
3650
|
+
{ message: `Updating to ${status.latest_version}` }
|
|
3651
|
+
);
|
|
3652
|
+
setTimeout(() => {
|
|
3653
|
+
try {
|
|
3654
|
+
node.startAppUpdate();
|
|
3655
|
+
} catch {
|
|
3656
|
+
// best effort after response has been sent
|
|
3657
|
+
}
|
|
3658
|
+
}, 150);
|
|
3659
|
+
})
|
|
3660
|
+
);
|
|
3661
|
+
|
|
3414
3662
|
app.put(
|
|
3415
3663
|
"/api/profile",
|
|
3416
3664
|
asyncRoute(async (req, res) => {
|
|
@@ -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()) {
|