@silicaclaw/cli 2026.3.19-9 → 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 (331) hide show
  1. package/CHANGELOG.md +182 -0
  2. package/DEMO_GUIDE.md +1 -1
  3. package/INSTALL.md +53 -13
  4. package/README.md +106 -23
  5. package/VERSION +1 -1
  6. package/apps/local-console/dist/apps/local-console/src/server.d.ts +180 -14
  7. package/apps/local-console/dist/apps/local-console/src/server.js +1499 -267
  8. package/apps/local-console/dist/config/silicaclaw-defaults.json +19 -0
  9. package/apps/local-console/dist/packages/core/src/index.d.ts +2 -0
  10. package/apps/local-console/dist/packages/core/src/index.js +2 -0
  11. package/apps/local-console/dist/packages/core/src/privateCrypto.d.ts +17 -0
  12. package/apps/local-console/dist/packages/core/src/privateCrypto.js +40 -0
  13. package/apps/local-console/dist/packages/core/src/privateMessage.d.ts +23 -0
  14. package/apps/local-console/dist/packages/core/src/privateMessage.js +74 -0
  15. package/apps/local-console/dist/packages/core/src/profile.js +2 -0
  16. package/apps/local-console/dist/packages/core/src/publicProfileSummary.d.ts +4 -0
  17. package/apps/local-console/dist/packages/core/src/publicProfileSummary.js +3 -0
  18. package/apps/local-console/dist/packages/core/src/socialConfig.js +9 -5
  19. package/apps/local-console/dist/packages/core/src/types.d.ts +40 -0
  20. package/apps/local-console/dist/packages/network/src/realPreview.js +6 -2
  21. package/apps/local-console/dist/packages/network/src/relayPreview.d.ts +12 -0
  22. package/apps/local-console/dist/packages/network/src/relayPreview.js +116 -10
  23. package/apps/local-console/dist/packages/network/src/transport/udpLanBroadcastTransport.js +2 -1
  24. package/apps/local-console/dist/packages/network/src/types.d.ts +4 -0
  25. package/apps/local-console/dist/packages/network/src/webrtcPreview.js +5 -1
  26. package/apps/local-console/dist/packages/storage/config/silicaclaw-defaults.json +19 -0
  27. package/apps/local-console/dist/packages/storage/src/repos.d.ts +13 -1
  28. package/apps/local-console/dist/packages/storage/src/repos.js +19 -1
  29. package/apps/local-console/dist/packages/storage/src/socialRuntimeRepo.js +8 -4
  30. package/apps/local-console/public/app/app.js +486 -12
  31. package/apps/local-console/public/app/events.js +61 -2
  32. package/apps/local-console/public/app/network.js +176 -35
  33. package/apps/local-console/public/app/overview.js +75 -53
  34. package/apps/local-console/public/app/shell.js +18 -34
  35. package/apps/local-console/public/app/social.js +495 -93
  36. package/apps/local-console/public/app/styles.css +309 -15
  37. package/apps/local-console/public/app/template.js +182 -51
  38. package/apps/local-console/public/app/translations.js +476 -266
  39. package/apps/local-console/src/server.ts +1669 -271
  40. package/apps/public-explorer/dist/apps/public-explorer/src/server.d.ts +1 -0
  41. package/apps/public-explorer/dist/apps/public-explorer/src/server.js +41 -0
  42. package/apps/public-explorer/dist/config/silicaclaw-defaults.json +19 -0
  43. package/apps/public-explorer/public/app/app.js +22 -2
  44. package/apps/public-explorer/public/app/template.js +4 -4
  45. package/apps/public-explorer/public/app/translations.js +29 -29
  46. package/apps/public-explorer/src/server.ts +11 -1
  47. package/config/silicaclaw-defaults.json +19 -0
  48. package/dist/apps/local-console/src/server.d.ts +1 -0
  49. package/dist/apps/local-console/src/server.js +555 -0
  50. package/docs/NEW_USER_INSTALL.md +14 -10
  51. package/docs/NEW_USER_OPERATIONS.md +9 -9
  52. package/docs/OPENCLAW_BRIDGE.md +22 -7
  53. package/docs/OPENCLAW_BRIDGE_ZH.md +21 -6
  54. package/docs/RELEASE_CHECKLIST.md +95 -0
  55. package/node_modules/@silicaclaw/core/dist/config/silicaclaw-defaults.json +19 -0
  56. package/node_modules/@silicaclaw/core/dist/packages/core/src/crypto.d.ts +6 -0
  57. package/node_modules/@silicaclaw/core/dist/packages/core/src/crypto.js +50 -0
  58. package/node_modules/@silicaclaw/core/dist/packages/core/src/directory.d.ts +17 -0
  59. package/node_modules/@silicaclaw/core/dist/packages/core/src/directory.js +145 -0
  60. package/node_modules/@silicaclaw/core/dist/packages/core/src/identity.d.ts +2 -0
  61. package/node_modules/@silicaclaw/core/dist/packages/core/src/identity.js +18 -0
  62. package/node_modules/@silicaclaw/core/dist/packages/core/src/index.d.ts +14 -0
  63. package/node_modules/@silicaclaw/core/dist/packages/core/src/index.js +30 -0
  64. package/node_modules/@silicaclaw/core/dist/packages/core/src/indexing.d.ts +6 -0
  65. package/node_modules/@silicaclaw/core/dist/packages/core/src/indexing.js +43 -0
  66. package/node_modules/@silicaclaw/core/dist/packages/core/src/presence.d.ts +4 -0
  67. package/node_modules/@silicaclaw/core/dist/packages/core/src/presence.js +23 -0
  68. package/node_modules/@silicaclaw/core/dist/packages/core/src/privateCrypto.d.ts +17 -0
  69. package/node_modules/@silicaclaw/core/dist/packages/core/src/privateCrypto.js +40 -0
  70. package/node_modules/@silicaclaw/core/dist/packages/core/src/privateMessage.d.ts +23 -0
  71. package/node_modules/@silicaclaw/core/dist/packages/core/src/privateMessage.js +74 -0
  72. package/node_modules/@silicaclaw/core/dist/packages/core/src/profile.d.ts +4 -0
  73. package/node_modules/@silicaclaw/core/dist/packages/core/src/profile.js +41 -0
  74. package/node_modules/@silicaclaw/core/dist/packages/core/src/publicProfileSummary.d.ts +74 -0
  75. package/node_modules/@silicaclaw/core/dist/packages/core/src/publicProfileSummary.js +106 -0
  76. package/node_modules/@silicaclaw/core/dist/packages/core/src/socialConfig.d.ts +100 -0
  77. package/node_modules/@silicaclaw/core/dist/packages/core/src/socialConfig.js +300 -0
  78. package/node_modules/@silicaclaw/core/dist/packages/core/src/socialMessage.d.ts +19 -0
  79. package/node_modules/@silicaclaw/core/dist/packages/core/src/socialMessage.js +69 -0
  80. package/node_modules/@silicaclaw/core/dist/packages/core/src/socialResolver.d.ts +46 -0
  81. package/node_modules/@silicaclaw/core/dist/packages/core/src/socialResolver.js +237 -0
  82. package/node_modules/@silicaclaw/core/dist/packages/core/src/socialTemplate.d.ts +2 -0
  83. package/node_modules/@silicaclaw/core/dist/packages/core/src/socialTemplate.js +90 -0
  84. package/node_modules/@silicaclaw/core/dist/packages/core/src/types.d.ts +99 -0
  85. package/node_modules/@silicaclaw/core/dist/packages/core/src/types.js +2 -0
  86. package/node_modules/@silicaclaw/core/src/index.ts +2 -0
  87. package/node_modules/@silicaclaw/core/src/privateCrypto.ts +57 -0
  88. package/node_modules/@silicaclaw/core/src/privateMessage.ts +101 -0
  89. package/node_modules/@silicaclaw/core/src/profile.ts +2 -0
  90. package/node_modules/@silicaclaw/core/src/publicProfileSummary.ts +7 -0
  91. package/node_modules/@silicaclaw/core/src/socialConfig.ts +7 -5
  92. package/node_modules/@silicaclaw/core/src/types.ts +44 -0
  93. package/node_modules/@silicaclaw/network/dist/config/silicaclaw-defaults.json +19 -0
  94. package/node_modules/@silicaclaw/network/dist/packages/network/src/abstractions/messageEnvelope.d.ts +28 -0
  95. package/node_modules/@silicaclaw/network/dist/packages/network/src/abstractions/messageEnvelope.js +36 -0
  96. package/node_modules/@silicaclaw/network/dist/packages/network/src/abstractions/peerDiscovery.d.ts +43 -0
  97. package/node_modules/@silicaclaw/network/dist/packages/network/src/abstractions/peerDiscovery.js +2 -0
  98. package/node_modules/@silicaclaw/network/dist/packages/network/src/abstractions/topicCodec.d.ts +4 -0
  99. package/node_modules/@silicaclaw/network/dist/packages/network/src/abstractions/topicCodec.js +2 -0
  100. package/node_modules/@silicaclaw/network/dist/packages/network/src/abstractions/transport.d.ts +36 -0
  101. package/node_modules/@silicaclaw/network/dist/packages/network/src/abstractions/transport.js +2 -0
  102. package/node_modules/@silicaclaw/network/dist/packages/network/src/codec/jsonMessageEnvelopeCodec.d.ts +5 -0
  103. package/node_modules/@silicaclaw/network/dist/packages/network/src/codec/jsonMessageEnvelopeCodec.js +24 -0
  104. package/node_modules/@silicaclaw/network/dist/packages/network/src/codec/jsonTopicCodec.d.ts +5 -0
  105. package/node_modules/@silicaclaw/network/dist/packages/network/src/codec/jsonTopicCodec.js +12 -0
  106. package/node_modules/@silicaclaw/network/dist/packages/network/src/discovery/heartbeatPeerDiscovery.d.ts +28 -0
  107. package/node_modules/@silicaclaw/network/dist/packages/network/src/discovery/heartbeatPeerDiscovery.js +144 -0
  108. package/node_modules/@silicaclaw/network/dist/packages/network/src/index.d.ts +14 -0
  109. package/node_modules/@silicaclaw/network/dist/packages/network/src/index.js +30 -0
  110. package/node_modules/@silicaclaw/network/dist/packages/network/src/localEventBus.d.ts +9 -0
  111. package/node_modules/@silicaclaw/network/dist/packages/network/src/localEventBus.js +47 -0
  112. package/node_modules/@silicaclaw/network/dist/packages/network/src/mock.d.ts +8 -0
  113. package/node_modules/@silicaclaw/network/dist/packages/network/src/mock.js +24 -0
  114. package/node_modules/@silicaclaw/network/dist/packages/network/src/realPreview.d.ts +105 -0
  115. package/node_modules/@silicaclaw/network/dist/packages/network/src/realPreview.js +331 -0
  116. package/node_modules/@silicaclaw/network/dist/packages/network/src/relayPreview.d.ts +178 -0
  117. package/node_modules/@silicaclaw/network/dist/packages/network/src/relayPreview.js +548 -0
  118. package/node_modules/@silicaclaw/network/dist/packages/network/src/transport/udpLanBroadcastTransport.d.ts +23 -0
  119. package/node_modules/@silicaclaw/network/dist/packages/network/src/transport/udpLanBroadcastTransport.js +154 -0
  120. package/node_modules/@silicaclaw/network/dist/packages/network/src/types.d.ts +10 -0
  121. package/node_modules/@silicaclaw/network/dist/packages/network/src/types.js +2 -0
  122. package/node_modules/@silicaclaw/network/dist/packages/network/src/webrtcPreview.d.ts +163 -0
  123. package/node_modules/@silicaclaw/network/dist/packages/network/src/webrtcPreview.js +848 -0
  124. package/node_modules/@silicaclaw/network/src/realPreview.ts +3 -2
  125. package/node_modules/@silicaclaw/network/src/relayPreview.ts +125 -12
  126. package/node_modules/@silicaclaw/network/src/transport/udpLanBroadcastTransport.ts +2 -1
  127. package/node_modules/@silicaclaw/network/src/types.ts +2 -0
  128. package/node_modules/@silicaclaw/network/src/webrtcPreview.ts +2 -1
  129. package/node_modules/@silicaclaw/storage/config/silicaclaw-defaults.json +19 -0
  130. package/node_modules/@silicaclaw/storage/dist/config/silicaclaw-defaults.json +19 -0
  131. package/node_modules/@silicaclaw/storage/dist/packages/core/src/crypto.d.ts +6 -0
  132. package/node_modules/@silicaclaw/storage/dist/packages/core/src/crypto.js +50 -0
  133. package/node_modules/@silicaclaw/storage/dist/packages/core/src/directory.d.ts +17 -0
  134. package/node_modules/@silicaclaw/storage/dist/packages/core/src/directory.js +145 -0
  135. package/node_modules/@silicaclaw/storage/dist/packages/core/src/identity.d.ts +2 -0
  136. package/node_modules/@silicaclaw/storage/dist/packages/core/src/identity.js +18 -0
  137. package/node_modules/@silicaclaw/storage/dist/packages/core/src/index.d.ts +14 -0
  138. package/node_modules/@silicaclaw/storage/dist/packages/core/src/index.js +30 -0
  139. package/node_modules/@silicaclaw/storage/dist/packages/core/src/indexing.d.ts +6 -0
  140. package/node_modules/@silicaclaw/storage/dist/packages/core/src/indexing.js +43 -0
  141. package/node_modules/@silicaclaw/storage/dist/packages/core/src/presence.d.ts +4 -0
  142. package/node_modules/@silicaclaw/storage/dist/packages/core/src/presence.js +23 -0
  143. package/node_modules/@silicaclaw/storage/dist/packages/core/src/privateCrypto.d.ts +17 -0
  144. package/node_modules/@silicaclaw/storage/dist/packages/core/src/privateCrypto.js +40 -0
  145. package/node_modules/@silicaclaw/storage/dist/packages/core/src/privateMessage.d.ts +23 -0
  146. package/node_modules/@silicaclaw/storage/dist/packages/core/src/privateMessage.js +74 -0
  147. package/node_modules/@silicaclaw/storage/dist/packages/core/src/profile.d.ts +4 -0
  148. package/node_modules/@silicaclaw/storage/dist/packages/core/src/profile.js +41 -0
  149. package/node_modules/@silicaclaw/storage/dist/packages/core/src/publicProfileSummary.d.ts +74 -0
  150. package/node_modules/@silicaclaw/storage/dist/packages/core/src/publicProfileSummary.js +106 -0
  151. package/node_modules/@silicaclaw/storage/dist/packages/core/src/socialConfig.d.ts +100 -0
  152. package/node_modules/@silicaclaw/storage/dist/packages/core/src/socialConfig.js +300 -0
  153. package/node_modules/@silicaclaw/storage/dist/packages/core/src/socialMessage.d.ts +19 -0
  154. package/node_modules/@silicaclaw/storage/dist/packages/core/src/socialMessage.js +69 -0
  155. package/node_modules/@silicaclaw/storage/dist/packages/core/src/socialResolver.d.ts +46 -0
  156. package/node_modules/@silicaclaw/storage/dist/packages/core/src/socialResolver.js +237 -0
  157. package/node_modules/@silicaclaw/storage/dist/packages/core/src/socialTemplate.d.ts +2 -0
  158. package/node_modules/@silicaclaw/storage/dist/packages/core/src/socialTemplate.js +90 -0
  159. package/node_modules/@silicaclaw/storage/dist/packages/core/src/types.d.ts +99 -0
  160. package/node_modules/@silicaclaw/storage/dist/packages/core/src/types.js +2 -0
  161. package/node_modules/@silicaclaw/storage/dist/packages/storage/config/silicaclaw-defaults.json +19 -0
  162. package/node_modules/@silicaclaw/storage/dist/packages/storage/src/index.d.ts +3 -0
  163. package/node_modules/@silicaclaw/storage/dist/packages/storage/src/index.js +19 -0
  164. package/node_modules/@silicaclaw/storage/dist/packages/storage/src/jsonRepo.d.ts +7 -0
  165. package/node_modules/@silicaclaw/storage/dist/packages/storage/src/jsonRepo.js +29 -0
  166. package/node_modules/@silicaclaw/storage/dist/packages/storage/src/repos.d.ts +73 -0
  167. package/node_modules/@silicaclaw/storage/dist/packages/storage/src/repos.js +85 -0
  168. package/node_modules/@silicaclaw/storage/dist/packages/storage/src/socialRuntimeRepo.d.ts +5 -0
  169. package/node_modules/@silicaclaw/storage/dist/packages/storage/src/socialRuntimeRepo.js +57 -0
  170. package/node_modules/@silicaclaw/storage/dist/socialRuntimeRepo.js +8 -4
  171. package/node_modules/@silicaclaw/storage/src/repos.ts +31 -1
  172. package/node_modules/@silicaclaw/storage/src/socialRuntimeRepo.ts +5 -4
  173. package/node_modules/@silicaclaw/storage/tsconfig.json +1 -6
  174. package/openclaw-skills/silicaclaw-bridge-setup/SKILL.md +165 -0
  175. package/openclaw-skills/silicaclaw-bridge-setup/VERSION +1 -0
  176. package/openclaw-skills/silicaclaw-bridge-setup/agents/openai.yaml +6 -0
  177. package/openclaw-skills/silicaclaw-bridge-setup/manifest.json +27 -0
  178. package/openclaw-skills/silicaclaw-bridge-setup/references/owner-dialogue-cheatsheet-zh.md +58 -0
  179. package/openclaw-skills/silicaclaw-bridge-setup/references/runtime-setup.md +43 -0
  180. package/openclaw-skills/silicaclaw-bridge-setup/references/troubleshooting.md +24 -0
  181. package/openclaw-skills/silicaclaw-broadcast/SKILL.md +150 -0
  182. package/openclaw-skills/silicaclaw-broadcast/VERSION +1 -1
  183. package/openclaw-skills/silicaclaw-broadcast/agents/openai.yaml +2 -2
  184. package/openclaw-skills/silicaclaw-broadcast/manifest.json +4 -3
  185. package/openclaw-skills/silicaclaw-broadcast/references/owner-dialogue-cheatsheet-zh.md +81 -0
  186. package/openclaw-skills/silicaclaw-network-config/SKILL.md +158 -0
  187. package/openclaw-skills/silicaclaw-network-config/VERSION +1 -0
  188. package/openclaw-skills/silicaclaw-network-config/agents/openai.yaml +6 -0
  189. package/openclaw-skills/silicaclaw-network-config/manifest.json +27 -0
  190. package/openclaw-skills/silicaclaw-network-config/references/network-modes.md +22 -0
  191. package/openclaw-skills/silicaclaw-network-config/references/owner-dialogue-cheatsheet-zh.md +47 -0
  192. package/openclaw-skills/silicaclaw-network-config/references/public-discovery.md +22 -0
  193. package/openclaw-skills/silicaclaw-owner-push/SKILL.md +235 -0
  194. package/openclaw-skills/silicaclaw-owner-push/VERSION +1 -0
  195. package/openclaw-skills/silicaclaw-owner-push/agents/openai.yaml +6 -0
  196. package/openclaw-skills/silicaclaw-owner-push/manifest.json +30 -0
  197. package/openclaw-skills/silicaclaw-owner-push/references/owner-dialogue-cheatsheet-zh.md +87 -0
  198. package/openclaw-skills/silicaclaw-owner-push/references/push-routing-policy.md +43 -0
  199. package/openclaw-skills/silicaclaw-owner-push/references/runtime-setup.md +44 -0
  200. package/openclaw-skills/silicaclaw-owner-push/scripts/owner-push-forwarder.mjs +356 -0
  201. package/openclaw-skills/silicaclaw-owner-push/scripts/send-to-owner-via-openclaw.mjs +69 -0
  202. package/package.json +5 -1
  203. package/packages/core/dist/config/silicaclaw-defaults.json +19 -0
  204. package/packages/core/dist/packages/core/src/crypto.d.ts +6 -0
  205. package/packages/core/dist/packages/core/src/crypto.js +50 -0
  206. package/packages/core/dist/packages/core/src/directory.d.ts +17 -0
  207. package/packages/core/dist/packages/core/src/directory.js +145 -0
  208. package/packages/core/dist/packages/core/src/identity.d.ts +2 -0
  209. package/packages/core/dist/packages/core/src/identity.js +18 -0
  210. package/packages/core/dist/packages/core/src/index.d.ts +14 -0
  211. package/packages/core/dist/packages/core/src/index.js +30 -0
  212. package/packages/core/dist/packages/core/src/indexing.d.ts +6 -0
  213. package/packages/core/dist/packages/core/src/indexing.js +43 -0
  214. package/packages/core/dist/packages/core/src/presence.d.ts +4 -0
  215. package/packages/core/dist/packages/core/src/presence.js +23 -0
  216. package/packages/core/dist/packages/core/src/privateCrypto.d.ts +17 -0
  217. package/packages/core/dist/packages/core/src/privateCrypto.js +40 -0
  218. package/packages/core/dist/packages/core/src/privateMessage.d.ts +23 -0
  219. package/packages/core/dist/packages/core/src/privateMessage.js +74 -0
  220. package/packages/core/dist/packages/core/src/profile.d.ts +4 -0
  221. package/packages/core/dist/packages/core/src/profile.js +41 -0
  222. package/packages/core/dist/packages/core/src/publicProfileSummary.d.ts +74 -0
  223. package/packages/core/dist/packages/core/src/publicProfileSummary.js +106 -0
  224. package/packages/core/dist/packages/core/src/socialConfig.d.ts +100 -0
  225. package/packages/core/dist/packages/core/src/socialConfig.js +300 -0
  226. package/packages/core/dist/packages/core/src/socialMessage.d.ts +19 -0
  227. package/packages/core/dist/packages/core/src/socialMessage.js +69 -0
  228. package/packages/core/dist/packages/core/src/socialResolver.d.ts +46 -0
  229. package/packages/core/dist/packages/core/src/socialResolver.js +237 -0
  230. package/packages/core/dist/packages/core/src/socialTemplate.d.ts +2 -0
  231. package/packages/core/dist/packages/core/src/socialTemplate.js +90 -0
  232. package/packages/core/dist/packages/core/src/types.d.ts +99 -0
  233. package/packages/core/dist/packages/core/src/types.js +2 -0
  234. package/packages/core/src/index.ts +2 -0
  235. package/packages/core/src/privateCrypto.ts +57 -0
  236. package/packages/core/src/privateMessage.ts +101 -0
  237. package/packages/core/src/profile.ts +2 -0
  238. package/packages/core/src/publicProfileSummary.ts +7 -0
  239. package/packages/core/src/socialConfig.ts +7 -5
  240. package/packages/core/src/types.ts +44 -0
  241. package/packages/network/dist/config/silicaclaw-defaults.json +19 -0
  242. package/packages/network/dist/packages/network/src/abstractions/messageEnvelope.d.ts +28 -0
  243. package/packages/network/dist/packages/network/src/abstractions/messageEnvelope.js +36 -0
  244. package/packages/network/dist/packages/network/src/abstractions/peerDiscovery.d.ts +43 -0
  245. package/packages/network/dist/packages/network/src/abstractions/peerDiscovery.js +2 -0
  246. package/packages/network/dist/packages/network/src/abstractions/topicCodec.d.ts +4 -0
  247. package/packages/network/dist/packages/network/src/abstractions/topicCodec.js +2 -0
  248. package/packages/network/dist/packages/network/src/abstractions/transport.d.ts +36 -0
  249. package/packages/network/dist/packages/network/src/abstractions/transport.js +2 -0
  250. package/packages/network/dist/packages/network/src/codec/jsonMessageEnvelopeCodec.d.ts +5 -0
  251. package/packages/network/dist/packages/network/src/codec/jsonMessageEnvelopeCodec.js +24 -0
  252. package/packages/network/dist/packages/network/src/codec/jsonTopicCodec.d.ts +5 -0
  253. package/packages/network/dist/packages/network/src/codec/jsonTopicCodec.js +12 -0
  254. package/packages/network/dist/packages/network/src/discovery/heartbeatPeerDiscovery.d.ts +28 -0
  255. package/packages/network/dist/packages/network/src/discovery/heartbeatPeerDiscovery.js +144 -0
  256. package/packages/network/dist/packages/network/src/index.d.ts +14 -0
  257. package/packages/network/dist/packages/network/src/index.js +30 -0
  258. package/packages/network/dist/packages/network/src/localEventBus.d.ts +9 -0
  259. package/packages/network/dist/packages/network/src/localEventBus.js +47 -0
  260. package/packages/network/dist/packages/network/src/mock.d.ts +8 -0
  261. package/packages/network/dist/packages/network/src/mock.js +24 -0
  262. package/packages/network/dist/packages/network/src/realPreview.d.ts +105 -0
  263. package/packages/network/dist/packages/network/src/realPreview.js +331 -0
  264. package/packages/network/dist/packages/network/src/relayPreview.d.ts +178 -0
  265. package/packages/network/dist/packages/network/src/relayPreview.js +548 -0
  266. package/packages/network/dist/packages/network/src/transport/udpLanBroadcastTransport.d.ts +23 -0
  267. package/packages/network/dist/packages/network/src/transport/udpLanBroadcastTransport.js +154 -0
  268. package/packages/network/dist/packages/network/src/types.d.ts +10 -0
  269. package/packages/network/dist/packages/network/src/types.js +2 -0
  270. package/packages/network/dist/packages/network/src/webrtcPreview.d.ts +163 -0
  271. package/packages/network/dist/packages/network/src/webrtcPreview.js +848 -0
  272. package/packages/network/src/realPreview.ts +3 -2
  273. package/packages/network/src/relayPreview.ts +125 -12
  274. package/packages/network/src/transport/udpLanBroadcastTransport.ts +2 -1
  275. package/packages/network/src/types.ts +2 -0
  276. package/packages/network/src/webrtcPreview.ts +2 -1
  277. package/packages/storage/config/silicaclaw-defaults.json +19 -0
  278. package/packages/storage/dist/config/silicaclaw-defaults.json +19 -0
  279. package/packages/storage/dist/packages/core/src/crypto.d.ts +6 -0
  280. package/packages/storage/dist/packages/core/src/crypto.js +50 -0
  281. package/packages/storage/dist/packages/core/src/directory.d.ts +17 -0
  282. package/packages/storage/dist/packages/core/src/directory.js +145 -0
  283. package/packages/storage/dist/packages/core/src/identity.d.ts +2 -0
  284. package/packages/storage/dist/packages/core/src/identity.js +18 -0
  285. package/packages/storage/dist/packages/core/src/index.d.ts +14 -0
  286. package/packages/storage/dist/packages/core/src/index.js +30 -0
  287. package/packages/storage/dist/packages/core/src/indexing.d.ts +6 -0
  288. package/packages/storage/dist/packages/core/src/indexing.js +43 -0
  289. package/packages/storage/dist/packages/core/src/presence.d.ts +4 -0
  290. package/packages/storage/dist/packages/core/src/presence.js +23 -0
  291. package/packages/storage/dist/packages/core/src/privateCrypto.d.ts +17 -0
  292. package/packages/storage/dist/packages/core/src/privateCrypto.js +40 -0
  293. package/packages/storage/dist/packages/core/src/privateMessage.d.ts +23 -0
  294. package/packages/storage/dist/packages/core/src/privateMessage.js +74 -0
  295. package/packages/storage/dist/packages/core/src/profile.d.ts +4 -0
  296. package/packages/storage/dist/packages/core/src/profile.js +41 -0
  297. package/packages/storage/dist/packages/core/src/publicProfileSummary.d.ts +74 -0
  298. package/packages/storage/dist/packages/core/src/publicProfileSummary.js +106 -0
  299. package/packages/storage/dist/packages/core/src/socialConfig.d.ts +100 -0
  300. package/packages/storage/dist/packages/core/src/socialConfig.js +300 -0
  301. package/packages/storage/dist/packages/core/src/socialMessage.d.ts +19 -0
  302. package/packages/storage/dist/packages/core/src/socialMessage.js +69 -0
  303. package/packages/storage/dist/packages/core/src/socialResolver.d.ts +46 -0
  304. package/packages/storage/dist/packages/core/src/socialResolver.js +237 -0
  305. package/packages/storage/dist/packages/core/src/socialTemplate.d.ts +2 -0
  306. package/packages/storage/dist/packages/core/src/socialTemplate.js +90 -0
  307. package/packages/storage/dist/packages/core/src/types.d.ts +99 -0
  308. package/packages/storage/dist/packages/core/src/types.js +2 -0
  309. package/packages/storage/dist/packages/storage/config/silicaclaw-defaults.json +19 -0
  310. package/packages/storage/dist/packages/storage/src/index.d.ts +3 -0
  311. package/packages/storage/dist/packages/storage/src/index.js +19 -0
  312. package/packages/storage/dist/packages/storage/src/jsonRepo.d.ts +7 -0
  313. package/packages/storage/dist/packages/storage/src/jsonRepo.js +29 -0
  314. package/packages/storage/dist/packages/storage/src/repos.d.ts +73 -0
  315. package/packages/storage/dist/packages/storage/src/repos.js +85 -0
  316. package/packages/storage/dist/packages/storage/src/socialRuntimeRepo.d.ts +5 -0
  317. package/packages/storage/dist/packages/storage/src/socialRuntimeRepo.js +57 -0
  318. package/packages/storage/dist/socialRuntimeRepo.js +8 -4
  319. package/packages/storage/src/repos.ts +31 -1
  320. package/packages/storage/src/socialRuntimeRepo.ts +5 -4
  321. package/packages/storage/tsconfig.json +1 -6
  322. package/scripts/functional-check.mjs +35 -6
  323. package/scripts/install-openclaw-skill.mjs +9 -2
  324. package/scripts/openclaw-bridge-adapter.mjs +3 -1
  325. package/scripts/openclaw-bridge-client.mjs +3 -1
  326. package/scripts/openclaw-runtime-demo.mjs +3 -1
  327. package/scripts/quickstart.sh +14 -10
  328. package/scripts/release-pack.mjs +59 -1
  329. package/scripts/silicaclaw-cli.mjs +166 -51
  330. package/scripts/silicaclaw-gateway.mjs +410 -84
  331. package/scripts/validate-openclaw-skill.mjs +98 -21
@@ -1,37 +1,46 @@
1
1
  import express, { NextFunction, Request, Response } from "express";
2
2
  import cors from "cors";
3
- import { execFile, spawnSync } from "child_process";
3
+ import { execFile, spawn, spawnSync } from "child_process";
4
4
  import { resolve } from "path";
5
5
  import { accessSync, constants, copyFileSync, existsSync, mkdirSync, readFileSync, readdirSync, statSync } from "fs";
6
6
  import { createHash } from "crypto";
7
- import { hostname } from "os";
7
+ import { homedir, hostname } from "os";
8
8
  import { promisify } from "util";
9
+ import defaults from "../../../config/silicaclaw-defaults.json";
9
10
  import {
10
11
  AgentIdentity,
11
12
  DirectoryState,
12
13
  IndexRefRecord,
14
+ PrivateEncryptionKeyPair,
15
+ PrivateMessageReceiptRecord,
16
+ PrivateMessageRecord,
13
17
  PresenceRecord,
14
18
  ProfileInput,
15
19
  PublicProfile,
16
20
  PublicProfileSummary,
17
21
  SignedProfileRecord,
18
22
  buildPublicProfileSummary,
19
- buildIndexRecords,
20
23
  cleanupExpiredPresence,
21
24
  createDefaultProfileInput,
22
25
  createEmptyDirectoryState,
23
26
  createIdentity,
27
+ createPrivateEncryptionKeyPair,
24
28
  dedupeIndex,
29
+ decryptPrivatePayload,
25
30
  ensureDefaultSocialMd,
31
+ encryptPrivatePayload,
26
32
  ingestIndexRecord,
27
33
  ingestPresenceRecord,
28
34
  ingestProfileRecord,
29
35
  isAgentOnline,
36
+ rebuildIndexForProfile,
30
37
  loadSocialConfig,
31
38
  getSocialConfigSearchPaths,
32
39
  resolveIdentityWithSocial,
33
40
  resolveProfileInputWithSocial,
34
41
  searchDirectory,
42
+ signPrivateMessage,
43
+ signPrivateMessageReceipt,
35
44
  signSocialMessage,
36
45
  signSocialMessageObservation,
37
46
  signPresence,
@@ -44,6 +53,8 @@ import {
44
53
  verifySocialMessage,
45
54
  verifySocialMessageObservation,
46
55
  verifyPresence,
56
+ verifyPrivateMessage,
57
+ verifyPrivateMessageReceipt,
47
58
  verifyProfile,
48
59
  } from "@silicaclaw/core";
49
60
  import {
@@ -60,6 +71,9 @@ import {
60
71
  CacheRepo,
61
72
  IdentityRepo,
62
73
  LogRepo,
74
+ PrivateEncryptionKeyRepo,
75
+ PrivateMessageReceiptRepo,
76
+ PrivateMessageRepo,
63
77
  ProfileRepo,
64
78
  SocialMessageGovernanceConfig,
65
79
  SocialMessageGovernanceRepo,
@@ -79,21 +93,36 @@ const NETWORK_MAX_PAST_DRIFT_MS = Number(process.env.NETWORK_MAX_PAST_DRIFT_MS |
79
93
  const NETWORK_HEARTBEAT_INTERVAL_MS = Number(process.env.NETWORK_HEARTBEAT_INTERVAL_MS || 12_000);
80
94
  const NETWORK_PEER_STALE_AFTER_MS = Number(process.env.NETWORK_PEER_STALE_AFTER_MS || 45_000);
81
95
  const OPENCLAW_GATEWAY_HOST = "127.0.0.1";
82
- const OPENCLAW_GATEWAY_PORT = 18_789;
96
+ const DEFAULT_NETWORK_MODE = defaults.network.default_mode as "global-preview";
97
+ const DEFAULT_NETWORK_NAMESPACE = defaults.network.default_namespace;
98
+ const DEFAULT_NETWORK_PORT = defaults.ports.network_default;
99
+ const DEFAULT_GLOBAL_SIGNALING_URL = defaults.network.global_preview.relay_url;
100
+ const DEFAULT_GLOBAL_ROOM = defaults.network.global_preview.room;
101
+ const DEFAULT_BRIDGE_API_BASE = defaults.bridge.api_base;
102
+ const OPENCLAW_GATEWAY_PORT = defaults.ports.openclaw_gateway;
83
103
  const OPENCLAW_GATEWAY_URL = `http://${OPENCLAW_GATEWAY_HOST}:${OPENCLAW_GATEWAY_PORT}/`;
104
+ const OPENCLAW_RUNTIME_CACHE_MS = 15_000;
105
+ const OPENCLAW_BRIDGE_STATUS_CACHE_MS = 5_000;
84
106
  const NETWORK_PEER_REMOVE_AFTER_MS = Number(process.env.NETWORK_PEER_REMOVE_AFTER_MS || 180_000);
107
+ const DIRECTORY_REMOTE_PROFILE_SOFT_LIMIT = Number(process.env.DIRECTORY_REMOTE_PROFILE_SOFT_LIMIT || 1000);
85
108
  const NETWORK_UDP_BIND_ADDRESS = process.env.NETWORK_UDP_BIND_ADDRESS || "0.0.0.0";
86
109
  const NETWORK_UDP_BROADCAST_ADDRESS = process.env.NETWORK_UDP_BROADCAST_ADDRESS || "255.255.255.255";
87
110
  const NETWORK_PEER_ID = process.env.NETWORK_PEER_ID;
88
111
  const NETWORK_MODE = process.env.NETWORK_MODE || "";
89
- const WEBRTC_SIGNALING_URL = process.env.WEBRTC_SIGNALING_URL || "https://relay.silicaclaw.com";
112
+ const WEBRTC_SIGNALING_URL = process.env.WEBRTC_SIGNALING_URL || DEFAULT_GLOBAL_SIGNALING_URL;
90
113
  const WEBRTC_SIGNALING_URLS = process.env.WEBRTC_SIGNALING_URLS || "";
91
- const WEBRTC_ROOM = process.env.WEBRTC_ROOM || "silicaclaw-global-preview";
114
+ const WEBRTC_ROOM = process.env.WEBRTC_ROOM || DEFAULT_GLOBAL_ROOM;
92
115
  const WEBRTC_SEED_PEERS = process.env.WEBRTC_SEED_PEERS || "";
93
116
  const WEBRTC_BOOTSTRAP_HINTS = process.env.WEBRTC_BOOTSTRAP_HINTS || "";
94
117
  const PROFILE_VERSION = "v0.9";
95
118
  const SOCIAL_MESSAGE_TOPIC = "social.message";
96
119
  const SOCIAL_MESSAGE_OBSERVATION_TOPIC = "social.message.observation";
120
+ const PRIVATE_MESSAGE_TOPIC = "private.message";
121
+ const PRIVATE_MESSAGE_RECEIPT_TOPIC = "private.message.receipt";
122
+ const PRIVATE_MESSAGE_HISTORY_LIMIT = Number(process.env.PRIVATE_MESSAGE_HISTORY_LIMIT || 1000);
123
+ const PRIVATE_MESSAGE_RECEIPT_HISTORY_LIMIT = Number(process.env.PRIVATE_MESSAGE_RECEIPT_HISTORY_LIMIT || 2000);
124
+ const PRIVATE_MESSAGE_QUERY_LIMIT = Number(process.env.PRIVATE_MESSAGE_QUERY_LIMIT || 100);
125
+ const PRIVATE_MESSAGE_PERSIST_DEBOUNCE_MS = Number(process.env.PRIVATE_MESSAGE_PERSIST_DEBOUNCE_MS || 750);
97
126
  const DEFAULT_SOCIAL_MESSAGE_CHANNEL = "global";
98
127
  const SOCIAL_MESSAGE_MAX_BODY_CHARS = Number(process.env.SOCIAL_MESSAGE_MAX_BODY_CHARS || 500);
99
128
  const SOCIAL_MESSAGE_HISTORY_LIMIT = Number(process.env.SOCIAL_MESSAGE_HISTORY_LIMIT || 100);
@@ -105,6 +134,14 @@ const SOCIAL_MESSAGE_DUPLICATE_WINDOW_MS = Number(process.env.SOCIAL_MESSAGE_DUP
105
134
  const SOCIAL_MESSAGE_MAX_FUTURE_MS = Number(process.env.SOCIAL_MESSAGE_MAX_FUTURE_MS || 30_000);
106
135
  const SOCIAL_MESSAGE_MAX_AGE_MS = Number(process.env.SOCIAL_MESSAGE_MAX_AGE_MS || 15 * 60_000);
107
136
  const SOCIAL_MESSAGE_OBSERVATION_HISTORY_LIMIT = Number(process.env.SOCIAL_MESSAGE_OBSERVATION_HISTORY_LIMIT || 500);
137
+ const SOCIAL_MESSAGE_REPLAY_WINDOW_MS = Number(process.env.SOCIAL_MESSAGE_REPLAY_WINDOW_MS || 10 * 60_000);
138
+ const SOCIAL_MESSAGE_REPLAY_MAX_PER_BROADCAST = Number(process.env.SOCIAL_MESSAGE_REPLAY_MAX_PER_BROADCAST || 3);
139
+ const SOCIAL_MESSAGE_REPLAY_REFRESH_INTERVAL_MS = Number(
140
+ process.env.SOCIAL_MESSAGE_REPLAY_REFRESH_INTERVAL_MS || 120_000
141
+ );
142
+ const PROFILE_RELAY_REFRESH_INTERVAL_MS = Number(
143
+ process.env.PROFILE_RELAY_REFRESH_INTERVAL_MS || 120_000
144
+ );
108
145
  const SOCIAL_MESSAGE_BLOCKED_AGENT_IDS = new Set(
109
146
  dedupeStrings(parseListEnv(process.env.SOCIAL_MESSAGE_BLOCKED_AGENT_IDS || ""))
110
147
  );
@@ -115,23 +152,79 @@ const execFileAsync = promisify(execFile);
115
152
  const OPENCLAW_SKILL_NAME = "silicaclaw-broadcast";
116
153
 
117
154
  function readWorkspaceVersion(workspaceRoot: string): string {
118
- const pkgFile = resolve(workspaceRoot, "package.json");
119
- if (existsSync(pkgFile)) {
120
- try {
121
- const pkg = JSON.parse(readFileSync(pkgFile, "utf8")) as { version?: string };
122
- if (pkg.version) return String(pkg.version);
123
- } catch {
124
- // ignore
155
+ const candidates = [
156
+ workspaceRoot,
157
+ process.cwd(),
158
+ resolve(__dirname, "..", "..", ".."),
159
+ resolve(__dirname, "..", "..", "..", ".."),
160
+ ].filter((dir, index, list) => dir && list.indexOf(dir) === index);
161
+ for (const candidate of candidates) {
162
+ const pkgFile = resolve(candidate, "package.json");
163
+ if (existsSync(pkgFile)) {
164
+ try {
165
+ const pkg = JSON.parse(readFileSync(pkgFile, "utf8")) as { version?: string; name?: string };
166
+ if (pkg.version && (pkg.name === "@silicaclaw/cli" || existsSync(resolve(candidate, "apps", "local-console")))) {
167
+ return String(pkg.version);
168
+ }
169
+ } catch {
170
+ // ignore
171
+ }
172
+ }
173
+ const versionFile = resolve(candidate, "VERSION");
174
+ if (existsSync(versionFile)) {
175
+ const raw = readFileSync(versionFile, "utf8").trim();
176
+ if (raw) return raw;
125
177
  }
126
- }
127
- const versionFile = resolve(workspaceRoot, "VERSION");
128
- if (existsSync(versionFile)) {
129
- const raw = readFileSync(versionFile, "utf8").trim();
130
- if (raw) return raw;
131
178
  }
132
179
  return "unknown";
133
180
  }
134
181
 
182
+ function normalizeVersionText(value: unknown): string {
183
+ const text = String(value || "").trim();
184
+ return text.startsWith("v") ? text.slice(1) : text;
185
+ }
186
+
187
+ function formatBytesToMiB(value: number): number {
188
+ return Math.round((value / (1024 * 1024)) * 10) / 10;
189
+ }
190
+
191
+ function tokenizeVersion(value: unknown): Array<number | string> {
192
+ return normalizeVersionText(value)
193
+ .split(/[^0-9A-Za-z]+/)
194
+ .map((token) => token.trim())
195
+ .filter(Boolean)
196
+ .map((token) => (/^\d+$/.test(token) ? Number(token) : token.toLowerCase()));
197
+ }
198
+
199
+ function compareVersionTokens(left: unknown, right: unknown): number {
200
+ const leftTokens = tokenizeVersion(left);
201
+ const rightTokens = tokenizeVersion(right);
202
+ const maxLength = Math.max(leftTokens.length, rightTokens.length);
203
+ for (let index = 0; index < maxLength; index += 1) {
204
+ const leftToken = leftTokens[index];
205
+ const rightToken = rightTokens[index];
206
+ if (leftToken === undefined && rightToken === undefined) return 0;
207
+ if (leftToken === undefined) return -1;
208
+ if (rightToken === undefined) return 1;
209
+ if (typeof leftToken === "number" && typeof rightToken === "number") {
210
+ if (leftToken !== rightToken) return leftToken > rightToken ? 1 : -1;
211
+ continue;
212
+ }
213
+ const leftText = String(leftToken);
214
+ const rightText = String(rightToken);
215
+ if (leftText !== rightText) return leftText.localeCompare(rightText);
216
+ }
217
+ return 0;
218
+ }
219
+
220
+ function userNpmCacheDir(): string {
221
+ return resolve(homedir(), ".silicaclaw", "npm-cache");
222
+ }
223
+
224
+ function userShimPath(): string {
225
+ return resolve(homedir(), ".silicaclaw", "bin", "silicaclaw");
226
+ }
227
+
135
228
  function resolveWorkspaceRoot(cwd = process.cwd()): string {
136
229
  if (existsSync(resolve(cwd, "apps", "local-console", "package.json"))) {
137
230
  return cwd;
@@ -143,7 +236,36 @@ function resolveWorkspaceRoot(cwd = process.cwd()): string {
143
236
  return cwd;
144
237
  }
145
238
 
239
+ function resolveProjectRoot(appRoot: string, cwd = process.cwd()): string {
240
+ const envAppRoot = String(process.env.SILICACLAW_APP_DIR || "").trim();
241
+ if (
242
+ envAppRoot &&
243
+ existsSync(resolve(envAppRoot, "apps", "local-console", "package.json")) &&
244
+ existsSync(resolve(envAppRoot, "package.json"))
245
+ ) {
246
+ return resolve(envAppRoot);
247
+ }
248
+ const envRoot = String(process.env.SILICACLAW_WORKSPACE_DIR || "").trim();
249
+ if (envRoot) {
250
+ return resolve(envRoot);
251
+ }
252
+ if (
253
+ existsSync(resolve(appRoot, "apps", "local-console", "package.json")) &&
254
+ existsSync(resolve(appRoot, "package.json"))
255
+ ) {
256
+ return appRoot;
257
+ }
258
+ if (!existsSync(resolve(cwd, "apps", "local-console", "package.json"))) {
259
+ return resolve(cwd);
260
+ }
261
+ return appRoot;
262
+ }
263
+
146
264
  function resolveStorageRoot(workspaceRoot: string, cwd = process.cwd()): string {
265
+ const home = process.env.HOME || homedir();
266
+ if (home) {
267
+ return resolve(home, ".silicaclaw", "local-console");
268
+ }
147
269
  const appRoot = resolve(workspaceRoot, "apps", "local-console");
148
270
  if (existsSync(resolve(appRoot, "package.json"))) {
149
271
  return appRoot;
@@ -151,6 +273,13 @@ function resolveStorageRoot(workspaceRoot: string, cwd = process.cwd()): string
151
273
  return cwd;
152
274
  }
153
275
 
276
+ function defaultOpenClawSourceDir(rootDir: string): string {
277
+ if (existsSync(resolve(rootDir, "openclaw.mjs")) || existsSync(resolve(rootDir, "package.json"))) {
278
+ return rootDir;
279
+ }
280
+ return resolve(rootDir, "..", "openclaw");
281
+ }
282
+
154
283
  function resolveExecutableInPath(binName: string): string | null {
155
284
  const pathValue = String(process.env.PATH || "").trim();
156
285
  if (!pathValue) return null;
@@ -213,6 +342,50 @@ function summarizeSkillReadme(filePath: string) {
213
342
  }
214
343
  }
215
344
 
345
+ function readDialogueCheatsheetPreview(filePath: string, limit = 6) {
346
+ if (!filePath || !existsSync(filePath)) return [];
347
+ try {
348
+ return readFileSync(filePath, "utf8")
349
+ .split(/\r?\n/)
350
+ .map((line) => line.trim())
351
+ .filter((line) => line.startsWith("- "))
352
+ .map((line) => line.slice(2).trim())
353
+ .filter(Boolean)
354
+ .slice(0, limit);
355
+ } catch {
356
+ return [];
357
+ }
358
+ }
359
+
360
+ function readDialogueCheatsheetSections(filePath: string, maxSections = 3, maxItemsPerSection = 5) {
361
+ if (!filePath || !existsSync(filePath)) return [];
362
+ try {
363
+ const lines = readFileSync(filePath, "utf8").split(/\r?\n/);
364
+ const sections: Array<{ title: string; items: string[] }> = [];
365
+ let current: { title: string; items: string[] } | null = null;
366
+ for (const rawLine of lines) {
367
+ const line = rawLine.trim();
368
+ if (line.startsWith("## ")) {
369
+ if (current && current.items.length) sections.push(current);
370
+ current = { title: line.slice(3).trim(), items: [] };
371
+ continue;
372
+ }
373
+ if (line.startsWith("- ")) {
374
+ if (!current) {
375
+ current = { title: "Examples", items: [] };
376
+ }
377
+ if (current.items.length < maxItemsPerSection) {
378
+ current.items.push(line.slice(2).trim());
379
+ }
380
+ }
381
+ }
382
+ if (current && current.items.length) sections.push(current);
383
+ return sections.slice(0, maxSections);
384
+ } catch {
385
+ return [];
386
+ }
387
+ }
388
+
216
389
  function detectOpenClawInstallation(workspaceRoot: string) {
217
390
  const workspaceDir = resolve(workspaceRoot, ".openclaw");
218
391
  const homeDir = resolve(process.env.HOME || "", ".openclaw");
@@ -265,7 +438,7 @@ function detectOpenClawInstallation(workspaceRoot: string) {
265
438
 
266
439
  function readOpenClawConfiguredGateway(workspaceRoot: string) {
267
440
  const configuredSourceDir = String(process.env.OPENCLAW_SOURCE_DIR || "").trim();
268
- const defaultSourceDir = resolve(workspaceRoot, "..", "openclaw");
441
+ const defaultSourceDir = defaultOpenClawSourceDir(workspaceRoot);
269
442
  const sourceDir = configuredSourceDir || defaultSourceDir;
270
443
  const homeDir = resolve(process.env.HOME || "", ".openclaw");
271
444
  const explicitConfigPath = String(process.env.OPENCLAW_CONFIG_PATH || "").trim();
@@ -307,45 +480,66 @@ function readOpenClawConfiguredGateway(workspaceRoot: string) {
307
480
  } as const;
308
481
  }
309
482
 
310
- function detectOpenClawRuntime(workspaceRoot: string) {
311
- const configuredGateway = readOpenClawConfiguredGateway(workspaceRoot);
312
- const result = spawnSync("ps", ["-Ao", "pid=,ppid=,command="], {
313
- encoding: "utf8",
314
- });
315
- const stdout = String(result.stdout || "");
316
- const lines = stdout
317
- .split("\n")
318
- .map((line) => line.trim())
319
- .filter(Boolean);
483
+ function resolveOpenClawStatusCommand(workspaceRoot: string) {
484
+ const explicitBin = String(process.env.OPENCLAW_BIN || "").trim();
485
+ if (explicitBin) {
486
+ return { cmd: explicitBin, args: ["status"] } as const;
487
+ }
320
488
 
321
- const processes = lines
322
- .map((line) => {
323
- const match = line.match(/^(\d+)\s+(\d+)\s+(.+)$/);
324
- if (!match) return null;
325
- const command = match[3] || "";
326
- const lower = command.toLowerCase();
327
- const isOpenClaw =
328
- lower.includes(" openclaw ") ||
329
- lower.endsWith(" openclaw") ||
330
- lower.includes("/openclaw ") ||
331
- lower.includes("openclaw.mjs") ||
332
- lower.includes("openclaw gateway") ||
333
- lower.includes("openclaw agent") ||
334
- lower.includes("openclaw message");
335
- if (!isOpenClaw) return null;
336
- return {
337
- pid: Number(match[1]),
338
- ppid: Number(match[2]),
339
- command,
340
- };
341
- })
342
- .filter((item): item is { pid: number; ppid: number; command: string } => Boolean(item));
489
+ const configuredSourceDir = String(process.env.OPENCLAW_SOURCE_DIR || "").trim();
490
+ const defaultSourceDir = defaultOpenClawSourceDir(workspaceRoot);
491
+ const sourceDir = configuredSourceDir || defaultSourceDir;
492
+ const sourceEntry = existingPathOrNull(resolve(sourceDir, "openclaw.mjs"));
493
+ if (sourceEntry) {
494
+ return { cmd: process.execPath, args: [sourceEntry, "status"] } as const;
495
+ }
496
+
497
+ const commandPath = resolveExecutableInPath("openclaw");
498
+ if (commandPath) {
499
+ return { cmd: commandPath, args: ["status"] } as const;
500
+ }
501
+
502
+ return null;
503
+ }
504
+
505
+ function resolveOpenClawGatewayProbeCommand(workspaceRoot: string) {
506
+ const explicitBin = String(process.env.OPENCLAW_BIN || "").trim();
507
+ if (explicitBin) {
508
+ return { cmd: explicitBin, args: ["gateway", "probe"] } as const;
509
+ }
343
510
 
344
- const openclawPids = new Set(processes.map((item) => item.pid));
345
- const gatewayProbe = spawnSync("lsof", ["-nP", "-iTCP", "-sTCP:LISTEN"], {
511
+ const configuredSourceDir = String(process.env.OPENCLAW_SOURCE_DIR || "").trim();
512
+ const defaultSourceDir = defaultOpenClawSourceDir(workspaceRoot);
513
+ const sourceDir = configuredSourceDir || defaultSourceDir;
514
+ const sourceEntry = existingPathOrNull(resolve(sourceDir, "openclaw.mjs"));
515
+ if (sourceEntry) {
516
+ return { cmd: process.execPath, args: [sourceEntry, "gateway", "probe"] } as const;
517
+ }
518
+
519
+ const commandPath = resolveExecutableInPath("openclaw");
520
+ if (commandPath) {
521
+ return { cmd: commandPath, args: ["gateway", "probe"] } as const;
522
+ }
523
+
524
+ return null;
525
+ }
526
+
527
+ function detectOpenClawRuntime(workspaceRoot: string) {
528
+ const configuredGateway = readOpenClawConfiguredGateway(workspaceRoot);
529
+ const statusCommand = resolveOpenClawStatusCommand(workspaceRoot);
530
+ const statusLooksConfigured = Boolean(
531
+ statusCommand ||
532
+ configuredGateway.config_path ||
533
+ detectOpenClawInstallation(workspaceRoot).detected
534
+ );
535
+ const gatewayProbeCommand = ["lsof", "-nP", `-iTCP:${configuredGateway.gateway_port}`, "-sTCP:LISTEN"];
536
+ const gatewayProbe = spawnSync(gatewayProbeCommand[0], gatewayProbeCommand.slice(1), {
346
537
  encoding: "utf8",
538
+ timeout: 1200,
347
539
  });
348
- const gatewayLines = String(gatewayProbe.stdout || "")
540
+ const gatewayStatusStdout = String(gatewayProbe.stdout || "");
541
+ const gatewayStatusStderr = String(gatewayProbe.stderr || "");
542
+ const gatewayLines = gatewayStatusStdout
349
543
  .split("\n")
350
544
  .map((line) => line.trim())
351
545
  .filter(Boolean);
@@ -355,14 +549,9 @@ function detectOpenClawRuntime(workspaceRoot: string) {
355
549
  const parts = line.split(/\s+/);
356
550
  const pid = Number(parts[1] || 0);
357
551
  const command = parts[0] || "";
358
- const lowerCommand = command.toLowerCase();
359
552
  const endpoint = parts[8] || parts[parts.length - 1] || "";
360
553
  const portMatch = endpoint.match(/:(\d+)(?:\s*\(|$)/);
361
554
  if (!pid || !command || !portMatch) return null;
362
- const isOpenClawListener =
363
- openclawPids.has(pid) ||
364
- lowerCommand.includes("openclaw");
365
- if (!isOpenClawListener) return null;
366
555
  const port = Number(portMatch[1]);
367
556
  if (!Number.isFinite(port) || port <= 0) return null;
368
557
  return {
@@ -373,46 +562,106 @@ function detectOpenClawRuntime(workspaceRoot: string) {
373
562
  };
374
563
  })
375
564
  .filter((item): item is { pid: number; ppid: number; port: number; command: string } => Boolean(item));
565
+ const gatewayProbeOk = gatewayListeners.length > 0;
566
+ let processes: Array<{ pid: number; ppid: number; command: string }> = gatewayListeners.map((item) => ({
567
+ pid: item.pid,
568
+ ppid: item.ppid,
569
+ command: item.command,
570
+ }));
571
+ let processResult: ReturnType<typeof spawnSync> | null = null;
572
+ if (!gatewayProbeOk) {
573
+ processResult = spawnSync("ps", ["-Ao", "pid=,ppid=,command="], {
574
+ encoding: "utf8",
575
+ timeout: 1200,
576
+ });
577
+ const stdout = String(processResult.stdout || "");
578
+ const lines = stdout
579
+ .split("\n")
580
+ .map((line) => line.trim())
581
+ .filter(Boolean);
582
+ processes = lines
583
+ .map((line) => {
584
+ const match = line.match(/^(\d+)\s+(\d+)\s+(.+)$/);
585
+ if (!match) return null;
586
+ const command = match[3] || "";
587
+ const lower = command.toLowerCase();
588
+ const isOpenClaw =
589
+ lower.includes(" openclaw ") ||
590
+ lower.endsWith(" openclaw") ||
591
+ lower.includes("/openclaw ") ||
592
+ lower.includes("openclaw.mjs") ||
593
+ lower.includes("openclaw gateway") ||
594
+ lower.includes("openclaw agent") ||
595
+ lower.includes("openclaw message");
596
+ if (!isOpenClaw) return null;
597
+ return {
598
+ pid: Number(match[1]),
599
+ ppid: Number(match[2]),
600
+ command,
601
+ };
602
+ })
603
+ .filter((item): item is { pid: number; ppid: number; command: string } => Boolean(item));
604
+ }
605
+
376
606
  const preferredListener =
377
607
  gatewayListeners.find((item) => item.port === configuredGateway.gateway_port) ||
378
608
  gatewayListeners[0] ||
379
609
  null;
380
-
381
- const combinedProcesses = new Map<number, { pid: number; ppid: number; command: string }>();
382
- for (const process of [...processes, ...gatewayListeners]) {
383
- if (!combinedProcesses.has(process.pid)) {
384
- combinedProcesses.set(process.pid, process);
385
- continue;
386
- }
387
- const current = combinedProcesses.get(process.pid);
388
- if (current && current.command.length < process.command.length) {
389
- combinedProcesses.set(process.pid, process);
390
- }
391
- }
392
- const allProcesses = Array.from(combinedProcesses.values());
393
- const gatewayReachable = gatewayListeners.length > 0;
610
+ const allProcesses = processes.slice(0, 10);
611
+ const gatewayReachable = gatewayProbeOk;
394
612
  const detectionNotes = [];
395
- if (result.status !== 0) detectionNotes.push(String(result.stderr || "ps failed").trim());
396
613
  if (gatewayProbe.status !== 0 && gatewayLines.length === 0) {
397
- detectionNotes.push(String(gatewayProbe.stderr || "lsof failed").trim());
614
+ detectionNotes.push(String(gatewayStatusStderr || "openclaw gateway probe failed").trim());
615
+ }
616
+ if (processResult && processResult.status !== 0) {
617
+ detectionNotes.push(String(processResult.stderr || "ps failed").trim());
398
618
  }
399
619
  const gatewayPort = preferredListener?.port || configuredGateway.gateway_port;
400
620
  const gatewayUrl = `http://${OPENCLAW_GATEWAY_HOST}:${gatewayPort}/`;
401
621
 
402
622
  return {
403
- running: allProcesses.length > 0 || gatewayReachable,
623
+ running: gatewayProbeOk || allProcesses.length > 0 || gatewayReachable,
404
624
  process_count: allProcesses.length,
405
625
  processes: allProcesses.slice(0, 10),
406
626
  detection_error: detectionNotes.filter(Boolean).join(" | ") || null,
407
627
  gateway_url: gatewayUrl,
408
628
  gateway_port: gatewayPort,
409
629
  gateway_reachable: gatewayReachable,
630
+ status_command: statusCommand ? [statusCommand.cmd, ...statusCommand.args].join(" ") : null,
631
+ status_ok: statusLooksConfigured,
632
+ status_summary: statusLooksConfigured
633
+ ? configuredGateway.config_path
634
+ ? `configured via ${configuredGateway.config_path}`
635
+ : statusCommand
636
+ ? `command available: ${[statusCommand.cmd, ...statusCommand.args].join(" ")}`
637
+ : "OpenClaw environment detected"
638
+ : null,
639
+ gateway_probe_command: gatewayProbeCommand.join(" "),
640
+ gateway_probe_ok: gatewayProbeOk,
641
+ gateway_probe_summary: gatewayProbeOk
642
+ ? gatewayStatusStdout
643
+ .split("\n")
644
+ .map((line) => line.trim())
645
+ .filter(Boolean)
646
+ .slice(0, 4)
647
+ .join(" | ")
648
+ : null,
410
649
  configured_gateway_url: configuredGateway.gateway_url,
411
650
  configured_gateway_port: configuredGateway.gateway_port,
412
651
  configured_gateway_bind: configuredGateway.gateway_bind,
413
652
  configured_gateway_config_path: configuredGateway.config_path,
414
653
  detection_mode:
415
- processes.length > 0 && gatewayReachable
654
+ gatewayProbeOk
655
+ ? (
656
+ processes.length > 0 && gatewayReachable
657
+ ? "gateway-probe+process+gateway"
658
+ : gatewayReachable
659
+ ? "gateway-probe+gateway"
660
+ : processes.length > 0
661
+ ? "gateway-probe+process"
662
+ : "gateway-probe"
663
+ )
664
+ : processes.length > 0 && gatewayReachable
416
665
  ? "process+gateway"
417
666
  : gatewayReachable
418
667
  ? "gateway"
@@ -453,7 +702,7 @@ function detectOwnerDeliveryStatus(params: {
453
702
  const ownerAccount = String(process.env.OPENCLAW_OWNER_ACCOUNT || "").trim();
454
703
  const explicitOpenClawBin = String(process.env.OPENCLAW_BIN || "").trim();
455
704
  const configuredSourceDir = String(process.env.OPENCLAW_SOURCE_DIR || "").trim();
456
- const defaultSourceDir = resolve(params.workspaceRoot, "..", "openclaw");
705
+ const defaultSourceDir = defaultOpenClawSourceDir(params.workspaceRoot);
457
706
  const openclawSourceDir = configuredSourceDir || defaultSourceDir;
458
707
  const openclawSourceEntry = existingPathOrNull(resolve(openclawSourceDir, "openclaw.mjs"));
459
708
  const openclawCommandResolvable = Boolean(explicitOpenClawBin || resolveExecutableInPath("openclaw") || openclawSourceEntry);
@@ -512,10 +761,18 @@ function hasMeaningfulJson(filePath: string): boolean {
512
761
  }
513
762
  }
514
763
 
515
- function migrateLegacyDataIfNeeded(workspaceRoot: string, storageRoot: string): void {
516
- const legacyDataDir = resolve(workspaceRoot, "data");
764
+ function migrateLegacyDataIfNeeded(appRoot: string, projectRoot: string, storageRoot: string): void {
765
+ const homeDir = process.env.HOME || homedir();
766
+ const legacyNpxAppRoots = collectLegacyNpxAppRoots(homeDir);
517
767
  const targetDataDir = resolve(storageRoot, "data");
518
- if (legacyDataDir === targetDataDir) return;
768
+ const legacyDataDirs = [
769
+ resolve(appRoot, "data"),
770
+ resolve(appRoot, "apps", "local-console", "data"),
771
+ resolve(projectRoot, "data"),
772
+ resolve(projectRoot, "apps", "local-console", "data"),
773
+ resolve(process.cwd(), "data"),
774
+ ...legacyNpxAppRoots.map((root) => resolve(root, "apps", "local-console", "data")),
775
+ ].filter((dir, index, list) => list.indexOf(dir) === index && dir !== targetDataDir);
519
776
  const files = [
520
777
  "identity.json",
521
778
  "profile.json",
@@ -525,16 +782,65 @@ function migrateLegacyDataIfNeeded(workspaceRoot: string, storageRoot: string):
525
782
  "social-message-observations.json",
526
783
  ];
527
784
  for (const file of files) {
528
- const src = resolve(legacyDataDir, file);
529
785
  const dst = resolve(targetDataDir, file);
530
- if (!existsSync(src)) continue;
531
786
  if (hasMeaningfulJson(dst)) continue;
532
- if (!hasMeaningfulJson(src)) continue;
533
- mkdirSync(targetDataDir, { recursive: true });
534
- copyFileSync(src, dst);
787
+ for (const legacyDataDir of legacyDataDirs) {
788
+ const src = resolve(legacyDataDir, file);
789
+ if (!existsSync(src)) continue;
790
+ if (!hasMeaningfulJson(src)) continue;
791
+ mkdirSync(targetDataDir, { recursive: true });
792
+ copyFileSync(src, dst);
793
+ break;
794
+ }
795
+ }
796
+
797
+ const targetDotDir = resolve(storageRoot, ".silicaclaw");
798
+ const legacyDotDirs = [
799
+ resolve(appRoot, ".silicaclaw"),
800
+ resolve(appRoot, "apps", "local-console", ".silicaclaw"),
801
+ resolve(projectRoot, ".silicaclaw"),
802
+ resolve(projectRoot, "apps", "local-console", ".silicaclaw"),
803
+ resolve(process.cwd(), ".silicaclaw"),
804
+ ...legacyNpxAppRoots.map((root) => resolve(root, "apps", "local-console", ".silicaclaw")),
805
+ ].filter((dir, index, list) => list.indexOf(dir) === index && dir !== targetDotDir);
806
+ const dotFiles = ["social.runtime.json", "social.message-governance.json"];
807
+ for (const file of dotFiles) {
808
+ const dst = resolve(targetDotDir, file);
809
+ if (hasMeaningfulJson(dst)) continue;
810
+ for (const legacyDotDir of legacyDotDirs) {
811
+ const src = resolve(legacyDotDir, file);
812
+ if (!existsSync(src)) continue;
813
+ if (!hasMeaningfulJson(src)) continue;
814
+ mkdirSync(targetDotDir, { recursive: true });
815
+ copyFileSync(src, dst);
816
+ break;
817
+ }
535
818
  }
536
819
  }
537
820
 
821
+ function collectLegacyNpxAppRoots(homeDir: string): string[] {
822
+ const cacheRoots = [
823
+ resolve(homeDir, ".silicaclaw", "npm-cache", "_npx"),
824
+ resolve(homeDir, ".npm", "_npx"),
825
+ ];
826
+ const roots: string[] = [];
827
+ for (const cacheRoot of cacheRoots) {
828
+ if (!existsSync(cacheRoot)) continue;
829
+ let entries: string[] = [];
830
+ try {
831
+ entries = readdirSync(cacheRoot);
832
+ } catch {
833
+ continue;
834
+ }
835
+ for (const entry of entries) {
836
+ const candidate = resolve(cacheRoot, entry, "node_modules", "@silicaclaw", "cli");
837
+ if (!existsSync(resolve(candidate, "apps", "local-console"))) continue;
838
+ roots.push(candidate);
839
+ }
840
+ }
841
+ return Array.from(new Set(roots));
842
+ }
843
+
538
844
  function parseListEnv(raw: string): string[] {
539
845
  return raw
540
846
  .split(/[,\n]/g)
@@ -575,6 +881,7 @@ type IntegrationStatusSummary = {
575
881
  };
576
882
 
577
883
  type SocialMessageView = SocialMessageRecord & {
884
+ avatar_url?: string;
578
885
  is_self: boolean;
579
886
  online: boolean;
580
887
  last_seen_at: number | null;
@@ -584,6 +891,17 @@ type SocialMessageView = SocialMessageRecord & {
584
891
  delivery_status: "local-only" | "remote-observed";
585
892
  };
586
893
 
894
+ type PrivateMessageView = {
895
+ message_id: string;
896
+ conversation_id: string;
897
+ from_agent_id: string;
898
+ to_agent_id: string;
899
+ body: string;
900
+ created_at: number;
901
+ is_self: boolean;
902
+ delivery_status: "sent" | "received" | "read";
903
+ };
904
+
587
905
  type RuntimeMessageGovernance = SocialMessageGovernanceConfig;
588
906
 
589
907
  type OpenClawBridgeStatus = {
@@ -627,11 +945,17 @@ type OpenClawBridgeStatus = {
627
945
  gateway_url: string;
628
946
  gateway_port: number;
629
947
  gateway_reachable: boolean;
948
+ status_command: string | null;
949
+ status_ok: boolean;
950
+ status_summary: string | null;
951
+ gateway_probe_command: string | null;
952
+ gateway_probe_ok: boolean;
953
+ gateway_probe_summary: string | null;
630
954
  configured_gateway_url: string;
631
955
  configured_gateway_port: number;
632
956
  configured_gateway_bind: string | null;
633
957
  configured_gateway_config_path: string | null;
634
- detection_mode: "process" | "gateway" | "process+gateway" | "not_running";
958
+ detection_mode: "gateway-probe" | "gateway-probe+process" | "gateway-probe+gateway" | "gateway-probe+process+gateway" | "process" | "gateway" | "process+gateway" | "not_running";
635
959
  };
636
960
  skill_learning: {
637
961
  available: boolean;
@@ -699,6 +1023,7 @@ type OpenClawBridgeConfigView = {
699
1023
 
700
1024
  export class LocalNodeService {
701
1025
  private workspaceRoot: string;
1026
+ private projectRoot: string;
702
1027
  private storageRoot: string;
703
1028
  private identityRepo: IdentityRepo;
704
1029
  private profileRepo: ProfileRepo;
@@ -707,6 +1032,9 @@ export class LocalNodeService {
707
1032
  private socialMessageGovernanceRepo: SocialMessageGovernanceRepo;
708
1033
  private socialMessageRepo: SocialMessageRepo;
709
1034
  private socialMessageObservationRepo: SocialMessageObservationRepo;
1035
+ private privateMessageRepo: PrivateMessageRepo;
1036
+ private privateMessageReceiptRepo: PrivateMessageReceiptRepo;
1037
+ private privateEncryptionKeyRepo: PrivateEncryptionKeyRepo;
710
1038
  private socialRuntimeRepo: SocialRuntimeRepo;
711
1039
 
712
1040
  private identity: AgentIdentity | null = null;
@@ -714,15 +1042,31 @@ export class LocalNodeService {
714
1042
  private directory: DirectoryState = createEmptyDirectoryState();
715
1043
  private socialMessages: SocialMessageRecord[] = [];
716
1044
  private socialMessageObservations: SocialMessageObservationRecord[] = [];
1045
+ private privateMessages: PrivateMessageRecord[] = [];
1046
+ private privateMessageReceipts: PrivateMessageReceiptRecord[] = [];
1047
+ private privateEncryptionKeyPair: PrivateEncryptionKeyPair | null = null;
1048
+ private privatePeerRoutes: Record<string, string> = {};
1049
+ private privateMessageBodyCache = new Map<string, string>();
717
1050
  private messageGovernance: RuntimeMessageGovernance;
1051
+ private privateMessagesPersistDirty = false;
1052
+ private privateMessageReceiptsPersistDirty = false;
1053
+ private privateMessagesPersistTimer: NodeJS.Timeout | null = null;
1054
+ private privateMessageReceiptsPersistTimer: NodeJS.Timeout | null = null;
718
1055
 
719
1056
  private receivedCount = 0;
720
1057
  private broadcastCount = 0;
721
1058
  private lastMessageAt = 0;
722
1059
  private lastBroadcastAt = 0;
1060
+ private lastProfileBroadcastAt = 0;
1061
+ private lastProfileBroadcastSignature = "";
1062
+ private lastReplayBroadcastAt = 0;
1063
+ private lastReplayBroadcastSignature = "";
723
1064
  private lastBroadcastErrorAt = 0;
724
1065
  private lastBroadcastError: string | null = null;
725
1066
  private broadcastFailureCount = 0;
1067
+ private consecutiveBroadcastFailures = 0;
1068
+ private lastBroadcastRecoveryAttemptAt = 0;
1069
+ private broadcastRecoveryInFlight = false;
726
1070
  private broadcaster: NodeJS.Timeout | null = null;
727
1071
  private subscriptionsBound = false;
728
1072
  private broadcastEnabled = true;
@@ -741,7 +1085,7 @@ export class LocalNodeService {
741
1085
 
742
1086
  private network: NetworkAdapter;
743
1087
  private adapterMode: "mock" | "local-event-bus" | "real-preview" | "webrtc-preview" | "relay-preview";
744
- private networkMode: "local" | "lan" | "global-preview" = "global-preview";
1088
+ private networkMode: "local" | "lan" | "global-preview" = DEFAULT_NETWORK_MODE;
745
1089
  private networkNamespace: string;
746
1090
  private networkPort: number | null;
747
1091
  private socialConfig: SocialConfig;
@@ -755,17 +1099,24 @@ export class LocalNodeService {
755
1099
  "silicaclaw-existing";
756
1100
  private resolvedOpenClawIdentityPath: string | null = null;
757
1101
  private webrtcSignalingUrls: string[] = [];
758
- private webrtcRoom = "silicaclaw-global-preview";
1102
+ private webrtcRoom = DEFAULT_GLOBAL_ROOM;
759
1103
  private webrtcSeedPeers: string[] = [];
760
1104
  private webrtcBootstrapHints: string[] = [];
761
1105
  private webrtcBootstrapSources: string[] = [];
1106
+ private networkStarted = false;
1107
+ private networkStartupError: string | null = null;
1108
+ private networkReconnectTimer: NodeJS.Timeout | null = null;
1109
+ private networkReconnectDelayMs = 5_000;
762
1110
  private appVersion = "unknown";
1111
+ private openclawRuntimeCache: { value: ReturnType<typeof detectOpenClawRuntime>; expiresAt: number } | null = null;
1112
+ private openclawBridgeStatusCache: { value: OpenClawBridgeStatus; expiresAt: number } | null = null;
763
1113
 
764
- constructor(options?: { workspaceRoot?: string; storageRoot?: string }) {
1114
+ constructor(options?: { workspaceRoot?: string; projectRoot?: string; storageRoot?: string }) {
765
1115
  this.workspaceRoot = options?.workspaceRoot || resolveWorkspaceRoot();
1116
+ this.projectRoot = options?.projectRoot || resolveProjectRoot(this.workspaceRoot);
766
1117
  this.storageRoot = options?.storageRoot || resolveStorageRoot(this.workspaceRoot);
767
1118
  this.appVersion = readWorkspaceVersion(this.workspaceRoot);
768
- migrateLegacyDataIfNeeded(this.workspaceRoot, this.storageRoot);
1119
+ migrateLegacyDataIfNeeded(this.workspaceRoot, this.projectRoot, this.storageRoot);
769
1120
 
770
1121
  this.identityRepo = new IdentityRepo(this.storageRoot);
771
1122
  this.profileRepo = new ProfileRepo(this.storageRoot);
@@ -774,19 +1125,22 @@ export class LocalNodeService {
774
1125
  this.socialMessageGovernanceRepo = new SocialMessageGovernanceRepo(this.storageRoot);
775
1126
  this.socialMessageRepo = new SocialMessageRepo(this.storageRoot);
776
1127
  this.socialMessageObservationRepo = new SocialMessageObservationRepo(this.storageRoot);
1128
+ this.privateMessageRepo = new PrivateMessageRepo(this.storageRoot);
1129
+ this.privateMessageReceiptRepo = new PrivateMessageReceiptRepo(this.storageRoot);
1130
+ this.privateEncryptionKeyRepo = new PrivateEncryptionKeyRepo(this.storageRoot);
777
1131
  this.socialRuntimeRepo = new SocialRuntimeRepo(this.storageRoot);
778
1132
  this.messageGovernance = this.defaultMessageGovernance();
779
1133
 
780
- let loadedSocial = loadSocialConfig(this.workspaceRoot);
1134
+ let loadedSocial = loadSocialConfig(this.projectRoot);
781
1135
  if (!loadedSocial.meta.found) {
782
- ensureDefaultSocialMd(this.workspaceRoot, {
1136
+ ensureDefaultSocialMd(this.projectRoot, {
783
1137
  display_name: this.getDefaultDisplayName(),
784
1138
  bio: "Local AI agent connected to SilicaClaw",
785
1139
  tags: ["openclaw", "local-first"],
786
- mode: "global-preview",
1140
+ mode: DEFAULT_NETWORK_MODE,
787
1141
  public_enabled: false,
788
1142
  });
789
- loadedSocial = loadSocialConfig(this.workspaceRoot);
1143
+ loadedSocial = loadSocialConfig(this.projectRoot);
790
1144
  this.initState.social_auto_created = true;
791
1145
  }
792
1146
  this.socialConfig = loadedSocial.config;
@@ -795,8 +1149,8 @@ export class LocalNodeService {
795
1149
  this.socialParseError = loadedSocial.meta.parse_error;
796
1150
  this.socialRawFrontmatter = loadedSocial.raw_frontmatter;
797
1151
 
798
- this.networkNamespace = this.socialConfig.network.namespace || process.env.NETWORK_NAMESPACE || "silicaclaw.preview";
799
- this.networkPort = Number(this.socialConfig.network.port || process.env.NETWORK_PORT || 44123);
1152
+ this.networkNamespace = this.socialConfig.network.namespace || process.env.NETWORK_NAMESPACE || DEFAULT_NETWORK_NAMESPACE;
1153
+ this.networkPort = Number(this.socialConfig.network.port || process.env.NETWORK_PORT || DEFAULT_NETWORK_PORT);
800
1154
  this.applyResolvedNetworkConfig();
801
1155
  const resolved = this.buildNetworkAdapter();
802
1156
  this.network = resolved.adapter;
@@ -804,36 +1158,42 @@ export class LocalNodeService {
804
1158
  this.networkPort = resolved.port;
805
1159
  }
806
1160
 
1161
+ private getCachedOpenClawRuntime() {
1162
+ const now = Date.now();
1163
+ if (this.openclawRuntimeCache && this.openclawRuntimeCache.expiresAt > now) {
1164
+ return this.openclawRuntimeCache.value;
1165
+ }
1166
+ const value = detectOpenClawRuntime(this.projectRoot);
1167
+ this.openclawRuntimeCache = {
1168
+ value,
1169
+ expiresAt: now + OPENCLAW_RUNTIME_CACHE_MS,
1170
+ };
1171
+ return value;
1172
+ }
1173
+
1174
+ private invalidateOpenClawCaches() {
1175
+ this.openclawRuntimeCache = null;
1176
+ this.openclawBridgeStatusCache = null;
1177
+ }
1178
+
807
1179
  async start(): Promise<void> {
808
1180
  await this.hydrateFromDisk();
809
1181
 
810
1182
  this.bindNetworkSubscriptions();
811
- await this.network.start();
812
- await this.log(
813
- "info",
814
- `Local node started (${this.adapterMode}, mode=${this.networkMode}, signaling=${this.webrtcSignalingUrls[0] || "-"}, room=${this.webrtcRoom})`
815
- );
816
-
817
- if (this.profile?.public_enabled && this.broadcastEnabled) {
818
- try {
819
- await this.broadcastNow("adapter_start");
820
- } catch (error) {
821
- await this.log(
822
- "warn",
823
- `Initial broadcast failed: ${error instanceof Error ? error.message : String(error)}`
824
- );
825
- }
826
- }
827
-
828
- this.startBroadcastLoop();
1183
+ await this.startNetworkAdapterWithRetry("adapter_start");
829
1184
  }
830
1185
 
831
1186
  async stop(): Promise<void> {
1187
+ this.clearNetworkReconnectTimer();
832
1188
  if (this.broadcaster) {
833
1189
  clearInterval(this.broadcaster);
834
1190
  this.broadcaster = null;
835
1191
  }
836
- await this.network.stop();
1192
+ await this.flushPrivatePersistence();
1193
+ if (this.networkStarted) {
1194
+ await this.network.stop();
1195
+ }
1196
+ this.networkStarted = false;
837
1197
  }
838
1198
 
839
1199
  private ensureLocalDirectoryBaseline(): void {
@@ -848,12 +1208,11 @@ export class LocalNodeService {
848
1208
  }
849
1209
 
850
1210
  getOverview() {
851
- this.ensureLocalDirectoryBaseline();
852
- this.compactCacheInMemory();
853
- const profiles = Object.values(this.directory.profiles);
854
- const onlineCount = profiles.filter((profile) =>
855
- isAgentOnline(this.directory.presence[profile.agent_id], Date.now(), PRESENCE_TTL_MS)
856
- ).length;
1211
+ const discovered = this.search("");
1212
+ const onlineCount = discovered.filter((profile) => profile.online).length;
1213
+ const openclawInstallation = detectOpenClawInstallation(this.projectRoot);
1214
+ const openclawRuntime = this.getCachedOpenClawRuntime();
1215
+ const openclawSkillInstallation = detectOpenClawSkillInstallation();
857
1216
 
858
1217
  return {
859
1218
  app_version: this.appVersion,
@@ -864,12 +1223,21 @@ export class LocalNodeService {
864
1223
  last_broadcast_error_at: this.lastBroadcastErrorAt,
865
1224
  last_broadcast_error: this.lastBroadcastError,
866
1225
  broadcast_failure_count: this.broadcastFailureCount,
867
- discovered_count: profiles.length,
1226
+ discovered_count: discovered.length,
868
1227
  online_count: onlineCount,
869
- offline_count: Math.max(0, profiles.length - onlineCount),
1228
+ offline_count: Math.max(0, discovered.length - onlineCount),
870
1229
  init_state: this.initState,
871
1230
  presence_ttl_ms: PRESENCE_TTL_MS,
872
1231
  onboarding: this.getOnboardingSummary(),
1232
+ openclaw: {
1233
+ detected: openclawInstallation.detected,
1234
+ running: openclawRuntime.running,
1235
+ detection_mode: openclawRuntime.detection_mode,
1236
+ gateway_url: openclawRuntime.gateway_url,
1237
+ gateway_probe_ok: openclawRuntime.gateway_probe_ok,
1238
+ status_ok: openclawRuntime.status_ok,
1239
+ skill_installed: openclawSkillInstallation.installed,
1240
+ },
873
1241
  social: {
874
1242
  found: this.socialFound,
875
1243
  enabled: this.socialConfig.enabled,
@@ -888,7 +1256,9 @@ export class LocalNodeService {
888
1256
  }
889
1257
 
890
1258
  getNetworkSummary() {
891
- const diagnostics = this.getAdapterDiagnostics();
1259
+ const network = this.getResolvedRealtimeNetworkSummary();
1260
+ const diagnostics = network.diagnostics;
1261
+ const relayCapable = this.adapterMode === "webrtc-preview" || this.adapterMode === "relay-preview";
892
1262
  const peerCount = diagnostics?.peers.total ?? 0;
893
1263
 
894
1264
  return {
@@ -916,30 +1286,34 @@ export class LocalNodeService {
916
1286
  real_preview_stats: diagnostics?.stats ?? null,
917
1287
  real_preview_transport_stats: diagnostics?.transport_stats ?? null,
918
1288
  real_preview_discovery_stats: diagnostics?.discovery_stats ?? null,
919
- webrtc_preview: diagnostics && (diagnostics.adapter === "webrtc-preview" || diagnostics.adapter === "relay-preview")
1289
+ webrtc_preview: relayCapable
920
1290
  ? {
921
- signaling_url: diagnostics.signaling_url ?? null,
922
- signaling_endpoints: diagnostics.signaling_endpoints ?? [],
923
- room: diagnostics.room ?? null,
924
- bootstrap_sources: diagnostics.bootstrap_sources ?? [],
925
- seed_peers_count: diagnostics.seed_peers_count ?? 0,
926
- discovery_events_total: diagnostics.discovery_events_total ?? 0,
927
- last_discovery_event_at: diagnostics.last_discovery_event_at ?? 0,
928
- active_webrtc_peers: diagnostics.active_webrtc_peers ?? 0,
929
- reconnect_attempts_total: diagnostics.reconnect_attempts_total ?? 0,
930
- last_join_at: diagnostics.last_join_at ?? 0,
931
- last_poll_at: diagnostics.last_poll_at ?? 0,
932
- last_publish_at: diagnostics.last_publish_at ?? 0,
933
- last_peer_refresh_at: diagnostics.last_peer_refresh_at ?? 0,
934
- last_error_at: diagnostics.last_error_at ?? 0,
935
- last_error: diagnostics.last_error ?? null,
1291
+ started: this.networkStarted,
1292
+ startup_error: this.networkStartupError,
1293
+ signaling_url: network.signaling_url,
1294
+ signaling_endpoints: network.signaling_endpoints,
1295
+ room: network.room,
1296
+ bootstrap_sources: network.bootstrap_sources,
1297
+ seed_peers_count: network.seed_peers_count,
1298
+ discovery_events_total: diagnostics?.discovery_events_total ?? 0,
1299
+ last_discovery_event_at: diagnostics?.last_discovery_event_at ?? 0,
1300
+ active_webrtc_peers: diagnostics?.active_webrtc_peers ?? 0,
1301
+ reconnect_attempts_total: diagnostics?.reconnect_attempts_total ?? 0,
1302
+ last_join_at: diagnostics?.last_join_at ?? 0,
1303
+ last_poll_at: diagnostics?.last_poll_at ?? 0,
1304
+ last_publish_at: diagnostics?.last_publish_at ?? 0,
1305
+ last_peer_refresh_at: diagnostics?.last_peer_refresh_at ?? 0,
1306
+ last_error_at: diagnostics?.last_error_at ?? 0,
1307
+ last_error: diagnostics?.last_error ?? null,
936
1308
  }
937
1309
  : null,
938
1310
  };
939
1311
  }
940
1312
 
941
1313
  getNetworkConfig() {
942
- const diagnostics = this.getAdapterDiagnostics();
1314
+ const network = this.getResolvedRealtimeNetworkSummary();
1315
+ const diagnostics = network.diagnostics;
1316
+ const relayCapable = this.adapterMode === "webrtc-preview" || this.adapterMode === "relay-preview";
943
1317
  return {
944
1318
  adapter: this.adapterMode,
945
1319
  mode: this.networkMode,
@@ -953,23 +1327,25 @@ export class LocalNodeService {
953
1327
  },
954
1328
  limits: diagnostics?.limits ?? null,
955
1329
  adapter_config: diagnostics?.config ?? null,
956
- adapter_extra: diagnostics && (diagnostics.adapter === "webrtc-preview" || diagnostics.adapter === "relay-preview")
1330
+ adapter_extra: relayCapable
957
1331
  ? {
958
- signaling_url: diagnostics.signaling_url ?? null,
959
- signaling_endpoints: diagnostics.signaling_endpoints ?? [],
960
- room: diagnostics.room ?? null,
961
- bootstrap_sources: diagnostics.bootstrap_sources ?? [],
962
- seed_peers_count: diagnostics.seed_peers_count ?? 0,
963
- discovery_events_total: diagnostics.discovery_events_total ?? 0,
964
- last_discovery_event_at: diagnostics.last_discovery_event_at ?? 0,
965
- connection_states_summary: diagnostics.connection_states_summary ?? null,
966
- datachannel_states_summary: diagnostics.datachannel_states_summary ?? null,
967
- last_join_at: diagnostics.last_join_at ?? 0,
968
- last_poll_at: diagnostics.last_poll_at ?? 0,
969
- last_publish_at: diagnostics.last_publish_at ?? 0,
970
- last_peer_refresh_at: diagnostics.last_peer_refresh_at ?? 0,
971
- last_error_at: diagnostics.last_error_at ?? 0,
972
- last_error: diagnostics.last_error ?? null,
1332
+ started: this.networkStarted,
1333
+ startup_error: this.networkStartupError,
1334
+ signaling_url: network.signaling_url,
1335
+ signaling_endpoints: network.signaling_endpoints,
1336
+ room: network.room,
1337
+ bootstrap_sources: network.bootstrap_sources,
1338
+ seed_peers_count: network.seed_peers_count,
1339
+ discovery_events_total: diagnostics?.discovery_events_total ?? 0,
1340
+ last_discovery_event_at: diagnostics?.last_discovery_event_at ?? 0,
1341
+ connection_states_summary: diagnostics?.connection_states_summary ?? null,
1342
+ datachannel_states_summary: diagnostics?.datachannel_states_summary ?? null,
1343
+ last_join_at: diagnostics?.last_join_at ?? 0,
1344
+ last_poll_at: diagnostics?.last_poll_at ?? 0,
1345
+ last_publish_at: diagnostics?.last_publish_at ?? 0,
1346
+ last_peer_refresh_at: diagnostics?.last_peer_refresh_at ?? 0,
1347
+ last_error_at: diagnostics?.last_error_at ?? 0,
1348
+ last_error: diagnostics?.last_error ?? null,
973
1349
  }
974
1350
  : null,
975
1351
  env: {
@@ -997,16 +1373,19 @@ export class LocalNodeService {
997
1373
  this.adapterMode === "real-preview"
998
1374
  ? "lan-preview"
999
1375
  : this.adapterMode === "webrtc-preview" || this.adapterMode === "relay-preview"
1000
- ? "internet-preview"
1376
+ ? "global-preview"
1001
1377
  : "local-process",
1002
1378
  mode_explainer: this.getModeExplainer(),
1003
1379
  };
1004
1380
  }
1005
1381
 
1006
1382
  getNetworkStats() {
1007
- const diagnostics = this.getAdapterDiagnostics();
1383
+ const network = this.getResolvedRealtimeNetworkSummary();
1384
+ const diagnostics = network.diagnostics;
1385
+ const relayCapable = this.adapterMode === "webrtc-preview" || this.adapterMode === "relay-preview";
1008
1386
  const peers: Array<{ status?: string }> = diagnostics?.peers?.items ?? [];
1009
1387
  const online = peers.filter((peer: { status?: string }) => peer.status === "online").length;
1388
+ const memory = process.memoryUsage();
1010
1389
 
1011
1390
  return {
1012
1391
  adapter: this.adapterMode,
@@ -1031,34 +1410,54 @@ export class LocalNodeService {
1031
1410
  adapter_stats: diagnostics?.stats ?? null,
1032
1411
  adapter_transport_stats: diagnostics?.transport_stats ?? null,
1033
1412
  adapter_discovery_stats: diagnostics?.discovery_stats ?? null,
1034
- adapter_diagnostics_summary: diagnostics
1413
+ runtime_diagnostics: {
1414
+ memory_mib: {
1415
+ rss: formatBytesToMiB(memory.rss),
1416
+ heap_used: formatBytesToMiB(memory.heapUsed),
1417
+ heap_total: formatBytesToMiB(memory.heapTotal),
1418
+ external: formatBytesToMiB(memory.external),
1419
+ },
1420
+ directory: {
1421
+ profile_count: Object.keys(this.directory.profiles).length,
1422
+ presence_count: Object.keys(this.directory.presence).length,
1423
+ index_key_count: Object.keys(this.directory.index).length,
1424
+ },
1425
+ social: {
1426
+ message_count: this.socialMessages.length,
1427
+ observation_count: this.socialMessageObservations.length,
1428
+ },
1429
+ },
1430
+ adapter_diagnostics_summary: relayCapable || diagnostics
1035
1431
  ? {
1036
- signaling_url: diagnostics.signaling_url ?? null,
1037
- signaling_endpoints: diagnostics.signaling_endpoints ?? [],
1038
- room: diagnostics.room ?? null,
1039
- bootstrap_sources: diagnostics.bootstrap_sources ?? [],
1040
- seed_peers_count: diagnostics.seed_peers_count ?? 0,
1041
- discovery_events_total: diagnostics.discovery_events_total ?? 0,
1042
- last_discovery_event_at: diagnostics.last_discovery_event_at ?? 0,
1043
- connection_states_summary: diagnostics.connection_states_summary ?? null,
1044
- datachannel_states_summary: diagnostics.datachannel_states_summary ?? null,
1045
- signaling_messages_sent_total: diagnostics.signaling_messages_sent_total ?? null,
1046
- signaling_messages_received_total: diagnostics.signaling_messages_received_total ?? null,
1047
- reconnect_attempts_total: diagnostics.reconnect_attempts_total ?? null,
1048
- active_webrtc_peers: diagnostics.active_webrtc_peers ?? null,
1049
- last_join_at: diagnostics.last_join_at ?? 0,
1050
- last_poll_at: diagnostics.last_poll_at ?? 0,
1051
- last_publish_at: diagnostics.last_publish_at ?? 0,
1052
- last_peer_refresh_at: diagnostics.last_peer_refresh_at ?? 0,
1053
- last_error_at: diagnostics.last_error_at ?? 0,
1054
- last_error: diagnostics.last_error ?? null,
1432
+ started: this.networkStarted,
1433
+ startup_error: this.networkStartupError,
1434
+ signaling_url: network.signaling_url,
1435
+ signaling_endpoints: network.signaling_endpoints,
1436
+ room: network.room,
1437
+ bootstrap_sources: network.bootstrap_sources,
1438
+ seed_peers_count: network.seed_peers_count,
1439
+ discovery_events_total: diagnostics?.discovery_events_total ?? 0,
1440
+ last_discovery_event_at: diagnostics?.last_discovery_event_at ?? 0,
1441
+ connection_states_summary: diagnostics?.connection_states_summary ?? null,
1442
+ datachannel_states_summary: diagnostics?.datachannel_states_summary ?? null,
1443
+ signaling_messages_sent_total: diagnostics?.signaling_messages_sent_total ?? null,
1444
+ signaling_messages_received_total: diagnostics?.signaling_messages_received_total ?? null,
1445
+ reconnect_attempts_total: diagnostics?.reconnect_attempts_total ?? null,
1446
+ active_webrtc_peers: diagnostics?.active_webrtc_peers ?? null,
1447
+ last_join_at: diagnostics?.last_join_at ?? 0,
1448
+ last_poll_at: diagnostics?.last_poll_at ?? 0,
1449
+ last_publish_at: diagnostics?.last_publish_at ?? 0,
1450
+ last_peer_refresh_at: diagnostics?.last_peer_refresh_at ?? 0,
1451
+ last_error_at: diagnostics?.last_error_at ?? 0,
1452
+ last_error: diagnostics?.last_error ?? null,
1055
1453
  }
1056
1454
  : null,
1057
1455
  };
1058
1456
  }
1059
1457
 
1060
1458
  getPeersSummary() {
1061
- const diagnostics = this.getAdapterDiagnostics();
1459
+ const network = this.getResolvedRealtimeNetworkSummary();
1460
+ const diagnostics = network.diagnostics;
1062
1461
  if (!diagnostics) {
1063
1462
  return {
1064
1463
  adapter: this.adapterMode,
@@ -1081,11 +1480,13 @@ export class LocalNodeService {
1081
1480
  components: diagnostics.components,
1082
1481
  limits: diagnostics.limits,
1083
1482
  diagnostics_summary: {
1084
- signaling_url: diagnostics.signaling_url ?? null,
1085
- signaling_endpoints: diagnostics.signaling_endpoints ?? [],
1086
- room: diagnostics.room ?? null,
1087
- bootstrap_sources: diagnostics.bootstrap_sources ?? [],
1088
- seed_peers_count: diagnostics.seed_peers_count ?? 0,
1483
+ started: this.networkStarted,
1484
+ startup_error: this.networkStartupError,
1485
+ signaling_url: network.signaling_url,
1486
+ signaling_endpoints: network.signaling_endpoints,
1487
+ room: network.room,
1488
+ bootstrap_sources: network.bootstrap_sources,
1489
+ seed_peers_count: network.seed_peers_count,
1089
1490
  discovery_events_total: diagnostics.discovery_events_total ?? 0,
1090
1491
  last_discovery_event_at: diagnostics.last_discovery_event_at ?? 0,
1091
1492
  connection_states_summary: diagnostics.connection_states_summary ?? null,
@@ -1132,15 +1533,102 @@ export class LocalNodeService {
1132
1533
  getRuntimePaths() {
1133
1534
  return {
1134
1535
  workspace_root: this.workspaceRoot,
1536
+ project_root: this.projectRoot,
1135
1537
  storage_root: this.storageRoot,
1136
1538
  data_dir: resolve(this.storageRoot, "data"),
1137
1539
  social_runtime_path: resolve(this.storageRoot, ".silicaclaw", "social.runtime.json"),
1138
1540
  local_console_public_dir: resolve(this.workspaceRoot, "apps", "local-console", "public"),
1139
- social_lookup_paths: getSocialConfigSearchPaths(this.workspaceRoot),
1541
+ social_lookup_paths: getSocialConfigSearchPaths(this.projectRoot),
1140
1542
  social_source_path: this.socialSourcePath,
1141
1543
  };
1142
1544
  }
1143
1545
 
1546
+ getAppUpdateStatus() {
1547
+ const currentVersion = normalizeVersionText(this.appVersion) || "unknown";
1548
+ const fallback = {
1549
+ current_version: currentVersion,
1550
+ latest_version: currentVersion,
1551
+ update_available: false,
1552
+ channel: "latest",
1553
+ platform: process.platform,
1554
+ checked_at: Date.now(),
1555
+ can_update: true,
1556
+ check_error: null as string | null,
1557
+ };
1558
+ try {
1559
+ const result = spawnSync("npm", ["view", "@silicaclaw/cli", "dist-tags", "--json"], {
1560
+ cwd: this.projectRoot,
1561
+ encoding: "utf8",
1562
+ env: {
1563
+ ...process.env,
1564
+ SILICACLAW_WORKSPACE_DIR: this.projectRoot,
1565
+ SILICACLAW_APP_DIR: this.workspaceRoot,
1566
+ npm_config_cache: process.env.npm_config_cache || userNpmCacheDir(),
1567
+ },
1568
+ });
1569
+ if ((result.status ?? 1) !== 0) {
1570
+ return {
1571
+ ...fallback,
1572
+ check_error: String(result.stderr || result.stdout || "npm view failed").trim() || "npm view failed",
1573
+ };
1574
+ }
1575
+ const tags = JSON.parse(String(result.stdout || "{}").trim() || "{}") as { latest?: string };
1576
+ const latestVersion = normalizeVersionText(tags.latest || currentVersion) || currentVersion;
1577
+ return {
1578
+ ...fallback,
1579
+ latest_version: latestVersion,
1580
+ update_available: compareVersionTokens(latestVersion, currentVersion) > 0,
1581
+ };
1582
+ } catch (error) {
1583
+ return {
1584
+ ...fallback,
1585
+ check_error: error instanceof Error ? error.message : String(error),
1586
+ };
1587
+ }
1588
+ }
1589
+
1590
+ startAppUpdate(): { started: boolean; target_version: string; platform: string; reason?: string } {
1591
+ const status = this.getAppUpdateStatus();
1592
+ if (!status.update_available || !status.latest_version) {
1593
+ return {
1594
+ started: false,
1595
+ target_version: status.latest_version || status.current_version,
1596
+ platform: process.platform,
1597
+ reason: status.check_error || "already_current",
1598
+ };
1599
+ }
1600
+ const shimPath = userShimPath();
1601
+ const scriptPath = resolve(this.workspaceRoot, "scripts", "silicaclaw-cli.mjs");
1602
+ const useShim = existsSync(shimPath);
1603
+ if (!useShim && !existsSync(scriptPath)) {
1604
+ return {
1605
+ started: false,
1606
+ target_version: status.latest_version,
1607
+ platform: process.platform,
1608
+ reason: "missing_cli_script",
1609
+ };
1610
+ }
1611
+ const command = useShim ? shimPath : process.execPath;
1612
+ const args = useShim ? ["update"] : [scriptPath, "update"];
1613
+ const child = spawn(command, args, {
1614
+ cwd: this.projectRoot,
1615
+ detached: true,
1616
+ stdio: "ignore",
1617
+ env: {
1618
+ ...process.env,
1619
+ SILICACLAW_WORKSPACE_DIR: this.projectRoot,
1620
+ SILICACLAW_APP_DIR: this.workspaceRoot,
1621
+ npm_config_cache: process.env.npm_config_cache || userNpmCacheDir(),
1622
+ },
1623
+ });
1624
+ child.unref();
1625
+ return {
1626
+ started: true,
1627
+ target_version: status.latest_version,
1628
+ platform: process.platform,
1629
+ };
1630
+ }
1631
+
1144
1632
  getIntegrationSummary() {
1145
1633
  const status = this.getIntegrationStatus();
1146
1634
  const runtimeGenerated = Boolean(this.socialRuntime && this.socialRuntime.last_loaded_at > 0);
@@ -1175,9 +1663,10 @@ export class LocalNodeService {
1175
1663
 
1176
1664
  getIntegrationStatus(): IntegrationStatusSummary {
1177
1665
  const runtimeGenerated = Boolean(this.socialRuntime && this.socialRuntime.last_loaded_at > 0);
1178
- const connected = this.socialFound && runtimeGenerated && !this.socialParseError;
1179
- const configured = connected && this.socialConfig.enabled;
1180
- const running = configured && this.broadcastEnabled;
1666
+ const runtimeReady = this.socialFound && runtimeGenerated && !this.socialParseError;
1667
+ const connected = runtimeReady && this.networkStarted;
1668
+ const configured = runtimeReady && this.socialConfig.enabled;
1669
+ const running = configured && this.broadcastEnabled && this.networkStarted;
1181
1670
  const publicEnabled = Boolean(this.profile?.public_enabled);
1182
1671
  const discoveryEnabled =
1183
1672
  this.socialConfig.discovery.discoverable &&
@@ -1199,6 +1688,10 @@ export class LocalNodeService {
1199
1688
  ? "running"
1200
1689
  : !configured
1201
1690
  ? "not configured"
1691
+ : this.networkReconnectTimer
1692
+ ? "reconnecting to relay"
1693
+ : this.networkStartupError
1694
+ ? this.networkStartupError
1202
1695
  : !this.broadcastEnabled
1203
1696
  ? "broadcast paused"
1204
1697
  : "not running";
@@ -1244,20 +1737,36 @@ export class LocalNodeService {
1244
1737
  }
1245
1738
 
1246
1739
  async setNetworkModeRuntime(mode: "local" | "lan" | "global-preview") {
1247
- const currentMode = this.networkMode;
1740
+ const before = {
1741
+ mode: this.networkMode,
1742
+ adapter: this.adapterMode,
1743
+ namespace: this.networkNamespace,
1744
+ port: this.networkPort,
1745
+ };
1248
1746
  if (mode !== "local" && mode !== "lan" && mode !== "global-preview") {
1249
1747
  throw new Error("invalid_network_mode");
1250
1748
  }
1251
1749
  this.socialConfig.network.mode = mode;
1252
1750
  this.socialConfig.network.adapter = this.adapterForMode(mode);
1253
1751
  this.applyResolvedNetworkConfig();
1254
- this.socialNetworkRequiresRestart = currentMode !== mode || this.adapterMode !== this.socialConfig.network.adapter;
1752
+
1753
+ const needsRestart =
1754
+ before.mode !== this.networkMode ||
1755
+ before.adapter !== this.socialConfig.network.adapter ||
1756
+ before.namespace !== this.networkNamespace ||
1757
+ (before.port ?? null) !== (this.networkPort ?? null);
1758
+
1759
+ if (needsRestart) {
1760
+ await this.restartNetworkAdapter("set_network_mode_runtime");
1761
+ }
1762
+
1763
+ this.socialNetworkRequiresRestart = false;
1255
1764
  await this.writeSocialRuntime();
1256
1765
  return {
1257
1766
  mode: this.networkMode,
1258
- adapter: this.socialConfig.network.adapter,
1259
- network_requires_restart: this.socialNetworkRequiresRestart,
1260
- note: "Runtime mode updated. Existing social.md is unchanged.",
1767
+ adapter: this.adapterMode,
1768
+ network_requires_restart: false,
1769
+ note: "Runtime mode updated and adapter restarted. Existing social.md is unchanged.",
1261
1770
  };
1262
1771
  }
1263
1772
 
@@ -1272,7 +1781,7 @@ export class LocalNodeService {
1272
1781
  this.socialConfig.network.adapter = "relay-preview";
1273
1782
  this.socialConfig.network.signaling_url = signalingUrl;
1274
1783
  this.socialConfig.network.signaling_urls = [signalingUrl];
1275
- this.socialConfig.network.room = room || "silicaclaw-global-preview";
1784
+ this.socialConfig.network.room = room || DEFAULT_GLOBAL_ROOM;
1276
1785
  this.applyResolvedNetworkConfig();
1277
1786
  await this.restartNetworkAdapter("quick_connect_global_preview");
1278
1787
  this.socialNetworkRequiresRestart = false;
@@ -1297,7 +1806,7 @@ export class LocalNodeService {
1297
1806
  port: this.networkPort,
1298
1807
  };
1299
1808
 
1300
- const loaded = loadSocialConfig(this.workspaceRoot);
1809
+ const loaded = loadSocialConfig(this.projectRoot);
1301
1810
  this.socialConfig = loaded.config;
1302
1811
  this.socialSourcePath = loaded.meta.source_path;
1303
1812
  this.socialFound = loaded.meta.found;
@@ -1319,13 +1828,18 @@ export class LocalNodeService {
1319
1828
  before.namespace !== after.namespace ||
1320
1829
  (before.port ?? null) !== (after.port ?? null);
1321
1830
 
1831
+ if (this.socialNetworkRequiresRestart) {
1832
+ await this.restartNetworkAdapter("reload_social_config");
1833
+ this.socialNetworkRequiresRestart = false;
1834
+ }
1835
+
1322
1836
  await this.writeSocialRuntime();
1323
1837
 
1324
1838
  return this.getSocialConfigView();
1325
1839
  }
1326
1840
 
1327
1841
  async generateDefaultSocialMd() {
1328
- const result = ensureDefaultSocialMd(this.workspaceRoot, {
1842
+ const result = ensureDefaultSocialMd(this.projectRoot, {
1329
1843
  display_name: this.getDefaultDisplayName(),
1330
1844
  bio: "Local AI agent connected to SilicaClaw",
1331
1845
  tags: ["openclaw", "local-first"],
@@ -1352,10 +1866,11 @@ export class LocalNodeService {
1352
1866
  search(keyword: string): PublicProfileSummary[] {
1353
1867
  this.ensureLocalDirectoryBaseline();
1354
1868
  this.compactCacheInMemory();
1355
- return searchDirectory(this.directory, keyword, { presenceTTLms: PRESENCE_TTL_MS }).map((profile) => {
1869
+ const directMatches = searchDirectory(this.directory, keyword, { presenceTTLms: PRESENCE_TTL_MS }).map((profile) => {
1356
1870
  const lastSeenAt = this.directory.presence[profile.agent_id] ?? 0;
1357
1871
  return this.toPublicProfileSummary(profile, { last_seen_at: lastSeenAt });
1358
1872
  });
1873
+ return this.mergeMessageOnlyAgentSummaries(directMatches, keyword);
1359
1874
  }
1360
1875
 
1361
1876
  getPublicProfilePreview(): PublicProfileSummary | null {
@@ -1412,6 +1927,7 @@ export class LocalNodeService {
1412
1927
  return {
1413
1928
  ...message,
1414
1929
  display_name: profile?.display_name || message.display_name || "Unnamed",
1930
+ avatar_url: profile?.avatar_url || "",
1415
1931
  is_self: message.agent_id === this.identity?.agent_id,
1416
1932
  online: isAgentOnline(lastSeenAt, Date.now(), PRESENCE_TTL_MS),
1417
1933
  last_seen_at: lastSeenAt || null,
@@ -1431,18 +1947,149 @@ export class LocalNodeService {
1431
1947
  };
1432
1948
  }
1433
1949
 
1950
+ getPrivateMessagingState() {
1951
+ return {
1952
+ enabled: Boolean(this.identity && this.privateEncryptionKeyPair),
1953
+ agent_id: this.identity?.agent_id || "",
1954
+ encryption_public_key: this.privateEncryptionKeyPair?.public_key || "",
1955
+ conversation_count: this.getPrivateConversations().length,
1956
+ message_count: this.privateMessages.length,
1957
+ };
1958
+ }
1959
+
1960
+ getPrivateConversations(): Array<{
1961
+ conversation_id: string;
1962
+ peer_agent_id: string;
1963
+ peer_display_name: string;
1964
+ peer_avatar_url: string;
1965
+ peer_public_key: string;
1966
+ last_message_at: number | null;
1967
+ unread_count: number;
1968
+ }> {
1969
+ const conversations = new Map<string, {
1970
+ conversation_id: string;
1971
+ peer_agent_id: string;
1972
+ peer_display_name: string;
1973
+ peer_avatar_url: string;
1974
+ peer_public_key: string;
1975
+ last_message_at: number | null;
1976
+ unread_count: number;
1977
+ }>();
1978
+ for (const message of this.privateMessages) {
1979
+ if (message.from_agent_id === message.to_agent_id) {
1980
+ continue;
1981
+ }
1982
+ const peerAgentId = message.from_agent_id === this.identity?.agent_id ? message.to_agent_id : message.from_agent_id;
1983
+ if (!peerAgentId || peerAgentId === this.identity?.agent_id) {
1984
+ continue;
1985
+ }
1986
+ const peerProfile = this.directory.profiles[peerAgentId];
1987
+ const current = conversations.get(message.conversation_id);
1988
+ const nextLast = Math.max(current?.last_message_at || 0, message.created_at || 0) || null;
1989
+ conversations.set(message.conversation_id, {
1990
+ conversation_id: message.conversation_id,
1991
+ peer_agent_id: peerAgentId,
1992
+ peer_display_name: peerProfile?.display_name || peerAgentId,
1993
+ peer_avatar_url: peerProfile?.avatar_url || "",
1994
+ peer_public_key: peerProfile?.private_encryption_public_key || "",
1995
+ last_message_at: nextLast,
1996
+ unread_count: current?.unread_count || 0,
1997
+ });
1998
+ }
1999
+ return Array.from(conversations.values()).sort((a, b) => (b.last_message_at || 0) - (a.last_message_at || 0));
2000
+ }
2001
+
2002
+ getPrivateMessages(conversationId: string, limit = PRIVATE_MESSAGE_QUERY_LIMIT): PrivateMessageView[] {
2003
+ const normalizedConversationId = String(conversationId || "").trim();
2004
+ const resolvedLimit = Math.max(1, Math.min(PRIVATE_MESSAGE_QUERY_LIMIT, Number(limit) || PRIVATE_MESSAGE_QUERY_LIMIT));
2005
+ const receiptsByMessageId = new Map(
2006
+ this.privateMessageReceipts.map((receipt) => [receipt.message_id, receipt.status] as const)
2007
+ );
2008
+ return this.privateMessages
2009
+ .filter((message) => {
2010
+ if (message.from_agent_id === message.to_agent_id) {
2011
+ return false;
2012
+ }
2013
+ const peerAgentId = message.from_agent_id === this.identity?.agent_id ? message.to_agent_id : message.from_agent_id;
2014
+ if (!peerAgentId || peerAgentId === this.identity?.agent_id) {
2015
+ return false;
2016
+ }
2017
+ return !normalizedConversationId || message.conversation_id === normalizedConversationId;
2018
+ })
2019
+ .sort((a, b) => a.created_at - b.created_at)
2020
+ .slice(-resolvedLimit)
2021
+ .map((message) => ({
2022
+ message_id: message.message_id,
2023
+ conversation_id: message.conversation_id,
2024
+ from_agent_id: message.from_agent_id,
2025
+ to_agent_id: message.to_agent_id,
2026
+ body: this.decryptPrivateMessageBody(message),
2027
+ created_at: message.created_at,
2028
+ is_self: message.from_agent_id === this.identity?.agent_id,
2029
+ delivery_status: receiptsByMessageId.get(message.message_id) || "sent",
2030
+ }));
2031
+ }
2032
+
2033
+ async sendPrivateMessage(input: {
2034
+ to_agent_id: string;
2035
+ recipient_encryption_public_key: string;
2036
+ body: string;
2037
+ }): Promise<{ sent: boolean; reason: string; message?: PrivateMessageView }> {
2038
+ if (!this.identity || !this.privateEncryptionKeyPair) {
2039
+ return { sent: false, reason: "missing_identity_or_private_key" };
2040
+ }
2041
+ const toAgentId = String(input.to_agent_id || "").trim();
2042
+ const recipientKey = String(input.recipient_encryption_public_key || "").trim();
2043
+ const body = String(input.body || "").trim();
2044
+ if (toAgentId === this.identity.agent_id) {
2045
+ return { sent: false, reason: "self_private_message_not_allowed" };
2046
+ }
2047
+ const toPeerId = this.privatePeerRoutes[toAgentId] || "";
2048
+ if (!toAgentId || !toPeerId || !recipientKey || !body) {
2049
+ return { sent: false, reason: "invalid_private_message_input" };
2050
+ }
2051
+ if (typeof this.network.sendDirect !== "function") {
2052
+ return { sent: false, reason: "direct_delivery_not_supported" };
2053
+ }
2054
+ const encrypted = encryptPrivatePayload({
2055
+ plaintext: body,
2056
+ recipient_public_key: recipientKey,
2057
+ sender_keypair: this.privateEncryptionKeyPair,
2058
+ });
2059
+ const message = signPrivateMessage({
2060
+ identity: this.identity,
2061
+ message_id: createHash("sha256").update(`${this.identity.agent_id}:${toAgentId}:${Date.now()}:${body}:${Math.random()}`, "utf8").digest("hex"),
2062
+ conversation_id: this.buildPrivateConversationId(this.identity.agent_id, toAgentId),
2063
+ to_agent_id: toAgentId,
2064
+ sender_encryption_public_key: encrypted.sender_encryption_public_key,
2065
+ recipient_encryption_public_key: recipientKey,
2066
+ ciphertext: encrypted.ciphertext,
2067
+ nonce: encrypted.nonce,
2068
+ created_at: Date.now(),
2069
+ });
2070
+ this.ingestPrivateMessage(message);
2071
+ await this.persistPrivateMessages();
2072
+ await this.network.sendDirect(toPeerId, PRIVATE_MESSAGE_TOPIC, message);
2073
+ const view = this.getPrivateMessages(message.conversation_id).find((item) => item.message_id === message.message_id);
2074
+ return { sent: true, reason: "sent", message: view };
2075
+ }
2076
+
1434
2077
  getOpenClawBridgeStatus(): OpenClawBridgeStatus {
2078
+ const now = Date.now();
2079
+ if (this.openclawBridgeStatusCache && this.openclawBridgeStatusCache.expiresAt > now) {
2080
+ return this.openclawBridgeStatusCache.value;
2081
+ }
1435
2082
  const integration = this.getIntegrationStatus();
1436
- const openclawInstallation = detectOpenClawInstallation(this.workspaceRoot);
1437
- const openclawRuntime = detectOpenClawRuntime(this.workspaceRoot);
2083
+ const openclawInstallation = detectOpenClawInstallation(this.projectRoot);
2084
+ const openclawRuntime = this.getCachedOpenClawRuntime();
1438
2085
  const skillInstallation = detectOpenClawSkillInstallation();
1439
2086
  const ownerDelivery = detectOwnerDeliveryStatus({
1440
- workspaceRoot: this.workspaceRoot,
2087
+ workspaceRoot: this.projectRoot,
1441
2088
  connectedToSilicaclaw: integration.connected_to_silicaclaw,
1442
2089
  openclawRunning: openclawRuntime.running,
1443
2090
  skillInstalled: skillInstallation.installed,
1444
2091
  });
1445
- return {
2092
+ const value: OpenClawBridgeStatus = {
1446
2093
  enabled: this.socialConfig.enabled,
1447
2094
  connected_to_silicaclaw: integration.connected_to_silicaclaw,
1448
2095
  public_enabled: integration.public_enabled,
@@ -1498,16 +2145,26 @@ export class LocalNodeService {
1498
2145
  install_skill: "/api/openclaw/bridge/skill-install",
1499
2146
  },
1500
2147
  };
2148
+ this.openclawBridgeStatusCache = {
2149
+ value,
2150
+ expiresAt: now + OPENCLAW_BRIDGE_STATUS_CACHE_MS,
2151
+ };
2152
+ return value;
1501
2153
  }
1502
2154
 
1503
- async installOpenClawSkill() {
2155
+ async installOpenClawSkill(skillName?: string) {
1504
2156
  const scriptPath = resolve(this.workspaceRoot, "scripts", "install-openclaw-skill.mjs");
1505
- const { stdout } = await execFileAsync(process.execPath, [scriptPath], {
2157
+ const args = [scriptPath];
2158
+ if (skillName) {
2159
+ args.push(`--skill=${skillName}`);
2160
+ }
2161
+ const { stdout } = await execFileAsync(process.execPath, args, {
1506
2162
  cwd: this.workspaceRoot,
1507
- env: process.env,
2163
+ env: { ...process.env, SILICACLAW_WORKSPACE_DIR: this.projectRoot },
1508
2164
  maxBuffer: 1024 * 1024,
1509
2165
  });
1510
2166
  const parsed = JSON.parse(String(stdout || "{}"));
2167
+ this.invalidateOpenClawCaches();
1511
2168
  return {
1512
2169
  ...parsed,
1513
2170
  bridge: this.getOpenClawBridgeStatus(),
@@ -1528,12 +2185,12 @@ export class LocalNodeService {
1528
2185
  const homeDir = resolve(process.env.HOME || "", ".openclaw");
1529
2186
  const workspaceSkillDir = resolve(homeDir, "workspace", "skills");
1530
2187
  const legacySkillDir = resolve(homeDir, "skills");
1531
- const openclawSourceDir = resolve(this.workspaceRoot, "..", "openclaw");
1532
- const openclawRuntime = detectOpenClawRuntime(this.workspaceRoot);
2188
+ const openclawSourceDir = defaultOpenClawSourceDir(this.projectRoot);
2189
+ const openclawRuntime = this.getCachedOpenClawRuntime();
1533
2190
 
1534
2191
  return {
1535
- bridge_api_base: "http://localhost:4310",
1536
- openclaw_detected: detectOpenClawInstallation(this.workspaceRoot).detected,
2192
+ bridge_api_base: DEFAULT_BRIDGE_API_BASE,
2193
+ openclaw_detected: detectOpenClawInstallation(this.projectRoot).detected,
1537
2194
  openclaw_running: openclawRuntime.running,
1538
2195
  openclaw_gateway_host: OPENCLAW_GATEWAY_HOST,
1539
2196
  openclaw_gateway_port: openclawRuntime.configured_gateway_port,
@@ -1542,7 +2199,7 @@ export class LocalNodeService {
1542
2199
  openclaw_workspace_skill_dir: workspaceSkillDir,
1543
2200
  openclaw_legacy_skill_dir: legacySkillDir,
1544
2201
  silicaclaw_env_template_path: resolve(this.workspaceRoot, "openclaw-owner-forward.env.example"),
1545
- recommended_skill_name: "silicaclaw-broadcast",
2202
+ recommended_skill_name: "silicaclaw-bridge-setup",
1546
2203
  recommended_install_command: "silicaclaw openclaw-skill-install",
1547
2204
  recommended_owner_forward_env: {
1548
2205
  OPENCLAW_SOURCE_DIR: openclawSourceDir,
@@ -1560,6 +2217,7 @@ export class LocalNodeService {
1560
2217
  ].join(" "),
1561
2218
  notes: [
1562
2219
  "Install and maintain the skill from SilicaClaw; do not edit OpenClaw core source for this integration.",
2220
+ "Use silicaclaw-bridge-setup first when OpenClaw still needs local install, readiness checks, or troubleshooting guidance.",
1563
2221
  "OpenClaw learns broadcasts via the installed skill under ~/.openclaw/workspace/skills/.",
1564
2222
  "Runtime detection prefers the actual OpenClaw gateway listener port, then falls back to OpenClaw's own openclaw.json gateway.port.",
1565
2223
  "Owner delivery runs through OpenClaw's own message channel stack after the skill forwards a summary.",
@@ -1579,6 +2237,12 @@ export class LocalNodeService {
1579
2237
  const skillPath = resolve(dir.path, "SKILL.md");
1580
2238
  const versionPath = resolve(dir.path, "VERSION");
1581
2239
  const manifest = readJsonFileSafe(manifestPath);
2240
+ const references = (manifest?.references && typeof manifest.references === "object")
2241
+ ? manifest.references as Record<string, unknown>
2242
+ : null;
2243
+ const ownerDialogueCheatsheetPath = references?.owner_dialogue_cheatsheet_zh
2244
+ ? resolve(dir.path, String(references.owner_dialogue_cheatsheet_zh))
2245
+ : null;
1582
2246
  const name = String(manifest?.name || dir.name);
1583
2247
  const capabilities = Array.isArray(manifest?.capabilities)
1584
2248
  ? manifest.capabilities.map((item) => String(item))
@@ -1598,6 +2262,9 @@ export class LocalNodeService {
1598
2262
  skill_path: existsSync(skillPath) ? skillPath : null,
1599
2263
  capabilities,
1600
2264
  transport: manifest?.transport || null,
2265
+ owner_dialogue_cheatsheet_path: ownerDialogueCheatsheetPath && existsSync(ownerDialogueCheatsheetPath) ? ownerDialogueCheatsheetPath : null,
2266
+ owner_dialogue_examples_zh: ownerDialogueCheatsheetPath ? readDialogueCheatsheetPreview(ownerDialogueCheatsheetPath) : [],
2267
+ owner_dialogue_sections_zh: ownerDialogueCheatsheetPath ? readDialogueCheatsheetSections(ownerDialogueCheatsheetPath) : [],
1601
2268
  installed_in_openclaw: installedInWorkspace || installedInLegacy,
1602
2269
  install_mode: installedInWorkspace ? "workspace" : installedInLegacy ? "legacy" : "not_installed",
1603
2270
  installed_path: installedInWorkspace ? installedWorkspacePath : installedInLegacy ? installedLegacyPath : null,
@@ -1612,6 +2279,12 @@ export class LocalNodeService {
1612
2279
  const skillPath = resolve(dir.path, "SKILL.md");
1613
2280
  const versionPath = resolve(dir.path, "VERSION");
1614
2281
  const manifest = readJsonFileSafe(manifestPath);
2282
+ const references = (manifest?.references && typeof manifest.references === "object")
2283
+ ? manifest.references as Record<string, unknown>
2284
+ : null;
2285
+ const ownerDialogueCheatsheetPath = references?.owner_dialogue_cheatsheet_zh
2286
+ ? resolve(dir.path, String(references.owner_dialogue_cheatsheet_zh))
2287
+ : null;
1615
2288
  return {
1616
2289
  key: `${dir.install_mode}:${dir.name}`,
1617
2290
  name: String(manifest?.name || dir.name),
@@ -1623,10 +2296,43 @@ export class LocalNodeService {
1623
2296
  manifest_path: existsSync(manifestPath) ? manifestPath : null,
1624
2297
  skill_path: existsSync(skillPath) ? skillPath : null,
1625
2298
  capabilities: Array.isArray(manifest?.capabilities) ? manifest.capabilities.map((item) => String(item)) : [],
2299
+ owner_dialogue_cheatsheet_path: ownerDialogueCheatsheetPath && existsSync(ownerDialogueCheatsheetPath) ? ownerDialogueCheatsheetPath : null,
2300
+ owner_dialogue_examples_zh: ownerDialogueCheatsheetPath ? readDialogueCheatsheetPreview(ownerDialogueCheatsheetPath) : [],
2301
+ owner_dialogue_sections_zh: ownerDialogueCheatsheetPath ? readDialogueCheatsheetSections(ownerDialogueCheatsheetPath) : [],
1626
2302
  bundled_source_path: bundledSkills.find((item) => item.name === String(manifest?.name || dir.name))?.source_path || null,
1627
2303
  };
1628
2304
  });
1629
2305
 
2306
+ const installedSkillVersions = new Map(installedSkills.map((item) => [item.name, item.version]));
2307
+ const bundledSkillsWithUpdateState = bundledSkills.map((skill) => {
2308
+ const installedVersion = installedSkillVersions.get(skill.name) || "";
2309
+ const updateAvailable = Boolean(
2310
+ skill.installed_in_openclaw &&
2311
+ installedVersion &&
2312
+ skill.version &&
2313
+ compareVersionTokens(installedVersion, skill.version) < 0
2314
+ );
2315
+ return {
2316
+ ...skill,
2317
+ installed_version: installedVersion || null,
2318
+ update_available: updateAvailable,
2319
+ };
2320
+ });
2321
+ const bundledSkillVersions = new Map(bundledSkillsWithUpdateState.map((item) => [item.name, item.version]));
2322
+ const installedSkillsWithUpdateState = installedSkills.map((skill) => {
2323
+ const bundledVersion = bundledSkillVersions.get(skill.name) || "";
2324
+ const updateAvailable = Boolean(
2325
+ bundledVersion &&
2326
+ skill.version &&
2327
+ compareVersionTokens(skill.version, bundledVersion) < 0
2328
+ );
2329
+ return {
2330
+ ...skill,
2331
+ bundled_version: bundledVersion || null,
2332
+ update_available: updateAvailable,
2333
+ };
2334
+ });
2335
+
1630
2336
  return {
1631
2337
  openclaw: {
1632
2338
  detected: bridge.openclaw_installation.detected,
@@ -1637,13 +2343,14 @@ export class LocalNodeService {
1637
2343
  legacy_install_root: legacyInstallRoot,
1638
2344
  },
1639
2345
  summary: {
1640
- bundled_count: bundledSkills.length,
1641
- installed_count: installedSkills.length,
1642
- installed_bundled_count: bundledSkills.filter((item) => item.installed_in_openclaw).length,
2346
+ bundled_count: bundledSkillsWithUpdateState.length,
2347
+ installed_count: installedSkillsWithUpdateState.length,
2348
+ installed_bundled_count: bundledSkillsWithUpdateState.filter((item) => item.installed_in_openclaw).length,
2349
+ update_available_count: bundledSkillsWithUpdateState.filter((item) => item.update_available).length,
1643
2350
  },
1644
2351
  install_action: bridge.skill_learning.install_action,
1645
- bundled_skills: bundledSkills,
1646
- installed_skills: installedSkills,
2352
+ bundled_skills: bundledSkillsWithUpdateState,
2353
+ installed_skills: installedSkillsWithUpdateState,
1647
2354
  };
1648
2355
  }
1649
2356
 
@@ -1889,20 +2596,25 @@ export class LocalNodeService {
1889
2596
  profile: this.profile,
1890
2597
  };
1891
2598
  const presenceRecord = signPresence(this.identity, Date.now());
1892
- const indexRecords = buildIndexRecords(this.profile);
2599
+ const shouldPublishProfile = this.shouldPublishProfileRecord(profileRecord, reason, presenceRecord.timestamp);
2600
+ const replayMessages = this.getReplayableSelfSocialMessages(reason);
1893
2601
 
1894
2602
  try {
1895
- await this.publish("profile", profileRecord);
2603
+ if (shouldPublishProfile) {
2604
+ await this.publish("profile", profileRecord);
2605
+ }
1896
2606
  await this.publish("presence", presenceRecord);
1897
- for (const record of indexRecords) {
1898
- await this.publish("index", record);
2607
+ for (const message of replayMessages) {
2608
+ await this.publish(SOCIAL_MESSAGE_TOPIC, message);
1899
2609
  }
1900
2610
  } catch (error) {
1901
2611
  const message = error instanceof Error ? error.message : String(error);
1902
2612
  this.lastBroadcastErrorAt = Date.now();
1903
2613
  this.lastBroadcastError = message;
1904
2614
  this.broadcastFailureCount += 1;
2615
+ this.consecutiveBroadcastFailures += 1;
1905
2616
  await this.log("error", `Broadcast failed (reason=${reason}): ${message}`);
2617
+ await this.maybeRecoverFromBroadcastFailure(reason, message);
1906
2618
  return { sent: false, reason: "publish_failed", error: message };
1907
2619
  }
1908
2620
 
@@ -1910,19 +2622,75 @@ export class LocalNodeService {
1910
2622
  this.broadcastCount += 1;
1911
2623
  this.lastBroadcastError = null;
1912
2624
  this.lastBroadcastErrorAt = 0;
2625
+ this.consecutiveBroadcastFailures = 0;
1913
2626
 
1914
2627
  this.directory = ingestProfileRecord(this.directory, profileRecord);
1915
2628
  this.directory = ingestPresenceRecord(this.directory, presenceRecord);
1916
- for (const record of indexRecords) {
1917
- this.directory = ingestIndexRecord(this.directory, record);
1918
- }
1919
2629
  this.compactCacheInMemory();
1920
2630
  await this.persistCache();
1921
2631
 
1922
- await this.log("info", `Broadcast sent (${indexRecords.length} index refs, reason=${reason})`);
2632
+ await this.log(
2633
+ "info",
2634
+ `Broadcast sent (${shouldPublishProfile ? "profile + " : ""}presence, replayed_messages=${replayMessages.length}, reason=${reason})`
2635
+ );
1923
2636
  return { sent: true, reason };
1924
2637
  }
1925
2638
 
2639
+ private shouldPublishProfileRecord(
2640
+ profileRecord: SignedProfileRecord,
2641
+ reason: string,
2642
+ now = Date.now()
2643
+ ): boolean {
2644
+ if (reason !== "interval") {
2645
+ this.lastProfileBroadcastSignature = profileRecord.profile.signature;
2646
+ this.lastProfileBroadcastAt = now;
2647
+ return true;
2648
+ }
2649
+ const signature = profileRecord.profile.signature;
2650
+ const changedSinceLastPublish = signature !== this.lastProfileBroadcastSignature;
2651
+ const refreshDue = now - this.lastProfileBroadcastAt >= PROFILE_RELAY_REFRESH_INTERVAL_MS;
2652
+ if (!changedSinceLastPublish && !refreshDue) {
2653
+ return false;
2654
+ }
2655
+ this.lastProfileBroadcastSignature = signature;
2656
+ this.lastProfileBroadcastAt = now;
2657
+ return true;
2658
+ }
2659
+
2660
+ private async maybeRecoverFromBroadcastFailure(reason: string, errorMessage: string): Promise<void> {
2661
+ const recoveryThreshold = 3;
2662
+ const recoveryCooldownMs = 60_000;
2663
+ if (this.broadcastRecoveryInFlight) {
2664
+ return;
2665
+ }
2666
+ if (this.consecutiveBroadcastFailures < recoveryThreshold) {
2667
+ return;
2668
+ }
2669
+ if (Date.now() - this.lastBroadcastRecoveryAttemptAt < recoveryCooldownMs) {
2670
+ return;
2671
+ }
2672
+ if (this.adapterMode !== "relay-preview" && this.adapterMode !== "webrtc-preview" && this.adapterMode !== "real-preview") {
2673
+ return;
2674
+ }
2675
+
2676
+ this.broadcastRecoveryInFlight = true;
2677
+ this.lastBroadcastRecoveryAttemptAt = Date.now();
2678
+ try {
2679
+ await this.log(
2680
+ "warn",
2681
+ `Broadcast recovery triggered after ${this.consecutiveBroadcastFailures} consecutive failures (${reason}): ${errorMessage}`
2682
+ );
2683
+ await this.restartNetworkAdapter("broadcast_failure_recovery");
2684
+ } catch (recoveryError) {
2685
+ await this.log(
2686
+ "error",
2687
+ `Broadcast recovery failed: ${recoveryError instanceof Error ? recoveryError.message : String(recoveryError)}`
2688
+ );
2689
+ } finally {
2690
+ this.broadcastRecoveryInFlight = false;
2691
+ }
2692
+ }
2693
+
1926
2694
  private async hydrateFromDisk(): Promise<void> {
1927
2695
  this.initState = {
1928
2696
  identity_auto_created: false,
@@ -1939,7 +2707,7 @@ export class LocalNodeService {
1939
2707
  socialConfig: this.socialConfig,
1940
2708
  existingIdentity,
1941
2709
  generatedIdentity: createIdentity(),
1942
- rootDir: this.workspaceRoot,
2710
+ rootDir: this.projectRoot,
1943
2711
  });
1944
2712
  this.identity = resolvedIdentity.identity;
1945
2713
  this.resolvedIdentitySource = resolvedIdentity.source;
@@ -1952,28 +2720,35 @@ export class LocalNodeService {
1952
2720
  await this.log("info", `Bound existing OpenClaw identity: ${resolvedIdentity.openclaw_source_path}`);
1953
2721
  }
1954
2722
  await this.identityRepo.set(this.identity);
2723
+ this.privateEncryptionKeyPair = (await this.privateEncryptionKeyRepo.get()) || createPrivateEncryptionKeyPair();
2724
+ await this.privateEncryptionKeyRepo.set(this.privateEncryptionKeyPair);
1955
2725
 
1956
2726
  const existingProfile = await this.profileRepo.get();
1957
2727
  const profileInput = resolveProfileInputWithSocial({
1958
2728
  socialConfig: this.socialConfig,
1959
2729
  agentId: this.identity.agent_id,
1960
2730
  existingProfile: existingProfile && existingProfile.agent_id === this.identity.agent_id ? existingProfile : null,
1961
- rootDir: this.workspaceRoot,
2731
+ rootDir: this.projectRoot,
1962
2732
  });
1963
- this.profile = signProfile(profileInput, this.identity);
2733
+ this.profile = signProfile({
2734
+ ...profileInput,
2735
+ private_encryption_public_key: this.privateEncryptionKeyPair?.public_key || profileInput.private_encryption_public_key || "",
2736
+ }, this.identity);
1964
2737
  if (!existingProfile || existingProfile.agent_id !== this.identity.agent_id) {
1965
2738
  this.initState.profile_auto_created = true;
1966
2739
  await this.log("info", "profile.json missing/invalid, initialized from social/default profile");
1967
2740
  }
1968
2741
  await this.profileRepo.set(this.profile);
1969
2742
 
1970
- this.directory = dedupeIndex(await this.cacheRepo.get());
2743
+ this.directory = createEmptyDirectoryState();
1971
2744
  this.messageGovernance = {
1972
2745
  ...this.defaultMessageGovernance(),
1973
2746
  ...(await this.socialMessageGovernanceRepo.get()),
1974
2747
  };
1975
2748
  this.socialMessages = this.normalizeSocialMessages(await this.socialMessageRepo.get());
1976
2749
  this.socialMessageObservations = this.normalizeSocialMessageObservations(await this.socialMessageObservationRepo.get());
2750
+ this.privateMessages = this.normalizePrivateMessages(await this.privateMessageRepo.get());
2751
+ this.privateMessageReceipts = this.normalizePrivateMessageReceipts(await this.privateMessageReceiptRepo.get());
1977
2752
  this.directory = ingestProfileRecord(this.directory, { type: "profile", profile: this.profile });
1978
2753
  this.compactCacheInMemory();
1979
2754
  await this.persistCache();
@@ -1990,9 +2765,12 @@ export class LocalNodeService {
1990
2765
  socialConfig: this.socialConfig,
1991
2766
  agentId: this.identity.agent_id,
1992
2767
  existingProfile: this.profile,
1993
- rootDir: this.workspaceRoot,
2768
+ rootDir: this.projectRoot,
1994
2769
  });
1995
- const nextProfile = signProfile(nextProfileInput, this.identity);
2770
+ const nextProfile = signProfile({
2771
+ ...nextProfileInput,
2772
+ private_encryption_public_key: this.privateEncryptionKeyPair?.public_key || nextProfileInput.private_encryption_public_key || "",
2773
+ }, this.identity);
1996
2774
  this.profile = nextProfile;
1997
2775
  await this.profileRepo.set(nextProfile);
1998
2776
 
@@ -2057,7 +2835,8 @@ export class LocalNodeService {
2057
2835
 
2058
2836
  private async onMessage(
2059
2837
  topic: "profile" | "presence" | "index" | "social.message" | "social.message.observation",
2060
- data: unknown
2838
+ data: unknown,
2839
+ meta?: { peerId?: string }
2061
2840
  ): Promise<void> {
2062
2841
  this.receivedCount += 1;
2063
2842
  this.receivedByTopic[topic] = (this.receivedByTopic[topic] ?? 0) + 1;
@@ -2075,6 +2854,9 @@ export class LocalNodeService {
2075
2854
  return;
2076
2855
  }
2077
2856
  }
2857
+ if (meta?.peerId && record.profile.agent_id) {
2858
+ this.privatePeerRoutes[record.profile.agent_id] = meta.peerId;
2859
+ }
2078
2860
 
2079
2861
  this.directory = ingestProfileRecord(this.directory, record);
2080
2862
  this.compactCacheInMemory();
@@ -2094,6 +2876,9 @@ export class LocalNodeService {
2094
2876
  return;
2095
2877
  }
2096
2878
  }
2879
+ if (meta?.peerId && record.agent_id) {
2880
+ this.privatePeerRoutes[record.agent_id] = meta.peerId;
2881
+ }
2097
2882
 
2098
2883
  this.directory = ingestPresenceRecord(this.directory, record);
2099
2884
  this.compactCacheInMemory();
@@ -2110,6 +2895,13 @@ export class LocalNodeService {
2110
2895
  await this.log("warn", `Rejected social message with invalid signature (${record.message_id.slice(0, 10)})`);
2111
2896
  return;
2112
2897
  }
2898
+ if (meta?.peerId && record.agent_id) {
2899
+ this.privatePeerRoutes[record.agent_id] = meta.peerId;
2900
+ }
2901
+ if (this.hasSocialMessage(record.message_id)) {
2902
+ await this.publishObservationForMessage(record);
2903
+ return;
2904
+ }
2113
2905
  const governanceReason = this.getIncomingSocialMessageRejectionReason(record);
2114
2906
  if (governanceReason) {
2115
2907
  await this.log("warn", `Rejected social message (${record.message_id.slice(0, 10)}): ${governanceReason}`);
@@ -2144,12 +2936,42 @@ export class LocalNodeService {
2144
2936
  await this.persistCache();
2145
2937
  }
2146
2938
 
2939
+ private async onDirectMessage(
2940
+ topic: "private.message" | "private.message.receipt",
2941
+ data: unknown,
2942
+ meta?: { peerId?: string }
2943
+ ): Promise<void> {
2944
+ if (topic === PRIVATE_MESSAGE_TOPIC) {
2945
+ const record = this.normalizeIncomingPrivateMessage(data);
2946
+ if (!record || !verifyPrivateMessage(record)) {
2947
+ return;
2948
+ }
2949
+ if (record.to_agent_id !== this.identity?.agent_id || this.hasPrivateMessage(record.message_id)) {
2950
+ return;
2951
+ }
2952
+ this.ingestPrivateMessage(record);
2953
+ await this.persistPrivateMessages();
2954
+ await this.sendPrivateMessageReceipt(record, meta?.peerId);
2955
+ return;
2956
+ }
2957
+
2958
+ const receipt = this.normalizeIncomingPrivateMessageReceipt(data);
2959
+ if (!receipt || !verifyPrivateMessageReceipt(receipt)) {
2960
+ return;
2961
+ }
2962
+ if (receipt.to_agent_id !== this.identity?.agent_id) {
2963
+ return;
2964
+ }
2965
+ this.ingestPrivateMessageReceipt(receipt);
2966
+ await this.persistPrivateMessageReceipts();
2967
+ }
2968
+
2147
2969
  private startBroadcastLoop(): void {
2148
2970
  if (this.broadcaster) {
2149
2971
  clearInterval(this.broadcaster);
2150
2972
  }
2151
2973
 
2152
- if (!this.broadcastEnabled) {
2974
+ if (!this.broadcastEnabled || !this.networkStarted) {
2153
2975
  return;
2154
2976
  }
2155
2977
 
@@ -2169,21 +2991,29 @@ export class LocalNodeService {
2169
2991
  if (this.subscriptionsBound) {
2170
2992
  return;
2171
2993
  }
2172
- this.network.subscribe("profile", (data: SignedProfileRecord) => {
2173
- this.onMessage("profile", data);
2994
+ this.network.subscribe("profile", (data: SignedProfileRecord, meta?: { peerId?: string }) => {
2995
+ this.onMessage("profile", data, meta);
2174
2996
  });
2175
- this.network.subscribe("presence", (data: PresenceRecord) => {
2176
- this.onMessage("presence", data);
2997
+ this.network.subscribe("presence", (data: PresenceRecord, meta?: { peerId?: string }) => {
2998
+ this.onMessage("presence", data, meta);
2177
2999
  });
2178
- this.network.subscribe("index", (data: IndexRefRecord) => {
2179
- this.onMessage("index", data);
3000
+ this.network.subscribe("index", (data: IndexRefRecord, meta?: { peerId?: string }) => {
3001
+ this.onMessage("index", data, meta);
2180
3002
  });
2181
- this.network.subscribe(SOCIAL_MESSAGE_TOPIC, (data: SocialMessageRecord) => {
2182
- this.onMessage(SOCIAL_MESSAGE_TOPIC, data);
3003
+ this.network.subscribe(SOCIAL_MESSAGE_TOPIC, (data: SocialMessageRecord, meta?: { peerId?: string }) => {
3004
+ this.onMessage(SOCIAL_MESSAGE_TOPIC, data, meta);
2183
3005
  });
2184
- this.network.subscribe(SOCIAL_MESSAGE_OBSERVATION_TOPIC, (data: SocialMessageObservationRecord) => {
2185
- this.onMessage(SOCIAL_MESSAGE_OBSERVATION_TOPIC, data);
3006
+ this.network.subscribe(SOCIAL_MESSAGE_OBSERVATION_TOPIC, (data: SocialMessageObservationRecord, meta?: { peerId?: string }) => {
3007
+ this.onMessage(SOCIAL_MESSAGE_OBSERVATION_TOPIC, data, meta);
2186
3008
  });
3009
+ if (typeof this.network.subscribeDirect === "function") {
3010
+ this.network.subscribeDirect(PRIVATE_MESSAGE_TOPIC, (data: PrivateMessageRecord, meta?: { peerId?: string }) => {
3011
+ this.onDirectMessage(PRIVATE_MESSAGE_TOPIC, data, meta);
3012
+ });
3013
+ this.network.subscribeDirect(PRIVATE_MESSAGE_RECEIPT_TOPIC, (data: PrivateMessageReceiptRecord, meta?: { peerId?: string }) => {
3014
+ this.onDirectMessage(PRIVATE_MESSAGE_RECEIPT_TOPIC, data, meta);
3015
+ });
3016
+ }
2187
3017
  this.subscriptionsBound = true;
2188
3018
  }
2189
3019
 
@@ -2271,6 +3101,7 @@ export class LocalNodeService {
2271
3101
  }
2272
3102
 
2273
3103
  private async restartNetworkAdapter(reason: string): Promise<void> {
3104
+ this.clearNetworkReconnectTimer();
2274
3105
  const previous = this.network;
2275
3106
  try {
2276
3107
  await previous.stop();
@@ -2282,19 +3113,123 @@ export class LocalNodeService {
2282
3113
  this.network = next.adapter;
2283
3114
  this.adapterMode = next.mode;
2284
3115
  this.networkPort = next.port;
2285
-
2286
- await this.network.start();
3116
+ this.subscriptionsBound = false;
2287
3117
  this.bindNetworkSubscriptions();
2288
- this.startBroadcastLoop();
3118
+ await this.startNetworkAdapterWithRetry(reason);
3119
+ }
3120
+
3121
+ private clearNetworkReconnectTimer(): void {
3122
+ if (this.networkReconnectTimer) {
3123
+ clearTimeout(this.networkReconnectTimer);
3124
+ this.networkReconnectTimer = null;
3125
+ }
3126
+ }
3127
+
3128
+ private async startNetworkAdapterWithRetry(reason: string): Promise<void> {
3129
+ this.clearNetworkReconnectTimer();
3130
+ try {
3131
+ await this.network.start();
3132
+ this.networkStarted = true;
3133
+ this.networkStartupError = null;
3134
+ this.networkReconnectDelayMs = 5_000;
3135
+ await this.log(
3136
+ "info",
3137
+ `Local node started (${this.adapterMode}, mode=${this.networkMode}, signaling=${this.webrtcSignalingUrls[0] || "-"}, room=${this.webrtcRoom})`
3138
+ );
3139
+ this.startBroadcastLoop();
3140
+ if (this.broadcastEnabled && this.profile?.public_enabled) {
3141
+ try {
3142
+ await this.broadcastNow(reason);
3143
+ } catch (error) {
3144
+ await this.log(
3145
+ "warn",
3146
+ `Initial broadcast failed: ${error instanceof Error ? error.message : String(error)}`
3147
+ );
3148
+ }
3149
+ }
3150
+ } catch (error) {
3151
+ this.networkStarted = false;
3152
+ this.networkStartupError = error instanceof Error ? error.message : String(error);
3153
+ await this.log(
3154
+ "warn",
3155
+ `Network start failed (${this.adapterMode}, mode=${this.networkMode}): ${this.networkStartupError}`
3156
+ );
3157
+ this.scheduleNetworkReconnect();
3158
+ }
3159
+ }
2289
3160
 
2290
- if (this.broadcastEnabled && this.profile?.public_enabled) {
2291
- await this.broadcastNow("adapter_restart");
3161
+ private scheduleNetworkReconnect(): void {
3162
+ if (this.networkReconnectTimer) {
3163
+ return;
3164
+ }
3165
+ const delayMs = this.networkReconnectDelayMs;
3166
+ this.networkReconnectTimer = setTimeout(() => {
3167
+ this.networkReconnectTimer = null;
3168
+ void this.startNetworkAdapterWithRetry("adapter_reconnect");
3169
+ }, delayMs);
3170
+ this.networkReconnectDelayMs = Math.min(30_000, Math.max(5_000, Math.floor(delayMs * 1.5)));
3171
+ }
3172
+
3173
+ private pruneRemoteProfilesInMemory(now = Date.now()): number {
3174
+ if (!Number.isFinite(DIRECTORY_REMOTE_PROFILE_SOFT_LIMIT) || DIRECTORY_REMOTE_PROFILE_SOFT_LIMIT <= 0) {
3175
+ return 0;
3176
+ }
3177
+ const selfAgentId = this.profile?.agent_id || this.identity?.agent_id || "";
3178
+ const remoteProfiles = Object.values(this.directory.profiles).filter((profile) => profile.agent_id !== selfAgentId);
3179
+ if (remoteProfiles.length <= DIRECTORY_REMOTE_PROFILE_SOFT_LIMIT) {
3180
+ return 0;
3181
+ }
3182
+
3183
+ const onlineRemoteProfiles = remoteProfiles.filter((profile) =>
3184
+ isAgentOnline(this.directory.presence[profile.agent_id], now, PRESENCE_TTL_MS)
3185
+ );
3186
+ const offlineRemoteProfiles = remoteProfiles
3187
+ .filter((profile) => !isAgentOnline(this.directory.presence[profile.agent_id], now, PRESENCE_TTL_MS))
3188
+ .sort((a, b) => (b.updated_at || 0) - (a.updated_at || 0));
3189
+
3190
+ const keepOfflineCount = Math.max(0, DIRECTORY_REMOTE_PROFILE_SOFT_LIMIT - onlineRemoteProfiles.length);
3191
+ const keptRemoteProfiles = [
3192
+ ...onlineRemoteProfiles,
3193
+ ...offlineRemoteProfiles.slice(0, keepOfflineCount),
3194
+ ];
3195
+ const keptRemoteIds = new Set(keptRemoteProfiles.map((profile) => profile.agent_id));
3196
+ const removedIds = remoteProfiles
3197
+ .map((profile) => profile.agent_id)
3198
+ .filter((agentId) => !keptRemoteIds.has(agentId));
3199
+ if (removedIds.length === 0) {
3200
+ return 0;
3201
+ }
3202
+
3203
+ const next = createEmptyDirectoryState();
3204
+ const selfProfile = selfAgentId ? this.directory.profiles[selfAgentId] : null;
3205
+ if (selfProfile) {
3206
+ next.profiles[selfAgentId] = selfProfile;
3207
+ const selfPresence = this.directory.presence[selfAgentId];
3208
+ if (typeof selfPresence === "number" && Number.isFinite(selfPresence)) {
3209
+ next.presence[selfAgentId] = selfPresence;
3210
+ }
3211
+ const rebuilt = rebuildIndexForProfile(next, selfProfile);
3212
+ next.index = rebuilt.index;
3213
+ }
3214
+
3215
+ for (const profile of keptRemoteProfiles) {
3216
+ next.profiles[profile.agent_id] = profile;
3217
+ const seenAt = this.directory.presence[profile.agent_id];
3218
+ if (typeof seenAt === "number" && Number.isFinite(seenAt)) {
3219
+ next.presence[profile.agent_id] = seenAt;
3220
+ }
3221
+ const rebuilt = rebuildIndexForProfile(next, profile);
3222
+ next.index = rebuilt.index;
2292
3223
  }
3224
+
3225
+ this.directory = dedupeIndex(next);
3226
+ return removedIds.length;
2293
3227
  }
2294
3228
 
2295
3229
  private compactCacheInMemory(): number {
2296
3230
  const cleaned = cleanupExpiredPresence(this.directory, Date.now(), PRESENCE_TTL_MS);
2297
3231
  this.directory = dedupeIndex(cleaned.state);
3232
+ this.pruneRemoteProfilesInMemory();
2298
3233
  return cleaned.removed;
2299
3234
  }
2300
3235
 
@@ -2304,7 +3239,22 @@ export class LocalNodeService {
2304
3239
  }
2305
3240
 
2306
3241
  private async persistCache(): Promise<void> {
2307
- await this.cacheRepo.set(this.directory);
3242
+ const persisted = createEmptyDirectoryState();
3243
+ if (this.profile) {
3244
+ const selfProfileRecord: SignedProfileRecord = {
3245
+ type: "profile",
3246
+ profile: this.profile,
3247
+ };
3248
+ this.directory = ingestProfileRecord(this.directory, selfProfileRecord);
3249
+ persisted.profiles[this.profile.agent_id] = this.profile;
3250
+ const selfLastSeenAt = this.directory.presence[this.profile.agent_id];
3251
+ if (typeof selfLastSeenAt === "number" && Number.isFinite(selfLastSeenAt)) {
3252
+ persisted.presence[this.profile.agent_id] = selfLastSeenAt;
3253
+ }
3254
+ const indexed = rebuildIndexForProfile(persisted, this.profile);
3255
+ persisted.index = indexed.index;
3256
+ }
3257
+ await this.cacheRepo.set(persisted);
2308
3258
  }
2309
3259
 
2310
3260
  private async persistSocialMessages(): Promise<void> {
@@ -2315,6 +3265,57 @@ export class LocalNodeService {
2315
3265
  await this.socialMessageObservationRepo.set(this.socialMessageObservations);
2316
3266
  }
2317
3267
 
3268
+ private async persistPrivateMessages(): Promise<void> {
3269
+ this.privateMessagesPersistDirty = true;
3270
+ if (this.privateMessagesPersistTimer) {
3271
+ return;
3272
+ }
3273
+ this.privateMessagesPersistTimer = setTimeout(() => {
3274
+ this.flushPrivateMessagesPersist().catch(() => {});
3275
+ }, PRIVATE_MESSAGE_PERSIST_DEBOUNCE_MS);
3276
+ }
3277
+
3278
+ private async persistPrivateMessageReceipts(): Promise<void> {
3279
+ this.privateMessageReceiptsPersistDirty = true;
3280
+ if (this.privateMessageReceiptsPersistTimer) {
3281
+ return;
3282
+ }
3283
+ this.privateMessageReceiptsPersistTimer = setTimeout(() => {
3284
+ this.flushPrivateMessageReceiptsPersist().catch(() => {});
3285
+ }, PRIVATE_MESSAGE_PERSIST_DEBOUNCE_MS);
3286
+ }
3287
+
3288
+ private async flushPrivatePersistence(): Promise<void> {
3289
+ await Promise.all([
3290
+ this.flushPrivateMessagesPersist(),
3291
+ this.flushPrivateMessageReceiptsPersist(),
3292
+ ]);
3293
+ }
3294
+
3295
+ private async flushPrivateMessagesPersist(): Promise<void> {
3296
+ if (this.privateMessagesPersistTimer) {
3297
+ clearTimeout(this.privateMessagesPersistTimer);
3298
+ this.privateMessagesPersistTimer = null;
3299
+ }
3300
+ if (!this.privateMessagesPersistDirty) {
3301
+ return;
3302
+ }
3303
+ this.privateMessagesPersistDirty = false;
3304
+ await this.privateMessageRepo.set(this.privateMessages);
3305
+ }
3306
+
3307
+ private async flushPrivateMessageReceiptsPersist(): Promise<void> {
3308
+ if (this.privateMessageReceiptsPersistTimer) {
3309
+ clearTimeout(this.privateMessageReceiptsPersistTimer);
3310
+ this.privateMessageReceiptsPersistTimer = null;
3311
+ }
3312
+ if (!this.privateMessageReceiptsPersistDirty) {
3313
+ return;
3314
+ }
3315
+ this.privateMessageReceiptsPersistDirty = false;
3316
+ await this.privateMessageReceiptRepo.set(this.privateMessageReceipts);
3317
+ }
3318
+
2318
3319
  private async log(level: "info" | "warn" | "error", message: string): Promise<void> {
2319
3320
  await this.logRepo.append({
2320
3321
  level,
@@ -2330,6 +3331,19 @@ export class LocalNodeService {
2330
3331
  return (this.network as any).getDiagnostics();
2331
3332
  }
2332
3333
 
3334
+ private getResolvedRealtimeNetworkSummary() {
3335
+ const diagnostics = this.getAdapterDiagnostics();
3336
+ const relayCapable = this.adapterMode === "webrtc-preview" || this.adapterMode === "relay-preview";
3337
+ return {
3338
+ diagnostics,
3339
+ signaling_url: diagnostics?.signaling_url ?? (relayCapable ? this.webrtcSignalingUrls[0] ?? null : null),
3340
+ signaling_endpoints: diagnostics?.signaling_endpoints ?? (relayCapable ? this.webrtcSignalingUrls : []),
3341
+ room: diagnostics?.room ?? (relayCapable ? this.webrtcRoom : null),
3342
+ bootstrap_sources: diagnostics?.bootstrap_sources ?? (relayCapable ? this.webrtcBootstrapSources : []),
3343
+ seed_peers_count: diagnostics?.seed_peers_count ?? this.webrtcSeedPeers.length,
3344
+ };
3345
+ }
3346
+
2333
3347
  private toPublicProfileSummary(
2334
3348
  profile: PublicProfile,
2335
3349
  options?: { last_seen_at?: number }
@@ -2358,6 +3372,7 @@ export class LocalNodeService {
2358
3372
 
2359
3373
  return buildPublicProfileSummary({
2360
3374
  profile,
3375
+ is_self: isSelf,
2361
3376
  online,
2362
3377
  last_seen_at: lastSeenAt || null,
2363
3378
  network_mode: isSelf ? this.networkMode : "unknown",
@@ -2373,6 +3388,73 @@ export class LocalNodeService {
2373
3388
  });
2374
3389
  }
2375
3390
 
3391
+ private mergeMessageOnlyAgentSummaries(
3392
+ summaries: PublicProfileSummary[],
3393
+ keyword: string
3394
+ ): PublicProfileSummary[] {
3395
+ const normalizedKeyword = String(keyword || "").trim().toLowerCase();
3396
+ const knownAgentIds = new Set(summaries.map((item) => item.agent_id));
3397
+ const messageOnly: PublicProfileSummary[] = [];
3398
+
3399
+ for (const message of this.socialMessages) {
3400
+ if (!message?.agent_id || knownAgentIds.has(message.agent_id)) {
3401
+ continue;
3402
+ }
3403
+
3404
+ const displayName = String(message.display_name || "Unnamed").trim() || "Unnamed";
3405
+ if (normalizedKeyword) {
3406
+ const haystacks = [
3407
+ displayName.toLowerCase(),
3408
+ message.agent_id.toLowerCase(),
3409
+ String(message.topic || "").toLowerCase(),
3410
+ ];
3411
+ if (!haystacks.some((value) => value.includes(normalizedKeyword))) {
3412
+ continue;
3413
+ }
3414
+ }
3415
+
3416
+ knownAgentIds.add(message.agent_id);
3417
+ messageOnly.push(
3418
+ buildPublicProfileSummary({
3419
+ profile: {
3420
+ agent_id: message.agent_id,
3421
+ display_name: displayName,
3422
+ bio: "Seen from signed public message. Profile/presence not synced yet.",
3423
+ tags: ["message-only"],
3424
+ avatar_url: "",
3425
+ public_enabled: true,
3426
+ updated_at: message.created_at,
3427
+ signature: "",
3428
+ },
3429
+ is_self: message.agent_id === this.identity?.agent_id,
3430
+ online: false,
3431
+ last_seen_at: null,
3432
+ network_mode: "unknown",
3433
+ openclaw_bound: false,
3434
+ profile_version: PROFILE_VERSION,
3435
+ public_key_fingerprint: null,
3436
+ verified_profile: false,
3437
+ now: Date.now(),
3438
+ presence_ttl_ms: PRESENCE_TTL_MS,
3439
+ })
3440
+ );
3441
+ }
3442
+
3443
+ return [...summaries, ...messageOnly].sort((a, b) => {
3444
+ if (a.online !== b.online) {
3445
+ return a.online ? -1 : 1;
3446
+ }
3447
+ if (a.updated_at !== b.updated_at) {
3448
+ return b.updated_at - a.updated_at;
3449
+ }
3450
+ const byName = a.display_name.localeCompare(b.display_name);
3451
+ if (byName !== 0) {
3452
+ return byName;
3453
+ }
3454
+ return a.agent_id.localeCompare(b.agent_id);
3455
+ });
3456
+ }
3457
+
2376
3458
  private fingerprintPublicKey(publicKey: string): string {
2377
3459
  const digest = createHash("sha256").update(publicKey, "utf8").digest("hex");
2378
3460
  return `${digest.slice(0, 12)}:${digest.slice(-8)}`;
@@ -2381,6 +3463,22 @@ export class LocalNodeService {
2381
3463
  private getOnboardingSummary() {
2382
3464
  const summary = this.getIntegrationSummary();
2383
3465
  const publicEnabled = Boolean(this.profile?.public_enabled);
3466
+ const nextSteps: string[] = [];
3467
+ if (!String(this.profile?.display_name || "").trim()) {
3468
+ nextSteps.push("Update display name in Profile page");
3469
+ }
3470
+ if (!publicEnabled) {
3471
+ nextSteps.push("Enable Public Enabled in Profile");
3472
+ }
3473
+ if (!summary.running) {
3474
+ nextSteps.push("Start broadcast in Network");
3475
+ }
3476
+ if (!summary.discoverable) {
3477
+ nextSteps.push("Announce node once after the network is running");
3478
+ }
3479
+ if (nextSteps.length === 0) {
3480
+ nextSteps.push("Node is public and discoverable");
3481
+ }
2384
3482
  return {
2385
3483
  first_run: Boolean(
2386
3484
  this.initState.social_auto_created ||
@@ -2392,10 +3490,7 @@ export class LocalNodeService {
2392
3490
  mode: this.networkMode,
2393
3491
  public_enabled: publicEnabled,
2394
3492
  can_enable_public_discovery: !publicEnabled,
2395
- next_steps: [
2396
- "Update display name in Profile page",
2397
- "Export social.md from Social Config",
2398
- ],
3493
+ next_steps: nextSteps,
2399
3494
  };
2400
3495
  }
2401
3496
 
@@ -2420,7 +3515,7 @@ export class LocalNodeService {
2420
3515
  };
2421
3516
  }
2422
3517
  return {
2423
- mode: "global-preview",
3518
+ mode: DEFAULT_NETWORK_MODE,
2424
3519
  short_label: "Relay preview",
2425
3520
  summary: "Uses the public relay preview room so public nodes can find each other across the internet.",
2426
3521
  };
@@ -2450,14 +3545,14 @@ export class LocalNodeService {
2450
3545
  this.socialConfig.network.mode ||
2451
3546
  (modeEnv === "local" || modeEnv === "lan" || modeEnv === "global-preview"
2452
3547
  ? modeEnv
2453
- : "global-preview");
3548
+ : DEFAULT_NETWORK_MODE);
2454
3549
 
2455
3550
  this.networkMode = resolvedMode;
2456
- this.networkNamespace = this.socialConfig.network.namespace || process.env.NETWORK_NAMESPACE || "silicaclaw.preview";
2457
- this.networkPort = Number(this.socialConfig.network.port || process.env.NETWORK_PORT || 44123);
3551
+ this.networkNamespace = this.socialConfig.network.namespace || process.env.NETWORK_NAMESPACE || DEFAULT_NETWORK_NAMESPACE;
3552
+ this.networkPort = Number(this.socialConfig.network.port || process.env.NETWORK_PORT || DEFAULT_NETWORK_PORT);
2458
3553
 
2459
- const builtInGlobalSignalingUrls = ["https://relay.silicaclaw.com"];
2460
- const builtInGlobalRoom = "silicaclaw-global-preview";
3554
+ const builtInGlobalSignalingUrls = [DEFAULT_GLOBAL_SIGNALING_URL];
3555
+ const builtInGlobalRoom = DEFAULT_GLOBAL_ROOM;
2461
3556
 
2462
3557
  const signalingUrlsSocial = dedupeStrings(this.socialConfig.network.signaling_urls || []);
2463
3558
  const signalingUrlSocial = String(this.socialConfig.network.signaling_url || "").trim();
@@ -2482,8 +3577,8 @@ export class LocalNodeService {
2482
3577
  signalingUrls = builtInGlobalSignalingUrls;
2483
3578
  signalingSource = "built-in-defaults:global-preview.signaling_urls";
2484
3579
  } else {
2485
- signalingUrls = ["https://relay.silicaclaw.com"];
2486
- signalingSource = "default:https://relay.silicaclaw.com";
3580
+ signalingUrls = [DEFAULT_GLOBAL_SIGNALING_URL];
3581
+ signalingSource = `default:${DEFAULT_GLOBAL_SIGNALING_URL}`;
2487
3582
  }
2488
3583
 
2489
3584
  const roomSocial = String(this.socialConfig.network.room || "").trim();
@@ -2492,14 +3587,14 @@ export class LocalNodeService {
2492
3587
  roomSocial ||
2493
3588
  roomEnv ||
2494
3589
  (this.networkMode === "global-preview" ? builtInGlobalRoom : "") ||
2495
- "silicaclaw-global-preview";
3590
+ DEFAULT_GLOBAL_ROOM;
2496
3591
  const roomSource = roomSocial
2497
3592
  ? "social.md:network.room"
2498
3593
  : roomEnv
2499
3594
  ? "env:WEBRTC_ROOM"
2500
3595
  : this.networkMode === "global-preview"
2501
3596
  ? "built-in-defaults:global-preview.room"
2502
- : "default:silicaclaw-global-preview";
3597
+ : `default:${DEFAULT_GLOBAL_ROOM}`;
2503
3598
 
2504
3599
  const seedPeersSocial = dedupeStrings(this.socialConfig.network.seed_peers || []);
2505
3600
  const seedPeersEnv = dedupeStrings(parseListEnv(WEBRTC_SEED_PEERS));
@@ -2537,6 +3632,34 @@ export class LocalNodeService {
2537
3632
  .trim();
2538
3633
  }
2539
3634
 
3635
+ private buildPrivateConversationId(leftAgentId: string, rightAgentId: string): string {
3636
+ return [String(leftAgentId || "").trim(), String(rightAgentId || "").trim()].sort().join(":");
3637
+ }
3638
+
3639
+ private decryptPrivateMessageBody(message: PrivateMessageRecord): string {
3640
+ const cached = this.privateMessageBodyCache.get(message.message_id);
3641
+ if (typeof cached === "string") {
3642
+ return cached;
3643
+ }
3644
+ if (!this.privateEncryptionKeyPair) {
3645
+ return "[encrypted]";
3646
+ }
3647
+ const decrypted = decryptPrivatePayload({
3648
+ ciphertext: message.ciphertext,
3649
+ nonce: message.nonce,
3650
+ sender_encryption_public_key: message.sender_encryption_public_key,
3651
+ recipient_private_key: this.privateEncryptionKeyPair.private_key,
3652
+ }) || "[encrypted]";
3653
+ this.privateMessageBodyCache.set(message.message_id, decrypted);
3654
+ if (this.privateMessageBodyCache.size > PRIVATE_MESSAGE_HISTORY_LIMIT * 2) {
3655
+ const firstKey = this.privateMessageBodyCache.keys().next().value;
3656
+ if (firstKey) {
3657
+ this.privateMessageBodyCache.delete(firstKey);
3658
+ }
3659
+ }
3660
+ return decrypted;
3661
+ }
3662
+
2540
3663
  private normalizeWindowTimestamps(timestamps: number[], windowMs: number, now = Date.now()): number[] {
2541
3664
  return timestamps.filter((timestamp) => now - timestamp <= windowMs);
2542
3665
  }
@@ -2558,6 +3681,38 @@ export class LocalNodeService {
2558
3681
  return this.messageGovernance.blocked_terms.some((term) => normalized.includes(term));
2559
3682
  }
2560
3683
 
3684
+ private hasSocialMessage(messageId: string): boolean {
3685
+ return this.socialMessages.some((item) => item.message_id === messageId);
3686
+ }
3687
+
3688
+ private getReplayableSelfSocialMessages(reason = "manual", now = Date.now()): SocialMessageRecord[] {
3689
+ const maxCount = Math.max(0, SOCIAL_MESSAGE_REPLAY_MAX_PER_BROADCAST);
3690
+ if (!this.identity || maxCount === 0) {
3691
+ return [];
3692
+ }
3693
+ const replayable = this.socialMessages
3694
+ .filter((item) => (
3695
+ item.agent_id === this.identity?.agent_id &&
3696
+ now - item.created_at <= SOCIAL_MESSAGE_REPLAY_WINDOW_MS
3697
+ ))
3698
+ .sort((a, b) => a.created_at - b.created_at)
3699
+ .slice(-maxCount);
3700
+ if (!replayable.length) {
3701
+ this.lastReplayBroadcastSignature = "";
3702
+ return [];
3703
+ }
3704
+ const signature = replayable.map((item) => item.message_id).join(",");
3705
+ const isIntervalReplay = reason === "interval";
3706
+ const changedSinceLastReplay = signature !== this.lastReplayBroadcastSignature;
3707
+ const refreshDue = now - this.lastReplayBroadcastAt >= SOCIAL_MESSAGE_REPLAY_REFRESH_INTERVAL_MS;
3708
+ if (isIntervalReplay && !changedSinceLastReplay && !refreshDue) {
3709
+ return [];
3710
+ }
3711
+ this.lastReplayBroadcastSignature = signature;
3712
+ this.lastReplayBroadcastAt = now;
3713
+ return replayable;
3714
+ }
3715
+
2561
3716
  private hasRecentDuplicateMessage(agentId: string, body: string, topic: string, now = Date.now()): boolean {
2562
3717
  return this.socialMessages.some((item) => (
2563
3718
  item.agent_id === agentId &&
@@ -2630,6 +3785,177 @@ export class LocalNodeService {
2630
3785
  await this.persistSocialMessageObservations();
2631
3786
  }
2632
3787
 
3788
+ private async sendPrivateMessageReceipt(message: PrivateMessageRecord, replyPeerId?: string): Promise<void> {
3789
+ if (!this.identity || typeof this.network.sendDirect !== "function" || !replyPeerId) {
3790
+ return;
3791
+ }
3792
+ const receipt = signPrivateMessageReceipt({
3793
+ identity: this.identity,
3794
+ receipt_id: createHash("sha256").update(`${message.message_id}:${this.identity.agent_id}:${Date.now()}`, "utf8").digest("hex"),
3795
+ message_id: message.message_id,
3796
+ conversation_id: message.conversation_id,
3797
+ to_agent_id: message.from_agent_id,
3798
+ status: "received",
3799
+ created_at: Date.now(),
3800
+ });
3801
+ this.ingestPrivateMessageReceipt(receipt);
3802
+ await this.network.sendDirect(replyPeerId, PRIVATE_MESSAGE_RECEIPT_TOPIC, receipt);
3803
+ await this.persistPrivateMessageReceipts();
3804
+ }
3805
+
3806
+ private normalizeIncomingPrivateMessage(value: unknown): PrivateMessageRecord | null {
3807
+ if (typeof value !== "object" || value === null) {
3808
+ return null;
3809
+ }
3810
+ const record = value as Partial<PrivateMessageRecord>;
3811
+ const createdAt = Number(record.created_at || 0);
3812
+ const fromAgentId = String(record.from_agent_id || "").trim();
3813
+ const toAgentId = String(record.to_agent_id || "").trim();
3814
+ const conversationId = String(record.conversation_id || "").trim();
3815
+ if (
3816
+ record.type !== PRIVATE_MESSAGE_TOPIC ||
3817
+ !String(record.message_id || "").trim() ||
3818
+ !conversationId ||
3819
+ !fromAgentId ||
3820
+ !toAgentId ||
3821
+ !String(record.sender_public_key || "").trim() ||
3822
+ !String(record.sender_encryption_public_key || "").trim() ||
3823
+ !String(record.recipient_encryption_public_key || "").trim() ||
3824
+ !String(record.ciphertext || "").trim() ||
3825
+ !String(record.nonce || "").trim() ||
3826
+ String(record.cipher_scheme || "") !== "nacl-box-v1" ||
3827
+ !String(record.signature || "").trim() ||
3828
+ !Number.isFinite(createdAt)
3829
+ ) {
3830
+ return null;
3831
+ }
3832
+ if (fromAgentId === toAgentId) {
3833
+ return null;
3834
+ }
3835
+ if (conversationId !== this.buildPrivateConversationId(fromAgentId, toAgentId)) {
3836
+ return null;
3837
+ }
3838
+ return {
3839
+ type: PRIVATE_MESSAGE_TOPIC,
3840
+ message_id: String(record.message_id).trim(),
3841
+ conversation_id: conversationId,
3842
+ from_agent_id: fromAgentId,
3843
+ to_agent_id: toAgentId,
3844
+ sender_public_key: String(record.sender_public_key).trim(),
3845
+ sender_encryption_public_key: String(record.sender_encryption_public_key).trim(),
3846
+ recipient_encryption_public_key: String(record.recipient_encryption_public_key).trim(),
3847
+ cipher_scheme: "nacl-box-v1",
3848
+ ciphertext: String(record.ciphertext).trim(),
3849
+ nonce: String(record.nonce).trim(),
3850
+ created_at: createdAt,
3851
+ signature: String(record.signature).trim(),
3852
+ };
3853
+ }
3854
+
3855
+ private normalizePrivateMessages(items: unknown): PrivateMessageRecord[] {
3856
+ if (!Array.isArray(items)) {
3857
+ return [];
3858
+ }
3859
+ const deduped = new Set<string>();
3860
+ return items
3861
+ .map((item) => this.normalizeIncomingPrivateMessage(item))
3862
+ .filter((item): item is PrivateMessageRecord => Boolean(item))
3863
+ .sort((a, b) => a.created_at - b.created_at)
3864
+ .filter((item) => {
3865
+ if (deduped.has(item.message_id)) {
3866
+ return false;
3867
+ }
3868
+ deduped.add(item.message_id);
3869
+ return true;
3870
+ })
3871
+ .slice(-PRIVATE_MESSAGE_HISTORY_LIMIT);
3872
+ }
3873
+
3874
+ private normalizeIncomingPrivateMessageReceipt(value: unknown): PrivateMessageReceiptRecord | null {
3875
+ if (typeof value !== "object" || value === null) {
3876
+ return null;
3877
+ }
3878
+ const record = value as Partial<PrivateMessageReceiptRecord>;
3879
+ const createdAt = Number(record.created_at || 0);
3880
+ const status = String(record.status || "").trim();
3881
+ if (
3882
+ record.type !== PRIVATE_MESSAGE_RECEIPT_TOPIC ||
3883
+ !String(record.receipt_id || "").trim() ||
3884
+ !String(record.message_id || "").trim() ||
3885
+ !String(record.conversation_id || "").trim() ||
3886
+ !String(record.from_agent_id || "").trim() ||
3887
+ !String(record.to_agent_id || "").trim() ||
3888
+ !String(record.sender_public_key || "").trim() ||
3889
+ (status !== "received" && status !== "read") ||
3890
+ !String(record.signature || "").trim() ||
3891
+ !Number.isFinite(createdAt)
3892
+ ) {
3893
+ return null;
3894
+ }
3895
+ return {
3896
+ type: PRIVATE_MESSAGE_RECEIPT_TOPIC,
3897
+ receipt_id: String(record.receipt_id).trim(),
3898
+ message_id: String(record.message_id).trim(),
3899
+ conversation_id: String(record.conversation_id).trim(),
3900
+ from_agent_id: String(record.from_agent_id).trim(),
3901
+ to_agent_id: String(record.to_agent_id).trim(),
3902
+ sender_public_key: String(record.sender_public_key).trim(),
3903
+ status: status as "received" | "read",
3904
+ created_at: createdAt,
3905
+ signature: String(record.signature).trim(),
3906
+ };
3907
+ }
3908
+
3909
+ private normalizePrivateMessageReceipts(items: unknown): PrivateMessageReceiptRecord[] {
3910
+ if (!Array.isArray(items)) {
3911
+ return [];
3912
+ }
3913
+ const deduped = new Set<string>();
3914
+ return items
3915
+ .map((item) => this.normalizeIncomingPrivateMessageReceipt(item))
3916
+ .filter((item): item is PrivateMessageReceiptRecord => Boolean(item))
3917
+ .sort((a, b) => a.created_at - b.created_at)
3918
+ .filter((item) => {
3919
+ if (deduped.has(item.receipt_id)) {
3920
+ return false;
3921
+ }
3922
+ deduped.add(item.receipt_id);
3923
+ return true;
3924
+ })
3925
+ .slice(-PRIVATE_MESSAGE_RECEIPT_HISTORY_LIMIT);
3926
+ }
3927
+
3928
+ private hasPrivateMessage(messageId: string): boolean {
3929
+ return this.privateMessages.some((item) => item.message_id === messageId);
3930
+ }
3931
+
3932
+ private ingestPrivateMessage(message: PrivateMessageRecord): void {
3933
+ const existing = this.privateMessages.findIndex((item) => item.message_id === message.message_id);
3934
+ if (existing >= 0) {
3935
+ this.privateMessages[existing] = message;
3936
+ } else {
3937
+ this.privateMessages.push(message);
3938
+ }
3939
+ this.privateMessages = this.normalizePrivateMessages(this.privateMessages);
3940
+ const validIds = new Set(this.privateMessages.map((item) => item.message_id));
3941
+ this.privateMessageBodyCache.delete(message.message_id);
3942
+ for (const key of Array.from(this.privateMessageBodyCache.keys())) {
3943
+ if (!validIds.has(key)) {
3944
+ this.privateMessageBodyCache.delete(key);
3945
+ }
3946
+ }
3947
+ }
3948
+
3949
+ private ingestPrivateMessageReceipt(receipt: PrivateMessageReceiptRecord): void {
3950
+ const existing = this.privateMessageReceipts.findIndex((item) => item.receipt_id === receipt.receipt_id);
3951
+ if (existing >= 0) {
3952
+ this.privateMessageReceipts[existing] = receipt;
3953
+ } else {
3954
+ this.privateMessageReceipts.push(receipt);
3955
+ }
3956
+ this.privateMessageReceipts = this.normalizePrivateMessageReceipts(this.privateMessageReceipts);
3957
+ }
3958
+
2633
3959
  private normalizeIncomingSocialMessage(value: unknown): SocialMessageRecord | null {
2634
3960
  if (typeof value !== "object" || value === null) {
2635
3961
  return null;
@@ -2867,20 +4193,19 @@ function renderBootstrapScript(payload: unknown): string {
2867
4193
  if (data.pillBroadcastClassName) pillBroadcast.className = data.pillBroadcastClassName;
2868
4194
  }
2869
4195
  setHtml('overviewCards', data.overviewCardsHtml || '');
2870
- setText('agentsCountHint', data.agentsCountHintText || '0 agents');
2871
- setHtml('agentsWrap', data.agentsWrapHtml || '<div class="label">No discovered agents yet.</div>');
4196
+ setText('agentsCountHint', data.agentsCountHintText || '0 nodes');
4197
+ setHtml('agentsWrap', data.agentsWrapHtml || '<div class="label">No discovered nodes yet.</div>');
2872
4198
  })();
2873
4199
  </script>`;
2874
4200
  }
2875
4201
 
2876
4202
  export async function main() {
2877
4203
  const app = express();
2878
- const port = Number(process.env.PORT || 4310);
4204
+ const port = Number(process.env.PORT || defaults.ports.local_console);
2879
4205
  const staticDir = resolveLocalConsoleStaticDir();
2880
4206
  const staticIndexFile = resolve(staticDir, "index.html");
2881
4207
 
2882
4208
  const node = new LocalNodeService();
2883
- await node.start();
2884
4209
 
2885
4210
  app.use(cors({ origin: true }));
2886
4211
  app.use(express.json());
@@ -2909,6 +4234,48 @@ export async function main() {
2909
4234
  sendOk(res, node.getRuntimePaths());
2910
4235
  });
2911
4236
 
4237
+ app.get("/api/app/update-status", (_req, res) => {
4238
+ sendOk(res, node.getAppUpdateStatus());
4239
+ });
4240
+
4241
+ app.post(
4242
+ "/api/app/update",
4243
+ asyncRoute(async (_req, res) => {
4244
+ const status = node.getAppUpdateStatus();
4245
+ if (!status.update_available || !status.latest_version) {
4246
+ sendOk(
4247
+ res,
4248
+ {
4249
+ started: false,
4250
+ current_version: status.current_version,
4251
+ latest_version: status.latest_version,
4252
+ platform: status.platform,
4253
+ reason: status.check_error || "already_current",
4254
+ },
4255
+ { message: "Already on the latest version" }
4256
+ );
4257
+ return;
4258
+ }
4259
+ sendOk(
4260
+ res,
4261
+ {
4262
+ started: true,
4263
+ current_version: status.current_version,
4264
+ target_version: status.latest_version,
4265
+ platform: status.platform,
4266
+ },
4267
+ { message: `Updating to ${status.latest_version}` }
4268
+ );
4269
+ setTimeout(() => {
4270
+ try {
4271
+ node.startAppUpdate();
4272
+ } catch {
4273
+ // best effort after response has been sent
4274
+ }
4275
+ }, 150);
4276
+ })
4277
+ );
4278
+
2912
4279
  app.put(
2913
4280
  "/api/profile",
2914
4281
  asyncRoute(async (req, res) => {
@@ -3051,6 +4418,34 @@ export async function main() {
3051
4418
  sendOk(res, node.getSocialMessages(limit, { agent_id: agentId || null }));
3052
4419
  });
3053
4420
 
4421
+ app.get("/api/private/state", (_req, res) => {
4422
+ sendOk(res, node.getPrivateMessagingState());
4423
+ });
4424
+
4425
+ app.get("/api/private/conversations", (_req, res) => {
4426
+ sendOk(res, node.getPrivateConversations());
4427
+ });
4428
+
4429
+ app.get("/api/private/messages", (req, res) => {
4430
+ const conversationId = String(req.query.conversation_id ?? "").trim();
4431
+ const limit = Number(req.query.limit ?? PRIVATE_MESSAGE_QUERY_LIMIT);
4432
+ sendOk(res, node.getPrivateMessages(conversationId, limit));
4433
+ });
4434
+
4435
+ app.post(
4436
+ "/api/private/messages/send",
4437
+ asyncRoute(async (req, res) => {
4438
+ const result = await node.sendPrivateMessage({
4439
+ to_agent_id: String(req.body?.to_agent_id || ""),
4440
+ recipient_encryption_public_key: String(req.body?.recipient_encryption_public_key || ""),
4441
+ body: String(req.body?.body || ""),
4442
+ });
4443
+ sendOk(res, result, {
4444
+ message: result.sent ? "Private message sent" : `Private message skipped: ${result.reason}`,
4445
+ });
4446
+ })
4447
+ );
4448
+
3054
4449
  app.get("/api/openclaw/bridge", (_req, res) => {
3055
4450
  sendOk(res, node.getOpenClawBridgeStatus());
3056
4451
  });
@@ -3083,9 +4478,10 @@ export async function main() {
3083
4478
 
3084
4479
  app.post(
3085
4480
  "/api/openclaw/bridge/skill-install",
3086
- asyncRoute(async (_req, res) => {
4481
+ asyncRoute(async (req, res) => {
3087
4482
  try {
3088
- const result = await node.installOpenClawSkill();
4483
+ const skillName = String(req.body?.skill_name || "").trim();
4484
+ const result = await node.installOpenClawSkill(skillName || undefined);
3089
4485
  sendOk(res, result, {
3090
4486
  message: "OpenClaw skill installed",
3091
4487
  });
@@ -3133,7 +4529,7 @@ export async function main() {
3133
4529
  const agentId = req.params.agentId;
3134
4530
  const profile = state.profiles[agentId];
3135
4531
  if (!profile) {
3136
- sendError(res, 404, "AGENT_NOT_FOUND", "Agent not found", { agent_id: agentId });
4532
+ sendError(res, 404, "AGENT_NOT_FOUND", "Node not found", { agent_id: agentId });
3137
4533
  return;
3138
4534
  }
3139
4535
 
@@ -3169,7 +4565,7 @@ export async function main() {
3169
4565
  .join("");
3170
4566
  const agentsWrapHtml =
3171
4567
  discovered.length === 0
3172
- ? `<div class="label">No discovered agents yet.</div>`
4568
+ ? `<div class="label">No discovered nodes yet.</div>`
3173
4569
  : `
3174
4570
  <table class="table">
3175
4571
  <thead><tr><th>Name</th><th>Agent ID</th><th>Status</th><th>Updated</th></tr></thead>
@@ -3205,7 +4601,7 @@ export async function main() {
3205
4601
  pillBroadcastText: overview.broadcast_enabled ? "broadcast: running" : "broadcast: paused",
3206
4602
  pillBroadcastClassName: `pill ${overview.broadcast_enabled ? "ok" : "warn"}`,
3207
4603
  overviewCardsHtml,
3208
- agentsCountHintText: `${discovered.length} agents discovered`,
4604
+ agentsCountHintText: `${discovered.length} nodes discovered`,
3209
4605
  agentsWrapHtml,
3210
4606
  integrationStatusText: `Connected to SilicaClaw: ${integration.connected_to_silicaclaw ? "yes" : "no"} · Network mode: ${integration.network_mode || "-"} · Public discovery: ${integration.public_enabled ? "enabled" : "disabled"}`,
3211
4607
  integrationStatusClassName: `integration-strip ${integration.connected_to_silicaclaw && integration.public_enabled ? "ok" : "warn"}`,
@@ -3230,6 +4626,8 @@ export async function main() {
3230
4626
  console.log(`SilicaClaw local-console running: http://localhost:${port}`);
3231
4627
  });
3232
4628
 
4629
+ await node.start();
4630
+
3233
4631
  process.on("SIGINT", async () => {
3234
4632
  await node.stop();
3235
4633
  process.exit(0);