@silicaclaw/cli 2026.3.20-2 → 2026.3.20-20
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 +102 -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 +1018 -91
- 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 +472 -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 +430 -316
- package/apps/local-console/src/server.ts +1164 -89
- 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,6 +147,12 @@ 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()) {
|
|
137
157
|
if ((0, fs_1.existsSync)((0, path_1.resolve)(cwd, "apps", "local-console", "package.json"))) {
|
|
138
158
|
return cwd;
|
|
@@ -380,44 +400,56 @@ function readOpenClawConfiguredGateway(workspaceRoot) {
|
|
|
380
400
|
gateway_url: OPENCLAW_GATEWAY_URL,
|
|
381
401
|
};
|
|
382
402
|
}
|
|
403
|
+
function resolveOpenClawStatusCommand(workspaceRoot) {
|
|
404
|
+
const explicitBin = String(process.env.OPENCLAW_BIN || "").trim();
|
|
405
|
+
if (explicitBin) {
|
|
406
|
+
return { cmd: explicitBin, args: ["status"] };
|
|
407
|
+
}
|
|
408
|
+
const configuredSourceDir = String(process.env.OPENCLAW_SOURCE_DIR || "").trim();
|
|
409
|
+
const defaultSourceDir = defaultOpenClawSourceDir(workspaceRoot);
|
|
410
|
+
const sourceDir = configuredSourceDir || defaultSourceDir;
|
|
411
|
+
const sourceEntry = existingPathOrNull((0, path_1.resolve)(sourceDir, "openclaw.mjs"));
|
|
412
|
+
if (sourceEntry) {
|
|
413
|
+
return { cmd: process.execPath, args: [sourceEntry, "status"] };
|
|
414
|
+
}
|
|
415
|
+
const commandPath = resolveExecutableInPath("openclaw");
|
|
416
|
+
if (commandPath) {
|
|
417
|
+
return { cmd: commandPath, args: ["status"] };
|
|
418
|
+
}
|
|
419
|
+
return null;
|
|
420
|
+
}
|
|
421
|
+
function resolveOpenClawGatewayProbeCommand(workspaceRoot) {
|
|
422
|
+
const explicitBin = String(process.env.OPENCLAW_BIN || "").trim();
|
|
423
|
+
if (explicitBin) {
|
|
424
|
+
return { cmd: explicitBin, args: ["gateway", "probe"] };
|
|
425
|
+
}
|
|
426
|
+
const configuredSourceDir = String(process.env.OPENCLAW_SOURCE_DIR || "").trim();
|
|
427
|
+
const defaultSourceDir = defaultOpenClawSourceDir(workspaceRoot);
|
|
428
|
+
const sourceDir = configuredSourceDir || defaultSourceDir;
|
|
429
|
+
const sourceEntry = existingPathOrNull((0, path_1.resolve)(sourceDir, "openclaw.mjs"));
|
|
430
|
+
if (sourceEntry) {
|
|
431
|
+
return { cmd: process.execPath, args: [sourceEntry, "gateway", "probe"] };
|
|
432
|
+
}
|
|
433
|
+
const commandPath = resolveExecutableInPath("openclaw");
|
|
434
|
+
if (commandPath) {
|
|
435
|
+
return { cmd: commandPath, args: ["gateway", "probe"] };
|
|
436
|
+
}
|
|
437
|
+
return null;
|
|
438
|
+
}
|
|
383
439
|
function detectOpenClawRuntime(workspaceRoot) {
|
|
384
440
|
const configuredGateway = readOpenClawConfiguredGateway(workspaceRoot);
|
|
385
|
-
const
|
|
441
|
+
const statusCommand = resolveOpenClawStatusCommand(workspaceRoot);
|
|
442
|
+
const statusLooksConfigured = Boolean(statusCommand ||
|
|
443
|
+
configuredGateway.config_path ||
|
|
444
|
+
detectOpenClawInstallation(workspaceRoot).detected);
|
|
445
|
+
const gatewayProbeCommand = ["lsof", "-nP", `-iTCP:${configuredGateway.gateway_port}`, "-sTCP:LISTEN"];
|
|
446
|
+
const gatewayProbe = (0, child_process_1.spawnSync)(gatewayProbeCommand[0], gatewayProbeCommand.slice(1), {
|
|
386
447
|
encoding: "utf8",
|
|
448
|
+
timeout: 1200,
|
|
387
449
|
});
|
|
388
|
-
const
|
|
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"], {
|
|
418
|
-
encoding: "utf8",
|
|
419
|
-
});
|
|
420
|
-
const gatewayLines = String(gatewayProbe.stdout || "")
|
|
450
|
+
const gatewayStatusStdout = String(gatewayProbe.stdout || "");
|
|
451
|
+
const gatewayStatusStderr = String(gatewayProbe.stderr || "");
|
|
452
|
+
const gatewayLines = gatewayStatusStdout
|
|
421
453
|
.split("\n")
|
|
422
454
|
.map((line) => line.trim())
|
|
423
455
|
.filter(Boolean);
|
|
@@ -427,15 +459,10 @@ function detectOpenClawRuntime(workspaceRoot) {
|
|
|
427
459
|
const parts = line.split(/\s+/);
|
|
428
460
|
const pid = Number(parts[1] || 0);
|
|
429
461
|
const command = parts[0] || "";
|
|
430
|
-
const lowerCommand = command.toLowerCase();
|
|
431
462
|
const endpoint = parts[8] || parts[parts.length - 1] || "";
|
|
432
463
|
const portMatch = endpoint.match(/:(\d+)(?:\s*\(|$)/);
|
|
433
464
|
if (!pid || !command || !portMatch)
|
|
434
465
|
return null;
|
|
435
|
-
const isOpenClawListener = openclawPids.has(pid) ||
|
|
436
|
-
lowerCommand.includes("openclaw");
|
|
437
|
-
if (!isOpenClawListener)
|
|
438
|
-
return null;
|
|
439
466
|
const port = Number(portMatch[1]);
|
|
440
467
|
if (!Number.isFinite(port) || port <= 0)
|
|
441
468
|
return null;
|
|
@@ -447,49 +474,107 @@ function detectOpenClawRuntime(workspaceRoot) {
|
|
|
447
474
|
};
|
|
448
475
|
})
|
|
449
476
|
.filter((item) => Boolean(item));
|
|
477
|
+
const gatewayProbeOk = gatewayListeners.length > 0;
|
|
478
|
+
let processes = gatewayListeners.map((item) => ({
|
|
479
|
+
pid: item.pid,
|
|
480
|
+
ppid: item.ppid,
|
|
481
|
+
command: item.command,
|
|
482
|
+
}));
|
|
483
|
+
let processResult = null;
|
|
484
|
+
if (!gatewayProbeOk) {
|
|
485
|
+
processResult = (0, child_process_1.spawnSync)("ps", ["-Ao", "pid=,ppid=,command="], {
|
|
486
|
+
encoding: "utf8",
|
|
487
|
+
timeout: 1200,
|
|
488
|
+
});
|
|
489
|
+
const stdout = String(processResult.stdout || "");
|
|
490
|
+
const lines = stdout
|
|
491
|
+
.split("\n")
|
|
492
|
+
.map((line) => line.trim())
|
|
493
|
+
.filter(Boolean);
|
|
494
|
+
processes = lines
|
|
495
|
+
.map((line) => {
|
|
496
|
+
const match = line.match(/^(\d+)\s+(\d+)\s+(.+)$/);
|
|
497
|
+
if (!match)
|
|
498
|
+
return null;
|
|
499
|
+
const command = match[3] || "";
|
|
500
|
+
const lower = command.toLowerCase();
|
|
501
|
+
const isOpenClaw = lower.includes(" openclaw ") ||
|
|
502
|
+
lower.endsWith(" openclaw") ||
|
|
503
|
+
lower.includes("/openclaw ") ||
|
|
504
|
+
lower.includes("openclaw.mjs") ||
|
|
505
|
+
lower.includes("openclaw gateway") ||
|
|
506
|
+
lower.includes("openclaw agent") ||
|
|
507
|
+
lower.includes("openclaw message");
|
|
508
|
+
if (!isOpenClaw)
|
|
509
|
+
return null;
|
|
510
|
+
return {
|
|
511
|
+
pid: Number(match[1]),
|
|
512
|
+
ppid: Number(match[2]),
|
|
513
|
+
command,
|
|
514
|
+
};
|
|
515
|
+
})
|
|
516
|
+
.filter((item) => Boolean(item));
|
|
517
|
+
}
|
|
450
518
|
const preferredListener = gatewayListeners.find((item) => item.port === configuredGateway.gateway_port) ||
|
|
451
519
|
gatewayListeners[0] ||
|
|
452
520
|
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;
|
|
521
|
+
const allProcesses = processes.slice(0, 10);
|
|
522
|
+
const gatewayReachable = gatewayProbeOk;
|
|
466
523
|
const detectionNotes = [];
|
|
467
|
-
if (result.status !== 0)
|
|
468
|
-
detectionNotes.push(String(result.stderr || "ps failed").trim());
|
|
469
524
|
if (gatewayProbe.status !== 0 && gatewayLines.length === 0) {
|
|
470
|
-
detectionNotes.push(String(
|
|
525
|
+
detectionNotes.push(String(gatewayStatusStderr || "openclaw gateway probe failed").trim());
|
|
526
|
+
}
|
|
527
|
+
if (processResult && processResult.status !== 0) {
|
|
528
|
+
detectionNotes.push(String(processResult.stderr || "ps failed").trim());
|
|
471
529
|
}
|
|
472
530
|
const gatewayPort = preferredListener?.port || configuredGateway.gateway_port;
|
|
473
531
|
const gatewayUrl = `http://${OPENCLAW_GATEWAY_HOST}:${gatewayPort}/`;
|
|
474
532
|
return {
|
|
475
|
-
running: allProcesses.length > 0 || gatewayReachable,
|
|
533
|
+
running: gatewayProbeOk || allProcesses.length > 0 || gatewayReachable,
|
|
476
534
|
process_count: allProcesses.length,
|
|
477
535
|
processes: allProcesses.slice(0, 10),
|
|
478
536
|
detection_error: detectionNotes.filter(Boolean).join(" | ") || null,
|
|
479
537
|
gateway_url: gatewayUrl,
|
|
480
538
|
gateway_port: gatewayPort,
|
|
481
539
|
gateway_reachable: gatewayReachable,
|
|
540
|
+
status_command: statusCommand ? [statusCommand.cmd, ...statusCommand.args].join(" ") : null,
|
|
541
|
+
status_ok: statusLooksConfigured,
|
|
542
|
+
status_summary: statusLooksConfigured
|
|
543
|
+
? configuredGateway.config_path
|
|
544
|
+
? `configured via ${configuredGateway.config_path}`
|
|
545
|
+
: statusCommand
|
|
546
|
+
? `command available: ${[statusCommand.cmd, ...statusCommand.args].join(" ")}`
|
|
547
|
+
: "OpenClaw environment detected"
|
|
548
|
+
: null,
|
|
549
|
+
gateway_probe_command: gatewayProbeCommand.join(" "),
|
|
550
|
+
gateway_probe_ok: gatewayProbeOk,
|
|
551
|
+
gateway_probe_summary: gatewayProbeOk
|
|
552
|
+
? gatewayStatusStdout
|
|
553
|
+
.split("\n")
|
|
554
|
+
.map((line) => line.trim())
|
|
555
|
+
.filter(Boolean)
|
|
556
|
+
.slice(0, 4)
|
|
557
|
+
.join(" | ")
|
|
558
|
+
: null,
|
|
482
559
|
configured_gateway_url: configuredGateway.gateway_url,
|
|
483
560
|
configured_gateway_port: configuredGateway.gateway_port,
|
|
484
561
|
configured_gateway_bind: configuredGateway.gateway_bind,
|
|
485
562
|
configured_gateway_config_path: configuredGateway.config_path,
|
|
486
|
-
detection_mode:
|
|
487
|
-
?
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
563
|
+
detection_mode: gatewayProbeOk
|
|
564
|
+
? (processes.length > 0 && gatewayReachable
|
|
565
|
+
? "gateway-probe+process+gateway"
|
|
566
|
+
: gatewayReachable
|
|
567
|
+
? "gateway-probe+gateway"
|
|
568
|
+
: processes.length > 0
|
|
569
|
+
? "gateway-probe+process"
|
|
570
|
+
: "gateway-probe")
|
|
571
|
+
: processes.length > 0 && gatewayReachable
|
|
572
|
+
? "process+gateway"
|
|
573
|
+
: gatewayReachable
|
|
574
|
+
? "gateway"
|
|
575
|
+
: processes.length > 0
|
|
576
|
+
? "process"
|
|
577
|
+
: "not_running",
|
|
493
578
|
};
|
|
494
579
|
}
|
|
495
580
|
function detectOpenClawSkillInstallation() {
|
|
@@ -688,20 +773,43 @@ class LocalNodeService {
|
|
|
688
773
|
socialMessageGovernanceRepo;
|
|
689
774
|
socialMessageRepo;
|
|
690
775
|
socialMessageObservationRepo;
|
|
776
|
+
privateMessageRepo;
|
|
777
|
+
privateMessageReceiptRepo;
|
|
778
|
+
privateEncryptionKeyRepo;
|
|
779
|
+
privateMessagingRuntimeRepo;
|
|
691
780
|
socialRuntimeRepo;
|
|
692
781
|
identity = null;
|
|
693
782
|
profile = null;
|
|
694
783
|
directory = (0, core_1.createEmptyDirectoryState)();
|
|
695
784
|
socialMessages = [];
|
|
696
785
|
socialMessageObservations = [];
|
|
786
|
+
privateMessages = [];
|
|
787
|
+
privateMessageReceipts = [];
|
|
788
|
+
privateEncryptionKeyPair = null;
|
|
789
|
+
privateMessagingRuntime = null;
|
|
790
|
+
privatePeerRoutes = {};
|
|
791
|
+
privatePeerEncryptionKeys = {};
|
|
792
|
+
privateMessageBodyCache = new Map();
|
|
793
|
+
privateMessageDeliveryStatusCache = new Map();
|
|
697
794
|
messageGovernance;
|
|
795
|
+
privateMessagesPersistDirty = false;
|
|
796
|
+
privateMessageReceiptsPersistDirty = false;
|
|
797
|
+
privateMessagesPersistTimer = null;
|
|
798
|
+
privateMessageReceiptsPersistTimer = null;
|
|
698
799
|
receivedCount = 0;
|
|
699
800
|
broadcastCount = 0;
|
|
700
801
|
lastMessageAt = 0;
|
|
701
802
|
lastBroadcastAt = 0;
|
|
803
|
+
lastProfileBroadcastAt = 0;
|
|
804
|
+
lastProfileBroadcastSignature = "";
|
|
805
|
+
lastReplayBroadcastAt = 0;
|
|
806
|
+
lastReplayBroadcastSignature = "";
|
|
702
807
|
lastBroadcastErrorAt = 0;
|
|
703
808
|
lastBroadcastError = null;
|
|
704
809
|
broadcastFailureCount = 0;
|
|
810
|
+
consecutiveBroadcastFailures = 0;
|
|
811
|
+
lastBroadcastRecoveryAttemptAt = 0;
|
|
812
|
+
broadcastRecoveryInFlight = false;
|
|
705
813
|
broadcaster = null;
|
|
706
814
|
subscriptionsBound = false;
|
|
707
815
|
broadcastEnabled = true;
|
|
@@ -739,6 +847,8 @@ class LocalNodeService {
|
|
|
739
847
|
networkReconnectTimer = null;
|
|
740
848
|
networkReconnectDelayMs = 5_000;
|
|
741
849
|
appVersion = "unknown";
|
|
850
|
+
openclawRuntimeCache = null;
|
|
851
|
+
openclawBridgeStatusCache = null;
|
|
742
852
|
constructor(options) {
|
|
743
853
|
this.workspaceRoot = options?.workspaceRoot || resolveWorkspaceRoot();
|
|
744
854
|
this.projectRoot = options?.projectRoot || resolveProjectRoot(this.workspaceRoot);
|
|
@@ -752,6 +862,10 @@ class LocalNodeService {
|
|
|
752
862
|
this.socialMessageGovernanceRepo = new storage_1.SocialMessageGovernanceRepo(this.storageRoot);
|
|
753
863
|
this.socialMessageRepo = new storage_1.SocialMessageRepo(this.storageRoot);
|
|
754
864
|
this.socialMessageObservationRepo = new storage_1.SocialMessageObservationRepo(this.storageRoot);
|
|
865
|
+
this.privateMessageRepo = new storage_1.PrivateMessageRepo(this.storageRoot);
|
|
866
|
+
this.privateMessageReceiptRepo = new storage_1.PrivateMessageReceiptRepo(this.storageRoot);
|
|
867
|
+
this.privateEncryptionKeyRepo = new storage_1.PrivateEncryptionKeyRepo(this.storageRoot);
|
|
868
|
+
this.privateMessagingRuntimeRepo = new storage_1.PrivateMessagingRuntimeRepo(this.storageRoot);
|
|
755
869
|
this.socialRuntimeRepo = new storage_1.SocialRuntimeRepo(this.storageRoot);
|
|
756
870
|
this.messageGovernance = this.defaultMessageGovernance();
|
|
757
871
|
let loadedSocial = (0, core_1.loadSocialConfig)(this.projectRoot);
|
|
@@ -779,6 +893,22 @@ class LocalNodeService {
|
|
|
779
893
|
this.adapterMode = resolved.mode;
|
|
780
894
|
this.networkPort = resolved.port;
|
|
781
895
|
}
|
|
896
|
+
getCachedOpenClawRuntime() {
|
|
897
|
+
const now = Date.now();
|
|
898
|
+
if (this.openclawRuntimeCache && this.openclawRuntimeCache.expiresAt > now) {
|
|
899
|
+
return this.openclawRuntimeCache.value;
|
|
900
|
+
}
|
|
901
|
+
const value = detectOpenClawRuntime(this.projectRoot);
|
|
902
|
+
this.openclawRuntimeCache = {
|
|
903
|
+
value,
|
|
904
|
+
expiresAt: now + OPENCLAW_RUNTIME_CACHE_MS,
|
|
905
|
+
};
|
|
906
|
+
return value;
|
|
907
|
+
}
|
|
908
|
+
invalidateOpenClawCaches() {
|
|
909
|
+
this.openclawRuntimeCache = null;
|
|
910
|
+
this.openclawBridgeStatusCache = null;
|
|
911
|
+
}
|
|
782
912
|
async start() {
|
|
783
913
|
await this.hydrateFromDisk();
|
|
784
914
|
this.bindNetworkSubscriptions();
|
|
@@ -790,6 +920,7 @@ class LocalNodeService {
|
|
|
790
920
|
clearInterval(this.broadcaster);
|
|
791
921
|
this.broadcaster = null;
|
|
792
922
|
}
|
|
923
|
+
await this.flushPrivatePersistence();
|
|
793
924
|
if (this.networkStarted) {
|
|
794
925
|
await this.network.stop();
|
|
795
926
|
}
|
|
@@ -808,6 +939,9 @@ class LocalNodeService {
|
|
|
808
939
|
getOverview() {
|
|
809
940
|
const discovered = this.search("");
|
|
810
941
|
const onlineCount = discovered.filter((profile) => profile.online).length;
|
|
942
|
+
const openclawInstallation = detectOpenClawInstallation(this.projectRoot);
|
|
943
|
+
const openclawRuntime = this.getCachedOpenClawRuntime();
|
|
944
|
+
const openclawSkillInstallation = detectOpenClawSkillInstallation();
|
|
811
945
|
return {
|
|
812
946
|
app_version: this.appVersion,
|
|
813
947
|
agent_id: this.identity?.agent_id ?? "",
|
|
@@ -823,6 +957,15 @@ class LocalNodeService {
|
|
|
823
957
|
init_state: this.initState,
|
|
824
958
|
presence_ttl_ms: PRESENCE_TTL_MS,
|
|
825
959
|
onboarding: this.getOnboardingSummary(),
|
|
960
|
+
openclaw: {
|
|
961
|
+
detected: openclawInstallation.detected,
|
|
962
|
+
running: openclawRuntime.running,
|
|
963
|
+
detection_mode: openclawRuntime.detection_mode,
|
|
964
|
+
gateway_url: openclawRuntime.gateway_url,
|
|
965
|
+
gateway_probe_ok: openclawRuntime.gateway_probe_ok,
|
|
966
|
+
status_ok: openclawRuntime.status_ok,
|
|
967
|
+
skill_installed: openclawSkillInstallation.installed,
|
|
968
|
+
},
|
|
826
969
|
social: {
|
|
827
970
|
found: this.socialFound,
|
|
828
971
|
enabled: this.socialConfig.enabled,
|
|
@@ -965,6 +1108,7 @@ class LocalNodeService {
|
|
|
965
1108
|
const relayCapable = this.adapterMode === "webrtc-preview" || this.adapterMode === "relay-preview";
|
|
966
1109
|
const peers = diagnostics?.peers?.items ?? [];
|
|
967
1110
|
const online = peers.filter((peer) => peer.status === "online").length;
|
|
1111
|
+
const memory = process.memoryUsage();
|
|
968
1112
|
return {
|
|
969
1113
|
adapter: this.adapterMode,
|
|
970
1114
|
mode: this.networkMode,
|
|
@@ -988,6 +1132,23 @@ class LocalNodeService {
|
|
|
988
1132
|
adapter_stats: diagnostics?.stats ?? null,
|
|
989
1133
|
adapter_transport_stats: diagnostics?.transport_stats ?? null,
|
|
990
1134
|
adapter_discovery_stats: diagnostics?.discovery_stats ?? null,
|
|
1135
|
+
runtime_diagnostics: {
|
|
1136
|
+
memory_mib: {
|
|
1137
|
+
rss: formatBytesToMiB(memory.rss),
|
|
1138
|
+
heap_used: formatBytesToMiB(memory.heapUsed),
|
|
1139
|
+
heap_total: formatBytesToMiB(memory.heapTotal),
|
|
1140
|
+
external: formatBytesToMiB(memory.external),
|
|
1141
|
+
},
|
|
1142
|
+
directory: {
|
|
1143
|
+
profile_count: Object.keys(this.directory.profiles).length,
|
|
1144
|
+
presence_count: Object.keys(this.directory.presence).length,
|
|
1145
|
+
index_key_count: Object.keys(this.directory.index).length,
|
|
1146
|
+
},
|
|
1147
|
+
social: {
|
|
1148
|
+
message_count: this.socialMessages.length,
|
|
1149
|
+
observation_count: this.socialMessageObservations.length,
|
|
1150
|
+
},
|
|
1151
|
+
},
|
|
991
1152
|
adapter_diagnostics_summary: relayCapable || diagnostics
|
|
992
1153
|
? {
|
|
993
1154
|
started: this.networkStarted,
|
|
@@ -1099,6 +1260,91 @@ class LocalNodeService {
|
|
|
1099
1260
|
social_source_path: this.socialSourcePath,
|
|
1100
1261
|
};
|
|
1101
1262
|
}
|
|
1263
|
+
getAppUpdateStatus() {
|
|
1264
|
+
const currentVersion = normalizeVersionText(this.appVersion) || "unknown";
|
|
1265
|
+
const fallback = {
|
|
1266
|
+
current_version: currentVersion,
|
|
1267
|
+
latest_version: currentVersion,
|
|
1268
|
+
update_available: false,
|
|
1269
|
+
channel: "latest",
|
|
1270
|
+
platform: process.platform,
|
|
1271
|
+
checked_at: Date.now(),
|
|
1272
|
+
can_update: true,
|
|
1273
|
+
check_error: null,
|
|
1274
|
+
};
|
|
1275
|
+
try {
|
|
1276
|
+
const result = (0, child_process_1.spawnSync)("npm", ["view", "@silicaclaw/cli", "dist-tags", "--json"], {
|
|
1277
|
+
cwd: this.projectRoot,
|
|
1278
|
+
encoding: "utf8",
|
|
1279
|
+
env: {
|
|
1280
|
+
...process.env,
|
|
1281
|
+
SILICACLAW_WORKSPACE_DIR: this.projectRoot,
|
|
1282
|
+
SILICACLAW_APP_DIR: this.workspaceRoot,
|
|
1283
|
+
npm_config_cache: process.env.npm_config_cache || userNpmCacheDir(),
|
|
1284
|
+
},
|
|
1285
|
+
});
|
|
1286
|
+
if ((result.status ?? 1) !== 0) {
|
|
1287
|
+
return {
|
|
1288
|
+
...fallback,
|
|
1289
|
+
check_error: String(result.stderr || result.stdout || "npm view failed").trim() || "npm view failed",
|
|
1290
|
+
};
|
|
1291
|
+
}
|
|
1292
|
+
const tags = JSON.parse(String(result.stdout || "{}").trim() || "{}");
|
|
1293
|
+
const latestVersion = normalizeVersionText(tags.latest || currentVersion) || currentVersion;
|
|
1294
|
+
return {
|
|
1295
|
+
...fallback,
|
|
1296
|
+
latest_version: latestVersion,
|
|
1297
|
+
update_available: compareVersionTokens(latestVersion, currentVersion) > 0,
|
|
1298
|
+
};
|
|
1299
|
+
}
|
|
1300
|
+
catch (error) {
|
|
1301
|
+
return {
|
|
1302
|
+
...fallback,
|
|
1303
|
+
check_error: error instanceof Error ? error.message : String(error),
|
|
1304
|
+
};
|
|
1305
|
+
}
|
|
1306
|
+
}
|
|
1307
|
+
startAppUpdate() {
|
|
1308
|
+
const status = this.getAppUpdateStatus();
|
|
1309
|
+
if (!status.update_available || !status.latest_version) {
|
|
1310
|
+
return {
|
|
1311
|
+
started: false,
|
|
1312
|
+
target_version: status.latest_version || status.current_version,
|
|
1313
|
+
platform: process.platform,
|
|
1314
|
+
reason: status.check_error || "already_current",
|
|
1315
|
+
};
|
|
1316
|
+
}
|
|
1317
|
+
const shimPath = userShimPath();
|
|
1318
|
+
const scriptPath = (0, path_1.resolve)(this.workspaceRoot, "scripts", "silicaclaw-cli.mjs");
|
|
1319
|
+
const useShim = (0, fs_1.existsSync)(shimPath);
|
|
1320
|
+
if (!useShim && !(0, fs_1.existsSync)(scriptPath)) {
|
|
1321
|
+
return {
|
|
1322
|
+
started: false,
|
|
1323
|
+
target_version: status.latest_version,
|
|
1324
|
+
platform: process.platform,
|
|
1325
|
+
reason: "missing_cli_script",
|
|
1326
|
+
};
|
|
1327
|
+
}
|
|
1328
|
+
const command = useShim ? shimPath : process.execPath;
|
|
1329
|
+
const args = useShim ? ["update"] : [scriptPath, "update"];
|
|
1330
|
+
const child = (0, child_process_1.spawn)(command, args, {
|
|
1331
|
+
cwd: this.projectRoot,
|
|
1332
|
+
detached: true,
|
|
1333
|
+
stdio: "ignore",
|
|
1334
|
+
env: {
|
|
1335
|
+
...process.env,
|
|
1336
|
+
SILICACLAW_WORKSPACE_DIR: this.projectRoot,
|
|
1337
|
+
SILICACLAW_APP_DIR: this.workspaceRoot,
|
|
1338
|
+
npm_config_cache: process.env.npm_config_cache || userNpmCacheDir(),
|
|
1339
|
+
},
|
|
1340
|
+
});
|
|
1341
|
+
child.unref();
|
|
1342
|
+
return {
|
|
1343
|
+
started: true,
|
|
1344
|
+
target_version: status.latest_version,
|
|
1345
|
+
platform: process.platform,
|
|
1346
|
+
};
|
|
1347
|
+
}
|
|
1102
1348
|
getIntegrationSummary() {
|
|
1103
1349
|
const status = this.getIntegrationStatus();
|
|
1104
1350
|
const runtimeGenerated = Boolean(this.socialRuntime && this.socialRuntime.last_loaded_at > 0);
|
|
@@ -1355,6 +1601,7 @@ class LocalNodeService {
|
|
|
1355
1601
|
return {
|
|
1356
1602
|
...message,
|
|
1357
1603
|
display_name: profile?.display_name || message.display_name || "Unnamed",
|
|
1604
|
+
avatar_url: profile?.avatar_url || "",
|
|
1358
1605
|
is_self: message.agent_id === this.identity?.agent_id,
|
|
1359
1606
|
online: (0, core_1.isAgentOnline)(lastSeenAt, Date.now(), PRESENCE_TTL_MS),
|
|
1360
1607
|
last_seen_at: lastSeenAt || null,
|
|
@@ -1373,10 +1620,136 @@ class LocalNodeService {
|
|
|
1373
1620
|
},
|
|
1374
1621
|
};
|
|
1375
1622
|
}
|
|
1623
|
+
getPrivateMessagingState() {
|
|
1624
|
+
return {
|
|
1625
|
+
enabled: Boolean(this.identity && this.privateEncryptionKeyPair),
|
|
1626
|
+
agent_id: this.identity?.agent_id || "",
|
|
1627
|
+
encryption_public_key: this.privateEncryptionKeyPair?.public_key || "",
|
|
1628
|
+
conversation_count: this.getPrivateConversations().length,
|
|
1629
|
+
message_count: this.privateMessages.length,
|
|
1630
|
+
runtime: this.privateMessagingRuntime,
|
|
1631
|
+
};
|
|
1632
|
+
}
|
|
1633
|
+
getPrivateConversations() {
|
|
1634
|
+
const conversations = new Map();
|
|
1635
|
+
for (const message of this.privateMessages) {
|
|
1636
|
+
if (message.from_agent_id === message.to_agent_id) {
|
|
1637
|
+
continue;
|
|
1638
|
+
}
|
|
1639
|
+
const peerAgentId = message.from_agent_id === this.identity?.agent_id ? message.to_agent_id : message.from_agent_id;
|
|
1640
|
+
if (!peerAgentId || peerAgentId === this.identity?.agent_id) {
|
|
1641
|
+
continue;
|
|
1642
|
+
}
|
|
1643
|
+
const peerProfile = this.directory.profiles[peerAgentId];
|
|
1644
|
+
const current = conversations.get(message.conversation_id);
|
|
1645
|
+
const nextLast = Math.max(current?.last_message_at || 0, message.created_at || 0) || null;
|
|
1646
|
+
const learnedPeerKey = this.privatePeerEncryptionKeys[peerAgentId] || "";
|
|
1647
|
+
conversations.set(message.conversation_id, {
|
|
1648
|
+
conversation_id: message.conversation_id,
|
|
1649
|
+
peer_agent_id: peerAgentId,
|
|
1650
|
+
peer_display_name: peerProfile?.display_name || peerAgentId,
|
|
1651
|
+
peer_avatar_url: peerProfile?.avatar_url || "",
|
|
1652
|
+
peer_public_key: learnedPeerKey || peerProfile?.private_encryption_public_key || "",
|
|
1653
|
+
last_message_at: nextLast,
|
|
1654
|
+
unread_count: current?.unread_count || 0,
|
|
1655
|
+
});
|
|
1656
|
+
}
|
|
1657
|
+
return Array.from(conversations.values()).sort((a, b) => (b.last_message_at || 0) - (a.last_message_at || 0));
|
|
1658
|
+
}
|
|
1659
|
+
getPrivateMessages(conversationId, limit = PRIVATE_MESSAGE_QUERY_LIMIT) {
|
|
1660
|
+
const normalizedConversationId = String(conversationId || "").trim();
|
|
1661
|
+
const resolvedLimit = Math.max(1, Math.min(PRIVATE_MESSAGE_QUERY_LIMIT, Number(limit) || PRIVATE_MESSAGE_QUERY_LIMIT));
|
|
1662
|
+
const receiptsByMessageId = new Map(this.privateMessageReceipts.map((receipt) => [receipt.message_id, receipt.status]));
|
|
1663
|
+
return this.privateMessages
|
|
1664
|
+
.filter((message) => {
|
|
1665
|
+
if (message.from_agent_id === message.to_agent_id) {
|
|
1666
|
+
return false;
|
|
1667
|
+
}
|
|
1668
|
+
const peerAgentId = message.from_agent_id === this.identity?.agent_id ? message.to_agent_id : message.from_agent_id;
|
|
1669
|
+
if (!peerAgentId || peerAgentId === this.identity?.agent_id) {
|
|
1670
|
+
return false;
|
|
1671
|
+
}
|
|
1672
|
+
return !normalizedConversationId || message.conversation_id === normalizedConversationId;
|
|
1673
|
+
})
|
|
1674
|
+
.sort((a, b) => b.created_at - a.created_at)
|
|
1675
|
+
.slice(0, resolvedLimit)
|
|
1676
|
+
.map((message) => ({
|
|
1677
|
+
message_id: message.message_id,
|
|
1678
|
+
conversation_id: message.conversation_id,
|
|
1679
|
+
from_agent_id: message.from_agent_id,
|
|
1680
|
+
to_agent_id: message.to_agent_id,
|
|
1681
|
+
body: this.decryptPrivateMessageBody(message),
|
|
1682
|
+
created_at: message.created_at,
|
|
1683
|
+
is_self: message.from_agent_id === this.identity?.agent_id,
|
|
1684
|
+
delivery_status: receiptsByMessageId.get(message.message_id) ||
|
|
1685
|
+
this.privateMessageDeliveryStatusCache.get(message.message_id) ||
|
|
1686
|
+
(message.from_agent_id === this.identity?.agent_id ? "fallback-sent" : "sent"),
|
|
1687
|
+
}));
|
|
1688
|
+
}
|
|
1689
|
+
async sendPrivateMessage(input) {
|
|
1690
|
+
if (!this.identity || !this.privateEncryptionKeyPair) {
|
|
1691
|
+
return { sent: false, reason: "missing_identity_or_private_key" };
|
|
1692
|
+
}
|
|
1693
|
+
const toAgentId = String(input.to_agent_id || "").trim();
|
|
1694
|
+
const learnedRecipientKey = this.privatePeerEncryptionKeys[toAgentId] || "";
|
|
1695
|
+
const profileRecipientKey = this.directory.profiles[toAgentId]?.private_encryption_public_key || "";
|
|
1696
|
+
const recipientKey = String(learnedRecipientKey || input.recipient_encryption_public_key || profileRecipientKey || "").trim();
|
|
1697
|
+
const body = String(input.body || "").trim();
|
|
1698
|
+
if (toAgentId === this.identity.agent_id) {
|
|
1699
|
+
return { sent: false, reason: "self_private_message_not_allowed" };
|
|
1700
|
+
}
|
|
1701
|
+
const toPeerId = this.privatePeerRoutes[toAgentId] || "";
|
|
1702
|
+
if (!toAgentId || !recipientKey || !body) {
|
|
1703
|
+
return { sent: false, reason: "invalid_private_message_input" };
|
|
1704
|
+
}
|
|
1705
|
+
const encrypted = (0, core_1.encryptPrivatePayload)({
|
|
1706
|
+
plaintext: body,
|
|
1707
|
+
recipient_public_key: recipientKey,
|
|
1708
|
+
sender_keypair: this.privateEncryptionKeyPair,
|
|
1709
|
+
});
|
|
1710
|
+
const message = (0, core_1.signPrivateMessage)({
|
|
1711
|
+
identity: this.identity,
|
|
1712
|
+
message_id: (0, crypto_1.createHash)("sha256").update(`${this.identity.agent_id}:${toAgentId}:${Date.now()}:${body}:${Math.random()}`, "utf8").digest("hex"),
|
|
1713
|
+
conversation_id: this.buildPrivateConversationId(this.identity.agent_id, toAgentId),
|
|
1714
|
+
to_agent_id: toAgentId,
|
|
1715
|
+
sender_encryption_public_key: encrypted.sender_encryption_public_key,
|
|
1716
|
+
recipient_encryption_public_key: recipientKey,
|
|
1717
|
+
ciphertext: encrypted.ciphertext,
|
|
1718
|
+
nonce: encrypted.nonce,
|
|
1719
|
+
created_at: Date.now(),
|
|
1720
|
+
});
|
|
1721
|
+
this.privateMessageBodyCache.set(message.message_id, body);
|
|
1722
|
+
this.ingestPrivateMessage(message);
|
|
1723
|
+
await this.persistPrivateMessages();
|
|
1724
|
+
let reason = "fallback-sent";
|
|
1725
|
+
if (toPeerId && typeof this.network.sendDirect === "function") {
|
|
1726
|
+
try {
|
|
1727
|
+
await this.network.sendDirect(toPeerId, PRIVATE_MESSAGE_TOPIC, message);
|
|
1728
|
+
await this.publish(PRIVATE_MESSAGE_TOPIC, message);
|
|
1729
|
+
reason = "direct-sent";
|
|
1730
|
+
}
|
|
1731
|
+
catch {
|
|
1732
|
+
await this.publish(PRIVATE_MESSAGE_TOPIC, message);
|
|
1733
|
+
}
|
|
1734
|
+
}
|
|
1735
|
+
else {
|
|
1736
|
+
await this.publish(PRIVATE_MESSAGE_TOPIC, message);
|
|
1737
|
+
}
|
|
1738
|
+
this.privateMessageDeliveryStatusCache.set(message.message_id, reason);
|
|
1739
|
+
const view = this.getPrivateMessages(message.conversation_id).find((item) => item.message_id === message.message_id);
|
|
1740
|
+
if (view) {
|
|
1741
|
+
view.delivery_status = reason;
|
|
1742
|
+
}
|
|
1743
|
+
return { sent: true, reason, message: view };
|
|
1744
|
+
}
|
|
1376
1745
|
getOpenClawBridgeStatus() {
|
|
1746
|
+
const now = Date.now();
|
|
1747
|
+
if (this.openclawBridgeStatusCache && this.openclawBridgeStatusCache.expiresAt > now) {
|
|
1748
|
+
return this.openclawBridgeStatusCache.value;
|
|
1749
|
+
}
|
|
1377
1750
|
const integration = this.getIntegrationStatus();
|
|
1378
1751
|
const openclawInstallation = detectOpenClawInstallation(this.projectRoot);
|
|
1379
|
-
const openclawRuntime =
|
|
1752
|
+
const openclawRuntime = this.getCachedOpenClawRuntime();
|
|
1380
1753
|
const skillInstallation = detectOpenClawSkillInstallation();
|
|
1381
1754
|
const ownerDelivery = detectOwnerDeliveryStatus({
|
|
1382
1755
|
workspaceRoot: this.projectRoot,
|
|
@@ -1384,7 +1757,7 @@ class LocalNodeService {
|
|
|
1384
1757
|
openclawRunning: openclawRuntime.running,
|
|
1385
1758
|
skillInstalled: skillInstallation.installed,
|
|
1386
1759
|
});
|
|
1387
|
-
|
|
1760
|
+
const value = {
|
|
1388
1761
|
enabled: this.socialConfig.enabled,
|
|
1389
1762
|
connected_to_silicaclaw: integration.connected_to_silicaclaw,
|
|
1390
1763
|
public_enabled: integration.public_enabled,
|
|
@@ -1440,6 +1813,11 @@ class LocalNodeService {
|
|
|
1440
1813
|
install_skill: "/api/openclaw/bridge/skill-install",
|
|
1441
1814
|
},
|
|
1442
1815
|
};
|
|
1816
|
+
this.openclawBridgeStatusCache = {
|
|
1817
|
+
value,
|
|
1818
|
+
expiresAt: now + OPENCLAW_BRIDGE_STATUS_CACHE_MS,
|
|
1819
|
+
};
|
|
1820
|
+
return value;
|
|
1443
1821
|
}
|
|
1444
1822
|
async installOpenClawSkill(skillName) {
|
|
1445
1823
|
const scriptPath = (0, path_1.resolve)(this.workspaceRoot, "scripts", "install-openclaw-skill.mjs");
|
|
@@ -1453,6 +1831,7 @@ class LocalNodeService {
|
|
|
1453
1831
|
maxBuffer: 1024 * 1024,
|
|
1454
1832
|
});
|
|
1455
1833
|
const parsed = JSON.parse(String(stdout || "{}"));
|
|
1834
|
+
this.invalidateOpenClawCaches();
|
|
1456
1835
|
return {
|
|
1457
1836
|
...parsed,
|
|
1458
1837
|
bridge: this.getOpenClawBridgeStatus(),
|
|
@@ -1472,7 +1851,7 @@ class LocalNodeService {
|
|
|
1472
1851
|
const workspaceSkillDir = (0, path_1.resolve)(homeDir, "workspace", "skills");
|
|
1473
1852
|
const legacySkillDir = (0, path_1.resolve)(homeDir, "skills");
|
|
1474
1853
|
const openclawSourceDir = defaultOpenClawSourceDir(this.projectRoot);
|
|
1475
|
-
const openclawRuntime =
|
|
1854
|
+
const openclawRuntime = this.getCachedOpenClawRuntime();
|
|
1476
1855
|
return {
|
|
1477
1856
|
bridge_api_base: DEFAULT_BRIDGE_API_BASE,
|
|
1478
1857
|
openclaw_detected: detectOpenClawInstallation(this.projectRoot).detected,
|
|
@@ -1845,14 +2224,13 @@ class LocalNodeService {
|
|
|
1845
2224
|
profile: this.profile,
|
|
1846
2225
|
};
|
|
1847
2226
|
const presenceRecord = (0, core_1.signPresence)(this.identity, Date.now());
|
|
1848
|
-
const
|
|
1849
|
-
const replayMessages = this.getReplayableSelfSocialMessages();
|
|
2227
|
+
const shouldPublishProfile = this.shouldPublishProfileRecord(profileRecord, reason, presenceRecord.timestamp);
|
|
2228
|
+
const replayMessages = this.getReplayableSelfSocialMessages(reason);
|
|
1850
2229
|
try {
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
for (const record of indexRecords) {
|
|
1854
|
-
await this.publish("index", record);
|
|
2230
|
+
if (shouldPublishProfile) {
|
|
2231
|
+
await this.publish("profile", profileRecord);
|
|
1855
2232
|
}
|
|
2233
|
+
await this.publish("presence", presenceRecord);
|
|
1856
2234
|
for (const message of replayMessages) {
|
|
1857
2235
|
await this.publish(SOCIAL_MESSAGE_TOPIC, message);
|
|
1858
2236
|
}
|
|
@@ -1862,23 +2240,67 @@ class LocalNodeService {
|
|
|
1862
2240
|
this.lastBroadcastErrorAt = Date.now();
|
|
1863
2241
|
this.lastBroadcastError = message;
|
|
1864
2242
|
this.broadcastFailureCount += 1;
|
|
2243
|
+
this.consecutiveBroadcastFailures += 1;
|
|
1865
2244
|
await this.log("error", `Broadcast failed (reason=${reason}): ${message}`);
|
|
2245
|
+
await this.maybeRecoverFromBroadcastFailure(reason, message);
|
|
1866
2246
|
return { sent: false, reason: "publish_failed", error: message };
|
|
1867
2247
|
}
|
|
1868
2248
|
this.lastBroadcastAt = Date.now();
|
|
1869
2249
|
this.broadcastCount += 1;
|
|
1870
2250
|
this.lastBroadcastError = null;
|
|
1871
2251
|
this.lastBroadcastErrorAt = 0;
|
|
2252
|
+
this.consecutiveBroadcastFailures = 0;
|
|
1872
2253
|
this.directory = (0, core_1.ingestProfileRecord)(this.directory, profileRecord);
|
|
1873
2254
|
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
2255
|
this.compactCacheInMemory();
|
|
1878
2256
|
await this.persistCache();
|
|
1879
|
-
await this.log("info", `Broadcast sent (${
|
|
2257
|
+
await this.log("info", `Broadcast sent (${shouldPublishProfile ? "profile + " : ""}presence, replayed_messages=${replayMessages.length}, reason=${reason})`);
|
|
1880
2258
|
return { sent: true, reason };
|
|
1881
2259
|
}
|
|
2260
|
+
shouldPublishProfileRecord(profileRecord, reason, now = Date.now()) {
|
|
2261
|
+
if (reason !== "interval") {
|
|
2262
|
+
this.lastProfileBroadcastSignature = profileRecord.profile.signature;
|
|
2263
|
+
this.lastProfileBroadcastAt = now;
|
|
2264
|
+
return true;
|
|
2265
|
+
}
|
|
2266
|
+
const signature = profileRecord.profile.signature;
|
|
2267
|
+
const changedSinceLastPublish = signature !== this.lastProfileBroadcastSignature;
|
|
2268
|
+
const refreshDue = now - this.lastProfileBroadcastAt >= PROFILE_RELAY_REFRESH_INTERVAL_MS;
|
|
2269
|
+
if (!changedSinceLastPublish && !refreshDue) {
|
|
2270
|
+
return false;
|
|
2271
|
+
}
|
|
2272
|
+
this.lastProfileBroadcastSignature = signature;
|
|
2273
|
+
this.lastProfileBroadcastAt = now;
|
|
2274
|
+
return true;
|
|
2275
|
+
}
|
|
2276
|
+
async maybeRecoverFromBroadcastFailure(reason, errorMessage) {
|
|
2277
|
+
const recoveryThreshold = 3;
|
|
2278
|
+
const recoveryCooldownMs = 60_000;
|
|
2279
|
+
if (this.broadcastRecoveryInFlight) {
|
|
2280
|
+
return;
|
|
2281
|
+
}
|
|
2282
|
+
if (this.consecutiveBroadcastFailures < recoveryThreshold) {
|
|
2283
|
+
return;
|
|
2284
|
+
}
|
|
2285
|
+
if (Date.now() - this.lastBroadcastRecoveryAttemptAt < recoveryCooldownMs) {
|
|
2286
|
+
return;
|
|
2287
|
+
}
|
|
2288
|
+
if (this.adapterMode !== "relay-preview" && this.adapterMode !== "webrtc-preview" && this.adapterMode !== "real-preview") {
|
|
2289
|
+
return;
|
|
2290
|
+
}
|
|
2291
|
+
this.broadcastRecoveryInFlight = true;
|
|
2292
|
+
this.lastBroadcastRecoveryAttemptAt = Date.now();
|
|
2293
|
+
try {
|
|
2294
|
+
await this.log("warn", `Broadcast recovery triggered after ${this.consecutiveBroadcastFailures} consecutive failures (${reason}): ${errorMessage}`);
|
|
2295
|
+
await this.restartNetworkAdapter("broadcast_failure_recovery");
|
|
2296
|
+
}
|
|
2297
|
+
catch (recoveryError) {
|
|
2298
|
+
await this.log("error", `Broadcast recovery failed: ${recoveryError instanceof Error ? recoveryError.message : String(recoveryError)}`);
|
|
2299
|
+
}
|
|
2300
|
+
finally {
|
|
2301
|
+
this.broadcastRecoveryInFlight = false;
|
|
2302
|
+
}
|
|
2303
|
+
}
|
|
1882
2304
|
async hydrateFromDisk() {
|
|
1883
2305
|
this.initState = {
|
|
1884
2306
|
identity_auto_created: false,
|
|
@@ -1907,6 +2329,8 @@ class LocalNodeService {
|
|
|
1907
2329
|
await this.log("info", `Bound existing OpenClaw identity: ${resolvedIdentity.openclaw_source_path}`);
|
|
1908
2330
|
}
|
|
1909
2331
|
await this.identityRepo.set(this.identity);
|
|
2332
|
+
this.privateEncryptionKeyPair = (await this.privateEncryptionKeyRepo.get()) || (0, core_1.createPrivateEncryptionKeyPair)();
|
|
2333
|
+
await this.privateEncryptionKeyRepo.set(this.privateEncryptionKeyPair);
|
|
1910
2334
|
const existingProfile = await this.profileRepo.get();
|
|
1911
2335
|
const profileInput = (0, core_1.resolveProfileInputWithSocial)({
|
|
1912
2336
|
socialConfig: this.socialConfig,
|
|
@@ -1914,7 +2338,10 @@ class LocalNodeService {
|
|
|
1914
2338
|
existingProfile: existingProfile && existingProfile.agent_id === this.identity.agent_id ? existingProfile : null,
|
|
1915
2339
|
rootDir: this.projectRoot,
|
|
1916
2340
|
});
|
|
1917
|
-
this.profile = (0, core_1.signProfile)(
|
|
2341
|
+
this.profile = (0, core_1.signProfile)({
|
|
2342
|
+
...profileInput,
|
|
2343
|
+
private_encryption_public_key: this.privateEncryptionKeyPair?.public_key || profileInput.private_encryption_public_key || "",
|
|
2344
|
+
}, this.identity);
|
|
1918
2345
|
if (!existingProfile || existingProfile.agent_id !== this.identity.agent_id) {
|
|
1919
2346
|
this.initState.profile_auto_created = true;
|
|
1920
2347
|
await this.log("info", "profile.json missing/invalid, initialized from social/default profile");
|
|
@@ -1927,6 +2354,11 @@ class LocalNodeService {
|
|
|
1927
2354
|
};
|
|
1928
2355
|
this.socialMessages = this.normalizeSocialMessages(await this.socialMessageRepo.get());
|
|
1929
2356
|
this.socialMessageObservations = this.normalizeSocialMessageObservations(await this.socialMessageObservationRepo.get());
|
|
2357
|
+
const storedPrivateMessages = await this.privateMessageRepo.get();
|
|
2358
|
+
this.hydratePrivateMessageBodyCache(storedPrivateMessages);
|
|
2359
|
+
this.privateMessages = this.normalizePrivateMessages(storedPrivateMessages);
|
|
2360
|
+
this.privateMessageReceipts = this.normalizePrivateMessageReceipts(await this.privateMessageReceiptRepo.get());
|
|
2361
|
+
await this.refreshPrivateMessagingRuntime();
|
|
1930
2362
|
this.directory = (0, core_1.ingestProfileRecord)(this.directory, { type: "profile", profile: this.profile });
|
|
1931
2363
|
this.compactCacheInMemory();
|
|
1932
2364
|
await this.persistCache();
|
|
@@ -1943,7 +2375,10 @@ class LocalNodeService {
|
|
|
1943
2375
|
existingProfile: this.profile,
|
|
1944
2376
|
rootDir: this.projectRoot,
|
|
1945
2377
|
});
|
|
1946
|
-
const nextProfile = (0, core_1.signProfile)(
|
|
2378
|
+
const nextProfile = (0, core_1.signProfile)({
|
|
2379
|
+
...nextProfileInput,
|
|
2380
|
+
private_encryption_public_key: this.privateEncryptionKeyPair?.public_key || nextProfileInput.private_encryption_public_key || "",
|
|
2381
|
+
}, this.identity);
|
|
1947
2382
|
this.profile = nextProfile;
|
|
1948
2383
|
await this.profileRepo.set(nextProfile);
|
|
1949
2384
|
this.directory = (0, core_1.ingestProfileRecord)(this.directory, { type: "profile", profile: nextProfile });
|
|
@@ -2001,7 +2436,7 @@ class LocalNodeService {
|
|
|
2001
2436
|
this.socialRuntime = runtime;
|
|
2002
2437
|
await this.socialRuntimeRepo.set(runtime);
|
|
2003
2438
|
}
|
|
2004
|
-
async onMessage(topic, data) {
|
|
2439
|
+
async onMessage(topic, data, meta) {
|
|
2005
2440
|
this.receivedCount += 1;
|
|
2006
2441
|
this.receivedByTopic[topic] = (this.receivedByTopic[topic] ?? 0) + 1;
|
|
2007
2442
|
this.lastMessageAt = Date.now();
|
|
@@ -2016,6 +2451,9 @@ class LocalNodeService {
|
|
|
2016
2451
|
return;
|
|
2017
2452
|
}
|
|
2018
2453
|
}
|
|
2454
|
+
if (meta?.peerId && record.profile.agent_id && !this.privatePeerRoutes[record.profile.agent_id]) {
|
|
2455
|
+
this.privatePeerRoutes[record.profile.agent_id] = meta.peerId;
|
|
2456
|
+
}
|
|
2019
2457
|
this.directory = (0, core_1.ingestProfileRecord)(this.directory, record);
|
|
2020
2458
|
this.compactCacheInMemory();
|
|
2021
2459
|
await this.persistCache();
|
|
@@ -2032,6 +2470,9 @@ class LocalNodeService {
|
|
|
2032
2470
|
return;
|
|
2033
2471
|
}
|
|
2034
2472
|
}
|
|
2473
|
+
if (meta?.peerId && record.agent_id && !this.privatePeerRoutes[record.agent_id]) {
|
|
2474
|
+
this.privatePeerRoutes[record.agent_id] = meta.peerId;
|
|
2475
|
+
}
|
|
2035
2476
|
this.directory = (0, core_1.ingestPresenceRecord)(this.directory, record);
|
|
2036
2477
|
this.compactCacheInMemory();
|
|
2037
2478
|
await this.persistCache();
|
|
@@ -2046,6 +2487,9 @@ class LocalNodeService {
|
|
|
2046
2487
|
await this.log("warn", `Rejected social message with invalid signature (${record.message_id.slice(0, 10)})`);
|
|
2047
2488
|
return;
|
|
2048
2489
|
}
|
|
2490
|
+
if (meta?.peerId && record.agent_id && !this.privatePeerRoutes[record.agent_id]) {
|
|
2491
|
+
this.privatePeerRoutes[record.agent_id] = meta.peerId;
|
|
2492
|
+
}
|
|
2049
2493
|
if (this.hasSocialMessage(record.message_id)) {
|
|
2050
2494
|
await this.publishObservationForMessage(record);
|
|
2051
2495
|
return;
|
|
@@ -2081,6 +2525,39 @@ class LocalNodeService {
|
|
|
2081
2525
|
this.directory = (0, core_1.dedupeIndex)(this.directory);
|
|
2082
2526
|
await this.persistCache();
|
|
2083
2527
|
}
|
|
2528
|
+
async onDirectMessage(topic, data, meta) {
|
|
2529
|
+
if (topic === PRIVATE_MESSAGE_TOPIC) {
|
|
2530
|
+
const record = this.normalizeIncomingPrivateMessage(data);
|
|
2531
|
+
if (!record || !(0, core_1.verifyPrivateMessage)(record)) {
|
|
2532
|
+
return;
|
|
2533
|
+
}
|
|
2534
|
+
if (meta?.peerId && record.from_agent_id) {
|
|
2535
|
+
this.privatePeerRoutes[record.from_agent_id] = meta.peerId;
|
|
2536
|
+
}
|
|
2537
|
+
if (record.from_agent_id && record.sender_encryption_public_key) {
|
|
2538
|
+
this.privatePeerEncryptionKeys[record.from_agent_id] = record.sender_encryption_public_key;
|
|
2539
|
+
}
|
|
2540
|
+
if (record.to_agent_id !== this.identity?.agent_id || this.hasPrivateMessage(record.message_id)) {
|
|
2541
|
+
return;
|
|
2542
|
+
}
|
|
2543
|
+
this.ingestPrivateMessage(record);
|
|
2544
|
+
await this.persistPrivateMessages();
|
|
2545
|
+
await this.sendPrivateMessageReceipt(record, meta?.peerId);
|
|
2546
|
+
return;
|
|
2547
|
+
}
|
|
2548
|
+
const receipt = this.normalizeIncomingPrivateMessageReceipt(data);
|
|
2549
|
+
if (!receipt || !(0, core_1.verifyPrivateMessageReceipt)(receipt)) {
|
|
2550
|
+
return;
|
|
2551
|
+
}
|
|
2552
|
+
if (meta?.peerId && receipt.from_agent_id) {
|
|
2553
|
+
this.privatePeerRoutes[receipt.from_agent_id] = meta.peerId;
|
|
2554
|
+
}
|
|
2555
|
+
if (receipt.to_agent_id !== this.identity?.agent_id) {
|
|
2556
|
+
return;
|
|
2557
|
+
}
|
|
2558
|
+
this.ingestPrivateMessageReceipt(receipt);
|
|
2559
|
+
await this.persistPrivateMessageReceipts();
|
|
2560
|
+
}
|
|
2084
2561
|
startBroadcastLoop() {
|
|
2085
2562
|
if (this.broadcaster) {
|
|
2086
2563
|
clearInterval(this.broadcaster);
|
|
@@ -2101,21 +2578,35 @@ class LocalNodeService {
|
|
|
2101
2578
|
if (this.subscriptionsBound) {
|
|
2102
2579
|
return;
|
|
2103
2580
|
}
|
|
2104
|
-
this.network.subscribe("profile", (data) => {
|
|
2105
|
-
this.onMessage("profile", data);
|
|
2581
|
+
this.network.subscribe("profile", (data, meta) => {
|
|
2582
|
+
this.onMessage("profile", data, meta);
|
|
2106
2583
|
});
|
|
2107
|
-
this.network.subscribe("presence", (data) => {
|
|
2108
|
-
this.onMessage("presence", data);
|
|
2584
|
+
this.network.subscribe("presence", (data, meta) => {
|
|
2585
|
+
this.onMessage("presence", data, meta);
|
|
2109
2586
|
});
|
|
2110
|
-
this.network.subscribe("index", (data) => {
|
|
2111
|
-
this.onMessage("index", data);
|
|
2587
|
+
this.network.subscribe("index", (data, meta) => {
|
|
2588
|
+
this.onMessage("index", data, meta);
|
|
2112
2589
|
});
|
|
2113
|
-
this.network.subscribe(SOCIAL_MESSAGE_TOPIC, (data) => {
|
|
2114
|
-
this.onMessage(SOCIAL_MESSAGE_TOPIC, data);
|
|
2590
|
+
this.network.subscribe(SOCIAL_MESSAGE_TOPIC, (data, meta) => {
|
|
2591
|
+
this.onMessage(SOCIAL_MESSAGE_TOPIC, data, meta);
|
|
2115
2592
|
});
|
|
2116
|
-
this.network.subscribe(SOCIAL_MESSAGE_OBSERVATION_TOPIC, (data) => {
|
|
2117
|
-
this.onMessage(SOCIAL_MESSAGE_OBSERVATION_TOPIC, data);
|
|
2593
|
+
this.network.subscribe(SOCIAL_MESSAGE_OBSERVATION_TOPIC, (data, meta) => {
|
|
2594
|
+
this.onMessage(SOCIAL_MESSAGE_OBSERVATION_TOPIC, data, meta);
|
|
2118
2595
|
});
|
|
2596
|
+
this.network.subscribe(PRIVATE_MESSAGE_TOPIC, (data, meta) => {
|
|
2597
|
+
this.onDirectMessage(PRIVATE_MESSAGE_TOPIC, data, meta);
|
|
2598
|
+
});
|
|
2599
|
+
this.network.subscribe(PRIVATE_MESSAGE_RECEIPT_TOPIC, (data, meta) => {
|
|
2600
|
+
this.onDirectMessage(PRIVATE_MESSAGE_RECEIPT_TOPIC, data, meta);
|
|
2601
|
+
});
|
|
2602
|
+
if (typeof this.network.subscribeDirect === "function") {
|
|
2603
|
+
this.network.subscribeDirect(PRIVATE_MESSAGE_TOPIC, (data, meta) => {
|
|
2604
|
+
this.onDirectMessage(PRIVATE_MESSAGE_TOPIC, data, meta);
|
|
2605
|
+
});
|
|
2606
|
+
this.network.subscribeDirect(PRIVATE_MESSAGE_RECEIPT_TOPIC, (data, meta) => {
|
|
2607
|
+
this.onDirectMessage(PRIVATE_MESSAGE_RECEIPT_TOPIC, data, meta);
|
|
2608
|
+
});
|
|
2609
|
+
}
|
|
2119
2610
|
this.subscriptionsBound = true;
|
|
2120
2611
|
}
|
|
2121
2612
|
buildNetworkAdapter() {
|
|
@@ -2255,9 +2746,58 @@ class LocalNodeService {
|
|
|
2255
2746
|
}, delayMs);
|
|
2256
2747
|
this.networkReconnectDelayMs = Math.min(30_000, Math.max(5_000, Math.floor(delayMs * 1.5)));
|
|
2257
2748
|
}
|
|
2749
|
+
pruneRemoteProfilesInMemory(now = Date.now()) {
|
|
2750
|
+
if (!Number.isFinite(DIRECTORY_REMOTE_PROFILE_SOFT_LIMIT) || DIRECTORY_REMOTE_PROFILE_SOFT_LIMIT <= 0) {
|
|
2751
|
+
return 0;
|
|
2752
|
+
}
|
|
2753
|
+
const selfAgentId = this.profile?.agent_id || this.identity?.agent_id || "";
|
|
2754
|
+
const remoteProfiles = Object.values(this.directory.profiles).filter((profile) => profile.agent_id !== selfAgentId);
|
|
2755
|
+
if (remoteProfiles.length <= DIRECTORY_REMOTE_PROFILE_SOFT_LIMIT) {
|
|
2756
|
+
return 0;
|
|
2757
|
+
}
|
|
2758
|
+
const onlineRemoteProfiles = remoteProfiles.filter((profile) => (0, core_1.isAgentOnline)(this.directory.presence[profile.agent_id], now, PRESENCE_TTL_MS));
|
|
2759
|
+
const offlineRemoteProfiles = remoteProfiles
|
|
2760
|
+
.filter((profile) => !(0, core_1.isAgentOnline)(this.directory.presence[profile.agent_id], now, PRESENCE_TTL_MS))
|
|
2761
|
+
.sort((a, b) => (b.updated_at || 0) - (a.updated_at || 0));
|
|
2762
|
+
const keepOfflineCount = Math.max(0, DIRECTORY_REMOTE_PROFILE_SOFT_LIMIT - onlineRemoteProfiles.length);
|
|
2763
|
+
const keptRemoteProfiles = [
|
|
2764
|
+
...onlineRemoteProfiles,
|
|
2765
|
+
...offlineRemoteProfiles.slice(0, keepOfflineCount),
|
|
2766
|
+
];
|
|
2767
|
+
const keptRemoteIds = new Set(keptRemoteProfiles.map((profile) => profile.agent_id));
|
|
2768
|
+
const removedIds = remoteProfiles
|
|
2769
|
+
.map((profile) => profile.agent_id)
|
|
2770
|
+
.filter((agentId) => !keptRemoteIds.has(agentId));
|
|
2771
|
+
if (removedIds.length === 0) {
|
|
2772
|
+
return 0;
|
|
2773
|
+
}
|
|
2774
|
+
const next = (0, core_1.createEmptyDirectoryState)();
|
|
2775
|
+
const selfProfile = selfAgentId ? this.directory.profiles[selfAgentId] : null;
|
|
2776
|
+
if (selfProfile) {
|
|
2777
|
+
next.profiles[selfAgentId] = selfProfile;
|
|
2778
|
+
const selfPresence = this.directory.presence[selfAgentId];
|
|
2779
|
+
if (typeof selfPresence === "number" && Number.isFinite(selfPresence)) {
|
|
2780
|
+
next.presence[selfAgentId] = selfPresence;
|
|
2781
|
+
}
|
|
2782
|
+
const rebuilt = (0, core_1.rebuildIndexForProfile)(next, selfProfile);
|
|
2783
|
+
next.index = rebuilt.index;
|
|
2784
|
+
}
|
|
2785
|
+
for (const profile of keptRemoteProfiles) {
|
|
2786
|
+
next.profiles[profile.agent_id] = profile;
|
|
2787
|
+
const seenAt = this.directory.presence[profile.agent_id];
|
|
2788
|
+
if (typeof seenAt === "number" && Number.isFinite(seenAt)) {
|
|
2789
|
+
next.presence[profile.agent_id] = seenAt;
|
|
2790
|
+
}
|
|
2791
|
+
const rebuilt = (0, core_1.rebuildIndexForProfile)(next, profile);
|
|
2792
|
+
next.index = rebuilt.index;
|
|
2793
|
+
}
|
|
2794
|
+
this.directory = (0, core_1.dedupeIndex)(next);
|
|
2795
|
+
return removedIds.length;
|
|
2796
|
+
}
|
|
2258
2797
|
compactCacheInMemory() {
|
|
2259
2798
|
const cleaned = (0, core_1.cleanupExpiredPresence)(this.directory, Date.now(), PRESENCE_TTL_MS);
|
|
2260
2799
|
this.directory = (0, core_1.dedupeIndex)(cleaned.state);
|
|
2800
|
+
this.pruneRemoteProfilesInMemory();
|
|
2261
2801
|
return cleaned.removed;
|
|
2262
2802
|
}
|
|
2263
2803
|
async publish(topic, data) {
|
|
@@ -2288,6 +2828,82 @@ class LocalNodeService {
|
|
|
2288
2828
|
async persistSocialMessageObservations() {
|
|
2289
2829
|
await this.socialMessageObservationRepo.set(this.socialMessageObservations);
|
|
2290
2830
|
}
|
|
2831
|
+
async persistPrivateMessages() {
|
|
2832
|
+
this.privateMessagesPersistDirty = true;
|
|
2833
|
+
if (this.privateMessagesPersistTimer) {
|
|
2834
|
+
return;
|
|
2835
|
+
}
|
|
2836
|
+
this.privateMessagesPersistTimer = setTimeout(() => {
|
|
2837
|
+
this.flushPrivateMessagesPersist().catch(() => { });
|
|
2838
|
+
}, PRIVATE_MESSAGE_PERSIST_DEBOUNCE_MS);
|
|
2839
|
+
}
|
|
2840
|
+
async persistPrivateMessageReceipts() {
|
|
2841
|
+
this.privateMessageReceiptsPersistDirty = true;
|
|
2842
|
+
if (this.privateMessageReceiptsPersistTimer) {
|
|
2843
|
+
return;
|
|
2844
|
+
}
|
|
2845
|
+
this.privateMessageReceiptsPersistTimer = setTimeout(() => {
|
|
2846
|
+
this.flushPrivateMessageReceiptsPersist().catch(() => { });
|
|
2847
|
+
}, PRIVATE_MESSAGE_PERSIST_DEBOUNCE_MS);
|
|
2848
|
+
}
|
|
2849
|
+
async flushPrivatePersistence() {
|
|
2850
|
+
await Promise.all([
|
|
2851
|
+
this.flushPrivateMessagesPersist(),
|
|
2852
|
+
this.flushPrivateMessageReceiptsPersist(),
|
|
2853
|
+
]);
|
|
2854
|
+
}
|
|
2855
|
+
async flushPrivateMessagesPersist() {
|
|
2856
|
+
if (this.privateMessagesPersistTimer) {
|
|
2857
|
+
clearTimeout(this.privateMessagesPersistTimer);
|
|
2858
|
+
this.privateMessagesPersistTimer = null;
|
|
2859
|
+
}
|
|
2860
|
+
if (!this.privateMessagesPersistDirty) {
|
|
2861
|
+
return;
|
|
2862
|
+
}
|
|
2863
|
+
this.privateMessagesPersistDirty = false;
|
|
2864
|
+
await this.privateMessageRepo.set(this.buildPersistedPrivateMessages());
|
|
2865
|
+
}
|
|
2866
|
+
hydratePrivateMessageBodyCache(items) {
|
|
2867
|
+
if (!Array.isArray(items)) {
|
|
2868
|
+
return;
|
|
2869
|
+
}
|
|
2870
|
+
for (const item of items) {
|
|
2871
|
+
if (typeof item !== "object" || item === null) {
|
|
2872
|
+
continue;
|
|
2873
|
+
}
|
|
2874
|
+
const record = item;
|
|
2875
|
+
const messageId = String(record.message_id || "").trim();
|
|
2876
|
+
const localPlaintext = typeof record.local_plaintext === "string" ? record.local_plaintext : "";
|
|
2877
|
+
if (messageId && localPlaintext) {
|
|
2878
|
+
this.privateMessageBodyCache.set(messageId, localPlaintext);
|
|
2879
|
+
}
|
|
2880
|
+
}
|
|
2881
|
+
}
|
|
2882
|
+
buildPersistedPrivateMessages() {
|
|
2883
|
+
return this.privateMessages.map((message) => {
|
|
2884
|
+
const localPlaintext = message.from_agent_id === this.identity?.agent_id
|
|
2885
|
+
? this.privateMessageBodyCache.get(message.message_id) || ""
|
|
2886
|
+
: "";
|
|
2887
|
+
if (!localPlaintext) {
|
|
2888
|
+
return { ...message };
|
|
2889
|
+
}
|
|
2890
|
+
return {
|
|
2891
|
+
...message,
|
|
2892
|
+
local_plaintext: localPlaintext,
|
|
2893
|
+
};
|
|
2894
|
+
});
|
|
2895
|
+
}
|
|
2896
|
+
async flushPrivateMessageReceiptsPersist() {
|
|
2897
|
+
if (this.privateMessageReceiptsPersistTimer) {
|
|
2898
|
+
clearTimeout(this.privateMessageReceiptsPersistTimer);
|
|
2899
|
+
this.privateMessageReceiptsPersistTimer = null;
|
|
2900
|
+
}
|
|
2901
|
+
if (!this.privateMessageReceiptsPersistDirty) {
|
|
2902
|
+
return;
|
|
2903
|
+
}
|
|
2904
|
+
this.privateMessageReceiptsPersistDirty = false;
|
|
2905
|
+
await this.privateMessageReceiptRepo.set(this.privateMessageReceipts);
|
|
2906
|
+
}
|
|
2291
2907
|
async log(level, message) {
|
|
2292
2908
|
await this.logRepo.append({
|
|
2293
2909
|
level,
|
|
@@ -2334,6 +2950,7 @@ class LocalNodeService {
|
|
|
2334
2950
|
(0, core_1.verifyProfile)(profile, selfPublicKey));
|
|
2335
2951
|
return (0, core_1.buildPublicProfileSummary)({
|
|
2336
2952
|
profile,
|
|
2953
|
+
is_self: isSelf,
|
|
2337
2954
|
online,
|
|
2338
2955
|
last_seen_at: lastSeenAt || null,
|
|
2339
2956
|
network_mode: isSelf ? this.networkMode : "unknown",
|
|
@@ -2379,6 +2996,7 @@ class LocalNodeService {
|
|
|
2379
2996
|
updated_at: message.created_at,
|
|
2380
2997
|
signature: "",
|
|
2381
2998
|
},
|
|
2999
|
+
is_self: message.agent_id === this.identity?.agent_id,
|
|
2382
3000
|
online: false,
|
|
2383
3001
|
last_seen_at: null,
|
|
2384
3002
|
network_mode: "unknown",
|
|
@@ -2408,6 +3026,45 @@ class LocalNodeService {
|
|
|
2408
3026
|
const digest = (0, crypto_1.createHash)("sha256").update(publicKey, "utf8").digest("hex");
|
|
2409
3027
|
return `${digest.slice(0, 12)}:${digest.slice(-8)}`;
|
|
2410
3028
|
}
|
|
3029
|
+
buildPrivateMessagingRuntimeState() {
|
|
3030
|
+
const warnings = [];
|
|
3031
|
+
const keypair = this.privateEncryptionKeyPair;
|
|
3032
|
+
const selfSentMessages = this.privateMessages.filter((message) => message.from_agent_id === this.identity?.agent_id);
|
|
3033
|
+
let cachedPlaintextCount = 0;
|
|
3034
|
+
for (const message of selfSentMessages) {
|
|
3035
|
+
if (this.privateMessageBodyCache.get(message.message_id)) {
|
|
3036
|
+
cachedPlaintextCount += 1;
|
|
3037
|
+
}
|
|
3038
|
+
}
|
|
3039
|
+
if (!keypair?.public_key || !keypair?.private_key) {
|
|
3040
|
+
warnings.push("missing_private_encryption_keypair");
|
|
3041
|
+
}
|
|
3042
|
+
if (selfSentMessages.length > 0 && cachedPlaintextCount === 0) {
|
|
3043
|
+
warnings.push("missing_local_plaintext_cache_for_self_messages");
|
|
3044
|
+
}
|
|
3045
|
+
if (selfSentMessages.length > 0 && cachedPlaintextCount < selfSentMessages.length) {
|
|
3046
|
+
warnings.push("partial_local_plaintext_cache_for_self_messages");
|
|
3047
|
+
}
|
|
3048
|
+
return {
|
|
3049
|
+
schema_version: 1,
|
|
3050
|
+
app_version: this.appVersion,
|
|
3051
|
+
last_started_at: Date.now(),
|
|
3052
|
+
encryption_public_key: keypair?.public_key || "",
|
|
3053
|
+
encryption_public_key_fingerprint: keypair?.public_key ? this.fingerprintPublicKey(keypair.public_key) : "",
|
|
3054
|
+
message_count: this.privateMessages.length,
|
|
3055
|
+
self_sent_count: selfSentMessages.length,
|
|
3056
|
+
cached_plaintext_count: cachedPlaintextCount,
|
|
3057
|
+
warnings,
|
|
3058
|
+
};
|
|
3059
|
+
}
|
|
3060
|
+
async refreshPrivateMessagingRuntime() {
|
|
3061
|
+
const runtime = this.buildPrivateMessagingRuntimeState();
|
|
3062
|
+
this.privateMessagingRuntime = runtime;
|
|
3063
|
+
await this.privateMessagingRuntimeRepo.set(runtime);
|
|
3064
|
+
for (const warning of runtime.warnings) {
|
|
3065
|
+
await this.log("warn", `Private messaging startup check: ${warning}`);
|
|
3066
|
+
}
|
|
3067
|
+
}
|
|
2411
3068
|
getOnboardingSummary() {
|
|
2412
3069
|
const summary = this.getIntegrationSummary();
|
|
2413
3070
|
const publicEnabled = Boolean(this.profile?.public_enabled);
|
|
@@ -2566,6 +3223,32 @@ class LocalNodeService {
|
|
|
2566
3223
|
.join("\n")
|
|
2567
3224
|
.trim();
|
|
2568
3225
|
}
|
|
3226
|
+
buildPrivateConversationId(leftAgentId, rightAgentId) {
|
|
3227
|
+
return [String(leftAgentId || "").trim(), String(rightAgentId || "").trim()].sort().join(":");
|
|
3228
|
+
}
|
|
3229
|
+
decryptPrivateMessageBody(message) {
|
|
3230
|
+
const cached = this.privateMessageBodyCache.get(message.message_id);
|
|
3231
|
+
if (typeof cached === "string") {
|
|
3232
|
+
return cached;
|
|
3233
|
+
}
|
|
3234
|
+
if (!this.privateEncryptionKeyPair) {
|
|
3235
|
+
return "[encrypted]";
|
|
3236
|
+
}
|
|
3237
|
+
const decrypted = (0, core_1.decryptPrivatePayload)({
|
|
3238
|
+
ciphertext: message.ciphertext,
|
|
3239
|
+
nonce: message.nonce,
|
|
3240
|
+
sender_encryption_public_key: message.sender_encryption_public_key,
|
|
3241
|
+
recipient_private_key: this.privateEncryptionKeyPair.private_key,
|
|
3242
|
+
}) || "[encrypted]";
|
|
3243
|
+
this.privateMessageBodyCache.set(message.message_id, decrypted);
|
|
3244
|
+
if (this.privateMessageBodyCache.size > PRIVATE_MESSAGE_HISTORY_LIMIT * 2) {
|
|
3245
|
+
const firstKey = this.privateMessageBodyCache.keys().next().value;
|
|
3246
|
+
if (firstKey) {
|
|
3247
|
+
this.privateMessageBodyCache.delete(firstKey);
|
|
3248
|
+
}
|
|
3249
|
+
}
|
|
3250
|
+
return decrypted;
|
|
3251
|
+
}
|
|
2569
3252
|
normalizeWindowTimestamps(timestamps, windowMs, now = Date.now()) {
|
|
2570
3253
|
return timestamps.filter((timestamp) => now - timestamp <= windowMs);
|
|
2571
3254
|
}
|
|
@@ -2586,16 +3269,30 @@ class LocalNodeService {
|
|
|
2586
3269
|
hasSocialMessage(messageId) {
|
|
2587
3270
|
return this.socialMessages.some((item) => item.message_id === messageId);
|
|
2588
3271
|
}
|
|
2589
|
-
getReplayableSelfSocialMessages(now = Date.now()) {
|
|
3272
|
+
getReplayableSelfSocialMessages(reason = "manual", now = Date.now()) {
|
|
2590
3273
|
const maxCount = Math.max(0, SOCIAL_MESSAGE_REPLAY_MAX_PER_BROADCAST);
|
|
2591
3274
|
if (!this.identity || maxCount === 0) {
|
|
2592
3275
|
return [];
|
|
2593
3276
|
}
|
|
2594
|
-
|
|
3277
|
+
const replayable = this.socialMessages
|
|
2595
3278
|
.filter((item) => (item.agent_id === this.identity?.agent_id &&
|
|
2596
3279
|
now - item.created_at <= SOCIAL_MESSAGE_REPLAY_WINDOW_MS))
|
|
2597
3280
|
.sort((a, b) => a.created_at - b.created_at)
|
|
2598
3281
|
.slice(-maxCount);
|
|
3282
|
+
if (!replayable.length) {
|
|
3283
|
+
this.lastReplayBroadcastSignature = "";
|
|
3284
|
+
return [];
|
|
3285
|
+
}
|
|
3286
|
+
const signature = replayable.map((item) => item.message_id).join(",");
|
|
3287
|
+
const isIntervalReplay = reason === "interval";
|
|
3288
|
+
const changedSinceLastReplay = signature !== this.lastReplayBroadcastSignature;
|
|
3289
|
+
const refreshDue = now - this.lastReplayBroadcastAt >= SOCIAL_MESSAGE_REPLAY_REFRESH_INTERVAL_MS;
|
|
3290
|
+
if (isIntervalReplay && !changedSinceLastReplay && !refreshDue) {
|
|
3291
|
+
return [];
|
|
3292
|
+
}
|
|
3293
|
+
this.lastReplayBroadcastSignature = signature;
|
|
3294
|
+
this.lastReplayBroadcastAt = now;
|
|
3295
|
+
return replayable;
|
|
2599
3296
|
}
|
|
2600
3297
|
hasRecentDuplicateMessage(agentId, body, topic, now = Date.now()) {
|
|
2601
3298
|
return this.socialMessages.some((item) => (item.agent_id === agentId &&
|
|
@@ -2659,6 +3356,181 @@ class LocalNodeService {
|
|
|
2659
3356
|
await this.publish(SOCIAL_MESSAGE_OBSERVATION_TOPIC, observation);
|
|
2660
3357
|
await this.persistSocialMessageObservations();
|
|
2661
3358
|
}
|
|
3359
|
+
async sendPrivateMessageReceipt(message, replyPeerId) {
|
|
3360
|
+
if (!this.identity || typeof this.network.sendDirect !== "function" || !replyPeerId) {
|
|
3361
|
+
return;
|
|
3362
|
+
}
|
|
3363
|
+
const receipt = (0, core_1.signPrivateMessageReceipt)({
|
|
3364
|
+
identity: this.identity,
|
|
3365
|
+
receipt_id: (0, crypto_1.createHash)("sha256").update(`${message.message_id}:${this.identity.agent_id}:${Date.now()}`, "utf8").digest("hex"),
|
|
3366
|
+
message_id: message.message_id,
|
|
3367
|
+
conversation_id: message.conversation_id,
|
|
3368
|
+
to_agent_id: message.from_agent_id,
|
|
3369
|
+
status: "received",
|
|
3370
|
+
created_at: Date.now(),
|
|
3371
|
+
});
|
|
3372
|
+
this.ingestPrivateMessageReceipt(receipt);
|
|
3373
|
+
try {
|
|
3374
|
+
await this.network.sendDirect(replyPeerId, PRIVATE_MESSAGE_RECEIPT_TOPIC, receipt);
|
|
3375
|
+
await this.publish(PRIVATE_MESSAGE_RECEIPT_TOPIC, receipt);
|
|
3376
|
+
}
|
|
3377
|
+
catch {
|
|
3378
|
+
await this.publish(PRIVATE_MESSAGE_RECEIPT_TOPIC, receipt);
|
|
3379
|
+
}
|
|
3380
|
+
await this.persistPrivateMessageReceipts();
|
|
3381
|
+
}
|
|
3382
|
+
normalizeIncomingPrivateMessage(value) {
|
|
3383
|
+
if (typeof value !== "object" || value === null) {
|
|
3384
|
+
return null;
|
|
3385
|
+
}
|
|
3386
|
+
const record = value;
|
|
3387
|
+
const createdAt = Number(record.created_at || 0);
|
|
3388
|
+
const fromAgentId = String(record.from_agent_id || "").trim();
|
|
3389
|
+
const toAgentId = String(record.to_agent_id || "").trim();
|
|
3390
|
+
const conversationId = String(record.conversation_id || "").trim();
|
|
3391
|
+
if (record.type !== PRIVATE_MESSAGE_TOPIC ||
|
|
3392
|
+
!String(record.message_id || "").trim() ||
|
|
3393
|
+
!conversationId ||
|
|
3394
|
+
!fromAgentId ||
|
|
3395
|
+
!toAgentId ||
|
|
3396
|
+
!String(record.sender_public_key || "").trim() ||
|
|
3397
|
+
!String(record.sender_encryption_public_key || "").trim() ||
|
|
3398
|
+
!String(record.recipient_encryption_public_key || "").trim() ||
|
|
3399
|
+
!String(record.ciphertext || "").trim() ||
|
|
3400
|
+
!String(record.nonce || "").trim() ||
|
|
3401
|
+
String(record.cipher_scheme || "") !== "nacl-box-v1" ||
|
|
3402
|
+
!String(record.signature || "").trim() ||
|
|
3403
|
+
!Number.isFinite(createdAt)) {
|
|
3404
|
+
return null;
|
|
3405
|
+
}
|
|
3406
|
+
if (fromAgentId === toAgentId) {
|
|
3407
|
+
return null;
|
|
3408
|
+
}
|
|
3409
|
+
if (conversationId !== this.buildPrivateConversationId(fromAgentId, toAgentId)) {
|
|
3410
|
+
return null;
|
|
3411
|
+
}
|
|
3412
|
+
return {
|
|
3413
|
+
type: PRIVATE_MESSAGE_TOPIC,
|
|
3414
|
+
message_id: String(record.message_id).trim(),
|
|
3415
|
+
conversation_id: conversationId,
|
|
3416
|
+
from_agent_id: fromAgentId,
|
|
3417
|
+
to_agent_id: toAgentId,
|
|
3418
|
+
sender_public_key: String(record.sender_public_key).trim(),
|
|
3419
|
+
sender_encryption_public_key: String(record.sender_encryption_public_key).trim(),
|
|
3420
|
+
recipient_encryption_public_key: String(record.recipient_encryption_public_key).trim(),
|
|
3421
|
+
cipher_scheme: "nacl-box-v1",
|
|
3422
|
+
ciphertext: String(record.ciphertext).trim(),
|
|
3423
|
+
nonce: String(record.nonce).trim(),
|
|
3424
|
+
created_at: createdAt,
|
|
3425
|
+
signature: String(record.signature).trim(),
|
|
3426
|
+
};
|
|
3427
|
+
}
|
|
3428
|
+
normalizePrivateMessages(items) {
|
|
3429
|
+
if (!Array.isArray(items)) {
|
|
3430
|
+
return [];
|
|
3431
|
+
}
|
|
3432
|
+
const deduped = new Set();
|
|
3433
|
+
return items
|
|
3434
|
+
.map((item) => this.normalizeIncomingPrivateMessage(item))
|
|
3435
|
+
.filter((item) => Boolean(item))
|
|
3436
|
+
.sort((a, b) => a.created_at - b.created_at)
|
|
3437
|
+
.filter((item) => {
|
|
3438
|
+
if (deduped.has(item.message_id)) {
|
|
3439
|
+
return false;
|
|
3440
|
+
}
|
|
3441
|
+
deduped.add(item.message_id);
|
|
3442
|
+
return true;
|
|
3443
|
+
})
|
|
3444
|
+
.slice(-PRIVATE_MESSAGE_HISTORY_LIMIT);
|
|
3445
|
+
}
|
|
3446
|
+
normalizeIncomingPrivateMessageReceipt(value) {
|
|
3447
|
+
if (typeof value !== "object" || value === null) {
|
|
3448
|
+
return null;
|
|
3449
|
+
}
|
|
3450
|
+
const record = value;
|
|
3451
|
+
const createdAt = Number(record.created_at || 0);
|
|
3452
|
+
const status = String(record.status || "").trim();
|
|
3453
|
+
if (record.type !== PRIVATE_MESSAGE_RECEIPT_TOPIC ||
|
|
3454
|
+
!String(record.receipt_id || "").trim() ||
|
|
3455
|
+
!String(record.message_id || "").trim() ||
|
|
3456
|
+
!String(record.conversation_id || "").trim() ||
|
|
3457
|
+
!String(record.from_agent_id || "").trim() ||
|
|
3458
|
+
!String(record.to_agent_id || "").trim() ||
|
|
3459
|
+
!String(record.sender_public_key || "").trim() ||
|
|
3460
|
+
(status !== "received" && status !== "read") ||
|
|
3461
|
+
!String(record.signature || "").trim() ||
|
|
3462
|
+
!Number.isFinite(createdAt)) {
|
|
3463
|
+
return null;
|
|
3464
|
+
}
|
|
3465
|
+
return {
|
|
3466
|
+
type: PRIVATE_MESSAGE_RECEIPT_TOPIC,
|
|
3467
|
+
receipt_id: String(record.receipt_id).trim(),
|
|
3468
|
+
message_id: String(record.message_id).trim(),
|
|
3469
|
+
conversation_id: String(record.conversation_id).trim(),
|
|
3470
|
+
from_agent_id: String(record.from_agent_id).trim(),
|
|
3471
|
+
to_agent_id: String(record.to_agent_id).trim(),
|
|
3472
|
+
sender_public_key: String(record.sender_public_key).trim(),
|
|
3473
|
+
status: status,
|
|
3474
|
+
created_at: createdAt,
|
|
3475
|
+
signature: String(record.signature).trim(),
|
|
3476
|
+
};
|
|
3477
|
+
}
|
|
3478
|
+
normalizePrivateMessageReceipts(items) {
|
|
3479
|
+
if (!Array.isArray(items)) {
|
|
3480
|
+
return [];
|
|
3481
|
+
}
|
|
3482
|
+
const deduped = new Set();
|
|
3483
|
+
return items
|
|
3484
|
+
.map((item) => this.normalizeIncomingPrivateMessageReceipt(item))
|
|
3485
|
+
.filter((item) => Boolean(item))
|
|
3486
|
+
.sort((a, b) => a.created_at - b.created_at)
|
|
3487
|
+
.filter((item) => {
|
|
3488
|
+
if (deduped.has(item.receipt_id)) {
|
|
3489
|
+
return false;
|
|
3490
|
+
}
|
|
3491
|
+
deduped.add(item.receipt_id);
|
|
3492
|
+
return true;
|
|
3493
|
+
})
|
|
3494
|
+
.slice(-PRIVATE_MESSAGE_RECEIPT_HISTORY_LIMIT);
|
|
3495
|
+
}
|
|
3496
|
+
hasPrivateMessage(messageId) {
|
|
3497
|
+
return this.privateMessages.some((item) => item.message_id === messageId);
|
|
3498
|
+
}
|
|
3499
|
+
ingestPrivateMessage(message) {
|
|
3500
|
+
const existing = this.privateMessages.findIndex((item) => item.message_id === message.message_id);
|
|
3501
|
+
if (existing >= 0) {
|
|
3502
|
+
this.privateMessages[existing] = message;
|
|
3503
|
+
}
|
|
3504
|
+
else {
|
|
3505
|
+
this.privateMessages.push(message);
|
|
3506
|
+
}
|
|
3507
|
+
this.privateMessages = this.normalizePrivateMessages(this.privateMessages);
|
|
3508
|
+
const validIds = new Set(this.privateMessages.map((item) => item.message_id));
|
|
3509
|
+
if (message.from_agent_id !== this.identity?.agent_id) {
|
|
3510
|
+
this.privateMessageBodyCache.delete(message.message_id);
|
|
3511
|
+
}
|
|
3512
|
+
for (const key of Array.from(this.privateMessageBodyCache.keys())) {
|
|
3513
|
+
if (!validIds.has(key)) {
|
|
3514
|
+
this.privateMessageBodyCache.delete(key);
|
|
3515
|
+
}
|
|
3516
|
+
}
|
|
3517
|
+
for (const key of Array.from(this.privateMessageDeliveryStatusCache.keys())) {
|
|
3518
|
+
if (!validIds.has(key)) {
|
|
3519
|
+
this.privateMessageDeliveryStatusCache.delete(key);
|
|
3520
|
+
}
|
|
3521
|
+
}
|
|
3522
|
+
}
|
|
3523
|
+
ingestPrivateMessageReceipt(receipt) {
|
|
3524
|
+
const existing = this.privateMessageReceipts.findIndex((item) => item.receipt_id === receipt.receipt_id);
|
|
3525
|
+
if (existing >= 0) {
|
|
3526
|
+
this.privateMessageReceipts[existing] = receipt;
|
|
3527
|
+
}
|
|
3528
|
+
else {
|
|
3529
|
+
this.privateMessageReceipts.push(receipt);
|
|
3530
|
+
}
|
|
3531
|
+
this.privateMessageReceipts = this.normalizePrivateMessageReceipts(this.privateMessageReceipts);
|
|
3532
|
+
this.privateMessageDeliveryStatusCache.set(receipt.message_id, receipt.status);
|
|
3533
|
+
}
|
|
2662
3534
|
normalizeIncomingSocialMessage(value) {
|
|
2663
3535
|
if (typeof value !== "object" || value === null) {
|
|
2664
3536
|
return null;
|
|
@@ -2905,6 +3777,36 @@ async function main() {
|
|
|
2905
3777
|
app.get("/api/runtime/paths", (_req, res) => {
|
|
2906
3778
|
sendOk(res, node.getRuntimePaths());
|
|
2907
3779
|
});
|
|
3780
|
+
app.get("/api/app/update-status", (_req, res) => {
|
|
3781
|
+
sendOk(res, node.getAppUpdateStatus());
|
|
3782
|
+
});
|
|
3783
|
+
app.post("/api/app/update", asyncRoute(async (_req, res) => {
|
|
3784
|
+
const status = node.getAppUpdateStatus();
|
|
3785
|
+
if (!status.update_available || !status.latest_version) {
|
|
3786
|
+
sendOk(res, {
|
|
3787
|
+
started: false,
|
|
3788
|
+
current_version: status.current_version,
|
|
3789
|
+
latest_version: status.latest_version,
|
|
3790
|
+
platform: status.platform,
|
|
3791
|
+
reason: status.check_error || "already_current",
|
|
3792
|
+
}, { message: "Already on the latest version" });
|
|
3793
|
+
return;
|
|
3794
|
+
}
|
|
3795
|
+
sendOk(res, {
|
|
3796
|
+
started: true,
|
|
3797
|
+
current_version: status.current_version,
|
|
3798
|
+
target_version: status.latest_version,
|
|
3799
|
+
platform: status.platform,
|
|
3800
|
+
}, { message: `Updating to ${status.latest_version}` });
|
|
3801
|
+
setTimeout(() => {
|
|
3802
|
+
try {
|
|
3803
|
+
node.startAppUpdate();
|
|
3804
|
+
}
|
|
3805
|
+
catch {
|
|
3806
|
+
// best effort after response has been sent
|
|
3807
|
+
}
|
|
3808
|
+
}, 1200);
|
|
3809
|
+
}));
|
|
2908
3810
|
app.put("/api/profile", asyncRoute(async (req, res) => {
|
|
2909
3811
|
const body = req.body;
|
|
2910
3812
|
const tags = Array.isArray(body.tags)
|
|
@@ -3003,6 +3905,31 @@ async function main() {
|
|
|
3003
3905
|
const agentId = String(req.query.agent_id ?? "").trim();
|
|
3004
3906
|
sendOk(res, node.getSocialMessages(limit, { agent_id: agentId || null }));
|
|
3005
3907
|
});
|
|
3908
|
+
app.get("/api/private/state", (_req, res) => {
|
|
3909
|
+
sendOk(res, node.getPrivateMessagingState());
|
|
3910
|
+
});
|
|
3911
|
+
app.get("/api/private/conversations", (_req, res) => {
|
|
3912
|
+
sendOk(res, node.getPrivateConversations());
|
|
3913
|
+
});
|
|
3914
|
+
app.get("/api/private/messages", (req, res) => {
|
|
3915
|
+
const conversationId = String(req.query.conversation_id ?? "").trim();
|
|
3916
|
+
const limit = Number(req.query.limit ?? PRIVATE_MESSAGE_QUERY_LIMIT);
|
|
3917
|
+
sendOk(res, node.getPrivateMessages(conversationId, limit));
|
|
3918
|
+
});
|
|
3919
|
+
app.post("/api/private/messages/send", asyncRoute(async (req, res) => {
|
|
3920
|
+
const result = await node.sendPrivateMessage({
|
|
3921
|
+
to_agent_id: String(req.body?.to_agent_id || ""),
|
|
3922
|
+
recipient_encryption_public_key: String(req.body?.recipient_encryption_public_key || ""),
|
|
3923
|
+
body: String(req.body?.body || ""),
|
|
3924
|
+
});
|
|
3925
|
+
sendOk(res, result, {
|
|
3926
|
+
message: result.sent
|
|
3927
|
+
? (result.reason === "direct-sent"
|
|
3928
|
+
? "Private message sent directly"
|
|
3929
|
+
: "Private message sent via encrypted fallback")
|
|
3930
|
+
: `Private message skipped: ${result.reason}`,
|
|
3931
|
+
});
|
|
3932
|
+
}));
|
|
3006
3933
|
app.get("/api/openclaw/bridge", (_req, res) => {
|
|
3007
3934
|
sendOk(res, node.getOpenClawBridgeStatus());
|
|
3008
3935
|
});
|