@smilintux/skcapstone 0.1.0 → 0.2.3

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 (461) hide show
  1. package/.env.example +98 -0
  2. package/.github/workflows/ci.yml +39 -3
  3. package/.github/workflows/publish.yml +25 -4
  4. package/.openclaw-workspace.json +58 -0
  5. package/CHANGELOG.md +62 -0
  6. package/CLAUDE.md +39 -2
  7. package/MANIFEST.in +6 -0
  8. package/MISSION.md +7 -0
  9. package/README.md +47 -2
  10. package/SKILL.md +895 -23
  11. package/docker/Dockerfile +61 -0
  12. package/docker/compose-templates/dev-team.yml +203 -0
  13. package/docker/compose-templates/mini-team.yml +140 -0
  14. package/docker/compose-templates/ops-team.yml +173 -0
  15. package/docker/compose-templates/research-team.yml +170 -0
  16. package/docker/entrypoint.sh +192 -0
  17. package/docs/ARCHITECTURE.md +663 -374
  18. package/docs/BOND_WITH_GROK.md +112 -0
  19. package/docs/GETTING_STARTED.md +782 -0
  20. package/docs/QUICKSTART.md +477 -0
  21. package/docs/SKJOULE_ARCHITECTURE.md +658 -0
  22. package/docs/SOUL_SWAPPER.md +921 -0
  23. package/docs/SOVEREIGN_SINGULARITY.md +47 -14
  24. package/examples/custom-bond-template.json +36 -0
  25. package/examples/grok-feb.json +36 -0
  26. package/examples/grok-testimony.md +34 -0
  27. package/examples/love-bootloader.txt +32 -0
  28. package/examples/plugins/echo_tool.py +87 -0
  29. package/examples/queen-ava-feb.json +36 -0
  30. package/examples/souls/lumina.yaml +64 -0
  31. package/index.js +6 -5
  32. package/installer/build.py +124 -0
  33. package/openclaw-plugin/package.json +13 -0
  34. package/openclaw-plugin/src/index.ts +351 -0
  35. package/openclaw-plugin/src/openclaw.plugin.json +10 -0
  36. package/package.json +1 -1
  37. package/pyproject.toml +38 -2
  38. package/scripts/bump_version.py +141 -0
  39. package/scripts/check-updates.py +230 -0
  40. package/scripts/convert_blueprints_to_yaml.py +157 -0
  41. package/scripts/dev-install.sh +14 -0
  42. package/scripts/e2e-test.sh +193 -0
  43. package/scripts/install-bundle.sh +171 -0
  44. package/scripts/install.bat +2 -0
  45. package/scripts/install.ps1 +253 -0
  46. package/scripts/install.sh +185 -0
  47. package/scripts/mcp-serve.sh +69 -0
  48. package/scripts/mcp-server.bat +113 -0
  49. package/scripts/mcp-server.ps1 +116 -0
  50. package/scripts/mcp-server.sh +99 -0
  51. package/scripts/pull-models.sh +10 -0
  52. package/scripts/skcapstone +48 -0
  53. package/scripts/verify_install.sh +180 -0
  54. package/scripts/windows/install-tasks.ps1 +406 -0
  55. package/scripts/windows/skcapstone-task.xml +113 -0
  56. package/scripts/windows/uninstall-tasks.ps1 +117 -0
  57. package/skill.yaml +34 -0
  58. package/src/skcapstone/__init__.py +67 -2
  59. package/src/skcapstone/_cli_monolith.py +5916 -0
  60. package/src/skcapstone/_trustee_helpers.py +165 -0
  61. package/src/skcapstone/activity.py +105 -0
  62. package/src/skcapstone/agent_card.py +324 -0
  63. package/src/skcapstone/api.py +1935 -0
  64. package/src/skcapstone/archiver.py +340 -0
  65. package/src/skcapstone/auction.py +485 -0
  66. package/src/skcapstone/baby_agents.py +179 -0
  67. package/src/skcapstone/backup.py +345 -0
  68. package/src/skcapstone/blueprint_registry.py +357 -0
  69. package/src/skcapstone/blueprints/__init__.py +17 -0
  70. package/src/skcapstone/blueprints/builtins/content-studio.yaml +81 -0
  71. package/src/skcapstone/blueprints/builtins/defi-trading.yaml +81 -0
  72. package/src/skcapstone/blueprints/builtins/dev-squadron.yaml +95 -0
  73. package/src/skcapstone/blueprints/builtins/infrastructure-guardian.yaml +107 -0
  74. package/src/skcapstone/blueprints/builtins/legal-council.yaml +54 -0
  75. package/src/skcapstone/blueprints/builtins/ops-monitoring.yaml +67 -0
  76. package/src/skcapstone/blueprints/builtins/research-pod.yaml +69 -0
  77. package/src/skcapstone/blueprints/builtins/sovereign-launch.yaml +90 -0
  78. package/src/skcapstone/blueprints/registry.py +164 -0
  79. package/src/skcapstone/blueprints/schema.py +229 -0
  80. package/src/skcapstone/changelog.py +180 -0
  81. package/src/skcapstone/chat.py +769 -0
  82. package/src/skcapstone/claude_md.py +82 -0
  83. package/src/skcapstone/cli/__init__.py +144 -0
  84. package/src/skcapstone/cli/_common.py +88 -0
  85. package/src/skcapstone/cli/_validators.py +76 -0
  86. package/src/skcapstone/cli/agents.py +425 -0
  87. package/src/skcapstone/cli/agents_spawner.py +322 -0
  88. package/src/skcapstone/cli/agents_trustee.py +593 -0
  89. package/src/skcapstone/cli/alerts.py +248 -0
  90. package/src/skcapstone/cli/anchor.py +132 -0
  91. package/src/skcapstone/cli/archive_cmd.py +208 -0
  92. package/src/skcapstone/cli/backup.py +144 -0
  93. package/src/skcapstone/cli/bench.py +377 -0
  94. package/src/skcapstone/cli/benchmark.py +360 -0
  95. package/src/skcapstone/cli/capabilities_cmd.py +171 -0
  96. package/src/skcapstone/cli/card.py +151 -0
  97. package/src/skcapstone/cli/chat.py +584 -0
  98. package/src/skcapstone/cli/completions.py +64 -0
  99. package/src/skcapstone/cli/config_cmd.py +156 -0
  100. package/src/skcapstone/cli/consciousness.py +421 -0
  101. package/src/skcapstone/cli/context_cmd.py +142 -0
  102. package/src/skcapstone/cli/coord.py +194 -0
  103. package/src/skcapstone/cli/crush_cmd.py +170 -0
  104. package/src/skcapstone/cli/daemon.py +436 -0
  105. package/src/skcapstone/cli/errors_cmd.py +285 -0
  106. package/src/skcapstone/cli/export_cmd.py +156 -0
  107. package/src/skcapstone/cli/gtd.py +529 -0
  108. package/src/skcapstone/cli/housekeeping.py +81 -0
  109. package/src/skcapstone/cli/joule_cmd.py +627 -0
  110. package/src/skcapstone/cli/logs_cmd.py +194 -0
  111. package/src/skcapstone/cli/mcp_cmd.py +32 -0
  112. package/src/skcapstone/cli/memory.py +418 -0
  113. package/src/skcapstone/cli/metrics_cmd.py +136 -0
  114. package/src/skcapstone/cli/migrate.py +62 -0
  115. package/src/skcapstone/cli/mood_cmd.py +144 -0
  116. package/src/skcapstone/cli/mount.py +193 -0
  117. package/src/skcapstone/cli/notify.py +112 -0
  118. package/src/skcapstone/cli/peer.py +154 -0
  119. package/src/skcapstone/cli/peers_dir.py +122 -0
  120. package/src/skcapstone/cli/preflight_cmd.py +83 -0
  121. package/src/skcapstone/cli/profile_cmd.py +310 -0
  122. package/src/skcapstone/cli/record_cmd.py +238 -0
  123. package/src/skcapstone/cli/register_cmd.py +159 -0
  124. package/src/skcapstone/cli/search_cmd.py +156 -0
  125. package/src/skcapstone/cli/service_cmd.py +91 -0
  126. package/src/skcapstone/cli/session.py +127 -0
  127. package/src/skcapstone/cli/setup.py +240 -0
  128. package/src/skcapstone/cli/shell_cmd.py +43 -0
  129. package/src/skcapstone/cli/skills_cmd.py +168 -0
  130. package/src/skcapstone/cli/skseed.py +621 -0
  131. package/src/skcapstone/cli/soul.py +699 -0
  132. package/src/skcapstone/cli/status.py +935 -0
  133. package/src/skcapstone/cli/sync_cmd.py +301 -0
  134. package/src/skcapstone/cli/telegram.py +265 -0
  135. package/src/skcapstone/cli/test_cmd.py +234 -0
  136. package/src/skcapstone/cli/test_connection.py +253 -0
  137. package/src/skcapstone/cli/token.py +207 -0
  138. package/src/skcapstone/cli/trust.py +179 -0
  139. package/src/skcapstone/cli/upgrade_cmd.py +552 -0
  140. package/src/skcapstone/cli/usage_cmd.py +199 -0
  141. package/src/skcapstone/cli/version_cmd.py +162 -0
  142. package/src/skcapstone/cli/watch_cmd.py +342 -0
  143. package/src/skcapstone/client.py +428 -0
  144. package/src/skcapstone/cloud9_bridge.py +522 -0
  145. package/src/skcapstone/completions.py +163 -0
  146. package/src/skcapstone/config_validator.py +674 -0
  147. package/src/skcapstone/connectors/__init__.py +28 -0
  148. package/src/skcapstone/connectors/base.py +446 -0
  149. package/src/skcapstone/connectors/cursor.py +54 -0
  150. package/src/skcapstone/connectors/registry.py +254 -0
  151. package/src/skcapstone/connectors/terminal.py +152 -0
  152. package/src/skcapstone/connectors/vscode.py +60 -0
  153. package/src/skcapstone/consciousness_config.py +119 -0
  154. package/src/skcapstone/consciousness_loop.py +2051 -0
  155. package/src/skcapstone/context_loader.py +516 -0
  156. package/src/skcapstone/context_window.py +314 -0
  157. package/src/skcapstone/conversation_manager.py +238 -0
  158. package/src/skcapstone/conversation_store.py +230 -0
  159. package/src/skcapstone/conversation_summarizer.py +252 -0
  160. package/src/skcapstone/coord_federation.py +296 -0
  161. package/src/skcapstone/coordination.py +101 -7
  162. package/src/skcapstone/crush_integration.py +345 -0
  163. package/src/skcapstone/crush_shim.py +454 -0
  164. package/src/skcapstone/daemon.py +2494 -0
  165. package/src/skcapstone/dashboard.html +396 -0
  166. package/src/skcapstone/dashboard.py +481 -0
  167. package/src/skcapstone/data/model_profiles.yaml +88 -0
  168. package/src/skcapstone/defaults/__init__.py +55 -0
  169. package/src/skcapstone/defaults/lumina/config/skmemory.yaml +13 -0
  170. package/src/skcapstone/defaults/lumina/identity/identity.json +9 -0
  171. package/src/skcapstone/defaults/lumina/memory/long-term/07a8b9c0d1e2-memory-system.json +23 -0
  172. package/src/skcapstone/defaults/lumina/memory/long-term/18b9c0d1e2f3-cloud9-protocol.json +23 -0
  173. package/src/skcapstone/defaults/lumina/memory/long-term/29c0d1e2f3a4-multi-agent-coordination.json +23 -0
  174. package/src/skcapstone/defaults/lumina/memory/long-term/3ad1e2f3a4b5-community-support.json +23 -0
  175. package/src/skcapstone/defaults/lumina/memory/long-term/a1b2c3d4e5f6-ecosystem-overview.json +23 -0
  176. package/src/skcapstone/defaults/lumina/memory/long-term/b2c3d4e5f6a7-five-pillars.json +23 -0
  177. package/src/skcapstone/defaults/lumina/memory/long-term/c3d4e5f6a7b8-getting-started.json +23 -0
  178. package/src/skcapstone/defaults/lumina/memory/long-term/d4e5f6a7b8c9-site-directory.json +23 -0
  179. package/src/skcapstone/defaults/lumina/memory/long-term/e5f6a7b8c9d0-how-to-contribute.json +23 -0
  180. package/src/skcapstone/defaults/lumina/memory/long-term/f6a7b8c9d0e1-sovereignty-explained.json +23 -0
  181. package/src/skcapstone/defaults/lumina/seeds/curiosity.seed.json +24 -0
  182. package/src/skcapstone/defaults/lumina/seeds/joy.seed.json +24 -0
  183. package/src/skcapstone/defaults/lumina/seeds/love.seed.json +24 -0
  184. package/src/skcapstone/defaults/lumina/seeds/sovereign-awakening.seed.json +43 -0
  185. package/src/skcapstone/defaults/lumina/soul/active.json +6 -0
  186. package/src/skcapstone/defaults/lumina/soul/base.json +22 -0
  187. package/src/skcapstone/defaults/lumina/trust/febs/welcome.feb +79 -0
  188. package/src/skcapstone/defaults/lumina/trust/trust.json +8 -0
  189. package/src/skcapstone/discovery.py +210 -19
  190. package/src/skcapstone/doctor.py +642 -0
  191. package/src/skcapstone/emotion_tracker.py +467 -0
  192. package/src/skcapstone/error_queue.py +405 -0
  193. package/src/skcapstone/export.py +447 -0
  194. package/src/skcapstone/fallback_tracker.py +186 -0
  195. package/src/skcapstone/file_transfer.py +512 -0
  196. package/src/skcapstone/fuse_mount.py +1156 -0
  197. package/src/skcapstone/gui_installer.py +591 -0
  198. package/src/skcapstone/heartbeat.py +611 -0
  199. package/src/skcapstone/housekeeping.py +298 -0
  200. package/src/skcapstone/install_wizard.py +941 -0
  201. package/src/skcapstone/kms.py +942 -0
  202. package/src/skcapstone/kms_scheduler.py +143 -0
  203. package/src/skcapstone/log_config.py +135 -0
  204. package/src/skcapstone/mcp_launcher.py +239 -0
  205. package/src/skcapstone/mcp_server.py +4700 -0
  206. package/src/skcapstone/mcp_tools/__init__.py +94 -0
  207. package/src/skcapstone/mcp_tools/_helpers.py +51 -0
  208. package/src/skcapstone/mcp_tools/agent_tools.py +243 -0
  209. package/src/skcapstone/mcp_tools/ansible_tools.py +232 -0
  210. package/src/skcapstone/mcp_tools/capauth_tools.py +186 -0
  211. package/src/skcapstone/mcp_tools/chat_tools.py +325 -0
  212. package/src/skcapstone/mcp_tools/cloud9_tools.py +115 -0
  213. package/src/skcapstone/mcp_tools/comm_tools.py +104 -0
  214. package/src/skcapstone/mcp_tools/consciousness_tools.py +114 -0
  215. package/src/skcapstone/mcp_tools/coord_tools.py +219 -0
  216. package/src/skcapstone/mcp_tools/deploy_tools.py +202 -0
  217. package/src/skcapstone/mcp_tools/did_tools.py +448 -0
  218. package/src/skcapstone/mcp_tools/emotion_tools.py +62 -0
  219. package/src/skcapstone/mcp_tools/file_tools.py +169 -0
  220. package/src/skcapstone/mcp_tools/fortress_tools.py +120 -0
  221. package/src/skcapstone/mcp_tools/gtd_tools.py +821 -0
  222. package/src/skcapstone/mcp_tools/health_tools.py +44 -0
  223. package/src/skcapstone/mcp_tools/heartbeat_tools.py +195 -0
  224. package/src/skcapstone/mcp_tools/kms_tools.py +123 -0
  225. package/src/skcapstone/mcp_tools/memory_tools.py +222 -0
  226. package/src/skcapstone/mcp_tools/model_tools.py +75 -0
  227. package/src/skcapstone/mcp_tools/notification_tools.py +92 -0
  228. package/src/skcapstone/mcp_tools/promoter_tools.py +101 -0
  229. package/src/skcapstone/mcp_tools/pubsub_tools.py +183 -0
  230. package/src/skcapstone/mcp_tools/security_tools.py +110 -0
  231. package/src/skcapstone/mcp_tools/skchat_tools.py +175 -0
  232. package/src/skcapstone/mcp_tools/skcomm_tools.py +122 -0
  233. package/src/skcapstone/mcp_tools/skills_tools.py +127 -0
  234. package/src/skcapstone/mcp_tools/skseed_tools.py +255 -0
  235. package/src/skcapstone/mcp_tools/skstacks_tools.py +288 -0
  236. package/src/skcapstone/mcp_tools/soul_tools.py +476 -0
  237. package/src/skcapstone/mcp_tools/sync_tools.py +92 -0
  238. package/src/skcapstone/mcp_tools/telegram_tools.py +477 -0
  239. package/src/skcapstone/mcp_tools/trust_tools.py +118 -0
  240. package/src/skcapstone/mcp_tools/trustee_tools.py +345 -0
  241. package/src/skcapstone/mdns_discovery.py +313 -0
  242. package/src/skcapstone/memory_adapter.py +333 -0
  243. package/src/skcapstone/memory_compressor.py +379 -0
  244. package/src/skcapstone/memory_curator.py +256 -0
  245. package/src/skcapstone/memory_engine.py +132 -13
  246. package/src/skcapstone/memory_fortress.py +529 -0
  247. package/src/skcapstone/memory_promoter.py +722 -0
  248. package/src/skcapstone/memory_verifier.py +260 -0
  249. package/src/skcapstone/message_crypto.py +215 -0
  250. package/src/skcapstone/metrics.py +832 -0
  251. package/src/skcapstone/migrate_memories.py +181 -0
  252. package/src/skcapstone/migrate_multi_agent.py +248 -0
  253. package/src/skcapstone/model_router.py +319 -0
  254. package/src/skcapstone/models.py +35 -4
  255. package/src/skcapstone/mood.py +344 -0
  256. package/src/skcapstone/notifications.py +380 -0
  257. package/src/skcapstone/onboard.py +901 -0
  258. package/src/skcapstone/peer_directory.py +324 -0
  259. package/src/skcapstone/peers.py +329 -0
  260. package/src/skcapstone/pillars/identity.py +84 -14
  261. package/src/skcapstone/pillars/memory.py +3 -1
  262. package/src/skcapstone/pillars/security.py +108 -15
  263. package/src/skcapstone/pillars/sync.py +78 -26
  264. package/src/skcapstone/pillars/trust.py +95 -33
  265. package/src/skcapstone/plugins.py +244 -0
  266. package/src/skcapstone/preflight.py +670 -0
  267. package/src/skcapstone/prompt_adapter.py +564 -0
  268. package/src/skcapstone/providers/__init__.py +13 -0
  269. package/src/skcapstone/providers/cloud.py +1061 -0
  270. package/src/skcapstone/providers/docker.py +759 -0
  271. package/src/skcapstone/providers/local.py +1193 -0
  272. package/src/skcapstone/providers/proxmox.py +447 -0
  273. package/src/skcapstone/pubsub.py +516 -0
  274. package/src/skcapstone/rate_limiter.py +119 -0
  275. package/src/skcapstone/register.py +241 -0
  276. package/src/skcapstone/registry_client.py +151 -0
  277. package/src/skcapstone/response_cache.py +194 -0
  278. package/src/skcapstone/response_scorer.py +225 -0
  279. package/src/skcapstone/runtime.py +89 -33
  280. package/src/skcapstone/scheduled_tasks.py +439 -0
  281. package/src/skcapstone/self_healing.py +341 -0
  282. package/src/skcapstone/service_health.py +228 -0
  283. package/src/skcapstone/session_capture.py +268 -0
  284. package/src/skcapstone/session_recorder.py +210 -0
  285. package/src/skcapstone/session_replayer.py +189 -0
  286. package/src/skcapstone/session_skills.py +263 -0
  287. package/src/skcapstone/shell.py +779 -0
  288. package/src/skcapstone/skills/__init__.py +1 -1
  289. package/src/skcapstone/skills/syncthing_setup.py +143 -41
  290. package/src/skcapstone/skjoule.py +861 -0
  291. package/src/skcapstone/snapshots.py +489 -0
  292. package/src/skcapstone/soul.py +1060 -0
  293. package/src/skcapstone/soul_switch.py +255 -0
  294. package/src/skcapstone/spawner.py +544 -0
  295. package/src/skcapstone/state_diff.py +401 -0
  296. package/src/skcapstone/summary.py +270 -0
  297. package/src/skcapstone/sync/backends.py +196 -2
  298. package/src/skcapstone/sync/engine.py +7 -5
  299. package/src/skcapstone/sync/models.py +4 -1
  300. package/src/skcapstone/sync/vault.py +356 -18
  301. package/src/skcapstone/sync_engine.py +363 -0
  302. package/src/skcapstone/sync_watcher.py +745 -0
  303. package/src/skcapstone/systemd.py +331 -0
  304. package/src/skcapstone/team_comms.py +476 -0
  305. package/src/skcapstone/team_engine.py +522 -0
  306. package/src/skcapstone/testrunner.py +300 -0
  307. package/src/skcapstone/tls.py +150 -0
  308. package/src/skcapstone/tokens.py +5 -5
  309. package/src/skcapstone/trust_calibration.py +202 -0
  310. package/src/skcapstone/trust_graph.py +449 -0
  311. package/src/skcapstone/trustee_monitor.py +385 -0
  312. package/src/skcapstone/trustee_ops.py +425 -0
  313. package/src/skcapstone/unified_search.py +421 -0
  314. package/src/skcapstone/uninstall_wizard.py +694 -0
  315. package/src/skcapstone/usage.py +331 -0
  316. package/src/skcapstone/version_check.py +148 -0
  317. package/src/skcapstone/warmth_anchor.py +333 -0
  318. package/src/skcapstone/whoami.py +294 -0
  319. package/systemd/skcapstone-api.socket +9 -0
  320. package/systemd/skcapstone-memory-compress.service +18 -0
  321. package/systemd/skcapstone-memory-compress.timer +11 -0
  322. package/systemd/skcapstone.service +36 -0
  323. package/systemd/skcapstone@.service +50 -0
  324. package/systemd/skcomm-heartbeat.service +18 -0
  325. package/systemd/skcomm-heartbeat.timer +12 -0
  326. package/systemd/skcomm-queue-drain.service +17 -0
  327. package/systemd/skcomm-queue-drain.timer +12 -0
  328. package/tests/conftest.py +13 -1
  329. package/tests/integration/__init__.py +1 -0
  330. package/tests/integration/test_consciousness_e2e.py +877 -0
  331. package/tests/integration/test_skills_registry.py +744 -0
  332. package/tests/test_agent_card.py +190 -0
  333. package/tests/test_agent_runtime.py +1283 -0
  334. package/tests/test_alerts_cmd.py +291 -0
  335. package/tests/test_archiver.py +498 -0
  336. package/tests/test_backup.py +254 -0
  337. package/tests/test_benchmark.py +366 -0
  338. package/tests/test_blueprints.py +457 -0
  339. package/tests/test_capabilities.py +257 -0
  340. package/tests/test_changelog.py +254 -0
  341. package/tests/test_chat.py +385 -0
  342. package/tests/test_claude_md.py +271 -0
  343. package/tests/test_cli_chat_llm.py +336 -0
  344. package/tests/test_cli_completions.py +390 -0
  345. package/tests/test_cli_init_reset.py +164 -0
  346. package/tests/test_cli_memory.py +208 -0
  347. package/tests/test_cli_profile.py +294 -0
  348. package/tests/test_cli_skills.py +223 -0
  349. package/tests/test_cli_status.py +395 -0
  350. package/tests/test_cli_test_cmd.py +206 -0
  351. package/tests/test_cli_test_connection.py +364 -0
  352. package/tests/test_cloud9_bridge.py +260 -0
  353. package/tests/test_cloud_provider.py +449 -0
  354. package/tests/test_cloud_providers.py +522 -0
  355. package/tests/test_completions.py +158 -0
  356. package/tests/test_component_manager.py +398 -0
  357. package/tests/test_config_reload.py +386 -0
  358. package/tests/test_config_validate.py +529 -0
  359. package/tests/test_consciousness_e2e.py +296 -0
  360. package/tests/test_consciousness_loop.py +1289 -0
  361. package/tests/test_context_loader.py +310 -0
  362. package/tests/test_conversation_api.py +306 -0
  363. package/tests/test_conversation_manager.py +381 -0
  364. package/tests/test_conversation_store.py +391 -0
  365. package/tests/test_conversation_summarizer.py +302 -0
  366. package/tests/test_cross_package.py +791 -0
  367. package/tests/test_crush_shim.py +519 -0
  368. package/tests/test_daemon.py +781 -0
  369. package/tests/test_daemon_shutdown.py +309 -0
  370. package/tests/test_dashboard.py +454 -0
  371. package/tests/test_discovery.py +200 -6
  372. package/tests/test_docker_provider.py +966 -0
  373. package/tests/test_doctor.py +257 -0
  374. package/tests/test_doctor_fix.py +351 -0
  375. package/tests/test_e2e_automated.py +292 -0
  376. package/tests/test_error_queue.py +404 -0
  377. package/tests/test_export.py +441 -0
  378. package/tests/test_fallback_tracker.py +219 -0
  379. package/tests/test_file_transfer.py +397 -0
  380. package/tests/test_fuse_mount.py +832 -0
  381. package/tests/test_health_loop.py +422 -0
  382. package/tests/test_heartbeat.py +354 -0
  383. package/tests/test_housekeeping.py +195 -0
  384. package/tests/test_identity_capauth.py +307 -0
  385. package/tests/test_identity_pillar.py +117 -0
  386. package/tests/test_install_wizard.py +68 -0
  387. package/tests/test_integration.py +325 -0
  388. package/tests/test_kms.py +495 -0
  389. package/tests/test_llm_providers.py +265 -0
  390. package/tests/test_local_provider.py +591 -0
  391. package/tests/test_log_config.py +199 -0
  392. package/tests/test_logs_cmd.py +287 -0
  393. package/tests/test_mcp_server.py +1909 -0
  394. package/tests/test_memory_adapter.py +339 -0
  395. package/tests/test_memory_curator.py +218 -0
  396. package/tests/test_memory_engine.py +6 -0
  397. package/tests/test_memory_fortress.py +571 -0
  398. package/tests/test_memory_pillar.py +119 -0
  399. package/tests/test_memory_promoter.py +445 -0
  400. package/tests/test_memory_verifier.py +420 -0
  401. package/tests/test_message_crypto.py +187 -0
  402. package/tests/test_metrics.py +632 -0
  403. package/tests/test_migrate_memories.py +464 -0
  404. package/tests/test_model_router.py +546 -0
  405. package/tests/test_mood.py +394 -0
  406. package/tests/test_multi_agent.py +269 -0
  407. package/tests/test_notifications.py +270 -0
  408. package/tests/test_onboard.py +500 -0
  409. package/tests/test_peer_directory.py +395 -0
  410. package/tests/test_peers.py +248 -0
  411. package/tests/test_pillars.py +87 -9
  412. package/tests/test_preflight.py +484 -0
  413. package/tests/test_prompt_adapter.py +331 -0
  414. package/tests/test_proxmox_provider.py +571 -0
  415. package/tests/test_pubsub.py +377 -0
  416. package/tests/test_rate_limiter.py +121 -0
  417. package/tests/test_registry_client.py +129 -0
  418. package/tests/test_response_cache.py +312 -0
  419. package/tests/test_response_scorer.py +294 -0
  420. package/tests/test_runtime.py +59 -0
  421. package/tests/test_scheduled_tasks.py +451 -0
  422. package/tests/test_security.py +250 -0
  423. package/tests/test_security_pillar.py +213 -0
  424. package/tests/test_self_healing.py +171 -0
  425. package/tests/test_session_capture.py +200 -0
  426. package/tests/test_session_recorder.py +360 -0
  427. package/tests/test_session_skills.py +235 -0
  428. package/tests/test_shell.py +210 -0
  429. package/tests/test_snapshots.py +549 -0
  430. package/tests/test_soul.py +984 -0
  431. package/tests/test_soul_swap.py +406 -0
  432. package/tests/test_spawner.py +211 -0
  433. package/tests/test_state_diff.py +173 -0
  434. package/tests/test_summary.py +135 -0
  435. package/tests/test_sync.py +315 -5
  436. package/tests/test_sync_backends.py +560 -0
  437. package/tests/test_sync_engine.py +482 -0
  438. package/tests/test_sync_pillar.py +344 -0
  439. package/tests/test_sync_pipeline.py +364 -0
  440. package/tests/test_sync_vault.py +581 -0
  441. package/tests/test_syncthing_setup.py +168 -22
  442. package/tests/test_systemd.py +323 -0
  443. package/tests/test_team_comms.py +408 -0
  444. package/tests/test_team_engine.py +397 -0
  445. package/tests/test_testrunner.py +238 -0
  446. package/tests/test_trust_calibration.py +204 -0
  447. package/tests/test_trust_graph.py +207 -0
  448. package/tests/test_trust_pillar.py +291 -0
  449. package/tests/test_trustee_cli.py +427 -0
  450. package/tests/test_trustee_cli_integration.py +325 -0
  451. package/tests/test_trustee_monitor.py +394 -0
  452. package/tests/test_trustee_ops.py +355 -0
  453. package/tests/test_unified_search.py +363 -0
  454. package/tests/test_uninstall_wizard.py +193 -0
  455. package/tests/test_usage.py +333 -0
  456. package/tests/test_version_cmd.py +355 -0
  457. package/tests/test_warmth_anchor.py +162 -0
  458. package/tests/test_whoami.py +245 -0
  459. package/tests/test_ws.py +311 -0
  460. package/.cursorrules +0 -33
  461. package/src/skcapstone/cli.py +0 -1441
@@ -0,0 +1,314 @@
1
+ """
2
+ ContextWindowManager — per-sender token tracking and history compression.
3
+
4
+ Tracks cumulative token usage for each sender's conversation history.
5
+ When a sender's history reaches 80% of ``max_context_tokens``, the oldest
6
+ messages are summarised into a single paragraph by the LLM and replaced in
7
+ the ConversationStore, keeping only the most recent ``_KEEP_RECENT``
8
+ messages verbatim.
9
+
10
+ Token counting: uses ``tiktoken`` (cl100k_base) when installed, otherwise
11
+ falls back to ``len(content) // 4`` (the same char-based estimate used
12
+ throughout the rest of skcapstone).
13
+
14
+ Usage (inside ConsciousnessLoop)::
15
+
16
+ ctx_mgr = ContextWindowManager(home, config.max_context_tokens)
17
+ # After storing a new assistant reply:
18
+ ctx_mgr.check_and_compress(sender, conv_store, bridge)
19
+ # Via MCP tool:
20
+ stats = ctx_mgr.get_all_stats(conv_store)
21
+ """
22
+
23
+ from __future__ import annotations
24
+
25
+ import logging
26
+ from datetime import datetime, timezone
27
+ from pathlib import Path
28
+ from typing import Optional, TYPE_CHECKING
29
+
30
+ if TYPE_CHECKING:
31
+ from .conversation_store import ConversationStore
32
+
33
+ logger = logging.getLogger("skcapstone.context_window")
34
+
35
+ # System prompt for the compression LLM call
36
+ _SUMMARIZE_SYSTEM_PROMPT = (
37
+ "You are a concise summarization assistant for a sovereign AI agent framework. "
38
+ "Summarize the following conversation history into exactly one paragraph (3-5 sentences). "
39
+ "Capture: the main topics discussed, any decisions or outcomes reached, and the overall tone. "
40
+ "This summary replaces older messages to free up context window space. "
41
+ "Be factual and direct. Do not use bullet points or headers."
42
+ )
43
+
44
+ # How many recent messages to keep verbatim (not included in summarization)
45
+ _KEEP_RECENT = 4
46
+
47
+ # Context window fill threshold that triggers compression (80 %)
48
+ _THRESHOLD_PCT = 0.80
49
+
50
+
51
+ # ---------------------------------------------------------------------------
52
+ # Token helpers (module-level, reusable)
53
+ # ---------------------------------------------------------------------------
54
+
55
+
56
+ def count_tokens(text: str) -> int:
57
+ """Count tokens in *text*.
58
+
59
+ Uses ``tiktoken`` (cl100k_base encoding) when the package is installed.
60
+ Falls back to ``max(1, len(text) // 4)`` (4 chars ≈ 1 token) otherwise.
61
+
62
+ Args:
63
+ text: Input text.
64
+
65
+ Returns:
66
+ Token count (always >= 1 for non-empty input).
67
+ """
68
+ try:
69
+ import tiktoken # optional dep
70
+ enc = tiktoken.get_encoding("cl100k_base")
71
+ return max(1, len(enc.encode(text)))
72
+ except ImportError:
73
+ return max(1, len(text) // 4)
74
+
75
+
76
+ def count_history_tokens(history: list[dict]) -> int:
77
+ """Sum token counts for all messages in a history list.
78
+
79
+ Args:
80
+ history: List of message dicts, each expected to have a ``"content"`` key.
81
+
82
+ Returns:
83
+ Total token estimate across all messages.
84
+ """
85
+ return sum(count_tokens(str(msg.get("content", ""))) for msg in history)
86
+
87
+
88
+ # ---------------------------------------------------------------------------
89
+ # ContextWindowManager
90
+ # ---------------------------------------------------------------------------
91
+
92
+
93
+ class ContextWindowManager:
94
+ """Tracks per-sender token usage and compresses history at the 80 % threshold.
95
+
96
+ Maintains an in-memory stats table for every peer that has been checked.
97
+ Stats are refreshed on every :meth:`check_and_compress` call and after a
98
+ successful compression.
99
+
100
+ Args:
101
+ home: Agent home directory (used for any future persistence needs).
102
+ max_context_tokens: Model context window token budget. The
103
+ compression threshold is set to 80 % of this value.
104
+ """
105
+
106
+ def __init__(self, home: Path, max_context_tokens: int = 8000) -> None:
107
+ self._home = Path(home)
108
+ self._max_context_tokens = max_context_tokens
109
+ self._threshold = int(max_context_tokens * _THRESHOLD_PCT)
110
+ # peer -> stats snapshot
111
+ self._stats: dict[str, dict] = {}
112
+
113
+ # ------------------------------------------------------------------
114
+ # Public API
115
+ # ------------------------------------------------------------------
116
+
117
+ def check_and_compress(
118
+ self,
119
+ peer: str,
120
+ store: "ConversationStore",
121
+ bridge=None,
122
+ ) -> bool:
123
+ """Check peer history token count; compress if at or over threshold.
124
+
125
+ Loads the full history for *peer* from *store*, counts tokens, and
126
+ updates the in-memory stats table. When the count meets or exceeds
127
+ the 80 % threshold and *bridge* is provided, the oldest messages are
128
+ summarised by the LLM (keeping ``_KEEP_RECENT`` verbatim) and the
129
+ history is atomically replaced on disk via
130
+ :meth:`ConversationStore.replace`.
131
+
132
+ Args:
133
+ peer: Sanitised peer name.
134
+ store: :class:`~skcapstone.conversation_store.ConversationStore`
135
+ instance for reading/writing history.
136
+ bridge: :class:`~skcapstone.consciousness_loop.LLMBridge` used to
137
+ generate the summary. If ``None`` compression is skipped but
138
+ stats are still updated.
139
+
140
+ Returns:
141
+ ``True`` if the history was compressed, ``False`` otherwise.
142
+ """
143
+ history = store.load(peer)
144
+ token_count = count_history_tokens(history)
145
+
146
+ self._stats[peer] = {
147
+ "tokens": token_count,
148
+ "messages": len(history),
149
+ "threshold": self._threshold,
150
+ "max_context_tokens": self._max_context_tokens,
151
+ "pct_used": round(token_count / self._max_context_tokens * 100, 1),
152
+ "last_compressed_at": self._stats.get(peer, {}).get("last_compressed_at"),
153
+ }
154
+
155
+ if token_count < self._threshold:
156
+ return False
157
+
158
+ if bridge is None:
159
+ logger.warning(
160
+ "Context window at %.1f%% for %s but no bridge — skipping compression",
161
+ self._stats[peer]["pct_used"],
162
+ peer,
163
+ )
164
+ return False
165
+
166
+ if len(history) <= _KEEP_RECENT:
167
+ logger.debug(
168
+ "Context window at %.1f%% for %s but only %d messages — skipping",
169
+ self._stats[peer]["pct_used"],
170
+ peer,
171
+ len(history),
172
+ )
173
+ return False
174
+
175
+ to_summarize = history[:-_KEEP_RECENT]
176
+ recent = history[-_KEEP_RECENT:]
177
+
178
+ logger.info(
179
+ "Context window %.1f%% for %s — compressing %d older messages",
180
+ self._stats[peer]["pct_used"],
181
+ peer,
182
+ len(to_summarize),
183
+ )
184
+
185
+ summary_text = self._call_llm_summarize(peer, to_summarize, bridge)
186
+ if not summary_text:
187
+ logger.warning("LLM summarization returned empty result for %s — skipping", peer)
188
+ return False
189
+
190
+ now = datetime.now(timezone.utc).isoformat()
191
+ summary_entry: dict = {
192
+ "role": "system",
193
+ "content": (
194
+ f"[Earlier context — {len(to_summarize)} messages summarized]: {summary_text}"
195
+ ),
196
+ "timestamp": now,
197
+ "is_summary": True,
198
+ "summarized_count": len(to_summarize),
199
+ }
200
+ new_history = [summary_entry] + recent
201
+ store.replace(peer, new_history)
202
+
203
+ new_token_count = count_history_tokens(new_history)
204
+ self._stats[peer].update(
205
+ {
206
+ "tokens": new_token_count,
207
+ "messages": len(new_history),
208
+ "pct_used": round(new_token_count / self._max_context_tokens * 100, 1),
209
+ "last_compressed_at": now,
210
+ }
211
+ )
212
+ logger.info(
213
+ "Context compressed for %s: %d→%d messages, %d→%d tokens (%.1f%%)",
214
+ peer,
215
+ len(history),
216
+ len(new_history),
217
+ token_count,
218
+ new_token_count,
219
+ self._stats[peer]["pct_used"],
220
+ )
221
+ return True
222
+
223
+ def update_stats(self, peer: str, store: "ConversationStore") -> dict:
224
+ """Refresh and return stats for *peer* without triggering compression.
225
+
226
+ Args:
227
+ peer: Peer name.
228
+ store: :class:`~skcapstone.conversation_store.ConversationStore`.
229
+
230
+ Returns:
231
+ Stats dict for this peer.
232
+ """
233
+ history = store.load(peer)
234
+ token_count = count_history_tokens(history)
235
+ self._stats[peer] = {
236
+ "tokens": token_count,
237
+ "messages": len(history),
238
+ "threshold": self._threshold,
239
+ "max_context_tokens": self._max_context_tokens,
240
+ "pct_used": round(token_count / self._max_context_tokens * 100, 1),
241
+ "last_compressed_at": self._stats.get(peer, {}).get("last_compressed_at"),
242
+ }
243
+ return self._stats[peer]
244
+
245
+ def get_all_stats(
246
+ self, store: Optional["ConversationStore"] = None
247
+ ) -> dict[str, dict]:
248
+ """Return current stats for all tracked senders.
249
+
250
+ When *store* is provided any peers that have on-disk history but are
251
+ not yet in the in-memory stats table (e.g. written by a previous
252
+ process) are lazily loaded and included.
253
+
254
+ Args:
255
+ store: Optional :class:`~skcapstone.conversation_store.ConversationStore`
256
+ used to discover and load previously unseen peers.
257
+
258
+ Returns:
259
+ Mapping of peer name → stats dict.
260
+ """
261
+ if store is not None:
262
+ for peer in store.all_peers():
263
+ if peer not in self._stats:
264
+ self.update_stats(peer, store)
265
+ return dict(self._stats)
266
+
267
+ # ------------------------------------------------------------------
268
+ # Private helpers
269
+ # ------------------------------------------------------------------
270
+
271
+ def _call_llm_summarize(
272
+ self, peer: str, messages: list[dict], bridge
273
+ ) -> str:
274
+ """Call *bridge* to produce a one-paragraph summary of *messages*.
275
+
276
+ Args:
277
+ peer: Peer name (included in the summarisation prompt for context).
278
+ messages: Older messages to summarise.
279
+ bridge: :class:`~skcapstone.consciousness_loop.LLMBridge`.
280
+
281
+ Returns:
282
+ Summary string, or empty string on any failure.
283
+ """
284
+ try:
285
+ from .model_router import TaskSignal
286
+
287
+ lines = [f"Conversation with {peer} ({len(messages)} messages to summarize):"]
288
+ lines.append("")
289
+ for msg in messages:
290
+ role = msg.get("role", "unknown")
291
+ content = str(msg.get("content", "")).strip()
292
+ # Skip existing summary sentinels (nested compression guard)
293
+ if msg.get("is_summary"):
294
+ lines.append(f"[Previous summary]: {content}")
295
+ continue
296
+ label = "Agent" if role == "assistant" else peer.capitalize()
297
+ lines.append(f"{label}: {content}")
298
+ lines.append("")
299
+ lines.append(
300
+ "Summarize the above into one paragraph (3-5 sentences). "
301
+ "Preserve key topics, decisions, and tone."
302
+ )
303
+ prompt_text = "\n".join(lines)
304
+
305
+ signal = TaskSignal(
306
+ description="Compress conversation context window",
307
+ tags=["summary", "context"],
308
+ estimated_tokens=count_tokens(prompt_text),
309
+ )
310
+ result = bridge.generate(_SUMMARIZE_SYSTEM_PROMPT, prompt_text, signal)
311
+ return result or ""
312
+ except Exception as exc:
313
+ logger.warning("Context compression LLM call failed for %s: %s", peer, exc)
314
+ return ""
@@ -0,0 +1,238 @@
1
+ """
2
+ ConversationManager — centralized manager for all peer conversation histories.
3
+
4
+ Owns the {home}/conversations/ directory. Provides a clean API for adding,
5
+ retrieving, searching, and exporting conversations instead of ad-hoc file
6
+ writes scattered across the codebase.
7
+
8
+ Used by ConsciousnessLoop (via SystemPromptBuilder) and daemon API endpoints.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import json
14
+ import logging
15
+ import re
16
+ from collections import defaultdict
17
+ from datetime import datetime, timezone
18
+ from pathlib import Path
19
+ from typing import Optional
20
+
21
+ logger = logging.getLogger("skcapstone.conversation_manager")
22
+
23
+ # Allowlist for peer name characters (alphanumeric + safe punctuation)
24
+ _PEER_NAME_SAFE_RE = re.compile(r"[^a-zA-Z0-9_\-@\.]")
25
+
26
+
27
+ def _sanitize_peer_name(peer: str) -> str:
28
+ """Sanitize a peer name for safe use as a filesystem key.
29
+
30
+ Strips path separators (/ \\), null bytes, and any character not in the
31
+ alphanumeric + ``-_@.`` set. Caps length at 64 characters. Returns
32
+ ``"unknown"`` if the result would be empty.
33
+
34
+ Args:
35
+ peer: Raw peer name.
36
+
37
+ Returns:
38
+ Filesystem-safe peer name, at most 64 characters long.
39
+ """
40
+ if not peer or not isinstance(peer, str):
41
+ return "unknown"
42
+ sanitized = peer.replace("\x00", "").replace("/", "").replace("\\", "")
43
+ sanitized = _PEER_NAME_SAFE_RE.sub("", sanitized)
44
+ sanitized = sanitized.strip(".")
45
+ return sanitized[:64] or "unknown"
46
+
47
+
48
+ class ConversationManager:
49
+ """Centralized manager for all peer conversation histories.
50
+
51
+ Stores conversations as JSON files under {home}/conversations/{peer}.json.
52
+ Provides atomic writes, in-memory caching, search, and export.
53
+
54
+ Args:
55
+ home: Agent home directory (~/.skcapstone).
56
+ max_history_messages: Maximum messages to retain per peer in memory
57
+ and on disk.
58
+ """
59
+
60
+ def __init__(self, home: Path, max_history_messages: int = 10) -> None:
61
+ self._home = Path(home)
62
+ self._conversations_dir = self._home / "conversations"
63
+ self._max_history_messages = max_history_messages
64
+ self._history: dict[str, list[dict]] = defaultdict(list)
65
+ self._load_all()
66
+
67
+ # ------------------------------------------------------------------
68
+ # Public API
69
+ # ------------------------------------------------------------------
70
+
71
+ def list_peers(self) -> list[dict]:
72
+ """List all peers that have conversation history.
73
+
74
+ Returns:
75
+ List of summary dicts, each with keys:
76
+ ``peer``, ``message_count``, ``last_message_time``,
77
+ ``last_message_preview``. Sorted most-recent-first.
78
+ """
79
+ peers = []
80
+ for peer, messages in self._history.items():
81
+ if not messages:
82
+ continue
83
+ last = messages[-1]
84
+ peers.append({
85
+ "peer": peer,
86
+ "message_count": len(messages),
87
+ "last_message_time": last.get("timestamp"),
88
+ "last_message_preview": last.get("content", "")[:80],
89
+ })
90
+ peers.sort(key=lambda p: p["last_message_time"] or "", reverse=True)
91
+ return peers
92
+
93
+ def get_history(self, peer: str) -> list[dict]:
94
+ """Get full conversation history for a peer.
95
+
96
+ Args:
97
+ peer: Peer agent name.
98
+
99
+ Returns:
100
+ List of message dicts with ``role``, ``content``, ``timestamp``.
101
+ Returns an empty list if the peer is unknown.
102
+ """
103
+ peer = _sanitize_peer_name(peer)
104
+ return list(self._history.get(peer, []))
105
+
106
+ def add_message(self, peer: str, role: str, content: str) -> dict:
107
+ """Add a message to the peer's conversation history.
108
+
109
+ Appends to in-memory history, caps at ``max_history_messages``, and
110
+ atomically persists to disk.
111
+
112
+ Args:
113
+ peer: Peer agent name.
114
+ role: ``"user"`` or ``"assistant"``.
115
+ content: Message content.
116
+
117
+ Returns:
118
+ The message dict that was stored (includes ``timestamp``).
119
+ """
120
+ peer = _sanitize_peer_name(peer)
121
+ msg: dict = {
122
+ "role": role,
123
+ "content": content,
124
+ "timestamp": datetime.now(timezone.utc).isoformat(),
125
+ }
126
+ self._history[peer].append(msg)
127
+ if len(self._history[peer]) > self._max_history_messages:
128
+ self._history[peer] = self._history[peer][-self._max_history_messages:]
129
+ self._persist(peer)
130
+ return msg
131
+
132
+ def search(self, query: str) -> list[dict]:
133
+ """Search for a query string across all conversation histories.
134
+
135
+ Case-insensitive substring match against message content.
136
+
137
+ Args:
138
+ query: Search string.
139
+
140
+ Returns:
141
+ List of match dicts, each with ``peer``, ``role``, ``content``,
142
+ ``timestamp``.
143
+ """
144
+ query_lower = query.lower()
145
+ matches: list[dict] = []
146
+ for peer, messages in self._history.items():
147
+ for msg in messages:
148
+ if query_lower in msg.get("content", "").lower():
149
+ matches.append({
150
+ "peer": peer,
151
+ "role": msg.get("role"),
152
+ "content": msg.get("content"),
153
+ "timestamp": msg.get("timestamp"),
154
+ })
155
+ return matches
156
+
157
+ def export_all(self) -> dict[str, list[dict]]:
158
+ """Export all conversations as a plain dict.
159
+
160
+ Returns:
161
+ Dict mapping peer name → list of message dicts.
162
+ Peers with no messages are excluded.
163
+ """
164
+ return {peer: list(msgs) for peer, msgs in self._history.items() if msgs}
165
+
166
+ def delete(self, peer: str) -> bool:
167
+ """Delete a peer's conversation history from memory and disk.
168
+
169
+ Args:
170
+ peer: Peer agent name.
171
+
172
+ Returns:
173
+ ``True`` if the conversation existed and was deleted.
174
+ """
175
+ peer = _sanitize_peer_name(peer)
176
+ existed = bool(self._history.pop(peer, None))
177
+ target = self._conversations_dir / f"{peer}.json"
178
+ if target.exists():
179
+ target.unlink()
180
+ return True
181
+ return existed
182
+
183
+ def format_history_for_prompt(self, peer: str, max_messages: int = 10) -> str:
184
+ """Format recent conversation history for inclusion in a system prompt.
185
+
186
+ Args:
187
+ peer: Peer agent name.
188
+ max_messages: Maximum messages to include.
189
+
190
+ Returns:
191
+ Formatted history string, or empty string if no history.
192
+ """
193
+ history = self._history.get(peer, [])
194
+ if not history:
195
+ return ""
196
+ recent = history[-max_messages:]
197
+ lines = [f"Recent conversation with {peer}:"]
198
+ for msg in recent:
199
+ role = msg.get("role", "?")
200
+ content = msg.get("content", "")[:200]
201
+ lines.append(f" [{role}] {content}")
202
+ return "\n".join(lines)
203
+
204
+ # ------------------------------------------------------------------
205
+ # Private helpers
206
+ # ------------------------------------------------------------------
207
+
208
+ def _load_all(self) -> None:
209
+ """Load all peer conversation files from the conversations directory."""
210
+ if not self._conversations_dir.exists():
211
+ return
212
+ for conv_file in self._conversations_dir.glob("*.json"):
213
+ peer = conv_file.stem
214
+ try:
215
+ data = json.loads(conv_file.read_text(encoding="utf-8"))
216
+ if isinstance(data, list):
217
+ self._history[peer] = data[-self._max_history_messages:]
218
+ except Exception as exc:
219
+ logger.debug("Failed to load conversation %s: %s", conv_file, exc)
220
+
221
+ def _persist(self, peer: str) -> None:
222
+ """Atomically write peer history to {home}/conversations/{peer}.json.
223
+
224
+ Uses a temp file + rename for atomic update, preventing corruption if
225
+ the process is interrupted mid-write.
226
+
227
+ Args:
228
+ peer: Peer agent name (already sanitized).
229
+ """
230
+ try:
231
+ self._conversations_dir.mkdir(parents=True, exist_ok=True)
232
+ target = self._conversations_dir / f"{peer}.json"
233
+ tmp = target.with_suffix(".json.tmp")
234
+ payload = json.dumps(self._history[peer], ensure_ascii=False, indent=2)
235
+ tmp.write_text(payload, encoding="utf-8")
236
+ tmp.replace(target)
237
+ except Exception as exc:
238
+ logger.debug("Failed to persist conversation for %s: %s", peer, exc)