@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.
Files changed (145) hide show
  1. package/CHANGELOG.md +102 -0
  2. package/INSTALL.md +13 -7
  3. package/README.md +60 -12
  4. package/VERSION +1 -1
  5. package/apps/local-console/dist/apps/local-console/src/server.d.ts +139 -3
  6. package/apps/local-console/dist/apps/local-console/src/server.js +1018 -91
  7. package/apps/local-console/dist/packages/core/src/index.d.ts +2 -0
  8. package/apps/local-console/dist/packages/core/src/index.js +2 -0
  9. package/apps/local-console/dist/packages/core/src/privateCrypto.d.ts +17 -0
  10. package/apps/local-console/dist/packages/core/src/privateCrypto.js +40 -0
  11. package/apps/local-console/dist/packages/core/src/privateMessage.d.ts +23 -0
  12. package/apps/local-console/dist/packages/core/src/privateMessage.js +74 -0
  13. package/apps/local-console/dist/packages/core/src/profile.js +2 -0
  14. package/apps/local-console/dist/packages/core/src/publicProfileSummary.d.ts +4 -0
  15. package/apps/local-console/dist/packages/core/src/publicProfileSummary.js +3 -0
  16. package/apps/local-console/dist/packages/core/src/types.d.ts +40 -0
  17. package/apps/local-console/dist/packages/network/src/relayPreview.d.ts +12 -0
  18. package/apps/local-console/dist/packages/network/src/relayPreview.js +108 -8
  19. package/apps/local-console/dist/packages/network/src/types.d.ts +4 -0
  20. package/apps/local-console/dist/packages/storage/src/repos.d.ts +27 -1
  21. package/apps/local-console/dist/packages/storage/src/repos.js +35 -1
  22. package/apps/local-console/public/app/app.js +472 -11
  23. package/apps/local-console/public/app/events.js +21 -0
  24. package/apps/local-console/public/app/network.js +144 -32
  25. package/apps/local-console/public/app/overview.js +57 -27
  26. package/apps/local-console/public/app/social.js +342 -105
  27. package/apps/local-console/public/app/styles.css +149 -43
  28. package/apps/local-console/public/app/template.js +196 -100
  29. package/apps/local-console/public/app/translations.js +430 -316
  30. package/apps/local-console/src/server.ts +1164 -89
  31. package/apps/public-explorer/public/app/template.js +2 -2
  32. package/apps/public-explorer/public/app/translations.js +36 -36
  33. package/docs/NEW_USER_OPERATIONS.md +5 -5
  34. package/docs/OPENCLAW_BRIDGE.md +7 -7
  35. package/docs/OPENCLAW_BRIDGE_ZH.md +6 -6
  36. package/node_modules/@silicaclaw/core/dist/packages/core/src/index.d.ts +2 -0
  37. package/node_modules/@silicaclaw/core/dist/packages/core/src/index.js +2 -0
  38. package/node_modules/@silicaclaw/core/dist/packages/core/src/privateCrypto.d.ts +17 -0
  39. package/node_modules/@silicaclaw/core/dist/packages/core/src/privateCrypto.js +40 -0
  40. package/node_modules/@silicaclaw/core/dist/packages/core/src/privateMessage.d.ts +23 -0
  41. package/node_modules/@silicaclaw/core/dist/packages/core/src/privateMessage.js +74 -0
  42. package/node_modules/@silicaclaw/core/dist/packages/core/src/profile.js +2 -0
  43. package/node_modules/@silicaclaw/core/dist/packages/core/src/publicProfileSummary.d.ts +4 -0
  44. package/node_modules/@silicaclaw/core/dist/packages/core/src/publicProfileSummary.js +3 -0
  45. package/node_modules/@silicaclaw/core/dist/packages/core/src/types.d.ts +40 -0
  46. package/node_modules/@silicaclaw/core/package.json +2 -2
  47. package/node_modules/@silicaclaw/core/src/index.ts +2 -0
  48. package/node_modules/@silicaclaw/core/src/privateCrypto.ts +57 -0
  49. package/node_modules/@silicaclaw/core/src/privateMessage.ts +101 -0
  50. package/node_modules/@silicaclaw/core/src/profile.ts +2 -0
  51. package/node_modules/@silicaclaw/core/src/publicProfileSummary.ts +7 -0
  52. package/node_modules/@silicaclaw/core/src/types.ts +44 -0
  53. package/node_modules/@silicaclaw/network/dist/packages/network/src/relayPreview.d.ts +12 -0
  54. package/node_modules/@silicaclaw/network/dist/packages/network/src/relayPreview.js +108 -8
  55. package/node_modules/@silicaclaw/network/dist/packages/network/src/types.d.ts +4 -0
  56. package/node_modules/@silicaclaw/network/src/relayPreview.ts +120 -10
  57. package/node_modules/@silicaclaw/network/src/types.ts +2 -0
  58. package/node_modules/@silicaclaw/storage/dist/packages/core/src/index.d.ts +2 -0
  59. package/node_modules/@silicaclaw/storage/dist/packages/core/src/index.js +2 -0
  60. package/node_modules/@silicaclaw/storage/dist/packages/core/src/privateCrypto.d.ts +17 -0
  61. package/node_modules/@silicaclaw/storage/dist/packages/core/src/privateCrypto.js +40 -0
  62. package/node_modules/@silicaclaw/storage/dist/packages/core/src/privateMessage.d.ts +23 -0
  63. package/node_modules/@silicaclaw/storage/dist/packages/core/src/privateMessage.js +74 -0
  64. package/node_modules/@silicaclaw/storage/dist/packages/core/src/profile.js +2 -0
  65. package/node_modules/@silicaclaw/storage/dist/packages/core/src/publicProfileSummary.d.ts +4 -0
  66. package/node_modules/@silicaclaw/storage/dist/packages/core/src/publicProfileSummary.js +3 -0
  67. package/node_modules/@silicaclaw/storage/dist/packages/core/src/types.d.ts +40 -0
  68. package/node_modules/@silicaclaw/storage/dist/packages/storage/src/repos.d.ts +27 -1
  69. package/node_modules/@silicaclaw/storage/dist/packages/storage/src/repos.js +35 -1
  70. package/node_modules/@silicaclaw/storage/package.json +2 -2
  71. package/node_modules/@silicaclaw/storage/src/repos.ts +59 -1
  72. package/openclaw-skills/silicaclaw-bridge-setup/SKILL.md +18 -0
  73. package/openclaw-skills/silicaclaw-bridge-setup/VERSION +1 -1
  74. package/openclaw-skills/silicaclaw-bridge-setup/manifest.json +2 -2
  75. package/openclaw-skills/silicaclaw-broadcast/SKILL.md +18 -0
  76. package/openclaw-skills/silicaclaw-broadcast/VERSION +1 -1
  77. package/openclaw-skills/silicaclaw-broadcast/manifest.json +2 -2
  78. package/openclaw-skills/silicaclaw-network-config/SKILL.md +158 -0
  79. package/openclaw-skills/silicaclaw-network-config/VERSION +1 -0
  80. package/openclaw-skills/silicaclaw-network-config/agents/openai.yaml +6 -0
  81. package/openclaw-skills/silicaclaw-network-config/manifest.json +27 -0
  82. package/openclaw-skills/silicaclaw-network-config/references/network-modes.md +22 -0
  83. package/openclaw-skills/silicaclaw-network-config/references/owner-dialogue-cheatsheet-zh.md +47 -0
  84. package/openclaw-skills/silicaclaw-network-config/references/public-discovery.md +22 -0
  85. package/openclaw-skills/silicaclaw-owner-push/SKILL.md +18 -0
  86. package/openclaw-skills/silicaclaw-owner-push/VERSION +1 -1
  87. package/openclaw-skills/silicaclaw-owner-push/manifest.json +2 -2
  88. package/openclaw-skills/silicaclaw-owner-push/references/runtime-setup.md +3 -0
  89. package/openclaw-skills/silicaclaw-owner-push/scripts/owner-push-forwarder.mjs +151 -9
  90. package/package.json +1 -1
  91. package/packages/core/dist/packages/core/src/index.d.ts +2 -0
  92. package/packages/core/dist/packages/core/src/index.js +2 -0
  93. package/packages/core/dist/packages/core/src/privateCrypto.d.ts +17 -0
  94. package/packages/core/dist/packages/core/src/privateCrypto.js +40 -0
  95. package/packages/core/dist/packages/core/src/privateMessage.d.ts +23 -0
  96. package/packages/core/dist/packages/core/src/privateMessage.js +74 -0
  97. package/packages/core/dist/packages/core/src/profile.js +2 -0
  98. package/packages/core/dist/packages/core/src/publicProfileSummary.d.ts +4 -0
  99. package/packages/core/dist/packages/core/src/publicProfileSummary.js +3 -0
  100. package/packages/core/dist/packages/core/src/types.d.ts +40 -0
  101. package/packages/core/package.json +2 -2
  102. package/packages/core/src/index.ts +2 -0
  103. package/packages/core/src/privateCrypto.ts +57 -0
  104. package/packages/core/src/privateMessage.ts +101 -0
  105. package/packages/core/src/profile.ts +2 -0
  106. package/packages/core/src/publicProfileSummary.ts +7 -0
  107. package/packages/core/src/types.ts +44 -0
  108. package/packages/network/dist/packages/network/src/relayPreview.d.ts +12 -0
  109. package/packages/network/dist/packages/network/src/relayPreview.js +108 -8
  110. package/packages/network/dist/packages/network/src/types.d.ts +4 -0
  111. package/packages/network/src/relayPreview.ts +120 -10
  112. package/packages/network/src/types.ts +2 -0
  113. package/packages/storage/dist/packages/core/src/index.d.ts +2 -0
  114. package/packages/storage/dist/packages/core/src/index.js +2 -0
  115. package/packages/storage/dist/packages/core/src/privateCrypto.d.ts +17 -0
  116. package/packages/storage/dist/packages/core/src/privateCrypto.js +40 -0
  117. package/packages/storage/dist/packages/core/src/privateMessage.d.ts +23 -0
  118. package/packages/storage/dist/packages/core/src/privateMessage.js +74 -0
  119. package/packages/storage/dist/packages/core/src/profile.js +2 -0
  120. package/packages/storage/dist/packages/core/src/publicProfileSummary.d.ts +4 -0
  121. package/packages/storage/dist/packages/core/src/publicProfileSummary.js +3 -0
  122. package/packages/storage/dist/packages/core/src/types.d.ts +40 -0
  123. package/packages/storage/dist/packages/storage/src/repos.d.ts +27 -1
  124. package/packages/storage/dist/packages/storage/src/repos.js +35 -1
  125. package/packages/storage/package.json +2 -2
  126. package/packages/storage/src/repos.ts +59 -1
  127. package/scripts/silicaclaw-cli.mjs +4 -1
  128. package/scripts/silicaclaw-gateway.mjs +114 -2
  129. package/scripts/validate-openclaw-skill.mjs +19 -0
  130. package/node_modules/@silicaclaw/storage/dist/index.d.ts +0 -3
  131. package/node_modules/@silicaclaw/storage/dist/index.js +0 -19
  132. package/node_modules/@silicaclaw/storage/dist/jsonRepo.d.ts +0 -7
  133. package/node_modules/@silicaclaw/storage/dist/jsonRepo.js +0 -29
  134. package/node_modules/@silicaclaw/storage/dist/repos.d.ts +0 -61
  135. package/node_modules/@silicaclaw/storage/dist/repos.js +0 -67
  136. package/node_modules/@silicaclaw/storage/dist/socialRuntimeRepo.d.ts +0 -5
  137. package/node_modules/@silicaclaw/storage/dist/socialRuntimeRepo.js +0 -57
  138. package/packages/storage/dist/index.d.ts +0 -3
  139. package/packages/storage/dist/index.js +0 -19
  140. package/packages/storage/dist/jsonRepo.d.ts +0 -7
  141. package/packages/storage/dist/jsonRepo.js +0 -29
  142. package/packages/storage/dist/repos.d.ts +0 -61
  143. package/packages/storage/dist/repos.js +0 -67
  144. package/packages/storage/dist/socialRuntimeRepo.d.ts +0 -5
  145. 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 result = (0, child_process_1.spawnSync)("ps", ["-Ao", "pid=,ppid=,command="], {
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 stdout = String(result.stdout || "");
389
- const lines = stdout
390
- .split("\n")
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 combinedProcesses = new Map();
454
- for (const process of [...processes, ...gatewayListeners]) {
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(gatewayProbe.stderr || "lsof failed").trim());
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: processes.length > 0 && gatewayReachable
487
- ? "process+gateway"
488
- : gatewayReachable
489
- ? "gateway"
490
- : processes.length > 0
491
- ? "process"
492
- : "not_running",
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 = detectOpenClawRuntime(this.projectRoot);
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
- return {
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 = detectOpenClawRuntime(this.projectRoot);
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 indexRecords = (0, core_1.buildIndexRecords)(this.profile);
1849
- const replayMessages = this.getReplayableSelfSocialMessages();
2227
+ const shouldPublishProfile = this.shouldPublishProfileRecord(profileRecord, reason, presenceRecord.timestamp);
2228
+ const replayMessages = this.getReplayableSelfSocialMessages(reason);
1850
2229
  try {
1851
- await this.publish("profile", profileRecord);
1852
- await this.publish("presence", presenceRecord);
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 (${indexRecords.length} index refs, replayed_messages=${replayMessages.length}, reason=${reason})`);
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)(profileInput, this.identity);
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)(nextProfileInput, this.identity);
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
- return this.socialMessages
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
  });