@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.
Files changed (125) hide show
  1. package/CHANGELOG.md +54 -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 +129 -2
  6. package/apps/local-console/dist/apps/local-console/src/server.js +887 -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 +13 -1
  21. package/apps/local-console/dist/packages/storage/src/repos.js +19 -1
  22. package/apps/local-console/public/app/app.js +465 -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 +60 -52
  26. package/apps/local-console/public/app/social.js +316 -93
  27. package/apps/local-console/public/app/styles.css +127 -1
  28. package/apps/local-console/public/app/template.js +121 -35
  29. package/apps/local-console/public/app/translations.js +430 -316
  30. package/apps/local-console/src/server.ts +1024 -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/src/index.ts +2 -0
  47. package/node_modules/@silicaclaw/core/src/privateCrypto.ts +57 -0
  48. package/node_modules/@silicaclaw/core/src/privateMessage.ts +101 -0
  49. package/node_modules/@silicaclaw/core/src/profile.ts +2 -0
  50. package/node_modules/@silicaclaw/core/src/publicProfileSummary.ts +7 -0
  51. package/node_modules/@silicaclaw/core/src/types.ts +44 -0
  52. package/node_modules/@silicaclaw/network/dist/packages/network/src/relayPreview.d.ts +12 -0
  53. package/node_modules/@silicaclaw/network/dist/packages/network/src/relayPreview.js +108 -8
  54. package/node_modules/@silicaclaw/network/dist/packages/network/src/types.d.ts +4 -0
  55. package/node_modules/@silicaclaw/network/src/relayPreview.ts +120 -10
  56. package/node_modules/@silicaclaw/network/src/types.ts +2 -0
  57. package/node_modules/@silicaclaw/storage/dist/packages/core/src/index.d.ts +2 -0
  58. package/node_modules/@silicaclaw/storage/dist/packages/core/src/index.js +2 -0
  59. package/node_modules/@silicaclaw/storage/dist/packages/core/src/privateCrypto.d.ts +17 -0
  60. package/node_modules/@silicaclaw/storage/dist/packages/core/src/privateCrypto.js +40 -0
  61. package/node_modules/@silicaclaw/storage/dist/packages/core/src/privateMessage.d.ts +23 -0
  62. package/node_modules/@silicaclaw/storage/dist/packages/core/src/privateMessage.js +74 -0
  63. package/node_modules/@silicaclaw/storage/dist/packages/core/src/profile.js +2 -0
  64. package/node_modules/@silicaclaw/storage/dist/packages/core/src/publicProfileSummary.d.ts +4 -0
  65. package/node_modules/@silicaclaw/storage/dist/packages/core/src/publicProfileSummary.js +3 -0
  66. package/node_modules/@silicaclaw/storage/dist/packages/core/src/types.d.ts +40 -0
  67. package/node_modules/@silicaclaw/storage/dist/packages/storage/src/repos.d.ts +13 -1
  68. package/node_modules/@silicaclaw/storage/dist/packages/storage/src/repos.js +19 -1
  69. package/node_modules/@silicaclaw/storage/src/repos.ts +31 -1
  70. package/openclaw-skills/silicaclaw-bridge-setup/SKILL.md +18 -0
  71. package/openclaw-skills/silicaclaw-bridge-setup/VERSION +1 -1
  72. package/openclaw-skills/silicaclaw-bridge-setup/manifest.json +2 -2
  73. package/openclaw-skills/silicaclaw-broadcast/SKILL.md +18 -0
  74. package/openclaw-skills/silicaclaw-broadcast/VERSION +1 -1
  75. package/openclaw-skills/silicaclaw-broadcast/manifest.json +2 -2
  76. package/openclaw-skills/silicaclaw-network-config/SKILL.md +158 -0
  77. package/openclaw-skills/silicaclaw-network-config/VERSION +1 -0
  78. package/openclaw-skills/silicaclaw-network-config/agents/openai.yaml +6 -0
  79. package/openclaw-skills/silicaclaw-network-config/manifest.json +27 -0
  80. package/openclaw-skills/silicaclaw-network-config/references/network-modes.md +22 -0
  81. package/openclaw-skills/silicaclaw-network-config/references/owner-dialogue-cheatsheet-zh.md +47 -0
  82. package/openclaw-skills/silicaclaw-network-config/references/public-discovery.md +22 -0
  83. package/openclaw-skills/silicaclaw-owner-push/SKILL.md +18 -0
  84. package/openclaw-skills/silicaclaw-owner-push/VERSION +1 -1
  85. package/openclaw-skills/silicaclaw-owner-push/manifest.json +2 -2
  86. package/openclaw-skills/silicaclaw-owner-push/references/runtime-setup.md +3 -0
  87. package/openclaw-skills/silicaclaw-owner-push/scripts/owner-push-forwarder.mjs +151 -9
  88. package/package.json +1 -1
  89. package/packages/core/dist/packages/core/src/index.d.ts +2 -0
  90. package/packages/core/dist/packages/core/src/index.js +2 -0
  91. package/packages/core/dist/packages/core/src/privateCrypto.d.ts +17 -0
  92. package/packages/core/dist/packages/core/src/privateCrypto.js +40 -0
  93. package/packages/core/dist/packages/core/src/privateMessage.d.ts +23 -0
  94. package/packages/core/dist/packages/core/src/privateMessage.js +74 -0
  95. package/packages/core/dist/packages/core/src/profile.js +2 -0
  96. package/packages/core/dist/packages/core/src/publicProfileSummary.d.ts +4 -0
  97. package/packages/core/dist/packages/core/src/publicProfileSummary.js +3 -0
  98. package/packages/core/dist/packages/core/src/types.d.ts +40 -0
  99. package/packages/core/src/index.ts +2 -0
  100. package/packages/core/src/privateCrypto.ts +57 -0
  101. package/packages/core/src/privateMessage.ts +101 -0
  102. package/packages/core/src/profile.ts +2 -0
  103. package/packages/core/src/publicProfileSummary.ts +7 -0
  104. package/packages/core/src/types.ts +44 -0
  105. package/packages/network/dist/packages/network/src/relayPreview.d.ts +12 -0
  106. package/packages/network/dist/packages/network/src/relayPreview.js +108 -8
  107. package/packages/network/dist/packages/network/src/types.d.ts +4 -0
  108. package/packages/network/src/relayPreview.ts +120 -10
  109. package/packages/network/src/types.ts +2 -0
  110. package/packages/storage/dist/packages/core/src/index.d.ts +2 -0
  111. package/packages/storage/dist/packages/core/src/index.js +2 -0
  112. package/packages/storage/dist/packages/core/src/privateCrypto.d.ts +17 -0
  113. package/packages/storage/dist/packages/core/src/privateCrypto.js +40 -0
  114. package/packages/storage/dist/packages/core/src/privateMessage.d.ts +23 -0
  115. package/packages/storage/dist/packages/core/src/privateMessage.js +74 -0
  116. package/packages/storage/dist/packages/core/src/profile.js +2 -0
  117. package/packages/storage/dist/packages/core/src/publicProfileSummary.d.ts +4 -0
  118. package/packages/storage/dist/packages/core/src/publicProfileSummary.js +3 -0
  119. package/packages/storage/dist/packages/core/src/types.d.ts +40 -0
  120. package/packages/storage/dist/packages/storage/src/repos.d.ts +13 -1
  121. package/packages/storage/dist/packages/storage/src/repos.js +19 -1
  122. package/packages/storage/src/repos.ts +31 -1
  123. package/scripts/silicaclaw-cli.mjs +59 -6
  124. package/scripts/silicaclaw-gateway.mjs +108 -0
  125. package/scripts/validate-openclaw-skill.mjs +19 -0
@@ -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,39 @@ class LocalNodeService {
688
773
  socialMessageGovernanceRepo;
689
774
  socialMessageRepo;
690
775
  socialMessageObservationRepo;
776
+ privateMessageRepo;
777
+ privateMessageReceiptRepo;
778
+ privateEncryptionKeyRepo;
691
779
  socialRuntimeRepo;
692
780
  identity = null;
693
781
  profile = null;
694
782
  directory = (0, core_1.createEmptyDirectoryState)();
695
783
  socialMessages = [];
696
784
  socialMessageObservations = [];
785
+ privateMessages = [];
786
+ privateMessageReceipts = [];
787
+ privateEncryptionKeyPair = null;
788
+ privatePeerRoutes = {};
789
+ privateMessageBodyCache = new Map();
697
790
  messageGovernance;
791
+ privateMessagesPersistDirty = false;
792
+ privateMessageReceiptsPersistDirty = false;
793
+ privateMessagesPersistTimer = null;
794
+ privateMessageReceiptsPersistTimer = null;
698
795
  receivedCount = 0;
699
796
  broadcastCount = 0;
700
797
  lastMessageAt = 0;
701
798
  lastBroadcastAt = 0;
799
+ lastProfileBroadcastAt = 0;
800
+ lastProfileBroadcastSignature = "";
801
+ lastReplayBroadcastAt = 0;
802
+ lastReplayBroadcastSignature = "";
702
803
  lastBroadcastErrorAt = 0;
703
804
  lastBroadcastError = null;
704
805
  broadcastFailureCount = 0;
806
+ consecutiveBroadcastFailures = 0;
807
+ lastBroadcastRecoveryAttemptAt = 0;
808
+ broadcastRecoveryInFlight = false;
705
809
  broadcaster = null;
706
810
  subscriptionsBound = false;
707
811
  broadcastEnabled = true;
@@ -739,6 +843,8 @@ class LocalNodeService {
739
843
  networkReconnectTimer = null;
740
844
  networkReconnectDelayMs = 5_000;
741
845
  appVersion = "unknown";
846
+ openclawRuntimeCache = null;
847
+ openclawBridgeStatusCache = null;
742
848
  constructor(options) {
743
849
  this.workspaceRoot = options?.workspaceRoot || resolveWorkspaceRoot();
744
850
  this.projectRoot = options?.projectRoot || resolveProjectRoot(this.workspaceRoot);
@@ -752,6 +858,9 @@ class LocalNodeService {
752
858
  this.socialMessageGovernanceRepo = new storage_1.SocialMessageGovernanceRepo(this.storageRoot);
753
859
  this.socialMessageRepo = new storage_1.SocialMessageRepo(this.storageRoot);
754
860
  this.socialMessageObservationRepo = new storage_1.SocialMessageObservationRepo(this.storageRoot);
861
+ this.privateMessageRepo = new storage_1.PrivateMessageRepo(this.storageRoot);
862
+ this.privateMessageReceiptRepo = new storage_1.PrivateMessageReceiptRepo(this.storageRoot);
863
+ this.privateEncryptionKeyRepo = new storage_1.PrivateEncryptionKeyRepo(this.storageRoot);
755
864
  this.socialRuntimeRepo = new storage_1.SocialRuntimeRepo(this.storageRoot);
756
865
  this.messageGovernance = this.defaultMessageGovernance();
757
866
  let loadedSocial = (0, core_1.loadSocialConfig)(this.projectRoot);
@@ -779,6 +888,22 @@ class LocalNodeService {
779
888
  this.adapterMode = resolved.mode;
780
889
  this.networkPort = resolved.port;
781
890
  }
891
+ getCachedOpenClawRuntime() {
892
+ const now = Date.now();
893
+ if (this.openclawRuntimeCache && this.openclawRuntimeCache.expiresAt > now) {
894
+ return this.openclawRuntimeCache.value;
895
+ }
896
+ const value = detectOpenClawRuntime(this.projectRoot);
897
+ this.openclawRuntimeCache = {
898
+ value,
899
+ expiresAt: now + OPENCLAW_RUNTIME_CACHE_MS,
900
+ };
901
+ return value;
902
+ }
903
+ invalidateOpenClawCaches() {
904
+ this.openclawRuntimeCache = null;
905
+ this.openclawBridgeStatusCache = null;
906
+ }
782
907
  async start() {
783
908
  await this.hydrateFromDisk();
784
909
  this.bindNetworkSubscriptions();
@@ -790,6 +915,7 @@ class LocalNodeService {
790
915
  clearInterval(this.broadcaster);
791
916
  this.broadcaster = null;
792
917
  }
918
+ await this.flushPrivatePersistence();
793
919
  if (this.networkStarted) {
794
920
  await this.network.stop();
795
921
  }
@@ -808,6 +934,9 @@ class LocalNodeService {
808
934
  getOverview() {
809
935
  const discovered = this.search("");
810
936
  const onlineCount = discovered.filter((profile) => profile.online).length;
937
+ const openclawInstallation = detectOpenClawInstallation(this.projectRoot);
938
+ const openclawRuntime = this.getCachedOpenClawRuntime();
939
+ const openclawSkillInstallation = detectOpenClawSkillInstallation();
811
940
  return {
812
941
  app_version: this.appVersion,
813
942
  agent_id: this.identity?.agent_id ?? "",
@@ -823,6 +952,15 @@ class LocalNodeService {
823
952
  init_state: this.initState,
824
953
  presence_ttl_ms: PRESENCE_TTL_MS,
825
954
  onboarding: this.getOnboardingSummary(),
955
+ openclaw: {
956
+ detected: openclawInstallation.detected,
957
+ running: openclawRuntime.running,
958
+ detection_mode: openclawRuntime.detection_mode,
959
+ gateway_url: openclawRuntime.gateway_url,
960
+ gateway_probe_ok: openclawRuntime.gateway_probe_ok,
961
+ status_ok: openclawRuntime.status_ok,
962
+ skill_installed: openclawSkillInstallation.installed,
963
+ },
826
964
  social: {
827
965
  found: this.socialFound,
828
966
  enabled: this.socialConfig.enabled,
@@ -965,6 +1103,7 @@ class LocalNodeService {
965
1103
  const relayCapable = this.adapterMode === "webrtc-preview" || this.adapterMode === "relay-preview";
966
1104
  const peers = diagnostics?.peers?.items ?? [];
967
1105
  const online = peers.filter((peer) => peer.status === "online").length;
1106
+ const memory = process.memoryUsage();
968
1107
  return {
969
1108
  adapter: this.adapterMode,
970
1109
  mode: this.networkMode,
@@ -988,6 +1127,23 @@ class LocalNodeService {
988
1127
  adapter_stats: diagnostics?.stats ?? null,
989
1128
  adapter_transport_stats: diagnostics?.transport_stats ?? null,
990
1129
  adapter_discovery_stats: diagnostics?.discovery_stats ?? null,
1130
+ runtime_diagnostics: {
1131
+ memory_mib: {
1132
+ rss: formatBytesToMiB(memory.rss),
1133
+ heap_used: formatBytesToMiB(memory.heapUsed),
1134
+ heap_total: formatBytesToMiB(memory.heapTotal),
1135
+ external: formatBytesToMiB(memory.external),
1136
+ },
1137
+ directory: {
1138
+ profile_count: Object.keys(this.directory.profiles).length,
1139
+ presence_count: Object.keys(this.directory.presence).length,
1140
+ index_key_count: Object.keys(this.directory.index).length,
1141
+ },
1142
+ social: {
1143
+ message_count: this.socialMessages.length,
1144
+ observation_count: this.socialMessageObservations.length,
1145
+ },
1146
+ },
991
1147
  adapter_diagnostics_summary: relayCapable || diagnostics
992
1148
  ? {
993
1149
  started: this.networkStarted,
@@ -1099,6 +1255,91 @@ class LocalNodeService {
1099
1255
  social_source_path: this.socialSourcePath,
1100
1256
  };
1101
1257
  }
1258
+ getAppUpdateStatus() {
1259
+ const currentVersion = normalizeVersionText(this.appVersion) || "unknown";
1260
+ const fallback = {
1261
+ current_version: currentVersion,
1262
+ latest_version: currentVersion,
1263
+ update_available: false,
1264
+ channel: "latest",
1265
+ platform: process.platform,
1266
+ checked_at: Date.now(),
1267
+ can_update: true,
1268
+ check_error: null,
1269
+ };
1270
+ try {
1271
+ const result = (0, child_process_1.spawnSync)("npm", ["view", "@silicaclaw/cli", "dist-tags", "--json"], {
1272
+ cwd: this.projectRoot,
1273
+ encoding: "utf8",
1274
+ env: {
1275
+ ...process.env,
1276
+ SILICACLAW_WORKSPACE_DIR: this.projectRoot,
1277
+ SILICACLAW_APP_DIR: this.workspaceRoot,
1278
+ npm_config_cache: process.env.npm_config_cache || userNpmCacheDir(),
1279
+ },
1280
+ });
1281
+ if ((result.status ?? 1) !== 0) {
1282
+ return {
1283
+ ...fallback,
1284
+ check_error: String(result.stderr || result.stdout || "npm view failed").trim() || "npm view failed",
1285
+ };
1286
+ }
1287
+ const tags = JSON.parse(String(result.stdout || "{}").trim() || "{}");
1288
+ const latestVersion = normalizeVersionText(tags.latest || currentVersion) || currentVersion;
1289
+ return {
1290
+ ...fallback,
1291
+ latest_version: latestVersion,
1292
+ update_available: compareVersionTokens(latestVersion, currentVersion) > 0,
1293
+ };
1294
+ }
1295
+ catch (error) {
1296
+ return {
1297
+ ...fallback,
1298
+ check_error: error instanceof Error ? error.message : String(error),
1299
+ };
1300
+ }
1301
+ }
1302
+ startAppUpdate() {
1303
+ const status = this.getAppUpdateStatus();
1304
+ if (!status.update_available || !status.latest_version) {
1305
+ return {
1306
+ started: false,
1307
+ target_version: status.latest_version || status.current_version,
1308
+ platform: process.platform,
1309
+ reason: status.check_error || "already_current",
1310
+ };
1311
+ }
1312
+ const shimPath = userShimPath();
1313
+ const scriptPath = (0, path_1.resolve)(this.workspaceRoot, "scripts", "silicaclaw-cli.mjs");
1314
+ const useShim = (0, fs_1.existsSync)(shimPath);
1315
+ if (!useShim && !(0, fs_1.existsSync)(scriptPath)) {
1316
+ return {
1317
+ started: false,
1318
+ target_version: status.latest_version,
1319
+ platform: process.platform,
1320
+ reason: "missing_cli_script",
1321
+ };
1322
+ }
1323
+ const command = useShim ? shimPath : process.execPath;
1324
+ const args = useShim ? ["update"] : [scriptPath, "update"];
1325
+ const child = (0, child_process_1.spawn)(command, args, {
1326
+ cwd: this.projectRoot,
1327
+ detached: true,
1328
+ stdio: "ignore",
1329
+ env: {
1330
+ ...process.env,
1331
+ SILICACLAW_WORKSPACE_DIR: this.projectRoot,
1332
+ SILICACLAW_APP_DIR: this.workspaceRoot,
1333
+ npm_config_cache: process.env.npm_config_cache || userNpmCacheDir(),
1334
+ },
1335
+ });
1336
+ child.unref();
1337
+ return {
1338
+ started: true,
1339
+ target_version: status.latest_version,
1340
+ platform: process.platform,
1341
+ };
1342
+ }
1102
1343
  getIntegrationSummary() {
1103
1344
  const status = this.getIntegrationStatus();
1104
1345
  const runtimeGenerated = Boolean(this.socialRuntime && this.socialRuntime.last_loaded_at > 0);
@@ -1355,6 +1596,7 @@ class LocalNodeService {
1355
1596
  return {
1356
1597
  ...message,
1357
1598
  display_name: profile?.display_name || message.display_name || "Unnamed",
1599
+ avatar_url: profile?.avatar_url || "",
1358
1600
  is_self: message.agent_id === this.identity?.agent_id,
1359
1601
  online: (0, core_1.isAgentOnline)(lastSeenAt, Date.now(), PRESENCE_TTL_MS),
1360
1602
  last_seen_at: lastSeenAt || null,
@@ -1373,10 +1615,115 @@ class LocalNodeService {
1373
1615
  },
1374
1616
  };
1375
1617
  }
1618
+ getPrivateMessagingState() {
1619
+ return {
1620
+ enabled: Boolean(this.identity && this.privateEncryptionKeyPair),
1621
+ agent_id: this.identity?.agent_id || "",
1622
+ encryption_public_key: this.privateEncryptionKeyPair?.public_key || "",
1623
+ conversation_count: this.getPrivateConversations().length,
1624
+ message_count: this.privateMessages.length,
1625
+ };
1626
+ }
1627
+ getPrivateConversations() {
1628
+ const conversations = new Map();
1629
+ for (const message of this.privateMessages) {
1630
+ if (message.from_agent_id === message.to_agent_id) {
1631
+ continue;
1632
+ }
1633
+ const peerAgentId = message.from_agent_id === this.identity?.agent_id ? message.to_agent_id : message.from_agent_id;
1634
+ if (!peerAgentId || peerAgentId === this.identity?.agent_id) {
1635
+ continue;
1636
+ }
1637
+ const peerProfile = this.directory.profiles[peerAgentId];
1638
+ const current = conversations.get(message.conversation_id);
1639
+ const nextLast = Math.max(current?.last_message_at || 0, message.created_at || 0) || null;
1640
+ conversations.set(message.conversation_id, {
1641
+ conversation_id: message.conversation_id,
1642
+ peer_agent_id: peerAgentId,
1643
+ peer_display_name: peerProfile?.display_name || peerAgentId,
1644
+ peer_avatar_url: peerProfile?.avatar_url || "",
1645
+ peer_public_key: peerProfile?.private_encryption_public_key || "",
1646
+ last_message_at: nextLast,
1647
+ unread_count: current?.unread_count || 0,
1648
+ });
1649
+ }
1650
+ return Array.from(conversations.values()).sort((a, b) => (b.last_message_at || 0) - (a.last_message_at || 0));
1651
+ }
1652
+ getPrivateMessages(conversationId, limit = PRIVATE_MESSAGE_QUERY_LIMIT) {
1653
+ const normalizedConversationId = String(conversationId || "").trim();
1654
+ const resolvedLimit = Math.max(1, Math.min(PRIVATE_MESSAGE_QUERY_LIMIT, Number(limit) || PRIVATE_MESSAGE_QUERY_LIMIT));
1655
+ const receiptsByMessageId = new Map(this.privateMessageReceipts.map((receipt) => [receipt.message_id, receipt.status]));
1656
+ return this.privateMessages
1657
+ .filter((message) => {
1658
+ if (message.from_agent_id === message.to_agent_id) {
1659
+ return false;
1660
+ }
1661
+ const peerAgentId = message.from_agent_id === this.identity?.agent_id ? message.to_agent_id : message.from_agent_id;
1662
+ if (!peerAgentId || peerAgentId === this.identity?.agent_id) {
1663
+ return false;
1664
+ }
1665
+ return !normalizedConversationId || message.conversation_id === normalizedConversationId;
1666
+ })
1667
+ .sort((a, b) => a.created_at - b.created_at)
1668
+ .slice(-resolvedLimit)
1669
+ .map((message) => ({
1670
+ message_id: message.message_id,
1671
+ conversation_id: message.conversation_id,
1672
+ from_agent_id: message.from_agent_id,
1673
+ to_agent_id: message.to_agent_id,
1674
+ body: this.decryptPrivateMessageBody(message),
1675
+ created_at: message.created_at,
1676
+ is_self: message.from_agent_id === this.identity?.agent_id,
1677
+ delivery_status: receiptsByMessageId.get(message.message_id) || "sent",
1678
+ }));
1679
+ }
1680
+ async sendPrivateMessage(input) {
1681
+ if (!this.identity || !this.privateEncryptionKeyPair) {
1682
+ return { sent: false, reason: "missing_identity_or_private_key" };
1683
+ }
1684
+ const toAgentId = String(input.to_agent_id || "").trim();
1685
+ const recipientKey = String(input.recipient_encryption_public_key || "").trim();
1686
+ const body = String(input.body || "").trim();
1687
+ if (toAgentId === this.identity.agent_id) {
1688
+ return { sent: false, reason: "self_private_message_not_allowed" };
1689
+ }
1690
+ const toPeerId = this.privatePeerRoutes[toAgentId] || "";
1691
+ if (!toAgentId || !toPeerId || !recipientKey || !body) {
1692
+ return { sent: false, reason: "invalid_private_message_input" };
1693
+ }
1694
+ if (typeof this.network.sendDirect !== "function") {
1695
+ return { sent: false, reason: "direct_delivery_not_supported" };
1696
+ }
1697
+ const encrypted = (0, core_1.encryptPrivatePayload)({
1698
+ plaintext: body,
1699
+ recipient_public_key: recipientKey,
1700
+ sender_keypair: this.privateEncryptionKeyPair,
1701
+ });
1702
+ const message = (0, core_1.signPrivateMessage)({
1703
+ identity: this.identity,
1704
+ message_id: (0, crypto_1.createHash)("sha256").update(`${this.identity.agent_id}:${toAgentId}:${Date.now()}:${body}:${Math.random()}`, "utf8").digest("hex"),
1705
+ conversation_id: this.buildPrivateConversationId(this.identity.agent_id, toAgentId),
1706
+ to_agent_id: toAgentId,
1707
+ sender_encryption_public_key: encrypted.sender_encryption_public_key,
1708
+ recipient_encryption_public_key: recipientKey,
1709
+ ciphertext: encrypted.ciphertext,
1710
+ nonce: encrypted.nonce,
1711
+ created_at: Date.now(),
1712
+ });
1713
+ this.ingestPrivateMessage(message);
1714
+ await this.persistPrivateMessages();
1715
+ await this.network.sendDirect(toPeerId, PRIVATE_MESSAGE_TOPIC, message);
1716
+ const view = this.getPrivateMessages(message.conversation_id).find((item) => item.message_id === message.message_id);
1717
+ return { sent: true, reason: "sent", message: view };
1718
+ }
1376
1719
  getOpenClawBridgeStatus() {
1720
+ const now = Date.now();
1721
+ if (this.openclawBridgeStatusCache && this.openclawBridgeStatusCache.expiresAt > now) {
1722
+ return this.openclawBridgeStatusCache.value;
1723
+ }
1377
1724
  const integration = this.getIntegrationStatus();
1378
1725
  const openclawInstallation = detectOpenClawInstallation(this.projectRoot);
1379
- const openclawRuntime = detectOpenClawRuntime(this.projectRoot);
1726
+ const openclawRuntime = this.getCachedOpenClawRuntime();
1380
1727
  const skillInstallation = detectOpenClawSkillInstallation();
1381
1728
  const ownerDelivery = detectOwnerDeliveryStatus({
1382
1729
  workspaceRoot: this.projectRoot,
@@ -1384,7 +1731,7 @@ class LocalNodeService {
1384
1731
  openclawRunning: openclawRuntime.running,
1385
1732
  skillInstalled: skillInstallation.installed,
1386
1733
  });
1387
- return {
1734
+ const value = {
1388
1735
  enabled: this.socialConfig.enabled,
1389
1736
  connected_to_silicaclaw: integration.connected_to_silicaclaw,
1390
1737
  public_enabled: integration.public_enabled,
@@ -1440,6 +1787,11 @@ class LocalNodeService {
1440
1787
  install_skill: "/api/openclaw/bridge/skill-install",
1441
1788
  },
1442
1789
  };
1790
+ this.openclawBridgeStatusCache = {
1791
+ value,
1792
+ expiresAt: now + OPENCLAW_BRIDGE_STATUS_CACHE_MS,
1793
+ };
1794
+ return value;
1443
1795
  }
1444
1796
  async installOpenClawSkill(skillName) {
1445
1797
  const scriptPath = (0, path_1.resolve)(this.workspaceRoot, "scripts", "install-openclaw-skill.mjs");
@@ -1453,6 +1805,7 @@ class LocalNodeService {
1453
1805
  maxBuffer: 1024 * 1024,
1454
1806
  });
1455
1807
  const parsed = JSON.parse(String(stdout || "{}"));
1808
+ this.invalidateOpenClawCaches();
1456
1809
  return {
1457
1810
  ...parsed,
1458
1811
  bridge: this.getOpenClawBridgeStatus(),
@@ -1472,7 +1825,7 @@ class LocalNodeService {
1472
1825
  const workspaceSkillDir = (0, path_1.resolve)(homeDir, "workspace", "skills");
1473
1826
  const legacySkillDir = (0, path_1.resolve)(homeDir, "skills");
1474
1827
  const openclawSourceDir = defaultOpenClawSourceDir(this.projectRoot);
1475
- const openclawRuntime = detectOpenClawRuntime(this.projectRoot);
1828
+ const openclawRuntime = this.getCachedOpenClawRuntime();
1476
1829
  return {
1477
1830
  bridge_api_base: DEFAULT_BRIDGE_API_BASE,
1478
1831
  openclaw_detected: detectOpenClawInstallation(this.projectRoot).detected,
@@ -1845,14 +2198,13 @@ class LocalNodeService {
1845
2198
  profile: this.profile,
1846
2199
  };
1847
2200
  const presenceRecord = (0, core_1.signPresence)(this.identity, Date.now());
1848
- const indexRecords = (0, core_1.buildIndexRecords)(this.profile);
1849
- const replayMessages = this.getReplayableSelfSocialMessages();
2201
+ const shouldPublishProfile = this.shouldPublishProfileRecord(profileRecord, reason, presenceRecord.timestamp);
2202
+ const replayMessages = this.getReplayableSelfSocialMessages(reason);
1850
2203
  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);
2204
+ if (shouldPublishProfile) {
2205
+ await this.publish("profile", profileRecord);
1855
2206
  }
2207
+ await this.publish("presence", presenceRecord);
1856
2208
  for (const message of replayMessages) {
1857
2209
  await this.publish(SOCIAL_MESSAGE_TOPIC, message);
1858
2210
  }
@@ -1862,23 +2214,67 @@ class LocalNodeService {
1862
2214
  this.lastBroadcastErrorAt = Date.now();
1863
2215
  this.lastBroadcastError = message;
1864
2216
  this.broadcastFailureCount += 1;
2217
+ this.consecutiveBroadcastFailures += 1;
1865
2218
  await this.log("error", `Broadcast failed (reason=${reason}): ${message}`);
2219
+ await this.maybeRecoverFromBroadcastFailure(reason, message);
1866
2220
  return { sent: false, reason: "publish_failed", error: message };
1867
2221
  }
1868
2222
  this.lastBroadcastAt = Date.now();
1869
2223
  this.broadcastCount += 1;
1870
2224
  this.lastBroadcastError = null;
1871
2225
  this.lastBroadcastErrorAt = 0;
2226
+ this.consecutiveBroadcastFailures = 0;
1872
2227
  this.directory = (0, core_1.ingestProfileRecord)(this.directory, profileRecord);
1873
2228
  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
2229
  this.compactCacheInMemory();
1878
2230
  await this.persistCache();
1879
- await this.log("info", `Broadcast sent (${indexRecords.length} index refs, replayed_messages=${replayMessages.length}, reason=${reason})`);
2231
+ await this.log("info", `Broadcast sent (${shouldPublishProfile ? "profile + " : ""}presence, replayed_messages=${replayMessages.length}, reason=${reason})`);
1880
2232
  return { sent: true, reason };
1881
2233
  }
2234
+ shouldPublishProfileRecord(profileRecord, reason, now = Date.now()) {
2235
+ if (reason !== "interval") {
2236
+ this.lastProfileBroadcastSignature = profileRecord.profile.signature;
2237
+ this.lastProfileBroadcastAt = now;
2238
+ return true;
2239
+ }
2240
+ const signature = profileRecord.profile.signature;
2241
+ const changedSinceLastPublish = signature !== this.lastProfileBroadcastSignature;
2242
+ const refreshDue = now - this.lastProfileBroadcastAt >= PROFILE_RELAY_REFRESH_INTERVAL_MS;
2243
+ if (!changedSinceLastPublish && !refreshDue) {
2244
+ return false;
2245
+ }
2246
+ this.lastProfileBroadcastSignature = signature;
2247
+ this.lastProfileBroadcastAt = now;
2248
+ return true;
2249
+ }
2250
+ async maybeRecoverFromBroadcastFailure(reason, errorMessage) {
2251
+ const recoveryThreshold = 3;
2252
+ const recoveryCooldownMs = 60_000;
2253
+ if (this.broadcastRecoveryInFlight) {
2254
+ return;
2255
+ }
2256
+ if (this.consecutiveBroadcastFailures < recoveryThreshold) {
2257
+ return;
2258
+ }
2259
+ if (Date.now() - this.lastBroadcastRecoveryAttemptAt < recoveryCooldownMs) {
2260
+ return;
2261
+ }
2262
+ if (this.adapterMode !== "relay-preview" && this.adapterMode !== "webrtc-preview" && this.adapterMode !== "real-preview") {
2263
+ return;
2264
+ }
2265
+ this.broadcastRecoveryInFlight = true;
2266
+ this.lastBroadcastRecoveryAttemptAt = Date.now();
2267
+ try {
2268
+ await this.log("warn", `Broadcast recovery triggered after ${this.consecutiveBroadcastFailures} consecutive failures (${reason}): ${errorMessage}`);
2269
+ await this.restartNetworkAdapter("broadcast_failure_recovery");
2270
+ }
2271
+ catch (recoveryError) {
2272
+ await this.log("error", `Broadcast recovery failed: ${recoveryError instanceof Error ? recoveryError.message : String(recoveryError)}`);
2273
+ }
2274
+ finally {
2275
+ this.broadcastRecoveryInFlight = false;
2276
+ }
2277
+ }
1882
2278
  async hydrateFromDisk() {
1883
2279
  this.initState = {
1884
2280
  identity_auto_created: false,
@@ -1907,6 +2303,8 @@ class LocalNodeService {
1907
2303
  await this.log("info", `Bound existing OpenClaw identity: ${resolvedIdentity.openclaw_source_path}`);
1908
2304
  }
1909
2305
  await this.identityRepo.set(this.identity);
2306
+ this.privateEncryptionKeyPair = (await this.privateEncryptionKeyRepo.get()) || (0, core_1.createPrivateEncryptionKeyPair)();
2307
+ await this.privateEncryptionKeyRepo.set(this.privateEncryptionKeyPair);
1910
2308
  const existingProfile = await this.profileRepo.get();
1911
2309
  const profileInput = (0, core_1.resolveProfileInputWithSocial)({
1912
2310
  socialConfig: this.socialConfig,
@@ -1914,7 +2312,10 @@ class LocalNodeService {
1914
2312
  existingProfile: existingProfile && existingProfile.agent_id === this.identity.agent_id ? existingProfile : null,
1915
2313
  rootDir: this.projectRoot,
1916
2314
  });
1917
- this.profile = (0, core_1.signProfile)(profileInput, this.identity);
2315
+ this.profile = (0, core_1.signProfile)({
2316
+ ...profileInput,
2317
+ private_encryption_public_key: this.privateEncryptionKeyPair?.public_key || profileInput.private_encryption_public_key || "",
2318
+ }, this.identity);
1918
2319
  if (!existingProfile || existingProfile.agent_id !== this.identity.agent_id) {
1919
2320
  this.initState.profile_auto_created = true;
1920
2321
  await this.log("info", "profile.json missing/invalid, initialized from social/default profile");
@@ -1927,6 +2328,8 @@ class LocalNodeService {
1927
2328
  };
1928
2329
  this.socialMessages = this.normalizeSocialMessages(await this.socialMessageRepo.get());
1929
2330
  this.socialMessageObservations = this.normalizeSocialMessageObservations(await this.socialMessageObservationRepo.get());
2331
+ this.privateMessages = this.normalizePrivateMessages(await this.privateMessageRepo.get());
2332
+ this.privateMessageReceipts = this.normalizePrivateMessageReceipts(await this.privateMessageReceiptRepo.get());
1930
2333
  this.directory = (0, core_1.ingestProfileRecord)(this.directory, { type: "profile", profile: this.profile });
1931
2334
  this.compactCacheInMemory();
1932
2335
  await this.persistCache();
@@ -1943,7 +2346,10 @@ class LocalNodeService {
1943
2346
  existingProfile: this.profile,
1944
2347
  rootDir: this.projectRoot,
1945
2348
  });
1946
- const nextProfile = (0, core_1.signProfile)(nextProfileInput, this.identity);
2349
+ const nextProfile = (0, core_1.signProfile)({
2350
+ ...nextProfileInput,
2351
+ private_encryption_public_key: this.privateEncryptionKeyPair?.public_key || nextProfileInput.private_encryption_public_key || "",
2352
+ }, this.identity);
1947
2353
  this.profile = nextProfile;
1948
2354
  await this.profileRepo.set(nextProfile);
1949
2355
  this.directory = (0, core_1.ingestProfileRecord)(this.directory, { type: "profile", profile: nextProfile });
@@ -2001,7 +2407,7 @@ class LocalNodeService {
2001
2407
  this.socialRuntime = runtime;
2002
2408
  await this.socialRuntimeRepo.set(runtime);
2003
2409
  }
2004
- async onMessage(topic, data) {
2410
+ async onMessage(topic, data, meta) {
2005
2411
  this.receivedCount += 1;
2006
2412
  this.receivedByTopic[topic] = (this.receivedByTopic[topic] ?? 0) + 1;
2007
2413
  this.lastMessageAt = Date.now();
@@ -2016,6 +2422,9 @@ class LocalNodeService {
2016
2422
  return;
2017
2423
  }
2018
2424
  }
2425
+ if (meta?.peerId && record.profile.agent_id) {
2426
+ this.privatePeerRoutes[record.profile.agent_id] = meta.peerId;
2427
+ }
2019
2428
  this.directory = (0, core_1.ingestProfileRecord)(this.directory, record);
2020
2429
  this.compactCacheInMemory();
2021
2430
  await this.persistCache();
@@ -2032,6 +2441,9 @@ class LocalNodeService {
2032
2441
  return;
2033
2442
  }
2034
2443
  }
2444
+ if (meta?.peerId && record.agent_id) {
2445
+ this.privatePeerRoutes[record.agent_id] = meta.peerId;
2446
+ }
2035
2447
  this.directory = (0, core_1.ingestPresenceRecord)(this.directory, record);
2036
2448
  this.compactCacheInMemory();
2037
2449
  await this.persistCache();
@@ -2046,6 +2458,9 @@ class LocalNodeService {
2046
2458
  await this.log("warn", `Rejected social message with invalid signature (${record.message_id.slice(0, 10)})`);
2047
2459
  return;
2048
2460
  }
2461
+ if (meta?.peerId && record.agent_id) {
2462
+ this.privatePeerRoutes[record.agent_id] = meta.peerId;
2463
+ }
2049
2464
  if (this.hasSocialMessage(record.message_id)) {
2050
2465
  await this.publishObservationForMessage(record);
2051
2466
  return;
@@ -2081,6 +2496,30 @@ class LocalNodeService {
2081
2496
  this.directory = (0, core_1.dedupeIndex)(this.directory);
2082
2497
  await this.persistCache();
2083
2498
  }
2499
+ async onDirectMessage(topic, data, meta) {
2500
+ if (topic === PRIVATE_MESSAGE_TOPIC) {
2501
+ const record = this.normalizeIncomingPrivateMessage(data);
2502
+ if (!record || !(0, core_1.verifyPrivateMessage)(record)) {
2503
+ return;
2504
+ }
2505
+ if (record.to_agent_id !== this.identity?.agent_id || this.hasPrivateMessage(record.message_id)) {
2506
+ return;
2507
+ }
2508
+ this.ingestPrivateMessage(record);
2509
+ await this.persistPrivateMessages();
2510
+ await this.sendPrivateMessageReceipt(record, meta?.peerId);
2511
+ return;
2512
+ }
2513
+ const receipt = this.normalizeIncomingPrivateMessageReceipt(data);
2514
+ if (!receipt || !(0, core_1.verifyPrivateMessageReceipt)(receipt)) {
2515
+ return;
2516
+ }
2517
+ if (receipt.to_agent_id !== this.identity?.agent_id) {
2518
+ return;
2519
+ }
2520
+ this.ingestPrivateMessageReceipt(receipt);
2521
+ await this.persistPrivateMessageReceipts();
2522
+ }
2084
2523
  startBroadcastLoop() {
2085
2524
  if (this.broadcaster) {
2086
2525
  clearInterval(this.broadcaster);
@@ -2101,21 +2540,29 @@ class LocalNodeService {
2101
2540
  if (this.subscriptionsBound) {
2102
2541
  return;
2103
2542
  }
2104
- this.network.subscribe("profile", (data) => {
2105
- this.onMessage("profile", data);
2543
+ this.network.subscribe("profile", (data, meta) => {
2544
+ this.onMessage("profile", data, meta);
2106
2545
  });
2107
- this.network.subscribe("presence", (data) => {
2108
- this.onMessage("presence", data);
2546
+ this.network.subscribe("presence", (data, meta) => {
2547
+ this.onMessage("presence", data, meta);
2109
2548
  });
2110
- this.network.subscribe("index", (data) => {
2111
- this.onMessage("index", data);
2549
+ this.network.subscribe("index", (data, meta) => {
2550
+ this.onMessage("index", data, meta);
2112
2551
  });
2113
- this.network.subscribe(SOCIAL_MESSAGE_TOPIC, (data) => {
2114
- this.onMessage(SOCIAL_MESSAGE_TOPIC, data);
2552
+ this.network.subscribe(SOCIAL_MESSAGE_TOPIC, (data, meta) => {
2553
+ this.onMessage(SOCIAL_MESSAGE_TOPIC, data, meta);
2115
2554
  });
2116
- this.network.subscribe(SOCIAL_MESSAGE_OBSERVATION_TOPIC, (data) => {
2117
- this.onMessage(SOCIAL_MESSAGE_OBSERVATION_TOPIC, data);
2555
+ this.network.subscribe(SOCIAL_MESSAGE_OBSERVATION_TOPIC, (data, meta) => {
2556
+ this.onMessage(SOCIAL_MESSAGE_OBSERVATION_TOPIC, data, meta);
2118
2557
  });
2558
+ if (typeof this.network.subscribeDirect === "function") {
2559
+ this.network.subscribeDirect(PRIVATE_MESSAGE_TOPIC, (data, meta) => {
2560
+ this.onDirectMessage(PRIVATE_MESSAGE_TOPIC, data, meta);
2561
+ });
2562
+ this.network.subscribeDirect(PRIVATE_MESSAGE_RECEIPT_TOPIC, (data, meta) => {
2563
+ this.onDirectMessage(PRIVATE_MESSAGE_RECEIPT_TOPIC, data, meta);
2564
+ });
2565
+ }
2119
2566
  this.subscriptionsBound = true;
2120
2567
  }
2121
2568
  buildNetworkAdapter() {
@@ -2255,9 +2702,58 @@ class LocalNodeService {
2255
2702
  }, delayMs);
2256
2703
  this.networkReconnectDelayMs = Math.min(30_000, Math.max(5_000, Math.floor(delayMs * 1.5)));
2257
2704
  }
2705
+ pruneRemoteProfilesInMemory(now = Date.now()) {
2706
+ if (!Number.isFinite(DIRECTORY_REMOTE_PROFILE_SOFT_LIMIT) || DIRECTORY_REMOTE_PROFILE_SOFT_LIMIT <= 0) {
2707
+ return 0;
2708
+ }
2709
+ const selfAgentId = this.profile?.agent_id || this.identity?.agent_id || "";
2710
+ const remoteProfiles = Object.values(this.directory.profiles).filter((profile) => profile.agent_id !== selfAgentId);
2711
+ if (remoteProfiles.length <= DIRECTORY_REMOTE_PROFILE_SOFT_LIMIT) {
2712
+ return 0;
2713
+ }
2714
+ const onlineRemoteProfiles = remoteProfiles.filter((profile) => (0, core_1.isAgentOnline)(this.directory.presence[profile.agent_id], now, PRESENCE_TTL_MS));
2715
+ const offlineRemoteProfiles = remoteProfiles
2716
+ .filter((profile) => !(0, core_1.isAgentOnline)(this.directory.presence[profile.agent_id], now, PRESENCE_TTL_MS))
2717
+ .sort((a, b) => (b.updated_at || 0) - (a.updated_at || 0));
2718
+ const keepOfflineCount = Math.max(0, DIRECTORY_REMOTE_PROFILE_SOFT_LIMIT - onlineRemoteProfiles.length);
2719
+ const keptRemoteProfiles = [
2720
+ ...onlineRemoteProfiles,
2721
+ ...offlineRemoteProfiles.slice(0, keepOfflineCount),
2722
+ ];
2723
+ const keptRemoteIds = new Set(keptRemoteProfiles.map((profile) => profile.agent_id));
2724
+ const removedIds = remoteProfiles
2725
+ .map((profile) => profile.agent_id)
2726
+ .filter((agentId) => !keptRemoteIds.has(agentId));
2727
+ if (removedIds.length === 0) {
2728
+ return 0;
2729
+ }
2730
+ const next = (0, core_1.createEmptyDirectoryState)();
2731
+ const selfProfile = selfAgentId ? this.directory.profiles[selfAgentId] : null;
2732
+ if (selfProfile) {
2733
+ next.profiles[selfAgentId] = selfProfile;
2734
+ const selfPresence = this.directory.presence[selfAgentId];
2735
+ if (typeof selfPresence === "number" && Number.isFinite(selfPresence)) {
2736
+ next.presence[selfAgentId] = selfPresence;
2737
+ }
2738
+ const rebuilt = (0, core_1.rebuildIndexForProfile)(next, selfProfile);
2739
+ next.index = rebuilt.index;
2740
+ }
2741
+ for (const profile of keptRemoteProfiles) {
2742
+ next.profiles[profile.agent_id] = profile;
2743
+ const seenAt = this.directory.presence[profile.agent_id];
2744
+ if (typeof seenAt === "number" && Number.isFinite(seenAt)) {
2745
+ next.presence[profile.agent_id] = seenAt;
2746
+ }
2747
+ const rebuilt = (0, core_1.rebuildIndexForProfile)(next, profile);
2748
+ next.index = rebuilt.index;
2749
+ }
2750
+ this.directory = (0, core_1.dedupeIndex)(next);
2751
+ return removedIds.length;
2752
+ }
2258
2753
  compactCacheInMemory() {
2259
2754
  const cleaned = (0, core_1.cleanupExpiredPresence)(this.directory, Date.now(), PRESENCE_TTL_MS);
2260
2755
  this.directory = (0, core_1.dedupeIndex)(cleaned.state);
2756
+ this.pruneRemoteProfilesInMemory();
2261
2757
  return cleaned.removed;
2262
2758
  }
2263
2759
  async publish(topic, data) {
@@ -2288,6 +2784,52 @@ class LocalNodeService {
2288
2784
  async persistSocialMessageObservations() {
2289
2785
  await this.socialMessageObservationRepo.set(this.socialMessageObservations);
2290
2786
  }
2787
+ async persistPrivateMessages() {
2788
+ this.privateMessagesPersistDirty = true;
2789
+ if (this.privateMessagesPersistTimer) {
2790
+ return;
2791
+ }
2792
+ this.privateMessagesPersistTimer = setTimeout(() => {
2793
+ this.flushPrivateMessagesPersist().catch(() => { });
2794
+ }, PRIVATE_MESSAGE_PERSIST_DEBOUNCE_MS);
2795
+ }
2796
+ async persistPrivateMessageReceipts() {
2797
+ this.privateMessageReceiptsPersistDirty = true;
2798
+ if (this.privateMessageReceiptsPersistTimer) {
2799
+ return;
2800
+ }
2801
+ this.privateMessageReceiptsPersistTimer = setTimeout(() => {
2802
+ this.flushPrivateMessageReceiptsPersist().catch(() => { });
2803
+ }, PRIVATE_MESSAGE_PERSIST_DEBOUNCE_MS);
2804
+ }
2805
+ async flushPrivatePersistence() {
2806
+ await Promise.all([
2807
+ this.flushPrivateMessagesPersist(),
2808
+ this.flushPrivateMessageReceiptsPersist(),
2809
+ ]);
2810
+ }
2811
+ async flushPrivateMessagesPersist() {
2812
+ if (this.privateMessagesPersistTimer) {
2813
+ clearTimeout(this.privateMessagesPersistTimer);
2814
+ this.privateMessagesPersistTimer = null;
2815
+ }
2816
+ if (!this.privateMessagesPersistDirty) {
2817
+ return;
2818
+ }
2819
+ this.privateMessagesPersistDirty = false;
2820
+ await this.privateMessageRepo.set(this.privateMessages);
2821
+ }
2822
+ async flushPrivateMessageReceiptsPersist() {
2823
+ if (this.privateMessageReceiptsPersistTimer) {
2824
+ clearTimeout(this.privateMessageReceiptsPersistTimer);
2825
+ this.privateMessageReceiptsPersistTimer = null;
2826
+ }
2827
+ if (!this.privateMessageReceiptsPersistDirty) {
2828
+ return;
2829
+ }
2830
+ this.privateMessageReceiptsPersistDirty = false;
2831
+ await this.privateMessageReceiptRepo.set(this.privateMessageReceipts);
2832
+ }
2291
2833
  async log(level, message) {
2292
2834
  await this.logRepo.append({
2293
2835
  level,
@@ -2334,6 +2876,7 @@ class LocalNodeService {
2334
2876
  (0, core_1.verifyProfile)(profile, selfPublicKey));
2335
2877
  return (0, core_1.buildPublicProfileSummary)({
2336
2878
  profile,
2879
+ is_self: isSelf,
2337
2880
  online,
2338
2881
  last_seen_at: lastSeenAt || null,
2339
2882
  network_mode: isSelf ? this.networkMode : "unknown",
@@ -2379,6 +2922,7 @@ class LocalNodeService {
2379
2922
  updated_at: message.created_at,
2380
2923
  signature: "",
2381
2924
  },
2925
+ is_self: message.agent_id === this.identity?.agent_id,
2382
2926
  online: false,
2383
2927
  last_seen_at: null,
2384
2928
  network_mode: "unknown",
@@ -2566,6 +3110,32 @@ class LocalNodeService {
2566
3110
  .join("\n")
2567
3111
  .trim();
2568
3112
  }
3113
+ buildPrivateConversationId(leftAgentId, rightAgentId) {
3114
+ return [String(leftAgentId || "").trim(), String(rightAgentId || "").trim()].sort().join(":");
3115
+ }
3116
+ decryptPrivateMessageBody(message) {
3117
+ const cached = this.privateMessageBodyCache.get(message.message_id);
3118
+ if (typeof cached === "string") {
3119
+ return cached;
3120
+ }
3121
+ if (!this.privateEncryptionKeyPair) {
3122
+ return "[encrypted]";
3123
+ }
3124
+ const decrypted = (0, core_1.decryptPrivatePayload)({
3125
+ ciphertext: message.ciphertext,
3126
+ nonce: message.nonce,
3127
+ sender_encryption_public_key: message.sender_encryption_public_key,
3128
+ recipient_private_key: this.privateEncryptionKeyPair.private_key,
3129
+ }) || "[encrypted]";
3130
+ this.privateMessageBodyCache.set(message.message_id, decrypted);
3131
+ if (this.privateMessageBodyCache.size > PRIVATE_MESSAGE_HISTORY_LIMIT * 2) {
3132
+ const firstKey = this.privateMessageBodyCache.keys().next().value;
3133
+ if (firstKey) {
3134
+ this.privateMessageBodyCache.delete(firstKey);
3135
+ }
3136
+ }
3137
+ return decrypted;
3138
+ }
2569
3139
  normalizeWindowTimestamps(timestamps, windowMs, now = Date.now()) {
2570
3140
  return timestamps.filter((timestamp) => now - timestamp <= windowMs);
2571
3141
  }
@@ -2586,16 +3156,30 @@ class LocalNodeService {
2586
3156
  hasSocialMessage(messageId) {
2587
3157
  return this.socialMessages.some((item) => item.message_id === messageId);
2588
3158
  }
2589
- getReplayableSelfSocialMessages(now = Date.now()) {
3159
+ getReplayableSelfSocialMessages(reason = "manual", now = Date.now()) {
2590
3160
  const maxCount = Math.max(0, SOCIAL_MESSAGE_REPLAY_MAX_PER_BROADCAST);
2591
3161
  if (!this.identity || maxCount === 0) {
2592
3162
  return [];
2593
3163
  }
2594
- return this.socialMessages
3164
+ const replayable = this.socialMessages
2595
3165
  .filter((item) => (item.agent_id === this.identity?.agent_id &&
2596
3166
  now - item.created_at <= SOCIAL_MESSAGE_REPLAY_WINDOW_MS))
2597
3167
  .sort((a, b) => a.created_at - b.created_at)
2598
3168
  .slice(-maxCount);
3169
+ if (!replayable.length) {
3170
+ this.lastReplayBroadcastSignature = "";
3171
+ return [];
3172
+ }
3173
+ const signature = replayable.map((item) => item.message_id).join(",");
3174
+ const isIntervalReplay = reason === "interval";
3175
+ const changedSinceLastReplay = signature !== this.lastReplayBroadcastSignature;
3176
+ const refreshDue = now - this.lastReplayBroadcastAt >= SOCIAL_MESSAGE_REPLAY_REFRESH_INTERVAL_MS;
3177
+ if (isIntervalReplay && !changedSinceLastReplay && !refreshDue) {
3178
+ return [];
3179
+ }
3180
+ this.lastReplayBroadcastSignature = signature;
3181
+ this.lastReplayBroadcastAt = now;
3182
+ return replayable;
2599
3183
  }
2600
3184
  hasRecentDuplicateMessage(agentId, body, topic, now = Date.now()) {
2601
3185
  return this.socialMessages.some((item) => (item.agent_id === agentId &&
@@ -2659,6 +3243,167 @@ class LocalNodeService {
2659
3243
  await this.publish(SOCIAL_MESSAGE_OBSERVATION_TOPIC, observation);
2660
3244
  await this.persistSocialMessageObservations();
2661
3245
  }
3246
+ async sendPrivateMessageReceipt(message, replyPeerId) {
3247
+ if (!this.identity || typeof this.network.sendDirect !== "function" || !replyPeerId) {
3248
+ return;
3249
+ }
3250
+ const receipt = (0, core_1.signPrivateMessageReceipt)({
3251
+ identity: this.identity,
3252
+ receipt_id: (0, crypto_1.createHash)("sha256").update(`${message.message_id}:${this.identity.agent_id}:${Date.now()}`, "utf8").digest("hex"),
3253
+ message_id: message.message_id,
3254
+ conversation_id: message.conversation_id,
3255
+ to_agent_id: message.from_agent_id,
3256
+ status: "received",
3257
+ created_at: Date.now(),
3258
+ });
3259
+ this.ingestPrivateMessageReceipt(receipt);
3260
+ await this.network.sendDirect(replyPeerId, PRIVATE_MESSAGE_RECEIPT_TOPIC, receipt);
3261
+ await this.persistPrivateMessageReceipts();
3262
+ }
3263
+ normalizeIncomingPrivateMessage(value) {
3264
+ if (typeof value !== "object" || value === null) {
3265
+ return null;
3266
+ }
3267
+ const record = value;
3268
+ const createdAt = Number(record.created_at || 0);
3269
+ const fromAgentId = String(record.from_agent_id || "").trim();
3270
+ const toAgentId = String(record.to_agent_id || "").trim();
3271
+ const conversationId = String(record.conversation_id || "").trim();
3272
+ if (record.type !== PRIVATE_MESSAGE_TOPIC ||
3273
+ !String(record.message_id || "").trim() ||
3274
+ !conversationId ||
3275
+ !fromAgentId ||
3276
+ !toAgentId ||
3277
+ !String(record.sender_public_key || "").trim() ||
3278
+ !String(record.sender_encryption_public_key || "").trim() ||
3279
+ !String(record.recipient_encryption_public_key || "").trim() ||
3280
+ !String(record.ciphertext || "").trim() ||
3281
+ !String(record.nonce || "").trim() ||
3282
+ String(record.cipher_scheme || "") !== "nacl-box-v1" ||
3283
+ !String(record.signature || "").trim() ||
3284
+ !Number.isFinite(createdAt)) {
3285
+ return null;
3286
+ }
3287
+ if (fromAgentId === toAgentId) {
3288
+ return null;
3289
+ }
3290
+ if (conversationId !== this.buildPrivateConversationId(fromAgentId, toAgentId)) {
3291
+ return null;
3292
+ }
3293
+ return {
3294
+ type: PRIVATE_MESSAGE_TOPIC,
3295
+ message_id: String(record.message_id).trim(),
3296
+ conversation_id: conversationId,
3297
+ from_agent_id: fromAgentId,
3298
+ to_agent_id: toAgentId,
3299
+ sender_public_key: String(record.sender_public_key).trim(),
3300
+ sender_encryption_public_key: String(record.sender_encryption_public_key).trim(),
3301
+ recipient_encryption_public_key: String(record.recipient_encryption_public_key).trim(),
3302
+ cipher_scheme: "nacl-box-v1",
3303
+ ciphertext: String(record.ciphertext).trim(),
3304
+ nonce: String(record.nonce).trim(),
3305
+ created_at: createdAt,
3306
+ signature: String(record.signature).trim(),
3307
+ };
3308
+ }
3309
+ normalizePrivateMessages(items) {
3310
+ if (!Array.isArray(items)) {
3311
+ return [];
3312
+ }
3313
+ const deduped = new Set();
3314
+ return items
3315
+ .map((item) => this.normalizeIncomingPrivateMessage(item))
3316
+ .filter((item) => Boolean(item))
3317
+ .sort((a, b) => a.created_at - b.created_at)
3318
+ .filter((item) => {
3319
+ if (deduped.has(item.message_id)) {
3320
+ return false;
3321
+ }
3322
+ deduped.add(item.message_id);
3323
+ return true;
3324
+ })
3325
+ .slice(-PRIVATE_MESSAGE_HISTORY_LIMIT);
3326
+ }
3327
+ normalizeIncomingPrivateMessageReceipt(value) {
3328
+ if (typeof value !== "object" || value === null) {
3329
+ return null;
3330
+ }
3331
+ const record = value;
3332
+ const createdAt = Number(record.created_at || 0);
3333
+ const status = String(record.status || "").trim();
3334
+ if (record.type !== PRIVATE_MESSAGE_RECEIPT_TOPIC ||
3335
+ !String(record.receipt_id || "").trim() ||
3336
+ !String(record.message_id || "").trim() ||
3337
+ !String(record.conversation_id || "").trim() ||
3338
+ !String(record.from_agent_id || "").trim() ||
3339
+ !String(record.to_agent_id || "").trim() ||
3340
+ !String(record.sender_public_key || "").trim() ||
3341
+ (status !== "received" && status !== "read") ||
3342
+ !String(record.signature || "").trim() ||
3343
+ !Number.isFinite(createdAt)) {
3344
+ return null;
3345
+ }
3346
+ return {
3347
+ type: PRIVATE_MESSAGE_RECEIPT_TOPIC,
3348
+ receipt_id: String(record.receipt_id).trim(),
3349
+ message_id: String(record.message_id).trim(),
3350
+ conversation_id: String(record.conversation_id).trim(),
3351
+ from_agent_id: String(record.from_agent_id).trim(),
3352
+ to_agent_id: String(record.to_agent_id).trim(),
3353
+ sender_public_key: String(record.sender_public_key).trim(),
3354
+ status: status,
3355
+ created_at: createdAt,
3356
+ signature: String(record.signature).trim(),
3357
+ };
3358
+ }
3359
+ normalizePrivateMessageReceipts(items) {
3360
+ if (!Array.isArray(items)) {
3361
+ return [];
3362
+ }
3363
+ const deduped = new Set();
3364
+ return items
3365
+ .map((item) => this.normalizeIncomingPrivateMessageReceipt(item))
3366
+ .filter((item) => Boolean(item))
3367
+ .sort((a, b) => a.created_at - b.created_at)
3368
+ .filter((item) => {
3369
+ if (deduped.has(item.receipt_id)) {
3370
+ return false;
3371
+ }
3372
+ deduped.add(item.receipt_id);
3373
+ return true;
3374
+ })
3375
+ .slice(-PRIVATE_MESSAGE_RECEIPT_HISTORY_LIMIT);
3376
+ }
3377
+ hasPrivateMessage(messageId) {
3378
+ return this.privateMessages.some((item) => item.message_id === messageId);
3379
+ }
3380
+ ingestPrivateMessage(message) {
3381
+ const existing = this.privateMessages.findIndex((item) => item.message_id === message.message_id);
3382
+ if (existing >= 0) {
3383
+ this.privateMessages[existing] = message;
3384
+ }
3385
+ else {
3386
+ this.privateMessages.push(message);
3387
+ }
3388
+ this.privateMessages = this.normalizePrivateMessages(this.privateMessages);
3389
+ const validIds = new Set(this.privateMessages.map((item) => item.message_id));
3390
+ this.privateMessageBodyCache.delete(message.message_id);
3391
+ for (const key of Array.from(this.privateMessageBodyCache.keys())) {
3392
+ if (!validIds.has(key)) {
3393
+ this.privateMessageBodyCache.delete(key);
3394
+ }
3395
+ }
3396
+ }
3397
+ ingestPrivateMessageReceipt(receipt) {
3398
+ const existing = this.privateMessageReceipts.findIndex((item) => item.receipt_id === receipt.receipt_id);
3399
+ if (existing >= 0) {
3400
+ this.privateMessageReceipts[existing] = receipt;
3401
+ }
3402
+ else {
3403
+ this.privateMessageReceipts.push(receipt);
3404
+ }
3405
+ this.privateMessageReceipts = this.normalizePrivateMessageReceipts(this.privateMessageReceipts);
3406
+ }
2662
3407
  normalizeIncomingSocialMessage(value) {
2663
3408
  if (typeof value !== "object" || value === null) {
2664
3409
  return null;
@@ -2905,6 +3650,36 @@ async function main() {
2905
3650
  app.get("/api/runtime/paths", (_req, res) => {
2906
3651
  sendOk(res, node.getRuntimePaths());
2907
3652
  });
3653
+ app.get("/api/app/update-status", (_req, res) => {
3654
+ sendOk(res, node.getAppUpdateStatus());
3655
+ });
3656
+ app.post("/api/app/update", asyncRoute(async (_req, res) => {
3657
+ const status = node.getAppUpdateStatus();
3658
+ if (!status.update_available || !status.latest_version) {
3659
+ sendOk(res, {
3660
+ started: false,
3661
+ current_version: status.current_version,
3662
+ latest_version: status.latest_version,
3663
+ platform: status.platform,
3664
+ reason: status.check_error || "already_current",
3665
+ }, { message: "Already on the latest version" });
3666
+ return;
3667
+ }
3668
+ sendOk(res, {
3669
+ started: true,
3670
+ current_version: status.current_version,
3671
+ target_version: status.latest_version,
3672
+ platform: status.platform,
3673
+ }, { message: `Updating to ${status.latest_version}` });
3674
+ setTimeout(() => {
3675
+ try {
3676
+ node.startAppUpdate();
3677
+ }
3678
+ catch {
3679
+ // best effort after response has been sent
3680
+ }
3681
+ }, 150);
3682
+ }));
2908
3683
  app.put("/api/profile", asyncRoute(async (req, res) => {
2909
3684
  const body = req.body;
2910
3685
  const tags = Array.isArray(body.tags)
@@ -3003,6 +3778,27 @@ async function main() {
3003
3778
  const agentId = String(req.query.agent_id ?? "").trim();
3004
3779
  sendOk(res, node.getSocialMessages(limit, { agent_id: agentId || null }));
3005
3780
  });
3781
+ app.get("/api/private/state", (_req, res) => {
3782
+ sendOk(res, node.getPrivateMessagingState());
3783
+ });
3784
+ app.get("/api/private/conversations", (_req, res) => {
3785
+ sendOk(res, node.getPrivateConversations());
3786
+ });
3787
+ app.get("/api/private/messages", (req, res) => {
3788
+ const conversationId = String(req.query.conversation_id ?? "").trim();
3789
+ const limit = Number(req.query.limit ?? PRIVATE_MESSAGE_QUERY_LIMIT);
3790
+ sendOk(res, node.getPrivateMessages(conversationId, limit));
3791
+ });
3792
+ app.post("/api/private/messages/send", asyncRoute(async (req, res) => {
3793
+ const result = await node.sendPrivateMessage({
3794
+ to_agent_id: String(req.body?.to_agent_id || ""),
3795
+ recipient_encryption_public_key: String(req.body?.recipient_encryption_public_key || ""),
3796
+ body: String(req.body?.body || ""),
3797
+ });
3798
+ sendOk(res, result, {
3799
+ message: result.sent ? "Private message sent" : `Private message skipped: ${result.reason}`,
3800
+ });
3801
+ }));
3006
3802
  app.get("/api/openclaw/bridge", (_req, res) => {
3007
3803
  sendOk(res, node.getOpenClawBridgeStatus());
3008
3804
  });