superlocalmemory 3.6.23 → 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 (301) hide show
  1. package/CHANGELOG.md +52 -0
  2. package/README.md +271 -71
  3. package/bin/slm-npm +43 -89
  4. package/ide/configs/antigravity-mcp.json +2 -2
  5. package/ide/configs/chatgpt-desktop-mcp.json +1 -1
  6. package/ide/configs/claude-desktop-mcp.json +2 -2
  7. package/ide/configs/windsurf-mcp.json +2 -2
  8. package/ide/hooks/context-hook.js +6 -2
  9. package/ide/hooks/post-recall-hook.js +7 -3
  10. package/ide/hooks/tool-event-hook.sh +2 -1
  11. package/package.json +18 -10
  12. package/plugin/.claude-plugin/plugin.json +1 -1
  13. package/plugin/_GENERATED.md +1 -1
  14. package/plugin/agents/slm-memory-advisor.md +1 -1
  15. package/plugin/requirements.txt +1 -1
  16. package/plugin/skills/slm-session/SKILL.md +1 -1
  17. package/plugin-src/rules/AGENTS.md +1 -1
  18. package/pyproject.toml +40 -8
  19. package/scripts/postinstall-interactive.js +17 -94
  20. package/scripts/postinstall.js +185 -258
  21. package/scripts/preuninstall.js +9 -50
  22. package/src/superlocalmemory/__init__.py +2 -2
  23. package/src/superlocalmemory/attribution/mathematical_dna.py +1 -1
  24. package/src/superlocalmemory/attribution/signer.py +34 -19
  25. package/src/superlocalmemory/attribution/watermark.py +1 -1
  26. package/src/superlocalmemory/cli/_lazy_init.py +3 -5
  27. package/src/superlocalmemory/cli/commands.py +453 -191
  28. package/src/superlocalmemory/cli/context_commands.py +5 -4
  29. package/src/superlocalmemory/cli/daemon.py +282 -187
  30. package/src/superlocalmemory/cli/db_migrate.py +3 -1
  31. package/src/superlocalmemory/cli/diagnostics_cmd.py +28 -0
  32. package/src/superlocalmemory/cli/evidence_cmd.py +103 -0
  33. package/src/superlocalmemory/cli/ingest_cmd.py +7 -3
  34. package/src/superlocalmemory/cli/main.py +128 -31
  35. package/src/superlocalmemory/cli/pending_store.py +54 -38
  36. package/src/superlocalmemory/cli/scale_engine_cmd.py +37 -0
  37. package/src/superlocalmemory/cli/service_installer.py +57 -52
  38. package/src/superlocalmemory/cli/setup_wizard.py +142 -88
  39. package/src/superlocalmemory/cli/version_banner.py +2 -1
  40. package/src/superlocalmemory/code_graph/config.py +3 -1
  41. package/src/superlocalmemory/core/backend_orchestrator.py +81 -21
  42. package/src/superlocalmemory/core/config.py +65 -20
  43. package/src/superlocalmemory/core/consolidation_engine.py +9 -7
  44. package/src/superlocalmemory/core/context_cache.py +56 -8
  45. package/src/superlocalmemory/core/derivation_lineage.py +246 -0
  46. package/src/superlocalmemory/core/embedding_worker.py +32 -20
  47. package/src/superlocalmemory/core/embeddings.py +54 -18
  48. package/src/superlocalmemory/core/engine.py +150 -104
  49. package/src/superlocalmemory/core/engine_ingestion.py +513 -0
  50. package/src/superlocalmemory/core/engine_wiring.py +2 -0
  51. package/src/superlocalmemory/core/evidence_bundle.py +526 -0
  52. package/src/superlocalmemory/core/fact_consolidator.py +5 -11
  53. package/src/superlocalmemory/core/graph_analyzer.py +2 -2
  54. package/src/superlocalmemory/core/health_monitor.py +4 -2
  55. package/src/superlocalmemory/core/ingestion_command.py +636 -0
  56. package/src/superlocalmemory/core/injection.py +69 -18
  57. package/src/superlocalmemory/core/lifecycle_state.py +153 -0
  58. package/src/superlocalmemory/core/maintenance.py +1 -1
  59. package/src/superlocalmemory/core/maintenance_scheduler.py +51 -35
  60. package/src/superlocalmemory/core/mutations.py +143 -0
  61. package/src/superlocalmemory/core/platform_utils.py +7 -4
  62. package/src/superlocalmemory/core/ram_lock.py +16 -5
  63. package/src/superlocalmemory/core/rate_limit.py +1 -1
  64. package/src/superlocalmemory/core/recall_pipeline.py +60 -101
  65. package/src/superlocalmemory/core/recall_worker.py +76 -59
  66. package/src/superlocalmemory/core/registry.py +1 -1
  67. package/src/superlocalmemory/core/scale_engine.py +293 -0
  68. package/src/superlocalmemory/core/score_contract.py +62 -0
  69. package/src/superlocalmemory/core/security_primitives.py +3 -1
  70. package/src/superlocalmemory/core/slm_disabled.py +3 -5
  71. package/src/superlocalmemory/core/store_pipeline.py +172 -40
  72. package/src/superlocalmemory/core/tier_manager.py +32 -20
  73. package/src/superlocalmemory/core/worker_pool.py +13 -4
  74. package/src/superlocalmemory/dynamics/activation_guided_quantization.py +1 -1
  75. package/src/superlocalmemory/dynamics/eap_scheduler.py +10 -3
  76. package/src/superlocalmemory/dynamics/ebbinghaus_langevin_coupling.py +1 -1
  77. package/src/superlocalmemory/dynamics/fisher_langevin_coupling.py +1 -1
  78. package/src/superlocalmemory/encoding/auto_linker.py +1 -1
  79. package/src/superlocalmemory/encoding/cognitive_consolidator.py +7 -16
  80. package/src/superlocalmemory/encoding/consolidator.py +22 -5
  81. package/src/superlocalmemory/encoding/fact_extractor.py +1 -1
  82. package/src/superlocalmemory/encoding/foresight.py +2 -0
  83. package/src/superlocalmemory/encoding/graph_builder.py +1 -1
  84. package/src/superlocalmemory/encoding/temporal_parser.py +2 -0
  85. package/src/superlocalmemory/evaluation/__init__.py +13 -0
  86. package/src/superlocalmemory/evaluation/calibration.py +308 -0
  87. package/src/superlocalmemory/evolution/skill_evolver.py +2 -1
  88. package/src/superlocalmemory/graph/cozo_backend.py +256 -23
  89. package/src/superlocalmemory/hooks/_outcome_common.py +21 -11
  90. package/src/superlocalmemory/hooks/antigravity_adapter.py +10 -31
  91. package/src/superlocalmemory/hooks/auto_invoker.py +25 -27
  92. package/src/superlocalmemory/hooks/auto_recall.py +31 -6
  93. package/src/superlocalmemory/hooks/auto_recall_hook.py +13 -33
  94. package/src/superlocalmemory/hooks/before_web_hook.py +9 -7
  95. package/src/superlocalmemory/hooks/claude_code_hooks.py +123 -35
  96. package/src/superlocalmemory/hooks/codex_assets.py +59 -0
  97. package/src/superlocalmemory/hooks/codex_hooks.py +186 -0
  98. package/src/superlocalmemory/hooks/context_payload.py +1 -1
  99. package/src/superlocalmemory/hooks/copilot_adapter.py +9 -24
  100. package/src/superlocalmemory/hooks/cursor_adapter.py +10 -32
  101. package/src/superlocalmemory/hooks/hook_daemon.py +4 -2
  102. package/src/superlocalmemory/hooks/hook_handlers.py +219 -32
  103. package/src/superlocalmemory/hooks/memory_protocol.py +5 -3
  104. package/src/superlocalmemory/hooks/post_tool_async_hook.py +4 -2
  105. package/src/superlocalmemory/hooks/session_registry.py +15 -8
  106. package/src/superlocalmemory/hooks/stop_outcome_hook.py +10 -6
  107. package/src/superlocalmemory/hooks/topic_shift_hook.py +42 -12
  108. package/src/superlocalmemory/hooks/user_prompt_hook.py +9 -14
  109. package/src/superlocalmemory/hooks/user_prompt_rehash_hook.py +19 -11
  110. package/src/superlocalmemory/infra/auth_middleware.py +38 -5
  111. package/src/superlocalmemory/infra/backup.py +7 -5
  112. package/src/superlocalmemory/infra/cloud_backup.py +18 -8
  113. package/src/superlocalmemory/infra/daemon_identity.py +248 -0
  114. package/src/superlocalmemory/infra/data_root.py +199 -0
  115. package/src/superlocalmemory/infra/event_bus.py +3 -1
  116. package/src/superlocalmemory/infra/local_diagnostics.py +327 -0
  117. package/src/superlocalmemory/infra/process_reaper.py +23 -0
  118. package/src/superlocalmemory/ingestion/adapter_manager.py +27 -9
  119. package/src/superlocalmemory/ingestion/base_adapter.py +25 -31
  120. package/src/superlocalmemory/ingestion/calendar_adapter.py +13 -4
  121. package/src/superlocalmemory/ingestion/credentials.py +14 -7
  122. package/src/superlocalmemory/ingestion/gmail_adapter.py +13 -4
  123. package/src/superlocalmemory/ingestion/transcript_adapter.py +7 -2
  124. package/src/superlocalmemory/learning/consolidation_quantization_worker.py +1 -1
  125. package/src/superlocalmemory/learning/ensemble.py +11 -0
  126. package/src/superlocalmemory/learning/entity_compiler.py +1 -1
  127. package/src/superlocalmemory/learning/feedback.py +1 -1
  128. package/src/superlocalmemory/learning/forgetting_scheduler.py +12 -7
  129. package/src/superlocalmemory/learning/quantization_scheduler.py +1 -1
  130. package/src/superlocalmemory/learning/ranker.py +4 -1
  131. package/src/superlocalmemory/learning/source_quality.py +1 -1
  132. package/src/superlocalmemory/learning/trigram_index.py +3 -2
  133. package/src/superlocalmemory/llm/backbone.py +1 -1
  134. package/src/superlocalmemory/math/ebbinghaus.py +1 -1
  135. package/src/superlocalmemory/math/fisher.py +1 -1
  136. package/src/superlocalmemory/math/fisher_quantized.py +1 -1
  137. package/src/superlocalmemory/math/hopfield.py +1 -1
  138. package/src/superlocalmemory/math/langevin.py +1 -1
  139. package/src/superlocalmemory/math/polar_quant.py +3 -4
  140. package/src/superlocalmemory/math/qjl.py +1 -1
  141. package/src/superlocalmemory/math/sheaf.py +1 -1
  142. package/src/superlocalmemory/math/turbo_quant.py +3 -2
  143. package/src/superlocalmemory/mcp/_daemon_proxy.py +12 -11
  144. package/src/superlocalmemory/mcp/_pool_adapter.py +27 -0
  145. package/src/superlocalmemory/mcp/http_transport.py +53 -0
  146. package/src/superlocalmemory/mcp/server.py +39 -13
  147. package/src/superlocalmemory/mcp/shared.py +69 -3
  148. package/src/superlocalmemory/mcp/tools_active.py +141 -31
  149. package/src/superlocalmemory/mcp/tools_core.py +128 -29
  150. package/src/superlocalmemory/mcp/tools_evolution.py +5 -7
  151. package/src/superlocalmemory/mcp/tools_learning.py +42 -2
  152. package/src/superlocalmemory/mcp/tools_mesh.py +7 -23
  153. package/src/superlocalmemory/mcp/tools_optimize.py +8 -1
  154. package/src/superlocalmemory/mcp/tools_v28.py +23 -2
  155. package/src/superlocalmemory/mcp/tools_v3.py +26 -1
  156. package/src/superlocalmemory/mcp/tools_v33.py +56 -17
  157. package/src/superlocalmemory/mesh/broker.py +2 -0
  158. package/src/superlocalmemory/mesh/remote_sync.py +50 -12
  159. package/src/superlocalmemory/optimize/cache/manager.py +77 -1
  160. package/src/superlocalmemory/optimize/cache/semantic.py +23 -3
  161. package/src/superlocalmemory/optimize/compress/ccr.py +4 -0
  162. package/src/superlocalmemory/optimize/compress/router.py +6 -1
  163. package/src/superlocalmemory/optimize/config/__init__.py +5 -0
  164. package/src/superlocalmemory/optimize/config/store.py +6 -4
  165. package/src/superlocalmemory/optimize/proxy/_helpers.py +15 -5
  166. package/src/superlocalmemory/optimize/proxy/capture.py +3 -2
  167. package/src/superlocalmemory/optimize/proxy/server.py +2 -2
  168. package/src/superlocalmemory/optimize/storage/db.py +12 -12
  169. package/src/superlocalmemory/retrieval/agentic.py +1 -1
  170. package/src/superlocalmemory/retrieval/ann_index.py +1 -1
  171. package/src/superlocalmemory/retrieval/bm25_channel.py +35 -11
  172. package/src/superlocalmemory/retrieval/bridge_discovery.py +73 -8
  173. package/src/superlocalmemory/retrieval/engine.py +169 -79
  174. package/src/superlocalmemory/retrieval/entity_channel.py +289 -67
  175. package/src/superlocalmemory/retrieval/forgetting_filter.py +1 -1
  176. package/src/superlocalmemory/retrieval/fusion.py +1 -1
  177. package/src/superlocalmemory/retrieval/hopfield_channel.py +118 -30
  178. package/src/superlocalmemory/retrieval/profile_channel.py +1 -1
  179. package/src/superlocalmemory/retrieval/quantization_aware_search.py +16 -10
  180. package/src/superlocalmemory/retrieval/reranker.py +56 -20
  181. package/src/superlocalmemory/retrieval/scope_policy.py +85 -0
  182. package/src/superlocalmemory/retrieval/semantic_channel.py +122 -14
  183. package/src/superlocalmemory/retrieval/spreading_activation.py +141 -25
  184. package/src/superlocalmemory/retrieval/strategy.py +1 -1
  185. package/src/superlocalmemory/retrieval/temporal_channel.py +30 -15
  186. package/src/superlocalmemory/retrieval/vector_store.py +1 -1
  187. package/src/superlocalmemory/server/api.py +10 -7
  188. package/src/superlocalmemory/server/bandit_loops.py +4 -2
  189. package/src/superlocalmemory/server/recall_serializer.py +24 -0
  190. package/src/superlocalmemory/server/route_mutations.py +84 -0
  191. package/src/superlocalmemory/server/routes/agents.py +8 -6
  192. package/src/superlocalmemory/server/routes/brain.py +14 -12
  193. package/src/superlocalmemory/server/routes/chat.py +29 -12
  194. package/src/superlocalmemory/server/routes/data_io.py +55 -24
  195. package/src/superlocalmemory/server/routes/helpers.py +8 -63
  196. package/src/superlocalmemory/server/routes/ingest.py +53 -36
  197. package/src/superlocalmemory/server/routes/memories.py +104 -43
  198. package/src/superlocalmemory/server/routes/mesh.py +31 -0
  199. package/src/superlocalmemory/server/routes/profiles.py +26 -4
  200. package/src/superlocalmemory/server/routes/tiers.py +43 -11
  201. package/src/superlocalmemory/server/routes/timeline.py +5 -1
  202. package/src/superlocalmemory/server/routes/v3_api.py +76 -21
  203. package/src/superlocalmemory/server/security_middleware.py +1 -1
  204. package/src/superlocalmemory/server/unified_daemon.py +680 -293
  205. package/src/superlocalmemory/server/write_identity.py +147 -0
  206. package/src/superlocalmemory/storage/access_log.py +4 -3
  207. package/src/superlocalmemory/storage/database.py +118 -25
  208. package/src/superlocalmemory/storage/migration_runner.py +84 -1
  209. package/src/superlocalmemory/storage/migration_v33.py +1 -1
  210. package/src/superlocalmemory/storage/migrations/M002_model_state_history.py +6 -60
  211. package/src/superlocalmemory/storage/migrations/M018_ingestion_operations.py +120 -0
  212. package/src/superlocalmemory/storage/migrations/M019_derivation_lineage.py +54 -0
  213. package/src/superlocalmemory/storage/migrations/M020_model_state_integrity.py +52 -0
  214. package/src/superlocalmemory/storage/migrations/__init__.py +5 -0
  215. package/src/superlocalmemory/storage/models.py +16 -0
  216. package/src/superlocalmemory/storage/quantized_store.py +20 -3
  217. package/src/superlocalmemory/storage/v2_migrator.py +5 -3
  218. package/src/superlocalmemory/ui/favicon.svg +5 -0
  219. package/src/superlocalmemory/ui/index.html +1 -0
  220. package/src/superlocalmemory/ui/js/compliance.js +1 -1
  221. package/src/superlocalmemory/ui/js/core.js +49 -8
  222. package/src/superlocalmemory/ui/js/dashboard.js +23 -2
  223. package/src/superlocalmemory/ui/js/feedback.js +1 -1
  224. package/src/superlocalmemory/ui/js/graph-filters.js +1 -1
  225. package/src/superlocalmemory/ui/js/graph-ui.js +1 -1
  226. package/src/superlocalmemory/ui/js/lifecycle.js +1 -1
  227. package/src/superlocalmemory/ui/js/ng-mesh.js +15 -49
  228. package/src/superlocalmemory/ui/js/settings.js +4 -2
  229. package/src/superlocalmemory/vector/lancedb_backend.py +57 -9
  230. package/bin/slm +0 -59
  231. package/bin/slm.bat +0 -77
  232. package/bin/slm.cmd +0 -5
  233. package/ide/integrations/langchain/README.md +0 -106
  234. package/ide/integrations/langchain/langchain_superlocalmemory/__init__.py +0 -9
  235. package/ide/integrations/langchain/langchain_superlocalmemory/chat_message_history.py +0 -201
  236. package/ide/integrations/langchain/pyproject.toml +0 -38
  237. package/ide/integrations/langchain/tests/__init__.py +0 -3
  238. package/ide/integrations/langchain/tests/test_chat_message_history.py +0 -215
  239. package/ide/integrations/langchain/tests/test_security.py +0 -117
  240. package/ide/integrations/llamaindex/README.md +0 -81
  241. package/ide/integrations/llamaindex/llama_index/storage/chat_store/superlocalmemory/__init__.py +0 -9
  242. package/ide/integrations/llamaindex/llama_index/storage/chat_store/superlocalmemory/base.py +0 -316
  243. package/ide/integrations/llamaindex/pyproject.toml +0 -43
  244. package/ide/integrations/llamaindex/tests/__init__.py +0 -3
  245. package/ide/integrations/llamaindex/tests/test_chat_store.py +0 -294
  246. package/ide/integrations/llamaindex/tests/test_security.py +0 -241
  247. package/plugin-src/.mcp.json +0 -12
  248. package/plugin-src/agents/slm-memory-advisor.md +0 -44
  249. package/plugin-src/agents/slm-optimize-advisor.md +0 -38
  250. package/plugin-src/hooks/.gitkeep +0 -0
  251. package/plugin-src/hooks/hooks.json +0 -23
  252. package/plugin-src/manifest.json +0 -25
  253. package/plugin-src/requirements.txt +0 -1
  254. package/plugin-src/rules/CLAUDE.md.fragment +0 -44
  255. package/plugin-src/scripts/ensure-venv.bat +0 -122
  256. package/plugin-src/scripts/ensure-venv.sh +0 -105
  257. package/plugin-src/scripts/slm-launch +0 -15
  258. package/plugin-src/scripts/slm-launch.bat +0 -17
  259. package/plugin-src/settings.json +0 -16
  260. package/plugin-src/skills/slm-cache/SKILL.md +0 -140
  261. package/plugin-src/skills/slm-compress/SKILL.md +0 -143
  262. package/plugin-src/skills/slm-graph/SKILL.md +0 -300
  263. package/plugin-src/skills/slm-recall/SKILL.md +0 -204
  264. package/plugin-src/skills/slm-remember/SKILL.md +0 -194
  265. package/plugin-src/skills/slm-session/SKILL.md +0 -207
  266. package/plugin-src/skills/slm-status/SKILL.md +0 -149
  267. package/scripts/__tests__/build-plugin.test.mjs +0 -613
  268. package/scripts/_savings_math.py +0 -270
  269. package/scripts/build-dmg.sh +0 -417
  270. package/scripts/build-plugin.js +0 -742
  271. package/scripts/build-slm-hook.ps1 +0 -40
  272. package/scripts/build-slm-hook.sh +0 -45
  273. package/scripts/build_entry.py +0 -452
  274. package/scripts/ci/stage5b_gate.sh +0 -50
  275. package/scripts/dogfood_savings.py +0 -490
  276. package/scripts/generate-thumbnails.py +0 -218
  277. package/scripts/install-skills.ps1 +0 -4
  278. package/scripts/install-skills.sh +0 -5
  279. package/scripts/install.ps1 +0 -701
  280. package/scripts/install.sh +0 -1015
  281. package/scripts/postinstall_binary.js +0 -287
  282. package/scripts/prepack.js +0 -33
  283. package/scripts/release_manifest.py +0 -273
  284. package/scripts/slm-hook.spec +0 -56
  285. package/scripts/start-dashboard.ps1 +0 -52
  286. package/scripts/start-dashboard.sh +0 -41
  287. package/scripts/sync-wiki.ps1 +0 -127
  288. package/scripts/sync-wiki.sh +0 -82
  289. package/scripts/test-dmg.sh +0 -161
  290. package/scripts/test-npm-package.ps1 +0 -252
  291. package/scripts/test-npm-package.sh +0 -207
  292. package/scripts/verify-install.ps1 +0 -294
  293. package/scripts/verify-install.sh +0 -266
  294. package/scripts/verify-v27.ps1 +0 -301
  295. package/scripts/verify-v27.sh +0 -233
  296. package/src/superlocalmemory.egg-info/PKG-INFO +0 -516
  297. package/src/superlocalmemory.egg-info/SOURCES.txt +0 -529
  298. package/src/superlocalmemory.egg-info/dependency_links.txt +0 -1
  299. package/src/superlocalmemory.egg-info/entry_points.txt +0 -2
  300. package/src/superlocalmemory.egg-info/requires.txt +0 -71
  301. package/src/superlocalmemory.egg-info/top_level.txt +0 -1
@@ -12,6 +12,7 @@ Part of Qualixar | Author: Varun Pratap Bhardwaj
12
12
  from __future__ import annotations
13
13
 
14
14
  import logging
15
+ import json
15
16
  from typing import TYPE_CHECKING, Any
16
17
 
17
18
  if TYPE_CHECKING:
@@ -181,6 +182,13 @@ def run_store(
181
182
  auto_linker: Any = None,
182
183
  context_generator: Any = None,
183
184
  consolidation_engine: Any = None,
185
+ existing_memory_id: str | None = None,
186
+ queryable_fact_ids: tuple[str, ...] = (),
187
+ trusted_actor_id: str = "",
188
+ pre_authorized: bool = False,
189
+ ingestion_source_type: str = "store",
190
+ ingestion_operation_id: str = "",
191
+ derivation_report: dict[str, bool] | None = None,
184
192
  ) -> list[str]:
185
193
  """Store content and extract structured facts. Returns fact_ids.
186
194
 
@@ -190,11 +198,15 @@ def run_store(
190
198
  # Pre-operation hooks (trust gate, ABAC, rate limiter)
191
199
  hook_ctx = {
192
200
  "operation": "store",
193
- "agent_id": metadata.get("agent_id", "unknown") if metadata else "unknown",
201
+ "agent_id": (
202
+ trusted_actor_id
203
+ or (metadata.get("agent_id", "unknown") if metadata else "unknown")
204
+ ),
194
205
  "profile_id": profile_id,
195
206
  "content_preview": content[:100],
196
207
  }
197
- hooks.run_pre("store", hook_ctx)
208
+ if not pre_authorized:
209
+ hooks.run_pre("store", hook_ctx)
198
210
 
199
211
  if entropy_gate and not entropy_gate.should_pass(content):
200
212
  return []
@@ -216,19 +228,65 @@ def run_store(
216
228
  parser = temporal_parser or TemporalParser()
217
229
  parsed_date = parser.parse_session_date(session_date) if session_date else None
218
230
 
219
- record = MemoryRecord(
220
- profile_id=profile_id, content=content,
221
- session_id=session_id, speaker=speaker, role=role,
222
- session_date=parsed_date, metadata=metadata or {},
223
- scope=scope, shared_with=shared_with,
224
- )
225
- db.store_memory(record)
231
+ queryable_ids = frozenset(queryable_fact_ids)
232
+ queryable_facts: list[AtomicFact] = []
233
+ if existing_memory_id:
234
+ memory_rows = db.execute(
235
+ "SELECT * FROM memories WHERE memory_id=?",
236
+ (existing_memory_id,),
237
+ )
238
+ if not memory_rows:
239
+ raise ValueError("queryable ingestion memory does not exist")
240
+ memory = dict(memory_rows[0])
241
+ if memory["profile_id"] != profile_id:
242
+ raise ValueError("queryable ingestion memory profile mismatch")
243
+ if memory["content"] != content:
244
+ raise ValueError("queryable ingestion memory content mismatch")
245
+ stored_shared = json.loads(memory.get("shared_with") or "null")
246
+ if (memory.get("scope") or "personal") != scope:
247
+ raise ValueError("queryable ingestion memory scope mismatch")
248
+ if (stored_shared or None) != (shared_with or None):
249
+ raise ValueError("queryable ingestion shared scope mismatch")
250
+ queryable_facts = db.get_facts_by_ids(list(queryable_ids), profile_id)
251
+ if len(queryable_facts) != len(queryable_ids):
252
+ raise ValueError("queryable ingestion fact profile mismatch or missing fact")
253
+ if any(fact.memory_id != existing_memory_id for fact in queryable_facts):
254
+ raise ValueError("queryable ingestion fact belongs to another memory")
255
+ record = MemoryRecord(
256
+ memory_id=existing_memory_id,
257
+ profile_id=profile_id,
258
+ content=content,
259
+ session_id=memory.get("session_id") or session_id,
260
+ speaker=memory.get("speaker") or speaker,
261
+ role=memory.get("role") or role,
262
+ session_date=memory.get("session_date") or parsed_date,
263
+ created_at=memory["created_at"],
264
+ metadata=json.loads(memory.get("metadata_json") or "{}"),
265
+ scope=scope,
266
+ shared_with=shared_with,
267
+ )
268
+ else:
269
+ record = MemoryRecord(
270
+ profile_id=profile_id, content=content,
271
+ session_id=session_id, speaker=speaker, role=role,
272
+ session_date=parsed_date, metadata=metadata or {},
273
+ scope=scope, shared_with=shared_with,
274
+ )
275
+ db.store_memory(record)
276
+
277
+ extraction_complete = False
278
+ consolidation_complete = consolidator is not None
279
+ canonicalization_complete = entity_resolver is not None
280
+ graph_complete = graph_builder is not None
281
+ temporal_complete = True
282
+ provenance_complete = provenance is not None
226
283
 
227
284
  try:
228
285
  facts = fact_extractor.extract_facts(
229
286
  turns=[content], session_id=session_id,
230
287
  session_date=parsed_date, speaker_a=speaker,
231
288
  )
289
+ extraction_complete = facts is not None
232
290
  except Exception as _extract_exc:
233
291
  # P0-1 (remember-write-04): an extractor EXCEPTION (transient LLM/embed
234
292
  # backend error) must NOT orphan the already-committed memory. The None
@@ -256,7 +314,8 @@ def run_store(
256
314
  # V3.3.20: Stronger verbatim filter — skip greetings, filler, short phrases.
257
315
  # Verbatim facts with just "Hey! How are you?" dilute embeddings and add noise.
258
316
  _MIN_VERBATIM_WORDS = 8
259
- if (content.strip()
317
+ if (not queryable_facts
318
+ and content.strip()
260
319
  and len(content.strip()) >= 40
261
320
  and len(content.strip().split()) >= _MIN_VERBATIM_WORDS):
262
321
  import uuid
@@ -285,6 +344,19 @@ def run_store(
285
344
  if verbatim.content.strip().lower() not in extracted_texts:
286
345
  facts.append(verbatim)
287
346
 
347
+ if queryable_facts:
348
+ # Replace any extractor-produced copy of the complete raw turn with the
349
+ # already-queryable projection, then promote that stable fact ID in
350
+ # place. Large inputs may have a clamped queryable projection, so the
351
+ # projection itself is authoritative for the verbatim retrieval unit.
352
+ raw_text = content.strip().lower()
353
+ facts = [
354
+ fact for fact in facts
355
+ if fact.content.strip().lower() != raw_text
356
+ and fact.fact_id not in queryable_ids
357
+ ]
358
+ facts.extend(queryable_facts)
359
+
288
360
  # V3.3.21: If fact extraction produced nothing (short input like "this is test"),
289
361
  # store the raw content as a minimal fact. User explicitly called `slm remember` —
290
362
  # their data should NEVER be silently dropped. The min-length and min-word filters
@@ -319,9 +391,39 @@ def run_store(
319
391
  temporal_parser=temporal_parser,
320
392
  )
321
393
 
394
+ is_queryable_promotion = fact.fact_id in queryable_ids
395
+ if is_queryable_promotion:
396
+ db.update_fact(fact.fact_id, {
397
+ "content": fact.content,
398
+ "fact_type": fact.fact_type,
399
+ "entities_json": fact.entities,
400
+ "canonical_entities_json": fact.canonical_entities,
401
+ "observation_date": fact.observation_date,
402
+ "referenced_date": fact.referenced_date,
403
+ "interval_start": fact.interval_start,
404
+ "interval_end": fact.interval_end,
405
+ "confidence": fact.confidence,
406
+ "importance": fact.importance,
407
+ "evidence_count": fact.evidence_count,
408
+ "access_count": fact.access_count,
409
+ "source_turn_ids_json": fact.source_turn_ids,
410
+ "session_id": fact.session_id,
411
+ "embedding": fact.embedding,
412
+ "fisher_mean": fact.fisher_mean,
413
+ "fisher_variance": fact.fisher_variance,
414
+ "lifecycle": fact.lifecycle,
415
+ "langevin_position": fact.langevin_position,
416
+ "emotional_valence": fact.emotional_valence,
417
+ "emotional_arousal": fact.emotional_arousal,
418
+ "signal_type": fact.signal_type,
419
+ })
322
420
  if consolidator:
323
421
  try:
324
- action = consolidator.consolidate(fact, profile_id)
422
+ action = consolidator.consolidate(
423
+ fact,
424
+ profile_id,
425
+ exclude_fact_ids=queryable_ids,
426
+ )
325
427
  except Exception as _consolidate_exc:
326
428
  # P0-1 (remember-write-03): a consolidate failure (e.g. LLM
327
429
  # timeout) must NOT orphan the already-committed memory. Fall
@@ -331,19 +433,31 @@ def run_store(
331
433
  "consolidate() failed for fact %s — storing raw fact as "
332
434
  "fallback: %s", fact.fact_id, _consolidate_exc,
333
435
  )
436
+ consolidation_complete = False
334
437
  action = None
335
438
 
336
439
  if action is not None:
337
440
  if action.action_type.value == "noop":
338
- continue
441
+ # A canonical ingestion projection already exists before
442
+ # enrichment. Reconcile it against pre-existing facts and
443
+ # remove it when consolidation proves it is a duplicate.
444
+ target_id = action.existing_fact_id
445
+ if is_queryable_promotion and target_id:
446
+ db.delete_fact(fact.fact_id)
447
+ existing_fact = db.get_fact(target_id) if target_id else None
448
+ if existing_fact is None:
449
+ continue
450
+ fact = existing_fact
339
451
 
340
452
  # Opinion confidence tracking: reinforce or decay
341
453
  if fact.fact_type == FactType.OPINION and action.action_type.value == "update":
342
454
  try:
343
- existing = db.get_fact(action.new_fact_id)
455
+ existing = db.get_fact(
456
+ action.existing_fact_id or action.new_fact_id
457
+ )
344
458
  if existing and existing.fact_type == FactType.OPINION:
345
459
  new_conf = min(1.0, existing.confidence + 0.1)
346
- db.update_fact(action.new_fact_id, {"confidence": new_conf})
460
+ db.update_fact(existing.fact_id, {"confidence": new_conf})
347
461
  except Exception:
348
462
  pass
349
463
  elif fact.fact_type == FactType.OPINION and action.action_type.value == "supersede":
@@ -358,35 +472,37 @@ def run_store(
358
472
  pass
359
473
 
360
474
  if action.action_type.value in ("update", "supersede"):
361
- updated_fact = db.get_fact(action.new_fact_id)
362
- if updated_fact:
363
- # P1-2 (embeddings-vector-01): the merged/superseding
364
- # fact must reach the vector store (embed on-demand if
365
- # it has none) — otherwise it is invisible to the
366
- # semantic channel despite living in atomic_facts.
367
- _upsert_fact_vectors(
368
- updated_fact, profile_id, ann_index, vector_store, embedder,
475
+ target_id = (
476
+ (action.existing_fact_id or action.new_fact_id)
477
+ if action.action_type.value == "update"
478
+ else action.new_fact_id
479
+ )
480
+ if is_queryable_promotion and target_id != fact.fact_id:
481
+ db.delete_fact(fact.fact_id)
482
+ updated_fact = db.get_fact(target_id)
483
+ if updated_fact is None:
484
+ raise RuntimeError(
485
+ f"consolidation {action.action_type.value} produced "
486
+ f"missing fact {target_id}"
369
487
  )
370
- if graph_builder:
371
- graph_builder.build_edges(updated_fact, profile_id)
372
- if observation_builder:
373
- for eid in updated_fact.canonical_entities:
374
- observation_builder.update_profile(
375
- eid, updated_fact, profile_id,
376
- )
377
- stored_ids.append(action.new_fact_id)
378
- continue
488
+ # Continue through the shared index/graph/temporal/
489
+ # provenance stages. The previous early continue made
490
+ # UPDATE/SUPERSEDE facts look stored while skipping half of
491
+ # canonical materialization.
492
+ fact = updated_fact
379
493
  # ADD case: consolidator already stored the fact (F8 fix)
380
494
  # Fall through to post-processing below
381
495
  else:
382
496
  # Consolidate failed → store the raw fact ourselves so the
383
497
  # memory is never left without a retrievable fact, then fall
384
498
  # through to post-processing (embeddings, graph, context).
385
- db.store_fact(fact)
386
- else:
499
+ if not is_queryable_promotion:
500
+ db.store_fact(fact)
501
+ elif not is_queryable_promotion:
387
502
  db.store_fact(fact)
388
503
 
389
- stored_ids.append(fact.fact_id)
504
+ if fact.fact_id not in stored_ids:
505
+ stored_ids.append(fact.fact_id)
390
506
 
391
507
  # Dual-write embedding to ANN index + vector store (embed on-demand if
392
508
  # a consolidated ADD fact arrived without one). See _upsert_fact_vectors.
@@ -461,6 +577,7 @@ def run_store(
461
577
  len(invalidations), fact.fact_id,
462
578
  )
463
579
  except Exception as exc:
580
+ temporal_complete = False
464
581
  logger.debug(
465
582
  "Temporal validation skipped for fact %s: %s",
466
583
  fact.fact_id, exc,
@@ -488,6 +605,8 @@ def run_store(
488
605
  event = TemporalEvent(
489
606
  profile_id=profile_id, entity_id=eid,
490
607
  fact_id=fact.fact_id,
608
+ scope=fact.scope,
609
+ shared_with=fact.shared_with,
491
610
  observation_date=fact.observation_date,
492
611
  referenced_date=fact.referenced_date,
493
612
  interval_start=fact.interval_start,
@@ -506,6 +625,8 @@ def run_store(
506
625
  profile_id=profile_id,
507
626
  entity_id=sig.get("entity_id", ""),
508
627
  fact_id=fact.fact_id,
628
+ scope=fact.scope,
629
+ shared_with=fact.shared_with,
509
630
  interval_start=sig.get("start_time"),
510
631
  interval_end=sig.get("end_time"),
511
632
  description=sig.get("description", ""),
@@ -525,12 +646,12 @@ def run_store(
525
646
  provenance.record(
526
647
  fact_id=fact.fact_id,
527
648
  profile_id=profile_id,
528
- source_type="store",
529
- source_id=session_id,
530
- created_by=speaker or "unknown",
649
+ source_type=ingestion_source_type,
650
+ source_id=ingestion_operation_id or session_id,
651
+ created_by=trusted_actor_id or speaker or "unknown",
531
652
  )
532
653
  except Exception:
533
- pass
654
+ provenance_complete = False
534
655
 
535
656
  logger.info("Stored %d facts (session=%s)", len(stored_ids), session_id)
536
657
 
@@ -539,6 +660,16 @@ def run_store(
539
660
  hook_ctx["fact_count"] = len(stored_ids)
540
661
  hooks.run_post("store", hook_ctx)
541
662
 
663
+ if derivation_report is not None:
664
+ derivation_report.update({
665
+ "extraction": extraction_complete,
666
+ "canonicalization": canonicalization_complete,
667
+ "consolidation": consolidation_complete,
668
+ "graph": graph_complete,
669
+ "temporal": temporal_complete,
670
+ "provenance": provenance_complete,
671
+ })
672
+
542
673
  # Phase 5: Step-count trigger for lightweight consolidation (L7)
543
674
  if consolidation_engine is not None:
544
675
  try:
@@ -602,8 +733,6 @@ def run_store_fact_direct(
602
733
  )
603
734
  fact.canonical_entities = list(canonical.values())
604
735
  db.store_fact(fact)
605
- # v3.4.5: Incremental sync to CozoDB/LanceDB (F-04)
606
- _sync_to_graph_backends(fact)
607
736
  if fact.embedding and ann_index:
608
737
  ann_index.add(fact.fact_id, fact.embedding)
609
738
  # V3.2: VectorStore upsert (dual-write)
@@ -615,6 +744,9 @@ def run_store_fact_direct(
615
744
  )
616
745
  if graph_builder:
617
746
  graph_builder.build_edges(fact, profile_id)
747
+ # The graph projection must run after GraphBuilder: syncing immediately
748
+ # after SQLite fact insertion omitted every new fact edge from Cozo.
749
+ _sync_to_graph_backends(fact)
618
750
  # BM25 indexing
619
751
  bm25 = getattr(retrieval_engine, '_bm25', None) if retrieval_engine else None
620
752
  if bm25:
@@ -29,6 +29,11 @@ import logging
29
29
  from datetime import datetime, timedelta, UTC
30
30
  from typing import TYPE_CHECKING
31
31
 
32
+ from superlocalmemory.core.lifecycle_state import (
33
+ reconcile_profile_lifecycle,
34
+ set_fact_lifecycle_zone,
35
+ )
36
+
32
37
  if TYPE_CHECKING:
33
38
  from superlocalmemory.storage.database import DatabaseManager
34
39
 
@@ -77,6 +82,10 @@ def evaluate_tiers(
77
82
  "total_evaluated": 0,
78
83
  }
79
84
 
85
+ # Repair any rows written by pre-V3.7 split lifecycle paths before making
86
+ # new transition decisions.
87
+ reconcile_profile_lifecycle(db, profile_id)
88
+
80
89
  now = datetime.now(UTC)
81
90
  pinned_ids = _get_pinned_fact_ids(db, profile_id)
82
91
 
@@ -126,13 +135,12 @@ def promote_on_access_batch(db: DatabaseManager, fact_ids: list[str]) -> int:
126
135
  """
127
136
  if not fact_ids:
128
137
  return 0
129
- placeholders = ",".join("?" * len(fact_ids))
130
- db.execute(
131
- f"UPDATE atomic_facts SET lifecycle = 'active' "
132
- f"WHERE fact_id IN ({placeholders}) AND lifecycle IN ('warm', 'cold')",
133
- tuple(fact_ids),
138
+ return set_fact_lifecycle_zone(
139
+ db,
140
+ fact_ids,
141
+ "active",
142
+ from_atomic=("warm", "cold"),
134
143
  )
135
- return len(fact_ids)
136
144
 
137
145
 
138
146
  def promote_on_access(db: DatabaseManager, fact_id: str) -> None:
@@ -140,10 +148,11 @@ def promote_on_access(db: DatabaseManager, fact_id: str) -> None:
140
148
 
141
149
  Kept for backward compatibility. Prefer promote_on_access_batch.
142
150
  """
143
- db.execute(
144
- "UPDATE atomic_facts SET lifecycle = 'active' "
145
- "WHERE fact_id = ? AND lifecycle IN ('warm', 'cold')",
146
- (fact_id,),
151
+ set_fact_lifecycle_zone(
152
+ db,
153
+ [fact_id],
154
+ "active",
155
+ from_atomic=("warm", "cold"),
147
156
  )
148
157
 
149
158
 
@@ -164,10 +173,8 @@ def pin_fact(
164
173
  "(fact_id, profile_id, pinned_at, reason) VALUES (?, ?, ?, ?)",
165
174
  (fact_id, profile_id, now, reason),
166
175
  )
167
- db.execute(
168
- "UPDATE atomic_facts SET lifecycle = 'active' "
169
- "WHERE fact_id = ? AND profile_id = ?",
170
- (fact_id, profile_id),
176
+ set_fact_lifecycle_zone(
177
+ db, [fact_id], "active", profile_id=profile_id,
171
178
  )
172
179
  return True
173
180
  except Exception as exc:
@@ -315,11 +322,12 @@ def _demote_tier(
315
322
  # Batch UPDATE in chunks of 500
316
323
  for i in range(0, len(demoted_ids), 500):
317
324
  batch = demoted_ids[i:i + 500]
318
- placeholders = ",".join("?" * len(batch))
319
- db.execute(
320
- f"UPDATE atomic_facts SET lifecycle = ? "
321
- f"WHERE fact_id IN ({placeholders}) AND lifecycle = ?",
322
- (to_tier, *batch, from_tier),
325
+ set_fact_lifecycle_zone(
326
+ db,
327
+ batch,
328
+ to_tier,
329
+ profile_id=profile_id,
330
+ from_atomic=(from_tier,),
323
331
  )
324
332
 
325
333
  return len(demoted_ids)
@@ -444,6 +452,10 @@ def _sync_tiers_to_backends(
444
452
 
445
453
  if _lancedb_backend and hasattr(_lancedb_backend, "bulk_update_tiers_from_sqlite"):
446
454
  try:
447
- _lancedb_backend.bulk_update_tiers_from_sqlite(db.conn)
455
+ # DatabaseManager intentionally has no public `.conn`; use its
456
+ # transaction boundary so tier synchronization cannot silently
457
+ # fail after a scale projection has been promoted.
458
+ with db.raw_connection() as conn:
459
+ _lancedb_backend.bulk_update_tiers_from_sqlite(conn)
448
460
  except Exception as exc:
449
461
  logger.warning("LanceDB tier sync failed: %s", exc)
@@ -101,12 +101,21 @@ class WorkerPool:
101
101
  })
102
102
 
103
103
  def delete_memory(self, fact_id: str, agent_id: str = "system") -> dict:
104
- """Delete a specific memory by fact_id. Logged for audit."""
105
- return self._send({"cmd": "delete_memory", "fact_id": fact_id, "agent_id": agent_id})
104
+ """Delete by fact ID; ``agent_id`` is untrusted audit metadata."""
105
+ return self._send({
106
+ "cmd": "delete_memory",
107
+ "fact_id": fact_id,
108
+ "source_agent_id": agent_id,
109
+ })
106
110
 
107
111
  def update_memory(self, fact_id: str, content: str, agent_id: str = "system") -> dict:
108
- """Update content of a specific memory. Logged for audit."""
109
- return self._send({"cmd": "update_memory", "fact_id": fact_id, "content": content, "agent_id": agent_id})
112
+ """Update content; ``agent_id`` is untrusted audit metadata."""
113
+ return self._send({
114
+ "cmd": "update_memory",
115
+ "fact_id": fact_id,
116
+ "content": content,
117
+ "source_agent_id": agent_id,
118
+ })
110
119
 
111
120
  def get_memory_facts(self, memory_id: str) -> dict:
112
121
  """Get original memory text + child atomic facts."""
@@ -22,7 +22,7 @@ HR-05: All SQL uses parameterized queries.
22
22
  HR-07: No-op when config.enabled=False.
23
23
 
24
24
  Part of Qualixar | Author: Varun Pratap Bhardwaj
25
- License: Elastic-2.0
25
+ License: AGPL-3.0-or-later
26
26
  """
27
27
 
28
28
  from __future__ import annotations
@@ -22,7 +22,7 @@ HR-03: Original float32 NEVER deleted unless keep_float32_backup=False
22
22
  HR-04: Quantization ONLY via EAP scheduler (not ad-hoc).
23
23
 
24
24
  Part of Qualixar | Author: Varun Pratap Bhardwaj
25
- License: Elastic-2.0
25
+ License: AGPL-3.0-or-later
26
26
  """
27
27
 
28
28
  from __future__ import annotations
@@ -96,7 +96,7 @@ class EAPScheduler:
96
96
  self._quantized_store = quantized_store
97
97
  self._config = config
98
98
 
99
- def run_eap_cycle(self, profile_id: str) -> dict:
99
+ def run_eap_cycle(self, profile_id: str, *, dry_run: bool = False) -> dict:
100
100
  """Execute one EAP cycle for a profile.
101
101
 
102
102
  Steps:
@@ -153,12 +153,16 @@ class EAPScheduler:
153
153
 
154
154
  if target_bw == 0:
155
155
  # Forgotten -- mark as deleted
156
- self._handle_deletion(fact_id, profile_id)
156
+ if not dry_run:
157
+ self._handle_deletion(fact_id, profile_id)
157
158
  stats["deleted"] += 1
158
159
  continue
159
160
 
160
161
  if target_bw < current_bw:
161
162
  # Downgrade -- compress to lower precision
163
+ if dry_run:
164
+ stats["downgrades"] += 1
165
+ continue
162
166
  success = self._handle_downgrade(
163
167
  fact_id, profile_id, target_bw,
164
168
  )
@@ -168,6 +172,9 @@ class EAPScheduler:
168
172
  stats["errors"] += 1
169
173
  else:
170
174
  # Upgrade -- restore to higher precision (only if float32 exists)
175
+ if dry_run:
176
+ stats["upgrades"] += 1
177
+ continue
171
178
  success = self._handle_upgrade(
172
179
  fact_id, profile_id, target_bw,
173
180
  )
@@ -21,7 +21,7 @@ Mathematical formulation:
21
21
  weight_combined = w_fisher * w_ebbinghaus
22
22
 
23
23
  Part of Qualixar | Author: Varun Pratap Bhardwaj
24
- License: Elastic-2.0
24
+ License: AGPL-3.0-or-later
25
25
  """
26
26
 
27
27
  from __future__ import annotations
@@ -27,7 +27,7 @@ Mathematical formulation:
27
27
  Adapted: uses Langevin position radius for lifecycle zone classification.
28
28
 
29
29
  Part of Qualixar | Author: Varun Pratap Bhardwaj
30
- License: Elastic-2.0
30
+ License: AGPL-3.0-or-later
31
31
  """
32
32
 
33
33
  from __future__ import annotations
@@ -14,7 +14,7 @@ Responsibilities:
14
14
  4. decay_unused(): Weight decay for stale edges
15
15
 
16
16
  Part of Qualixar | Author: Varun Pratap Bhardwaj
17
- License: Elastic-2.0
17
+ License: AGPL-3.0-or-later
18
18
  """
19
19
 
20
20
  from __future__ import annotations
@@ -34,7 +34,7 @@ References:
34
34
  TurboQuant (ICLR 2026). Recursive polar quantization.
35
35
 
36
36
  Part of Qualixar | Author: Varun Pratap Bhardwaj
37
- License: Elastic-2.0
37
+ License: AGPL-3.0-or-later
38
38
  IP Novelty: 92% (no prior art for retention-gated consolidation + polar quantization)
39
39
  """
40
40
 
@@ -741,13 +741,13 @@ class CognitiveConsolidator:
741
741
  cluster_id=cluster.cluster_id,
742
742
  )
743
743
 
744
- # Archive source facts (HR-04: soft-archive, never delete)
744
+ # Archive source facts (HR-04: soft-archive, never delete) through the
745
+ # lifecycle invariant writer.
746
+ from superlocalmemory.core.lifecycle_state import set_fact_lifecycle_zone
747
+ set_fact_lifecycle_zone(
748
+ self._db, cluster.fact_ids, "archive", profile_id=profile_id,
749
+ )
745
750
  for fact_id in cluster.fact_ids:
746
- self._db.execute(
747
- "UPDATE atomic_facts SET lifecycle = 'archived' "
748
- "WHERE fact_id = ? AND profile_id = ?",
749
- (fact_id, profile_id),
750
- )
751
751
  # Log access event
752
752
  self._db.execute(
753
753
  "INSERT INTO fact_access_log "
@@ -756,15 +756,6 @@ class CognitiveConsolidator:
756
756
  "VALUES (?, ?, ?, datetime('now'), 'consolidation', 'ccq')",
757
757
  (_new_id(), fact_id, profile_id),
758
758
  )
759
- # Update fact_retention zone
760
- self._db.execute(
761
- "UPDATE fact_retention "
762
- "SET lifecycle_zone = 'archive', "
763
- " last_computed_at = datetime('now') "
764
- "WHERE fact_id = ? AND profile_id = ?",
765
- (fact_id, profile_id),
766
- )
767
-
768
759
  return block_id
769
760
 
770
761
  # ------------------------------------------------------------------
@@ -13,7 +13,7 @@ Mode A: keyword-based contradiction detection (zero LLM).
13
13
  Mode B/C: LLM-assisted contradiction detection when available.
14
14
 
15
15
  Part of Qualixar | Author: Varun Pratap Bhardwaj
16
- License: Elastic-2.0
16
+ License: AGPL-3.0-or-later
17
17
  """
18
18
 
19
19
  from __future__ import annotations
@@ -100,13 +100,26 @@ class MemoryConsolidator:
100
100
  # -- Public API ---------------------------------------------------------
101
101
 
102
102
  def consolidate(
103
- self, new_fact: AtomicFact, profile_id: str,
103
+ self,
104
+ new_fact: AtomicFact,
105
+ profile_id: str,
106
+ *,
107
+ exclude_fact_ids: set[str] | frozenset[str] | None = None,
104
108
  ) -> ConsolidationAction:
105
109
  """Consolidate *new_fact* against existing knowledge.
106
110
 
107
111
  Returns a ``ConsolidationAction`` describing what was done.
112
+
113
+ ``exclude_fact_ids`` is reserved for an ingestion operation's own
114
+ queryable projection. That projection is evidence being promoted,
115
+ not pre-existing knowledge, and must not suppress its derived facts as
116
+ near-duplicates.
108
117
  """
109
- candidates = self._find_candidates(new_fact, profile_id)
118
+ candidates = self._find_candidates(
119
+ new_fact,
120
+ profile_id,
121
+ exclude_fact_ids=exclude_fact_ids,
122
+ )
110
123
 
111
124
  if not candidates:
112
125
  return self._execute_add(new_fact, profile_id, reason="no matching facts")
@@ -168,7 +181,11 @@ class MemoryConsolidator:
168
181
  # -- Candidate search ---------------------------------------------------
169
182
 
170
183
  def _find_candidates(
171
- self, new_fact: AtomicFact, profile_id: str,
184
+ self,
185
+ new_fact: AtomicFact,
186
+ profile_id: str,
187
+ *,
188
+ exclude_fact_ids: set[str] | frozenset[str] | None = None,
172
189
  ) -> list[tuple[AtomicFact, float]]:
173
190
  """Find and score candidate matches from existing facts.
174
191
 
@@ -178,7 +195,7 @@ class MemoryConsolidator:
178
195
 
179
196
  Returns sorted list of (fact, combined_score), descending.
180
197
  """
181
- seen_ids: set[str] = set()
198
+ seen_ids: set[str] = set(exclude_fact_ids or ())
182
199
  candidate_facts: list[AtomicFact] = []
183
200
 
184
201
  # --- entity-based candidates ---