superlocalmemory 3.6.22 → 3.7.0

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 (303) hide show
  1. package/CHANGELOG.md +52 -0
  2. package/README.md +275 -72
  3. package/bin/slm-npm +43 -89
  4. package/docs/pi-dev-integration.md +43 -0
  5. package/ide/configs/antigravity-mcp.json +2 -2
  6. package/ide/configs/chatgpt-desktop-mcp.json +1 -1
  7. package/ide/configs/claude-desktop-mcp.json +2 -2
  8. package/ide/configs/windsurf-mcp.json +2 -2
  9. package/ide/hooks/context-hook.js +6 -2
  10. package/ide/hooks/post-recall-hook.js +7 -3
  11. package/ide/hooks/tool-event-hook.sh +2 -1
  12. package/package.json +19 -10
  13. package/plugin/.claude-plugin/plugin.json +1 -1
  14. package/plugin/_GENERATED.md +1 -1
  15. package/plugin/agents/slm-memory-advisor.md +1 -1
  16. package/plugin/requirements.txt +1 -1
  17. package/plugin/skills/slm-session/SKILL.md +1 -1
  18. package/plugin-src/rules/AGENTS.md +1 -1
  19. package/pyproject.toml +40 -8
  20. package/scripts/postinstall-interactive.js +17 -94
  21. package/scripts/postinstall.js +185 -258
  22. package/scripts/preuninstall.js +9 -50
  23. package/src/superlocalmemory/__init__.py +2 -2
  24. package/src/superlocalmemory/attribution/mathematical_dna.py +1 -1
  25. package/src/superlocalmemory/attribution/signer.py +34 -19
  26. package/src/superlocalmemory/attribution/watermark.py +1 -1
  27. package/src/superlocalmemory/cli/_lazy_init.py +3 -5
  28. package/src/superlocalmemory/cli/commands.py +490 -195
  29. package/src/superlocalmemory/cli/context_commands.py +5 -4
  30. package/src/superlocalmemory/cli/daemon.py +282 -187
  31. package/src/superlocalmemory/cli/db_migrate.py +3 -1
  32. package/src/superlocalmemory/cli/diagnostics_cmd.py +28 -0
  33. package/src/superlocalmemory/cli/evidence_cmd.py +103 -0
  34. package/src/superlocalmemory/cli/ingest_cmd.py +7 -3
  35. package/src/superlocalmemory/cli/main.py +128 -31
  36. package/src/superlocalmemory/cli/pending_store.py +54 -38
  37. package/src/superlocalmemory/cli/scale_engine_cmd.py +37 -0
  38. package/src/superlocalmemory/cli/service_installer.py +57 -52
  39. package/src/superlocalmemory/cli/setup_wizard.py +142 -88
  40. package/src/superlocalmemory/cli/version_banner.py +2 -1
  41. package/src/superlocalmemory/code_graph/config.py +3 -1
  42. package/src/superlocalmemory/core/backend_orchestrator.py +81 -21
  43. package/src/superlocalmemory/core/config.py +65 -20
  44. package/src/superlocalmemory/core/consolidation_engine.py +9 -7
  45. package/src/superlocalmemory/core/context_cache.py +56 -8
  46. package/src/superlocalmemory/core/derivation_lineage.py +246 -0
  47. package/src/superlocalmemory/core/embedding_worker.py +32 -20
  48. package/src/superlocalmemory/core/embeddings.py +54 -18
  49. package/src/superlocalmemory/core/engine.py +150 -104
  50. package/src/superlocalmemory/core/engine_ingestion.py +513 -0
  51. package/src/superlocalmemory/core/engine_wiring.py +2 -0
  52. package/src/superlocalmemory/core/evidence_bundle.py +526 -0
  53. package/src/superlocalmemory/core/fact_consolidator.py +5 -11
  54. package/src/superlocalmemory/core/graph_analyzer.py +2 -2
  55. package/src/superlocalmemory/core/health_monitor.py +4 -2
  56. package/src/superlocalmemory/core/ingestion_command.py +636 -0
  57. package/src/superlocalmemory/core/injection.py +69 -18
  58. package/src/superlocalmemory/core/lifecycle_state.py +153 -0
  59. package/src/superlocalmemory/core/maintenance.py +23 -22
  60. package/src/superlocalmemory/core/maintenance_scheduler.py +51 -35
  61. package/src/superlocalmemory/core/mutations.py +143 -0
  62. package/src/superlocalmemory/core/platform_utils.py +7 -4
  63. package/src/superlocalmemory/core/ram_lock.py +16 -5
  64. package/src/superlocalmemory/core/rate_limit.py +1 -1
  65. package/src/superlocalmemory/core/recall_pipeline.py +60 -101
  66. package/src/superlocalmemory/core/recall_worker.py +76 -59
  67. package/src/superlocalmemory/core/registry.py +1 -1
  68. package/src/superlocalmemory/core/scale_engine.py +293 -0
  69. package/src/superlocalmemory/core/score_contract.py +62 -0
  70. package/src/superlocalmemory/core/security_primitives.py +3 -1
  71. package/src/superlocalmemory/core/slm_disabled.py +3 -5
  72. package/src/superlocalmemory/core/store_pipeline.py +172 -40
  73. package/src/superlocalmemory/core/tier_manager.py +32 -20
  74. package/src/superlocalmemory/core/worker_pool.py +13 -4
  75. package/src/superlocalmemory/dynamics/activation_guided_quantization.py +1 -1
  76. package/src/superlocalmemory/dynamics/eap_scheduler.py +10 -3
  77. package/src/superlocalmemory/dynamics/ebbinghaus_langevin_coupling.py +1 -1
  78. package/src/superlocalmemory/dynamics/fisher_langevin_coupling.py +1 -1
  79. package/src/superlocalmemory/encoding/auto_linker.py +1 -1
  80. package/src/superlocalmemory/encoding/cognitive_consolidator.py +7 -16
  81. package/src/superlocalmemory/encoding/consolidator.py +22 -5
  82. package/src/superlocalmemory/encoding/fact_extractor.py +1 -1
  83. package/src/superlocalmemory/encoding/foresight.py +2 -0
  84. package/src/superlocalmemory/encoding/graph_builder.py +1 -1
  85. package/src/superlocalmemory/encoding/temporal_parser.py +2 -0
  86. package/src/superlocalmemory/evaluation/__init__.py +13 -0
  87. package/src/superlocalmemory/evaluation/calibration.py +308 -0
  88. package/src/superlocalmemory/evolution/skill_evolver.py +2 -1
  89. package/src/superlocalmemory/graph/cozo_backend.py +256 -23
  90. package/src/superlocalmemory/hooks/_outcome_common.py +21 -11
  91. package/src/superlocalmemory/hooks/antigravity_adapter.py +10 -31
  92. package/src/superlocalmemory/hooks/auto_invoker.py +25 -27
  93. package/src/superlocalmemory/hooks/auto_recall.py +31 -6
  94. package/src/superlocalmemory/hooks/auto_recall_hook.py +13 -33
  95. package/src/superlocalmemory/hooks/before_web_hook.py +9 -7
  96. package/src/superlocalmemory/hooks/claude_code_hooks.py +126 -39
  97. package/src/superlocalmemory/hooks/codex_assets.py +59 -0
  98. package/src/superlocalmemory/hooks/codex_hooks.py +186 -0
  99. package/src/superlocalmemory/hooks/context_payload.py +1 -1
  100. package/src/superlocalmemory/hooks/copilot_adapter.py +9 -24
  101. package/src/superlocalmemory/hooks/cursor_adapter.py +10 -32
  102. package/src/superlocalmemory/hooks/hook_daemon.py +4 -2
  103. package/src/superlocalmemory/hooks/hook_handlers.py +241 -55
  104. package/src/superlocalmemory/hooks/memory_protocol.py +5 -3
  105. package/src/superlocalmemory/hooks/post_tool_async_hook.py +4 -2
  106. package/src/superlocalmemory/hooks/session_registry.py +15 -8
  107. package/src/superlocalmemory/hooks/stop_outcome_hook.py +10 -6
  108. package/src/superlocalmemory/hooks/topic_shift_hook.py +42 -12
  109. package/src/superlocalmemory/hooks/user_prompt_hook.py +9 -14
  110. package/src/superlocalmemory/hooks/user_prompt_rehash_hook.py +19 -11
  111. package/src/superlocalmemory/infra/auth_middleware.py +38 -5
  112. package/src/superlocalmemory/infra/backup.py +7 -5
  113. package/src/superlocalmemory/infra/cloud_backup.py +18 -8
  114. package/src/superlocalmemory/infra/daemon_identity.py +248 -0
  115. package/src/superlocalmemory/infra/data_root.py +199 -0
  116. package/src/superlocalmemory/infra/event_bus.py +3 -1
  117. package/src/superlocalmemory/infra/local_diagnostics.py +327 -0
  118. package/src/superlocalmemory/infra/process_reaper.py +23 -0
  119. package/src/superlocalmemory/ingestion/adapter_manager.py +27 -9
  120. package/src/superlocalmemory/ingestion/base_adapter.py +25 -31
  121. package/src/superlocalmemory/ingestion/calendar_adapter.py +13 -4
  122. package/src/superlocalmemory/ingestion/credentials.py +14 -7
  123. package/src/superlocalmemory/ingestion/gmail_adapter.py +13 -4
  124. package/src/superlocalmemory/ingestion/transcript_adapter.py +7 -2
  125. package/src/superlocalmemory/learning/consolidation_quantization_worker.py +1 -1
  126. package/src/superlocalmemory/learning/ensemble.py +11 -0
  127. package/src/superlocalmemory/learning/entity_compiler.py +1 -1
  128. package/src/superlocalmemory/learning/feedback.py +1 -1
  129. package/src/superlocalmemory/learning/forgetting_scheduler.py +12 -7
  130. package/src/superlocalmemory/learning/quantization_scheduler.py +1 -1
  131. package/src/superlocalmemory/learning/ranker.py +4 -1
  132. package/src/superlocalmemory/learning/source_quality.py +1 -1
  133. package/src/superlocalmemory/learning/trigram_index.py +3 -2
  134. package/src/superlocalmemory/llm/backbone.py +13 -8
  135. package/src/superlocalmemory/math/ebbinghaus.py +1 -1
  136. package/src/superlocalmemory/math/fisher.py +1 -1
  137. package/src/superlocalmemory/math/fisher_quantized.py +1 -1
  138. package/src/superlocalmemory/math/hopfield.py +1 -1
  139. package/src/superlocalmemory/math/langevin.py +1 -1
  140. package/src/superlocalmemory/math/polar_quant.py +3 -4
  141. package/src/superlocalmemory/math/qjl.py +1 -1
  142. package/src/superlocalmemory/math/sheaf.py +1 -1
  143. package/src/superlocalmemory/math/turbo_quant.py +3 -2
  144. package/src/superlocalmemory/mcp/_daemon_proxy.py +12 -11
  145. package/src/superlocalmemory/mcp/_pool_adapter.py +27 -0
  146. package/src/superlocalmemory/mcp/http_transport.py +53 -0
  147. package/src/superlocalmemory/mcp/server.py +39 -13
  148. package/src/superlocalmemory/mcp/shared.py +69 -3
  149. package/src/superlocalmemory/mcp/tools_active.py +141 -31
  150. package/src/superlocalmemory/mcp/tools_core.py +128 -29
  151. package/src/superlocalmemory/mcp/tools_evolution.py +5 -7
  152. package/src/superlocalmemory/mcp/tools_learning.py +42 -2
  153. package/src/superlocalmemory/mcp/tools_mesh.py +7 -23
  154. package/src/superlocalmemory/mcp/tools_optimize.py +8 -1
  155. package/src/superlocalmemory/mcp/tools_v28.py +23 -2
  156. package/src/superlocalmemory/mcp/tools_v3.py +26 -1
  157. package/src/superlocalmemory/mcp/tools_v33.py +56 -17
  158. package/src/superlocalmemory/mesh/broker.py +2 -0
  159. package/src/superlocalmemory/mesh/remote_sync.py +50 -12
  160. package/src/superlocalmemory/optimize/cache/manager.py +77 -1
  161. package/src/superlocalmemory/optimize/cache/semantic.py +23 -3
  162. package/src/superlocalmemory/optimize/compress/ccr.py +4 -0
  163. package/src/superlocalmemory/optimize/compress/router.py +6 -1
  164. package/src/superlocalmemory/optimize/config/__init__.py +5 -0
  165. package/src/superlocalmemory/optimize/config/store.py +6 -4
  166. package/src/superlocalmemory/optimize/proxy/_helpers.py +15 -5
  167. package/src/superlocalmemory/optimize/proxy/capture.py +3 -2
  168. package/src/superlocalmemory/optimize/proxy/server.py +2 -2
  169. package/src/superlocalmemory/optimize/storage/db.py +14 -13
  170. package/src/superlocalmemory/retrieval/agentic.py +1 -1
  171. package/src/superlocalmemory/retrieval/ann_index.py +1 -1
  172. package/src/superlocalmemory/retrieval/bm25_channel.py +35 -11
  173. package/src/superlocalmemory/retrieval/bridge_discovery.py +73 -8
  174. package/src/superlocalmemory/retrieval/engine.py +169 -79
  175. package/src/superlocalmemory/retrieval/entity_channel.py +289 -67
  176. package/src/superlocalmemory/retrieval/forgetting_filter.py +1 -1
  177. package/src/superlocalmemory/retrieval/fusion.py +1 -1
  178. package/src/superlocalmemory/retrieval/hopfield_channel.py +118 -30
  179. package/src/superlocalmemory/retrieval/profile_channel.py +1 -1
  180. package/src/superlocalmemory/retrieval/quantization_aware_search.py +16 -10
  181. package/src/superlocalmemory/retrieval/reranker.py +56 -20
  182. package/src/superlocalmemory/retrieval/scope_policy.py +85 -0
  183. package/src/superlocalmemory/retrieval/semantic_channel.py +122 -14
  184. package/src/superlocalmemory/retrieval/spreading_activation.py +141 -25
  185. package/src/superlocalmemory/retrieval/strategy.py +1 -1
  186. package/src/superlocalmemory/retrieval/temporal_channel.py +30 -15
  187. package/src/superlocalmemory/retrieval/vector_store.py +1 -1
  188. package/src/superlocalmemory/server/api.py +10 -7
  189. package/src/superlocalmemory/server/bandit_loops.py +4 -2
  190. package/src/superlocalmemory/server/recall_serializer.py +24 -0
  191. package/src/superlocalmemory/server/route_mutations.py +84 -0
  192. package/src/superlocalmemory/server/routes/agents.py +8 -6
  193. package/src/superlocalmemory/server/routes/brain.py +14 -12
  194. package/src/superlocalmemory/server/routes/chat.py +29 -12
  195. package/src/superlocalmemory/server/routes/data_io.py +55 -24
  196. package/src/superlocalmemory/server/routes/helpers.py +29 -4
  197. package/src/superlocalmemory/server/routes/ingest.py +53 -36
  198. package/src/superlocalmemory/server/routes/memories.py +104 -43
  199. package/src/superlocalmemory/server/routes/mesh.py +31 -0
  200. package/src/superlocalmemory/server/routes/profiles.py +26 -4
  201. package/src/superlocalmemory/server/routes/tiers.py +43 -11
  202. package/src/superlocalmemory/server/routes/timeline.py +5 -1
  203. package/src/superlocalmemory/server/routes/v3_api.py +76 -21
  204. package/src/superlocalmemory/server/security_middleware.py +1 -1
  205. package/src/superlocalmemory/server/ui.py +6 -3
  206. package/src/superlocalmemory/server/unified_daemon.py +680 -293
  207. package/src/superlocalmemory/server/write_identity.py +147 -0
  208. package/src/superlocalmemory/storage/access_log.py +4 -3
  209. package/src/superlocalmemory/storage/database.py +118 -25
  210. package/src/superlocalmemory/storage/migration_runner.py +84 -1
  211. package/src/superlocalmemory/storage/migration_v33.py +1 -1
  212. package/src/superlocalmemory/storage/migrations/M002_model_state_history.py +6 -60
  213. package/src/superlocalmemory/storage/migrations/M018_ingestion_operations.py +120 -0
  214. package/src/superlocalmemory/storage/migrations/M019_derivation_lineage.py +54 -0
  215. package/src/superlocalmemory/storage/migrations/M020_model_state_integrity.py +52 -0
  216. package/src/superlocalmemory/storage/migrations/__init__.py +5 -0
  217. package/src/superlocalmemory/storage/models.py +16 -0
  218. package/src/superlocalmemory/storage/quantized_store.py +20 -3
  219. package/src/superlocalmemory/storage/v2_migrator.py +5 -3
  220. package/src/superlocalmemory/ui/favicon.svg +5 -0
  221. package/src/superlocalmemory/ui/index.html +1 -0
  222. package/src/superlocalmemory/ui/js/compliance.js +1 -1
  223. package/src/superlocalmemory/ui/js/core.js +49 -8
  224. package/src/superlocalmemory/ui/js/dashboard.js +23 -2
  225. package/src/superlocalmemory/ui/js/feedback.js +1 -1
  226. package/src/superlocalmemory/ui/js/graph-filters.js +1 -1
  227. package/src/superlocalmemory/ui/js/graph-ui.js +1 -1
  228. package/src/superlocalmemory/ui/js/lifecycle.js +1 -1
  229. package/src/superlocalmemory/ui/js/ng-mesh.js +15 -49
  230. package/src/superlocalmemory/ui/js/settings.js +4 -2
  231. package/src/superlocalmemory/vector/lancedb_backend.py +57 -9
  232. package/bin/slm +0 -59
  233. package/bin/slm.bat +0 -77
  234. package/bin/slm.cmd +0 -5
  235. package/ide/integrations/langchain/README.md +0 -106
  236. package/ide/integrations/langchain/langchain_superlocalmemory/__init__.py +0 -9
  237. package/ide/integrations/langchain/langchain_superlocalmemory/chat_message_history.py +0 -201
  238. package/ide/integrations/langchain/pyproject.toml +0 -38
  239. package/ide/integrations/langchain/tests/__init__.py +0 -3
  240. package/ide/integrations/langchain/tests/test_chat_message_history.py +0 -215
  241. package/ide/integrations/langchain/tests/test_security.py +0 -117
  242. package/ide/integrations/llamaindex/README.md +0 -81
  243. package/ide/integrations/llamaindex/llama_index/storage/chat_store/superlocalmemory/__init__.py +0 -9
  244. package/ide/integrations/llamaindex/llama_index/storage/chat_store/superlocalmemory/base.py +0 -316
  245. package/ide/integrations/llamaindex/pyproject.toml +0 -43
  246. package/ide/integrations/llamaindex/tests/__init__.py +0 -3
  247. package/ide/integrations/llamaindex/tests/test_chat_store.py +0 -294
  248. package/ide/integrations/llamaindex/tests/test_security.py +0 -241
  249. package/plugin-src/.mcp.json +0 -12
  250. package/plugin-src/agents/slm-memory-advisor.md +0 -44
  251. package/plugin-src/agents/slm-optimize-advisor.md +0 -38
  252. package/plugin-src/hooks/.gitkeep +0 -0
  253. package/plugin-src/hooks/hooks.json +0 -23
  254. package/plugin-src/manifest.json +0 -25
  255. package/plugin-src/requirements.txt +0 -1
  256. package/plugin-src/rules/CLAUDE.md.fragment +0 -44
  257. package/plugin-src/scripts/ensure-venv.bat +0 -122
  258. package/plugin-src/scripts/ensure-venv.sh +0 -105
  259. package/plugin-src/scripts/slm-launch +0 -15
  260. package/plugin-src/scripts/slm-launch.bat +0 -17
  261. package/plugin-src/settings.json +0 -16
  262. package/plugin-src/skills/slm-cache/SKILL.md +0 -140
  263. package/plugin-src/skills/slm-compress/SKILL.md +0 -143
  264. package/plugin-src/skills/slm-graph/SKILL.md +0 -300
  265. package/plugin-src/skills/slm-recall/SKILL.md +0 -204
  266. package/plugin-src/skills/slm-remember/SKILL.md +0 -194
  267. package/plugin-src/skills/slm-session/SKILL.md +0 -207
  268. package/plugin-src/skills/slm-status/SKILL.md +0 -149
  269. package/scripts/__tests__/build-plugin.test.mjs +0 -613
  270. package/scripts/_savings_math.py +0 -270
  271. package/scripts/build-dmg.sh +0 -417
  272. package/scripts/build-plugin.js +0 -742
  273. package/scripts/build-slm-hook.ps1 +0 -40
  274. package/scripts/build-slm-hook.sh +0 -45
  275. package/scripts/build_entry.py +0 -452
  276. package/scripts/ci/stage5b_gate.sh +0 -50
  277. package/scripts/dogfood_savings.py +0 -490
  278. package/scripts/generate-thumbnails.py +0 -218
  279. package/scripts/install-skills.ps1 +0 -4
  280. package/scripts/install-skills.sh +0 -5
  281. package/scripts/install.ps1 +0 -701
  282. package/scripts/install.sh +0 -1015
  283. package/scripts/postinstall_binary.js +0 -287
  284. package/scripts/prepack.js +0 -33
  285. package/scripts/release_manifest.py +0 -273
  286. package/scripts/slm-hook.spec +0 -56
  287. package/scripts/start-dashboard.ps1 +0 -52
  288. package/scripts/start-dashboard.sh +0 -41
  289. package/scripts/sync-wiki.ps1 +0 -127
  290. package/scripts/sync-wiki.sh +0 -82
  291. package/scripts/test-dmg.sh +0 -161
  292. package/scripts/test-npm-package.ps1 +0 -252
  293. package/scripts/test-npm-package.sh +0 -207
  294. package/scripts/verify-install.ps1 +0 -294
  295. package/scripts/verify-install.sh +0 -266
  296. package/scripts/verify-v27.ps1 +0 -301
  297. package/scripts/verify-v27.sh +0 -233
  298. package/src/superlocalmemory.egg-info/PKG-INFO +0 -513
  299. package/src/superlocalmemory.egg-info/SOURCES.txt +0 -529
  300. package/src/superlocalmemory.egg-info/dependency_links.txt +0 -1
  301. package/src/superlocalmemory.egg-info/entry_points.txt +0 -2
  302. package/src/superlocalmemory.egg-info/requires.txt +0 -71
  303. package/src/superlocalmemory.egg-info/top_level.txt +0 -1
@@ -51,6 +51,62 @@ class CozoDBQueryError(CozoDBError):
51
51
  """Datalog query execution failed."""
52
52
 
53
53
 
54
+ class _CozoRows:
55
+ """Small pandas-values compatible view for PyCozo's dict results."""
56
+
57
+ def __init__(self, rows: list[list[Any]]) -> None:
58
+ self._rows = rows
59
+
60
+ def tolist(self) -> list[list[Any]]:
61
+ return self._rows
62
+
63
+
64
+ class _CozoResult:
65
+ """Normalize old PyCozo dict responses to the dataframe surface we use."""
66
+
67
+ def __init__(self, result: Any) -> None:
68
+ self._result = result
69
+ self.values = _CozoRows(list(result.get("rows", []))) if isinstance(result, dict) else result.values
70
+
71
+ def __len__(self) -> int:
72
+ return len(self.values.tolist())
73
+
74
+
75
+ class _CozoClientAdapter:
76
+ """Bridge PyCozo 0.3 embedded bindings and later client conveniences.
77
+
78
+ PyCozo 0.3 is the last client compatible with the published macOS native
79
+ binding. It returns dictionaries and exposes ``import_relations`` rather
80
+ than ``put``; later clients return dataframe-like values and add ``put``.
81
+ SLM only needs relation upserts and row results, so normalize those here.
82
+ """
83
+
84
+ def __init__(self, client: Any) -> None:
85
+ self._client = client
86
+
87
+ def run(self, script: str, params: dict[str, Any] | None = None) -> Any:
88
+ result = self._client.run(script) if params is None else self._client.run(script, params)
89
+ return _CozoResult(result) if isinstance(result, dict) else result
90
+
91
+ def put(self, relation: str, rows: list[dict[str, Any]]) -> None:
92
+ if not rows:
93
+ return
94
+ put = getattr(self._client, "put", None)
95
+ if callable(put):
96
+ put(relation, rows)
97
+ return
98
+ headers = list(rows[0])
99
+ self._client.import_relations({
100
+ relation: {
101
+ "headers": headers,
102
+ "rows": [[row.get(header) for header in headers] for row in rows],
103
+ },
104
+ })
105
+
106
+ def close(self) -> None:
107
+ self._client.close()
108
+
109
+
54
110
  # ---------------------------------------------------------------------------
55
111
  # CozoDBGraphBackend
56
112
  # ---------------------------------------------------------------------------
@@ -70,7 +126,11 @@ class CozoDBGraphBackend:
70
126
  path = Path(db_path)
71
127
  path.parent.mkdir(parents=True, exist_ok=True)
72
128
  self._db_path = str(path)
73
- self._db = _CozoClient("rocksdb", self._db_path) # type: ignore[misc]
129
+ client = _CozoClient("rocksdb", self._db_path, dataframe=False) # type: ignore[misc]
130
+ self._db = _CozoClientAdapter(client)
131
+ self._shadow_checks = 0
132
+ self._shadow_mismatches = 0
133
+ self._shadow_errors = 0
74
134
  self._ensure_schema()
75
135
 
76
136
  def close(self) -> None:
@@ -100,8 +160,8 @@ class CozoDBGraphBackend:
100
160
  try:
101
161
  self._db.run("""
102
162
  :create edge {
103
- from_id: String, to_id: String =>
104
- edge_type: String, weight: Float default 1.0,
163
+ from_id: String, to_id: String, edge_type: String =>
164
+ weight: Float default 1.0,
105
165
  metadata: String default '{}',
106
166
  profile_id: String default 'default',
107
167
  created_at: String
@@ -110,6 +170,21 @@ class CozoDBGraphBackend:
110
170
  except Exception:
111
171
  pass
112
172
 
173
+ # The entity recall channel resolves a query to canonical entity IDs,
174
+ # while graph_edges links fact IDs. Keeping those relations separate
175
+ # is essential: treating fact IDs as entities produces a healthy but
176
+ # semantically incompatible graph. ``fact_entity`` is the bridge
177
+ # that lets Cozo traverse the same two spaces as the SQLite channel.
178
+ try:
179
+ self._db.run("""
180
+ :create fact_entity {
181
+ fact_id: String, entity_id: String =>
182
+ profile_id: String default 'default'
183
+ }
184
+ """)
185
+ except Exception:
186
+ pass
187
+
113
188
  # ------------------------------------------------------------------
114
189
  # Write Path
115
190
  # ------------------------------------------------------------------
@@ -158,6 +233,60 @@ class CozoDBGraphBackend:
158
233
  "created_at": now,
159
234
  }])
160
235
 
236
+ def add_fact_entities(
237
+ self,
238
+ fact_id: str,
239
+ entity_ids: list[str],
240
+ profile_id: str = "default",
241
+ ) -> None:
242
+ """Upsert the canonical entities attached to one fact.
243
+
244
+ This is deliberately a separate relation from ``edge``. Fact edges
245
+ and canonical entity IDs are different namespaces in SLM.
246
+ """
247
+ self._db.put("fact_entity", [
248
+ {"fact_id": fact_id, "entity_id": entity_id, "profile_id": profile_id}
249
+ for entity_id in dict.fromkeys(entity_ids)
250
+ if entity_id
251
+ ])
252
+
253
+ def remove_fact(self, fact_id: str) -> None:
254
+ """Remove a fact's derived graph records using bound query values."""
255
+ # Cozo :rm needs every non-key column in the output relation. Binding
256
+ # ``fact_id`` keeps apostrophes and Datalog syntax in external IDs from
257
+ # becoming executable query text.
258
+ self._db.run("""
259
+ ?[fact_id, entity_id, profile_id] :=
260
+ *fact_entity{fact_id, entity_id, profile_id}, fact_id = $fact_id
261
+ :rm fact_entity {fact_id, entity_id => profile_id}
262
+ """, {"fact_id": fact_id})
263
+
264
+ def record_shadow_comparison(
265
+ self,
266
+ *,
267
+ matches: bool,
268
+ projected: list[tuple[str, float]],
269
+ canonical: list[tuple[str, float]],
270
+ ) -> None:
271
+ """Retain aggregate parity telemetry without persisting recalled text."""
272
+ self._shadow_checks += 1
273
+ if not matches:
274
+ self._shadow_mismatches += 1
275
+ logger.warning(
276
+ "Cozo entity recall diverged from canonical SQLite; using SQLite "
277
+ "(projected=%d canonical=%d)", len(projected), len(canonical),
278
+ )
279
+
280
+ def record_shadow_error(self, error: str) -> None:
281
+ self._shadow_errors += 1
282
+ logger.warning("Cozo entity recall failed closed to SQLite: %s", error)
283
+ self._db.run("""
284
+ ?[from_id, to_id, edge_type, weight, metadata, profile_id, created_at] :=
285
+ *edge{from_id, to_id, edge_type, weight, metadata, profile_id, created_at},
286
+ (from_id = $fact_id or to_id = $fact_id)
287
+ :rm edge {from_id, to_id, edge_type => weight, metadata, profile_id, created_at}
288
+ """, {"fact_id": fact_id})
289
+
161
290
  # ------------------------------------------------------------------
162
291
  # Bulk Import (SQLite → CozoDB)
163
292
  # ------------------------------------------------------------------
@@ -178,37 +307,57 @@ class CozoDBGraphBackend:
178
307
  if tier_filter is None:
179
308
  tier_filter = ["active", "warm"]
180
309
 
181
- # Step 1: Export all unique node IDs from graph_edges as entities.
182
- # graph_edges uses fact IDs as nodes. canonical_entities uses separate entity IDs.
183
- # CozoDB graph mirrors the graph_edges adjacency — node = fact ID.
310
+ # Step 1: Export canonical entity records. Do *not* synthesize
311
+ # entities from graph_edges: those are fact IDs and belong to the
312
+ # separate fact graph relation below.
184
313
  entities_sql = """
185
- SELECT DISTINCT node_id FROM (
186
- SELECT source_id as node_id FROM graph_edges WHERE profile_id = ?
187
- UNION
188
- SELECT target_id as node_id FROM graph_edges WHERE profile_id = ?
189
- )
314
+ SELECT entity_id, canonical_name, entity_type, first_seen, last_seen, fact_count
315
+ FROM canonical_entities WHERE profile_id = ?
190
316
  """
191
- rows = conn.execute(entities_sql, (profile_id, profile_id)).fetchall()
317
+ rows = conn.execute(entities_sql, (profile_id,)).fetchall()
192
318
 
193
319
  entity_dicts = []
194
320
  now = datetime.now().isoformat()
195
- for (nid,) in rows:
321
+ for entity_id, name, entity_type, first_seen, last_seen, fact_count in rows:
196
322
  entity_dicts.append({
197
- "id": nid,
198
- "name": nid[:12],
199
- "entity_type": "fact_node",
323
+ "id": entity_id,
324
+ "name": name,
325
+ "entity_type": entity_type or "concept",
200
326
  "tier": "active",
201
- "properties": "{}",
327
+ "properties": json.dumps({"fact_count": int(fact_count or 0)}),
202
328
  "profile_id": profile_id,
203
- "created_at": now,
204
- "updated_at": now,
329
+ "created_at": first_seen or now,
330
+ "updated_at": last_seen or now,
205
331
  })
206
332
 
207
333
  if entity_dicts:
208
334
  self._db.put("entity", entity_dicts)
209
335
  logger.info("CozoDB: imported %d entities", len(entity_dicts))
210
336
 
211
- # Step 2: Export edges directly (source_id/target_id are fact IDs)
337
+ # Step 2: Export fact-to-canonical-entity mappings. This relation is
338
+ # what allows a canonical query seed to enter the fact graph.
339
+ facts_sql = """
340
+ SELECT fact_id, canonical_entities_json
341
+ FROM atomic_facts WHERE profile_id = ?
342
+ """
343
+ fact_entity_dicts: list[dict[str, str]] = []
344
+ for fact_id, raw_entities in conn.execute(facts_sql, (profile_id,)).fetchall():
345
+ try:
346
+ entity_ids = json.loads(raw_entities or "[]")
347
+ except (TypeError, ValueError, json.JSONDecodeError):
348
+ entity_ids = []
349
+ for entity_id in dict.fromkeys(entity_ids):
350
+ if entity_id:
351
+ fact_entity_dicts.append({
352
+ "fact_id": fact_id,
353
+ "entity_id": str(entity_id),
354
+ "profile_id": profile_id,
355
+ })
356
+ if fact_entity_dicts:
357
+ self._db.put("fact_entity", fact_entity_dicts)
358
+
359
+ # Step 3: Export fact graph edges directly. Fact graph traversal is
360
+ # intentionally kept in its native fact-ID namespace.
212
361
  edges_sql = """
213
362
  SELECT source_id, target_id, edge_type, weight
214
363
  FROM graph_edges WHERE profile_id = ?
@@ -234,6 +383,83 @@ class CozoDBGraphBackend:
234
383
 
235
384
  return len(edge_dicts)
236
385
 
386
+ def recall_facts(
387
+ self,
388
+ seed_entity_ids: list[str],
389
+ *,
390
+ profile_id: str = "default",
391
+ depth: int = 4,
392
+ decay: float = 0.7,
393
+ threshold: float = 0.05,
394
+ top_k: int = 50,
395
+ ) -> list[tuple[str, float]]:
396
+ """Mirror SLM's entity-to-fact/fact-graph activation in Cozo storage.
397
+
398
+ Query values never enter Datalog source. Cozo is used as the durable
399
+ projection; activation runs in Python so the algorithm stays aligned
400
+ with the SQLite in-memory channel and can be shadow-compared exactly.
401
+ """
402
+ if not seed_entity_ids:
403
+ return []
404
+ entity_rows = self._db.run(
405
+ "?[fact_id, entity_id] := *fact_entity{fact_id, entity_id, profile_id}, profile_id = $profile_id",
406
+ {"profile_id": profile_id},
407
+ )
408
+ edge_rows = self._db.run(
409
+ "?[from_id, to_id, weight] := *edge{from_id, to_id, weight, profile_id}, profile_id = $profile_id",
410
+ {"profile_id": profile_id},
411
+ )
412
+ entity_to_facts: dict[str, list[str]] = {}
413
+ fact_to_entities: dict[str, list[str]] = {}
414
+ for fact_id, entity_id in entity_rows.values.tolist() if len(entity_rows) else []:
415
+ entity_to_facts.setdefault(str(entity_id), []).append(str(fact_id))
416
+ fact_to_entities.setdefault(str(fact_id), []).append(str(entity_id))
417
+ adjacency: dict[str, list[tuple[str, float]]] = {}
418
+ for source_id, target_id, weight in edge_rows.values.tolist() if len(edge_rows) else []:
419
+ source, target = str(source_id), str(target_id)
420
+ # Match EntityGraphChannel: graph edges are bidirectional during
421
+ # activation even when stored as directed rows.
422
+ adjacency.setdefault(source, []).append((target, float(weight)))
423
+ adjacency.setdefault(target, []).append((source, float(weight)))
424
+
425
+ activation: dict[str, float] = {}
426
+ visited_entities = set(seed_entity_ids)
427
+ for entity_id in seed_entity_ids:
428
+ for fact_id in entity_to_facts.get(entity_id, ()):
429
+ activation[fact_id] = max(activation.get(fact_id, 0.0), 1.0)
430
+ frontier = set(activation)
431
+ for hop in range(1, depth):
432
+ hop_decay = decay ** hop
433
+ if hop_decay < threshold:
434
+ break
435
+ next_frontier: set[str] = set()
436
+ for fact_id in frontier:
437
+ for neighbor_id, _weight in adjacency.get(fact_id, ()):
438
+ # SQLite intentionally ignores edge weights when graph
439
+ # metrics are unavailable; use that same baseline here.
440
+ score = activation[fact_id] * decay
441
+ if score >= threshold and score > activation.get(neighbor_id, 0.0):
442
+ activation[neighbor_id] = score
443
+ next_frontier.add(neighbor_id)
444
+ for fact_id in frontier:
445
+ for entity_id in fact_to_entities.get(fact_id, ()):
446
+ if entity_id in visited_entities:
447
+ continue
448
+ visited_entities.add(entity_id)
449
+ for related_fact_id in entity_to_facts.get(entity_id, ()):
450
+ if hop_decay > activation.get(related_fact_id, 0.0):
451
+ activation[related_fact_id] = hop_decay
452
+ next_frontier.add(related_fact_id)
453
+ frontier = next_frontier
454
+ if not frontier:
455
+ break
456
+ results = [(fact_id, score) for fact_id, score in activation.items() if score >= threshold]
457
+ if not results:
458
+ return []
459
+ maximum = max(score for _, score in results)
460
+ results = [(fact_id, score / maximum) for fact_id, score in results]
461
+ return sorted(results, key=lambda item: item[1], reverse=True)[:top_k]
462
+
237
463
  # ------------------------------------------------------------------
238
464
  # Spreading Activation (Python BFS over CozoDB edges)
239
465
  # ------------------------------------------------------------------
@@ -267,10 +493,10 @@ class CozoDBGraphBackend:
267
493
  for entity_id in current_frontier:
268
494
  # Query all outgoing edges from this entity
269
495
  try:
270
- result = self._db.run(f"""
496
+ result = self._db.run("""
271
497
  ?[to_id, weight] :=
272
- *edge{{from_id: '{entity_id}', to_id, weight}}
273
- """)
498
+ *edge{from_id, to_id, weight}, from_id = $entity_id
499
+ """, {"entity_id": entity_id})
274
500
  df = result if hasattr(result, "values") else result
275
501
  if df is None or len(df) == 0:
276
502
  continue
@@ -497,6 +723,9 @@ class CozoDBGraphBackend:
497
723
  "status": "active",
498
724
  "entities": int(ec),
499
725
  "edges": int(edc),
726
+ "shadow_checks": self._shadow_checks,
727
+ "shadow_mismatches": self._shadow_mismatches,
728
+ "shadow_errors": self._shadow_errors,
500
729
  "db_path": self._db_path,
501
730
  }
502
731
  except Exception as exc:
@@ -522,6 +751,10 @@ class CozoDBGraphBackend:
522
751
  self._db.run("::remove edge")
523
752
  except Exception:
524
753
  pass
754
+ try:
755
+ self._db.run("::remove fact_entity")
756
+ except Exception:
757
+ pass
525
758
 
526
759
  self._ensure_schema()
527
760
  return self.bulk_import_from_sqlite(conn, profile_id)
@@ -36,7 +36,6 @@ import time
36
36
  from pathlib import Path
37
37
  from typing import IO, Optional
38
38
 
39
-
40
39
  # ---------------------------------------------------------------------------
41
40
  # Budget constants
42
41
  # ---------------------------------------------------------------------------
@@ -63,17 +62,19 @@ PERF_LOG_CHECK_EVERY: int = 256 # check size every N writes, not every write
63
62
 
64
63
 
65
64
  def slm_home() -> Path:
66
- """Return ``~/.superlocalmemory`` honouring ``SLM_HOME`` override.
65
+ """Return the canonical runtime-state root for outcome hooks.
67
66
 
68
- ``SLM_HOME`` exists solely so unit tests can isolate filesystem state.
69
- Production code sets nothing and falls back to the home-directory path.
67
+ The central resolver owns environment alias precedence. Keeping that
68
+ policy out of this hot-path module prevents hooks from silently selecting
69
+ a different database than the daemon or MCP server.
70
70
 
71
71
  SEC-M6 — first-creation chmod's the dir to 0700 so the audit marker
72
72
  in ``ram_lock.sem`` (``{pid}:{name}``) and session-state files are
73
73
  not world-readable on shared hosts.
74
74
  """
75
- override = os.environ.get("SLM_HOME", "").strip()
76
- base = Path(override) if override else (Path.home() / ".superlocalmemory")
75
+ from superlocalmemory.infra.data_root import canonical_data_root
76
+
77
+ base = canonical_data_root()
77
78
  try:
78
79
  if not base.exists():
79
80
  base.mkdir(parents=True, exist_ok=True)
@@ -266,7 +267,7 @@ _PERF_LOG_FD: Optional[IO[str]] = None
266
267
  _PERF_LOG_PATH: Optional[Path] = None
267
268
  # S9-W3 M-PERF-02: RLock (not Lock) so a reentrant acquire during
268
269
  # atexit shutdown — e.g. a handler that calls ``log_perf`` while
269
- # ``_perf_log_flush`` already holds the lock — does not deadlock
270
+ # ``close_perf_log`` already holds the lock — does not deadlock
270
271
  # the interpreter for the 30s graceful-shutdown timeout.
271
272
  _PERF_LOG_LOCK = threading.RLock()
272
273
  _PERF_LOG_WRITE_COUNT: int = 0 # SEC-M4 — rotation cadence counter
@@ -340,8 +341,12 @@ def _maybe_rotate_perf_log(path: Path) -> None:
340
341
  pass
341
342
 
342
343
 
343
- def _perf_log_flush() -> None:
344
- """Flush the cached perf log fd (atexit hook). Never raises."""
344
+ def close_perf_log() -> None:
345
+ """Flush and close the process-owned perf-log stream. Never raises.
346
+
347
+ Long-lived hosts and isolated tests can call this at their lifecycle
348
+ boundary; the atexit registration remains the final safety net.
349
+ """
345
350
  global _PERF_LOG_FD
346
351
  with _PERF_LOG_LOCK:
347
352
  fd = _PERF_LOG_FD
@@ -358,7 +363,12 @@ def _perf_log_flush() -> None:
358
363
  pass
359
364
 
360
365
 
361
- atexit.register(_perf_log_flush)
366
+ def _perf_log_flush() -> None:
367
+ """Backward-compatible private alias for existing daemon shutdown code."""
368
+ close_perf_log()
369
+
370
+
371
+ atexit.register(close_perf_log)
362
372
 
363
373
 
364
374
  #: S9-W3 C8: rotation flag set on hot path, drained on exit / next
@@ -407,7 +417,7 @@ def log_perf(hook_name: str, duration_ms: float, outcome: str) -> None:
407
417
 
408
418
  Best-effort: disk full / unwritable dir → silently skip. Uses a
409
419
  module-level append-only fd opened on first use and flushed on
410
- process exit via :func:`_perf_log_flush`.
420
+ process exit via :func:`close_perf_log`.
411
421
 
412
422
  S9-W3 C8: the rotation/rename/reopen workflow has moved OFF the
413
423
  hot-path lock. Previously every 256th call held the lock across
@@ -35,15 +35,11 @@ from superlocalmemory.hooks.adapter_base import (
35
35
  truncate_to_cap,
36
36
  )
37
37
  from superlocalmemory.hooks.context_payload import (
38
+ VERSION,
38
39
  ContextPayload,
39
40
  RecallFn,
40
- build_payload,
41
- format_decisions,
42
- format_entities,
43
- format_memories,
44
- format_topics,
45
- truncate_payload_for_cap,
46
41
  )
42
+ from superlocalmemory.hooks.memory_protocol import memory_protocol_markdown
47
43
 
48
44
  logger = logging.getLogger(__name__)
49
45
 
@@ -63,30 +59,21 @@ GLOBAL_REL = f".gemini/antigravity/{_SKILLS}/{_GLOBAL_SKILL_NAME}/SKILL.md"
63
59
  _FRONTMATTER = (
64
60
  "---\n"
65
61
  "name: slm-memory-adapter\n"
66
- "description: \"Surfaces SuperLocalMemory context "
67
- "(topics, entities, decisions) at the start of every "
68
- "Antigravity conversation.\"\n"
62
+ "description: \"SuperLocalMemory runtime MCP memory protocol.\"\n"
69
63
  "---\n"
70
64
  )
71
65
 
72
66
  _BODY_TEMPLATE = (
73
- "\n# SLM Memory Adapter\n\n"
74
- "_Auto-generated by SuperLocalMemory v{version}. Last sync: {generated_at}._\n\n"
75
- "## Preferences\n{topics}\n\n"
76
- "## Entities\n{entities}\n\n"
77
- "## Recent decisions\n{decisions}\n\n"
78
- "## Project memories\n{memories}\n"
67
+ "\n# SLM Runtime Memory Protocol\n\n"
68
+ "_Managed by SuperLocalMemory v{version}. This skill contains no recalled memory._\n\n"
69
+ "{protocol}"
79
70
  )
80
71
 
81
72
 
82
- def render_antigravity(payload: ContextPayload) -> bytes:
73
+ def render_antigravity(payload: ContextPayload | None = None) -> bytes:
83
74
  body = _BODY_TEMPLATE.format(
84
- version=payload.version,
85
- generated_at=payload.generated_at,
86
- topics=format_topics(payload),
87
- entities=format_entities(payload),
88
- decisions=format_decisions(payload),
89
- memories=format_memories(payload),
75
+ version=payload.version if payload is not None else VERSION,
76
+ protocol=memory_protocol_markdown(),
90
77
  )
91
78
  return (_FRONTMATTER + body).encode("utf-8")
92
79
 
@@ -144,15 +131,7 @@ class AntigravityAdapter:
144
131
  self._inactive_until_retry = True
145
132
  return False
146
133
 
147
- # For builder purposes, treat workspace scope as project.
148
- scope_for_builder = "project" if self._scope == "workspace" else "global"
149
- payload = build_payload(
150
- self._profile_id, scope_for_builder, self._base_dir,
151
- recall_fn=self._recall_fn,
152
- )
153
- rendered = truncate_payload_for_cap(
154
- payload, hard_cap=HARD_BYTES_CAP, render=render_antigravity,
155
- )
134
+ rendered = render_antigravity()
156
135
  rendered = truncate_to_cap(rendered, cap=HARD_BYTES_CAP)
157
136
 
158
137
  result: WriteResult = atomic_write(
@@ -21,11 +21,11 @@ Part of Qualixar | Author: Varun Pratap Bhardwaj
21
21
 
22
22
  from __future__ import annotations
23
23
 
24
- import json
25
24
  import logging
26
25
  import math
27
26
 
28
27
  from superlocalmemory.core.config import AutoInvokeConfig
28
+ from superlocalmemory.core.injection import InjectableMemory, render_context
29
29
 
30
30
  logger = logging.getLogger(__name__)
31
31
 
@@ -90,16 +90,19 @@ class AutoInvoker:
90
90
  limit=self._config.max_memories_injected,
91
91
  )
92
92
 
93
- memory_context = self.format_for_injection(results) if results else ""
94
-
95
- # V3.3: Inject soft prompts (priority over memory context)
96
93
  soft_prompt_text = self._get_soft_prompt_text()
97
- if soft_prompt_text and self._prompt_injector is not None:
98
- return self._prompt_injector.inject_into_context(
99
- soft_prompt_text, memory_context,
100
- )
101
-
102
- return soft_prompt_text + ("\n\n" + memory_context if memory_context else "") if soft_prompt_text else memory_context
94
+ if soft_prompt_text:
95
+ results = [
96
+ {
97
+ "fact_id": "",
98
+ "content": soft_prompt_text,
99
+ "fact_type": "behavioral-pattern",
100
+ "score": 0.0,
101
+ "contextual_description": "",
102
+ },
103
+ *results,
104
+ ]
105
+ return self.format_for_injection(results) if results else ""
103
106
  except Exception as exc:
104
107
  logger.debug("Auto-invoke failed: %s", exc)
105
108
  return ""
@@ -476,29 +479,24 @@ class AutoInvoker:
476
479
  # ------------------------------------------------------------------
477
480
 
478
481
  def format_for_injection(self, results: list[dict]) -> str:
479
- """Format results for system prompt injection.
480
-
481
- Output: Markdown list with content previews and context.
482
- """
482
+ """Format results as bounded, untrusted evidence with provenance."""
483
483
  if not results:
484
484
  return ""
485
485
 
486
- lines = ["# Relevant Memory Context", ""]
486
+ memories: list[InjectableMemory] = []
487
487
  for r in results:
488
- content_preview = r["content"][:200]
488
+ content = str(r.get("content", ""))
489
489
  ctx = r.get("contextual_description", "")
490
-
491
- line = f"- [{r['fact_type']}] {content_preview}"
492
490
  if ctx:
493
- line += f"\n > Context: {ctx}"
494
- lines.append(line)
495
-
496
- lines.append("")
497
- lines.append(
498
- f"_Auto-invoked {len(results)} memories "
499
- f"(FOK >= {self._config.fok_threshold})_"
500
- )
501
- return "\n".join(lines)
491
+ content += f"\nContext: {ctx}"
492
+ memories.append(InjectableMemory(
493
+ content=content,
494
+ score=float(r.get("score", 0.0) or 0.0),
495
+ fact_id=str(r.get("fact_id", "")),
496
+ source_type=str(r.get("fact_type", "auto-invoke")),
497
+ source_id=f"fok-threshold:{self._config.fok_threshold}",
498
+ ))
499
+ return render_context(memories, mode="B", cfg=None, wrap=True)
502
500
 
503
501
  # ------------------------------------------------------------------
504
502
  # V3.3: Soft prompt injection
@@ -9,6 +9,8 @@ from __future__ import annotations
9
9
  import logging
10
10
  from typing import Any, Callable
11
11
 
12
+ from superlocalmemory.core.injection import InjectableMemory, render_context
13
+
12
14
  logger = logging.getLogger(__name__)
13
15
 
14
16
 
@@ -72,12 +74,18 @@ class AutoRecall:
72
74
  if not relevant:
73
75
  return ""
74
76
 
75
- # Format for injection
76
- lines = ["# Relevant Memory Context", ""]
77
- for r in relevant[:self._max_memories]:
78
- lines.append(f"- {r.fact.content[:200]}")
79
-
80
- return "\n".join(lines)
77
+ memories = [
78
+ InjectableMemory(
79
+ content=r.fact.content,
80
+ score=float(r.score),
81
+ fact_id=str(r.fact.fact_id),
82
+ importance=float(getattr(r.fact, "importance", 0.0) or 0.0),
83
+ access_count=int(getattr(r.fact, "access_count", 0) or 0),
84
+ source_type="recall",
85
+ )
86
+ for r in relevant[:self._max_memories]
87
+ ]
88
+ return render_context(memories, mode="B", cfg=None, wrap=True)
81
89
  except Exception as exc:
82
90
  logger.warning("Auto-recall failed: %s", exc)
83
91
  return ""
@@ -103,6 +111,23 @@ class AutoRecall:
103
111
  "fact_id": r.fact.fact_id,
104
112
  "content": r.fact.content[:300],
105
113
  "score": round(r.score, 3),
114
+ "relevance_score": round(
115
+ getattr(r, "relevance_score", r.score) or 0.0, 3
116
+ ),
117
+ "ranking_score": getattr(r, "ranking_score", None),
118
+ "confidence": round(
119
+ getattr(
120
+ r, "memory_confidence",
121
+ getattr(r, "confidence", 0.0),
122
+ ) or 0.0, 3
123
+ ),
124
+ "memory_confidence": round(
125
+ getattr(
126
+ r, "memory_confidence",
127
+ getattr(r, "confidence", 0.0),
128
+ ) or 0.0, 3
129
+ ),
130
+ "rank_position": int(getattr(r, "rank_position", 0) or 0),
106
131
  })
107
132
  return results
108
133
  except Exception as exc: