superlocalmemory 2.8.6 → 3.0.1

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 (431) hide show
  1. package/LICENSE +9 -1
  2. package/NOTICE +63 -0
  3. package/README.md +165 -480
  4. package/bin/slm +17 -449
  5. package/bin/slm-npm +62 -48
  6. package/conftest.py +5 -0
  7. package/docs/api-reference.md +284 -0
  8. package/docs/architecture.md +149 -0
  9. package/docs/auto-memory.md +150 -0
  10. package/docs/cli-reference.md +276 -0
  11. package/docs/compliance.md +191 -0
  12. package/docs/configuration.md +182 -0
  13. package/docs/getting-started.md +102 -0
  14. package/docs/ide-setup.md +261 -0
  15. package/docs/mcp-tools.md +220 -0
  16. package/docs/migration-from-v2.md +170 -0
  17. package/docs/profiles.md +173 -0
  18. package/docs/troubleshooting.md +310 -0
  19. package/{configs → ide/configs}/antigravity-mcp.json +3 -3
  20. package/ide/configs/chatgpt-desktop-mcp.json +16 -0
  21. package/{configs → ide/configs}/claude-desktop-mcp.json +3 -3
  22. package/{configs → ide/configs}/codex-mcp.toml +4 -4
  23. package/{configs → ide/configs}/continue-mcp.yaml +4 -3
  24. package/{configs → ide/configs}/continue-skills.yaml +6 -6
  25. package/ide/configs/cursor-mcp.json +15 -0
  26. package/{configs → ide/configs}/gemini-cli-mcp.json +2 -2
  27. package/{configs → ide/configs}/jetbrains-mcp.json +2 -2
  28. package/{configs → ide/configs}/opencode-mcp.json +2 -2
  29. package/{configs → ide/configs}/perplexity-mcp.json +2 -2
  30. package/{configs → ide/configs}/vscode-copilot-mcp.json +2 -2
  31. package/{configs → ide/configs}/windsurf-mcp.json +3 -3
  32. package/{configs → ide/configs}/zed-mcp.json +2 -2
  33. package/{hooks → ide/hooks}/context-hook.js +9 -20
  34. package/ide/hooks/memory-list-skill.js +70 -0
  35. package/ide/hooks/memory-profile-skill.js +101 -0
  36. package/ide/hooks/memory-recall-skill.js +62 -0
  37. package/ide/hooks/memory-remember-skill.js +68 -0
  38. package/ide/hooks/memory-reset-skill.js +160 -0
  39. package/{hooks → ide/hooks}/post-recall-hook.js +2 -2
  40. package/ide/integrations/langchain/README.md +106 -0
  41. package/ide/integrations/langchain/langchain_superlocalmemory/__init__.py +9 -0
  42. package/ide/integrations/langchain/langchain_superlocalmemory/chat_message_history.py +201 -0
  43. package/ide/integrations/langchain/pyproject.toml +38 -0
  44. package/{src/learning → ide/integrations/langchain}/tests/__init__.py +1 -0
  45. package/ide/integrations/langchain/tests/test_chat_message_history.py +215 -0
  46. package/ide/integrations/langchain/tests/test_security.py +117 -0
  47. package/ide/integrations/llamaindex/README.md +81 -0
  48. package/ide/integrations/llamaindex/llama_index/storage/chat_store/superlocalmemory/__init__.py +9 -0
  49. package/ide/integrations/llamaindex/llama_index/storage/chat_store/superlocalmemory/base.py +316 -0
  50. package/ide/integrations/llamaindex/pyproject.toml +43 -0
  51. package/{src/lifecycle → ide/integrations/llamaindex}/tests/__init__.py +1 -2
  52. package/ide/integrations/llamaindex/tests/test_chat_store.py +294 -0
  53. package/ide/integrations/llamaindex/tests/test_security.py +241 -0
  54. package/{skills → ide/skills}/slm-build-graph/SKILL.md +6 -6
  55. package/{skills → ide/skills}/slm-list-recent/SKILL.md +5 -5
  56. package/{skills → ide/skills}/slm-recall/SKILL.md +5 -5
  57. package/{skills → ide/skills}/slm-remember/SKILL.md +6 -6
  58. package/{skills → ide/skills}/slm-show-patterns/SKILL.md +7 -7
  59. package/{skills → ide/skills}/slm-status/SKILL.md +9 -9
  60. package/{skills → ide/skills}/slm-switch-profile/SKILL.md +9 -9
  61. package/package.json +13 -22
  62. package/pyproject.toml +85 -0
  63. package/scripts/build-dmg.sh +417 -0
  64. package/scripts/install-skills.ps1 +334 -0
  65. package/scripts/postinstall.js +2 -2
  66. package/scripts/start-dashboard.ps1 +52 -0
  67. package/scripts/start-dashboard.sh +41 -0
  68. package/scripts/sync-wiki.ps1 +127 -0
  69. package/scripts/sync-wiki.sh +82 -0
  70. package/scripts/test-dmg.sh +161 -0
  71. package/scripts/test-npm-package.ps1 +252 -0
  72. package/scripts/test-npm-package.sh +207 -0
  73. package/scripts/verify-install.ps1 +294 -0
  74. package/scripts/verify-install.sh +266 -0
  75. package/src/superlocalmemory/__init__.py +0 -0
  76. package/src/superlocalmemory/attribution/__init__.py +9 -0
  77. package/src/superlocalmemory/attribution/mathematical_dna.py +235 -0
  78. package/src/superlocalmemory/attribution/signer.py +153 -0
  79. package/src/superlocalmemory/attribution/watermark.py +189 -0
  80. package/src/superlocalmemory/cli/__init__.py +5 -0
  81. package/src/superlocalmemory/cli/commands.py +245 -0
  82. package/src/superlocalmemory/cli/main.py +89 -0
  83. package/src/superlocalmemory/cli/migrate_cmd.py +55 -0
  84. package/src/superlocalmemory/cli/post_install.py +99 -0
  85. package/src/superlocalmemory/cli/setup_wizard.py +129 -0
  86. package/src/superlocalmemory/compliance/__init__.py +0 -0
  87. package/src/superlocalmemory/compliance/abac.py +204 -0
  88. package/src/superlocalmemory/compliance/audit.py +314 -0
  89. package/src/superlocalmemory/compliance/eu_ai_act.py +131 -0
  90. package/src/superlocalmemory/compliance/gdpr.py +294 -0
  91. package/src/superlocalmemory/compliance/lifecycle.py +158 -0
  92. package/src/superlocalmemory/compliance/retention.py +232 -0
  93. package/src/superlocalmemory/compliance/scheduler.py +148 -0
  94. package/src/superlocalmemory/core/__init__.py +0 -0
  95. package/src/superlocalmemory/core/config.py +391 -0
  96. package/src/superlocalmemory/core/embeddings.py +293 -0
  97. package/src/superlocalmemory/core/engine.py +701 -0
  98. package/src/superlocalmemory/core/hooks.py +65 -0
  99. package/src/superlocalmemory/core/maintenance.py +172 -0
  100. package/src/superlocalmemory/core/modes.py +140 -0
  101. package/src/superlocalmemory/core/profiles.py +234 -0
  102. package/src/superlocalmemory/core/registry.py +117 -0
  103. package/src/superlocalmemory/dynamics/__init__.py +0 -0
  104. package/src/superlocalmemory/dynamics/fisher_langevin_coupling.py +223 -0
  105. package/src/superlocalmemory/encoding/__init__.py +0 -0
  106. package/src/superlocalmemory/encoding/consolidator.py +485 -0
  107. package/src/superlocalmemory/encoding/emotional.py +125 -0
  108. package/src/superlocalmemory/encoding/entity_resolver.py +525 -0
  109. package/src/superlocalmemory/encoding/entropy_gate.py +104 -0
  110. package/src/superlocalmemory/encoding/fact_extractor.py +775 -0
  111. package/src/superlocalmemory/encoding/foresight.py +91 -0
  112. package/src/superlocalmemory/encoding/graph_builder.py +302 -0
  113. package/src/superlocalmemory/encoding/observation_builder.py +160 -0
  114. package/src/superlocalmemory/encoding/scene_builder.py +183 -0
  115. package/src/superlocalmemory/encoding/signal_inference.py +90 -0
  116. package/src/superlocalmemory/encoding/temporal_parser.py +426 -0
  117. package/src/superlocalmemory/encoding/type_router.py +235 -0
  118. package/src/superlocalmemory/hooks/__init__.py +3 -0
  119. package/src/superlocalmemory/hooks/auto_capture.py +111 -0
  120. package/src/superlocalmemory/hooks/auto_recall.py +93 -0
  121. package/src/superlocalmemory/hooks/ide_connector.py +204 -0
  122. package/src/superlocalmemory/hooks/rules_engine.py +99 -0
  123. package/src/superlocalmemory/infra/__init__.py +3 -0
  124. package/src/superlocalmemory/infra/auth_middleware.py +82 -0
  125. package/src/superlocalmemory/infra/backup.py +317 -0
  126. package/src/superlocalmemory/infra/cache_manager.py +267 -0
  127. package/src/superlocalmemory/infra/event_bus.py +381 -0
  128. package/src/superlocalmemory/infra/rate_limiter.py +135 -0
  129. package/src/{webhook_dispatcher.py → superlocalmemory/infra/webhook_dispatcher.py} +104 -101
  130. package/src/superlocalmemory/learning/__init__.py +0 -0
  131. package/src/superlocalmemory/learning/adaptive.py +172 -0
  132. package/src/superlocalmemory/learning/behavioral.py +490 -0
  133. package/src/superlocalmemory/learning/behavioral_listener.py +94 -0
  134. package/src/superlocalmemory/learning/bootstrap.py +298 -0
  135. package/src/superlocalmemory/learning/cross_project.py +399 -0
  136. package/src/superlocalmemory/learning/database.py +376 -0
  137. package/src/superlocalmemory/learning/engagement.py +323 -0
  138. package/src/superlocalmemory/learning/features.py +138 -0
  139. package/src/superlocalmemory/learning/feedback.py +316 -0
  140. package/src/superlocalmemory/learning/outcomes.py +255 -0
  141. package/src/superlocalmemory/learning/project_context.py +366 -0
  142. package/src/superlocalmemory/learning/ranker.py +155 -0
  143. package/src/superlocalmemory/learning/source_quality.py +303 -0
  144. package/src/superlocalmemory/learning/workflows.py +309 -0
  145. package/src/superlocalmemory/llm/__init__.py +0 -0
  146. package/src/superlocalmemory/llm/backbone.py +316 -0
  147. package/src/superlocalmemory/math/__init__.py +0 -0
  148. package/src/superlocalmemory/math/fisher.py +356 -0
  149. package/src/superlocalmemory/math/langevin.py +398 -0
  150. package/src/superlocalmemory/math/sheaf.py +257 -0
  151. package/src/superlocalmemory/mcp/__init__.py +0 -0
  152. package/src/superlocalmemory/mcp/resources.py +245 -0
  153. package/src/superlocalmemory/mcp/server.py +61 -0
  154. package/src/superlocalmemory/mcp/tools.py +18 -0
  155. package/src/superlocalmemory/mcp/tools_core.py +305 -0
  156. package/src/superlocalmemory/mcp/tools_v28.py +223 -0
  157. package/src/superlocalmemory/mcp/tools_v3.py +286 -0
  158. package/src/superlocalmemory/retrieval/__init__.py +0 -0
  159. package/src/superlocalmemory/retrieval/agentic.py +295 -0
  160. package/src/superlocalmemory/retrieval/ann_index.py +223 -0
  161. package/src/superlocalmemory/retrieval/bm25_channel.py +185 -0
  162. package/src/superlocalmemory/retrieval/bridge_discovery.py +170 -0
  163. package/src/superlocalmemory/retrieval/engine.py +390 -0
  164. package/src/superlocalmemory/retrieval/entity_channel.py +179 -0
  165. package/src/superlocalmemory/retrieval/fusion.py +78 -0
  166. package/src/superlocalmemory/retrieval/profile_channel.py +105 -0
  167. package/src/superlocalmemory/retrieval/reranker.py +154 -0
  168. package/src/superlocalmemory/retrieval/semantic_channel.py +232 -0
  169. package/src/superlocalmemory/retrieval/strategy.py +96 -0
  170. package/src/superlocalmemory/retrieval/temporal_channel.py +175 -0
  171. package/src/superlocalmemory/server/__init__.py +1 -0
  172. package/src/superlocalmemory/server/api.py +248 -0
  173. package/src/superlocalmemory/server/routes/__init__.py +4 -0
  174. package/src/superlocalmemory/server/routes/agents.py +107 -0
  175. package/src/superlocalmemory/server/routes/backup.py +91 -0
  176. package/src/superlocalmemory/server/routes/behavioral.py +127 -0
  177. package/src/superlocalmemory/server/routes/compliance.py +160 -0
  178. package/src/superlocalmemory/server/routes/data_io.py +188 -0
  179. package/src/superlocalmemory/server/routes/events.py +183 -0
  180. package/src/superlocalmemory/server/routes/helpers.py +85 -0
  181. package/src/superlocalmemory/server/routes/learning.py +273 -0
  182. package/src/superlocalmemory/server/routes/lifecycle.py +116 -0
  183. package/src/superlocalmemory/server/routes/memories.py +399 -0
  184. package/src/superlocalmemory/server/routes/profiles.py +219 -0
  185. package/src/superlocalmemory/server/routes/stats.py +346 -0
  186. package/src/superlocalmemory/server/routes/v3_api.py +365 -0
  187. package/src/superlocalmemory/server/routes/ws.py +82 -0
  188. package/src/superlocalmemory/server/security_middleware.py +57 -0
  189. package/src/superlocalmemory/server/ui.py +245 -0
  190. package/src/superlocalmemory/storage/__init__.py +0 -0
  191. package/src/superlocalmemory/storage/access_control.py +182 -0
  192. package/src/superlocalmemory/storage/database.py +594 -0
  193. package/src/superlocalmemory/storage/migrations.py +303 -0
  194. package/src/superlocalmemory/storage/models.py +406 -0
  195. package/src/superlocalmemory/storage/schema.py +726 -0
  196. package/src/superlocalmemory/storage/v2_migrator.py +317 -0
  197. package/src/superlocalmemory/trust/__init__.py +0 -0
  198. package/src/superlocalmemory/trust/gate.py +130 -0
  199. package/src/superlocalmemory/trust/provenance.py +124 -0
  200. package/src/superlocalmemory/trust/scorer.py +347 -0
  201. package/src/superlocalmemory/trust/signals.py +153 -0
  202. package/ui/index.html +278 -5
  203. package/ui/js/auto-settings.js +70 -0
  204. package/ui/js/dashboard.js +90 -0
  205. package/ui/js/fact-detail.js +92 -0
  206. package/ui/js/feedback.js +2 -2
  207. package/ui/js/ide-status.js +102 -0
  208. package/ui/js/math-health.js +98 -0
  209. package/ui/js/recall-lab.js +127 -0
  210. package/ui/js/settings.js +2 -2
  211. package/ui/js/trust-dashboard.js +73 -0
  212. package/api_server.py +0 -724
  213. package/bin/aider-smart +0 -72
  214. package/bin/superlocalmemoryv2-learning +0 -4
  215. package/bin/superlocalmemoryv2-list +0 -3
  216. package/bin/superlocalmemoryv2-patterns +0 -4
  217. package/bin/superlocalmemoryv2-profile +0 -3
  218. package/bin/superlocalmemoryv2-recall +0 -3
  219. package/bin/superlocalmemoryv2-remember +0 -3
  220. package/bin/superlocalmemoryv2-reset +0 -3
  221. package/bin/superlocalmemoryv2-status +0 -3
  222. package/configs/chatgpt-desktop-mcp.json +0 -16
  223. package/configs/cursor-mcp.json +0 -15
  224. package/hooks/memory-list-skill.js +0 -139
  225. package/hooks/memory-profile-skill.js +0 -273
  226. package/hooks/memory-recall-skill.js +0 -114
  227. package/hooks/memory-remember-skill.js +0 -127
  228. package/hooks/memory-reset-skill.js +0 -274
  229. package/mcp_server.py +0 -1808
  230. package/requirements-core.txt +0 -22
  231. package/requirements-learning.txt +0 -12
  232. package/requirements.txt +0 -12
  233. package/src/agent_registry.py +0 -411
  234. package/src/auth_middleware.py +0 -61
  235. package/src/auto_backup.py +0 -459
  236. package/src/behavioral/__init__.py +0 -49
  237. package/src/behavioral/behavioral_listener.py +0 -203
  238. package/src/behavioral/behavioral_patterns.py +0 -275
  239. package/src/behavioral/cross_project_transfer.py +0 -206
  240. package/src/behavioral/outcome_inference.py +0 -194
  241. package/src/behavioral/outcome_tracker.py +0 -193
  242. package/src/behavioral/tests/__init__.py +0 -4
  243. package/src/behavioral/tests/test_behavioral_integration.py +0 -108
  244. package/src/behavioral/tests/test_behavioral_patterns.py +0 -150
  245. package/src/behavioral/tests/test_cross_project_transfer.py +0 -142
  246. package/src/behavioral/tests/test_mcp_behavioral.py +0 -139
  247. package/src/behavioral/tests/test_mcp_report_outcome.py +0 -117
  248. package/src/behavioral/tests/test_outcome_inference.py +0 -107
  249. package/src/behavioral/tests/test_outcome_tracker.py +0 -96
  250. package/src/cache_manager.py +0 -518
  251. package/src/compliance/__init__.py +0 -48
  252. package/src/compliance/abac_engine.py +0 -149
  253. package/src/compliance/abac_middleware.py +0 -116
  254. package/src/compliance/audit_db.py +0 -215
  255. package/src/compliance/audit_logger.py +0 -148
  256. package/src/compliance/retention_manager.py +0 -289
  257. package/src/compliance/retention_scheduler.py +0 -186
  258. package/src/compliance/tests/__init__.py +0 -4
  259. package/src/compliance/tests/test_abac_enforcement.py +0 -95
  260. package/src/compliance/tests/test_abac_engine.py +0 -124
  261. package/src/compliance/tests/test_abac_mcp_integration.py +0 -118
  262. package/src/compliance/tests/test_audit_db.py +0 -123
  263. package/src/compliance/tests/test_audit_logger.py +0 -98
  264. package/src/compliance/tests/test_mcp_audit.py +0 -128
  265. package/src/compliance/tests/test_mcp_retention_policy.py +0 -125
  266. package/src/compliance/tests/test_retention_manager.py +0 -131
  267. package/src/compliance/tests/test_retention_scheduler.py +0 -99
  268. package/src/compression/__init__.py +0 -25
  269. package/src/compression/cli.py +0 -150
  270. package/src/compression/cold_storage.py +0 -217
  271. package/src/compression/config.py +0 -72
  272. package/src/compression/orchestrator.py +0 -133
  273. package/src/compression/tier2_compressor.py +0 -228
  274. package/src/compression/tier3_compressor.py +0 -153
  275. package/src/compression/tier_classifier.py +0 -148
  276. package/src/db_connection_manager.py +0 -536
  277. package/src/embedding_engine.py +0 -63
  278. package/src/embeddings/__init__.py +0 -47
  279. package/src/embeddings/cache.py +0 -70
  280. package/src/embeddings/cli.py +0 -113
  281. package/src/embeddings/constants.py +0 -47
  282. package/src/embeddings/database.py +0 -91
  283. package/src/embeddings/engine.py +0 -247
  284. package/src/embeddings/model_loader.py +0 -145
  285. package/src/event_bus.py +0 -562
  286. package/src/graph/__init__.py +0 -36
  287. package/src/graph/build_helpers.py +0 -74
  288. package/src/graph/cli.py +0 -87
  289. package/src/graph/cluster_builder.py +0 -188
  290. package/src/graph/cluster_summary.py +0 -148
  291. package/src/graph/constants.py +0 -47
  292. package/src/graph/edge_builder.py +0 -162
  293. package/src/graph/entity_extractor.py +0 -95
  294. package/src/graph/graph_core.py +0 -226
  295. package/src/graph/graph_search.py +0 -231
  296. package/src/graph/hierarchical.py +0 -207
  297. package/src/graph/schema.py +0 -99
  298. package/src/graph_engine.py +0 -52
  299. package/src/hnsw_index.py +0 -628
  300. package/src/hybrid_search.py +0 -46
  301. package/src/learning/__init__.py +0 -217
  302. package/src/learning/adaptive_ranker.py +0 -682
  303. package/src/learning/bootstrap/__init__.py +0 -69
  304. package/src/learning/bootstrap/constants.py +0 -93
  305. package/src/learning/bootstrap/db_queries.py +0 -316
  306. package/src/learning/bootstrap/sampling.py +0 -82
  307. package/src/learning/bootstrap/text_utils.py +0 -71
  308. package/src/learning/cross_project_aggregator.py +0 -857
  309. package/src/learning/db/__init__.py +0 -40
  310. package/src/learning/db/constants.py +0 -44
  311. package/src/learning/db/schema.py +0 -279
  312. package/src/learning/engagement_tracker.py +0 -628
  313. package/src/learning/feature_extractor.py +0 -708
  314. package/src/learning/feedback_collector.py +0 -806
  315. package/src/learning/learning_db.py +0 -915
  316. package/src/learning/project_context_manager.py +0 -572
  317. package/src/learning/ranking/__init__.py +0 -33
  318. package/src/learning/ranking/constants.py +0 -84
  319. package/src/learning/ranking/helpers.py +0 -278
  320. package/src/learning/source_quality_scorer.py +0 -676
  321. package/src/learning/synthetic_bootstrap.py +0 -755
  322. package/src/learning/tests/test_adaptive_ranker.py +0 -325
  323. package/src/learning/tests/test_adaptive_ranker_v28.py +0 -60
  324. package/src/learning/tests/test_aggregator.py +0 -306
  325. package/src/learning/tests/test_auto_retrain_v28.py +0 -35
  326. package/src/learning/tests/test_e2e_ranking_v28.py +0 -82
  327. package/src/learning/tests/test_feature_extractor_v28.py +0 -93
  328. package/src/learning/tests/test_feedback_collector.py +0 -294
  329. package/src/learning/tests/test_learning_db.py +0 -602
  330. package/src/learning/tests/test_learning_db_v28.py +0 -110
  331. package/src/learning/tests/test_learning_init_v28.py +0 -48
  332. package/src/learning/tests/test_outcome_signals.py +0 -48
  333. package/src/learning/tests/test_project_context.py +0 -292
  334. package/src/learning/tests/test_schema_migration.py +0 -319
  335. package/src/learning/tests/test_signal_inference.py +0 -397
  336. package/src/learning/tests/test_source_quality.py +0 -351
  337. package/src/learning/tests/test_synthetic_bootstrap.py +0 -429
  338. package/src/learning/tests/test_workflow_miner.py +0 -318
  339. package/src/learning/workflow_pattern_miner.py +0 -655
  340. package/src/lifecycle/__init__.py +0 -54
  341. package/src/lifecycle/bounded_growth.py +0 -239
  342. package/src/lifecycle/compaction_engine.py +0 -226
  343. package/src/lifecycle/lifecycle_engine.py +0 -355
  344. package/src/lifecycle/lifecycle_evaluator.py +0 -257
  345. package/src/lifecycle/lifecycle_scheduler.py +0 -130
  346. package/src/lifecycle/retention_policy.py +0 -285
  347. package/src/lifecycle/tests/test_bounded_growth.py +0 -193
  348. package/src/lifecycle/tests/test_compaction.py +0 -179
  349. package/src/lifecycle/tests/test_lifecycle_engine.py +0 -137
  350. package/src/lifecycle/tests/test_lifecycle_evaluation.py +0 -177
  351. package/src/lifecycle/tests/test_lifecycle_scheduler.py +0 -127
  352. package/src/lifecycle/tests/test_lifecycle_search.py +0 -109
  353. package/src/lifecycle/tests/test_mcp_compact.py +0 -149
  354. package/src/lifecycle/tests/test_mcp_lifecycle_status.py +0 -114
  355. package/src/lifecycle/tests/test_retention_policy.py +0 -162
  356. package/src/mcp_tools_v28.py +0 -281
  357. package/src/memory/__init__.py +0 -36
  358. package/src/memory/cli.py +0 -205
  359. package/src/memory/constants.py +0 -39
  360. package/src/memory/helpers.py +0 -28
  361. package/src/memory/schema.py +0 -166
  362. package/src/memory-profiles.py +0 -595
  363. package/src/memory-reset.py +0 -491
  364. package/src/memory_compression.py +0 -989
  365. package/src/memory_store_v2.py +0 -1155
  366. package/src/migrate_v1_to_v2.py +0 -629
  367. package/src/pattern_learner.py +0 -34
  368. package/src/patterns/__init__.py +0 -24
  369. package/src/patterns/analyzers.py +0 -251
  370. package/src/patterns/learner.py +0 -271
  371. package/src/patterns/scoring.py +0 -171
  372. package/src/patterns/store.py +0 -225
  373. package/src/patterns/terminology.py +0 -140
  374. package/src/provenance_tracker.py +0 -312
  375. package/src/qualixar_attribution.py +0 -139
  376. package/src/qualixar_watermark.py +0 -78
  377. package/src/query_optimizer.py +0 -511
  378. package/src/rate_limiter.py +0 -83
  379. package/src/search/__init__.py +0 -20
  380. package/src/search/cli.py +0 -77
  381. package/src/search/constants.py +0 -26
  382. package/src/search/engine.py +0 -241
  383. package/src/search/fusion.py +0 -122
  384. package/src/search/index_loader.py +0 -114
  385. package/src/search/methods.py +0 -162
  386. package/src/search_engine_v2.py +0 -401
  387. package/src/setup_validator.py +0 -482
  388. package/src/subscription_manager.py +0 -391
  389. package/src/tree/__init__.py +0 -59
  390. package/src/tree/builder.py +0 -185
  391. package/src/tree/nodes.py +0 -202
  392. package/src/tree/queries.py +0 -257
  393. package/src/tree/schema.py +0 -80
  394. package/src/tree_manager.py +0 -19
  395. package/src/trust/__init__.py +0 -45
  396. package/src/trust/constants.py +0 -66
  397. package/src/trust/queries.py +0 -157
  398. package/src/trust/schema.py +0 -95
  399. package/src/trust/scorer.py +0 -299
  400. package/src/trust/signals.py +0 -95
  401. package/src/trust_scorer.py +0 -44
  402. package/ui/app.js +0 -1588
  403. package/ui/js/graph-cytoscape-monolithic-backup.js +0 -1168
  404. package/ui/js/graph-cytoscape.js +0 -1168
  405. package/ui/js/graph-d3-backup.js +0 -32
  406. package/ui/js/graph.js +0 -32
  407. package/ui_server.py +0 -286
  408. /package/docs/{ACCESSIBILITY.md → v2-archive/ACCESSIBILITY.md} +0 -0
  409. /package/docs/{ARCHITECTURE.md → v2-archive/ARCHITECTURE.md} +0 -0
  410. /package/docs/{CLI-COMMANDS-REFERENCE.md → v2-archive/CLI-COMMANDS-REFERENCE.md} +0 -0
  411. /package/docs/{COMPRESSION-README.md → v2-archive/COMPRESSION-README.md} +0 -0
  412. /package/docs/{FRAMEWORK-INTEGRATIONS.md → v2-archive/FRAMEWORK-INTEGRATIONS.md} +0 -0
  413. /package/docs/{MCP-MANUAL-SETUP.md → v2-archive/MCP-MANUAL-SETUP.md} +0 -0
  414. /package/docs/{MCP-TROUBLESHOOTING.md → v2-archive/MCP-TROUBLESHOOTING.md} +0 -0
  415. /package/docs/{PATTERN-LEARNING.md → v2-archive/PATTERN-LEARNING.md} +0 -0
  416. /package/docs/{PROFILES-GUIDE.md → v2-archive/PROFILES-GUIDE.md} +0 -0
  417. /package/docs/{RESET-GUIDE.md → v2-archive/RESET-GUIDE.md} +0 -0
  418. /package/docs/{SEARCH-ENGINE-V2.2.0.md → v2-archive/SEARCH-ENGINE-V2.2.0.md} +0 -0
  419. /package/docs/{SEARCH-INTEGRATION-GUIDE.md → v2-archive/SEARCH-INTEGRATION-GUIDE.md} +0 -0
  420. /package/docs/{UI-SERVER.md → v2-archive/UI-SERVER.md} +0 -0
  421. /package/docs/{UNIVERSAL-INTEGRATION.md → v2-archive/UNIVERSAL-INTEGRATION.md} +0 -0
  422. /package/docs/{V2.2.0-OPTIONAL-SEARCH.md → v2-archive/V2.2.0-OPTIONAL-SEARCH.md} +0 -0
  423. /package/docs/{WINDOWS-INSTALL-README.txt → v2-archive/WINDOWS-INSTALL-README.txt} +0 -0
  424. /package/docs/{WINDOWS-POST-INSTALL.txt → v2-archive/WINDOWS-POST-INSTALL.txt} +0 -0
  425. /package/docs/{example_graph_usage.py → v2-archive/example_graph_usage.py} +0 -0
  426. /package/{completions → ide/completions}/slm.bash +0 -0
  427. /package/{completions → ide/completions}/slm.zsh +0 -0
  428. /package/{configs → ide/configs}/cody-commands.json +0 -0
  429. /package/{install-skills.sh → scripts/install-skills.sh} +0 -0
  430. /package/{install.ps1 → scripts/install.ps1} +0 -0
  431. /package/{install.sh → scripts/install.sh} +0 -0
@@ -0,0 +1,267 @@
1
+ # Copyright (c) 2026 Varun Pratap Bhardwaj / Qualixar
2
+ # Licensed under the MIT License - see LICENSE file
3
+ # Part of SuperLocalMemory V3 | https://qualixar.com | https://varunpratap.com
4
+ """LRU cache with optional TTL for search-result caching.
5
+
6
+ Key features:
7
+ * O(1) get / set via ``OrderedDict``
8
+ * Time-to-live expiry per entry
9
+ * Size-based eviction (LRU)
10
+ * Optional thread safety
11
+ * Hit / miss / eviction statistics
12
+ """
13
+
14
+ import hashlib
15
+ import json
16
+ import logging
17
+ import time
18
+ from collections import OrderedDict
19
+ from threading import RLock
20
+ from typing import Any, Dict, List, Optional, Tuple
21
+
22
+ logger = logging.getLogger("superlocalmemory.cache")
23
+
24
+
25
+ class CacheEntry:
26
+ """Single cache entry with metadata."""
27
+
28
+ __slots__ = ["value", "timestamp", "access_count", "size_estimate"]
29
+
30
+ def __init__(self, value: Any, size_estimate: int = 0) -> None:
31
+ self.value = value
32
+ self.timestamp = time.time()
33
+ self.access_count = 0
34
+ self.size_estimate = size_estimate
35
+
36
+ def is_expired(self, ttl_seconds: Optional[float]) -> bool:
37
+ """Return ``True`` when the entry has exceeded *ttl_seconds*."""
38
+ if ttl_seconds is None:
39
+ return False
40
+ return (time.time() - self.timestamp) > ttl_seconds
41
+
42
+ def mark_accessed(self) -> None:
43
+ """Increment access counter."""
44
+ self.access_count += 1
45
+
46
+
47
+ class CacheManager:
48
+ """LRU cache manager with TTL support.
49
+
50
+ Args:
51
+ max_size: Maximum number of entries before LRU eviction.
52
+ ttl_seconds: Per-entry time-to-live (``None`` = never expire).
53
+ thread_safe: Wrap operations in a reentrant lock.
54
+ """
55
+
56
+ def __init__(
57
+ self,
58
+ max_size: int = 100,
59
+ ttl_seconds: Optional[float] = 300,
60
+ thread_safe: bool = False,
61
+ ) -> None:
62
+ self.max_size = max(1, max_size)
63
+ self.ttl_seconds = ttl_seconds
64
+ self.thread_safe = thread_safe
65
+
66
+ self._cache: OrderedDict[str, CacheEntry] = OrderedDict()
67
+ self._lock: Optional[RLock] = RLock() if thread_safe else None
68
+
69
+ # Statistics
70
+ self._hits = 0
71
+ self._misses = 0
72
+ self._evictions = 0
73
+ self._total_size_estimate = 0
74
+
75
+ # ------------------------------------------------------------------
76
+ # Key helpers
77
+ # ------------------------------------------------------------------
78
+
79
+ @staticmethod
80
+ def _hash_key(query: str, **kwargs: Any) -> str:
81
+ """Deterministic cache key from *query* + keyword args."""
82
+ key_data = {"query": query, **kwargs}
83
+ key_str = json.dumps(key_data, sort_keys=True)
84
+ return hashlib.sha256(key_str.encode()).hexdigest()[:16]
85
+
86
+ @staticmethod
87
+ def _estimate_size(value: Any) -> int:
88
+ """Rough byte-size estimate for analytics."""
89
+ try:
90
+ if isinstance(value, list):
91
+ return len(value) * 100
92
+ return len(json.dumps(value, default=str))
93
+ except Exception:
94
+ return 1000
95
+
96
+ # ------------------------------------------------------------------
97
+ # Core API (simple key-based)
98
+ # ------------------------------------------------------------------
99
+
100
+ def set(self, key: str, value: Any) -> None:
101
+ """Store *value* under a plain string *key*.
102
+
103
+ This is the simple interface expected by the test suite.
104
+ """
105
+ self._put_internal(key, value)
106
+
107
+ def get(self, key: str, **kwargs: Any) -> Optional[Any]:
108
+ """Retrieve value by plain string *key*.
109
+
110
+ Returns ``None`` on cache miss or expiry.
111
+ """
112
+ if kwargs:
113
+ key = self._hash_key(key, **kwargs)
114
+ return self._get_internal(key)
115
+
116
+ # ------------------------------------------------------------------
117
+ # Query-oriented API (hashed keys)
118
+ # ------------------------------------------------------------------
119
+
120
+ def put(self, query: str, value: Any, **kwargs: Any) -> None:
121
+ """Store *value* keyed by the hash of *query* + *kwargs*."""
122
+ hashed = self._hash_key(query, **kwargs)
123
+ self._put_internal(hashed, value)
124
+
125
+ def get_by_query(self, query: str, **kwargs: Any) -> Optional[Any]:
126
+ """Retrieve value keyed by the hash of *query* + *kwargs*."""
127
+ hashed = self._hash_key(query, **kwargs)
128
+ return self._get_internal(hashed)
129
+
130
+ # ------------------------------------------------------------------
131
+ # Internal get / put (shared logic)
132
+ # ------------------------------------------------------------------
133
+
134
+ def _get_internal(self, key: str) -> Optional[Any]:
135
+ if self._lock:
136
+ self._lock.acquire()
137
+ try:
138
+ if key not in self._cache:
139
+ self._misses += 1
140
+ return None
141
+
142
+ entry = self._cache[key]
143
+
144
+ if entry.is_expired(self.ttl_seconds):
145
+ del self._cache[key]
146
+ self._total_size_estimate -= entry.size_estimate
147
+ self._misses += 1
148
+ return None
149
+
150
+ self._cache.move_to_end(key)
151
+ entry.mark_accessed()
152
+ self._hits += 1
153
+ return entry.value
154
+ finally:
155
+ if self._lock:
156
+ self._lock.release()
157
+
158
+ def _put_internal(self, key: str, value: Any) -> None:
159
+ size_estimate = self._estimate_size(value)
160
+
161
+ if self._lock:
162
+ self._lock.acquire()
163
+ try:
164
+ if key in self._cache:
165
+ old = self._cache[key]
166
+ self._total_size_estimate -= old.size_estimate
167
+ del self._cache[key]
168
+
169
+ if len(self._cache) >= self.max_size:
170
+ _, evicted = self._cache.popitem(last=False)
171
+ self._total_size_estimate -= evicted.size_estimate
172
+ self._evictions += 1
173
+
174
+ self._cache[key] = CacheEntry(value, size_estimate)
175
+ self._total_size_estimate += size_estimate
176
+ finally:
177
+ if self._lock:
178
+ self._lock.release()
179
+
180
+ # ------------------------------------------------------------------
181
+ # Maintenance
182
+ # ------------------------------------------------------------------
183
+
184
+ def invalidate(self, query: str, **kwargs: Any) -> bool:
185
+ """Remove entry for *query*. Returns ``True`` if it existed."""
186
+ key = self._hash_key(query, **kwargs)
187
+ if self._lock:
188
+ self._lock.acquire()
189
+ try:
190
+ if key in self._cache:
191
+ entry = self._cache.pop(key)
192
+ self._total_size_estimate -= entry.size_estimate
193
+ return True
194
+ return False
195
+ finally:
196
+ if self._lock:
197
+ self._lock.release()
198
+
199
+ def clear(self) -> None:
200
+ """Remove all entries."""
201
+ if self._lock:
202
+ self._lock.acquire()
203
+ try:
204
+ self._cache.clear()
205
+ self._total_size_estimate = 0
206
+ finally:
207
+ if self._lock:
208
+ self._lock.release()
209
+
210
+ def evict_expired(self) -> int:
211
+ """Manually remove all expired entries. Returns count removed."""
212
+ if self.ttl_seconds is None:
213
+ return 0
214
+ if self._lock:
215
+ self._lock.acquire()
216
+ try:
217
+ expired = [
218
+ k for k, e in self._cache.items()
219
+ if e.is_expired(self.ttl_seconds)
220
+ ]
221
+ for k in expired:
222
+ entry = self._cache.pop(k)
223
+ self._total_size_estimate -= entry.size_estimate
224
+ return len(expired)
225
+ finally:
226
+ if self._lock:
227
+ self._lock.release()
228
+
229
+ # ------------------------------------------------------------------
230
+ # Stats
231
+ # ------------------------------------------------------------------
232
+
233
+ def get_stats(self) -> Dict[str, Any]:
234
+ """Return cache statistics snapshot."""
235
+ total = self._hits + self._misses
236
+ hit_rate = self._hits / total if total > 0 else 0.0
237
+
238
+ avg_access = 0.0
239
+ if self._cache:
240
+ avg_access = sum(e.access_count for e in self._cache.values()) / len(self._cache)
241
+
242
+ return {
243
+ "max_size": self.max_size,
244
+ "current_size": len(self._cache),
245
+ "ttl_seconds": self.ttl_seconds,
246
+ "hits": self._hits,
247
+ "misses": self._misses,
248
+ "hit_rate": hit_rate,
249
+ "evictions": self._evictions,
250
+ "total_size_estimate_kb": self._total_size_estimate / 1024,
251
+ "avg_access_count": avg_access,
252
+ "thread_safe": self.thread_safe,
253
+ }
254
+
255
+ def get_top_queries(self, limit: int = 10) -> List[Tuple[str, int]]:
256
+ """Return the *limit* most-accessed cache keys."""
257
+ if self._lock:
258
+ self._lock.acquire()
259
+ try:
260
+ items = [
261
+ (k, e.access_count) for k, e in self._cache.items()
262
+ ]
263
+ items.sort(key=lambda x: x[1], reverse=True)
264
+ return items[:limit]
265
+ finally:
266
+ if self._lock:
267
+ self._lock.release()
@@ -0,0 +1,381 @@
1
+ # Copyright (c) 2026 Varun Pratap Bhardwaj / Qualixar
2
+ # Licensed under the MIT License - see LICENSE file
3
+ # Part of SuperLocalMemory V3 | https://qualixar.com | https://varunpratap.com
4
+ """EventBus -- Real-time event broadcasting for memory operations.
5
+
6
+ Thread-safe singleton per DB path. In-memory deque buffer + SQLite persistence.
7
+ Listener callbacks run on the emitter's thread.
8
+ """
9
+
10
+ import json
11
+ import logging
12
+ import sqlite3
13
+ import threading
14
+ from collections import deque
15
+ from datetime import datetime, timedelta
16
+ from pathlib import Path
17
+ from typing import Any, Callable, Dict, List, Optional
18
+
19
+ logger = logging.getLogger("superlocalmemory.events")
20
+
21
+ # Default retention windows (hours)
22
+ DEFAULT_HOT_HOURS = 48
23
+ DEFAULT_WARM_HOURS = 14 * 24 # 14 days
24
+ DEFAULT_COLD_HOURS = 30 * 24 # 30 days
25
+
26
+ # In-memory buffer size for real-time delivery
27
+ EVENT_BUFFER_SIZE = 200
28
+
29
+ # Valid event types (V3 superset)
30
+ VALID_EVENT_TYPES = frozenset([
31
+ "memory.stored", # New memory written (was memory.created in V2)
32
+ "memory.updated", # Existing memory modified
33
+ "memory.deleted", # Memory removed
34
+ "memory.recalled", # Memory retrieved by an agent
35
+ "graph.updated", # Knowledge graph rebuilt
36
+ "pattern.learned", # New pattern detected
37
+ "agent.connected", # New agent connects
38
+ "agent.disconnected", # Agent disconnects
39
+ "trust.signal", # V3: trust score change
40
+ "compliance.audit", # V3: compliance event logged
41
+ "learning.feedback", # V3: learning feedback received
42
+ ])
43
+
44
+
45
+ class EventBus:
46
+ """
47
+ Central event bus for SuperLocalMemory V3.
48
+
49
+ Singleton per database path. Emits events to persistent storage and
50
+ in-memory listeners simultaneously. Thread-safe.
51
+ """
52
+
53
+ _instances: Dict[str, "EventBus"] = {}
54
+ _instances_lock = threading.Lock()
55
+
56
+ @classmethod
57
+ def get_instance(cls, db_path: Optional[Path] = None) -> "EventBus":
58
+ """Get or create the singleton EventBus for a database path."""
59
+ if db_path is None:
60
+ db_path = Path.home() / ".superlocalmemory" / "memory.db"
61
+
62
+ key = str(db_path)
63
+ with cls._instances_lock:
64
+ if key not in cls._instances:
65
+ cls._instances[key] = cls(db_path)
66
+ return cls._instances[key]
67
+
68
+ @classmethod
69
+ def reset_instance(cls, db_path: Optional[Path] = None) -> None:
70
+ """Remove and close a singleton instance. Used for testing."""
71
+ with cls._instances_lock:
72
+ if db_path is None:
73
+ cls._instances.clear()
74
+ else:
75
+ key = str(db_path)
76
+ if key in cls._instances:
77
+ del cls._instances[key]
78
+
79
+ def __init__(self, db_path: Path) -> None:
80
+ """Initialize EventBus. Prefer get_instance() over direct construction."""
81
+ self.db_path = Path(db_path)
82
+ self._buffer: deque = deque(maxlen=EVENT_BUFFER_SIZE)
83
+ self._buffer_lock = threading.Lock()
84
+ self._event_counter = 0
85
+ self._counter_lock = threading.Lock()
86
+ self._listeners: List[Callable[[dict], None]] = []
87
+ self._listeners_lock = threading.Lock()
88
+ self._write_count = 0
89
+ self._last_prune = datetime.now()
90
+ self._init_schema()
91
+ logger.info("EventBus initialized: db=%s", self.db_path)
92
+
93
+ def _init_schema(self) -> None:
94
+ """Create the memory_events table if it does not exist."""
95
+ conn = sqlite3.connect(str(self.db_path))
96
+ try:
97
+ cur = conn.cursor()
98
+ cur.execute("""
99
+ CREATE TABLE IF NOT EXISTS memory_events (
100
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
101
+ event_type TEXT NOT NULL,
102
+ memory_id INTEGER,
103
+ source_agent TEXT DEFAULT 'user',
104
+ source_protocol TEXT DEFAULT 'internal',
105
+ payload TEXT,
106
+ importance INTEGER DEFAULT 5,
107
+ tier TEXT DEFAULT 'hot',
108
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
109
+ )
110
+ """)
111
+ cur.execute("CREATE INDEX IF NOT EXISTS idx_events_type ON memory_events(event_type)")
112
+ cur.execute("CREATE INDEX IF NOT EXISTS idx_events_created ON memory_events(created_at)")
113
+ cur.execute("CREATE INDEX IF NOT EXISTS idx_events_tier ON memory_events(tier)")
114
+ conn.commit()
115
+ finally:
116
+ conn.close()
117
+
118
+ def emit(
119
+ self,
120
+ event_type: str,
121
+ payload: Optional[Dict[str, Any]] = None,
122
+ memory_id: Optional[int] = None,
123
+ source_agent: str = "user",
124
+ source_protocol: str = "internal",
125
+ importance: int = 5,
126
+ ) -> Optional[int]:
127
+ """Emit an event to all subscribers and persist to database."""
128
+ if event_type not in VALID_EVENT_TYPES:
129
+ raise ValueError(
130
+ f"Invalid event type: {event_type}. "
131
+ f"Valid: {', '.join(sorted(VALID_EVENT_TYPES))}"
132
+ )
133
+
134
+ importance = max(1, min(10, importance))
135
+
136
+ now = datetime.now().isoformat()
137
+ with self._counter_lock:
138
+ self._event_counter += 1
139
+ seq = self._event_counter
140
+
141
+ event: Dict[str, Any] = {
142
+ "seq": seq,
143
+ "event_type": event_type,
144
+ "memory_id": memory_id,
145
+ "source_agent": source_agent,
146
+ "source_protocol": source_protocol,
147
+ "payload": payload or {},
148
+ "importance": importance,
149
+ "timestamp": now,
150
+ }
151
+
152
+ # 1. Persist
153
+ event_id = self._persist_event(event)
154
+ if event_id is not None:
155
+ event["id"] = event_id
156
+
157
+ # 2. Buffer
158
+ with self._buffer_lock:
159
+ self._buffer.append(event)
160
+
161
+ # 3. Notify
162
+ self._notify_listeners(event)
163
+
164
+ logger.debug(
165
+ "Event emitted: type=%s id=%s memory_id=%s",
166
+ event_type, event_id, memory_id,
167
+ )
168
+
169
+ # Auto-prune heuristic
170
+ self._write_count += 1
171
+ if (
172
+ self._write_count >= 100
173
+ or (datetime.now() - self._last_prune).total_seconds() > 86400
174
+ ):
175
+ try:
176
+ self.prune_events()
177
+ except Exception:
178
+ pass
179
+ self._write_count = 0
180
+ self._last_prune = datetime.now()
181
+
182
+ return event_id
183
+
184
+ # Alias for V3 compatibility
185
+ publish = emit
186
+
187
+ def _persist_event(self, event: dict) -> Optional[int]:
188
+ """Persist event to the memory_events table. Returns row id or None."""
189
+ try:
190
+ conn = sqlite3.connect(str(self.db_path))
191
+ try:
192
+ cur = conn.cursor()
193
+ cur.execute(
194
+ "INSERT INTO memory_events (event_type, memory_id, source_agent,"
195
+ " source_protocol, payload, importance, tier, created_at)"
196
+ " VALUES (?, ?, ?, ?, ?, ?, 'hot', ?)",
197
+ (event["event_type"], event.get("memory_id"),
198
+ event["source_agent"], event["source_protocol"],
199
+ json.dumps(event["payload"]), event["importance"],
200
+ event["timestamp"]),
201
+ )
202
+ conn.commit()
203
+ return cur.lastrowid
204
+ finally:
205
+ conn.close()
206
+ except Exception as exc:
207
+ logger.error("Failed to persist event: %s", exc)
208
+ return None
209
+
210
+ def add_listener(self, callback: Callable[[dict], None]) -> None:
211
+ """Register a listener that receives every emitted event."""
212
+ with self._listeners_lock:
213
+ self._listeners.append(callback)
214
+
215
+ # Alias
216
+ subscribe = add_listener
217
+
218
+ def remove_listener(self, callback: Callable[[dict], None]) -> None:
219
+ """Remove a previously registered listener."""
220
+ with self._listeners_lock:
221
+ try:
222
+ self._listeners.remove(callback)
223
+ except ValueError:
224
+ pass
225
+
226
+ # Alias
227
+ unsubscribe = remove_listener
228
+
229
+ def _notify_listeners(self, event: dict) -> None:
230
+ """Call all registered listeners. Errors are logged, not raised."""
231
+ with self._listeners_lock:
232
+ listeners = list(self._listeners)
233
+
234
+ for listener in listeners:
235
+ try:
236
+ listener(event)
237
+ except Exception as exc:
238
+ logger.error("Event listener failed: %s", exc)
239
+
240
+ def get_recent_events(
241
+ self,
242
+ since_id: Optional[int] = None,
243
+ limit: int = 50,
244
+ event_type: Optional[str] = None,
245
+ ) -> List[dict]:
246
+ """Get recent events from the database."""
247
+ limit = min(limit, 200)
248
+
249
+ try:
250
+ conn = sqlite3.connect(str(self.db_path))
251
+ try:
252
+ cur = conn.cursor()
253
+
254
+ query = ("SELECT id, event_type, memory_id, source_agent,"
255
+ " source_protocol, payload, importance, tier, created_at"
256
+ " FROM memory_events WHERE 1=1")
257
+ params: List[Any] = []
258
+
259
+ if since_id is not None:
260
+ query += " AND id > ?"
261
+ params.append(since_id)
262
+
263
+ if event_type:
264
+ query += " AND event_type = ?"
265
+ params.append(event_type)
266
+
267
+ query += " ORDER BY id ASC LIMIT ?"
268
+ params.append(limit)
269
+
270
+ cur.execute(query, params)
271
+ rows = cur.fetchall()
272
+ finally:
273
+ conn.close()
274
+
275
+ events: List[dict] = []
276
+ for row in rows:
277
+ try:
278
+ parsed = json.loads(row[5]) if row[5] else {}
279
+ except (json.JSONDecodeError, TypeError):
280
+ parsed = {}
281
+ events.append({
282
+ "id": row[0], "event_type": row[1], "memory_id": row[2],
283
+ "source_agent": row[3], "source_protocol": row[4],
284
+ "payload": parsed, "importance": row[6],
285
+ "tier": row[7], "timestamp": row[8],
286
+ })
287
+ return events
288
+
289
+ except Exception as exc:
290
+ logger.error("Failed to get recent events: %s", exc)
291
+ return []
292
+
293
+ def get_buffered_events(self, since_seq: int = 0) -> List[dict]:
294
+ """Get events from the in-memory buffer (no DB hit)."""
295
+ with self._buffer_lock:
296
+ return [e for e in self._buffer if e.get("seq", 0) > since_seq]
297
+
298
+ def get_event_stats(self) -> dict:
299
+ """Get event system statistics."""
300
+ try:
301
+ conn = sqlite3.connect(str(self.db_path))
302
+ try:
303
+ cur = conn.cursor()
304
+
305
+ total = cur.execute("SELECT COUNT(*) FROM memory_events").fetchone()[0]
306
+ cur.execute("SELECT event_type, COUNT(*) FROM memory_events GROUP BY event_type")
307
+ by_type = dict(cur.fetchall())
308
+ cur.execute("SELECT tier, COUNT(*) FROM memory_events GROUP BY tier")
309
+ by_tier = dict(cur.fetchall())
310
+ cur.execute("SELECT COUNT(*) FROM memory_events WHERE created_at >= datetime('now', '-24 hours')")
311
+ last_24h = cur.fetchone()[0]
312
+ finally:
313
+ conn.close()
314
+
315
+ return {
316
+ **by_type,
317
+ "total_events": total,
318
+ "events_last_24h": last_24h,
319
+ "by_type": by_type,
320
+ "by_tier": by_tier,
321
+ "buffer_size": len(self._buffer),
322
+ "listener_count": len(self._listeners),
323
+ }
324
+
325
+ except Exception as exc:
326
+ logger.error("Failed to get event stats: %s", exc)
327
+ return {"total_events": 0, "error": str(exc)}
328
+
329
+ def prune_events(
330
+ self,
331
+ hot_hours: int = DEFAULT_HOT_HOURS,
332
+ warm_hours: int = DEFAULT_WARM_HOURS,
333
+ cold_hours: int = DEFAULT_COLD_HOURS,
334
+ ) -> dict:
335
+ """Apply tiered retention policy to persisted events."""
336
+ try:
337
+ conn = sqlite3.connect(str(self.db_path))
338
+ try:
339
+ cur = conn.cursor()
340
+ now = datetime.now()
341
+ stats = {"hot_to_warm": 0, "warm_to_cold": 0, "archived": 0}
342
+
343
+ # Hot -> Warm: older than hot_hours, importance < 5
344
+ warm_cutoff = (now - timedelta(hours=hot_hours)).isoformat()
345
+ cur.execute(
346
+ "UPDATE memory_events SET tier = 'warm' "
347
+ "WHERE tier = 'hot' AND created_at < ? AND importance < 5",
348
+ (warm_cutoff,),
349
+ )
350
+ stats["hot_to_warm"] = cur.rowcount
351
+
352
+ # Warm -> Cold: delete warm events older than warm_hours
353
+ cold_cutoff = (now - timedelta(hours=warm_hours)).isoformat()
354
+ cur.execute(
355
+ "DELETE FROM memory_events "
356
+ "WHERE tier = 'warm' AND created_at < ?",
357
+ (cold_cutoff,),
358
+ )
359
+ stats["warm_to_cold"] = cur.rowcount
360
+
361
+ # Archive: delete everything older than cold_hours
362
+ archive_cutoff = (now - timedelta(hours=cold_hours)).isoformat()
363
+ cur.execute(
364
+ "DELETE FROM memory_events WHERE created_at < ?",
365
+ (archive_cutoff,),
366
+ )
367
+ stats["archived"] = cur.rowcount
368
+
369
+ conn.commit()
370
+ finally:
371
+ conn.close()
372
+
373
+ logger.info(
374
+ "Prune complete: hot->warm=%d warm->cold=%d archived=%d",
375
+ stats["hot_to_warm"], stats["warm_to_cold"], stats["archived"],
376
+ )
377
+ return stats
378
+
379
+ except Exception as exc:
380
+ logger.error("Event pruning failed: %s", exc)
381
+ return {"error": str(exc)}