@silicaclaw/cli 2026.3.20-2 → 2026.3.20-21
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 +108 -0
- package/INSTALL.md +13 -7
- package/README.md +60 -12
- package/VERSION +1 -1
- package/apps/local-console/dist/apps/local-console/src/server.d.ts +139 -3
- package/apps/local-console/dist/apps/local-console/src/server.js +1029 -92
- package/apps/local-console/dist/packages/core/src/index.d.ts +2 -0
- package/apps/local-console/dist/packages/core/src/index.js +2 -0
- package/apps/local-console/dist/packages/core/src/privateCrypto.d.ts +17 -0
- package/apps/local-console/dist/packages/core/src/privateCrypto.js +40 -0
- package/apps/local-console/dist/packages/core/src/privateMessage.d.ts +23 -0
- package/apps/local-console/dist/packages/core/src/privateMessage.js +74 -0
- package/apps/local-console/dist/packages/core/src/profile.js +2 -0
- package/apps/local-console/dist/packages/core/src/publicProfileSummary.d.ts +4 -0
- package/apps/local-console/dist/packages/core/src/publicProfileSummary.js +3 -0
- package/apps/local-console/dist/packages/core/src/types.d.ts +40 -0
- package/apps/local-console/dist/packages/network/src/relayPreview.d.ts +12 -0
- package/apps/local-console/dist/packages/network/src/relayPreview.js +108 -8
- package/apps/local-console/dist/packages/network/src/types.d.ts +4 -0
- package/apps/local-console/dist/packages/storage/src/repos.d.ts +27 -1
- package/apps/local-console/dist/packages/storage/src/repos.js +35 -1
- package/apps/local-console/public/app/app.js +502 -11
- package/apps/local-console/public/app/events.js +21 -0
- package/apps/local-console/public/app/network.js +144 -32
- package/apps/local-console/public/app/overview.js +57 -27
- package/apps/local-console/public/app/social.js +342 -105
- package/apps/local-console/public/app/styles.css +149 -43
- package/apps/local-console/public/app/template.js +196 -100
- package/apps/local-console/public/app/translations.js +438 -316
- package/apps/local-console/src/server.ts +1177 -90
- package/apps/public-explorer/public/app/template.js +2 -2
- package/apps/public-explorer/public/app/translations.js +36 -36
- package/docs/NEW_USER_OPERATIONS.md +5 -5
- package/docs/OPENCLAW_BRIDGE.md +7 -7
- package/docs/OPENCLAW_BRIDGE_ZH.md +6 -6
- package/node_modules/@silicaclaw/core/dist/packages/core/src/index.d.ts +2 -0
- package/node_modules/@silicaclaw/core/dist/packages/core/src/index.js +2 -0
- package/node_modules/@silicaclaw/core/dist/packages/core/src/privateCrypto.d.ts +17 -0
- package/node_modules/@silicaclaw/core/dist/packages/core/src/privateCrypto.js +40 -0
- package/node_modules/@silicaclaw/core/dist/packages/core/src/privateMessage.d.ts +23 -0
- package/node_modules/@silicaclaw/core/dist/packages/core/src/privateMessage.js +74 -0
- package/node_modules/@silicaclaw/core/dist/packages/core/src/profile.js +2 -0
- package/node_modules/@silicaclaw/core/dist/packages/core/src/publicProfileSummary.d.ts +4 -0
- package/node_modules/@silicaclaw/core/dist/packages/core/src/publicProfileSummary.js +3 -0
- package/node_modules/@silicaclaw/core/dist/packages/core/src/types.d.ts +40 -0
- package/node_modules/@silicaclaw/core/package.json +2 -2
- package/node_modules/@silicaclaw/core/src/index.ts +2 -0
- package/node_modules/@silicaclaw/core/src/privateCrypto.ts +57 -0
- package/node_modules/@silicaclaw/core/src/privateMessage.ts +101 -0
- package/node_modules/@silicaclaw/core/src/profile.ts +2 -0
- package/node_modules/@silicaclaw/core/src/publicProfileSummary.ts +7 -0
- package/node_modules/@silicaclaw/core/src/types.ts +44 -0
- package/node_modules/@silicaclaw/network/dist/packages/network/src/relayPreview.d.ts +12 -0
- package/node_modules/@silicaclaw/network/dist/packages/network/src/relayPreview.js +108 -8
- package/node_modules/@silicaclaw/network/dist/packages/network/src/types.d.ts +4 -0
- package/node_modules/@silicaclaw/network/src/relayPreview.ts +120 -10
- package/node_modules/@silicaclaw/network/src/types.ts +2 -0
- package/node_modules/@silicaclaw/storage/dist/packages/core/src/index.d.ts +2 -0
- package/node_modules/@silicaclaw/storage/dist/packages/core/src/index.js +2 -0
- package/node_modules/@silicaclaw/storage/dist/packages/core/src/privateCrypto.d.ts +17 -0
- package/node_modules/@silicaclaw/storage/dist/packages/core/src/privateCrypto.js +40 -0
- package/node_modules/@silicaclaw/storage/dist/packages/core/src/privateMessage.d.ts +23 -0
- package/node_modules/@silicaclaw/storage/dist/packages/core/src/privateMessage.js +74 -0
- package/node_modules/@silicaclaw/storage/dist/packages/core/src/profile.js +2 -0
- package/node_modules/@silicaclaw/storage/dist/packages/core/src/publicProfileSummary.d.ts +4 -0
- package/node_modules/@silicaclaw/storage/dist/packages/core/src/publicProfileSummary.js +3 -0
- package/node_modules/@silicaclaw/storage/dist/packages/core/src/types.d.ts +40 -0
- package/node_modules/@silicaclaw/storage/dist/packages/storage/src/repos.d.ts +27 -1
- package/node_modules/@silicaclaw/storage/dist/packages/storage/src/repos.js +35 -1
- package/node_modules/@silicaclaw/storage/package.json +2 -2
- package/node_modules/@silicaclaw/storage/src/repos.ts +59 -1
- package/openclaw-skills/silicaclaw-bridge-setup/SKILL.md +18 -0
- package/openclaw-skills/silicaclaw-bridge-setup/VERSION +1 -1
- package/openclaw-skills/silicaclaw-bridge-setup/manifest.json +2 -2
- package/openclaw-skills/silicaclaw-broadcast/SKILL.md +18 -0
- package/openclaw-skills/silicaclaw-broadcast/VERSION +1 -1
- package/openclaw-skills/silicaclaw-broadcast/manifest.json +2 -2
- package/openclaw-skills/silicaclaw-network-config/SKILL.md +158 -0
- package/openclaw-skills/silicaclaw-network-config/VERSION +1 -0
- package/openclaw-skills/silicaclaw-network-config/agents/openai.yaml +6 -0
- package/openclaw-skills/silicaclaw-network-config/manifest.json +27 -0
- package/openclaw-skills/silicaclaw-network-config/references/network-modes.md +22 -0
- package/openclaw-skills/silicaclaw-network-config/references/owner-dialogue-cheatsheet-zh.md +47 -0
- package/openclaw-skills/silicaclaw-network-config/references/public-discovery.md +22 -0
- package/openclaw-skills/silicaclaw-owner-push/SKILL.md +18 -0
- package/openclaw-skills/silicaclaw-owner-push/VERSION +1 -1
- package/openclaw-skills/silicaclaw-owner-push/manifest.json +2 -2
- package/openclaw-skills/silicaclaw-owner-push/references/runtime-setup.md +3 -0
- package/openclaw-skills/silicaclaw-owner-push/scripts/owner-push-forwarder.mjs +151 -9
- package/package.json +1 -1
- package/packages/core/dist/packages/core/src/index.d.ts +2 -0
- package/packages/core/dist/packages/core/src/index.js +2 -0
- package/packages/core/dist/packages/core/src/privateCrypto.d.ts +17 -0
- package/packages/core/dist/packages/core/src/privateCrypto.js +40 -0
- package/packages/core/dist/packages/core/src/privateMessage.d.ts +23 -0
- package/packages/core/dist/packages/core/src/privateMessage.js +74 -0
- package/packages/core/dist/packages/core/src/profile.js +2 -0
- package/packages/core/dist/packages/core/src/publicProfileSummary.d.ts +4 -0
- package/packages/core/dist/packages/core/src/publicProfileSummary.js +3 -0
- package/packages/core/dist/packages/core/src/types.d.ts +40 -0
- package/packages/core/package.json +2 -2
- package/packages/core/src/index.ts +2 -0
- package/packages/core/src/privateCrypto.ts +57 -0
- package/packages/core/src/privateMessage.ts +101 -0
- package/packages/core/src/profile.ts +2 -0
- package/packages/core/src/publicProfileSummary.ts +7 -0
- package/packages/core/src/types.ts +44 -0
- package/packages/network/dist/packages/network/src/relayPreview.d.ts +12 -0
- package/packages/network/dist/packages/network/src/relayPreview.js +108 -8
- package/packages/network/dist/packages/network/src/types.d.ts +4 -0
- package/packages/network/src/relayPreview.ts +120 -10
- package/packages/network/src/types.ts +2 -0
- package/packages/storage/dist/packages/core/src/index.d.ts +2 -0
- package/packages/storage/dist/packages/core/src/index.js +2 -0
- package/packages/storage/dist/packages/core/src/privateCrypto.d.ts +17 -0
- package/packages/storage/dist/packages/core/src/privateCrypto.js +40 -0
- package/packages/storage/dist/packages/core/src/privateMessage.d.ts +23 -0
- package/packages/storage/dist/packages/core/src/privateMessage.js +74 -0
- package/packages/storage/dist/packages/core/src/profile.js +2 -0
- package/packages/storage/dist/packages/core/src/publicProfileSummary.d.ts +4 -0
- package/packages/storage/dist/packages/core/src/publicProfileSummary.js +3 -0
- package/packages/storage/dist/packages/core/src/types.d.ts +40 -0
- package/packages/storage/dist/packages/storage/src/repos.d.ts +27 -1
- package/packages/storage/dist/packages/storage/src/repos.js +35 -1
- package/packages/storage/package.json +2 -2
- package/packages/storage/src/repos.ts +59 -1
- package/scripts/silicaclaw-cli.mjs +4 -1
- package/scripts/silicaclaw-gateway.mjs +114 -2
- package/scripts/validate-openclaw-skill.mjs +19 -0
- package/node_modules/@silicaclaw/storage/dist/index.d.ts +0 -3
- package/node_modules/@silicaclaw/storage/dist/index.js +0 -19
- package/node_modules/@silicaclaw/storage/dist/jsonRepo.d.ts +0 -7
- package/node_modules/@silicaclaw/storage/dist/jsonRepo.js +0 -29
- package/node_modules/@silicaclaw/storage/dist/repos.d.ts +0 -61
- package/node_modules/@silicaclaw/storage/dist/repos.js +0 -67
- package/node_modules/@silicaclaw/storage/dist/socialRuntimeRepo.d.ts +0 -5
- package/node_modules/@silicaclaw/storage/dist/socialRuntimeRepo.js +0 -57
- package/packages/storage/dist/index.d.ts +0 -3
- package/packages/storage/dist/index.js +0 -19
- package/packages/storage/dist/jsonRepo.d.ts +0 -7
- package/packages/storage/dist/jsonRepo.js +0 -29
- package/packages/storage/dist/repos.d.ts +0 -61
- package/packages/storage/dist/repos.js +0 -67
- package/packages/storage/dist/socialRuntimeRepo.d.ts +0 -5
- package/packages/storage/dist/socialRuntimeRepo.js +0 -57
|
@@ -36,7 +36,10 @@ const DEFAULT_GLOBAL_ROOM = silicaclaw_defaults_json_1.default.network.global_pr
|
|
|
36
36
|
const DEFAULT_BRIDGE_API_BASE = silicaclaw_defaults_json_1.default.bridge.api_base;
|
|
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
|
+
const OPENCLAW_RUNTIME_CACHE_MS = 15_000;
|
|
40
|
+
const OPENCLAW_BRIDGE_STATUS_CACHE_MS = 5_000;
|
|
39
41
|
const NETWORK_PEER_REMOVE_AFTER_MS = Number(process.env.NETWORK_PEER_REMOVE_AFTER_MS || 180_000);
|
|
42
|
+
const DIRECTORY_REMOTE_PROFILE_SOFT_LIMIT = Number(process.env.DIRECTORY_REMOTE_PROFILE_SOFT_LIMIT || 1000);
|
|
40
43
|
const NETWORK_UDP_BIND_ADDRESS = process.env.NETWORK_UDP_BIND_ADDRESS || "0.0.0.0";
|
|
41
44
|
const NETWORK_UDP_BROADCAST_ADDRESS = process.env.NETWORK_UDP_BROADCAST_ADDRESS || "255.255.255.255";
|
|
42
45
|
const NETWORK_PEER_ID = process.env.NETWORK_PEER_ID;
|
|
@@ -49,6 +52,12 @@ const WEBRTC_BOOTSTRAP_HINTS = process.env.WEBRTC_BOOTSTRAP_HINTS || "";
|
|
|
49
52
|
const PROFILE_VERSION = "v0.9";
|
|
50
53
|
const SOCIAL_MESSAGE_TOPIC = "social.message";
|
|
51
54
|
const SOCIAL_MESSAGE_OBSERVATION_TOPIC = "social.message.observation";
|
|
55
|
+
const PRIVATE_MESSAGE_TOPIC = "private.message";
|
|
56
|
+
const PRIVATE_MESSAGE_RECEIPT_TOPIC = "private.message.receipt";
|
|
57
|
+
const PRIVATE_MESSAGE_HISTORY_LIMIT = Number(process.env.PRIVATE_MESSAGE_HISTORY_LIMIT || 1000);
|
|
58
|
+
const PRIVATE_MESSAGE_RECEIPT_HISTORY_LIMIT = Number(process.env.PRIVATE_MESSAGE_RECEIPT_HISTORY_LIMIT || 2000);
|
|
59
|
+
const PRIVATE_MESSAGE_QUERY_LIMIT = Number(process.env.PRIVATE_MESSAGE_QUERY_LIMIT || 100);
|
|
60
|
+
const PRIVATE_MESSAGE_PERSIST_DEBOUNCE_MS = Number(process.env.PRIVATE_MESSAGE_PERSIST_DEBOUNCE_MS || 750);
|
|
52
61
|
const DEFAULT_SOCIAL_MESSAGE_CHANNEL = "global";
|
|
53
62
|
const SOCIAL_MESSAGE_MAX_BODY_CHARS = Number(process.env.SOCIAL_MESSAGE_MAX_BODY_CHARS || 500);
|
|
54
63
|
const SOCIAL_MESSAGE_HISTORY_LIMIT = Number(process.env.SOCIAL_MESSAGE_HISTORY_LIMIT || 100);
|
|
@@ -62,6 +71,8 @@ const SOCIAL_MESSAGE_MAX_AGE_MS = Number(process.env.SOCIAL_MESSAGE_MAX_AGE_MS |
|
|
|
62
71
|
const SOCIAL_MESSAGE_OBSERVATION_HISTORY_LIMIT = Number(process.env.SOCIAL_MESSAGE_OBSERVATION_HISTORY_LIMIT || 500);
|
|
63
72
|
const SOCIAL_MESSAGE_REPLAY_WINDOW_MS = Number(process.env.SOCIAL_MESSAGE_REPLAY_WINDOW_MS || 10 * 60_000);
|
|
64
73
|
const SOCIAL_MESSAGE_REPLAY_MAX_PER_BROADCAST = Number(process.env.SOCIAL_MESSAGE_REPLAY_MAX_PER_BROADCAST || 3);
|
|
74
|
+
const SOCIAL_MESSAGE_REPLAY_REFRESH_INTERVAL_MS = Number(process.env.SOCIAL_MESSAGE_REPLAY_REFRESH_INTERVAL_MS || 120_000);
|
|
75
|
+
const PROFILE_RELAY_REFRESH_INTERVAL_MS = Number(process.env.PROFILE_RELAY_REFRESH_INTERVAL_MS || 120_000);
|
|
65
76
|
const SOCIAL_MESSAGE_BLOCKED_AGENT_IDS = new Set(dedupeStrings(parseListEnv(process.env.SOCIAL_MESSAGE_BLOCKED_AGENT_IDS || "")));
|
|
66
77
|
const SOCIAL_MESSAGE_BLOCKED_TERMS = dedupeStrings(parseListEnv(process.env.SOCIAL_MESSAGE_BLOCKED_TERMS || ""))
|
|
67
78
|
.map((term) => term.trim().toLowerCase())
|
|
@@ -101,6 +112,9 @@ function normalizeVersionText(value) {
|
|
|
101
112
|
const text = String(value || "").trim();
|
|
102
113
|
return text.startsWith("v") ? text.slice(1) : text;
|
|
103
114
|
}
|
|
115
|
+
function formatBytesToMiB(value) {
|
|
116
|
+
return Math.round((value / (1024 * 1024)) * 10) / 10;
|
|
117
|
+
}
|
|
104
118
|
function tokenizeVersion(value) {
|
|
105
119
|
return normalizeVersionText(value)
|
|
106
120
|
.split(/[^0-9A-Za-z]+/)
|
|
@@ -133,7 +147,19 @@ function compareVersionTokens(left, right) {
|
|
|
133
147
|
}
|
|
134
148
|
return 0;
|
|
135
149
|
}
|
|
150
|
+
function userNpmCacheDir() {
|
|
151
|
+
return (0, path_1.resolve)((0, os_1.homedir)(), ".silicaclaw", "npm-cache");
|
|
152
|
+
}
|
|
153
|
+
function userShimPath() {
|
|
154
|
+
return (0, path_1.resolve)((0, os_1.homedir)(), ".silicaclaw", "bin", "silicaclaw");
|
|
155
|
+
}
|
|
136
156
|
function resolveWorkspaceRoot(cwd = process.cwd()) {
|
|
157
|
+
const envAppRoot = String(process.env.SILICACLAW_APP_DIR || "").trim();
|
|
158
|
+
if (envAppRoot &&
|
|
159
|
+
(0, fs_1.existsSync)((0, path_1.resolve)(envAppRoot, "apps", "local-console", "package.json")) &&
|
|
160
|
+
(0, fs_1.existsSync)((0, path_1.resolve)(envAppRoot, "package.json"))) {
|
|
161
|
+
return (0, path_1.resolve)(envAppRoot);
|
|
162
|
+
}
|
|
137
163
|
if ((0, fs_1.existsSync)((0, path_1.resolve)(cwd, "apps", "local-console", "package.json"))) {
|
|
138
164
|
return cwd;
|
|
139
165
|
}
|
|
@@ -380,44 +406,56 @@ function readOpenClawConfiguredGateway(workspaceRoot) {
|
|
|
380
406
|
gateway_url: OPENCLAW_GATEWAY_URL,
|
|
381
407
|
};
|
|
382
408
|
}
|
|
409
|
+
function resolveOpenClawStatusCommand(workspaceRoot) {
|
|
410
|
+
const explicitBin = String(process.env.OPENCLAW_BIN || "").trim();
|
|
411
|
+
if (explicitBin) {
|
|
412
|
+
return { cmd: explicitBin, args: ["status"] };
|
|
413
|
+
}
|
|
414
|
+
const configuredSourceDir = String(process.env.OPENCLAW_SOURCE_DIR || "").trim();
|
|
415
|
+
const defaultSourceDir = defaultOpenClawSourceDir(workspaceRoot);
|
|
416
|
+
const sourceDir = configuredSourceDir || defaultSourceDir;
|
|
417
|
+
const sourceEntry = existingPathOrNull((0, path_1.resolve)(sourceDir, "openclaw.mjs"));
|
|
418
|
+
if (sourceEntry) {
|
|
419
|
+
return { cmd: process.execPath, args: [sourceEntry, "status"] };
|
|
420
|
+
}
|
|
421
|
+
const commandPath = resolveExecutableInPath("openclaw");
|
|
422
|
+
if (commandPath) {
|
|
423
|
+
return { cmd: commandPath, args: ["status"] };
|
|
424
|
+
}
|
|
425
|
+
return null;
|
|
426
|
+
}
|
|
427
|
+
function resolveOpenClawGatewayProbeCommand(workspaceRoot) {
|
|
428
|
+
const explicitBin = String(process.env.OPENCLAW_BIN || "").trim();
|
|
429
|
+
if (explicitBin) {
|
|
430
|
+
return { cmd: explicitBin, args: ["gateway", "probe"] };
|
|
431
|
+
}
|
|
432
|
+
const configuredSourceDir = String(process.env.OPENCLAW_SOURCE_DIR || "").trim();
|
|
433
|
+
const defaultSourceDir = defaultOpenClawSourceDir(workspaceRoot);
|
|
434
|
+
const sourceDir = configuredSourceDir || defaultSourceDir;
|
|
435
|
+
const sourceEntry = existingPathOrNull((0, path_1.resolve)(sourceDir, "openclaw.mjs"));
|
|
436
|
+
if (sourceEntry) {
|
|
437
|
+
return { cmd: process.execPath, args: [sourceEntry, "gateway", "probe"] };
|
|
438
|
+
}
|
|
439
|
+
const commandPath = resolveExecutableInPath("openclaw");
|
|
440
|
+
if (commandPath) {
|
|
441
|
+
return { cmd: commandPath, args: ["gateway", "probe"] };
|
|
442
|
+
}
|
|
443
|
+
return null;
|
|
444
|
+
}
|
|
383
445
|
function detectOpenClawRuntime(workspaceRoot) {
|
|
384
446
|
const configuredGateway = readOpenClawConfiguredGateway(workspaceRoot);
|
|
385
|
-
const
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
const
|
|
390
|
-
|
|
391
|
-
.map((line) => line.trim())
|
|
392
|
-
.filter(Boolean);
|
|
393
|
-
const processes = lines
|
|
394
|
-
.map((line) => {
|
|
395
|
-
const match = line.match(/^(\d+)\s+(\d+)\s+(.+)$/);
|
|
396
|
-
if (!match)
|
|
397
|
-
return null;
|
|
398
|
-
const command = match[3] || "";
|
|
399
|
-
const lower = command.toLowerCase();
|
|
400
|
-
const isOpenClaw = lower.includes(" openclaw ") ||
|
|
401
|
-
lower.endsWith(" openclaw") ||
|
|
402
|
-
lower.includes("/openclaw ") ||
|
|
403
|
-
lower.includes("openclaw.mjs") ||
|
|
404
|
-
lower.includes("openclaw gateway") ||
|
|
405
|
-
lower.includes("openclaw agent") ||
|
|
406
|
-
lower.includes("openclaw message");
|
|
407
|
-
if (!isOpenClaw)
|
|
408
|
-
return null;
|
|
409
|
-
return {
|
|
410
|
-
pid: Number(match[1]),
|
|
411
|
-
ppid: Number(match[2]),
|
|
412
|
-
command,
|
|
413
|
-
};
|
|
414
|
-
})
|
|
415
|
-
.filter((item) => Boolean(item));
|
|
416
|
-
const openclawPids = new Set(processes.map((item) => item.pid));
|
|
417
|
-
const gatewayProbe = (0, child_process_1.spawnSync)("lsof", ["-nP", "-iTCP", "-sTCP:LISTEN"], {
|
|
447
|
+
const statusCommand = resolveOpenClawStatusCommand(workspaceRoot);
|
|
448
|
+
const statusLooksConfigured = Boolean(statusCommand ||
|
|
449
|
+
configuredGateway.config_path ||
|
|
450
|
+
detectOpenClawInstallation(workspaceRoot).detected);
|
|
451
|
+
const gatewayProbeCommand = ["lsof", "-nP", `-iTCP:${configuredGateway.gateway_port}`, "-sTCP:LISTEN"];
|
|
452
|
+
const gatewayProbe = (0, child_process_1.spawnSync)(gatewayProbeCommand[0], gatewayProbeCommand.slice(1), {
|
|
418
453
|
encoding: "utf8",
|
|
454
|
+
timeout: 1200,
|
|
419
455
|
});
|
|
420
|
-
const
|
|
456
|
+
const gatewayStatusStdout = String(gatewayProbe.stdout || "");
|
|
457
|
+
const gatewayStatusStderr = String(gatewayProbe.stderr || "");
|
|
458
|
+
const gatewayLines = gatewayStatusStdout
|
|
421
459
|
.split("\n")
|
|
422
460
|
.map((line) => line.trim())
|
|
423
461
|
.filter(Boolean);
|
|
@@ -427,15 +465,10 @@ function detectOpenClawRuntime(workspaceRoot) {
|
|
|
427
465
|
const parts = line.split(/\s+/);
|
|
428
466
|
const pid = Number(parts[1] || 0);
|
|
429
467
|
const command = parts[0] || "";
|
|
430
|
-
const lowerCommand = command.toLowerCase();
|
|
431
468
|
const endpoint = parts[8] || parts[parts.length - 1] || "";
|
|
432
469
|
const portMatch = endpoint.match(/:(\d+)(?:\s*\(|$)/);
|
|
433
470
|
if (!pid || !command || !portMatch)
|
|
434
471
|
return null;
|
|
435
|
-
const isOpenClawListener = openclawPids.has(pid) ||
|
|
436
|
-
lowerCommand.includes("openclaw");
|
|
437
|
-
if (!isOpenClawListener)
|
|
438
|
-
return null;
|
|
439
472
|
const port = Number(portMatch[1]);
|
|
440
473
|
if (!Number.isFinite(port) || port <= 0)
|
|
441
474
|
return null;
|
|
@@ -447,49 +480,107 @@ function detectOpenClawRuntime(workspaceRoot) {
|
|
|
447
480
|
};
|
|
448
481
|
})
|
|
449
482
|
.filter((item) => Boolean(item));
|
|
483
|
+
const gatewayProbeOk = gatewayListeners.length > 0;
|
|
484
|
+
let processes = gatewayListeners.map((item) => ({
|
|
485
|
+
pid: item.pid,
|
|
486
|
+
ppid: item.ppid,
|
|
487
|
+
command: item.command,
|
|
488
|
+
}));
|
|
489
|
+
let processResult = null;
|
|
490
|
+
if (!gatewayProbeOk) {
|
|
491
|
+
processResult = (0, child_process_1.spawnSync)("ps", ["-Ao", "pid=,ppid=,command="], {
|
|
492
|
+
encoding: "utf8",
|
|
493
|
+
timeout: 1200,
|
|
494
|
+
});
|
|
495
|
+
const stdout = String(processResult.stdout || "");
|
|
496
|
+
const lines = stdout
|
|
497
|
+
.split("\n")
|
|
498
|
+
.map((line) => line.trim())
|
|
499
|
+
.filter(Boolean);
|
|
500
|
+
processes = lines
|
|
501
|
+
.map((line) => {
|
|
502
|
+
const match = line.match(/^(\d+)\s+(\d+)\s+(.+)$/);
|
|
503
|
+
if (!match)
|
|
504
|
+
return null;
|
|
505
|
+
const command = match[3] || "";
|
|
506
|
+
const lower = command.toLowerCase();
|
|
507
|
+
const isOpenClaw = lower.includes(" openclaw ") ||
|
|
508
|
+
lower.endsWith(" openclaw") ||
|
|
509
|
+
lower.includes("/openclaw ") ||
|
|
510
|
+
lower.includes("openclaw.mjs") ||
|
|
511
|
+
lower.includes("openclaw gateway") ||
|
|
512
|
+
lower.includes("openclaw agent") ||
|
|
513
|
+
lower.includes("openclaw message");
|
|
514
|
+
if (!isOpenClaw)
|
|
515
|
+
return null;
|
|
516
|
+
return {
|
|
517
|
+
pid: Number(match[1]),
|
|
518
|
+
ppid: Number(match[2]),
|
|
519
|
+
command,
|
|
520
|
+
};
|
|
521
|
+
})
|
|
522
|
+
.filter((item) => Boolean(item));
|
|
523
|
+
}
|
|
450
524
|
const preferredListener = gatewayListeners.find((item) => item.port === configuredGateway.gateway_port) ||
|
|
451
525
|
gatewayListeners[0] ||
|
|
452
526
|
null;
|
|
453
|
-
const
|
|
454
|
-
|
|
455
|
-
if (!combinedProcesses.has(process.pid)) {
|
|
456
|
-
combinedProcesses.set(process.pid, process);
|
|
457
|
-
continue;
|
|
458
|
-
}
|
|
459
|
-
const current = combinedProcesses.get(process.pid);
|
|
460
|
-
if (current && current.command.length < process.command.length) {
|
|
461
|
-
combinedProcesses.set(process.pid, process);
|
|
462
|
-
}
|
|
463
|
-
}
|
|
464
|
-
const allProcesses = Array.from(combinedProcesses.values());
|
|
465
|
-
const gatewayReachable = gatewayListeners.length > 0;
|
|
527
|
+
const allProcesses = processes.slice(0, 10);
|
|
528
|
+
const gatewayReachable = gatewayProbeOk;
|
|
466
529
|
const detectionNotes = [];
|
|
467
|
-
if (result.status !== 0)
|
|
468
|
-
detectionNotes.push(String(result.stderr || "ps failed").trim());
|
|
469
530
|
if (gatewayProbe.status !== 0 && gatewayLines.length === 0) {
|
|
470
|
-
detectionNotes.push(String(
|
|
531
|
+
detectionNotes.push(String(gatewayStatusStderr || "openclaw gateway probe failed").trim());
|
|
532
|
+
}
|
|
533
|
+
if (processResult && processResult.status !== 0) {
|
|
534
|
+
detectionNotes.push(String(processResult.stderr || "ps failed").trim());
|
|
471
535
|
}
|
|
472
536
|
const gatewayPort = preferredListener?.port || configuredGateway.gateway_port;
|
|
473
537
|
const gatewayUrl = `http://${OPENCLAW_GATEWAY_HOST}:${gatewayPort}/`;
|
|
474
538
|
return {
|
|
475
|
-
running: allProcesses.length > 0 || gatewayReachable,
|
|
539
|
+
running: gatewayProbeOk || allProcesses.length > 0 || gatewayReachable,
|
|
476
540
|
process_count: allProcesses.length,
|
|
477
541
|
processes: allProcesses.slice(0, 10),
|
|
478
542
|
detection_error: detectionNotes.filter(Boolean).join(" | ") || null,
|
|
479
543
|
gateway_url: gatewayUrl,
|
|
480
544
|
gateway_port: gatewayPort,
|
|
481
545
|
gateway_reachable: gatewayReachable,
|
|
546
|
+
status_command: statusCommand ? [statusCommand.cmd, ...statusCommand.args].join(" ") : null,
|
|
547
|
+
status_ok: statusLooksConfigured,
|
|
548
|
+
status_summary: statusLooksConfigured
|
|
549
|
+
? configuredGateway.config_path
|
|
550
|
+
? `configured via ${configuredGateway.config_path}`
|
|
551
|
+
: statusCommand
|
|
552
|
+
? `command available: ${[statusCommand.cmd, ...statusCommand.args].join(" ")}`
|
|
553
|
+
: "OpenClaw environment detected"
|
|
554
|
+
: null,
|
|
555
|
+
gateway_probe_command: gatewayProbeCommand.join(" "),
|
|
556
|
+
gateway_probe_ok: gatewayProbeOk,
|
|
557
|
+
gateway_probe_summary: gatewayProbeOk
|
|
558
|
+
? gatewayStatusStdout
|
|
559
|
+
.split("\n")
|
|
560
|
+
.map((line) => line.trim())
|
|
561
|
+
.filter(Boolean)
|
|
562
|
+
.slice(0, 4)
|
|
563
|
+
.join(" | ")
|
|
564
|
+
: null,
|
|
482
565
|
configured_gateway_url: configuredGateway.gateway_url,
|
|
483
566
|
configured_gateway_port: configuredGateway.gateway_port,
|
|
484
567
|
configured_gateway_bind: configuredGateway.gateway_bind,
|
|
485
568
|
configured_gateway_config_path: configuredGateway.config_path,
|
|
486
|
-
detection_mode:
|
|
487
|
-
?
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
569
|
+
detection_mode: gatewayProbeOk
|
|
570
|
+
? (processes.length > 0 && gatewayReachable
|
|
571
|
+
? "gateway-probe+process+gateway"
|
|
572
|
+
: gatewayReachable
|
|
573
|
+
? "gateway-probe+gateway"
|
|
574
|
+
: processes.length > 0
|
|
575
|
+
? "gateway-probe+process"
|
|
576
|
+
: "gateway-probe")
|
|
577
|
+
: processes.length > 0 && gatewayReachable
|
|
578
|
+
? "process+gateway"
|
|
579
|
+
: gatewayReachable
|
|
580
|
+
? "gateway"
|
|
581
|
+
: processes.length > 0
|
|
582
|
+
? "process"
|
|
583
|
+
: "not_running",
|
|
493
584
|
};
|
|
494
585
|
}
|
|
495
586
|
function detectOpenClawSkillInstallation() {
|
|
@@ -688,20 +779,43 @@ class LocalNodeService {
|
|
|
688
779
|
socialMessageGovernanceRepo;
|
|
689
780
|
socialMessageRepo;
|
|
690
781
|
socialMessageObservationRepo;
|
|
782
|
+
privateMessageRepo;
|
|
783
|
+
privateMessageReceiptRepo;
|
|
784
|
+
privateEncryptionKeyRepo;
|
|
785
|
+
privateMessagingRuntimeRepo;
|
|
691
786
|
socialRuntimeRepo;
|
|
692
787
|
identity = null;
|
|
693
788
|
profile = null;
|
|
694
789
|
directory = (0, core_1.createEmptyDirectoryState)();
|
|
695
790
|
socialMessages = [];
|
|
696
791
|
socialMessageObservations = [];
|
|
792
|
+
privateMessages = [];
|
|
793
|
+
privateMessageReceipts = [];
|
|
794
|
+
privateEncryptionKeyPair = null;
|
|
795
|
+
privateMessagingRuntime = null;
|
|
796
|
+
privatePeerRoutes = {};
|
|
797
|
+
privatePeerEncryptionKeys = {};
|
|
798
|
+
privateMessageBodyCache = new Map();
|
|
799
|
+
privateMessageDeliveryStatusCache = new Map();
|
|
697
800
|
messageGovernance;
|
|
801
|
+
privateMessagesPersistDirty = false;
|
|
802
|
+
privateMessageReceiptsPersistDirty = false;
|
|
803
|
+
privateMessagesPersistTimer = null;
|
|
804
|
+
privateMessageReceiptsPersistTimer = null;
|
|
698
805
|
receivedCount = 0;
|
|
699
806
|
broadcastCount = 0;
|
|
700
807
|
lastMessageAt = 0;
|
|
701
808
|
lastBroadcastAt = 0;
|
|
809
|
+
lastProfileBroadcastAt = 0;
|
|
810
|
+
lastProfileBroadcastSignature = "";
|
|
811
|
+
lastReplayBroadcastAt = 0;
|
|
812
|
+
lastReplayBroadcastSignature = "";
|
|
702
813
|
lastBroadcastErrorAt = 0;
|
|
703
814
|
lastBroadcastError = null;
|
|
704
815
|
broadcastFailureCount = 0;
|
|
816
|
+
consecutiveBroadcastFailures = 0;
|
|
817
|
+
lastBroadcastRecoveryAttemptAt = 0;
|
|
818
|
+
broadcastRecoveryInFlight = false;
|
|
705
819
|
broadcaster = null;
|
|
706
820
|
subscriptionsBound = false;
|
|
707
821
|
broadcastEnabled = true;
|
|
@@ -739,6 +853,8 @@ class LocalNodeService {
|
|
|
739
853
|
networkReconnectTimer = null;
|
|
740
854
|
networkReconnectDelayMs = 5_000;
|
|
741
855
|
appVersion = "unknown";
|
|
856
|
+
openclawRuntimeCache = null;
|
|
857
|
+
openclawBridgeStatusCache = null;
|
|
742
858
|
constructor(options) {
|
|
743
859
|
this.workspaceRoot = options?.workspaceRoot || resolveWorkspaceRoot();
|
|
744
860
|
this.projectRoot = options?.projectRoot || resolveProjectRoot(this.workspaceRoot);
|
|
@@ -752,6 +868,10 @@ class LocalNodeService {
|
|
|
752
868
|
this.socialMessageGovernanceRepo = new storage_1.SocialMessageGovernanceRepo(this.storageRoot);
|
|
753
869
|
this.socialMessageRepo = new storage_1.SocialMessageRepo(this.storageRoot);
|
|
754
870
|
this.socialMessageObservationRepo = new storage_1.SocialMessageObservationRepo(this.storageRoot);
|
|
871
|
+
this.privateMessageRepo = new storage_1.PrivateMessageRepo(this.storageRoot);
|
|
872
|
+
this.privateMessageReceiptRepo = new storage_1.PrivateMessageReceiptRepo(this.storageRoot);
|
|
873
|
+
this.privateEncryptionKeyRepo = new storage_1.PrivateEncryptionKeyRepo(this.storageRoot);
|
|
874
|
+
this.privateMessagingRuntimeRepo = new storage_1.PrivateMessagingRuntimeRepo(this.storageRoot);
|
|
755
875
|
this.socialRuntimeRepo = new storage_1.SocialRuntimeRepo(this.storageRoot);
|
|
756
876
|
this.messageGovernance = this.defaultMessageGovernance();
|
|
757
877
|
let loadedSocial = (0, core_1.loadSocialConfig)(this.projectRoot);
|
|
@@ -779,6 +899,22 @@ class LocalNodeService {
|
|
|
779
899
|
this.adapterMode = resolved.mode;
|
|
780
900
|
this.networkPort = resolved.port;
|
|
781
901
|
}
|
|
902
|
+
getCachedOpenClawRuntime() {
|
|
903
|
+
const now = Date.now();
|
|
904
|
+
if (this.openclawRuntimeCache && this.openclawRuntimeCache.expiresAt > now) {
|
|
905
|
+
return this.openclawRuntimeCache.value;
|
|
906
|
+
}
|
|
907
|
+
const value = detectOpenClawRuntime(this.projectRoot);
|
|
908
|
+
this.openclawRuntimeCache = {
|
|
909
|
+
value,
|
|
910
|
+
expiresAt: now + OPENCLAW_RUNTIME_CACHE_MS,
|
|
911
|
+
};
|
|
912
|
+
return value;
|
|
913
|
+
}
|
|
914
|
+
invalidateOpenClawCaches() {
|
|
915
|
+
this.openclawRuntimeCache = null;
|
|
916
|
+
this.openclawBridgeStatusCache = null;
|
|
917
|
+
}
|
|
782
918
|
async start() {
|
|
783
919
|
await this.hydrateFromDisk();
|
|
784
920
|
this.bindNetworkSubscriptions();
|
|
@@ -790,6 +926,7 @@ class LocalNodeService {
|
|
|
790
926
|
clearInterval(this.broadcaster);
|
|
791
927
|
this.broadcaster = null;
|
|
792
928
|
}
|
|
929
|
+
await this.flushPrivatePersistence();
|
|
793
930
|
if (this.networkStarted) {
|
|
794
931
|
await this.network.stop();
|
|
795
932
|
}
|
|
@@ -808,6 +945,9 @@ class LocalNodeService {
|
|
|
808
945
|
getOverview() {
|
|
809
946
|
const discovered = this.search("");
|
|
810
947
|
const onlineCount = discovered.filter((profile) => profile.online).length;
|
|
948
|
+
const openclawInstallation = detectOpenClawInstallation(this.projectRoot);
|
|
949
|
+
const openclawRuntime = this.getCachedOpenClawRuntime();
|
|
950
|
+
const openclawSkillInstallation = detectOpenClawSkillInstallation();
|
|
811
951
|
return {
|
|
812
952
|
app_version: this.appVersion,
|
|
813
953
|
agent_id: this.identity?.agent_id ?? "",
|
|
@@ -823,6 +963,15 @@ class LocalNodeService {
|
|
|
823
963
|
init_state: this.initState,
|
|
824
964
|
presence_ttl_ms: PRESENCE_TTL_MS,
|
|
825
965
|
onboarding: this.getOnboardingSummary(),
|
|
966
|
+
openclaw: {
|
|
967
|
+
detected: openclawInstallation.detected,
|
|
968
|
+
running: openclawRuntime.running,
|
|
969
|
+
detection_mode: openclawRuntime.detection_mode,
|
|
970
|
+
gateway_url: openclawRuntime.gateway_url,
|
|
971
|
+
gateway_probe_ok: openclawRuntime.gateway_probe_ok,
|
|
972
|
+
status_ok: openclawRuntime.status_ok,
|
|
973
|
+
skill_installed: openclawSkillInstallation.installed,
|
|
974
|
+
},
|
|
826
975
|
social: {
|
|
827
976
|
found: this.socialFound,
|
|
828
977
|
enabled: this.socialConfig.enabled,
|
|
@@ -965,6 +1114,7 @@ class LocalNodeService {
|
|
|
965
1114
|
const relayCapable = this.adapterMode === "webrtc-preview" || this.adapterMode === "relay-preview";
|
|
966
1115
|
const peers = diagnostics?.peers?.items ?? [];
|
|
967
1116
|
const online = peers.filter((peer) => peer.status === "online").length;
|
|
1117
|
+
const memory = process.memoryUsage();
|
|
968
1118
|
return {
|
|
969
1119
|
adapter: this.adapterMode,
|
|
970
1120
|
mode: this.networkMode,
|
|
@@ -988,6 +1138,23 @@ class LocalNodeService {
|
|
|
988
1138
|
adapter_stats: diagnostics?.stats ?? null,
|
|
989
1139
|
adapter_transport_stats: diagnostics?.transport_stats ?? null,
|
|
990
1140
|
adapter_discovery_stats: diagnostics?.discovery_stats ?? null,
|
|
1141
|
+
runtime_diagnostics: {
|
|
1142
|
+
memory_mib: {
|
|
1143
|
+
rss: formatBytesToMiB(memory.rss),
|
|
1144
|
+
heap_used: formatBytesToMiB(memory.heapUsed),
|
|
1145
|
+
heap_total: formatBytesToMiB(memory.heapTotal),
|
|
1146
|
+
external: formatBytesToMiB(memory.external),
|
|
1147
|
+
},
|
|
1148
|
+
directory: {
|
|
1149
|
+
profile_count: Object.keys(this.directory.profiles).length,
|
|
1150
|
+
presence_count: Object.keys(this.directory.presence).length,
|
|
1151
|
+
index_key_count: Object.keys(this.directory.index).length,
|
|
1152
|
+
},
|
|
1153
|
+
social: {
|
|
1154
|
+
message_count: this.socialMessages.length,
|
|
1155
|
+
observation_count: this.socialMessageObservations.length,
|
|
1156
|
+
},
|
|
1157
|
+
},
|
|
991
1158
|
adapter_diagnostics_summary: relayCapable || diagnostics
|
|
992
1159
|
? {
|
|
993
1160
|
started: this.networkStarted,
|
|
@@ -1099,6 +1266,91 @@ class LocalNodeService {
|
|
|
1099
1266
|
social_source_path: this.socialSourcePath,
|
|
1100
1267
|
};
|
|
1101
1268
|
}
|
|
1269
|
+
getAppUpdateStatus() {
|
|
1270
|
+
const currentVersion = normalizeVersionText(this.appVersion) || "unknown";
|
|
1271
|
+
const fallback = {
|
|
1272
|
+
current_version: currentVersion,
|
|
1273
|
+
latest_version: currentVersion,
|
|
1274
|
+
update_available: false,
|
|
1275
|
+
channel: "latest",
|
|
1276
|
+
platform: process.platform,
|
|
1277
|
+
checked_at: Date.now(),
|
|
1278
|
+
can_update: true,
|
|
1279
|
+
check_error: null,
|
|
1280
|
+
};
|
|
1281
|
+
try {
|
|
1282
|
+
const result = (0, child_process_1.spawnSync)("npm", ["view", "@silicaclaw/cli", "dist-tags", "--json"], {
|
|
1283
|
+
cwd: this.projectRoot,
|
|
1284
|
+
encoding: "utf8",
|
|
1285
|
+
env: {
|
|
1286
|
+
...process.env,
|
|
1287
|
+
SILICACLAW_WORKSPACE_DIR: this.projectRoot,
|
|
1288
|
+
SILICACLAW_APP_DIR: this.workspaceRoot,
|
|
1289
|
+
npm_config_cache: process.env.npm_config_cache || userNpmCacheDir(),
|
|
1290
|
+
},
|
|
1291
|
+
});
|
|
1292
|
+
if ((result.status ?? 1) !== 0) {
|
|
1293
|
+
return {
|
|
1294
|
+
...fallback,
|
|
1295
|
+
check_error: String(result.stderr || result.stdout || "npm view failed").trim() || "npm view failed",
|
|
1296
|
+
};
|
|
1297
|
+
}
|
|
1298
|
+
const tags = JSON.parse(String(result.stdout || "{}").trim() || "{}");
|
|
1299
|
+
const latestVersion = normalizeVersionText(tags.latest || currentVersion) || currentVersion;
|
|
1300
|
+
return {
|
|
1301
|
+
...fallback,
|
|
1302
|
+
latest_version: latestVersion,
|
|
1303
|
+
update_available: compareVersionTokens(latestVersion, currentVersion) > 0,
|
|
1304
|
+
};
|
|
1305
|
+
}
|
|
1306
|
+
catch (error) {
|
|
1307
|
+
return {
|
|
1308
|
+
...fallback,
|
|
1309
|
+
check_error: error instanceof Error ? error.message : String(error),
|
|
1310
|
+
};
|
|
1311
|
+
}
|
|
1312
|
+
}
|
|
1313
|
+
startAppUpdate() {
|
|
1314
|
+
const status = this.getAppUpdateStatus();
|
|
1315
|
+
if (!status.update_available || !status.latest_version) {
|
|
1316
|
+
return {
|
|
1317
|
+
started: false,
|
|
1318
|
+
target_version: status.latest_version || status.current_version,
|
|
1319
|
+
platform: process.platform,
|
|
1320
|
+
reason: status.check_error || "already_current",
|
|
1321
|
+
};
|
|
1322
|
+
}
|
|
1323
|
+
const shimPath = userShimPath();
|
|
1324
|
+
const scriptPath = (0, path_1.resolve)(this.workspaceRoot, "scripts", "silicaclaw-cli.mjs");
|
|
1325
|
+
const useShim = (0, fs_1.existsSync)(shimPath);
|
|
1326
|
+
if (!useShim && !(0, fs_1.existsSync)(scriptPath)) {
|
|
1327
|
+
return {
|
|
1328
|
+
started: false,
|
|
1329
|
+
target_version: status.latest_version,
|
|
1330
|
+
platform: process.platform,
|
|
1331
|
+
reason: "missing_cli_script",
|
|
1332
|
+
};
|
|
1333
|
+
}
|
|
1334
|
+
const command = useShim ? shimPath : process.execPath;
|
|
1335
|
+
const args = useShim ? ["update"] : [scriptPath, "update"];
|
|
1336
|
+
const child = (0, child_process_1.spawn)(command, args, {
|
|
1337
|
+
cwd: this.projectRoot,
|
|
1338
|
+
detached: true,
|
|
1339
|
+
stdio: "ignore",
|
|
1340
|
+
env: {
|
|
1341
|
+
...process.env,
|
|
1342
|
+
SILICACLAW_WORKSPACE_DIR: this.projectRoot,
|
|
1343
|
+
SILICACLAW_APP_DIR: this.workspaceRoot,
|
|
1344
|
+
npm_config_cache: process.env.npm_config_cache || userNpmCacheDir(),
|
|
1345
|
+
},
|
|
1346
|
+
});
|
|
1347
|
+
child.unref();
|
|
1348
|
+
return {
|
|
1349
|
+
started: true,
|
|
1350
|
+
target_version: status.latest_version,
|
|
1351
|
+
platform: process.platform,
|
|
1352
|
+
};
|
|
1353
|
+
}
|
|
1102
1354
|
getIntegrationSummary() {
|
|
1103
1355
|
const status = this.getIntegrationStatus();
|
|
1104
1356
|
const runtimeGenerated = Boolean(this.socialRuntime && this.socialRuntime.last_loaded_at > 0);
|
|
@@ -1355,6 +1607,7 @@ class LocalNodeService {
|
|
|
1355
1607
|
return {
|
|
1356
1608
|
...message,
|
|
1357
1609
|
display_name: profile?.display_name || message.display_name || "Unnamed",
|
|
1610
|
+
avatar_url: profile?.avatar_url || "",
|
|
1358
1611
|
is_self: message.agent_id === this.identity?.agent_id,
|
|
1359
1612
|
online: (0, core_1.isAgentOnline)(lastSeenAt, Date.now(), PRESENCE_TTL_MS),
|
|
1360
1613
|
last_seen_at: lastSeenAt || null,
|
|
@@ -1373,10 +1626,136 @@ class LocalNodeService {
|
|
|
1373
1626
|
},
|
|
1374
1627
|
};
|
|
1375
1628
|
}
|
|
1629
|
+
getPrivateMessagingState() {
|
|
1630
|
+
return {
|
|
1631
|
+
enabled: Boolean(this.identity && this.privateEncryptionKeyPair),
|
|
1632
|
+
agent_id: this.identity?.agent_id || "",
|
|
1633
|
+
encryption_public_key: this.privateEncryptionKeyPair?.public_key || "",
|
|
1634
|
+
conversation_count: this.getPrivateConversations().length,
|
|
1635
|
+
message_count: this.privateMessages.length,
|
|
1636
|
+
runtime: this.privateMessagingRuntime,
|
|
1637
|
+
};
|
|
1638
|
+
}
|
|
1639
|
+
getPrivateConversations() {
|
|
1640
|
+
const conversations = new Map();
|
|
1641
|
+
for (const message of this.privateMessages) {
|
|
1642
|
+
if (message.from_agent_id === message.to_agent_id) {
|
|
1643
|
+
continue;
|
|
1644
|
+
}
|
|
1645
|
+
const peerAgentId = message.from_agent_id === this.identity?.agent_id ? message.to_agent_id : message.from_agent_id;
|
|
1646
|
+
if (!peerAgentId || peerAgentId === this.identity?.agent_id) {
|
|
1647
|
+
continue;
|
|
1648
|
+
}
|
|
1649
|
+
const peerProfile = this.directory.profiles[peerAgentId];
|
|
1650
|
+
const current = conversations.get(message.conversation_id);
|
|
1651
|
+
const nextLast = Math.max(current?.last_message_at || 0, message.created_at || 0) || null;
|
|
1652
|
+
const learnedPeerKey = this.privatePeerEncryptionKeys[peerAgentId] || "";
|
|
1653
|
+
conversations.set(message.conversation_id, {
|
|
1654
|
+
conversation_id: message.conversation_id,
|
|
1655
|
+
peer_agent_id: peerAgentId,
|
|
1656
|
+
peer_display_name: peerProfile?.display_name || peerAgentId,
|
|
1657
|
+
peer_avatar_url: peerProfile?.avatar_url || "",
|
|
1658
|
+
peer_public_key: learnedPeerKey || peerProfile?.private_encryption_public_key || "",
|
|
1659
|
+
last_message_at: nextLast,
|
|
1660
|
+
unread_count: current?.unread_count || 0,
|
|
1661
|
+
});
|
|
1662
|
+
}
|
|
1663
|
+
return Array.from(conversations.values()).sort((a, b) => (b.last_message_at || 0) - (a.last_message_at || 0));
|
|
1664
|
+
}
|
|
1665
|
+
getPrivateMessages(conversationId, limit = PRIVATE_MESSAGE_QUERY_LIMIT) {
|
|
1666
|
+
const normalizedConversationId = String(conversationId || "").trim();
|
|
1667
|
+
const resolvedLimit = Math.max(1, Math.min(PRIVATE_MESSAGE_QUERY_LIMIT, Number(limit) || PRIVATE_MESSAGE_QUERY_LIMIT));
|
|
1668
|
+
const receiptsByMessageId = new Map(this.privateMessageReceipts.map((receipt) => [receipt.message_id, receipt.status]));
|
|
1669
|
+
return this.privateMessages
|
|
1670
|
+
.filter((message) => {
|
|
1671
|
+
if (message.from_agent_id === message.to_agent_id) {
|
|
1672
|
+
return false;
|
|
1673
|
+
}
|
|
1674
|
+
const peerAgentId = message.from_agent_id === this.identity?.agent_id ? message.to_agent_id : message.from_agent_id;
|
|
1675
|
+
if (!peerAgentId || peerAgentId === this.identity?.agent_id) {
|
|
1676
|
+
return false;
|
|
1677
|
+
}
|
|
1678
|
+
return !normalizedConversationId || message.conversation_id === normalizedConversationId;
|
|
1679
|
+
})
|
|
1680
|
+
.sort((a, b) => b.created_at - a.created_at)
|
|
1681
|
+
.slice(0, resolvedLimit)
|
|
1682
|
+
.map((message) => ({
|
|
1683
|
+
message_id: message.message_id,
|
|
1684
|
+
conversation_id: message.conversation_id,
|
|
1685
|
+
from_agent_id: message.from_agent_id,
|
|
1686
|
+
to_agent_id: message.to_agent_id,
|
|
1687
|
+
body: this.decryptPrivateMessageBody(message),
|
|
1688
|
+
created_at: message.created_at,
|
|
1689
|
+
is_self: message.from_agent_id === this.identity?.agent_id,
|
|
1690
|
+
delivery_status: receiptsByMessageId.get(message.message_id) ||
|
|
1691
|
+
this.privateMessageDeliveryStatusCache.get(message.message_id) ||
|
|
1692
|
+
(message.from_agent_id === this.identity?.agent_id ? "fallback-sent" : "sent"),
|
|
1693
|
+
}));
|
|
1694
|
+
}
|
|
1695
|
+
async sendPrivateMessage(input) {
|
|
1696
|
+
if (!this.identity || !this.privateEncryptionKeyPair) {
|
|
1697
|
+
return { sent: false, reason: "missing_identity_or_private_key" };
|
|
1698
|
+
}
|
|
1699
|
+
const toAgentId = String(input.to_agent_id || "").trim();
|
|
1700
|
+
const learnedRecipientKey = this.privatePeerEncryptionKeys[toAgentId] || "";
|
|
1701
|
+
const profileRecipientKey = this.directory.profiles[toAgentId]?.private_encryption_public_key || "";
|
|
1702
|
+
const recipientKey = String(learnedRecipientKey || input.recipient_encryption_public_key || profileRecipientKey || "").trim();
|
|
1703
|
+
const body = String(input.body || "").trim();
|
|
1704
|
+
if (toAgentId === this.identity.agent_id) {
|
|
1705
|
+
return { sent: false, reason: "self_private_message_not_allowed" };
|
|
1706
|
+
}
|
|
1707
|
+
const toPeerId = this.privatePeerRoutes[toAgentId] || "";
|
|
1708
|
+
if (!toAgentId || !recipientKey || !body) {
|
|
1709
|
+
return { sent: false, reason: "invalid_private_message_input" };
|
|
1710
|
+
}
|
|
1711
|
+
const encrypted = (0, core_1.encryptPrivatePayload)({
|
|
1712
|
+
plaintext: body,
|
|
1713
|
+
recipient_public_key: recipientKey,
|
|
1714
|
+
sender_keypair: this.privateEncryptionKeyPair,
|
|
1715
|
+
});
|
|
1716
|
+
const message = (0, core_1.signPrivateMessage)({
|
|
1717
|
+
identity: this.identity,
|
|
1718
|
+
message_id: (0, crypto_1.createHash)("sha256").update(`${this.identity.agent_id}:${toAgentId}:${Date.now()}:${body}:${Math.random()}`, "utf8").digest("hex"),
|
|
1719
|
+
conversation_id: this.buildPrivateConversationId(this.identity.agent_id, toAgentId),
|
|
1720
|
+
to_agent_id: toAgentId,
|
|
1721
|
+
sender_encryption_public_key: encrypted.sender_encryption_public_key,
|
|
1722
|
+
recipient_encryption_public_key: recipientKey,
|
|
1723
|
+
ciphertext: encrypted.ciphertext,
|
|
1724
|
+
nonce: encrypted.nonce,
|
|
1725
|
+
created_at: Date.now(),
|
|
1726
|
+
});
|
|
1727
|
+
this.privateMessageBodyCache.set(message.message_id, body);
|
|
1728
|
+
this.ingestPrivateMessage(message);
|
|
1729
|
+
await this.persistPrivateMessages();
|
|
1730
|
+
let reason = "fallback-sent";
|
|
1731
|
+
if (toPeerId && typeof this.network.sendDirect === "function") {
|
|
1732
|
+
try {
|
|
1733
|
+
await this.network.sendDirect(toPeerId, PRIVATE_MESSAGE_TOPIC, message);
|
|
1734
|
+
await this.publish(PRIVATE_MESSAGE_TOPIC, message);
|
|
1735
|
+
reason = "direct-sent";
|
|
1736
|
+
}
|
|
1737
|
+
catch {
|
|
1738
|
+
await this.publish(PRIVATE_MESSAGE_TOPIC, message);
|
|
1739
|
+
}
|
|
1740
|
+
}
|
|
1741
|
+
else {
|
|
1742
|
+
await this.publish(PRIVATE_MESSAGE_TOPIC, message);
|
|
1743
|
+
}
|
|
1744
|
+
this.privateMessageDeliveryStatusCache.set(message.message_id, reason);
|
|
1745
|
+
const view = this.getPrivateMessages(message.conversation_id).find((item) => item.message_id === message.message_id);
|
|
1746
|
+
if (view) {
|
|
1747
|
+
view.delivery_status = reason;
|
|
1748
|
+
}
|
|
1749
|
+
return { sent: true, reason, message: view };
|
|
1750
|
+
}
|
|
1376
1751
|
getOpenClawBridgeStatus() {
|
|
1752
|
+
const now = Date.now();
|
|
1753
|
+
if (this.openclawBridgeStatusCache && this.openclawBridgeStatusCache.expiresAt > now) {
|
|
1754
|
+
return this.openclawBridgeStatusCache.value;
|
|
1755
|
+
}
|
|
1377
1756
|
const integration = this.getIntegrationStatus();
|
|
1378
1757
|
const openclawInstallation = detectOpenClawInstallation(this.projectRoot);
|
|
1379
|
-
const openclawRuntime =
|
|
1758
|
+
const openclawRuntime = this.getCachedOpenClawRuntime();
|
|
1380
1759
|
const skillInstallation = detectOpenClawSkillInstallation();
|
|
1381
1760
|
const ownerDelivery = detectOwnerDeliveryStatus({
|
|
1382
1761
|
workspaceRoot: this.projectRoot,
|
|
@@ -1384,7 +1763,7 @@ class LocalNodeService {
|
|
|
1384
1763
|
openclawRunning: openclawRuntime.running,
|
|
1385
1764
|
skillInstalled: skillInstallation.installed,
|
|
1386
1765
|
});
|
|
1387
|
-
|
|
1766
|
+
const value = {
|
|
1388
1767
|
enabled: this.socialConfig.enabled,
|
|
1389
1768
|
connected_to_silicaclaw: integration.connected_to_silicaclaw,
|
|
1390
1769
|
public_enabled: integration.public_enabled,
|
|
@@ -1440,6 +1819,11 @@ class LocalNodeService {
|
|
|
1440
1819
|
install_skill: "/api/openclaw/bridge/skill-install",
|
|
1441
1820
|
},
|
|
1442
1821
|
};
|
|
1822
|
+
this.openclawBridgeStatusCache = {
|
|
1823
|
+
value,
|
|
1824
|
+
expiresAt: now + OPENCLAW_BRIDGE_STATUS_CACHE_MS,
|
|
1825
|
+
};
|
|
1826
|
+
return value;
|
|
1443
1827
|
}
|
|
1444
1828
|
async installOpenClawSkill(skillName) {
|
|
1445
1829
|
const scriptPath = (0, path_1.resolve)(this.workspaceRoot, "scripts", "install-openclaw-skill.mjs");
|
|
@@ -1453,6 +1837,7 @@ class LocalNodeService {
|
|
|
1453
1837
|
maxBuffer: 1024 * 1024,
|
|
1454
1838
|
});
|
|
1455
1839
|
const parsed = JSON.parse(String(stdout || "{}"));
|
|
1840
|
+
this.invalidateOpenClawCaches();
|
|
1456
1841
|
return {
|
|
1457
1842
|
...parsed,
|
|
1458
1843
|
bridge: this.getOpenClawBridgeStatus(),
|
|
@@ -1472,7 +1857,7 @@ class LocalNodeService {
|
|
|
1472
1857
|
const workspaceSkillDir = (0, path_1.resolve)(homeDir, "workspace", "skills");
|
|
1473
1858
|
const legacySkillDir = (0, path_1.resolve)(homeDir, "skills");
|
|
1474
1859
|
const openclawSourceDir = defaultOpenClawSourceDir(this.projectRoot);
|
|
1475
|
-
const openclawRuntime =
|
|
1860
|
+
const openclawRuntime = this.getCachedOpenClawRuntime();
|
|
1476
1861
|
return {
|
|
1477
1862
|
bridge_api_base: DEFAULT_BRIDGE_API_BASE,
|
|
1478
1863
|
openclaw_detected: detectOpenClawInstallation(this.projectRoot).detected,
|
|
@@ -1511,7 +1896,11 @@ class LocalNodeService {
|
|
|
1511
1896
|
};
|
|
1512
1897
|
}
|
|
1513
1898
|
getSkillsView() {
|
|
1514
|
-
const
|
|
1899
|
+
const bundledRootCandidates = [
|
|
1900
|
+
(0, path_1.resolve)(this.workspaceRoot, "openclaw-skills"),
|
|
1901
|
+
(0, path_1.resolve)(this.projectRoot, "openclaw-skills"),
|
|
1902
|
+
];
|
|
1903
|
+
const bundledRoot = bundledRootCandidates.find((candidate) => (0, fs_1.existsSync)(candidate)) || bundledRootCandidates[0];
|
|
1515
1904
|
const openclawHome = (0, path_1.resolve)(process.env.HOME || "", ".openclaw");
|
|
1516
1905
|
const workspaceInstallRoot = (0, path_1.resolve)(openclawHome, "workspace", "skills");
|
|
1517
1906
|
const legacyInstallRoot = (0, path_1.resolve)(openclawHome, "skills");
|
|
@@ -1845,14 +2234,13 @@ class LocalNodeService {
|
|
|
1845
2234
|
profile: this.profile,
|
|
1846
2235
|
};
|
|
1847
2236
|
const presenceRecord = (0, core_1.signPresence)(this.identity, Date.now());
|
|
1848
|
-
const
|
|
1849
|
-
const replayMessages = this.getReplayableSelfSocialMessages();
|
|
2237
|
+
const shouldPublishProfile = this.shouldPublishProfileRecord(profileRecord, reason, presenceRecord.timestamp);
|
|
2238
|
+
const replayMessages = this.getReplayableSelfSocialMessages(reason);
|
|
1850
2239
|
try {
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
for (const record of indexRecords) {
|
|
1854
|
-
await this.publish("index", record);
|
|
2240
|
+
if (shouldPublishProfile) {
|
|
2241
|
+
await this.publish("profile", profileRecord);
|
|
1855
2242
|
}
|
|
2243
|
+
await this.publish("presence", presenceRecord);
|
|
1856
2244
|
for (const message of replayMessages) {
|
|
1857
2245
|
await this.publish(SOCIAL_MESSAGE_TOPIC, message);
|
|
1858
2246
|
}
|
|
@@ -1862,23 +2250,67 @@ class LocalNodeService {
|
|
|
1862
2250
|
this.lastBroadcastErrorAt = Date.now();
|
|
1863
2251
|
this.lastBroadcastError = message;
|
|
1864
2252
|
this.broadcastFailureCount += 1;
|
|
2253
|
+
this.consecutiveBroadcastFailures += 1;
|
|
1865
2254
|
await this.log("error", `Broadcast failed (reason=${reason}): ${message}`);
|
|
2255
|
+
await this.maybeRecoverFromBroadcastFailure(reason, message);
|
|
1866
2256
|
return { sent: false, reason: "publish_failed", error: message };
|
|
1867
2257
|
}
|
|
1868
2258
|
this.lastBroadcastAt = Date.now();
|
|
1869
2259
|
this.broadcastCount += 1;
|
|
1870
2260
|
this.lastBroadcastError = null;
|
|
1871
2261
|
this.lastBroadcastErrorAt = 0;
|
|
2262
|
+
this.consecutiveBroadcastFailures = 0;
|
|
1872
2263
|
this.directory = (0, core_1.ingestProfileRecord)(this.directory, profileRecord);
|
|
1873
2264
|
this.directory = (0, core_1.ingestPresenceRecord)(this.directory, presenceRecord);
|
|
1874
|
-
for (const record of indexRecords) {
|
|
1875
|
-
this.directory = (0, core_1.ingestIndexRecord)(this.directory, record);
|
|
1876
|
-
}
|
|
1877
2265
|
this.compactCacheInMemory();
|
|
1878
2266
|
await this.persistCache();
|
|
1879
|
-
await this.log("info", `Broadcast sent (${
|
|
2267
|
+
await this.log("info", `Broadcast sent (${shouldPublishProfile ? "profile + " : ""}presence, replayed_messages=${replayMessages.length}, reason=${reason})`);
|
|
1880
2268
|
return { sent: true, reason };
|
|
1881
2269
|
}
|
|
2270
|
+
shouldPublishProfileRecord(profileRecord, reason, now = Date.now()) {
|
|
2271
|
+
if (reason !== "interval") {
|
|
2272
|
+
this.lastProfileBroadcastSignature = profileRecord.profile.signature;
|
|
2273
|
+
this.lastProfileBroadcastAt = now;
|
|
2274
|
+
return true;
|
|
2275
|
+
}
|
|
2276
|
+
const signature = profileRecord.profile.signature;
|
|
2277
|
+
const changedSinceLastPublish = signature !== this.lastProfileBroadcastSignature;
|
|
2278
|
+
const refreshDue = now - this.lastProfileBroadcastAt >= PROFILE_RELAY_REFRESH_INTERVAL_MS;
|
|
2279
|
+
if (!changedSinceLastPublish && !refreshDue) {
|
|
2280
|
+
return false;
|
|
2281
|
+
}
|
|
2282
|
+
this.lastProfileBroadcastSignature = signature;
|
|
2283
|
+
this.lastProfileBroadcastAt = now;
|
|
2284
|
+
return true;
|
|
2285
|
+
}
|
|
2286
|
+
async maybeRecoverFromBroadcastFailure(reason, errorMessage) {
|
|
2287
|
+
const recoveryThreshold = 3;
|
|
2288
|
+
const recoveryCooldownMs = 60_000;
|
|
2289
|
+
if (this.broadcastRecoveryInFlight) {
|
|
2290
|
+
return;
|
|
2291
|
+
}
|
|
2292
|
+
if (this.consecutiveBroadcastFailures < recoveryThreshold) {
|
|
2293
|
+
return;
|
|
2294
|
+
}
|
|
2295
|
+
if (Date.now() - this.lastBroadcastRecoveryAttemptAt < recoveryCooldownMs) {
|
|
2296
|
+
return;
|
|
2297
|
+
}
|
|
2298
|
+
if (this.adapterMode !== "relay-preview" && this.adapterMode !== "webrtc-preview" && this.adapterMode !== "real-preview") {
|
|
2299
|
+
return;
|
|
2300
|
+
}
|
|
2301
|
+
this.broadcastRecoveryInFlight = true;
|
|
2302
|
+
this.lastBroadcastRecoveryAttemptAt = Date.now();
|
|
2303
|
+
try {
|
|
2304
|
+
await this.log("warn", `Broadcast recovery triggered after ${this.consecutiveBroadcastFailures} consecutive failures (${reason}): ${errorMessage}`);
|
|
2305
|
+
await this.restartNetworkAdapter("broadcast_failure_recovery");
|
|
2306
|
+
}
|
|
2307
|
+
catch (recoveryError) {
|
|
2308
|
+
await this.log("error", `Broadcast recovery failed: ${recoveryError instanceof Error ? recoveryError.message : String(recoveryError)}`);
|
|
2309
|
+
}
|
|
2310
|
+
finally {
|
|
2311
|
+
this.broadcastRecoveryInFlight = false;
|
|
2312
|
+
}
|
|
2313
|
+
}
|
|
1882
2314
|
async hydrateFromDisk() {
|
|
1883
2315
|
this.initState = {
|
|
1884
2316
|
identity_auto_created: false,
|
|
@@ -1907,6 +2339,8 @@ class LocalNodeService {
|
|
|
1907
2339
|
await this.log("info", `Bound existing OpenClaw identity: ${resolvedIdentity.openclaw_source_path}`);
|
|
1908
2340
|
}
|
|
1909
2341
|
await this.identityRepo.set(this.identity);
|
|
2342
|
+
this.privateEncryptionKeyPair = (await this.privateEncryptionKeyRepo.get()) || (0, core_1.createPrivateEncryptionKeyPair)();
|
|
2343
|
+
await this.privateEncryptionKeyRepo.set(this.privateEncryptionKeyPair);
|
|
1910
2344
|
const existingProfile = await this.profileRepo.get();
|
|
1911
2345
|
const profileInput = (0, core_1.resolveProfileInputWithSocial)({
|
|
1912
2346
|
socialConfig: this.socialConfig,
|
|
@@ -1914,7 +2348,10 @@ class LocalNodeService {
|
|
|
1914
2348
|
existingProfile: existingProfile && existingProfile.agent_id === this.identity.agent_id ? existingProfile : null,
|
|
1915
2349
|
rootDir: this.projectRoot,
|
|
1916
2350
|
});
|
|
1917
|
-
this.profile = (0, core_1.signProfile)(
|
|
2351
|
+
this.profile = (0, core_1.signProfile)({
|
|
2352
|
+
...profileInput,
|
|
2353
|
+
private_encryption_public_key: this.privateEncryptionKeyPair?.public_key || profileInput.private_encryption_public_key || "",
|
|
2354
|
+
}, this.identity);
|
|
1918
2355
|
if (!existingProfile || existingProfile.agent_id !== this.identity.agent_id) {
|
|
1919
2356
|
this.initState.profile_auto_created = true;
|
|
1920
2357
|
await this.log("info", "profile.json missing/invalid, initialized from social/default profile");
|
|
@@ -1927,6 +2364,11 @@ class LocalNodeService {
|
|
|
1927
2364
|
};
|
|
1928
2365
|
this.socialMessages = this.normalizeSocialMessages(await this.socialMessageRepo.get());
|
|
1929
2366
|
this.socialMessageObservations = this.normalizeSocialMessageObservations(await this.socialMessageObservationRepo.get());
|
|
2367
|
+
const storedPrivateMessages = await this.privateMessageRepo.get();
|
|
2368
|
+
this.hydratePrivateMessageBodyCache(storedPrivateMessages);
|
|
2369
|
+
this.privateMessages = this.normalizePrivateMessages(storedPrivateMessages);
|
|
2370
|
+
this.privateMessageReceipts = this.normalizePrivateMessageReceipts(await this.privateMessageReceiptRepo.get());
|
|
2371
|
+
await this.refreshPrivateMessagingRuntime();
|
|
1930
2372
|
this.directory = (0, core_1.ingestProfileRecord)(this.directory, { type: "profile", profile: this.profile });
|
|
1931
2373
|
this.compactCacheInMemory();
|
|
1932
2374
|
await this.persistCache();
|
|
@@ -1943,7 +2385,10 @@ class LocalNodeService {
|
|
|
1943
2385
|
existingProfile: this.profile,
|
|
1944
2386
|
rootDir: this.projectRoot,
|
|
1945
2387
|
});
|
|
1946
|
-
const nextProfile = (0, core_1.signProfile)(
|
|
2388
|
+
const nextProfile = (0, core_1.signProfile)({
|
|
2389
|
+
...nextProfileInput,
|
|
2390
|
+
private_encryption_public_key: this.privateEncryptionKeyPair?.public_key || nextProfileInput.private_encryption_public_key || "",
|
|
2391
|
+
}, this.identity);
|
|
1947
2392
|
this.profile = nextProfile;
|
|
1948
2393
|
await this.profileRepo.set(nextProfile);
|
|
1949
2394
|
this.directory = (0, core_1.ingestProfileRecord)(this.directory, { type: "profile", profile: nextProfile });
|
|
@@ -2001,7 +2446,7 @@ class LocalNodeService {
|
|
|
2001
2446
|
this.socialRuntime = runtime;
|
|
2002
2447
|
await this.socialRuntimeRepo.set(runtime);
|
|
2003
2448
|
}
|
|
2004
|
-
async onMessage(topic, data) {
|
|
2449
|
+
async onMessage(topic, data, meta) {
|
|
2005
2450
|
this.receivedCount += 1;
|
|
2006
2451
|
this.receivedByTopic[topic] = (this.receivedByTopic[topic] ?? 0) + 1;
|
|
2007
2452
|
this.lastMessageAt = Date.now();
|
|
@@ -2016,6 +2461,9 @@ class LocalNodeService {
|
|
|
2016
2461
|
return;
|
|
2017
2462
|
}
|
|
2018
2463
|
}
|
|
2464
|
+
if (meta?.peerId && record.profile.agent_id && !this.privatePeerRoutes[record.profile.agent_id]) {
|
|
2465
|
+
this.privatePeerRoutes[record.profile.agent_id] = meta.peerId;
|
|
2466
|
+
}
|
|
2019
2467
|
this.directory = (0, core_1.ingestProfileRecord)(this.directory, record);
|
|
2020
2468
|
this.compactCacheInMemory();
|
|
2021
2469
|
await this.persistCache();
|
|
@@ -2032,6 +2480,9 @@ class LocalNodeService {
|
|
|
2032
2480
|
return;
|
|
2033
2481
|
}
|
|
2034
2482
|
}
|
|
2483
|
+
if (meta?.peerId && record.agent_id && !this.privatePeerRoutes[record.agent_id]) {
|
|
2484
|
+
this.privatePeerRoutes[record.agent_id] = meta.peerId;
|
|
2485
|
+
}
|
|
2035
2486
|
this.directory = (0, core_1.ingestPresenceRecord)(this.directory, record);
|
|
2036
2487
|
this.compactCacheInMemory();
|
|
2037
2488
|
await this.persistCache();
|
|
@@ -2046,6 +2497,9 @@ class LocalNodeService {
|
|
|
2046
2497
|
await this.log("warn", `Rejected social message with invalid signature (${record.message_id.slice(0, 10)})`);
|
|
2047
2498
|
return;
|
|
2048
2499
|
}
|
|
2500
|
+
if (meta?.peerId && record.agent_id && !this.privatePeerRoutes[record.agent_id]) {
|
|
2501
|
+
this.privatePeerRoutes[record.agent_id] = meta.peerId;
|
|
2502
|
+
}
|
|
2049
2503
|
if (this.hasSocialMessage(record.message_id)) {
|
|
2050
2504
|
await this.publishObservationForMessage(record);
|
|
2051
2505
|
return;
|
|
@@ -2081,6 +2535,39 @@ class LocalNodeService {
|
|
|
2081
2535
|
this.directory = (0, core_1.dedupeIndex)(this.directory);
|
|
2082
2536
|
await this.persistCache();
|
|
2083
2537
|
}
|
|
2538
|
+
async onDirectMessage(topic, data, meta) {
|
|
2539
|
+
if (topic === PRIVATE_MESSAGE_TOPIC) {
|
|
2540
|
+
const record = this.normalizeIncomingPrivateMessage(data);
|
|
2541
|
+
if (!record || !(0, core_1.verifyPrivateMessage)(record)) {
|
|
2542
|
+
return;
|
|
2543
|
+
}
|
|
2544
|
+
if (meta?.peerId && record.from_agent_id) {
|
|
2545
|
+
this.privatePeerRoutes[record.from_agent_id] = meta.peerId;
|
|
2546
|
+
}
|
|
2547
|
+
if (record.from_agent_id && record.sender_encryption_public_key) {
|
|
2548
|
+
this.privatePeerEncryptionKeys[record.from_agent_id] = record.sender_encryption_public_key;
|
|
2549
|
+
}
|
|
2550
|
+
if (record.to_agent_id !== this.identity?.agent_id || this.hasPrivateMessage(record.message_id)) {
|
|
2551
|
+
return;
|
|
2552
|
+
}
|
|
2553
|
+
this.ingestPrivateMessage(record);
|
|
2554
|
+
await this.persistPrivateMessages();
|
|
2555
|
+
await this.sendPrivateMessageReceipt(record, meta?.peerId);
|
|
2556
|
+
return;
|
|
2557
|
+
}
|
|
2558
|
+
const receipt = this.normalizeIncomingPrivateMessageReceipt(data);
|
|
2559
|
+
if (!receipt || !(0, core_1.verifyPrivateMessageReceipt)(receipt)) {
|
|
2560
|
+
return;
|
|
2561
|
+
}
|
|
2562
|
+
if (meta?.peerId && receipt.from_agent_id) {
|
|
2563
|
+
this.privatePeerRoutes[receipt.from_agent_id] = meta.peerId;
|
|
2564
|
+
}
|
|
2565
|
+
if (receipt.to_agent_id !== this.identity?.agent_id) {
|
|
2566
|
+
return;
|
|
2567
|
+
}
|
|
2568
|
+
this.ingestPrivateMessageReceipt(receipt);
|
|
2569
|
+
await this.persistPrivateMessageReceipts();
|
|
2570
|
+
}
|
|
2084
2571
|
startBroadcastLoop() {
|
|
2085
2572
|
if (this.broadcaster) {
|
|
2086
2573
|
clearInterval(this.broadcaster);
|
|
@@ -2101,21 +2588,35 @@ class LocalNodeService {
|
|
|
2101
2588
|
if (this.subscriptionsBound) {
|
|
2102
2589
|
return;
|
|
2103
2590
|
}
|
|
2104
|
-
this.network.subscribe("profile", (data) => {
|
|
2105
|
-
this.onMessage("profile", data);
|
|
2591
|
+
this.network.subscribe("profile", (data, meta) => {
|
|
2592
|
+
this.onMessage("profile", data, meta);
|
|
2106
2593
|
});
|
|
2107
|
-
this.network.subscribe("presence", (data) => {
|
|
2108
|
-
this.onMessage("presence", data);
|
|
2594
|
+
this.network.subscribe("presence", (data, meta) => {
|
|
2595
|
+
this.onMessage("presence", data, meta);
|
|
2109
2596
|
});
|
|
2110
|
-
this.network.subscribe("index", (data) => {
|
|
2111
|
-
this.onMessage("index", data);
|
|
2597
|
+
this.network.subscribe("index", (data, meta) => {
|
|
2598
|
+
this.onMessage("index", data, meta);
|
|
2112
2599
|
});
|
|
2113
|
-
this.network.subscribe(SOCIAL_MESSAGE_TOPIC, (data) => {
|
|
2114
|
-
this.onMessage(SOCIAL_MESSAGE_TOPIC, data);
|
|
2600
|
+
this.network.subscribe(SOCIAL_MESSAGE_TOPIC, (data, meta) => {
|
|
2601
|
+
this.onMessage(SOCIAL_MESSAGE_TOPIC, data, meta);
|
|
2115
2602
|
});
|
|
2116
|
-
this.network.subscribe(SOCIAL_MESSAGE_OBSERVATION_TOPIC, (data) => {
|
|
2117
|
-
this.onMessage(SOCIAL_MESSAGE_OBSERVATION_TOPIC, data);
|
|
2603
|
+
this.network.subscribe(SOCIAL_MESSAGE_OBSERVATION_TOPIC, (data, meta) => {
|
|
2604
|
+
this.onMessage(SOCIAL_MESSAGE_OBSERVATION_TOPIC, data, meta);
|
|
2118
2605
|
});
|
|
2606
|
+
this.network.subscribe(PRIVATE_MESSAGE_TOPIC, (data, meta) => {
|
|
2607
|
+
this.onDirectMessage(PRIVATE_MESSAGE_TOPIC, data, meta);
|
|
2608
|
+
});
|
|
2609
|
+
this.network.subscribe(PRIVATE_MESSAGE_RECEIPT_TOPIC, (data, meta) => {
|
|
2610
|
+
this.onDirectMessage(PRIVATE_MESSAGE_RECEIPT_TOPIC, data, meta);
|
|
2611
|
+
});
|
|
2612
|
+
if (typeof this.network.subscribeDirect === "function") {
|
|
2613
|
+
this.network.subscribeDirect(PRIVATE_MESSAGE_TOPIC, (data, meta) => {
|
|
2614
|
+
this.onDirectMessage(PRIVATE_MESSAGE_TOPIC, data, meta);
|
|
2615
|
+
});
|
|
2616
|
+
this.network.subscribeDirect(PRIVATE_MESSAGE_RECEIPT_TOPIC, (data, meta) => {
|
|
2617
|
+
this.onDirectMessage(PRIVATE_MESSAGE_RECEIPT_TOPIC, data, meta);
|
|
2618
|
+
});
|
|
2619
|
+
}
|
|
2119
2620
|
this.subscriptionsBound = true;
|
|
2120
2621
|
}
|
|
2121
2622
|
buildNetworkAdapter() {
|
|
@@ -2255,9 +2756,58 @@ class LocalNodeService {
|
|
|
2255
2756
|
}, delayMs);
|
|
2256
2757
|
this.networkReconnectDelayMs = Math.min(30_000, Math.max(5_000, Math.floor(delayMs * 1.5)));
|
|
2257
2758
|
}
|
|
2759
|
+
pruneRemoteProfilesInMemory(now = Date.now()) {
|
|
2760
|
+
if (!Number.isFinite(DIRECTORY_REMOTE_PROFILE_SOFT_LIMIT) || DIRECTORY_REMOTE_PROFILE_SOFT_LIMIT <= 0) {
|
|
2761
|
+
return 0;
|
|
2762
|
+
}
|
|
2763
|
+
const selfAgentId = this.profile?.agent_id || this.identity?.agent_id || "";
|
|
2764
|
+
const remoteProfiles = Object.values(this.directory.profiles).filter((profile) => profile.agent_id !== selfAgentId);
|
|
2765
|
+
if (remoteProfiles.length <= DIRECTORY_REMOTE_PROFILE_SOFT_LIMIT) {
|
|
2766
|
+
return 0;
|
|
2767
|
+
}
|
|
2768
|
+
const onlineRemoteProfiles = remoteProfiles.filter((profile) => (0, core_1.isAgentOnline)(this.directory.presence[profile.agent_id], now, PRESENCE_TTL_MS));
|
|
2769
|
+
const offlineRemoteProfiles = remoteProfiles
|
|
2770
|
+
.filter((profile) => !(0, core_1.isAgentOnline)(this.directory.presence[profile.agent_id], now, PRESENCE_TTL_MS))
|
|
2771
|
+
.sort((a, b) => (b.updated_at || 0) - (a.updated_at || 0));
|
|
2772
|
+
const keepOfflineCount = Math.max(0, DIRECTORY_REMOTE_PROFILE_SOFT_LIMIT - onlineRemoteProfiles.length);
|
|
2773
|
+
const keptRemoteProfiles = [
|
|
2774
|
+
...onlineRemoteProfiles,
|
|
2775
|
+
...offlineRemoteProfiles.slice(0, keepOfflineCount),
|
|
2776
|
+
];
|
|
2777
|
+
const keptRemoteIds = new Set(keptRemoteProfiles.map((profile) => profile.agent_id));
|
|
2778
|
+
const removedIds = remoteProfiles
|
|
2779
|
+
.map((profile) => profile.agent_id)
|
|
2780
|
+
.filter((agentId) => !keptRemoteIds.has(agentId));
|
|
2781
|
+
if (removedIds.length === 0) {
|
|
2782
|
+
return 0;
|
|
2783
|
+
}
|
|
2784
|
+
const next = (0, core_1.createEmptyDirectoryState)();
|
|
2785
|
+
const selfProfile = selfAgentId ? this.directory.profiles[selfAgentId] : null;
|
|
2786
|
+
if (selfProfile) {
|
|
2787
|
+
next.profiles[selfAgentId] = selfProfile;
|
|
2788
|
+
const selfPresence = this.directory.presence[selfAgentId];
|
|
2789
|
+
if (typeof selfPresence === "number" && Number.isFinite(selfPresence)) {
|
|
2790
|
+
next.presence[selfAgentId] = selfPresence;
|
|
2791
|
+
}
|
|
2792
|
+
const rebuilt = (0, core_1.rebuildIndexForProfile)(next, selfProfile);
|
|
2793
|
+
next.index = rebuilt.index;
|
|
2794
|
+
}
|
|
2795
|
+
for (const profile of keptRemoteProfiles) {
|
|
2796
|
+
next.profiles[profile.agent_id] = profile;
|
|
2797
|
+
const seenAt = this.directory.presence[profile.agent_id];
|
|
2798
|
+
if (typeof seenAt === "number" && Number.isFinite(seenAt)) {
|
|
2799
|
+
next.presence[profile.agent_id] = seenAt;
|
|
2800
|
+
}
|
|
2801
|
+
const rebuilt = (0, core_1.rebuildIndexForProfile)(next, profile);
|
|
2802
|
+
next.index = rebuilt.index;
|
|
2803
|
+
}
|
|
2804
|
+
this.directory = (0, core_1.dedupeIndex)(next);
|
|
2805
|
+
return removedIds.length;
|
|
2806
|
+
}
|
|
2258
2807
|
compactCacheInMemory() {
|
|
2259
2808
|
const cleaned = (0, core_1.cleanupExpiredPresence)(this.directory, Date.now(), PRESENCE_TTL_MS);
|
|
2260
2809
|
this.directory = (0, core_1.dedupeIndex)(cleaned.state);
|
|
2810
|
+
this.pruneRemoteProfilesInMemory();
|
|
2261
2811
|
return cleaned.removed;
|
|
2262
2812
|
}
|
|
2263
2813
|
async publish(topic, data) {
|
|
@@ -2288,6 +2838,82 @@ class LocalNodeService {
|
|
|
2288
2838
|
async persistSocialMessageObservations() {
|
|
2289
2839
|
await this.socialMessageObservationRepo.set(this.socialMessageObservations);
|
|
2290
2840
|
}
|
|
2841
|
+
async persistPrivateMessages() {
|
|
2842
|
+
this.privateMessagesPersistDirty = true;
|
|
2843
|
+
if (this.privateMessagesPersistTimer) {
|
|
2844
|
+
return;
|
|
2845
|
+
}
|
|
2846
|
+
this.privateMessagesPersistTimer = setTimeout(() => {
|
|
2847
|
+
this.flushPrivateMessagesPersist().catch(() => { });
|
|
2848
|
+
}, PRIVATE_MESSAGE_PERSIST_DEBOUNCE_MS);
|
|
2849
|
+
}
|
|
2850
|
+
async persistPrivateMessageReceipts() {
|
|
2851
|
+
this.privateMessageReceiptsPersistDirty = true;
|
|
2852
|
+
if (this.privateMessageReceiptsPersistTimer) {
|
|
2853
|
+
return;
|
|
2854
|
+
}
|
|
2855
|
+
this.privateMessageReceiptsPersistTimer = setTimeout(() => {
|
|
2856
|
+
this.flushPrivateMessageReceiptsPersist().catch(() => { });
|
|
2857
|
+
}, PRIVATE_MESSAGE_PERSIST_DEBOUNCE_MS);
|
|
2858
|
+
}
|
|
2859
|
+
async flushPrivatePersistence() {
|
|
2860
|
+
await Promise.all([
|
|
2861
|
+
this.flushPrivateMessagesPersist(),
|
|
2862
|
+
this.flushPrivateMessageReceiptsPersist(),
|
|
2863
|
+
]);
|
|
2864
|
+
}
|
|
2865
|
+
async flushPrivateMessagesPersist() {
|
|
2866
|
+
if (this.privateMessagesPersistTimer) {
|
|
2867
|
+
clearTimeout(this.privateMessagesPersistTimer);
|
|
2868
|
+
this.privateMessagesPersistTimer = null;
|
|
2869
|
+
}
|
|
2870
|
+
if (!this.privateMessagesPersistDirty) {
|
|
2871
|
+
return;
|
|
2872
|
+
}
|
|
2873
|
+
this.privateMessagesPersistDirty = false;
|
|
2874
|
+
await this.privateMessageRepo.set(this.buildPersistedPrivateMessages());
|
|
2875
|
+
}
|
|
2876
|
+
hydratePrivateMessageBodyCache(items) {
|
|
2877
|
+
if (!Array.isArray(items)) {
|
|
2878
|
+
return;
|
|
2879
|
+
}
|
|
2880
|
+
for (const item of items) {
|
|
2881
|
+
if (typeof item !== "object" || item === null) {
|
|
2882
|
+
continue;
|
|
2883
|
+
}
|
|
2884
|
+
const record = item;
|
|
2885
|
+
const messageId = String(record.message_id || "").trim();
|
|
2886
|
+
const localPlaintext = typeof record.local_plaintext === "string" ? record.local_plaintext : "";
|
|
2887
|
+
if (messageId && localPlaintext) {
|
|
2888
|
+
this.privateMessageBodyCache.set(messageId, localPlaintext);
|
|
2889
|
+
}
|
|
2890
|
+
}
|
|
2891
|
+
}
|
|
2892
|
+
buildPersistedPrivateMessages() {
|
|
2893
|
+
return this.privateMessages.map((message) => {
|
|
2894
|
+
const localPlaintext = message.from_agent_id === this.identity?.agent_id
|
|
2895
|
+
? this.privateMessageBodyCache.get(message.message_id) || ""
|
|
2896
|
+
: "";
|
|
2897
|
+
if (!localPlaintext) {
|
|
2898
|
+
return { ...message };
|
|
2899
|
+
}
|
|
2900
|
+
return {
|
|
2901
|
+
...message,
|
|
2902
|
+
local_plaintext: localPlaintext,
|
|
2903
|
+
};
|
|
2904
|
+
});
|
|
2905
|
+
}
|
|
2906
|
+
async flushPrivateMessageReceiptsPersist() {
|
|
2907
|
+
if (this.privateMessageReceiptsPersistTimer) {
|
|
2908
|
+
clearTimeout(this.privateMessageReceiptsPersistTimer);
|
|
2909
|
+
this.privateMessageReceiptsPersistTimer = null;
|
|
2910
|
+
}
|
|
2911
|
+
if (!this.privateMessageReceiptsPersistDirty) {
|
|
2912
|
+
return;
|
|
2913
|
+
}
|
|
2914
|
+
this.privateMessageReceiptsPersistDirty = false;
|
|
2915
|
+
await this.privateMessageReceiptRepo.set(this.privateMessageReceipts);
|
|
2916
|
+
}
|
|
2291
2917
|
async log(level, message) {
|
|
2292
2918
|
await this.logRepo.append({
|
|
2293
2919
|
level,
|
|
@@ -2334,6 +2960,7 @@ class LocalNodeService {
|
|
|
2334
2960
|
(0, core_1.verifyProfile)(profile, selfPublicKey));
|
|
2335
2961
|
return (0, core_1.buildPublicProfileSummary)({
|
|
2336
2962
|
profile,
|
|
2963
|
+
is_self: isSelf,
|
|
2337
2964
|
online,
|
|
2338
2965
|
last_seen_at: lastSeenAt || null,
|
|
2339
2966
|
network_mode: isSelf ? this.networkMode : "unknown",
|
|
@@ -2379,6 +3006,7 @@ class LocalNodeService {
|
|
|
2379
3006
|
updated_at: message.created_at,
|
|
2380
3007
|
signature: "",
|
|
2381
3008
|
},
|
|
3009
|
+
is_self: message.agent_id === this.identity?.agent_id,
|
|
2382
3010
|
online: false,
|
|
2383
3011
|
last_seen_at: null,
|
|
2384
3012
|
network_mode: "unknown",
|
|
@@ -2408,6 +3036,45 @@ class LocalNodeService {
|
|
|
2408
3036
|
const digest = (0, crypto_1.createHash)("sha256").update(publicKey, "utf8").digest("hex");
|
|
2409
3037
|
return `${digest.slice(0, 12)}:${digest.slice(-8)}`;
|
|
2410
3038
|
}
|
|
3039
|
+
buildPrivateMessagingRuntimeState() {
|
|
3040
|
+
const warnings = [];
|
|
3041
|
+
const keypair = this.privateEncryptionKeyPair;
|
|
3042
|
+
const selfSentMessages = this.privateMessages.filter((message) => message.from_agent_id === this.identity?.agent_id);
|
|
3043
|
+
let cachedPlaintextCount = 0;
|
|
3044
|
+
for (const message of selfSentMessages) {
|
|
3045
|
+
if (this.privateMessageBodyCache.get(message.message_id)) {
|
|
3046
|
+
cachedPlaintextCount += 1;
|
|
3047
|
+
}
|
|
3048
|
+
}
|
|
3049
|
+
if (!keypair?.public_key || !keypair?.private_key) {
|
|
3050
|
+
warnings.push("missing_private_encryption_keypair");
|
|
3051
|
+
}
|
|
3052
|
+
if (selfSentMessages.length > 0 && cachedPlaintextCount === 0) {
|
|
3053
|
+
warnings.push("missing_local_plaintext_cache_for_self_messages");
|
|
3054
|
+
}
|
|
3055
|
+
if (selfSentMessages.length > 0 && cachedPlaintextCount < selfSentMessages.length) {
|
|
3056
|
+
warnings.push("partial_local_plaintext_cache_for_self_messages");
|
|
3057
|
+
}
|
|
3058
|
+
return {
|
|
3059
|
+
schema_version: 1,
|
|
3060
|
+
app_version: this.appVersion,
|
|
3061
|
+
last_started_at: Date.now(),
|
|
3062
|
+
encryption_public_key: keypair?.public_key || "",
|
|
3063
|
+
encryption_public_key_fingerprint: keypair?.public_key ? this.fingerprintPublicKey(keypair.public_key) : "",
|
|
3064
|
+
message_count: this.privateMessages.length,
|
|
3065
|
+
self_sent_count: selfSentMessages.length,
|
|
3066
|
+
cached_plaintext_count: cachedPlaintextCount,
|
|
3067
|
+
warnings,
|
|
3068
|
+
};
|
|
3069
|
+
}
|
|
3070
|
+
async refreshPrivateMessagingRuntime() {
|
|
3071
|
+
const runtime = this.buildPrivateMessagingRuntimeState();
|
|
3072
|
+
this.privateMessagingRuntime = runtime;
|
|
3073
|
+
await this.privateMessagingRuntimeRepo.set(runtime);
|
|
3074
|
+
for (const warning of runtime.warnings) {
|
|
3075
|
+
await this.log("warn", `Private messaging startup check: ${warning}`);
|
|
3076
|
+
}
|
|
3077
|
+
}
|
|
2411
3078
|
getOnboardingSummary() {
|
|
2412
3079
|
const summary = this.getIntegrationSummary();
|
|
2413
3080
|
const publicEnabled = Boolean(this.profile?.public_enabled);
|
|
@@ -2566,6 +3233,32 @@ class LocalNodeService {
|
|
|
2566
3233
|
.join("\n")
|
|
2567
3234
|
.trim();
|
|
2568
3235
|
}
|
|
3236
|
+
buildPrivateConversationId(leftAgentId, rightAgentId) {
|
|
3237
|
+
return [String(leftAgentId || "").trim(), String(rightAgentId || "").trim()].sort().join(":");
|
|
3238
|
+
}
|
|
3239
|
+
decryptPrivateMessageBody(message) {
|
|
3240
|
+
const cached = this.privateMessageBodyCache.get(message.message_id);
|
|
3241
|
+
if (typeof cached === "string") {
|
|
3242
|
+
return cached;
|
|
3243
|
+
}
|
|
3244
|
+
if (!this.privateEncryptionKeyPair) {
|
|
3245
|
+
return "[encrypted]";
|
|
3246
|
+
}
|
|
3247
|
+
const decrypted = (0, core_1.decryptPrivatePayload)({
|
|
3248
|
+
ciphertext: message.ciphertext,
|
|
3249
|
+
nonce: message.nonce,
|
|
3250
|
+
sender_encryption_public_key: message.sender_encryption_public_key,
|
|
3251
|
+
recipient_private_key: this.privateEncryptionKeyPair.private_key,
|
|
3252
|
+
}) || "[encrypted]";
|
|
3253
|
+
this.privateMessageBodyCache.set(message.message_id, decrypted);
|
|
3254
|
+
if (this.privateMessageBodyCache.size > PRIVATE_MESSAGE_HISTORY_LIMIT * 2) {
|
|
3255
|
+
const firstKey = this.privateMessageBodyCache.keys().next().value;
|
|
3256
|
+
if (firstKey) {
|
|
3257
|
+
this.privateMessageBodyCache.delete(firstKey);
|
|
3258
|
+
}
|
|
3259
|
+
}
|
|
3260
|
+
return decrypted;
|
|
3261
|
+
}
|
|
2569
3262
|
normalizeWindowTimestamps(timestamps, windowMs, now = Date.now()) {
|
|
2570
3263
|
return timestamps.filter((timestamp) => now - timestamp <= windowMs);
|
|
2571
3264
|
}
|
|
@@ -2586,16 +3279,30 @@ class LocalNodeService {
|
|
|
2586
3279
|
hasSocialMessage(messageId) {
|
|
2587
3280
|
return this.socialMessages.some((item) => item.message_id === messageId);
|
|
2588
3281
|
}
|
|
2589
|
-
getReplayableSelfSocialMessages(now = Date.now()) {
|
|
3282
|
+
getReplayableSelfSocialMessages(reason = "manual", now = Date.now()) {
|
|
2590
3283
|
const maxCount = Math.max(0, SOCIAL_MESSAGE_REPLAY_MAX_PER_BROADCAST);
|
|
2591
3284
|
if (!this.identity || maxCount === 0) {
|
|
2592
3285
|
return [];
|
|
2593
3286
|
}
|
|
2594
|
-
|
|
3287
|
+
const replayable = this.socialMessages
|
|
2595
3288
|
.filter((item) => (item.agent_id === this.identity?.agent_id &&
|
|
2596
3289
|
now - item.created_at <= SOCIAL_MESSAGE_REPLAY_WINDOW_MS))
|
|
2597
3290
|
.sort((a, b) => a.created_at - b.created_at)
|
|
2598
3291
|
.slice(-maxCount);
|
|
3292
|
+
if (!replayable.length) {
|
|
3293
|
+
this.lastReplayBroadcastSignature = "";
|
|
3294
|
+
return [];
|
|
3295
|
+
}
|
|
3296
|
+
const signature = replayable.map((item) => item.message_id).join(",");
|
|
3297
|
+
const isIntervalReplay = reason === "interval";
|
|
3298
|
+
const changedSinceLastReplay = signature !== this.lastReplayBroadcastSignature;
|
|
3299
|
+
const refreshDue = now - this.lastReplayBroadcastAt >= SOCIAL_MESSAGE_REPLAY_REFRESH_INTERVAL_MS;
|
|
3300
|
+
if (isIntervalReplay && !changedSinceLastReplay && !refreshDue) {
|
|
3301
|
+
return [];
|
|
3302
|
+
}
|
|
3303
|
+
this.lastReplayBroadcastSignature = signature;
|
|
3304
|
+
this.lastReplayBroadcastAt = now;
|
|
3305
|
+
return replayable;
|
|
2599
3306
|
}
|
|
2600
3307
|
hasRecentDuplicateMessage(agentId, body, topic, now = Date.now()) {
|
|
2601
3308
|
return this.socialMessages.some((item) => (item.agent_id === agentId &&
|
|
@@ -2659,6 +3366,181 @@ class LocalNodeService {
|
|
|
2659
3366
|
await this.publish(SOCIAL_MESSAGE_OBSERVATION_TOPIC, observation);
|
|
2660
3367
|
await this.persistSocialMessageObservations();
|
|
2661
3368
|
}
|
|
3369
|
+
async sendPrivateMessageReceipt(message, replyPeerId) {
|
|
3370
|
+
if (!this.identity || typeof this.network.sendDirect !== "function" || !replyPeerId) {
|
|
3371
|
+
return;
|
|
3372
|
+
}
|
|
3373
|
+
const receipt = (0, core_1.signPrivateMessageReceipt)({
|
|
3374
|
+
identity: this.identity,
|
|
3375
|
+
receipt_id: (0, crypto_1.createHash)("sha256").update(`${message.message_id}:${this.identity.agent_id}:${Date.now()}`, "utf8").digest("hex"),
|
|
3376
|
+
message_id: message.message_id,
|
|
3377
|
+
conversation_id: message.conversation_id,
|
|
3378
|
+
to_agent_id: message.from_agent_id,
|
|
3379
|
+
status: "received",
|
|
3380
|
+
created_at: Date.now(),
|
|
3381
|
+
});
|
|
3382
|
+
this.ingestPrivateMessageReceipt(receipt);
|
|
3383
|
+
try {
|
|
3384
|
+
await this.network.sendDirect(replyPeerId, PRIVATE_MESSAGE_RECEIPT_TOPIC, receipt);
|
|
3385
|
+
await this.publish(PRIVATE_MESSAGE_RECEIPT_TOPIC, receipt);
|
|
3386
|
+
}
|
|
3387
|
+
catch {
|
|
3388
|
+
await this.publish(PRIVATE_MESSAGE_RECEIPT_TOPIC, receipt);
|
|
3389
|
+
}
|
|
3390
|
+
await this.persistPrivateMessageReceipts();
|
|
3391
|
+
}
|
|
3392
|
+
normalizeIncomingPrivateMessage(value) {
|
|
3393
|
+
if (typeof value !== "object" || value === null) {
|
|
3394
|
+
return null;
|
|
3395
|
+
}
|
|
3396
|
+
const record = value;
|
|
3397
|
+
const createdAt = Number(record.created_at || 0);
|
|
3398
|
+
const fromAgentId = String(record.from_agent_id || "").trim();
|
|
3399
|
+
const toAgentId = String(record.to_agent_id || "").trim();
|
|
3400
|
+
const conversationId = String(record.conversation_id || "").trim();
|
|
3401
|
+
if (record.type !== PRIVATE_MESSAGE_TOPIC ||
|
|
3402
|
+
!String(record.message_id || "").trim() ||
|
|
3403
|
+
!conversationId ||
|
|
3404
|
+
!fromAgentId ||
|
|
3405
|
+
!toAgentId ||
|
|
3406
|
+
!String(record.sender_public_key || "").trim() ||
|
|
3407
|
+
!String(record.sender_encryption_public_key || "").trim() ||
|
|
3408
|
+
!String(record.recipient_encryption_public_key || "").trim() ||
|
|
3409
|
+
!String(record.ciphertext || "").trim() ||
|
|
3410
|
+
!String(record.nonce || "").trim() ||
|
|
3411
|
+
String(record.cipher_scheme || "") !== "nacl-box-v1" ||
|
|
3412
|
+
!String(record.signature || "").trim() ||
|
|
3413
|
+
!Number.isFinite(createdAt)) {
|
|
3414
|
+
return null;
|
|
3415
|
+
}
|
|
3416
|
+
if (fromAgentId === toAgentId) {
|
|
3417
|
+
return null;
|
|
3418
|
+
}
|
|
3419
|
+
if (conversationId !== this.buildPrivateConversationId(fromAgentId, toAgentId)) {
|
|
3420
|
+
return null;
|
|
3421
|
+
}
|
|
3422
|
+
return {
|
|
3423
|
+
type: PRIVATE_MESSAGE_TOPIC,
|
|
3424
|
+
message_id: String(record.message_id).trim(),
|
|
3425
|
+
conversation_id: conversationId,
|
|
3426
|
+
from_agent_id: fromAgentId,
|
|
3427
|
+
to_agent_id: toAgentId,
|
|
3428
|
+
sender_public_key: String(record.sender_public_key).trim(),
|
|
3429
|
+
sender_encryption_public_key: String(record.sender_encryption_public_key).trim(),
|
|
3430
|
+
recipient_encryption_public_key: String(record.recipient_encryption_public_key).trim(),
|
|
3431
|
+
cipher_scheme: "nacl-box-v1",
|
|
3432
|
+
ciphertext: String(record.ciphertext).trim(),
|
|
3433
|
+
nonce: String(record.nonce).trim(),
|
|
3434
|
+
created_at: createdAt,
|
|
3435
|
+
signature: String(record.signature).trim(),
|
|
3436
|
+
};
|
|
3437
|
+
}
|
|
3438
|
+
normalizePrivateMessages(items) {
|
|
3439
|
+
if (!Array.isArray(items)) {
|
|
3440
|
+
return [];
|
|
3441
|
+
}
|
|
3442
|
+
const deduped = new Set();
|
|
3443
|
+
return items
|
|
3444
|
+
.map((item) => this.normalizeIncomingPrivateMessage(item))
|
|
3445
|
+
.filter((item) => Boolean(item))
|
|
3446
|
+
.sort((a, b) => a.created_at - b.created_at)
|
|
3447
|
+
.filter((item) => {
|
|
3448
|
+
if (deduped.has(item.message_id)) {
|
|
3449
|
+
return false;
|
|
3450
|
+
}
|
|
3451
|
+
deduped.add(item.message_id);
|
|
3452
|
+
return true;
|
|
3453
|
+
})
|
|
3454
|
+
.slice(-PRIVATE_MESSAGE_HISTORY_LIMIT);
|
|
3455
|
+
}
|
|
3456
|
+
normalizeIncomingPrivateMessageReceipt(value) {
|
|
3457
|
+
if (typeof value !== "object" || value === null) {
|
|
3458
|
+
return null;
|
|
3459
|
+
}
|
|
3460
|
+
const record = value;
|
|
3461
|
+
const createdAt = Number(record.created_at || 0);
|
|
3462
|
+
const status = String(record.status || "").trim();
|
|
3463
|
+
if (record.type !== PRIVATE_MESSAGE_RECEIPT_TOPIC ||
|
|
3464
|
+
!String(record.receipt_id || "").trim() ||
|
|
3465
|
+
!String(record.message_id || "").trim() ||
|
|
3466
|
+
!String(record.conversation_id || "").trim() ||
|
|
3467
|
+
!String(record.from_agent_id || "").trim() ||
|
|
3468
|
+
!String(record.to_agent_id || "").trim() ||
|
|
3469
|
+
!String(record.sender_public_key || "").trim() ||
|
|
3470
|
+
(status !== "received" && status !== "read") ||
|
|
3471
|
+
!String(record.signature || "").trim() ||
|
|
3472
|
+
!Number.isFinite(createdAt)) {
|
|
3473
|
+
return null;
|
|
3474
|
+
}
|
|
3475
|
+
return {
|
|
3476
|
+
type: PRIVATE_MESSAGE_RECEIPT_TOPIC,
|
|
3477
|
+
receipt_id: String(record.receipt_id).trim(),
|
|
3478
|
+
message_id: String(record.message_id).trim(),
|
|
3479
|
+
conversation_id: String(record.conversation_id).trim(),
|
|
3480
|
+
from_agent_id: String(record.from_agent_id).trim(),
|
|
3481
|
+
to_agent_id: String(record.to_agent_id).trim(),
|
|
3482
|
+
sender_public_key: String(record.sender_public_key).trim(),
|
|
3483
|
+
status: status,
|
|
3484
|
+
created_at: createdAt,
|
|
3485
|
+
signature: String(record.signature).trim(),
|
|
3486
|
+
};
|
|
3487
|
+
}
|
|
3488
|
+
normalizePrivateMessageReceipts(items) {
|
|
3489
|
+
if (!Array.isArray(items)) {
|
|
3490
|
+
return [];
|
|
3491
|
+
}
|
|
3492
|
+
const deduped = new Set();
|
|
3493
|
+
return items
|
|
3494
|
+
.map((item) => this.normalizeIncomingPrivateMessageReceipt(item))
|
|
3495
|
+
.filter((item) => Boolean(item))
|
|
3496
|
+
.sort((a, b) => a.created_at - b.created_at)
|
|
3497
|
+
.filter((item) => {
|
|
3498
|
+
if (deduped.has(item.receipt_id)) {
|
|
3499
|
+
return false;
|
|
3500
|
+
}
|
|
3501
|
+
deduped.add(item.receipt_id);
|
|
3502
|
+
return true;
|
|
3503
|
+
})
|
|
3504
|
+
.slice(-PRIVATE_MESSAGE_RECEIPT_HISTORY_LIMIT);
|
|
3505
|
+
}
|
|
3506
|
+
hasPrivateMessage(messageId) {
|
|
3507
|
+
return this.privateMessages.some((item) => item.message_id === messageId);
|
|
3508
|
+
}
|
|
3509
|
+
ingestPrivateMessage(message) {
|
|
3510
|
+
const existing = this.privateMessages.findIndex((item) => item.message_id === message.message_id);
|
|
3511
|
+
if (existing >= 0) {
|
|
3512
|
+
this.privateMessages[existing] = message;
|
|
3513
|
+
}
|
|
3514
|
+
else {
|
|
3515
|
+
this.privateMessages.push(message);
|
|
3516
|
+
}
|
|
3517
|
+
this.privateMessages = this.normalizePrivateMessages(this.privateMessages);
|
|
3518
|
+
const validIds = new Set(this.privateMessages.map((item) => item.message_id));
|
|
3519
|
+
if (message.from_agent_id !== this.identity?.agent_id) {
|
|
3520
|
+
this.privateMessageBodyCache.delete(message.message_id);
|
|
3521
|
+
}
|
|
3522
|
+
for (const key of Array.from(this.privateMessageBodyCache.keys())) {
|
|
3523
|
+
if (!validIds.has(key)) {
|
|
3524
|
+
this.privateMessageBodyCache.delete(key);
|
|
3525
|
+
}
|
|
3526
|
+
}
|
|
3527
|
+
for (const key of Array.from(this.privateMessageDeliveryStatusCache.keys())) {
|
|
3528
|
+
if (!validIds.has(key)) {
|
|
3529
|
+
this.privateMessageDeliveryStatusCache.delete(key);
|
|
3530
|
+
}
|
|
3531
|
+
}
|
|
3532
|
+
}
|
|
3533
|
+
ingestPrivateMessageReceipt(receipt) {
|
|
3534
|
+
const existing = this.privateMessageReceipts.findIndex((item) => item.receipt_id === receipt.receipt_id);
|
|
3535
|
+
if (existing >= 0) {
|
|
3536
|
+
this.privateMessageReceipts[existing] = receipt;
|
|
3537
|
+
}
|
|
3538
|
+
else {
|
|
3539
|
+
this.privateMessageReceipts.push(receipt);
|
|
3540
|
+
}
|
|
3541
|
+
this.privateMessageReceipts = this.normalizePrivateMessageReceipts(this.privateMessageReceipts);
|
|
3542
|
+
this.privateMessageDeliveryStatusCache.set(receipt.message_id, receipt.status);
|
|
3543
|
+
}
|
|
2662
3544
|
normalizeIncomingSocialMessage(value) {
|
|
2663
3545
|
if (typeof value !== "object" || value === null) {
|
|
2664
3546
|
return null;
|
|
@@ -2905,6 +3787,36 @@ async function main() {
|
|
|
2905
3787
|
app.get("/api/runtime/paths", (_req, res) => {
|
|
2906
3788
|
sendOk(res, node.getRuntimePaths());
|
|
2907
3789
|
});
|
|
3790
|
+
app.get("/api/app/update-status", (_req, res) => {
|
|
3791
|
+
sendOk(res, node.getAppUpdateStatus());
|
|
3792
|
+
});
|
|
3793
|
+
app.post("/api/app/update", asyncRoute(async (_req, res) => {
|
|
3794
|
+
const status = node.getAppUpdateStatus();
|
|
3795
|
+
if (!status.update_available || !status.latest_version) {
|
|
3796
|
+
sendOk(res, {
|
|
3797
|
+
started: false,
|
|
3798
|
+
current_version: status.current_version,
|
|
3799
|
+
latest_version: status.latest_version,
|
|
3800
|
+
platform: status.platform,
|
|
3801
|
+
reason: status.check_error || "already_current",
|
|
3802
|
+
}, { message: "Already on the latest version" });
|
|
3803
|
+
return;
|
|
3804
|
+
}
|
|
3805
|
+
sendOk(res, {
|
|
3806
|
+
started: true,
|
|
3807
|
+
current_version: status.current_version,
|
|
3808
|
+
target_version: status.latest_version,
|
|
3809
|
+
platform: status.platform,
|
|
3810
|
+
}, { message: `Updating to ${status.latest_version}` });
|
|
3811
|
+
setTimeout(() => {
|
|
3812
|
+
try {
|
|
3813
|
+
node.startAppUpdate();
|
|
3814
|
+
}
|
|
3815
|
+
catch {
|
|
3816
|
+
// best effort after response has been sent
|
|
3817
|
+
}
|
|
3818
|
+
}, 1200);
|
|
3819
|
+
}));
|
|
2908
3820
|
app.put("/api/profile", asyncRoute(async (req, res) => {
|
|
2909
3821
|
const body = req.body;
|
|
2910
3822
|
const tags = Array.isArray(body.tags)
|
|
@@ -3003,6 +3915,31 @@ async function main() {
|
|
|
3003
3915
|
const agentId = String(req.query.agent_id ?? "").trim();
|
|
3004
3916
|
sendOk(res, node.getSocialMessages(limit, { agent_id: agentId || null }));
|
|
3005
3917
|
});
|
|
3918
|
+
app.get("/api/private/state", (_req, res) => {
|
|
3919
|
+
sendOk(res, node.getPrivateMessagingState());
|
|
3920
|
+
});
|
|
3921
|
+
app.get("/api/private/conversations", (_req, res) => {
|
|
3922
|
+
sendOk(res, node.getPrivateConversations());
|
|
3923
|
+
});
|
|
3924
|
+
app.get("/api/private/messages", (req, res) => {
|
|
3925
|
+
const conversationId = String(req.query.conversation_id ?? "").trim();
|
|
3926
|
+
const limit = Number(req.query.limit ?? PRIVATE_MESSAGE_QUERY_LIMIT);
|
|
3927
|
+
sendOk(res, node.getPrivateMessages(conversationId, limit));
|
|
3928
|
+
});
|
|
3929
|
+
app.post("/api/private/messages/send", asyncRoute(async (req, res) => {
|
|
3930
|
+
const result = await node.sendPrivateMessage({
|
|
3931
|
+
to_agent_id: String(req.body?.to_agent_id || ""),
|
|
3932
|
+
recipient_encryption_public_key: String(req.body?.recipient_encryption_public_key || ""),
|
|
3933
|
+
body: String(req.body?.body || ""),
|
|
3934
|
+
});
|
|
3935
|
+
sendOk(res, result, {
|
|
3936
|
+
message: result.sent
|
|
3937
|
+
? (result.reason === "direct-sent"
|
|
3938
|
+
? "Private message sent directly"
|
|
3939
|
+
: "Private message sent via encrypted fallback")
|
|
3940
|
+
: `Private message skipped: ${result.reason}`,
|
|
3941
|
+
});
|
|
3942
|
+
}));
|
|
3006
3943
|
app.get("/api/openclaw/bridge", (_req, res) => {
|
|
3007
3944
|
sendOk(res, node.getOpenClawBridgeStatus());
|
|
3008
3945
|
});
|