@silicaclaw/cli 2026.3.20-2 → 2026.3.20-21

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