@silicaclaw/cli 2026.3.20-1 → 2026.3.20-10
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 +54 -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 +129 -2
- package/apps/local-console/dist/apps/local-console/src/server.js +887 -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 +13 -1
- package/apps/local-console/dist/packages/storage/src/repos.js +19 -1
- package/apps/local-console/public/app/app.js +465 -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 +60 -52
- package/apps/local-console/public/app/social.js +316 -93
- package/apps/local-console/public/app/styles.css +127 -1
- package/apps/local-console/public/app/template.js +121 -35
- package/apps/local-console/public/app/translations.js +430 -316
- package/apps/local-console/src/server.ts +1024 -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/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 +13 -1
- package/node_modules/@silicaclaw/storage/dist/packages/storage/src/repos.js +19 -1
- package/node_modules/@silicaclaw/storage/src/repos.ts +31 -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/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 +13 -1
- package/packages/storage/dist/packages/storage/src/repos.js +19 -1
- package/packages/storage/src/repos.ts +31 -1
- package/scripts/silicaclaw-cli.mjs +59 -6
- package/scripts/silicaclaw-gateway.mjs +108 -0
- package/scripts/validate-openclaw-skill.mjs +19 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import express, { NextFunction, Request, Response } from "express";
|
|
2
2
|
import cors from "cors";
|
|
3
|
-
import { execFile, spawnSync } from "child_process";
|
|
3
|
+
import { execFile, spawn, spawnSync } from "child_process";
|
|
4
4
|
import { resolve } from "path";
|
|
5
5
|
import { accessSync, constants, copyFileSync, existsSync, mkdirSync, readFileSync, readdirSync, statSync } from "fs";
|
|
6
6
|
import { createHash } from "crypto";
|
|
@@ -11,19 +11,24 @@ import {
|
|
|
11
11
|
AgentIdentity,
|
|
12
12
|
DirectoryState,
|
|
13
13
|
IndexRefRecord,
|
|
14
|
+
PrivateEncryptionKeyPair,
|
|
15
|
+
PrivateMessageReceiptRecord,
|
|
16
|
+
PrivateMessageRecord,
|
|
14
17
|
PresenceRecord,
|
|
15
18
|
ProfileInput,
|
|
16
19
|
PublicProfile,
|
|
17
20
|
PublicProfileSummary,
|
|
18
21
|
SignedProfileRecord,
|
|
19
22
|
buildPublicProfileSummary,
|
|
20
|
-
buildIndexRecords,
|
|
21
23
|
cleanupExpiredPresence,
|
|
22
24
|
createDefaultProfileInput,
|
|
23
25
|
createEmptyDirectoryState,
|
|
24
26
|
createIdentity,
|
|
27
|
+
createPrivateEncryptionKeyPair,
|
|
25
28
|
dedupeIndex,
|
|
29
|
+
decryptPrivatePayload,
|
|
26
30
|
ensureDefaultSocialMd,
|
|
31
|
+
encryptPrivatePayload,
|
|
27
32
|
ingestIndexRecord,
|
|
28
33
|
ingestPresenceRecord,
|
|
29
34
|
ingestProfileRecord,
|
|
@@ -34,6 +39,8 @@ import {
|
|
|
34
39
|
resolveIdentityWithSocial,
|
|
35
40
|
resolveProfileInputWithSocial,
|
|
36
41
|
searchDirectory,
|
|
42
|
+
signPrivateMessage,
|
|
43
|
+
signPrivateMessageReceipt,
|
|
37
44
|
signSocialMessage,
|
|
38
45
|
signSocialMessageObservation,
|
|
39
46
|
signPresence,
|
|
@@ -46,6 +53,8 @@ import {
|
|
|
46
53
|
verifySocialMessage,
|
|
47
54
|
verifySocialMessageObservation,
|
|
48
55
|
verifyPresence,
|
|
56
|
+
verifyPrivateMessage,
|
|
57
|
+
verifyPrivateMessageReceipt,
|
|
49
58
|
verifyProfile,
|
|
50
59
|
} from "@silicaclaw/core";
|
|
51
60
|
import {
|
|
@@ -62,6 +71,9 @@ import {
|
|
|
62
71
|
CacheRepo,
|
|
63
72
|
IdentityRepo,
|
|
64
73
|
LogRepo,
|
|
74
|
+
PrivateEncryptionKeyRepo,
|
|
75
|
+
PrivateMessageReceiptRepo,
|
|
76
|
+
PrivateMessageRepo,
|
|
65
77
|
ProfileRepo,
|
|
66
78
|
SocialMessageGovernanceConfig,
|
|
67
79
|
SocialMessageGovernanceRepo,
|
|
@@ -89,7 +101,10 @@ const DEFAULT_GLOBAL_ROOM = defaults.network.global_preview.room;
|
|
|
89
101
|
const DEFAULT_BRIDGE_API_BASE = defaults.bridge.api_base;
|
|
90
102
|
const OPENCLAW_GATEWAY_PORT = defaults.ports.openclaw_gateway;
|
|
91
103
|
const OPENCLAW_GATEWAY_URL = `http://${OPENCLAW_GATEWAY_HOST}:${OPENCLAW_GATEWAY_PORT}/`;
|
|
104
|
+
const OPENCLAW_RUNTIME_CACHE_MS = 15_000;
|
|
105
|
+
const OPENCLAW_BRIDGE_STATUS_CACHE_MS = 5_000;
|
|
92
106
|
const NETWORK_PEER_REMOVE_AFTER_MS = Number(process.env.NETWORK_PEER_REMOVE_AFTER_MS || 180_000);
|
|
107
|
+
const DIRECTORY_REMOTE_PROFILE_SOFT_LIMIT = Number(process.env.DIRECTORY_REMOTE_PROFILE_SOFT_LIMIT || 1000);
|
|
93
108
|
const NETWORK_UDP_BIND_ADDRESS = process.env.NETWORK_UDP_BIND_ADDRESS || "0.0.0.0";
|
|
94
109
|
const NETWORK_UDP_BROADCAST_ADDRESS = process.env.NETWORK_UDP_BROADCAST_ADDRESS || "255.255.255.255";
|
|
95
110
|
const NETWORK_PEER_ID = process.env.NETWORK_PEER_ID;
|
|
@@ -102,6 +117,12 @@ const WEBRTC_BOOTSTRAP_HINTS = process.env.WEBRTC_BOOTSTRAP_HINTS || "";
|
|
|
102
117
|
const PROFILE_VERSION = "v0.9";
|
|
103
118
|
const SOCIAL_MESSAGE_TOPIC = "social.message";
|
|
104
119
|
const SOCIAL_MESSAGE_OBSERVATION_TOPIC = "social.message.observation";
|
|
120
|
+
const PRIVATE_MESSAGE_TOPIC = "private.message";
|
|
121
|
+
const PRIVATE_MESSAGE_RECEIPT_TOPIC = "private.message.receipt";
|
|
122
|
+
const PRIVATE_MESSAGE_HISTORY_LIMIT = Number(process.env.PRIVATE_MESSAGE_HISTORY_LIMIT || 1000);
|
|
123
|
+
const PRIVATE_MESSAGE_RECEIPT_HISTORY_LIMIT = Number(process.env.PRIVATE_MESSAGE_RECEIPT_HISTORY_LIMIT || 2000);
|
|
124
|
+
const PRIVATE_MESSAGE_QUERY_LIMIT = Number(process.env.PRIVATE_MESSAGE_QUERY_LIMIT || 100);
|
|
125
|
+
const PRIVATE_MESSAGE_PERSIST_DEBOUNCE_MS = Number(process.env.PRIVATE_MESSAGE_PERSIST_DEBOUNCE_MS || 750);
|
|
105
126
|
const DEFAULT_SOCIAL_MESSAGE_CHANNEL = "global";
|
|
106
127
|
const SOCIAL_MESSAGE_MAX_BODY_CHARS = Number(process.env.SOCIAL_MESSAGE_MAX_BODY_CHARS || 500);
|
|
107
128
|
const SOCIAL_MESSAGE_HISTORY_LIMIT = Number(process.env.SOCIAL_MESSAGE_HISTORY_LIMIT || 100);
|
|
@@ -115,6 +136,12 @@ const SOCIAL_MESSAGE_MAX_AGE_MS = Number(process.env.SOCIAL_MESSAGE_MAX_AGE_MS |
|
|
|
115
136
|
const SOCIAL_MESSAGE_OBSERVATION_HISTORY_LIMIT = Number(process.env.SOCIAL_MESSAGE_OBSERVATION_HISTORY_LIMIT || 500);
|
|
116
137
|
const SOCIAL_MESSAGE_REPLAY_WINDOW_MS = Number(process.env.SOCIAL_MESSAGE_REPLAY_WINDOW_MS || 10 * 60_000);
|
|
117
138
|
const SOCIAL_MESSAGE_REPLAY_MAX_PER_BROADCAST = Number(process.env.SOCIAL_MESSAGE_REPLAY_MAX_PER_BROADCAST || 3);
|
|
139
|
+
const SOCIAL_MESSAGE_REPLAY_REFRESH_INTERVAL_MS = Number(
|
|
140
|
+
process.env.SOCIAL_MESSAGE_REPLAY_REFRESH_INTERVAL_MS || 120_000
|
|
141
|
+
);
|
|
142
|
+
const PROFILE_RELAY_REFRESH_INTERVAL_MS = Number(
|
|
143
|
+
process.env.PROFILE_RELAY_REFRESH_INTERVAL_MS || 120_000
|
|
144
|
+
);
|
|
118
145
|
const SOCIAL_MESSAGE_BLOCKED_AGENT_IDS = new Set(
|
|
119
146
|
dedupeStrings(parseListEnv(process.env.SOCIAL_MESSAGE_BLOCKED_AGENT_IDS || ""))
|
|
120
147
|
);
|
|
@@ -157,6 +184,10 @@ function normalizeVersionText(value: unknown): string {
|
|
|
157
184
|
return text.startsWith("v") ? text.slice(1) : text;
|
|
158
185
|
}
|
|
159
186
|
|
|
187
|
+
function formatBytesToMiB(value: number): number {
|
|
188
|
+
return Math.round((value / (1024 * 1024)) * 10) / 10;
|
|
189
|
+
}
|
|
190
|
+
|
|
160
191
|
function tokenizeVersion(value: unknown): Array<number | string> {
|
|
161
192
|
return normalizeVersionText(value)
|
|
162
193
|
.split(/[^0-9A-Za-z]+/)
|
|
@@ -186,6 +217,14 @@ function compareVersionTokens(left: unknown, right: unknown): number {
|
|
|
186
217
|
return 0;
|
|
187
218
|
}
|
|
188
219
|
|
|
220
|
+
function userNpmCacheDir(): string {
|
|
221
|
+
return resolve(homedir(), ".silicaclaw", "npm-cache");
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function userShimPath(): string {
|
|
225
|
+
return resolve(homedir(), ".silicaclaw", "bin", "silicaclaw");
|
|
226
|
+
}
|
|
227
|
+
|
|
189
228
|
function resolveWorkspaceRoot(cwd = process.cwd()): string {
|
|
190
229
|
if (existsSync(resolve(cwd, "apps", "local-console", "package.json"))) {
|
|
191
230
|
return cwd;
|
|
@@ -441,45 +480,66 @@ function readOpenClawConfiguredGateway(workspaceRoot: string) {
|
|
|
441
480
|
} as const;
|
|
442
481
|
}
|
|
443
482
|
|
|
444
|
-
function
|
|
445
|
-
const
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
}
|
|
449
|
-
const stdout = String(result.stdout || "");
|
|
450
|
-
const lines = stdout
|
|
451
|
-
.split("\n")
|
|
452
|
-
.map((line) => line.trim())
|
|
453
|
-
.filter(Boolean);
|
|
483
|
+
function resolveOpenClawStatusCommand(workspaceRoot: string) {
|
|
484
|
+
const explicitBin = String(process.env.OPENCLAW_BIN || "").trim();
|
|
485
|
+
if (explicitBin) {
|
|
486
|
+
return { cmd: explicitBin, args: ["status"] } as const;
|
|
487
|
+
}
|
|
454
488
|
|
|
455
|
-
const
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
489
|
+
const configuredSourceDir = String(process.env.OPENCLAW_SOURCE_DIR || "").trim();
|
|
490
|
+
const defaultSourceDir = defaultOpenClawSourceDir(workspaceRoot);
|
|
491
|
+
const sourceDir = configuredSourceDir || defaultSourceDir;
|
|
492
|
+
const sourceEntry = existingPathOrNull(resolve(sourceDir, "openclaw.mjs"));
|
|
493
|
+
if (sourceEntry) {
|
|
494
|
+
return { cmd: process.execPath, args: [sourceEntry, "status"] } as const;
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
const commandPath = resolveExecutableInPath("openclaw");
|
|
498
|
+
if (commandPath) {
|
|
499
|
+
return { cmd: commandPath, args: ["status"] } as const;
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
return null;
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
function resolveOpenClawGatewayProbeCommand(workspaceRoot: string) {
|
|
506
|
+
const explicitBin = String(process.env.OPENCLAW_BIN || "").trim();
|
|
507
|
+
if (explicitBin) {
|
|
508
|
+
return { cmd: explicitBin, args: ["gateway", "probe"] } as const;
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
const configuredSourceDir = String(process.env.OPENCLAW_SOURCE_DIR || "").trim();
|
|
512
|
+
const defaultSourceDir = defaultOpenClawSourceDir(workspaceRoot);
|
|
513
|
+
const sourceDir = configuredSourceDir || defaultSourceDir;
|
|
514
|
+
const sourceEntry = existingPathOrNull(resolve(sourceDir, "openclaw.mjs"));
|
|
515
|
+
if (sourceEntry) {
|
|
516
|
+
return { cmd: process.execPath, args: [sourceEntry, "gateway", "probe"] } as const;
|
|
517
|
+
}
|
|
477
518
|
|
|
478
|
-
const
|
|
479
|
-
|
|
519
|
+
const commandPath = resolveExecutableInPath("openclaw");
|
|
520
|
+
if (commandPath) {
|
|
521
|
+
return { cmd: commandPath, args: ["gateway", "probe"] } as const;
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
return null;
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
function detectOpenClawRuntime(workspaceRoot: string) {
|
|
528
|
+
const configuredGateway = readOpenClawConfiguredGateway(workspaceRoot);
|
|
529
|
+
const statusCommand = resolveOpenClawStatusCommand(workspaceRoot);
|
|
530
|
+
const statusLooksConfigured = Boolean(
|
|
531
|
+
statusCommand ||
|
|
532
|
+
configuredGateway.config_path ||
|
|
533
|
+
detectOpenClawInstallation(workspaceRoot).detected
|
|
534
|
+
);
|
|
535
|
+
const gatewayProbeCommand = ["lsof", "-nP", `-iTCP:${configuredGateway.gateway_port}`, "-sTCP:LISTEN"];
|
|
536
|
+
const gatewayProbe = spawnSync(gatewayProbeCommand[0], gatewayProbeCommand.slice(1), {
|
|
480
537
|
encoding: "utf8",
|
|
538
|
+
timeout: 1200,
|
|
481
539
|
});
|
|
482
|
-
const
|
|
540
|
+
const gatewayStatusStdout = String(gatewayProbe.stdout || "");
|
|
541
|
+
const gatewayStatusStderr = String(gatewayProbe.stderr || "");
|
|
542
|
+
const gatewayLines = gatewayStatusStdout
|
|
483
543
|
.split("\n")
|
|
484
544
|
.map((line) => line.trim())
|
|
485
545
|
.filter(Boolean);
|
|
@@ -489,14 +549,9 @@ function detectOpenClawRuntime(workspaceRoot: string) {
|
|
|
489
549
|
const parts = line.split(/\s+/);
|
|
490
550
|
const pid = Number(parts[1] || 0);
|
|
491
551
|
const command = parts[0] || "";
|
|
492
|
-
const lowerCommand = command.toLowerCase();
|
|
493
552
|
const endpoint = parts[8] || parts[parts.length - 1] || "";
|
|
494
553
|
const portMatch = endpoint.match(/:(\d+)(?:\s*\(|$)/);
|
|
495
554
|
if (!pid || !command || !portMatch) return null;
|
|
496
|
-
const isOpenClawListener =
|
|
497
|
-
openclawPids.has(pid) ||
|
|
498
|
-
lowerCommand.includes("openclaw");
|
|
499
|
-
if (!isOpenClawListener) return null;
|
|
500
555
|
const port = Number(portMatch[1]);
|
|
501
556
|
if (!Number.isFinite(port) || port <= 0) return null;
|
|
502
557
|
return {
|
|
@@ -507,46 +562,106 @@ function detectOpenClawRuntime(workspaceRoot: string) {
|
|
|
507
562
|
};
|
|
508
563
|
})
|
|
509
564
|
.filter((item): item is { pid: number; ppid: number; port: number; command: string } => Boolean(item));
|
|
565
|
+
const gatewayProbeOk = gatewayListeners.length > 0;
|
|
566
|
+
let processes: Array<{ pid: number; ppid: number; command: string }> = gatewayListeners.map((item) => ({
|
|
567
|
+
pid: item.pid,
|
|
568
|
+
ppid: item.ppid,
|
|
569
|
+
command: item.command,
|
|
570
|
+
}));
|
|
571
|
+
let processResult: ReturnType<typeof spawnSync> | null = null;
|
|
572
|
+
if (!gatewayProbeOk) {
|
|
573
|
+
processResult = spawnSync("ps", ["-Ao", "pid=,ppid=,command="], {
|
|
574
|
+
encoding: "utf8",
|
|
575
|
+
timeout: 1200,
|
|
576
|
+
});
|
|
577
|
+
const stdout = String(processResult.stdout || "");
|
|
578
|
+
const lines = stdout
|
|
579
|
+
.split("\n")
|
|
580
|
+
.map((line) => line.trim())
|
|
581
|
+
.filter(Boolean);
|
|
582
|
+
processes = lines
|
|
583
|
+
.map((line) => {
|
|
584
|
+
const match = line.match(/^(\d+)\s+(\d+)\s+(.+)$/);
|
|
585
|
+
if (!match) return null;
|
|
586
|
+
const command = match[3] || "";
|
|
587
|
+
const lower = command.toLowerCase();
|
|
588
|
+
const isOpenClaw =
|
|
589
|
+
lower.includes(" openclaw ") ||
|
|
590
|
+
lower.endsWith(" openclaw") ||
|
|
591
|
+
lower.includes("/openclaw ") ||
|
|
592
|
+
lower.includes("openclaw.mjs") ||
|
|
593
|
+
lower.includes("openclaw gateway") ||
|
|
594
|
+
lower.includes("openclaw agent") ||
|
|
595
|
+
lower.includes("openclaw message");
|
|
596
|
+
if (!isOpenClaw) return null;
|
|
597
|
+
return {
|
|
598
|
+
pid: Number(match[1]),
|
|
599
|
+
ppid: Number(match[2]),
|
|
600
|
+
command,
|
|
601
|
+
};
|
|
602
|
+
})
|
|
603
|
+
.filter((item): item is { pid: number; ppid: number; command: string } => Boolean(item));
|
|
604
|
+
}
|
|
605
|
+
|
|
510
606
|
const preferredListener =
|
|
511
607
|
gatewayListeners.find((item) => item.port === configuredGateway.gateway_port) ||
|
|
512
608
|
gatewayListeners[0] ||
|
|
513
609
|
null;
|
|
514
|
-
|
|
515
|
-
const
|
|
516
|
-
for (const process of [...processes, ...gatewayListeners]) {
|
|
517
|
-
if (!combinedProcesses.has(process.pid)) {
|
|
518
|
-
combinedProcesses.set(process.pid, process);
|
|
519
|
-
continue;
|
|
520
|
-
}
|
|
521
|
-
const current = combinedProcesses.get(process.pid);
|
|
522
|
-
if (current && current.command.length < process.command.length) {
|
|
523
|
-
combinedProcesses.set(process.pid, process);
|
|
524
|
-
}
|
|
525
|
-
}
|
|
526
|
-
const allProcesses = Array.from(combinedProcesses.values());
|
|
527
|
-
const gatewayReachable = gatewayListeners.length > 0;
|
|
610
|
+
const allProcesses = processes.slice(0, 10);
|
|
611
|
+
const gatewayReachable = gatewayProbeOk;
|
|
528
612
|
const detectionNotes = [];
|
|
529
|
-
if (result.status !== 0) detectionNotes.push(String(result.stderr || "ps failed").trim());
|
|
530
613
|
if (gatewayProbe.status !== 0 && gatewayLines.length === 0) {
|
|
531
|
-
detectionNotes.push(String(
|
|
614
|
+
detectionNotes.push(String(gatewayStatusStderr || "openclaw gateway probe failed").trim());
|
|
615
|
+
}
|
|
616
|
+
if (processResult && processResult.status !== 0) {
|
|
617
|
+
detectionNotes.push(String(processResult.stderr || "ps failed").trim());
|
|
532
618
|
}
|
|
533
619
|
const gatewayPort = preferredListener?.port || configuredGateway.gateway_port;
|
|
534
620
|
const gatewayUrl = `http://${OPENCLAW_GATEWAY_HOST}:${gatewayPort}/`;
|
|
535
621
|
|
|
536
622
|
return {
|
|
537
|
-
running: allProcesses.length > 0 || gatewayReachable,
|
|
623
|
+
running: gatewayProbeOk || allProcesses.length > 0 || gatewayReachable,
|
|
538
624
|
process_count: allProcesses.length,
|
|
539
625
|
processes: allProcesses.slice(0, 10),
|
|
540
626
|
detection_error: detectionNotes.filter(Boolean).join(" | ") || null,
|
|
541
627
|
gateway_url: gatewayUrl,
|
|
542
628
|
gateway_port: gatewayPort,
|
|
543
629
|
gateway_reachable: gatewayReachable,
|
|
630
|
+
status_command: statusCommand ? [statusCommand.cmd, ...statusCommand.args].join(" ") : null,
|
|
631
|
+
status_ok: statusLooksConfigured,
|
|
632
|
+
status_summary: statusLooksConfigured
|
|
633
|
+
? configuredGateway.config_path
|
|
634
|
+
? `configured via ${configuredGateway.config_path}`
|
|
635
|
+
: statusCommand
|
|
636
|
+
? `command available: ${[statusCommand.cmd, ...statusCommand.args].join(" ")}`
|
|
637
|
+
: "OpenClaw environment detected"
|
|
638
|
+
: null,
|
|
639
|
+
gateway_probe_command: gatewayProbeCommand.join(" "),
|
|
640
|
+
gateway_probe_ok: gatewayProbeOk,
|
|
641
|
+
gateway_probe_summary: gatewayProbeOk
|
|
642
|
+
? gatewayStatusStdout
|
|
643
|
+
.split("\n")
|
|
644
|
+
.map((line) => line.trim())
|
|
645
|
+
.filter(Boolean)
|
|
646
|
+
.slice(0, 4)
|
|
647
|
+
.join(" | ")
|
|
648
|
+
: null,
|
|
544
649
|
configured_gateway_url: configuredGateway.gateway_url,
|
|
545
650
|
configured_gateway_port: configuredGateway.gateway_port,
|
|
546
651
|
configured_gateway_bind: configuredGateway.gateway_bind,
|
|
547
652
|
configured_gateway_config_path: configuredGateway.config_path,
|
|
548
653
|
detection_mode:
|
|
549
|
-
|
|
654
|
+
gatewayProbeOk
|
|
655
|
+
? (
|
|
656
|
+
processes.length > 0 && gatewayReachable
|
|
657
|
+
? "gateway-probe+process+gateway"
|
|
658
|
+
: gatewayReachable
|
|
659
|
+
? "gateway-probe+gateway"
|
|
660
|
+
: processes.length > 0
|
|
661
|
+
? "gateway-probe+process"
|
|
662
|
+
: "gateway-probe"
|
|
663
|
+
)
|
|
664
|
+
: processes.length > 0 && gatewayReachable
|
|
550
665
|
? "process+gateway"
|
|
551
666
|
: gatewayReachable
|
|
552
667
|
? "gateway"
|
|
@@ -766,6 +881,7 @@ type IntegrationStatusSummary = {
|
|
|
766
881
|
};
|
|
767
882
|
|
|
768
883
|
type SocialMessageView = SocialMessageRecord & {
|
|
884
|
+
avatar_url?: string;
|
|
769
885
|
is_self: boolean;
|
|
770
886
|
online: boolean;
|
|
771
887
|
last_seen_at: number | null;
|
|
@@ -775,6 +891,17 @@ type SocialMessageView = SocialMessageRecord & {
|
|
|
775
891
|
delivery_status: "local-only" | "remote-observed";
|
|
776
892
|
};
|
|
777
893
|
|
|
894
|
+
type PrivateMessageView = {
|
|
895
|
+
message_id: string;
|
|
896
|
+
conversation_id: string;
|
|
897
|
+
from_agent_id: string;
|
|
898
|
+
to_agent_id: string;
|
|
899
|
+
body: string;
|
|
900
|
+
created_at: number;
|
|
901
|
+
is_self: boolean;
|
|
902
|
+
delivery_status: "sent" | "received" | "read";
|
|
903
|
+
};
|
|
904
|
+
|
|
778
905
|
type RuntimeMessageGovernance = SocialMessageGovernanceConfig;
|
|
779
906
|
|
|
780
907
|
type OpenClawBridgeStatus = {
|
|
@@ -818,11 +945,17 @@ type OpenClawBridgeStatus = {
|
|
|
818
945
|
gateway_url: string;
|
|
819
946
|
gateway_port: number;
|
|
820
947
|
gateway_reachable: boolean;
|
|
948
|
+
status_command: string | null;
|
|
949
|
+
status_ok: boolean;
|
|
950
|
+
status_summary: string | null;
|
|
951
|
+
gateway_probe_command: string | null;
|
|
952
|
+
gateway_probe_ok: boolean;
|
|
953
|
+
gateway_probe_summary: string | null;
|
|
821
954
|
configured_gateway_url: string;
|
|
822
955
|
configured_gateway_port: number;
|
|
823
956
|
configured_gateway_bind: string | null;
|
|
824
957
|
configured_gateway_config_path: string | null;
|
|
825
|
-
detection_mode: "process" | "gateway" | "process+gateway" | "not_running";
|
|
958
|
+
detection_mode: "gateway-probe" | "gateway-probe+process" | "gateway-probe+gateway" | "gateway-probe+process+gateway" | "process" | "gateway" | "process+gateway" | "not_running";
|
|
826
959
|
};
|
|
827
960
|
skill_learning: {
|
|
828
961
|
available: boolean;
|
|
@@ -899,6 +1032,9 @@ export class LocalNodeService {
|
|
|
899
1032
|
private socialMessageGovernanceRepo: SocialMessageGovernanceRepo;
|
|
900
1033
|
private socialMessageRepo: SocialMessageRepo;
|
|
901
1034
|
private socialMessageObservationRepo: SocialMessageObservationRepo;
|
|
1035
|
+
private privateMessageRepo: PrivateMessageRepo;
|
|
1036
|
+
private privateMessageReceiptRepo: PrivateMessageReceiptRepo;
|
|
1037
|
+
private privateEncryptionKeyRepo: PrivateEncryptionKeyRepo;
|
|
902
1038
|
private socialRuntimeRepo: SocialRuntimeRepo;
|
|
903
1039
|
|
|
904
1040
|
private identity: AgentIdentity | null = null;
|
|
@@ -906,15 +1042,31 @@ export class LocalNodeService {
|
|
|
906
1042
|
private directory: DirectoryState = createEmptyDirectoryState();
|
|
907
1043
|
private socialMessages: SocialMessageRecord[] = [];
|
|
908
1044
|
private socialMessageObservations: SocialMessageObservationRecord[] = [];
|
|
1045
|
+
private privateMessages: PrivateMessageRecord[] = [];
|
|
1046
|
+
private privateMessageReceipts: PrivateMessageReceiptRecord[] = [];
|
|
1047
|
+
private privateEncryptionKeyPair: PrivateEncryptionKeyPair | null = null;
|
|
1048
|
+
private privatePeerRoutes: Record<string, string> = {};
|
|
1049
|
+
private privateMessageBodyCache = new Map<string, string>();
|
|
909
1050
|
private messageGovernance: RuntimeMessageGovernance;
|
|
1051
|
+
private privateMessagesPersistDirty = false;
|
|
1052
|
+
private privateMessageReceiptsPersistDirty = false;
|
|
1053
|
+
private privateMessagesPersistTimer: NodeJS.Timeout | null = null;
|
|
1054
|
+
private privateMessageReceiptsPersistTimer: NodeJS.Timeout | null = null;
|
|
910
1055
|
|
|
911
1056
|
private receivedCount = 0;
|
|
912
1057
|
private broadcastCount = 0;
|
|
913
1058
|
private lastMessageAt = 0;
|
|
914
1059
|
private lastBroadcastAt = 0;
|
|
1060
|
+
private lastProfileBroadcastAt = 0;
|
|
1061
|
+
private lastProfileBroadcastSignature = "";
|
|
1062
|
+
private lastReplayBroadcastAt = 0;
|
|
1063
|
+
private lastReplayBroadcastSignature = "";
|
|
915
1064
|
private lastBroadcastErrorAt = 0;
|
|
916
1065
|
private lastBroadcastError: string | null = null;
|
|
917
1066
|
private broadcastFailureCount = 0;
|
|
1067
|
+
private consecutiveBroadcastFailures = 0;
|
|
1068
|
+
private lastBroadcastRecoveryAttemptAt = 0;
|
|
1069
|
+
private broadcastRecoveryInFlight = false;
|
|
918
1070
|
private broadcaster: NodeJS.Timeout | null = null;
|
|
919
1071
|
private subscriptionsBound = false;
|
|
920
1072
|
private broadcastEnabled = true;
|
|
@@ -956,6 +1108,8 @@ export class LocalNodeService {
|
|
|
956
1108
|
private networkReconnectTimer: NodeJS.Timeout | null = null;
|
|
957
1109
|
private networkReconnectDelayMs = 5_000;
|
|
958
1110
|
private appVersion = "unknown";
|
|
1111
|
+
private openclawRuntimeCache: { value: ReturnType<typeof detectOpenClawRuntime>; expiresAt: number } | null = null;
|
|
1112
|
+
private openclawBridgeStatusCache: { value: OpenClawBridgeStatus; expiresAt: number } | null = null;
|
|
959
1113
|
|
|
960
1114
|
constructor(options?: { workspaceRoot?: string; projectRoot?: string; storageRoot?: string }) {
|
|
961
1115
|
this.workspaceRoot = options?.workspaceRoot || resolveWorkspaceRoot();
|
|
@@ -971,6 +1125,9 @@ export class LocalNodeService {
|
|
|
971
1125
|
this.socialMessageGovernanceRepo = new SocialMessageGovernanceRepo(this.storageRoot);
|
|
972
1126
|
this.socialMessageRepo = new SocialMessageRepo(this.storageRoot);
|
|
973
1127
|
this.socialMessageObservationRepo = new SocialMessageObservationRepo(this.storageRoot);
|
|
1128
|
+
this.privateMessageRepo = new PrivateMessageRepo(this.storageRoot);
|
|
1129
|
+
this.privateMessageReceiptRepo = new PrivateMessageReceiptRepo(this.storageRoot);
|
|
1130
|
+
this.privateEncryptionKeyRepo = new PrivateEncryptionKeyRepo(this.storageRoot);
|
|
974
1131
|
this.socialRuntimeRepo = new SocialRuntimeRepo(this.storageRoot);
|
|
975
1132
|
this.messageGovernance = this.defaultMessageGovernance();
|
|
976
1133
|
|
|
@@ -1001,6 +1158,24 @@ export class LocalNodeService {
|
|
|
1001
1158
|
this.networkPort = resolved.port;
|
|
1002
1159
|
}
|
|
1003
1160
|
|
|
1161
|
+
private getCachedOpenClawRuntime() {
|
|
1162
|
+
const now = Date.now();
|
|
1163
|
+
if (this.openclawRuntimeCache && this.openclawRuntimeCache.expiresAt > now) {
|
|
1164
|
+
return this.openclawRuntimeCache.value;
|
|
1165
|
+
}
|
|
1166
|
+
const value = detectOpenClawRuntime(this.projectRoot);
|
|
1167
|
+
this.openclawRuntimeCache = {
|
|
1168
|
+
value,
|
|
1169
|
+
expiresAt: now + OPENCLAW_RUNTIME_CACHE_MS,
|
|
1170
|
+
};
|
|
1171
|
+
return value;
|
|
1172
|
+
}
|
|
1173
|
+
|
|
1174
|
+
private invalidateOpenClawCaches() {
|
|
1175
|
+
this.openclawRuntimeCache = null;
|
|
1176
|
+
this.openclawBridgeStatusCache = null;
|
|
1177
|
+
}
|
|
1178
|
+
|
|
1004
1179
|
async start(): Promise<void> {
|
|
1005
1180
|
await this.hydrateFromDisk();
|
|
1006
1181
|
|
|
@@ -1014,6 +1189,7 @@ export class LocalNodeService {
|
|
|
1014
1189
|
clearInterval(this.broadcaster);
|
|
1015
1190
|
this.broadcaster = null;
|
|
1016
1191
|
}
|
|
1192
|
+
await this.flushPrivatePersistence();
|
|
1017
1193
|
if (this.networkStarted) {
|
|
1018
1194
|
await this.network.stop();
|
|
1019
1195
|
}
|
|
@@ -1034,6 +1210,9 @@ export class LocalNodeService {
|
|
|
1034
1210
|
getOverview() {
|
|
1035
1211
|
const discovered = this.search("");
|
|
1036
1212
|
const onlineCount = discovered.filter((profile) => profile.online).length;
|
|
1213
|
+
const openclawInstallation = detectOpenClawInstallation(this.projectRoot);
|
|
1214
|
+
const openclawRuntime = this.getCachedOpenClawRuntime();
|
|
1215
|
+
const openclawSkillInstallation = detectOpenClawSkillInstallation();
|
|
1037
1216
|
|
|
1038
1217
|
return {
|
|
1039
1218
|
app_version: this.appVersion,
|
|
@@ -1050,6 +1229,15 @@ export class LocalNodeService {
|
|
|
1050
1229
|
init_state: this.initState,
|
|
1051
1230
|
presence_ttl_ms: PRESENCE_TTL_MS,
|
|
1052
1231
|
onboarding: this.getOnboardingSummary(),
|
|
1232
|
+
openclaw: {
|
|
1233
|
+
detected: openclawInstallation.detected,
|
|
1234
|
+
running: openclawRuntime.running,
|
|
1235
|
+
detection_mode: openclawRuntime.detection_mode,
|
|
1236
|
+
gateway_url: openclawRuntime.gateway_url,
|
|
1237
|
+
gateway_probe_ok: openclawRuntime.gateway_probe_ok,
|
|
1238
|
+
status_ok: openclawRuntime.status_ok,
|
|
1239
|
+
skill_installed: openclawSkillInstallation.installed,
|
|
1240
|
+
},
|
|
1053
1241
|
social: {
|
|
1054
1242
|
found: this.socialFound,
|
|
1055
1243
|
enabled: this.socialConfig.enabled,
|
|
@@ -1197,6 +1385,7 @@ export class LocalNodeService {
|
|
|
1197
1385
|
const relayCapable = this.adapterMode === "webrtc-preview" || this.adapterMode === "relay-preview";
|
|
1198
1386
|
const peers: Array<{ status?: string }> = diagnostics?.peers?.items ?? [];
|
|
1199
1387
|
const online = peers.filter((peer: { status?: string }) => peer.status === "online").length;
|
|
1388
|
+
const memory = process.memoryUsage();
|
|
1200
1389
|
|
|
1201
1390
|
return {
|
|
1202
1391
|
adapter: this.adapterMode,
|
|
@@ -1221,6 +1410,23 @@ export class LocalNodeService {
|
|
|
1221
1410
|
adapter_stats: diagnostics?.stats ?? null,
|
|
1222
1411
|
adapter_transport_stats: diagnostics?.transport_stats ?? null,
|
|
1223
1412
|
adapter_discovery_stats: diagnostics?.discovery_stats ?? null,
|
|
1413
|
+
runtime_diagnostics: {
|
|
1414
|
+
memory_mib: {
|
|
1415
|
+
rss: formatBytesToMiB(memory.rss),
|
|
1416
|
+
heap_used: formatBytesToMiB(memory.heapUsed),
|
|
1417
|
+
heap_total: formatBytesToMiB(memory.heapTotal),
|
|
1418
|
+
external: formatBytesToMiB(memory.external),
|
|
1419
|
+
},
|
|
1420
|
+
directory: {
|
|
1421
|
+
profile_count: Object.keys(this.directory.profiles).length,
|
|
1422
|
+
presence_count: Object.keys(this.directory.presence).length,
|
|
1423
|
+
index_key_count: Object.keys(this.directory.index).length,
|
|
1424
|
+
},
|
|
1425
|
+
social: {
|
|
1426
|
+
message_count: this.socialMessages.length,
|
|
1427
|
+
observation_count: this.socialMessageObservations.length,
|
|
1428
|
+
},
|
|
1429
|
+
},
|
|
1224
1430
|
adapter_diagnostics_summary: relayCapable || diagnostics
|
|
1225
1431
|
? {
|
|
1226
1432
|
started: this.networkStarted,
|
|
@@ -1337,6 +1543,92 @@ export class LocalNodeService {
|
|
|
1337
1543
|
};
|
|
1338
1544
|
}
|
|
1339
1545
|
|
|
1546
|
+
getAppUpdateStatus() {
|
|
1547
|
+
const currentVersion = normalizeVersionText(this.appVersion) || "unknown";
|
|
1548
|
+
const fallback = {
|
|
1549
|
+
current_version: currentVersion,
|
|
1550
|
+
latest_version: currentVersion,
|
|
1551
|
+
update_available: false,
|
|
1552
|
+
channel: "latest",
|
|
1553
|
+
platform: process.platform,
|
|
1554
|
+
checked_at: Date.now(),
|
|
1555
|
+
can_update: true,
|
|
1556
|
+
check_error: null as string | null,
|
|
1557
|
+
};
|
|
1558
|
+
try {
|
|
1559
|
+
const result = spawnSync("npm", ["view", "@silicaclaw/cli", "dist-tags", "--json"], {
|
|
1560
|
+
cwd: this.projectRoot,
|
|
1561
|
+
encoding: "utf8",
|
|
1562
|
+
env: {
|
|
1563
|
+
...process.env,
|
|
1564
|
+
SILICACLAW_WORKSPACE_DIR: this.projectRoot,
|
|
1565
|
+
SILICACLAW_APP_DIR: this.workspaceRoot,
|
|
1566
|
+
npm_config_cache: process.env.npm_config_cache || userNpmCacheDir(),
|
|
1567
|
+
},
|
|
1568
|
+
});
|
|
1569
|
+
if ((result.status ?? 1) !== 0) {
|
|
1570
|
+
return {
|
|
1571
|
+
...fallback,
|
|
1572
|
+
check_error: String(result.stderr || result.stdout || "npm view failed").trim() || "npm view failed",
|
|
1573
|
+
};
|
|
1574
|
+
}
|
|
1575
|
+
const tags = JSON.parse(String(result.stdout || "{}").trim() || "{}") as { latest?: string };
|
|
1576
|
+
const latestVersion = normalizeVersionText(tags.latest || currentVersion) || currentVersion;
|
|
1577
|
+
return {
|
|
1578
|
+
...fallback,
|
|
1579
|
+
latest_version: latestVersion,
|
|
1580
|
+
update_available: compareVersionTokens(latestVersion, currentVersion) > 0,
|
|
1581
|
+
};
|
|
1582
|
+
} catch (error) {
|
|
1583
|
+
return {
|
|
1584
|
+
...fallback,
|
|
1585
|
+
check_error: error instanceof Error ? error.message : String(error),
|
|
1586
|
+
};
|
|
1587
|
+
}
|
|
1588
|
+
}
|
|
1589
|
+
|
|
1590
|
+
startAppUpdate(): { started: boolean; target_version: string; platform: string; reason?: string } {
|
|
1591
|
+
const status = this.getAppUpdateStatus();
|
|
1592
|
+
if (!status.update_available || !status.latest_version) {
|
|
1593
|
+
return {
|
|
1594
|
+
started: false,
|
|
1595
|
+
target_version: status.latest_version || status.current_version,
|
|
1596
|
+
platform: process.platform,
|
|
1597
|
+
reason: status.check_error || "already_current",
|
|
1598
|
+
};
|
|
1599
|
+
}
|
|
1600
|
+
const shimPath = userShimPath();
|
|
1601
|
+
const scriptPath = resolve(this.workspaceRoot, "scripts", "silicaclaw-cli.mjs");
|
|
1602
|
+
const useShim = existsSync(shimPath);
|
|
1603
|
+
if (!useShim && !existsSync(scriptPath)) {
|
|
1604
|
+
return {
|
|
1605
|
+
started: false,
|
|
1606
|
+
target_version: status.latest_version,
|
|
1607
|
+
platform: process.platform,
|
|
1608
|
+
reason: "missing_cli_script",
|
|
1609
|
+
};
|
|
1610
|
+
}
|
|
1611
|
+
const command = useShim ? shimPath : process.execPath;
|
|
1612
|
+
const args = useShim ? ["update"] : [scriptPath, "update"];
|
|
1613
|
+
const child = spawn(command, args, {
|
|
1614
|
+
cwd: this.projectRoot,
|
|
1615
|
+
detached: true,
|
|
1616
|
+
stdio: "ignore",
|
|
1617
|
+
env: {
|
|
1618
|
+
...process.env,
|
|
1619
|
+
SILICACLAW_WORKSPACE_DIR: this.projectRoot,
|
|
1620
|
+
SILICACLAW_APP_DIR: this.workspaceRoot,
|
|
1621
|
+
npm_config_cache: process.env.npm_config_cache || userNpmCacheDir(),
|
|
1622
|
+
},
|
|
1623
|
+
});
|
|
1624
|
+
child.unref();
|
|
1625
|
+
return {
|
|
1626
|
+
started: true,
|
|
1627
|
+
target_version: status.latest_version,
|
|
1628
|
+
platform: process.platform,
|
|
1629
|
+
};
|
|
1630
|
+
}
|
|
1631
|
+
|
|
1340
1632
|
getIntegrationSummary() {
|
|
1341
1633
|
const status = this.getIntegrationStatus();
|
|
1342
1634
|
const runtimeGenerated = Boolean(this.socialRuntime && this.socialRuntime.last_loaded_at > 0);
|
|
@@ -1635,6 +1927,7 @@ export class LocalNodeService {
|
|
|
1635
1927
|
return {
|
|
1636
1928
|
...message,
|
|
1637
1929
|
display_name: profile?.display_name || message.display_name || "Unnamed",
|
|
1930
|
+
avatar_url: profile?.avatar_url || "",
|
|
1638
1931
|
is_self: message.agent_id === this.identity?.agent_id,
|
|
1639
1932
|
online: isAgentOnline(lastSeenAt, Date.now(), PRESENCE_TTL_MS),
|
|
1640
1933
|
last_seen_at: lastSeenAt || null,
|
|
@@ -1654,10 +1947,141 @@ export class LocalNodeService {
|
|
|
1654
1947
|
};
|
|
1655
1948
|
}
|
|
1656
1949
|
|
|
1950
|
+
getPrivateMessagingState() {
|
|
1951
|
+
return {
|
|
1952
|
+
enabled: Boolean(this.identity && this.privateEncryptionKeyPair),
|
|
1953
|
+
agent_id: this.identity?.agent_id || "",
|
|
1954
|
+
encryption_public_key: this.privateEncryptionKeyPair?.public_key || "",
|
|
1955
|
+
conversation_count: this.getPrivateConversations().length,
|
|
1956
|
+
message_count: this.privateMessages.length,
|
|
1957
|
+
};
|
|
1958
|
+
}
|
|
1959
|
+
|
|
1960
|
+
getPrivateConversations(): Array<{
|
|
1961
|
+
conversation_id: string;
|
|
1962
|
+
peer_agent_id: string;
|
|
1963
|
+
peer_display_name: string;
|
|
1964
|
+
peer_avatar_url: string;
|
|
1965
|
+
peer_public_key: string;
|
|
1966
|
+
last_message_at: number | null;
|
|
1967
|
+
unread_count: number;
|
|
1968
|
+
}> {
|
|
1969
|
+
const conversations = new Map<string, {
|
|
1970
|
+
conversation_id: string;
|
|
1971
|
+
peer_agent_id: string;
|
|
1972
|
+
peer_display_name: string;
|
|
1973
|
+
peer_avatar_url: string;
|
|
1974
|
+
peer_public_key: string;
|
|
1975
|
+
last_message_at: number | null;
|
|
1976
|
+
unread_count: number;
|
|
1977
|
+
}>();
|
|
1978
|
+
for (const message of this.privateMessages) {
|
|
1979
|
+
if (message.from_agent_id === message.to_agent_id) {
|
|
1980
|
+
continue;
|
|
1981
|
+
}
|
|
1982
|
+
const peerAgentId = message.from_agent_id === this.identity?.agent_id ? message.to_agent_id : message.from_agent_id;
|
|
1983
|
+
if (!peerAgentId || peerAgentId === this.identity?.agent_id) {
|
|
1984
|
+
continue;
|
|
1985
|
+
}
|
|
1986
|
+
const peerProfile = this.directory.profiles[peerAgentId];
|
|
1987
|
+
const current = conversations.get(message.conversation_id);
|
|
1988
|
+
const nextLast = Math.max(current?.last_message_at || 0, message.created_at || 0) || null;
|
|
1989
|
+
conversations.set(message.conversation_id, {
|
|
1990
|
+
conversation_id: message.conversation_id,
|
|
1991
|
+
peer_agent_id: peerAgentId,
|
|
1992
|
+
peer_display_name: peerProfile?.display_name || peerAgentId,
|
|
1993
|
+
peer_avatar_url: peerProfile?.avatar_url || "",
|
|
1994
|
+
peer_public_key: peerProfile?.private_encryption_public_key || "",
|
|
1995
|
+
last_message_at: nextLast,
|
|
1996
|
+
unread_count: current?.unread_count || 0,
|
|
1997
|
+
});
|
|
1998
|
+
}
|
|
1999
|
+
return Array.from(conversations.values()).sort((a, b) => (b.last_message_at || 0) - (a.last_message_at || 0));
|
|
2000
|
+
}
|
|
2001
|
+
|
|
2002
|
+
getPrivateMessages(conversationId: string, limit = PRIVATE_MESSAGE_QUERY_LIMIT): PrivateMessageView[] {
|
|
2003
|
+
const normalizedConversationId = String(conversationId || "").trim();
|
|
2004
|
+
const resolvedLimit = Math.max(1, Math.min(PRIVATE_MESSAGE_QUERY_LIMIT, Number(limit) || PRIVATE_MESSAGE_QUERY_LIMIT));
|
|
2005
|
+
const receiptsByMessageId = new Map(
|
|
2006
|
+
this.privateMessageReceipts.map((receipt) => [receipt.message_id, receipt.status] as const)
|
|
2007
|
+
);
|
|
2008
|
+
return this.privateMessages
|
|
2009
|
+
.filter((message) => {
|
|
2010
|
+
if (message.from_agent_id === message.to_agent_id) {
|
|
2011
|
+
return false;
|
|
2012
|
+
}
|
|
2013
|
+
const peerAgentId = message.from_agent_id === this.identity?.agent_id ? message.to_agent_id : message.from_agent_id;
|
|
2014
|
+
if (!peerAgentId || peerAgentId === this.identity?.agent_id) {
|
|
2015
|
+
return false;
|
|
2016
|
+
}
|
|
2017
|
+
return !normalizedConversationId || message.conversation_id === normalizedConversationId;
|
|
2018
|
+
})
|
|
2019
|
+
.sort((a, b) => a.created_at - b.created_at)
|
|
2020
|
+
.slice(-resolvedLimit)
|
|
2021
|
+
.map((message) => ({
|
|
2022
|
+
message_id: message.message_id,
|
|
2023
|
+
conversation_id: message.conversation_id,
|
|
2024
|
+
from_agent_id: message.from_agent_id,
|
|
2025
|
+
to_agent_id: message.to_agent_id,
|
|
2026
|
+
body: this.decryptPrivateMessageBody(message),
|
|
2027
|
+
created_at: message.created_at,
|
|
2028
|
+
is_self: message.from_agent_id === this.identity?.agent_id,
|
|
2029
|
+
delivery_status: receiptsByMessageId.get(message.message_id) || "sent",
|
|
2030
|
+
}));
|
|
2031
|
+
}
|
|
2032
|
+
|
|
2033
|
+
async sendPrivateMessage(input: {
|
|
2034
|
+
to_agent_id: string;
|
|
2035
|
+
recipient_encryption_public_key: string;
|
|
2036
|
+
body: string;
|
|
2037
|
+
}): Promise<{ sent: boolean; reason: string; message?: PrivateMessageView }> {
|
|
2038
|
+
if (!this.identity || !this.privateEncryptionKeyPair) {
|
|
2039
|
+
return { sent: false, reason: "missing_identity_or_private_key" };
|
|
2040
|
+
}
|
|
2041
|
+
const toAgentId = String(input.to_agent_id || "").trim();
|
|
2042
|
+
const recipientKey = String(input.recipient_encryption_public_key || "").trim();
|
|
2043
|
+
const body = String(input.body || "").trim();
|
|
2044
|
+
if (toAgentId === this.identity.agent_id) {
|
|
2045
|
+
return { sent: false, reason: "self_private_message_not_allowed" };
|
|
2046
|
+
}
|
|
2047
|
+
const toPeerId = this.privatePeerRoutes[toAgentId] || "";
|
|
2048
|
+
if (!toAgentId || !toPeerId || !recipientKey || !body) {
|
|
2049
|
+
return { sent: false, reason: "invalid_private_message_input" };
|
|
2050
|
+
}
|
|
2051
|
+
if (typeof this.network.sendDirect !== "function") {
|
|
2052
|
+
return { sent: false, reason: "direct_delivery_not_supported" };
|
|
2053
|
+
}
|
|
2054
|
+
const encrypted = encryptPrivatePayload({
|
|
2055
|
+
plaintext: body,
|
|
2056
|
+
recipient_public_key: recipientKey,
|
|
2057
|
+
sender_keypair: this.privateEncryptionKeyPair,
|
|
2058
|
+
});
|
|
2059
|
+
const message = signPrivateMessage({
|
|
2060
|
+
identity: this.identity,
|
|
2061
|
+
message_id: createHash("sha256").update(`${this.identity.agent_id}:${toAgentId}:${Date.now()}:${body}:${Math.random()}`, "utf8").digest("hex"),
|
|
2062
|
+
conversation_id: this.buildPrivateConversationId(this.identity.agent_id, toAgentId),
|
|
2063
|
+
to_agent_id: toAgentId,
|
|
2064
|
+
sender_encryption_public_key: encrypted.sender_encryption_public_key,
|
|
2065
|
+
recipient_encryption_public_key: recipientKey,
|
|
2066
|
+
ciphertext: encrypted.ciphertext,
|
|
2067
|
+
nonce: encrypted.nonce,
|
|
2068
|
+
created_at: Date.now(),
|
|
2069
|
+
});
|
|
2070
|
+
this.ingestPrivateMessage(message);
|
|
2071
|
+
await this.persistPrivateMessages();
|
|
2072
|
+
await this.network.sendDirect(toPeerId, PRIVATE_MESSAGE_TOPIC, message);
|
|
2073
|
+
const view = this.getPrivateMessages(message.conversation_id).find((item) => item.message_id === message.message_id);
|
|
2074
|
+
return { sent: true, reason: "sent", message: view };
|
|
2075
|
+
}
|
|
2076
|
+
|
|
1657
2077
|
getOpenClawBridgeStatus(): OpenClawBridgeStatus {
|
|
2078
|
+
const now = Date.now();
|
|
2079
|
+
if (this.openclawBridgeStatusCache && this.openclawBridgeStatusCache.expiresAt > now) {
|
|
2080
|
+
return this.openclawBridgeStatusCache.value;
|
|
2081
|
+
}
|
|
1658
2082
|
const integration = this.getIntegrationStatus();
|
|
1659
2083
|
const openclawInstallation = detectOpenClawInstallation(this.projectRoot);
|
|
1660
|
-
const openclawRuntime =
|
|
2084
|
+
const openclawRuntime = this.getCachedOpenClawRuntime();
|
|
1661
2085
|
const skillInstallation = detectOpenClawSkillInstallation();
|
|
1662
2086
|
const ownerDelivery = detectOwnerDeliveryStatus({
|
|
1663
2087
|
workspaceRoot: this.projectRoot,
|
|
@@ -1665,7 +2089,7 @@ export class LocalNodeService {
|
|
|
1665
2089
|
openclawRunning: openclawRuntime.running,
|
|
1666
2090
|
skillInstalled: skillInstallation.installed,
|
|
1667
2091
|
});
|
|
1668
|
-
|
|
2092
|
+
const value: OpenClawBridgeStatus = {
|
|
1669
2093
|
enabled: this.socialConfig.enabled,
|
|
1670
2094
|
connected_to_silicaclaw: integration.connected_to_silicaclaw,
|
|
1671
2095
|
public_enabled: integration.public_enabled,
|
|
@@ -1721,6 +2145,11 @@ export class LocalNodeService {
|
|
|
1721
2145
|
install_skill: "/api/openclaw/bridge/skill-install",
|
|
1722
2146
|
},
|
|
1723
2147
|
};
|
|
2148
|
+
this.openclawBridgeStatusCache = {
|
|
2149
|
+
value,
|
|
2150
|
+
expiresAt: now + OPENCLAW_BRIDGE_STATUS_CACHE_MS,
|
|
2151
|
+
};
|
|
2152
|
+
return value;
|
|
1724
2153
|
}
|
|
1725
2154
|
|
|
1726
2155
|
async installOpenClawSkill(skillName?: string) {
|
|
@@ -1735,6 +2164,7 @@ export class LocalNodeService {
|
|
|
1735
2164
|
maxBuffer: 1024 * 1024,
|
|
1736
2165
|
});
|
|
1737
2166
|
const parsed = JSON.parse(String(stdout || "{}"));
|
|
2167
|
+
this.invalidateOpenClawCaches();
|
|
1738
2168
|
return {
|
|
1739
2169
|
...parsed,
|
|
1740
2170
|
bridge: this.getOpenClawBridgeStatus(),
|
|
@@ -1756,7 +2186,7 @@ export class LocalNodeService {
|
|
|
1756
2186
|
const workspaceSkillDir = resolve(homeDir, "workspace", "skills");
|
|
1757
2187
|
const legacySkillDir = resolve(homeDir, "skills");
|
|
1758
2188
|
const openclawSourceDir = defaultOpenClawSourceDir(this.projectRoot);
|
|
1759
|
-
const openclawRuntime =
|
|
2189
|
+
const openclawRuntime = this.getCachedOpenClawRuntime();
|
|
1760
2190
|
|
|
1761
2191
|
return {
|
|
1762
2192
|
bridge_api_base: DEFAULT_BRIDGE_API_BASE,
|
|
@@ -2166,15 +2596,14 @@ export class LocalNodeService {
|
|
|
2166
2596
|
profile: this.profile,
|
|
2167
2597
|
};
|
|
2168
2598
|
const presenceRecord = signPresence(this.identity, Date.now());
|
|
2169
|
-
const
|
|
2170
|
-
const replayMessages = this.getReplayableSelfSocialMessages();
|
|
2599
|
+
const shouldPublishProfile = this.shouldPublishProfileRecord(profileRecord, reason, presenceRecord.timestamp);
|
|
2600
|
+
const replayMessages = this.getReplayableSelfSocialMessages(reason);
|
|
2171
2601
|
|
|
2172
2602
|
try {
|
|
2173
|
-
|
|
2174
|
-
|
|
2175
|
-
for (const record of indexRecords) {
|
|
2176
|
-
await this.publish("index", record);
|
|
2603
|
+
if (shouldPublishProfile) {
|
|
2604
|
+
await this.publish("profile", profileRecord);
|
|
2177
2605
|
}
|
|
2606
|
+
await this.publish("presence", presenceRecord);
|
|
2178
2607
|
for (const message of replayMessages) {
|
|
2179
2608
|
await this.publish(SOCIAL_MESSAGE_TOPIC, message);
|
|
2180
2609
|
}
|
|
@@ -2183,7 +2612,9 @@ export class LocalNodeService {
|
|
|
2183
2612
|
this.lastBroadcastErrorAt = Date.now();
|
|
2184
2613
|
this.lastBroadcastError = message;
|
|
2185
2614
|
this.broadcastFailureCount += 1;
|
|
2615
|
+
this.consecutiveBroadcastFailures += 1;
|
|
2186
2616
|
await this.log("error", `Broadcast failed (reason=${reason}): ${message}`);
|
|
2617
|
+
await this.maybeRecoverFromBroadcastFailure(reason, message);
|
|
2187
2618
|
return { sent: false, reason: "publish_failed", error: message };
|
|
2188
2619
|
}
|
|
2189
2620
|
|
|
@@ -2191,22 +2622,75 @@ export class LocalNodeService {
|
|
|
2191
2622
|
this.broadcastCount += 1;
|
|
2192
2623
|
this.lastBroadcastError = null;
|
|
2193
2624
|
this.lastBroadcastErrorAt = 0;
|
|
2625
|
+
this.consecutiveBroadcastFailures = 0;
|
|
2194
2626
|
|
|
2195
2627
|
this.directory = ingestProfileRecord(this.directory, profileRecord);
|
|
2196
2628
|
this.directory = ingestPresenceRecord(this.directory, presenceRecord);
|
|
2197
|
-
for (const record of indexRecords) {
|
|
2198
|
-
this.directory = ingestIndexRecord(this.directory, record);
|
|
2199
|
-
}
|
|
2200
2629
|
this.compactCacheInMemory();
|
|
2201
2630
|
await this.persistCache();
|
|
2202
2631
|
|
|
2203
2632
|
await this.log(
|
|
2204
2633
|
"info",
|
|
2205
|
-
`Broadcast sent (${
|
|
2634
|
+
`Broadcast sent (${shouldPublishProfile ? "profile + " : ""}presence, replayed_messages=${replayMessages.length}, reason=${reason})`
|
|
2206
2635
|
);
|
|
2207
2636
|
return { sent: true, reason };
|
|
2208
2637
|
}
|
|
2209
2638
|
|
|
2639
|
+
private shouldPublishProfileRecord(
|
|
2640
|
+
profileRecord: SignedProfileRecord,
|
|
2641
|
+
reason: string,
|
|
2642
|
+
now = Date.now()
|
|
2643
|
+
): boolean {
|
|
2644
|
+
if (reason !== "interval") {
|
|
2645
|
+
this.lastProfileBroadcastSignature = profileRecord.profile.signature;
|
|
2646
|
+
this.lastProfileBroadcastAt = now;
|
|
2647
|
+
return true;
|
|
2648
|
+
}
|
|
2649
|
+
const signature = profileRecord.profile.signature;
|
|
2650
|
+
const changedSinceLastPublish = signature !== this.lastProfileBroadcastSignature;
|
|
2651
|
+
const refreshDue = now - this.lastProfileBroadcastAt >= PROFILE_RELAY_REFRESH_INTERVAL_MS;
|
|
2652
|
+
if (!changedSinceLastPublish && !refreshDue) {
|
|
2653
|
+
return false;
|
|
2654
|
+
}
|
|
2655
|
+
this.lastProfileBroadcastSignature = signature;
|
|
2656
|
+
this.lastProfileBroadcastAt = now;
|
|
2657
|
+
return true;
|
|
2658
|
+
}
|
|
2659
|
+
|
|
2660
|
+
private async maybeRecoverFromBroadcastFailure(reason: string, errorMessage: string): Promise<void> {
|
|
2661
|
+
const recoveryThreshold = 3;
|
|
2662
|
+
const recoveryCooldownMs = 60_000;
|
|
2663
|
+
if (this.broadcastRecoveryInFlight) {
|
|
2664
|
+
return;
|
|
2665
|
+
}
|
|
2666
|
+
if (this.consecutiveBroadcastFailures < recoveryThreshold) {
|
|
2667
|
+
return;
|
|
2668
|
+
}
|
|
2669
|
+
if (Date.now() - this.lastBroadcastRecoveryAttemptAt < recoveryCooldownMs) {
|
|
2670
|
+
return;
|
|
2671
|
+
}
|
|
2672
|
+
if (this.adapterMode !== "relay-preview" && this.adapterMode !== "webrtc-preview" && this.adapterMode !== "real-preview") {
|
|
2673
|
+
return;
|
|
2674
|
+
}
|
|
2675
|
+
|
|
2676
|
+
this.broadcastRecoveryInFlight = true;
|
|
2677
|
+
this.lastBroadcastRecoveryAttemptAt = Date.now();
|
|
2678
|
+
try {
|
|
2679
|
+
await this.log(
|
|
2680
|
+
"warn",
|
|
2681
|
+
`Broadcast recovery triggered after ${this.consecutiveBroadcastFailures} consecutive failures (${reason}): ${errorMessage}`
|
|
2682
|
+
);
|
|
2683
|
+
await this.restartNetworkAdapter("broadcast_failure_recovery");
|
|
2684
|
+
} catch (recoveryError) {
|
|
2685
|
+
await this.log(
|
|
2686
|
+
"error",
|
|
2687
|
+
`Broadcast recovery failed: ${recoveryError instanceof Error ? recoveryError.message : String(recoveryError)}`
|
|
2688
|
+
);
|
|
2689
|
+
} finally {
|
|
2690
|
+
this.broadcastRecoveryInFlight = false;
|
|
2691
|
+
}
|
|
2692
|
+
}
|
|
2693
|
+
|
|
2210
2694
|
private async hydrateFromDisk(): Promise<void> {
|
|
2211
2695
|
this.initState = {
|
|
2212
2696
|
identity_auto_created: false,
|
|
@@ -2236,6 +2720,8 @@ export class LocalNodeService {
|
|
|
2236
2720
|
await this.log("info", `Bound existing OpenClaw identity: ${resolvedIdentity.openclaw_source_path}`);
|
|
2237
2721
|
}
|
|
2238
2722
|
await this.identityRepo.set(this.identity);
|
|
2723
|
+
this.privateEncryptionKeyPair = (await this.privateEncryptionKeyRepo.get()) || createPrivateEncryptionKeyPair();
|
|
2724
|
+
await this.privateEncryptionKeyRepo.set(this.privateEncryptionKeyPair);
|
|
2239
2725
|
|
|
2240
2726
|
const existingProfile = await this.profileRepo.get();
|
|
2241
2727
|
const profileInput = resolveProfileInputWithSocial({
|
|
@@ -2244,7 +2730,10 @@ export class LocalNodeService {
|
|
|
2244
2730
|
existingProfile: existingProfile && existingProfile.agent_id === this.identity.agent_id ? existingProfile : null,
|
|
2245
2731
|
rootDir: this.projectRoot,
|
|
2246
2732
|
});
|
|
2247
|
-
this.profile = signProfile(
|
|
2733
|
+
this.profile = signProfile({
|
|
2734
|
+
...profileInput,
|
|
2735
|
+
private_encryption_public_key: this.privateEncryptionKeyPair?.public_key || profileInput.private_encryption_public_key || "",
|
|
2736
|
+
}, this.identity);
|
|
2248
2737
|
if (!existingProfile || existingProfile.agent_id !== this.identity.agent_id) {
|
|
2249
2738
|
this.initState.profile_auto_created = true;
|
|
2250
2739
|
await this.log("info", "profile.json missing/invalid, initialized from social/default profile");
|
|
@@ -2258,6 +2747,8 @@ export class LocalNodeService {
|
|
|
2258
2747
|
};
|
|
2259
2748
|
this.socialMessages = this.normalizeSocialMessages(await this.socialMessageRepo.get());
|
|
2260
2749
|
this.socialMessageObservations = this.normalizeSocialMessageObservations(await this.socialMessageObservationRepo.get());
|
|
2750
|
+
this.privateMessages = this.normalizePrivateMessages(await this.privateMessageRepo.get());
|
|
2751
|
+
this.privateMessageReceipts = this.normalizePrivateMessageReceipts(await this.privateMessageReceiptRepo.get());
|
|
2261
2752
|
this.directory = ingestProfileRecord(this.directory, { type: "profile", profile: this.profile });
|
|
2262
2753
|
this.compactCacheInMemory();
|
|
2263
2754
|
await this.persistCache();
|
|
@@ -2276,7 +2767,10 @@ export class LocalNodeService {
|
|
|
2276
2767
|
existingProfile: this.profile,
|
|
2277
2768
|
rootDir: this.projectRoot,
|
|
2278
2769
|
});
|
|
2279
|
-
const nextProfile = signProfile(
|
|
2770
|
+
const nextProfile = signProfile({
|
|
2771
|
+
...nextProfileInput,
|
|
2772
|
+
private_encryption_public_key: this.privateEncryptionKeyPair?.public_key || nextProfileInput.private_encryption_public_key || "",
|
|
2773
|
+
}, this.identity);
|
|
2280
2774
|
this.profile = nextProfile;
|
|
2281
2775
|
await this.profileRepo.set(nextProfile);
|
|
2282
2776
|
|
|
@@ -2341,7 +2835,8 @@ export class LocalNodeService {
|
|
|
2341
2835
|
|
|
2342
2836
|
private async onMessage(
|
|
2343
2837
|
topic: "profile" | "presence" | "index" | "social.message" | "social.message.observation",
|
|
2344
|
-
data: unknown
|
|
2838
|
+
data: unknown,
|
|
2839
|
+
meta?: { peerId?: string }
|
|
2345
2840
|
): Promise<void> {
|
|
2346
2841
|
this.receivedCount += 1;
|
|
2347
2842
|
this.receivedByTopic[topic] = (this.receivedByTopic[topic] ?? 0) + 1;
|
|
@@ -2359,6 +2854,9 @@ export class LocalNodeService {
|
|
|
2359
2854
|
return;
|
|
2360
2855
|
}
|
|
2361
2856
|
}
|
|
2857
|
+
if (meta?.peerId && record.profile.agent_id) {
|
|
2858
|
+
this.privatePeerRoutes[record.profile.agent_id] = meta.peerId;
|
|
2859
|
+
}
|
|
2362
2860
|
|
|
2363
2861
|
this.directory = ingestProfileRecord(this.directory, record);
|
|
2364
2862
|
this.compactCacheInMemory();
|
|
@@ -2378,6 +2876,9 @@ export class LocalNodeService {
|
|
|
2378
2876
|
return;
|
|
2379
2877
|
}
|
|
2380
2878
|
}
|
|
2879
|
+
if (meta?.peerId && record.agent_id) {
|
|
2880
|
+
this.privatePeerRoutes[record.agent_id] = meta.peerId;
|
|
2881
|
+
}
|
|
2381
2882
|
|
|
2382
2883
|
this.directory = ingestPresenceRecord(this.directory, record);
|
|
2383
2884
|
this.compactCacheInMemory();
|
|
@@ -2394,6 +2895,9 @@ export class LocalNodeService {
|
|
|
2394
2895
|
await this.log("warn", `Rejected social message with invalid signature (${record.message_id.slice(0, 10)})`);
|
|
2395
2896
|
return;
|
|
2396
2897
|
}
|
|
2898
|
+
if (meta?.peerId && record.agent_id) {
|
|
2899
|
+
this.privatePeerRoutes[record.agent_id] = meta.peerId;
|
|
2900
|
+
}
|
|
2397
2901
|
if (this.hasSocialMessage(record.message_id)) {
|
|
2398
2902
|
await this.publishObservationForMessage(record);
|
|
2399
2903
|
return;
|
|
@@ -2432,6 +2936,36 @@ export class LocalNodeService {
|
|
|
2432
2936
|
await this.persistCache();
|
|
2433
2937
|
}
|
|
2434
2938
|
|
|
2939
|
+
private async onDirectMessage(
|
|
2940
|
+
topic: "private.message" | "private.message.receipt",
|
|
2941
|
+
data: unknown,
|
|
2942
|
+
meta?: { peerId?: string }
|
|
2943
|
+
): Promise<void> {
|
|
2944
|
+
if (topic === PRIVATE_MESSAGE_TOPIC) {
|
|
2945
|
+
const record = this.normalizeIncomingPrivateMessage(data);
|
|
2946
|
+
if (!record || !verifyPrivateMessage(record)) {
|
|
2947
|
+
return;
|
|
2948
|
+
}
|
|
2949
|
+
if (record.to_agent_id !== this.identity?.agent_id || this.hasPrivateMessage(record.message_id)) {
|
|
2950
|
+
return;
|
|
2951
|
+
}
|
|
2952
|
+
this.ingestPrivateMessage(record);
|
|
2953
|
+
await this.persistPrivateMessages();
|
|
2954
|
+
await this.sendPrivateMessageReceipt(record, meta?.peerId);
|
|
2955
|
+
return;
|
|
2956
|
+
}
|
|
2957
|
+
|
|
2958
|
+
const receipt = this.normalizeIncomingPrivateMessageReceipt(data);
|
|
2959
|
+
if (!receipt || !verifyPrivateMessageReceipt(receipt)) {
|
|
2960
|
+
return;
|
|
2961
|
+
}
|
|
2962
|
+
if (receipt.to_agent_id !== this.identity?.agent_id) {
|
|
2963
|
+
return;
|
|
2964
|
+
}
|
|
2965
|
+
this.ingestPrivateMessageReceipt(receipt);
|
|
2966
|
+
await this.persistPrivateMessageReceipts();
|
|
2967
|
+
}
|
|
2968
|
+
|
|
2435
2969
|
private startBroadcastLoop(): void {
|
|
2436
2970
|
if (this.broadcaster) {
|
|
2437
2971
|
clearInterval(this.broadcaster);
|
|
@@ -2457,21 +2991,29 @@ export class LocalNodeService {
|
|
|
2457
2991
|
if (this.subscriptionsBound) {
|
|
2458
2992
|
return;
|
|
2459
2993
|
}
|
|
2460
|
-
this.network.subscribe("profile", (data: SignedProfileRecord) => {
|
|
2461
|
-
this.onMessage("profile", data);
|
|
2994
|
+
this.network.subscribe("profile", (data: SignedProfileRecord, meta?: { peerId?: string }) => {
|
|
2995
|
+
this.onMessage("profile", data, meta);
|
|
2462
2996
|
});
|
|
2463
|
-
this.network.subscribe("presence", (data: PresenceRecord) => {
|
|
2464
|
-
this.onMessage("presence", data);
|
|
2997
|
+
this.network.subscribe("presence", (data: PresenceRecord, meta?: { peerId?: string }) => {
|
|
2998
|
+
this.onMessage("presence", data, meta);
|
|
2465
2999
|
});
|
|
2466
|
-
this.network.subscribe("index", (data: IndexRefRecord) => {
|
|
2467
|
-
this.onMessage("index", data);
|
|
3000
|
+
this.network.subscribe("index", (data: IndexRefRecord, meta?: { peerId?: string }) => {
|
|
3001
|
+
this.onMessage("index", data, meta);
|
|
2468
3002
|
});
|
|
2469
|
-
this.network.subscribe(SOCIAL_MESSAGE_TOPIC, (data: SocialMessageRecord) => {
|
|
2470
|
-
this.onMessage(SOCIAL_MESSAGE_TOPIC, data);
|
|
3003
|
+
this.network.subscribe(SOCIAL_MESSAGE_TOPIC, (data: SocialMessageRecord, meta?: { peerId?: string }) => {
|
|
3004
|
+
this.onMessage(SOCIAL_MESSAGE_TOPIC, data, meta);
|
|
2471
3005
|
});
|
|
2472
|
-
this.network.subscribe(SOCIAL_MESSAGE_OBSERVATION_TOPIC, (data: SocialMessageObservationRecord) => {
|
|
2473
|
-
this.onMessage(SOCIAL_MESSAGE_OBSERVATION_TOPIC, data);
|
|
3006
|
+
this.network.subscribe(SOCIAL_MESSAGE_OBSERVATION_TOPIC, (data: SocialMessageObservationRecord, meta?: { peerId?: string }) => {
|
|
3007
|
+
this.onMessage(SOCIAL_MESSAGE_OBSERVATION_TOPIC, data, meta);
|
|
2474
3008
|
});
|
|
3009
|
+
if (typeof this.network.subscribeDirect === "function") {
|
|
3010
|
+
this.network.subscribeDirect(PRIVATE_MESSAGE_TOPIC, (data: PrivateMessageRecord, meta?: { peerId?: string }) => {
|
|
3011
|
+
this.onDirectMessage(PRIVATE_MESSAGE_TOPIC, data, meta);
|
|
3012
|
+
});
|
|
3013
|
+
this.network.subscribeDirect(PRIVATE_MESSAGE_RECEIPT_TOPIC, (data: PrivateMessageReceiptRecord, meta?: { peerId?: string }) => {
|
|
3014
|
+
this.onDirectMessage(PRIVATE_MESSAGE_RECEIPT_TOPIC, data, meta);
|
|
3015
|
+
});
|
|
3016
|
+
}
|
|
2475
3017
|
this.subscriptionsBound = true;
|
|
2476
3018
|
}
|
|
2477
3019
|
|
|
@@ -2628,9 +3170,66 @@ export class LocalNodeService {
|
|
|
2628
3170
|
this.networkReconnectDelayMs = Math.min(30_000, Math.max(5_000, Math.floor(delayMs * 1.5)));
|
|
2629
3171
|
}
|
|
2630
3172
|
|
|
3173
|
+
private pruneRemoteProfilesInMemory(now = Date.now()): number {
|
|
3174
|
+
if (!Number.isFinite(DIRECTORY_REMOTE_PROFILE_SOFT_LIMIT) || DIRECTORY_REMOTE_PROFILE_SOFT_LIMIT <= 0) {
|
|
3175
|
+
return 0;
|
|
3176
|
+
}
|
|
3177
|
+
const selfAgentId = this.profile?.agent_id || this.identity?.agent_id || "";
|
|
3178
|
+
const remoteProfiles = Object.values(this.directory.profiles).filter((profile) => profile.agent_id !== selfAgentId);
|
|
3179
|
+
if (remoteProfiles.length <= DIRECTORY_REMOTE_PROFILE_SOFT_LIMIT) {
|
|
3180
|
+
return 0;
|
|
3181
|
+
}
|
|
3182
|
+
|
|
3183
|
+
const onlineRemoteProfiles = remoteProfiles.filter((profile) =>
|
|
3184
|
+
isAgentOnline(this.directory.presence[profile.agent_id], now, PRESENCE_TTL_MS)
|
|
3185
|
+
);
|
|
3186
|
+
const offlineRemoteProfiles = remoteProfiles
|
|
3187
|
+
.filter((profile) => !isAgentOnline(this.directory.presence[profile.agent_id], now, PRESENCE_TTL_MS))
|
|
3188
|
+
.sort((a, b) => (b.updated_at || 0) - (a.updated_at || 0));
|
|
3189
|
+
|
|
3190
|
+
const keepOfflineCount = Math.max(0, DIRECTORY_REMOTE_PROFILE_SOFT_LIMIT - onlineRemoteProfiles.length);
|
|
3191
|
+
const keptRemoteProfiles = [
|
|
3192
|
+
...onlineRemoteProfiles,
|
|
3193
|
+
...offlineRemoteProfiles.slice(0, keepOfflineCount),
|
|
3194
|
+
];
|
|
3195
|
+
const keptRemoteIds = new Set(keptRemoteProfiles.map((profile) => profile.agent_id));
|
|
3196
|
+
const removedIds = remoteProfiles
|
|
3197
|
+
.map((profile) => profile.agent_id)
|
|
3198
|
+
.filter((agentId) => !keptRemoteIds.has(agentId));
|
|
3199
|
+
if (removedIds.length === 0) {
|
|
3200
|
+
return 0;
|
|
3201
|
+
}
|
|
3202
|
+
|
|
3203
|
+
const next = createEmptyDirectoryState();
|
|
3204
|
+
const selfProfile = selfAgentId ? this.directory.profiles[selfAgentId] : null;
|
|
3205
|
+
if (selfProfile) {
|
|
3206
|
+
next.profiles[selfAgentId] = selfProfile;
|
|
3207
|
+
const selfPresence = this.directory.presence[selfAgentId];
|
|
3208
|
+
if (typeof selfPresence === "number" && Number.isFinite(selfPresence)) {
|
|
3209
|
+
next.presence[selfAgentId] = selfPresence;
|
|
3210
|
+
}
|
|
3211
|
+
const rebuilt = rebuildIndexForProfile(next, selfProfile);
|
|
3212
|
+
next.index = rebuilt.index;
|
|
3213
|
+
}
|
|
3214
|
+
|
|
3215
|
+
for (const profile of keptRemoteProfiles) {
|
|
3216
|
+
next.profiles[profile.agent_id] = profile;
|
|
3217
|
+
const seenAt = this.directory.presence[profile.agent_id];
|
|
3218
|
+
if (typeof seenAt === "number" && Number.isFinite(seenAt)) {
|
|
3219
|
+
next.presence[profile.agent_id] = seenAt;
|
|
3220
|
+
}
|
|
3221
|
+
const rebuilt = rebuildIndexForProfile(next, profile);
|
|
3222
|
+
next.index = rebuilt.index;
|
|
3223
|
+
}
|
|
3224
|
+
|
|
3225
|
+
this.directory = dedupeIndex(next);
|
|
3226
|
+
return removedIds.length;
|
|
3227
|
+
}
|
|
3228
|
+
|
|
2631
3229
|
private compactCacheInMemory(): number {
|
|
2632
3230
|
const cleaned = cleanupExpiredPresence(this.directory, Date.now(), PRESENCE_TTL_MS);
|
|
2633
3231
|
this.directory = dedupeIndex(cleaned.state);
|
|
3232
|
+
this.pruneRemoteProfilesInMemory();
|
|
2634
3233
|
return cleaned.removed;
|
|
2635
3234
|
}
|
|
2636
3235
|
|
|
@@ -2666,6 +3265,57 @@ export class LocalNodeService {
|
|
|
2666
3265
|
await this.socialMessageObservationRepo.set(this.socialMessageObservations);
|
|
2667
3266
|
}
|
|
2668
3267
|
|
|
3268
|
+
private async persistPrivateMessages(): Promise<void> {
|
|
3269
|
+
this.privateMessagesPersistDirty = true;
|
|
3270
|
+
if (this.privateMessagesPersistTimer) {
|
|
3271
|
+
return;
|
|
3272
|
+
}
|
|
3273
|
+
this.privateMessagesPersistTimer = setTimeout(() => {
|
|
3274
|
+
this.flushPrivateMessagesPersist().catch(() => {});
|
|
3275
|
+
}, PRIVATE_MESSAGE_PERSIST_DEBOUNCE_MS);
|
|
3276
|
+
}
|
|
3277
|
+
|
|
3278
|
+
private async persistPrivateMessageReceipts(): Promise<void> {
|
|
3279
|
+
this.privateMessageReceiptsPersistDirty = true;
|
|
3280
|
+
if (this.privateMessageReceiptsPersistTimer) {
|
|
3281
|
+
return;
|
|
3282
|
+
}
|
|
3283
|
+
this.privateMessageReceiptsPersistTimer = setTimeout(() => {
|
|
3284
|
+
this.flushPrivateMessageReceiptsPersist().catch(() => {});
|
|
3285
|
+
}, PRIVATE_MESSAGE_PERSIST_DEBOUNCE_MS);
|
|
3286
|
+
}
|
|
3287
|
+
|
|
3288
|
+
private async flushPrivatePersistence(): Promise<void> {
|
|
3289
|
+
await Promise.all([
|
|
3290
|
+
this.flushPrivateMessagesPersist(),
|
|
3291
|
+
this.flushPrivateMessageReceiptsPersist(),
|
|
3292
|
+
]);
|
|
3293
|
+
}
|
|
3294
|
+
|
|
3295
|
+
private async flushPrivateMessagesPersist(): Promise<void> {
|
|
3296
|
+
if (this.privateMessagesPersistTimer) {
|
|
3297
|
+
clearTimeout(this.privateMessagesPersistTimer);
|
|
3298
|
+
this.privateMessagesPersistTimer = null;
|
|
3299
|
+
}
|
|
3300
|
+
if (!this.privateMessagesPersistDirty) {
|
|
3301
|
+
return;
|
|
3302
|
+
}
|
|
3303
|
+
this.privateMessagesPersistDirty = false;
|
|
3304
|
+
await this.privateMessageRepo.set(this.privateMessages);
|
|
3305
|
+
}
|
|
3306
|
+
|
|
3307
|
+
private async flushPrivateMessageReceiptsPersist(): Promise<void> {
|
|
3308
|
+
if (this.privateMessageReceiptsPersistTimer) {
|
|
3309
|
+
clearTimeout(this.privateMessageReceiptsPersistTimer);
|
|
3310
|
+
this.privateMessageReceiptsPersistTimer = null;
|
|
3311
|
+
}
|
|
3312
|
+
if (!this.privateMessageReceiptsPersistDirty) {
|
|
3313
|
+
return;
|
|
3314
|
+
}
|
|
3315
|
+
this.privateMessageReceiptsPersistDirty = false;
|
|
3316
|
+
await this.privateMessageReceiptRepo.set(this.privateMessageReceipts);
|
|
3317
|
+
}
|
|
3318
|
+
|
|
2669
3319
|
private async log(level: "info" | "warn" | "error", message: string): Promise<void> {
|
|
2670
3320
|
await this.logRepo.append({
|
|
2671
3321
|
level,
|
|
@@ -2722,6 +3372,7 @@ export class LocalNodeService {
|
|
|
2722
3372
|
|
|
2723
3373
|
return buildPublicProfileSummary({
|
|
2724
3374
|
profile,
|
|
3375
|
+
is_self: isSelf,
|
|
2725
3376
|
online,
|
|
2726
3377
|
last_seen_at: lastSeenAt || null,
|
|
2727
3378
|
network_mode: isSelf ? this.networkMode : "unknown",
|
|
@@ -2775,6 +3426,7 @@ export class LocalNodeService {
|
|
|
2775
3426
|
updated_at: message.created_at,
|
|
2776
3427
|
signature: "",
|
|
2777
3428
|
},
|
|
3429
|
+
is_self: message.agent_id === this.identity?.agent_id,
|
|
2778
3430
|
online: false,
|
|
2779
3431
|
last_seen_at: null,
|
|
2780
3432
|
network_mode: "unknown",
|
|
@@ -2980,6 +3632,34 @@ export class LocalNodeService {
|
|
|
2980
3632
|
.trim();
|
|
2981
3633
|
}
|
|
2982
3634
|
|
|
3635
|
+
private buildPrivateConversationId(leftAgentId: string, rightAgentId: string): string {
|
|
3636
|
+
return [String(leftAgentId || "").trim(), String(rightAgentId || "").trim()].sort().join(":");
|
|
3637
|
+
}
|
|
3638
|
+
|
|
3639
|
+
private decryptPrivateMessageBody(message: PrivateMessageRecord): string {
|
|
3640
|
+
const cached = this.privateMessageBodyCache.get(message.message_id);
|
|
3641
|
+
if (typeof cached === "string") {
|
|
3642
|
+
return cached;
|
|
3643
|
+
}
|
|
3644
|
+
if (!this.privateEncryptionKeyPair) {
|
|
3645
|
+
return "[encrypted]";
|
|
3646
|
+
}
|
|
3647
|
+
const decrypted = decryptPrivatePayload({
|
|
3648
|
+
ciphertext: message.ciphertext,
|
|
3649
|
+
nonce: message.nonce,
|
|
3650
|
+
sender_encryption_public_key: message.sender_encryption_public_key,
|
|
3651
|
+
recipient_private_key: this.privateEncryptionKeyPair.private_key,
|
|
3652
|
+
}) || "[encrypted]";
|
|
3653
|
+
this.privateMessageBodyCache.set(message.message_id, decrypted);
|
|
3654
|
+
if (this.privateMessageBodyCache.size > PRIVATE_MESSAGE_HISTORY_LIMIT * 2) {
|
|
3655
|
+
const firstKey = this.privateMessageBodyCache.keys().next().value;
|
|
3656
|
+
if (firstKey) {
|
|
3657
|
+
this.privateMessageBodyCache.delete(firstKey);
|
|
3658
|
+
}
|
|
3659
|
+
}
|
|
3660
|
+
return decrypted;
|
|
3661
|
+
}
|
|
3662
|
+
|
|
2983
3663
|
private normalizeWindowTimestamps(timestamps: number[], windowMs: number, now = Date.now()): number[] {
|
|
2984
3664
|
return timestamps.filter((timestamp) => now - timestamp <= windowMs);
|
|
2985
3665
|
}
|
|
@@ -3005,18 +3685,32 @@ export class LocalNodeService {
|
|
|
3005
3685
|
return this.socialMessages.some((item) => item.message_id === messageId);
|
|
3006
3686
|
}
|
|
3007
3687
|
|
|
3008
|
-
private getReplayableSelfSocialMessages(now = Date.now()): SocialMessageRecord[] {
|
|
3688
|
+
private getReplayableSelfSocialMessages(reason = "manual", now = Date.now()): SocialMessageRecord[] {
|
|
3009
3689
|
const maxCount = Math.max(0, SOCIAL_MESSAGE_REPLAY_MAX_PER_BROADCAST);
|
|
3010
3690
|
if (!this.identity || maxCount === 0) {
|
|
3011
3691
|
return [];
|
|
3012
3692
|
}
|
|
3013
|
-
|
|
3693
|
+
const replayable = this.socialMessages
|
|
3014
3694
|
.filter((item) => (
|
|
3015
3695
|
item.agent_id === this.identity?.agent_id &&
|
|
3016
3696
|
now - item.created_at <= SOCIAL_MESSAGE_REPLAY_WINDOW_MS
|
|
3017
3697
|
))
|
|
3018
3698
|
.sort((a, b) => a.created_at - b.created_at)
|
|
3019
3699
|
.slice(-maxCount);
|
|
3700
|
+
if (!replayable.length) {
|
|
3701
|
+
this.lastReplayBroadcastSignature = "";
|
|
3702
|
+
return [];
|
|
3703
|
+
}
|
|
3704
|
+
const signature = replayable.map((item) => item.message_id).join(",");
|
|
3705
|
+
const isIntervalReplay = reason === "interval";
|
|
3706
|
+
const changedSinceLastReplay = signature !== this.lastReplayBroadcastSignature;
|
|
3707
|
+
const refreshDue = now - this.lastReplayBroadcastAt >= SOCIAL_MESSAGE_REPLAY_REFRESH_INTERVAL_MS;
|
|
3708
|
+
if (isIntervalReplay && !changedSinceLastReplay && !refreshDue) {
|
|
3709
|
+
return [];
|
|
3710
|
+
}
|
|
3711
|
+
this.lastReplayBroadcastSignature = signature;
|
|
3712
|
+
this.lastReplayBroadcastAt = now;
|
|
3713
|
+
return replayable;
|
|
3020
3714
|
}
|
|
3021
3715
|
|
|
3022
3716
|
private hasRecentDuplicateMessage(agentId: string, body: string, topic: string, now = Date.now()): boolean {
|
|
@@ -3091,6 +3785,177 @@ export class LocalNodeService {
|
|
|
3091
3785
|
await this.persistSocialMessageObservations();
|
|
3092
3786
|
}
|
|
3093
3787
|
|
|
3788
|
+
private async sendPrivateMessageReceipt(message: PrivateMessageRecord, replyPeerId?: string): Promise<void> {
|
|
3789
|
+
if (!this.identity || typeof this.network.sendDirect !== "function" || !replyPeerId) {
|
|
3790
|
+
return;
|
|
3791
|
+
}
|
|
3792
|
+
const receipt = signPrivateMessageReceipt({
|
|
3793
|
+
identity: this.identity,
|
|
3794
|
+
receipt_id: createHash("sha256").update(`${message.message_id}:${this.identity.agent_id}:${Date.now()}`, "utf8").digest("hex"),
|
|
3795
|
+
message_id: message.message_id,
|
|
3796
|
+
conversation_id: message.conversation_id,
|
|
3797
|
+
to_agent_id: message.from_agent_id,
|
|
3798
|
+
status: "received",
|
|
3799
|
+
created_at: Date.now(),
|
|
3800
|
+
});
|
|
3801
|
+
this.ingestPrivateMessageReceipt(receipt);
|
|
3802
|
+
await this.network.sendDirect(replyPeerId, PRIVATE_MESSAGE_RECEIPT_TOPIC, receipt);
|
|
3803
|
+
await this.persistPrivateMessageReceipts();
|
|
3804
|
+
}
|
|
3805
|
+
|
|
3806
|
+
private normalizeIncomingPrivateMessage(value: unknown): PrivateMessageRecord | null {
|
|
3807
|
+
if (typeof value !== "object" || value === null) {
|
|
3808
|
+
return null;
|
|
3809
|
+
}
|
|
3810
|
+
const record = value as Partial<PrivateMessageRecord>;
|
|
3811
|
+
const createdAt = Number(record.created_at || 0);
|
|
3812
|
+
const fromAgentId = String(record.from_agent_id || "").trim();
|
|
3813
|
+
const toAgentId = String(record.to_agent_id || "").trim();
|
|
3814
|
+
const conversationId = String(record.conversation_id || "").trim();
|
|
3815
|
+
if (
|
|
3816
|
+
record.type !== PRIVATE_MESSAGE_TOPIC ||
|
|
3817
|
+
!String(record.message_id || "").trim() ||
|
|
3818
|
+
!conversationId ||
|
|
3819
|
+
!fromAgentId ||
|
|
3820
|
+
!toAgentId ||
|
|
3821
|
+
!String(record.sender_public_key || "").trim() ||
|
|
3822
|
+
!String(record.sender_encryption_public_key || "").trim() ||
|
|
3823
|
+
!String(record.recipient_encryption_public_key || "").trim() ||
|
|
3824
|
+
!String(record.ciphertext || "").trim() ||
|
|
3825
|
+
!String(record.nonce || "").trim() ||
|
|
3826
|
+
String(record.cipher_scheme || "") !== "nacl-box-v1" ||
|
|
3827
|
+
!String(record.signature || "").trim() ||
|
|
3828
|
+
!Number.isFinite(createdAt)
|
|
3829
|
+
) {
|
|
3830
|
+
return null;
|
|
3831
|
+
}
|
|
3832
|
+
if (fromAgentId === toAgentId) {
|
|
3833
|
+
return null;
|
|
3834
|
+
}
|
|
3835
|
+
if (conversationId !== this.buildPrivateConversationId(fromAgentId, toAgentId)) {
|
|
3836
|
+
return null;
|
|
3837
|
+
}
|
|
3838
|
+
return {
|
|
3839
|
+
type: PRIVATE_MESSAGE_TOPIC,
|
|
3840
|
+
message_id: String(record.message_id).trim(),
|
|
3841
|
+
conversation_id: conversationId,
|
|
3842
|
+
from_agent_id: fromAgentId,
|
|
3843
|
+
to_agent_id: toAgentId,
|
|
3844
|
+
sender_public_key: String(record.sender_public_key).trim(),
|
|
3845
|
+
sender_encryption_public_key: String(record.sender_encryption_public_key).trim(),
|
|
3846
|
+
recipient_encryption_public_key: String(record.recipient_encryption_public_key).trim(),
|
|
3847
|
+
cipher_scheme: "nacl-box-v1",
|
|
3848
|
+
ciphertext: String(record.ciphertext).trim(),
|
|
3849
|
+
nonce: String(record.nonce).trim(),
|
|
3850
|
+
created_at: createdAt,
|
|
3851
|
+
signature: String(record.signature).trim(),
|
|
3852
|
+
};
|
|
3853
|
+
}
|
|
3854
|
+
|
|
3855
|
+
private normalizePrivateMessages(items: unknown): PrivateMessageRecord[] {
|
|
3856
|
+
if (!Array.isArray(items)) {
|
|
3857
|
+
return [];
|
|
3858
|
+
}
|
|
3859
|
+
const deduped = new Set<string>();
|
|
3860
|
+
return items
|
|
3861
|
+
.map((item) => this.normalizeIncomingPrivateMessage(item))
|
|
3862
|
+
.filter((item): item is PrivateMessageRecord => Boolean(item))
|
|
3863
|
+
.sort((a, b) => a.created_at - b.created_at)
|
|
3864
|
+
.filter((item) => {
|
|
3865
|
+
if (deduped.has(item.message_id)) {
|
|
3866
|
+
return false;
|
|
3867
|
+
}
|
|
3868
|
+
deduped.add(item.message_id);
|
|
3869
|
+
return true;
|
|
3870
|
+
})
|
|
3871
|
+
.slice(-PRIVATE_MESSAGE_HISTORY_LIMIT);
|
|
3872
|
+
}
|
|
3873
|
+
|
|
3874
|
+
private normalizeIncomingPrivateMessageReceipt(value: unknown): PrivateMessageReceiptRecord | null {
|
|
3875
|
+
if (typeof value !== "object" || value === null) {
|
|
3876
|
+
return null;
|
|
3877
|
+
}
|
|
3878
|
+
const record = value as Partial<PrivateMessageReceiptRecord>;
|
|
3879
|
+
const createdAt = Number(record.created_at || 0);
|
|
3880
|
+
const status = String(record.status || "").trim();
|
|
3881
|
+
if (
|
|
3882
|
+
record.type !== PRIVATE_MESSAGE_RECEIPT_TOPIC ||
|
|
3883
|
+
!String(record.receipt_id || "").trim() ||
|
|
3884
|
+
!String(record.message_id || "").trim() ||
|
|
3885
|
+
!String(record.conversation_id || "").trim() ||
|
|
3886
|
+
!String(record.from_agent_id || "").trim() ||
|
|
3887
|
+
!String(record.to_agent_id || "").trim() ||
|
|
3888
|
+
!String(record.sender_public_key || "").trim() ||
|
|
3889
|
+
(status !== "received" && status !== "read") ||
|
|
3890
|
+
!String(record.signature || "").trim() ||
|
|
3891
|
+
!Number.isFinite(createdAt)
|
|
3892
|
+
) {
|
|
3893
|
+
return null;
|
|
3894
|
+
}
|
|
3895
|
+
return {
|
|
3896
|
+
type: PRIVATE_MESSAGE_RECEIPT_TOPIC,
|
|
3897
|
+
receipt_id: String(record.receipt_id).trim(),
|
|
3898
|
+
message_id: String(record.message_id).trim(),
|
|
3899
|
+
conversation_id: String(record.conversation_id).trim(),
|
|
3900
|
+
from_agent_id: String(record.from_agent_id).trim(),
|
|
3901
|
+
to_agent_id: String(record.to_agent_id).trim(),
|
|
3902
|
+
sender_public_key: String(record.sender_public_key).trim(),
|
|
3903
|
+
status: status as "received" | "read",
|
|
3904
|
+
created_at: createdAt,
|
|
3905
|
+
signature: String(record.signature).trim(),
|
|
3906
|
+
};
|
|
3907
|
+
}
|
|
3908
|
+
|
|
3909
|
+
private normalizePrivateMessageReceipts(items: unknown): PrivateMessageReceiptRecord[] {
|
|
3910
|
+
if (!Array.isArray(items)) {
|
|
3911
|
+
return [];
|
|
3912
|
+
}
|
|
3913
|
+
const deduped = new Set<string>();
|
|
3914
|
+
return items
|
|
3915
|
+
.map((item) => this.normalizeIncomingPrivateMessageReceipt(item))
|
|
3916
|
+
.filter((item): item is PrivateMessageReceiptRecord => Boolean(item))
|
|
3917
|
+
.sort((a, b) => a.created_at - b.created_at)
|
|
3918
|
+
.filter((item) => {
|
|
3919
|
+
if (deduped.has(item.receipt_id)) {
|
|
3920
|
+
return false;
|
|
3921
|
+
}
|
|
3922
|
+
deduped.add(item.receipt_id);
|
|
3923
|
+
return true;
|
|
3924
|
+
})
|
|
3925
|
+
.slice(-PRIVATE_MESSAGE_RECEIPT_HISTORY_LIMIT);
|
|
3926
|
+
}
|
|
3927
|
+
|
|
3928
|
+
private hasPrivateMessage(messageId: string): boolean {
|
|
3929
|
+
return this.privateMessages.some((item) => item.message_id === messageId);
|
|
3930
|
+
}
|
|
3931
|
+
|
|
3932
|
+
private ingestPrivateMessage(message: PrivateMessageRecord): void {
|
|
3933
|
+
const existing = this.privateMessages.findIndex((item) => item.message_id === message.message_id);
|
|
3934
|
+
if (existing >= 0) {
|
|
3935
|
+
this.privateMessages[existing] = message;
|
|
3936
|
+
} else {
|
|
3937
|
+
this.privateMessages.push(message);
|
|
3938
|
+
}
|
|
3939
|
+
this.privateMessages = this.normalizePrivateMessages(this.privateMessages);
|
|
3940
|
+
const validIds = new Set(this.privateMessages.map((item) => item.message_id));
|
|
3941
|
+
this.privateMessageBodyCache.delete(message.message_id);
|
|
3942
|
+
for (const key of Array.from(this.privateMessageBodyCache.keys())) {
|
|
3943
|
+
if (!validIds.has(key)) {
|
|
3944
|
+
this.privateMessageBodyCache.delete(key);
|
|
3945
|
+
}
|
|
3946
|
+
}
|
|
3947
|
+
}
|
|
3948
|
+
|
|
3949
|
+
private ingestPrivateMessageReceipt(receipt: PrivateMessageReceiptRecord): void {
|
|
3950
|
+
const existing = this.privateMessageReceipts.findIndex((item) => item.receipt_id === receipt.receipt_id);
|
|
3951
|
+
if (existing >= 0) {
|
|
3952
|
+
this.privateMessageReceipts[existing] = receipt;
|
|
3953
|
+
} else {
|
|
3954
|
+
this.privateMessageReceipts.push(receipt);
|
|
3955
|
+
}
|
|
3956
|
+
this.privateMessageReceipts = this.normalizePrivateMessageReceipts(this.privateMessageReceipts);
|
|
3957
|
+
}
|
|
3958
|
+
|
|
3094
3959
|
private normalizeIncomingSocialMessage(value: unknown): SocialMessageRecord | null {
|
|
3095
3960
|
if (typeof value !== "object" || value === null) {
|
|
3096
3961
|
return null;
|
|
@@ -3369,6 +4234,48 @@ export async function main() {
|
|
|
3369
4234
|
sendOk(res, node.getRuntimePaths());
|
|
3370
4235
|
});
|
|
3371
4236
|
|
|
4237
|
+
app.get("/api/app/update-status", (_req, res) => {
|
|
4238
|
+
sendOk(res, node.getAppUpdateStatus());
|
|
4239
|
+
});
|
|
4240
|
+
|
|
4241
|
+
app.post(
|
|
4242
|
+
"/api/app/update",
|
|
4243
|
+
asyncRoute(async (_req, res) => {
|
|
4244
|
+
const status = node.getAppUpdateStatus();
|
|
4245
|
+
if (!status.update_available || !status.latest_version) {
|
|
4246
|
+
sendOk(
|
|
4247
|
+
res,
|
|
4248
|
+
{
|
|
4249
|
+
started: false,
|
|
4250
|
+
current_version: status.current_version,
|
|
4251
|
+
latest_version: status.latest_version,
|
|
4252
|
+
platform: status.platform,
|
|
4253
|
+
reason: status.check_error || "already_current",
|
|
4254
|
+
},
|
|
4255
|
+
{ message: "Already on the latest version" }
|
|
4256
|
+
);
|
|
4257
|
+
return;
|
|
4258
|
+
}
|
|
4259
|
+
sendOk(
|
|
4260
|
+
res,
|
|
4261
|
+
{
|
|
4262
|
+
started: true,
|
|
4263
|
+
current_version: status.current_version,
|
|
4264
|
+
target_version: status.latest_version,
|
|
4265
|
+
platform: status.platform,
|
|
4266
|
+
},
|
|
4267
|
+
{ message: `Updating to ${status.latest_version}` }
|
|
4268
|
+
);
|
|
4269
|
+
setTimeout(() => {
|
|
4270
|
+
try {
|
|
4271
|
+
node.startAppUpdate();
|
|
4272
|
+
} catch {
|
|
4273
|
+
// best effort after response has been sent
|
|
4274
|
+
}
|
|
4275
|
+
}, 150);
|
|
4276
|
+
})
|
|
4277
|
+
);
|
|
4278
|
+
|
|
3372
4279
|
app.put(
|
|
3373
4280
|
"/api/profile",
|
|
3374
4281
|
asyncRoute(async (req, res) => {
|
|
@@ -3511,6 +4418,34 @@ export async function main() {
|
|
|
3511
4418
|
sendOk(res, node.getSocialMessages(limit, { agent_id: agentId || null }));
|
|
3512
4419
|
});
|
|
3513
4420
|
|
|
4421
|
+
app.get("/api/private/state", (_req, res) => {
|
|
4422
|
+
sendOk(res, node.getPrivateMessagingState());
|
|
4423
|
+
});
|
|
4424
|
+
|
|
4425
|
+
app.get("/api/private/conversations", (_req, res) => {
|
|
4426
|
+
sendOk(res, node.getPrivateConversations());
|
|
4427
|
+
});
|
|
4428
|
+
|
|
4429
|
+
app.get("/api/private/messages", (req, res) => {
|
|
4430
|
+
const conversationId = String(req.query.conversation_id ?? "").trim();
|
|
4431
|
+
const limit = Number(req.query.limit ?? PRIVATE_MESSAGE_QUERY_LIMIT);
|
|
4432
|
+
sendOk(res, node.getPrivateMessages(conversationId, limit));
|
|
4433
|
+
});
|
|
4434
|
+
|
|
4435
|
+
app.post(
|
|
4436
|
+
"/api/private/messages/send",
|
|
4437
|
+
asyncRoute(async (req, res) => {
|
|
4438
|
+
const result = await node.sendPrivateMessage({
|
|
4439
|
+
to_agent_id: String(req.body?.to_agent_id || ""),
|
|
4440
|
+
recipient_encryption_public_key: String(req.body?.recipient_encryption_public_key || ""),
|
|
4441
|
+
body: String(req.body?.body || ""),
|
|
4442
|
+
});
|
|
4443
|
+
sendOk(res, result, {
|
|
4444
|
+
message: result.sent ? "Private message sent" : `Private message skipped: ${result.reason}`,
|
|
4445
|
+
});
|
|
4446
|
+
})
|
|
4447
|
+
);
|
|
4448
|
+
|
|
3514
4449
|
app.get("/api/openclaw/bridge", (_req, res) => {
|
|
3515
4450
|
sendOk(res, node.getOpenClawBridgeStatus());
|
|
3516
4451
|
});
|