superlocalmemory 2.8.5 → 3.0.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 (434) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/LICENSE +9 -1
  3. package/NOTICE +63 -0
  4. package/README.md +165 -480
  5. package/bin/slm +17 -449
  6. package/bin/slm-npm +2 -2
  7. package/bin/slm.bat +4 -2
  8. package/conftest.py +5 -0
  9. package/docs/api-reference.md +284 -0
  10. package/docs/architecture.md +149 -0
  11. package/docs/auto-memory.md +150 -0
  12. package/docs/cli-reference.md +276 -0
  13. package/docs/compliance.md +191 -0
  14. package/docs/configuration.md +182 -0
  15. package/docs/getting-started.md +102 -0
  16. package/docs/ide-setup.md +261 -0
  17. package/docs/mcp-tools.md +220 -0
  18. package/docs/migration-from-v2.md +170 -0
  19. package/docs/profiles.md +173 -0
  20. package/docs/troubleshooting.md +310 -0
  21. package/{configs → ide/configs}/antigravity-mcp.json +3 -3
  22. package/ide/configs/chatgpt-desktop-mcp.json +16 -0
  23. package/{configs → ide/configs}/claude-desktop-mcp.json +3 -3
  24. package/{configs → ide/configs}/codex-mcp.toml +4 -4
  25. package/{configs → ide/configs}/continue-mcp.yaml +4 -3
  26. package/{configs → ide/configs}/continue-skills.yaml +6 -6
  27. package/ide/configs/cursor-mcp.json +15 -0
  28. package/{configs → ide/configs}/gemini-cli-mcp.json +2 -2
  29. package/{configs → ide/configs}/jetbrains-mcp.json +2 -2
  30. package/{configs → ide/configs}/opencode-mcp.json +2 -2
  31. package/{configs → ide/configs}/perplexity-mcp.json +2 -2
  32. package/{configs → ide/configs}/vscode-copilot-mcp.json +2 -2
  33. package/{configs → ide/configs}/windsurf-mcp.json +3 -3
  34. package/{configs → ide/configs}/zed-mcp.json +2 -2
  35. package/{hooks → ide/hooks}/context-hook.js +9 -20
  36. package/ide/hooks/memory-list-skill.js +70 -0
  37. package/ide/hooks/memory-profile-skill.js +101 -0
  38. package/ide/hooks/memory-recall-skill.js +62 -0
  39. package/ide/hooks/memory-remember-skill.js +68 -0
  40. package/ide/hooks/memory-reset-skill.js +160 -0
  41. package/{hooks → ide/hooks}/post-recall-hook.js +2 -2
  42. package/ide/integrations/langchain/README.md +106 -0
  43. package/ide/integrations/langchain/langchain_superlocalmemory/__init__.py +9 -0
  44. package/ide/integrations/langchain/langchain_superlocalmemory/chat_message_history.py +201 -0
  45. package/ide/integrations/langchain/pyproject.toml +38 -0
  46. package/{src/learning → ide/integrations/langchain}/tests/__init__.py +1 -0
  47. package/ide/integrations/langchain/tests/test_chat_message_history.py +215 -0
  48. package/ide/integrations/langchain/tests/test_security.py +117 -0
  49. package/ide/integrations/llamaindex/README.md +81 -0
  50. package/ide/integrations/llamaindex/llama_index/storage/chat_store/superlocalmemory/__init__.py +9 -0
  51. package/ide/integrations/llamaindex/llama_index/storage/chat_store/superlocalmemory/base.py +316 -0
  52. package/ide/integrations/llamaindex/pyproject.toml +43 -0
  53. package/{src/lifecycle → ide/integrations/llamaindex}/tests/__init__.py +1 -2
  54. package/ide/integrations/llamaindex/tests/test_chat_store.py +294 -0
  55. package/ide/integrations/llamaindex/tests/test_security.py +241 -0
  56. package/{skills → ide/skills}/slm-build-graph/SKILL.md +6 -6
  57. package/{skills → ide/skills}/slm-list-recent/SKILL.md +5 -5
  58. package/{skills → ide/skills}/slm-recall/SKILL.md +5 -5
  59. package/{skills → ide/skills}/slm-remember/SKILL.md +6 -6
  60. package/{skills → ide/skills}/slm-show-patterns/SKILL.md +7 -7
  61. package/{skills → ide/skills}/slm-status/SKILL.md +9 -9
  62. package/{skills → ide/skills}/slm-switch-profile/SKILL.md +9 -9
  63. package/package.json +13 -22
  64. package/pyproject.toml +85 -0
  65. package/scripts/build-dmg.sh +417 -0
  66. package/scripts/install-skills.ps1 +334 -0
  67. package/{install.ps1 → scripts/install.ps1} +36 -4
  68. package/{install.sh → scripts/install.sh} +14 -13
  69. package/scripts/postinstall.js +2 -2
  70. package/scripts/start-dashboard.ps1 +52 -0
  71. package/scripts/start-dashboard.sh +41 -0
  72. package/scripts/sync-wiki.ps1 +127 -0
  73. package/scripts/sync-wiki.sh +82 -0
  74. package/scripts/test-dmg.sh +161 -0
  75. package/scripts/test-npm-package.ps1 +252 -0
  76. package/scripts/test-npm-package.sh +207 -0
  77. package/scripts/verify-install.ps1 +294 -0
  78. package/scripts/verify-install.sh +266 -0
  79. package/src/superlocalmemory/__init__.py +0 -0
  80. package/src/superlocalmemory/attribution/__init__.py +9 -0
  81. package/src/superlocalmemory/attribution/mathematical_dna.py +235 -0
  82. package/src/superlocalmemory/attribution/signer.py +153 -0
  83. package/src/superlocalmemory/attribution/watermark.py +189 -0
  84. package/src/superlocalmemory/cli/__init__.py +5 -0
  85. package/src/superlocalmemory/cli/commands.py +245 -0
  86. package/src/superlocalmemory/cli/main.py +89 -0
  87. package/src/superlocalmemory/cli/migrate_cmd.py +55 -0
  88. package/src/superlocalmemory/cli/post_install.py +99 -0
  89. package/src/superlocalmemory/cli/setup_wizard.py +129 -0
  90. package/src/superlocalmemory/compliance/__init__.py +0 -0
  91. package/src/superlocalmemory/compliance/abac.py +204 -0
  92. package/src/superlocalmemory/compliance/audit.py +314 -0
  93. package/src/superlocalmemory/compliance/eu_ai_act.py +131 -0
  94. package/src/superlocalmemory/compliance/gdpr.py +294 -0
  95. package/src/superlocalmemory/compliance/lifecycle.py +158 -0
  96. package/src/superlocalmemory/compliance/retention.py +232 -0
  97. package/src/superlocalmemory/compliance/scheduler.py +148 -0
  98. package/src/superlocalmemory/core/__init__.py +0 -0
  99. package/src/superlocalmemory/core/config.py +391 -0
  100. package/src/superlocalmemory/core/embeddings.py +293 -0
  101. package/src/superlocalmemory/core/engine.py +701 -0
  102. package/src/superlocalmemory/core/hooks.py +65 -0
  103. package/src/superlocalmemory/core/maintenance.py +172 -0
  104. package/src/superlocalmemory/core/modes.py +140 -0
  105. package/src/superlocalmemory/core/profiles.py +234 -0
  106. package/src/superlocalmemory/core/registry.py +117 -0
  107. package/src/superlocalmemory/dynamics/__init__.py +0 -0
  108. package/src/superlocalmemory/dynamics/fisher_langevin_coupling.py +223 -0
  109. package/src/superlocalmemory/encoding/__init__.py +0 -0
  110. package/src/superlocalmemory/encoding/consolidator.py +485 -0
  111. package/src/superlocalmemory/encoding/emotional.py +125 -0
  112. package/src/superlocalmemory/encoding/entity_resolver.py +525 -0
  113. package/src/superlocalmemory/encoding/entropy_gate.py +104 -0
  114. package/src/superlocalmemory/encoding/fact_extractor.py +775 -0
  115. package/src/superlocalmemory/encoding/foresight.py +91 -0
  116. package/src/superlocalmemory/encoding/graph_builder.py +302 -0
  117. package/src/superlocalmemory/encoding/observation_builder.py +160 -0
  118. package/src/superlocalmemory/encoding/scene_builder.py +183 -0
  119. package/src/superlocalmemory/encoding/signal_inference.py +90 -0
  120. package/src/superlocalmemory/encoding/temporal_parser.py +426 -0
  121. package/src/superlocalmemory/encoding/type_router.py +235 -0
  122. package/src/superlocalmemory/hooks/__init__.py +3 -0
  123. package/src/superlocalmemory/hooks/auto_capture.py +111 -0
  124. package/src/superlocalmemory/hooks/auto_recall.py +93 -0
  125. package/src/superlocalmemory/hooks/ide_connector.py +204 -0
  126. package/src/superlocalmemory/hooks/rules_engine.py +99 -0
  127. package/src/superlocalmemory/infra/__init__.py +3 -0
  128. package/src/superlocalmemory/infra/auth_middleware.py +82 -0
  129. package/src/superlocalmemory/infra/backup.py +317 -0
  130. package/src/superlocalmemory/infra/cache_manager.py +267 -0
  131. package/src/superlocalmemory/infra/event_bus.py +381 -0
  132. package/src/superlocalmemory/infra/rate_limiter.py +135 -0
  133. package/src/{webhook_dispatcher.py → superlocalmemory/infra/webhook_dispatcher.py} +104 -101
  134. package/src/superlocalmemory/learning/__init__.py +0 -0
  135. package/src/superlocalmemory/learning/adaptive.py +172 -0
  136. package/src/superlocalmemory/learning/behavioral.py +490 -0
  137. package/src/superlocalmemory/learning/behavioral_listener.py +94 -0
  138. package/src/superlocalmemory/learning/bootstrap.py +298 -0
  139. package/src/superlocalmemory/learning/cross_project.py +399 -0
  140. package/src/superlocalmemory/learning/database.py +376 -0
  141. package/src/superlocalmemory/learning/engagement.py +323 -0
  142. package/src/superlocalmemory/learning/features.py +138 -0
  143. package/src/superlocalmemory/learning/feedback.py +316 -0
  144. package/src/superlocalmemory/learning/outcomes.py +255 -0
  145. package/src/superlocalmemory/learning/project_context.py +366 -0
  146. package/src/superlocalmemory/learning/ranker.py +155 -0
  147. package/src/superlocalmemory/learning/source_quality.py +303 -0
  148. package/src/superlocalmemory/learning/workflows.py +309 -0
  149. package/src/superlocalmemory/llm/__init__.py +0 -0
  150. package/src/superlocalmemory/llm/backbone.py +316 -0
  151. package/src/superlocalmemory/math/__init__.py +0 -0
  152. package/src/superlocalmemory/math/fisher.py +356 -0
  153. package/src/superlocalmemory/math/langevin.py +398 -0
  154. package/src/superlocalmemory/math/sheaf.py +257 -0
  155. package/src/superlocalmemory/mcp/__init__.py +0 -0
  156. package/src/superlocalmemory/mcp/resources.py +245 -0
  157. package/src/superlocalmemory/mcp/server.py +61 -0
  158. package/src/superlocalmemory/mcp/tools.py +18 -0
  159. package/src/superlocalmemory/mcp/tools_core.py +305 -0
  160. package/src/superlocalmemory/mcp/tools_v28.py +223 -0
  161. package/src/superlocalmemory/mcp/tools_v3.py +286 -0
  162. package/src/superlocalmemory/retrieval/__init__.py +0 -0
  163. package/src/superlocalmemory/retrieval/agentic.py +295 -0
  164. package/src/superlocalmemory/retrieval/ann_index.py +223 -0
  165. package/src/superlocalmemory/retrieval/bm25_channel.py +185 -0
  166. package/src/superlocalmemory/retrieval/bridge_discovery.py +170 -0
  167. package/src/superlocalmemory/retrieval/engine.py +390 -0
  168. package/src/superlocalmemory/retrieval/entity_channel.py +179 -0
  169. package/src/superlocalmemory/retrieval/fusion.py +78 -0
  170. package/src/superlocalmemory/retrieval/profile_channel.py +105 -0
  171. package/src/superlocalmemory/retrieval/reranker.py +154 -0
  172. package/src/superlocalmemory/retrieval/semantic_channel.py +232 -0
  173. package/src/superlocalmemory/retrieval/strategy.py +96 -0
  174. package/src/superlocalmemory/retrieval/temporal_channel.py +175 -0
  175. package/src/superlocalmemory/server/__init__.py +1 -0
  176. package/src/superlocalmemory/server/api.py +248 -0
  177. package/src/superlocalmemory/server/routes/__init__.py +4 -0
  178. package/src/superlocalmemory/server/routes/agents.py +107 -0
  179. package/src/superlocalmemory/server/routes/backup.py +91 -0
  180. package/src/superlocalmemory/server/routes/behavioral.py +127 -0
  181. package/src/superlocalmemory/server/routes/compliance.py +160 -0
  182. package/src/superlocalmemory/server/routes/data_io.py +188 -0
  183. package/src/superlocalmemory/server/routes/events.py +183 -0
  184. package/src/superlocalmemory/server/routes/helpers.py +85 -0
  185. package/src/superlocalmemory/server/routes/learning.py +273 -0
  186. package/src/superlocalmemory/server/routes/lifecycle.py +116 -0
  187. package/src/superlocalmemory/server/routes/memories.py +399 -0
  188. package/src/superlocalmemory/server/routes/profiles.py +219 -0
  189. package/src/superlocalmemory/server/routes/stats.py +346 -0
  190. package/src/superlocalmemory/server/routes/v3_api.py +365 -0
  191. package/src/superlocalmemory/server/routes/ws.py +82 -0
  192. package/src/superlocalmemory/server/security_middleware.py +57 -0
  193. package/src/superlocalmemory/server/ui.py +245 -0
  194. package/src/superlocalmemory/storage/__init__.py +0 -0
  195. package/src/superlocalmemory/storage/access_control.py +182 -0
  196. package/src/superlocalmemory/storage/database.py +594 -0
  197. package/src/superlocalmemory/storage/migrations.py +303 -0
  198. package/src/superlocalmemory/storage/models.py +406 -0
  199. package/src/superlocalmemory/storage/schema.py +726 -0
  200. package/src/superlocalmemory/storage/v2_migrator.py +317 -0
  201. package/src/superlocalmemory/trust/__init__.py +0 -0
  202. package/src/superlocalmemory/trust/gate.py +130 -0
  203. package/src/superlocalmemory/trust/provenance.py +124 -0
  204. package/src/superlocalmemory/trust/scorer.py +347 -0
  205. package/src/superlocalmemory/trust/signals.py +153 -0
  206. package/ui/index.html +278 -5
  207. package/ui/js/auto-settings.js +70 -0
  208. package/ui/js/dashboard.js +90 -0
  209. package/ui/js/fact-detail.js +92 -0
  210. package/ui/js/feedback.js +2 -2
  211. package/ui/js/ide-status.js +102 -0
  212. package/ui/js/math-health.js +98 -0
  213. package/ui/js/recall-lab.js +127 -0
  214. package/ui/js/settings.js +2 -2
  215. package/ui/js/trust-dashboard.js +73 -0
  216. package/api_server.py +0 -724
  217. package/bin/aider-smart +0 -72
  218. package/bin/superlocalmemoryv2-learning +0 -4
  219. package/bin/superlocalmemoryv2-list +0 -3
  220. package/bin/superlocalmemoryv2-patterns +0 -4
  221. package/bin/superlocalmemoryv2-profile +0 -3
  222. package/bin/superlocalmemoryv2-recall +0 -3
  223. package/bin/superlocalmemoryv2-remember +0 -3
  224. package/bin/superlocalmemoryv2-reset +0 -3
  225. package/bin/superlocalmemoryv2-status +0 -3
  226. package/configs/chatgpt-desktop-mcp.json +0 -16
  227. package/configs/cursor-mcp.json +0 -15
  228. package/docs/SECURITY-QUICK-REFERENCE.md +0 -214
  229. package/hooks/memory-list-skill.js +0 -139
  230. package/hooks/memory-profile-skill.js +0 -273
  231. package/hooks/memory-recall-skill.js +0 -114
  232. package/hooks/memory-remember-skill.js +0 -127
  233. package/hooks/memory-reset-skill.js +0 -274
  234. package/mcp_server.py +0 -1800
  235. package/requirements-core.txt +0 -22
  236. package/requirements-learning.txt +0 -12
  237. package/requirements.txt +0 -12
  238. package/src/agent_registry.py +0 -411
  239. package/src/auth_middleware.py +0 -61
  240. package/src/auto_backup.py +0 -459
  241. package/src/behavioral/__init__.py +0 -49
  242. package/src/behavioral/behavioral_listener.py +0 -203
  243. package/src/behavioral/behavioral_patterns.py +0 -275
  244. package/src/behavioral/cross_project_transfer.py +0 -206
  245. package/src/behavioral/outcome_inference.py +0 -194
  246. package/src/behavioral/outcome_tracker.py +0 -193
  247. package/src/behavioral/tests/__init__.py +0 -4
  248. package/src/behavioral/tests/test_behavioral_integration.py +0 -108
  249. package/src/behavioral/tests/test_behavioral_patterns.py +0 -150
  250. package/src/behavioral/tests/test_cross_project_transfer.py +0 -142
  251. package/src/behavioral/tests/test_mcp_behavioral.py +0 -139
  252. package/src/behavioral/tests/test_mcp_report_outcome.py +0 -117
  253. package/src/behavioral/tests/test_outcome_inference.py +0 -107
  254. package/src/behavioral/tests/test_outcome_tracker.py +0 -96
  255. package/src/cache_manager.py +0 -518
  256. package/src/compliance/__init__.py +0 -48
  257. package/src/compliance/abac_engine.py +0 -149
  258. package/src/compliance/abac_middleware.py +0 -116
  259. package/src/compliance/audit_db.py +0 -215
  260. package/src/compliance/audit_logger.py +0 -148
  261. package/src/compliance/retention_manager.py +0 -289
  262. package/src/compliance/retention_scheduler.py +0 -186
  263. package/src/compliance/tests/__init__.py +0 -4
  264. package/src/compliance/tests/test_abac_enforcement.py +0 -95
  265. package/src/compliance/tests/test_abac_engine.py +0 -124
  266. package/src/compliance/tests/test_abac_mcp_integration.py +0 -118
  267. package/src/compliance/tests/test_audit_db.py +0 -123
  268. package/src/compliance/tests/test_audit_logger.py +0 -98
  269. package/src/compliance/tests/test_mcp_audit.py +0 -128
  270. package/src/compliance/tests/test_mcp_retention_policy.py +0 -125
  271. package/src/compliance/tests/test_retention_manager.py +0 -131
  272. package/src/compliance/tests/test_retention_scheduler.py +0 -99
  273. package/src/compression/__init__.py +0 -25
  274. package/src/compression/cli.py +0 -150
  275. package/src/compression/cold_storage.py +0 -217
  276. package/src/compression/config.py +0 -72
  277. package/src/compression/orchestrator.py +0 -133
  278. package/src/compression/tier2_compressor.py +0 -228
  279. package/src/compression/tier3_compressor.py +0 -153
  280. package/src/compression/tier_classifier.py +0 -148
  281. package/src/db_connection_manager.py +0 -536
  282. package/src/embedding_engine.py +0 -63
  283. package/src/embeddings/__init__.py +0 -47
  284. package/src/embeddings/cache.py +0 -70
  285. package/src/embeddings/cli.py +0 -113
  286. package/src/embeddings/constants.py +0 -47
  287. package/src/embeddings/database.py +0 -91
  288. package/src/embeddings/engine.py +0 -247
  289. package/src/embeddings/model_loader.py +0 -145
  290. package/src/event_bus.py +0 -562
  291. package/src/graph/__init__.py +0 -36
  292. package/src/graph/build_helpers.py +0 -74
  293. package/src/graph/cli.py +0 -87
  294. package/src/graph/cluster_builder.py +0 -188
  295. package/src/graph/cluster_summary.py +0 -148
  296. package/src/graph/constants.py +0 -47
  297. package/src/graph/edge_builder.py +0 -162
  298. package/src/graph/entity_extractor.py +0 -95
  299. package/src/graph/graph_core.py +0 -226
  300. package/src/graph/graph_search.py +0 -231
  301. package/src/graph/hierarchical.py +0 -207
  302. package/src/graph/schema.py +0 -99
  303. package/src/graph_engine.py +0 -52
  304. package/src/hnsw_index.py +0 -628
  305. package/src/hybrid_search.py +0 -46
  306. package/src/learning/__init__.py +0 -217
  307. package/src/learning/adaptive_ranker.py +0 -682
  308. package/src/learning/bootstrap/__init__.py +0 -69
  309. package/src/learning/bootstrap/constants.py +0 -93
  310. package/src/learning/bootstrap/db_queries.py +0 -316
  311. package/src/learning/bootstrap/sampling.py +0 -82
  312. package/src/learning/bootstrap/text_utils.py +0 -71
  313. package/src/learning/cross_project_aggregator.py +0 -857
  314. package/src/learning/db/__init__.py +0 -40
  315. package/src/learning/db/constants.py +0 -44
  316. package/src/learning/db/schema.py +0 -279
  317. package/src/learning/engagement_tracker.py +0 -628
  318. package/src/learning/feature_extractor.py +0 -708
  319. package/src/learning/feedback_collector.py +0 -806
  320. package/src/learning/learning_db.py +0 -915
  321. package/src/learning/project_context_manager.py +0 -572
  322. package/src/learning/ranking/__init__.py +0 -33
  323. package/src/learning/ranking/constants.py +0 -84
  324. package/src/learning/ranking/helpers.py +0 -278
  325. package/src/learning/source_quality_scorer.py +0 -676
  326. package/src/learning/synthetic_bootstrap.py +0 -755
  327. package/src/learning/tests/test_adaptive_ranker.py +0 -325
  328. package/src/learning/tests/test_adaptive_ranker_v28.py +0 -60
  329. package/src/learning/tests/test_aggregator.py +0 -306
  330. package/src/learning/tests/test_auto_retrain_v28.py +0 -35
  331. package/src/learning/tests/test_e2e_ranking_v28.py +0 -82
  332. package/src/learning/tests/test_feature_extractor_v28.py +0 -93
  333. package/src/learning/tests/test_feedback_collector.py +0 -294
  334. package/src/learning/tests/test_learning_db.py +0 -602
  335. package/src/learning/tests/test_learning_db_v28.py +0 -110
  336. package/src/learning/tests/test_learning_init_v28.py +0 -48
  337. package/src/learning/tests/test_outcome_signals.py +0 -48
  338. package/src/learning/tests/test_project_context.py +0 -292
  339. package/src/learning/tests/test_schema_migration.py +0 -319
  340. package/src/learning/tests/test_signal_inference.py +0 -397
  341. package/src/learning/tests/test_source_quality.py +0 -351
  342. package/src/learning/tests/test_synthetic_bootstrap.py +0 -429
  343. package/src/learning/tests/test_workflow_miner.py +0 -318
  344. package/src/learning/workflow_pattern_miner.py +0 -655
  345. package/src/lifecycle/__init__.py +0 -54
  346. package/src/lifecycle/bounded_growth.py +0 -239
  347. package/src/lifecycle/compaction_engine.py +0 -226
  348. package/src/lifecycle/lifecycle_engine.py +0 -355
  349. package/src/lifecycle/lifecycle_evaluator.py +0 -257
  350. package/src/lifecycle/lifecycle_scheduler.py +0 -130
  351. package/src/lifecycle/retention_policy.py +0 -285
  352. package/src/lifecycle/tests/test_bounded_growth.py +0 -193
  353. package/src/lifecycle/tests/test_compaction.py +0 -179
  354. package/src/lifecycle/tests/test_lifecycle_engine.py +0 -137
  355. package/src/lifecycle/tests/test_lifecycle_evaluation.py +0 -177
  356. package/src/lifecycle/tests/test_lifecycle_scheduler.py +0 -127
  357. package/src/lifecycle/tests/test_lifecycle_search.py +0 -109
  358. package/src/lifecycle/tests/test_mcp_compact.py +0 -149
  359. package/src/lifecycle/tests/test_mcp_lifecycle_status.py +0 -114
  360. package/src/lifecycle/tests/test_retention_policy.py +0 -162
  361. package/src/mcp_tools_v28.py +0 -281
  362. package/src/memory/__init__.py +0 -36
  363. package/src/memory/cli.py +0 -205
  364. package/src/memory/constants.py +0 -39
  365. package/src/memory/helpers.py +0 -28
  366. package/src/memory/schema.py +0 -166
  367. package/src/memory-profiles.py +0 -595
  368. package/src/memory-reset.py +0 -491
  369. package/src/memory_compression.py +0 -989
  370. package/src/memory_store_v2.py +0 -1155
  371. package/src/migrate_v1_to_v2.py +0 -629
  372. package/src/pattern_learner.py +0 -34
  373. package/src/patterns/__init__.py +0 -24
  374. package/src/patterns/analyzers.py +0 -251
  375. package/src/patterns/learner.py +0 -271
  376. package/src/patterns/scoring.py +0 -171
  377. package/src/patterns/store.py +0 -225
  378. package/src/patterns/terminology.py +0 -140
  379. package/src/provenance_tracker.py +0 -312
  380. package/src/qualixar_attribution.py +0 -139
  381. package/src/qualixar_watermark.py +0 -78
  382. package/src/query_optimizer.py +0 -511
  383. package/src/rate_limiter.py +0 -83
  384. package/src/search/__init__.py +0 -20
  385. package/src/search/cli.py +0 -77
  386. package/src/search/constants.py +0 -26
  387. package/src/search/engine.py +0 -241
  388. package/src/search/fusion.py +0 -122
  389. package/src/search/index_loader.py +0 -114
  390. package/src/search/methods.py +0 -162
  391. package/src/search_engine_v2.py +0 -401
  392. package/src/setup_validator.py +0 -482
  393. package/src/subscription_manager.py +0 -391
  394. package/src/tree/__init__.py +0 -59
  395. package/src/tree/builder.py +0 -185
  396. package/src/tree/nodes.py +0 -202
  397. package/src/tree/queries.py +0 -257
  398. package/src/tree/schema.py +0 -80
  399. package/src/tree_manager.py +0 -19
  400. package/src/trust/__init__.py +0 -45
  401. package/src/trust/constants.py +0 -66
  402. package/src/trust/queries.py +0 -157
  403. package/src/trust/schema.py +0 -95
  404. package/src/trust/scorer.py +0 -299
  405. package/src/trust/signals.py +0 -95
  406. package/src/trust_scorer.py +0 -44
  407. package/ui/app.js +0 -1588
  408. package/ui/js/graph-cytoscape-monolithic-backup.js +0 -1168
  409. package/ui/js/graph-cytoscape.js +0 -1168
  410. package/ui/js/graph-d3-backup.js +0 -32
  411. package/ui/js/graph.js +0 -32
  412. package/ui_server.py +0 -266
  413. /package/docs/{ACCESSIBILITY.md → v2-archive/ACCESSIBILITY.md} +0 -0
  414. /package/docs/{ARCHITECTURE.md → v2-archive/ARCHITECTURE.md} +0 -0
  415. /package/docs/{CLI-COMMANDS-REFERENCE.md → v2-archive/CLI-COMMANDS-REFERENCE.md} +0 -0
  416. /package/docs/{COMPRESSION-README.md → v2-archive/COMPRESSION-README.md} +0 -0
  417. /package/docs/{FRAMEWORK-INTEGRATIONS.md → v2-archive/FRAMEWORK-INTEGRATIONS.md} +0 -0
  418. /package/docs/{MCP-MANUAL-SETUP.md → v2-archive/MCP-MANUAL-SETUP.md} +0 -0
  419. /package/docs/{MCP-TROUBLESHOOTING.md → v2-archive/MCP-TROUBLESHOOTING.md} +0 -0
  420. /package/docs/{PATTERN-LEARNING.md → v2-archive/PATTERN-LEARNING.md} +0 -0
  421. /package/docs/{PROFILES-GUIDE.md → v2-archive/PROFILES-GUIDE.md} +0 -0
  422. /package/docs/{RESET-GUIDE.md → v2-archive/RESET-GUIDE.md} +0 -0
  423. /package/docs/{SEARCH-ENGINE-V2.2.0.md → v2-archive/SEARCH-ENGINE-V2.2.0.md} +0 -0
  424. /package/docs/{SEARCH-INTEGRATION-GUIDE.md → v2-archive/SEARCH-INTEGRATION-GUIDE.md} +0 -0
  425. /package/docs/{UI-SERVER.md → v2-archive/UI-SERVER.md} +0 -0
  426. /package/docs/{UNIVERSAL-INTEGRATION.md → v2-archive/UNIVERSAL-INTEGRATION.md} +0 -0
  427. /package/docs/{V2.2.0-OPTIONAL-SEARCH.md → v2-archive/V2.2.0-OPTIONAL-SEARCH.md} +0 -0
  428. /package/docs/{WINDOWS-INSTALL-README.txt → v2-archive/WINDOWS-INSTALL-README.txt} +0 -0
  429. /package/docs/{WINDOWS-POST-INSTALL.txt → v2-archive/WINDOWS-POST-INSTALL.txt} +0 -0
  430. /package/docs/{example_graph_usage.py → v2-archive/example_graph_usage.py} +0 -0
  431. /package/{completions → ide/completions}/slm.bash +0 -0
  432. /package/{completions → ide/completions}/slm.zsh +0 -0
  433. /package/{configs → ide/configs}/cody-commands.json +0 -0
  434. /package/{install-skills.sh → scripts/install-skills.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)}