loop-memory 0.4.20__tar.gz → 0.4.22__tar.gz

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 (211) hide show
  1. {loop_memory-0.4.20 → loop_memory-0.4.22}/PKG-INFO +76 -79
  2. {loop_memory-0.4.20 → loop_memory-0.4.22}/README.md +75 -78
  3. {loop_memory-0.4.20 → loop_memory-0.4.22}/loop_memory/cli/commands/read.py +29 -4
  4. {loop_memory-0.4.20 → loop_memory-0.4.22}/loop_memory/cli/commands/serve.py +19 -0
  5. {loop_memory-0.4.20 → loop_memory-0.4.22}/loop_memory/doctor.py +75 -0
  6. {loop_memory-0.4.20 → loop_memory-0.4.22}/loop_memory/mcp/__init__.py +39 -2
  7. {loop_memory-0.4.20 → loop_memory-0.4.22}/loop_memory/sdk.py +117 -13
  8. {loop_memory-0.4.20 → loop_memory-0.4.22}/loop_memory/sdk_extensions.py +55 -13
  9. loop_memory-0.4.22/loop_memory/security/__init__.py +39 -0
  10. loop_memory-0.4.22/loop_memory/security/transport.py +339 -0
  11. {loop_memory-0.4.20 → loop_memory-0.4.22}/loop_memory/serve/routes/memories.py +26 -0
  12. {loop_memory-0.4.20 → loop_memory-0.4.22}/loop_memory/serve/routes/system.py +25 -1
  13. {loop_memory-0.4.20 → loop_memory-0.4.22}/loop_memory/storage/retrieval.py +36 -4
  14. {loop_memory-0.4.20 → loop_memory-0.4.22}/loop_memory/storage/sqlite_store.py +149 -12
  15. {loop_memory-0.4.20 → loop_memory-0.4.22}/loop_memory.egg-info/PKG-INFO +76 -79
  16. {loop_memory-0.4.20 → loop_memory-0.4.22}/loop_memory.egg-info/SOURCES.txt +3 -0
  17. {loop_memory-0.4.20 → loop_memory-0.4.22}/pyproject.toml +1 -1
  18. {loop_memory-0.4.20 → loop_memory-0.4.22}/tests/test_agent_memory_sdk.py +5 -1
  19. {loop_memory-0.4.20 → loop_memory-0.4.22}/tests/test_doctor_2026_10_12.py +8 -7
  20. loop_memory-0.4.22/tests/test_namespace_isolation_2026_11_15.py +569 -0
  21. loop_memory-0.4.22/tests/test_transport_security_2026_11_08.py +456 -0
  22. loop_memory-0.4.20/loop_memory/security/__init__.py +0 -20
  23. {loop_memory-0.4.20 → loop_memory-0.4.22}/LICENSE +0 -0
  24. {loop_memory-0.4.20 → loop_memory-0.4.22}/loop_memory/__init__.py +0 -0
  25. {loop_memory-0.4.20 → loop_memory-0.4.22}/loop_memory/backends/__init__.py +0 -0
  26. {loop_memory-0.4.20 → loop_memory-0.4.22}/loop_memory/backends/embedding.py +0 -0
  27. {loop_memory-0.4.20 → loop_memory-0.4.22}/loop_memory/backends/sentence_embedder.py +0 -0
  28. {loop_memory-0.4.20 → loop_memory-0.4.22}/loop_memory/backends/vector_store.py +0 -0
  29. {loop_memory-0.4.20 → loop_memory-0.4.22}/loop_memory/bench/__init__.py +0 -0
  30. {loop_memory-0.4.20 → loop_memory-0.4.22}/loop_memory/bench/fixtures/default.json +0 -0
  31. {loop_memory-0.4.20 → loop_memory-0.4.22}/loop_memory/bench/runner.py +0 -0
  32. {loop_memory-0.4.20 → loop_memory-0.4.22}/loop_memory/cli/__init__.py +0 -0
  33. {loop_memory-0.4.20 → loop_memory-0.4.22}/loop_memory/cli/_common.py +0 -0
  34. {loop_memory-0.4.20 → loop_memory-0.4.22}/loop_memory/cli/commands/__init__.py +0 -0
  35. {loop_memory-0.4.20 → loop_memory-0.4.22}/loop_memory/cli/commands/bench.py +0 -0
  36. {loop_memory-0.4.20 → loop_memory-0.4.22}/loop_memory/cli/commands/cognitive.py +0 -0
  37. {loop_memory-0.4.20 → loop_memory-0.4.22}/loop_memory/cli/commands/diag.py +0 -0
  38. {loop_memory-0.4.20 → loop_memory-0.4.22}/loop_memory/cli/commands/doctor.py +0 -0
  39. {loop_memory-0.4.20 → loop_memory-0.4.22}/loop_memory/cli/commands/graph.py +0 -0
  40. {loop_memory-0.4.20 → loop_memory-0.4.22}/loop_memory/cli/commands/hooks.py +0 -0
  41. {loop_memory-0.4.20 → loop_memory-0.4.22}/loop_memory/cli/commands/rules.py +0 -0
  42. {loop_memory-0.4.20 → loop_memory-0.4.22}/loop_memory/cli/commands/write.py +0 -0
  43. {loop_memory-0.4.20 → loop_memory-0.4.22}/loop_memory/cli/main.py +0 -0
  44. {loop_memory-0.4.20 → loop_memory-0.4.22}/loop_memory/decorators.py +0 -0
  45. {loop_memory-0.4.20 → loop_memory-0.4.22}/loop_memory/engine/__init__.py +0 -0
  46. {loop_memory-0.4.20 → loop_memory-0.4.22}/loop_memory/engine/loop.py +0 -0
  47. {loop_memory-0.4.20 → loop_memory-0.4.22}/loop_memory/engine/reflect.py +0 -0
  48. {loop_memory-0.4.20 → loop_memory-0.4.22}/loop_memory/examples/__init__.py +0 -0
  49. {loop_memory-0.4.20 → loop_memory-0.4.22}/loop_memory/examples/demo.py +0 -0
  50. {loop_memory-0.4.20 → loop_memory-0.4.22}/loop_memory/export/__init__.py +0 -0
  51. {loop_memory-0.4.20 → loop_memory-0.4.22}/loop_memory/export/memory_md.py +0 -0
  52. {loop_memory-0.4.20 → loop_memory-0.4.22}/loop_memory/graph/__init__.py +0 -0
  53. {loop_memory-0.4.20 → loop_memory-0.4.22}/loop_memory/graph/build.py +0 -0
  54. {loop_memory-0.4.20 → loop_memory-0.4.22}/loop_memory/graph/extract.py +0 -0
  55. {loop_memory-0.4.20 → loop_memory-0.4.22}/loop_memory/ingest/__init__.py +0 -0
  56. {loop_memory-0.4.20 → loop_memory-0.4.22}/loop_memory/ingest/loader.py +0 -0
  57. {loop_memory-0.4.20 → loop_memory-0.4.22}/loop_memory/ingest/pipeline.py +0 -0
  58. {loop_memory-0.4.20 → loop_memory-0.4.22}/loop_memory/jobs/__init__.py +0 -0
  59. {loop_memory-0.4.20 → loop_memory-0.4.22}/loop_memory/jobs/cognitive.py +0 -0
  60. {loop_memory-0.4.20 → loop_memory-0.4.22}/loop_memory/jobs/compact.py +0 -0
  61. {loop_memory-0.4.20 → loop_memory-0.4.22}/loop_memory/jobs/consolidate.py +0 -0
  62. {loop_memory-0.4.20 → loop_memory-0.4.22}/loop_memory/jobs/contradiction.py +0 -0
  63. {loop_memory-0.4.20 → loop_memory-0.4.22}/loop_memory/jobs/dream.py +0 -0
  64. {loop_memory-0.4.20 → loop_memory-0.4.22}/loop_memory/jobs/evolution.py +0 -0
  65. {loop_memory-0.4.20 → loop_memory-0.4.22}/loop_memory/jobs/graph.py +0 -0
  66. {loop_memory-0.4.20 → loop_memory-0.4.22}/loop_memory/jobs/llm_compact_pass.py +0 -0
  67. {loop_memory-0.4.20 → loop_memory-0.4.22}/loop_memory/jobs/llm_consolidate.py +0 -0
  68. {loop_memory-0.4.20 → loop_memory-0.4.22}/loop_memory/jobs/scheduler.py +0 -0
  69. {loop_memory-0.4.20 → loop_memory-0.4.22}/loop_memory/llm/__init__.py +0 -0
  70. {loop_memory-0.4.20 → loop_memory-0.4.22}/loop_memory/llm/base.py +0 -0
  71. {loop_memory-0.4.20 → loop_memory-0.4.22}/loop_memory/llm/openai_adapter.py +0 -0
  72. {loop_memory-0.4.20 → loop_memory-0.4.22}/loop_memory/llm/providers.py +0 -0
  73. {loop_memory-0.4.20 → loop_memory-0.4.22}/loop_memory/memory/__init__.py +0 -0
  74. {loop_memory-0.4.20 → loop_memory-0.4.22}/loop_memory/memory/types.py +0 -0
  75. {loop_memory-0.4.20 → loop_memory-0.4.22}/loop_memory/privacy/__init__.py +0 -0
  76. {loop_memory-0.4.20 → loop_memory-0.4.22}/loop_memory/privacy/private.py +0 -0
  77. {loop_memory-0.4.20 → loop_memory-0.4.22}/loop_memory/privacy/redact.py +0 -0
  78. {loop_memory-0.4.20 → loop_memory-0.4.22}/loop_memory/py.typed +0 -0
  79. {loop_memory-0.4.20 → loop_memory-0.4.22}/loop_memory/security/secrets.py +0 -0
  80. {loop_memory-0.4.20 → loop_memory-0.4.22}/loop_memory/serve/__init__.py +0 -0
  81. {loop_memory-0.4.20 → loop_memory-0.4.22}/loop_memory/serve/app.py +0 -0
  82. {loop_memory-0.4.20 → loop_memory-0.4.22}/loop_memory/serve/handlers.py +0 -0
  83. {loop_memory-0.4.20 → loop_memory-0.4.22}/loop_memory/serve/routes/_shared.py +0 -0
  84. {loop_memory-0.4.20 → loop_memory-0.4.22}/loop_memory/serve/routes/admin.py +0 -0
  85. {loop_memory-0.4.20 → loop_memory-0.4.22}/loop_memory/serve/routes/cognitive.py +0 -0
  86. {loop_memory-0.4.20 → loop_memory-0.4.22}/loop_memory/serve/routes/export.py +0 -0
  87. {loop_memory-0.4.20 → loop_memory-0.4.22}/loop_memory/serve/routes/graph.py +0 -0
  88. {loop_memory-0.4.20 → loop_memory-0.4.22}/loop_memory/serve/routes/insights.py +0 -0
  89. {loop_memory-0.4.20 → loop_memory-0.4.22}/loop_memory/serve/routes/sessions.py +0 -0
  90. {loop_memory-0.4.20 → loop_memory-0.4.22}/loop_memory/serve/routes/wiki.py +0 -0
  91. {loop_memory-0.4.20 → loop_memory-0.4.22}/loop_memory/serve/static/__init__.py +0 -0
  92. {loop_memory-0.4.20 → loop_memory-0.4.22}/loop_memory/serve/static/css/app.css +0 -0
  93. {loop_memory-0.4.20 → loop_memory-0.4.22}/loop_memory/serve/static/css/layout.css +0 -0
  94. {loop_memory-0.4.20 → loop_memory-0.4.22}/loop_memory/serve/static/i18n/en.json +0 -0
  95. {loop_memory-0.4.20 → loop_memory-0.4.22}/loop_memory/serve/static/i18n/zh.json +0 -0
  96. {loop_memory-0.4.20 → loop_memory-0.4.22}/loop_memory/serve/static/img/logo-light.svg +0 -0
  97. {loop_memory-0.4.20 → loop_memory-0.4.22}/loop_memory/serve/static/img/logo-mark.svg +0 -0
  98. {loop_memory-0.4.20 → loop_memory-0.4.22}/loop_memory/serve/static/img/logo.svg +0 -0
  99. {loop_memory-0.4.20 → loop_memory-0.4.22}/loop_memory/serve/static/index.html +0 -0
  100. {loop_memory-0.4.20 → loop_memory-0.4.22}/loop_memory/serve/static/js/App.js +0 -0
  101. {loop_memory-0.4.20 → loop_memory-0.4.22}/loop_memory/serve/static/js/api.js +0 -0
  102. {loop_memory-0.4.20 → loop_memory-0.4.22}/loop_memory/serve/static/js/components/Dashboard.js +0 -0
  103. {loop_memory-0.4.20 → loop_memory-0.4.22}/loop_memory/serve/static/js/components/Diagnostic.js +0 -0
  104. {loop_memory-0.4.20 → loop_memory-0.4.22}/loop_memory/serve/static/js/components/IngestPopover.js +0 -0
  105. {loop_memory-0.4.20 → loop_memory-0.4.22}/loop_memory/serve/static/js/components/KnowledgeGraph.js +0 -0
  106. {loop_memory-0.4.20 → loop_memory-0.4.22}/loop_memory/serve/static/js/components/RingMeter.js +0 -0
  107. {loop_memory-0.4.20 → loop_memory-0.4.22}/loop_memory/serve/static/js/components/RunStrip.js +0 -0
  108. {loop_memory-0.4.20 → loop_memory-0.4.22}/loop_memory/serve/static/js/components/SectionTitle.js +0 -0
  109. {loop_memory-0.4.20 → loop_memory-0.4.22}/loop_memory/serve/static/js/components/Settings.js +0 -0
  110. {loop_memory-0.4.20 → loop_memory-0.4.22}/loop_memory/serve/static/js/components/Sidebar.js +0 -0
  111. {loop_memory-0.4.20 → loop_memory-0.4.22}/loop_memory/serve/static/js/components/Tabs.js +0 -0
  112. {loop_memory-0.4.20 → loop_memory-0.4.22}/loop_memory/serve/static/js/components/Timeline.js +0 -0
  113. {loop_memory-0.4.20 → loop_memory-0.4.22}/loop_memory/serve/static/js/components/Toast.js +0 -0
  114. {loop_memory-0.4.20 → loop_memory-0.4.22}/loop_memory/serve/static/js/components/TopBar.js +0 -0
  115. {loop_memory-0.4.20 → loop_memory-0.4.22}/loop_memory/serve/static/js/components/Wiki.js +0 -0
  116. {loop_memory-0.4.20 → loop_memory-0.4.22}/loop_memory/serve/static/js/components/WikiEditor.js +0 -0
  117. {loop_memory-0.4.20 → loop_memory-0.4.22}/loop_memory/serve/static/js/composables/useI18n.js +0 -0
  118. {loop_memory-0.4.20 → loop_memory-0.4.22}/loop_memory/serve/static/js/lib/sanitize.js +0 -0
  119. {loop_memory-0.4.20 → loop_memory-0.4.22}/loop_memory/serve/static/js/lib/vue.esm-browser.prod.js +0 -0
  120. {loop_memory-0.4.20 → loop_memory-0.4.22}/loop_memory/serve/static/js/main.js +0 -0
  121. {loop_memory-0.4.20 → loop_memory-0.4.22}/loop_memory/serve/static/js/store.js +0 -0
  122. {loop_memory-0.4.20 → loop_memory-0.4.22}/loop_memory/serve/watcher.py +0 -0
  123. {loop_memory-0.4.20 → loop_memory-0.4.22}/loop_memory/storage/__init__.py +0 -0
  124. {loop_memory-0.4.20 → loop_memory-0.4.22}/loop_memory/storage/lexical.py +0 -0
  125. {loop_memory-0.4.20 → loop_memory-0.4.22}/loop_memory/storage/snapshot.py +0 -0
  126. {loop_memory-0.4.20 → loop_memory-0.4.22}/loop_memory/wiki/__init__.py +0 -0
  127. {loop_memory-0.4.20 → loop_memory-0.4.22}/loop_memory/wiki/backfill.py +0 -0
  128. {loop_memory-0.4.20 → loop_memory-0.4.22}/loop_memory/wiki/classifier.py +0 -0
  129. {loop_memory-0.4.20 → loop_memory-0.4.22}/loop_memory/wiki/prompts.py +0 -0
  130. {loop_memory-0.4.20 → loop_memory-0.4.22}/loop_memory/wiki/scope.py +0 -0
  131. {loop_memory-0.4.20 → loop_memory-0.4.22}/loop_memory.egg-info/dependency_links.txt +0 -0
  132. {loop_memory-0.4.20 → loop_memory-0.4.22}/loop_memory.egg-info/entry_points.txt +0 -0
  133. {loop_memory-0.4.20 → loop_memory-0.4.22}/loop_memory.egg-info/requires.txt +0 -0
  134. {loop_memory-0.4.20 → loop_memory-0.4.22}/loop_memory.egg-info/top_level.txt +0 -0
  135. {loop_memory-0.4.20 → loop_memory-0.4.22}/setup.cfg +0 -0
  136. {loop_memory-0.4.20 → loop_memory-0.4.22}/tests/test_admin_ingest_route.py +0 -0
  137. {loop_memory-0.4.20 → loop_memory-0.4.22}/tests/test_agent_memory_api.py +0 -0
  138. {loop_memory-0.4.20 → loop_memory-0.4.22}/tests/test_agents_init.py +0 -0
  139. {loop_memory-0.4.20 → loop_memory-0.4.22}/tests/test_audit_triggers_2026_09_27.py +0 -0
  140. {loop_memory-0.4.20 → loop_memory-0.4.22}/tests/test_auth_token_rotate.py +0 -0
  141. {loop_memory-0.4.20 → loop_memory-0.4.22}/tests/test_bench_2026_11_01.py +0 -0
  142. {loop_memory-0.4.20 → loop_memory-0.4.22}/tests/test_busy_timeout_2026_09_24.py +0 -0
  143. {loop_memory-0.4.20 → loop_memory-0.4.22}/tests/test_cache_cap_2026_09_23.py +0 -0
  144. {loop_memory-0.4.20 → loop_memory-0.4.22}/tests/test_cli_audit_supersede.py +0 -0
  145. {loop_memory-0.4.20 → loop_memory-0.4.22}/tests/test_cli_export_okf.py +0 -0
  146. {loop_memory-0.4.20 → loop_memory-0.4.22}/tests/test_cli_init.py +0 -0
  147. {loop_memory-0.4.20 → loop_memory-0.4.22}/tests/test_cli_memory_stats.py +0 -0
  148. {loop_memory-0.4.20 → loop_memory-0.4.22}/tests/test_cli_recall_as_of.py +0 -0
  149. {loop_memory-0.4.20 → loop_memory-0.4.22}/tests/test_cli_recall_paths.py +0 -0
  150. {loop_memory-0.4.20 → loop_memory-0.4.22}/tests/test_cli_recall_verbose.py +0 -0
  151. {loop_memory-0.4.20 → loop_memory-0.4.22}/tests/test_cli_rules.py +0 -0
  152. {loop_memory-0.4.20 → loop_memory-0.4.22}/tests/test_cli_snapshot.py +0 -0
  153. {loop_memory-0.4.20 → loop_memory-0.4.22}/tests/test_cli_v7.py +0 -0
  154. {loop_memory-0.4.20 → loop_memory-0.4.22}/tests/test_cli_version.py +0 -0
  155. {loop_memory-0.4.20 → loop_memory-0.4.22}/tests/test_content_hash_dedup_2026_10_18.py +0 -0
  156. {loop_memory-0.4.20 → loop_memory-0.4.22}/tests/test_contradictions.py +0 -0
  157. {loop_memory-0.4.20 → loop_memory-0.4.22}/tests/test_decorators_2026_10_18.py +0 -0
  158. {loop_memory-0.4.20 → loop_memory-0.4.22}/tests/test_dream_2026_10_25.py +0 -0
  159. {loop_memory-0.4.20 → loop_memory-0.4.22}/tests/test_dream_cli_http_2026_10_25.py +0 -0
  160. {loop_memory-0.4.20 → loop_memory-0.4.22}/tests/test_evolution.py +0 -0
  161. {loop_memory-0.4.20 → loop_memory-0.4.22}/tests/test_evolution_quality.py +0 -0
  162. {loop_memory-0.4.20 → loop_memory-0.4.22}/tests/test_expanduser.py +0 -0
  163. {loop_memory-0.4.20 → loop_memory-0.4.22}/tests/test_export_ask.py +0 -0
  164. {loop_memory-0.4.20 → loop_memory-0.4.22}/tests/test_export_okf.py +0 -0
  165. {loop_memory-0.4.20 → loop_memory-0.4.22}/tests/test_graph.py +0 -0
  166. {loop_memory-0.4.20 → loop_memory-0.4.22}/tests/test_healthz_readyz_2026_09_24.py +0 -0
  167. {loop_memory-0.4.20 → loop_memory-0.4.22}/tests/test_import_okf_2026_09_27.py +0 -0
  168. {loop_memory-0.4.20 → loop_memory-0.4.22}/tests/test_ingest.py +0 -0
  169. {loop_memory-0.4.20 → loop_memory-0.4.22}/tests/test_latest_only_2026_10_25.py +0 -0
  170. {loop_memory-0.4.20 → loop_memory-0.4.22}/tests/test_lexical_ranking_2026_11_01.py +0 -0
  171. {loop_memory-0.4.20 → loop_memory-0.4.22}/tests/test_llm_consolidator.py +0 -0
  172. {loop_memory-0.4.20 → loop_memory-0.4.22}/tests/test_llm_fingerprint_not_persisted.py +0 -0
  173. {loop_memory-0.4.20 → loop_memory-0.4.22}/tests/test_llm_providers.py +0 -0
  174. {loop_memory-0.4.20 → loop_memory-0.4.22}/tests/test_llm_test_endpoint.py +0 -0
  175. {loop_memory-0.4.20 → loop_memory-0.4.22}/tests/test_loop.py +0 -0
  176. {loop_memory-0.4.20 → loop_memory-0.4.22}/tests/test_mcp.py +0 -0
  177. {loop_memory-0.4.20 → loop_memory-0.4.22}/tests/test_memories_pagination.py +0 -0
  178. {loop_memory-0.4.20 → loop_memory-0.4.22}/tests/test_memory_stats.py +0 -0
  179. {loop_memory-0.4.20 → loop_memory-0.4.22}/tests/test_openclaw_loader.py +0 -0
  180. {loop_memory-0.4.20 → loop_memory-0.4.22}/tests/test_package_version.py +0 -0
  181. {loop_memory-0.4.20 → loop_memory-0.4.22}/tests/test_packaging_2026_11_01.py +0 -0
  182. {loop_memory-0.4.20 → loop_memory-0.4.22}/tests/test_path_safety_2026_09_23.py +0 -0
  183. {loop_memory-0.4.20 → loop_memory-0.4.22}/tests/test_recall_as_of.py +0 -0
  184. {loop_memory-0.4.20 → loop_memory-0.4.22}/tests/test_recall_budget_2026_10_05.py +0 -0
  185. {loop_memory-0.4.20 → loop_memory-0.4.22}/tests/test_recall_paths.py +0 -0
  186. {loop_memory-0.4.20 → loop_memory-0.4.22}/tests/test_recall_provenance.py +0 -0
  187. {loop_memory-0.4.20 → loop_memory-0.4.22}/tests/test_recall_validity_http_2026_10_18.py +0 -0
  188. {loop_memory-0.4.20 → loop_memory-0.4.22}/tests/test_reflection.py +0 -0
  189. {loop_memory-0.4.20 → loop_memory-0.4.22}/tests/test_repo_framing.py +0 -0
  190. {loop_memory-0.4.20 → loop_memory-0.4.22}/tests/test_retrieval_channels_2026_10_18.py +0 -0
  191. {loop_memory-0.4.20 → loop_memory-0.4.22}/tests/test_routes_audit_2026_09_13.py +0 -0
  192. {loop_memory-0.4.20 → loop_memory-0.4.22}/tests/test_routes_audit_2026_09_20.py +0 -0
  193. {loop_memory-0.4.20 → loop_memory-0.4.22}/tests/test_score_api.py +0 -0
  194. {loop_memory-0.4.20 → loop_memory-0.4.22}/tests/test_scoring_v2.py +0 -0
  195. {loop_memory-0.4.20 → loop_memory-0.4.22}/tests/test_secrets.py +0 -0
  196. {loop_memory-0.4.20 → loop_memory-0.4.22}/tests/test_serve_app.py +0 -0
  197. {loop_memory-0.4.20 → loop_memory-0.4.22}/tests/test_serve_handlers.py +0 -0
  198. {loop_memory-0.4.20 → loop_memory-0.4.22}/tests/test_session_order.py +0 -0
  199. {loop_memory-0.4.20 → loop_memory-0.4.22}/tests/test_short_query.py +0 -0
  200. {loop_memory-0.4.20 → loop_memory-0.4.22}/tests/test_snapshot.py +0 -0
  201. {loop_memory-0.4.20 → loop_memory-0.4.22}/tests/test_store.py +0 -0
  202. {loop_memory-0.4.20 → loop_memory-0.4.22}/tests/test_subgraph_hops_2026_09_23.py +0 -0
  203. {loop_memory-0.4.20 → loop_memory-0.4.22}/tests/test_subject_key_2026_10_25.py +0 -0
  204. {loop_memory-0.4.20 → loop_memory-0.4.22}/tests/test_summarization.py +0 -0
  205. {loop_memory-0.4.20 → loop_memory-0.4.22}/tests/test_supersession_chain.py +0 -0
  206. {loop_memory-0.4.20 → loop_memory-0.4.22}/tests/test_universal_memory.py +0 -0
  207. {loop_memory-0.4.20 → loop_memory-0.4.22}/tests/test_vector_store.py +0 -0
  208. {loop_memory-0.4.20 → loop_memory-0.4.22}/tests/test_watcher.py +0 -0
  209. {loop_memory-0.4.20 → loop_memory-0.4.22}/tests/test_wiki_classifier.py +0 -0
  210. {loop_memory-0.4.20 → loop_memory-0.4.22}/tests/test_wiki_export_escape.py +0 -0
  211. {loop_memory-0.4.20 → loop_memory-0.4.22}/tests/test_wiki_prompts.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: loop-memory
3
- Version: 0.4.20
3
+ Version: 0.4.22
4
4
  Summary: A general-purpose, local memory system for every AI agent you run. Loop Memory auto-captures conversations from Codex / Claude / Hermes / OpenClaw, scores them by importance × recency × usage × feedback, distils them into a curated wiki, and serves everything from a single web UI.
5
5
  Author: Loop Memory contributors <loop-memory@users.noreply.github.com>
6
6
  License-Expression: MIT
@@ -67,82 +67,40 @@ Dynamic: license-file
67
67
 
68
68
  ---
69
69
 
70
- > **What's new in 0.4.20** — **the packaging test works on the oldest supported Python** (audit 2026-11-01, patch follow-up to 0.4.19). The 0.4.19 packaging test parsed `pyproject.toml` with `tomllib`, which is stdlib only from Python **3.11** onwards, while the project declares `requires-python = ">=3.10"` — so the 3.10 CI job failed with `ModuleNotFoundError: No module named 'tomllib'` while 3.11 and 3.12 passed, the classic shape of a bug that only bites the oldest supported interpreter. The check now reads the `[tool.setuptools.package-data]` table directly (it is a flat mapping of package name to a list of glob strings), and a new case runs it in a subprocess with `tomllib` blocked at the import system level, so the 3.10 path is exercised on every version rather than only on the 3.10 runner. No behaviour change — the 0.4.19 distribution is unchanged and correct. 1 new regression case; suite at **1004 passed / 4 skipped**. [Full CHANGELOG →](./CHANGELOG.md#0420---2026-11-01)
71
-
72
- > **What's new in 0.4.19** — **the PyPI wheel now ships the whole dashboard and the benchmark fixture** (audit 2026-11-01, packaging follow-up to 0.4.18). The 0.4.18 wheel contained exactly two data files — `loop_memory/py.typed` and `loop_memory/serve/static/index.html` — because the package-data declaration was `loop_memory = ["py.typed", "serve/static/*.html"]`, and that glob is **not recursive**. Every CSS, JavaScript, i18n and image asset and the new benchmark fixture were **missing from the distribution**. Reproduced against the published wheel: `pip install loop-memory` started a server whose `GET /` returned `200` but whose `GET /static/js/main.js` and `GET /static/css/app.css` both returned **404** — a blank dashboard — and `loop-memory bench` crashed with `FileNotFoundError` on the packaged fixture. The declaration is now per package with recursive globs (`"loop_memory.serve.static" = ["**/*.html", "**/*.css", "**/*.js", "**/*.json", "**/*.svg"]`, `"loop_memory.bench" = ["fixtures/*.json"]`), so a new asset is picked up without anyone remembering to extend it, and the wheel contains all 32 non-Python files in the package tree. A new packaging test builds the real wheel and sdist **from a pristine source copy** — setuptools caches its file list in `egg-info/SOURCES.txt` and packages a stale `build/lib`, so building in the working tree would make the test pass for the wrong reason — and asserts every non-Python file actually reaches the distribution, then unpacks the wheel and runs the benchmark out of it. 8 new regression cases; suite at **1003 passed / 4 skipped**. [Full CHANGELOG →](./CHANGELOG.md#0419---2026-11-01)
73
-
74
- > **What's new in 0.4.18** — **the BM25 channel no longer publishes a score that collapses to zero, and there is finally a benchmark that measures it** (audit 2026-11-01, weekly ecosystem sync). SQLite's FTS5 `bm25()` computes the classic idf `log((N - n + 0.5) / (n + 0.5))` and **clamps every non-positive idf to `1e-6`** — so a query term present in 49 % of the corpus keeps a real idf while one present in 51 % has its weight annihilated, a factor of ~40 000 between two almost equally discriminative terms. With the `trigram` tokenizer that is the *normal* case for CJK (`知识图` alone occurs in more than half of any knowledge-graph-heavy corpus), and the visible symptom was the 0.4.15 evidence chain reporting **`bm25: 0.0`** — "this hit because bm25 0.0". The published score is now a textbook BM25 (`k1 = 1.2`, `b = 0.75`) with the Lucene / BM25+ idf `log(1 + (N - n + 0.5) / (n + 0.5))`, which is strictly positive for every term, so the scale cannot collapse. **A document that contains the query verbatim now outranks one that merely contains the same scattered trigrams** — the transferable half of `rohitg00/agentmemory` v0.9.31's prefix-score fix (2026-10-26: strict recall-all@5 **65.74 % → 84.68 %**, +18.94 pp), whose release note is careful to call its numbers "component retrieval results on inspected development data, not held-out answer accuracy". The `LIKE` fallback and the FTS5 hits are finally on the **same scale** (a `LIKE`-only hit used to publish the memory's `importance` under the name `bm25`). The candidate set is unchanged — the fix re-ranks, it does not add or drop documents. New **`loop-memory bench`**: a deterministic retrieval-quality benchmark over a fixed, versioned fixture (30 memories, 12 wiki pages, 11 judged queries) reporting **recall@1/3/5, MRR@10, nDCG@10** with the fixture's revision and the SHA-256 of its bytes; the run is fully offline (**no provider API is called, $0, no network**) and builds its store in a throwaway directory. On the shipped fixture the channel scored **MRR@10 = 0.6061, recall@1 = 0.2273, nDCG@10 = 0.7078** before and **1.0000 / 0.9545 / 1.0000** after — and the report keeps the raw FTS5 score next to the repaired one, so the defect stays visible instead of being quietly fixed and forgotten. Source: *agentmemory* v0.9.31 evaluation methodology + the FTS5 idf clamp in SQLite's `fts5_aux.c` (Apache-2.0 — *pattern only, no code copied*); honcho v3.4.1's "a filter must not silently drop a term" and v3.5.0's "a conclusion must cite its source" were reviewed and need no code (AGPL-3.0 — *pattern only*). 37 new regression cases; suite at **995 passed / 4 skipped**. [Full CHANGELOG →](./CHANGELOG.md#0418---2026-11-01)
75
- > **What's new in 0.4.17** — **a dry-run dream pass no longer writes to the database** (audit 2026-10-25). `loop-memory dream` and `POST /api/v1/dream` have always documented themselves as *"nothing is written unless `apply=True`"* — but the pass's decay stage called `MemoryStore.rescore_all()` unconditionally, which rewrote the `memories.score` column of **every** row while claiming to only inspect the store. The expiry and merge decisions were correctly left unapplied; the score decay was not. `rescore_all()` now takes a `dry_run` flag: it computes every new score and reports how many rows *would* change but issues no `UPDATE`, and the dream pass runs it with `dry_run=not apply`. A dry run still reports the size of the decay job through `report.rescored` while touching nothing. The contract wording is now exact rather than aspirational — a dry run changes **no memory data** (no expiry, no supersession, no rescore); the single write it still performs is one `cognitive_audit` row per proposed action with `action="suggest"` instead of `"applied"`, because that row *is* the reviewable plan the dashboard shows before you commit to it (`record_audit=False` suppresses even that). Also fixed in this release: `publish.yml` now sends `make_latest` as a JSON **boolean** (`gh api -F`) instead of a JSON **string** — GitHub's release API ignores the string form, so the "mark as latest" step had been returning 200 and reporting success while promoting nothing, leaving `/releases/latest` pointing at v0.4.14 through the 0.4.15 and 0.4.16 ships. The step now also re-reads `/releases/latest` and fails the job when the tag does not match, instead of trusting the exit code. 3 new regression cases (`tests/test_dream_2026_10_25.py`); suite at **958 passed / 4 skipped**. [Full CHANGELOG →](./CHANGELOG.md#0417---2026-10-25)
76
- > **What's new in 0.4.16** — **subject-key logical fact slots + `latest_only` recall + the "dream" sleep-time consolidation pass** (audit 2026-10-25, weekly ecosystem sync — Mem0 v2.2.1 "Dream", arXiv 2605.27366 "In Defense of Forgotten", Zep/Graphiti temporal edges, Letta memory blocks). New nullable `subject_key` column on `memories` names the *logical slot* a fact occupies (`"editor.preference"`); `upsert_memory(subject_key=...)` makes the new row the live version and soft-supersedes the previous one with a `superseded_by` pointer, so the version history survives and `recall_as_of()` can still answer "what did we believe last week?". The content-hash dedup path is now slot-aware — two different slots holding the same text stay two rows. `SCHEMA_VERSION` bumps 12 → 13 with an idempotent in-place migration. New `latest_only=False` flag on `recall()` / `recall_hybrid()` (Mem0 v2 `SearchMemoryOptions.latest_only` parity) collapses the answer in three layers: rows outside their validity window at *now* are dropped, a subject-key slot resolves to its newest **live** row (looked up in the store, so a stale version never comes back just because it matched the query better), and near-duplicate texts cluster by token Jaccard (default 0.85) with the newest row winning. Wired through `GET /api/recall?latest_only=1`, `GET /api/v1/recall?latest_only=1`, `loop-memory recall --latest-only` and `MemoryClient.recall(latest_only=True)`. New `loop_memory/jobs/dream.py` implements the sleep-time consolidation Mem0 v2 calls the "Dream" — deterministic, zero-dep and **non-destructive**: it decays scores, then judges every row past its deadline on **utility** (`0.45 × importance + 0.30 × usage + 0.25 × recency`) so a frequently-recalled old fact is *protected* while a never-used stale row is **soft-expired** (`invalid_at` stamped, row kept, history kept) instead of deleted, and finally merges near-duplicates into a supersession chain. Run it with `loop-memory dream [--apply] [--json]` or `POST /api/v1/dream` — it is a **dry run by default** and every decision lands in `cognitive_audit` so the dashboard can show and revert it. New store primitives `expire_memory()` / `revive_memory()`, and `memory_stats()` now reports `subject_key`, `ttl`, `valid_at`, `invalid_at` and a derived `ttl_state`. **Also fixed a real defect**: the hybrid recall path (BM25 / LIKE / semantic channels) did not filter `superseded_by`, so `recall_hybrid()` — the production path behind the CLI, MCP server and dashboard — returned both versions of a superseded fact. Source: Mem0 v2.2.1 Dream + ADD-only architecture + arXiv 2605.27366 utility-aware eviction + Zep/Graphiti bi-temporal edges (pattern only, no code copied). 77 new regression cases. [Full CHANGELOG →](./CHANGELOG.md#0416---2026-10-25)
77
- > **What's new in 0.4.15** — **content-hash dedup + bi-temporal validity window + evidence chain + `@agent_memory` decorator** (audit 2026-10-18, weekly ecosystem sync). Three new columns on `memories`: `content_hash` (SHA-256 of normalised text, indexed), `valid_at` (epoch the memory becomes active), `invalid_at` (epoch it expires — NULL means still open-ended). The schema migration bumps `SCHEMA_VERSION` 11 → 12 with an idempotent backfill so legacy DBs upgrade in-place. New `upsert_memory(valid_at=, invalid_at=, dedup=)` parameters let callers schedule future-dated or expiring memories and opt out of content-hash dedup for episodic event streams. `recall()` and `recall_hybrid()` now accept an optional `as_of_ts` (float epoch or ISO-8601) — when set, both methods drop memories whose `valid_at > as_of` or `invalid_at <= as_of`, complementing the existing `recall_as_of()` supersession-chain walk. Every recall hit now carries an `evidence: [{field, value, weight}]` array — BM25 / semantic / entity / rrf_total / high_importance channels are exposed individually so a UI can render "this hit because ... [bm25 1.0] [entity 0.7]" instead of a black-box score (Cognee 1.0 / Mem0 v2 evidence-citation pattern, zero-dep). New `loop_memory.decorators` exposes `@agent_memory(store, agent_id=, limit=)` — a one-line wrapper that recalls the top-k relevant memories before the body runs and writes a fresh `kind="interaction"` memory after. Strings returned from the wrapped function are stored verbatim; dict/list returns are JSON-serialised; the recall list is injected as a `memory_context` keyword when the function declares it, otherwise attached to dict results under `memories`. Sister `remember(store, text, ...)` helper for seeding. The BM25 `_like_fallback` now falls through to per-token OR-LIKE when the whole-string match returns nothing — natural-language questions like "What is the deploy process?" now hit stored facts even when FTS5 trigram can't match quoted exact phrases. Source: Cognee 1.0 ECL pipeline + Mem0 v2 hybrid fusion + Zep/Graphiti bi-temporal validity fields + Letta block-memory (pattern only, no code copied). 59 new regression cases. [Full CHANGELOG →](./CHANGELOG.md#0415---2026-10-18)
78
- > **What's new in 0.4.14** — **`loop-memory doctor` deep store-health check** (audit 2026-10-12). Ten diagnostic checks — `db_exists`, `db_integrity`, `schema_version`, `audit_triggers`, `audit_log_writable`, `audit_log_size`, `hooks_installed`, `version_consistent`, `wiki_no_orphans`, `recall_paths_works` — report `ok`/`info`/`warn`/`error` for the live `MemoryStore`. `--fix` auto-repairs the `audit_triggers` check. `--json` emits a stable shape for CI. `--list` prints the check function names without touching the store. `--verbose` shows `info`-level checks. Exit code `1` on any `error` status, `0` otherwise. Source: `cortexkit/magic-context doctor` auto-diagnostic pattern (MIT, 2026-10-12) + `dsh-memento` write-approval-gate pattern (Apache-2.0, 2026-10-12). Stdlib-only — we re-implement from scratch against our own `MemoryStore`. Also fixed a Python 3.14 bug: chained `store._conn().__enter__()` immediately triggers the contextmanager's `finally` and closes the connection mid-query — both call sites in `doctor.py` now use explicit `with` form. 27 new regression cases. [Full CHANGELOG →](./CHANGELOG.md#0414---2026-10-12)
79
-
80
- > **What's new in 0.4.13** — **recall token / char budget cap** + **unified `level=0|1|2` tiered-loader knob** (audit 2026-10-05). New `max_chars` parameter on `MemoryStore.recall()` / `recall_paths()` / `recall_hybrid()` / `recall_as_of()` caps the *total* payload across all returned hits — when the budget is exceeded the lowest-ranked hit shrinks first so the top-scoring hits always keep their full body. `recall()` / `recall_paths()` / `recall_as_of()` now also accept `level=0|1|2` so a caller can ask for the L0 abstract ladder rung inline (titles + tags + abstract only, never the full `text` / `body`) without routing through `recall_paths()` separately. Both knobs are wired through the HTTP routes (`GET /api/recall?max_chars=&level=`, `GET /api/recall/outline?max_chars=`), the CLI (`loop-memory recall --max-chars N --level 0|1|2`, `recall-paths --max-chars N`), the SDK (`LoopMemoryClient.recall(max_chars=, level=)`), and the MCP `tool_recall` server. Source: `vectorize-io/hindsight v0.10.2` "concise extraction by default" (Apache-2.0, 2026-09-29) + `aiming-lab/SimpleMem` ICML'26 semantic-lossless-compression (Apache-2.0, 2026-10-04) + `volcengine/OpenViking` tiered-loader (AGPLv3, 2026-10-02 — *pattern only, no code copied*). 26 new regression cases. [Full CHANGELOG →](./CHANGELOG.md#0413---2026-10-05)
81
-
82
- > **What's new in 0.4.12** — **OKF bundle import** + **DB-level audit enforcement** (audit 2026-09-27). The new `loop-memory import-okf <in_dir>` (and `POST /api/import/okf`) is the symmetric counterpart to `export-okf` (0.4.9) — bring an OKF v0.2 bundle back into Loop Memory as wiki pages, with idempotent upsert on `(scope, slug)`, optional `--dry-run` and `--skip-conflicts`. Source: `thecolourfoundation/rune` HN launch (MIT, 2026-09-25) + `okf-memory/okf-agent-memory` Issues #12-#15 + `Deja-Vu` (MIT, 2026-09-24) + Google OKF v0.2 spec (Apache-2.0, 2026-09-01). The new `MemoryStore._install_audit_triggers()` installs 12 SQLite triggers on `memories` / `wiki_pages` / `relations` / `consolidation_runs` so the `audit_log` table cannot be skipped — even a raw `sqlite3` writer that bypasses the Python wrapper still appends an audit row with `actor='db_trigger'`. New `MemoryStore.list_audit_log()` reads the log; `GET /api/stats` reports `audit_log` + `audit_triggers` counts. Source: `cogstate-eng/memory-eternal v0.4.3` `enforceAudit()` pattern (MIT, 2026-09-23). 31 new regression cases. [Full CHANGELOG →](./CHANGELOG.md#0412---2026-09-27)
83
-
84
- > **What's new in 0.4.11** — **operational probes** +
85
- **SQLite write-contention fix** (audit 2026-09-24). New
86
- ``GET /api/healthz`` (liveness) + ``GET /api/readyz``
87
- (readiness) endpoints let Kubernetes / launchd keepalive
88
- polls probe the serve without provisioning a Bearer token;
89
- ``/api/readyz`` returns 503 when the SQLite store is
90
- unreachable. Also fixed a real concurrency bug: every
91
- store connection now sets ``PRAGMA busy_timeout=5000``
92
- (was 0 ms) so the watcher + serve + scheduler threads no
93
- longer fail with ``database is locked`` when the
94
- consolidator writes concurrently. 7 new regression cases.
95
- [Full CHANGELOG →](./CHANGELOG.md#0411---2026-09-24)
96
-
97
- > **What's new in 0.4.10** — **path-safety guard** for the
98
- five export / snapshot endpoints. ``MemoryStore`` already
99
- validated query / wiki content; the new
100
- ``_safe_resolve_path`` helper (audit 2026-09-23) closes the
101
- remaining path-traversal vector by refusing any path that
102
- points at the live SQLite store, a system directory
103
- (``/etc`` / ``/usr`` / ``/System``), or a sensitive dotfile
104
- (``~/.ssh`` / ``~/.aws`` / ``~/.gnupg``). Also in this
105
- release: a hard cap on the in-process LLM response cache
106
- (was unbounded; now 256 entries with FIFO eviction) and a
107
- fix for ``subgraph_for(max_hops=...)`` which used to ignore
108
- the argument and always walk one hop (now BFS up to N hops,
109
- hard-capped at 8). 29 new regression cases. [Full
110
- CHANGELOG →](./CHANGELOG.md#0410---2026-09-23)
111
-
112
- > **What's new in 0.4.9** — **OKF v0.2 export** + **bi-temporal `as_of` recall**. `loop-memory export-okf <dir>` writes an [Open Knowledge Format](https://github.com/okf/SPEC) v0.2 bundle — one `.md` file per wiki page with YAML frontmatter + an `index.md` index — so a Loop Memory store can be handed to any OKF-aware tool (Grep, Obsidian, akitaonrails/ai-memory, okf-agent-memory, ...) without re-parsing the body (audit 2026-09-20, Google OKF v0.2 spec + akitaonrails/ai-memory 2.0 + okf-memory/okf-agent-memory, all Apache-2.0/MIT, 2026-09-01 → 2026-09-06). `loop-memory recall <q> --as-of <ISO|epoch>` answers the bi-temporal question "what did we know about this query at that moment?" — a memory written after the moment is invisible, and a superseded memory walks its chain to find whether the invalidation had happened by then (audit 2026-09-20, loomcycle v1.33-v1.49 RFCs BL/BS/BU/BV/BW, Apache-2.0). Both shapes ship with HTTP routes (`POST /api/export/okf`, `GET /api/recall?as_of=...`). 34 new regression cases pin both shapes. [Full CHANGELOG →](./CHANGELOG.md#049---2026-09-20)
113
-
114
- > **What's new in 0.4.8** — **L0 outline recall** +
115
- > **per-agent identity bootstrap**. `loop-memory recall-paths <q>`
116
- > (and the new `recall --outline` flag) returns only `id` +
117
- > `abstract` + `score` + `why` for each hit — never the full
118
- > body — so an agent can identify the top candidates cheaply
119
- > before deciding which bodies to fetch (audit 2026-09-13,
120
- > tigerless-labs/agent-memory v0.3.0 recall-ladder pattern).
121
- > `loop-memory init --agent <name>` registers an agent in the
122
- > new `agents` table and (with `--install-hooks`) also wires
123
- > the MCP + SessionStart hooks for codex/claude/hermes; the
124
- > index is bumped on every `install-hooks` run so a user can
125
- > always answer "which CLIs are wired into this store?" with
126
- > one SQL query (audit 2026-09-13, Mem0 CLI `init --agent`
127
- > pattern). Both shapes ship with HTTP routes
128
- > (`/api/recall/outline`, `/api/agents`, `/api/init/agent`).
129
- > 57 new regression cases pin both shapes. [Full
130
- > changelog →](CHANGELOG.md)
131
- >
132
- > **What's new in 0.4.7** — **per-memory lifetime stats** +
133
- > **portable SQLite snapshot**. `loop-memory memory-stats <id>`
134
- > returns a flat dict (recall_count, positive, negative, age,
135
- > etc.) so you can ask "is anyone still using this memory?"
136
- > without cracking open the SQLite file (audit 2026-09-06,
137
- > agentmemory v1.3.0 pattern). `loop-memory snapshot <out>` /
138
- > `loop-memory restore <in>` move a single-file, lossless
139
- > snapshot of the entire store — including recall-quality
140
- > signals and the supersession chain — to another machine
141
- > (audit 2026-09-06, codexa-memory v0.2.0 pattern). Both shapes
142
- > ship with HTTP routes (`/api/memories/{id}/stats`,
143
- > `/api/snapshot`, `/api/snapshot/restore`). 33 new regression
144
- > cases pin both shapes.
145
- > [Full changelog →](CHANGELOG.md)
70
+ > **What's new in 0.4.22** — **the agent-namespace filter now works, and it is enforced in the query instead of on top of the answer** (audit 2026-11-15, weekly ecosystem sync). The audit for this release (2026-11-15) turned up a real hole rather than a hypothetical one. `GET /api/v1/recall?agent_id=X` was documented as *"Unified recall filtered to a single Agent namespace"* — but the filter was applied to the store's **output**, and it had two separate failure modes. **On the legacy path it was a silent no-op:** `MemoryStore.recall()` never returned the `agent_id` / `user_id` columns, so the route's predicate read `None`/`None` for every row, decided every row was "global", and let all of them through — **an agent that explicitly asked for its own namespace was handed every other agent's memories as well.** **On the hybrid path it ran after the `LIMIT`:** the filter worked, but it ran on the store's already-trimmed result set, so a foreign namespace could spend the caller's *entire* result budget — an agent asking for 5 hits could get **zero**, while its own matching rows sat further down the ranking. Both failures have the same root cause: **the predicate was in the caller instead of in the query** — exactly what the upstream projects in this window converged on. `plastic-labs/honcho` v3.6.0 (2026-11-10, AGPL-3.0) made `tenant_id` lead every primary key, with Postgres row-level security scoping **every read and write**, and refused to hand a tenant an empty namespace; `EverMind-AI/EverOS` v1.6.0 (2026-11-11) made deletion owner-scoped and answered `503 SESSION_BUSY` rather than deleting half of a session; `thedotmack/claude-mem` (v15.16.x) gave each scope its own project by default and made *"an explicitly empty project selection show nothing instead of observations from every project"* (#5866); `vectorize-io/hindsight` v1.2.1 stopped a bank clone from taking over another bank's alias (#6280) — pattern references only, no code copied, and the two AGPL-3.0 projects contributed nothing but an idea. **What shipped:** a single predicate builder (`_scope_clause()`) that every retrieval channel builds its query from, so the namespace lives in the **SQL** of the BM25 channel (both the FTS5 query and the `LIKE` backstop), the semantic channel, the entity channel, `recall()`, `recall_hybrid()` and `recall_as_of()` — the fused ranking, and the `limit` that trims it, now only ever sees the caller's own rows; every hit now reports its `agent_id` / `user_id`, which is what makes the legacy no-op impossible to reintroduce silently; all four public surfaces expose it — `GET /api/v1/recall` and `GET /api/recall` accept `agent_id` / `user_id`, the MCP `recall` tool scopes to `LOOP_MEMORY_AGENT_ID` / `LOOP_MEMORY_USER_ID` (the MCP server is started once per client, so the environment already identifies the caller) and accepts explicit overrides, and `loop-memory recall` gained `--agent-id` / `--user-id`; and a client that **writes** into a namespace now **reads** from it — `MemoryClient.memory(store, agent_id="x")` and `MemoryClient.http(url, agent_id="x")` fall back to that identity on `recall()`, which is precisely the asymmetry that hid the leak (pass `agent_id=""` for the explicit escape hatch). A memory with no `agent_id` stays **global** and visible to everyone, because the ingest pipeline, the consolidator and the dream job all write global rows. 43 new regression cases; suite at **1084 passed / 4 skipped**. See [`docs/namespace-isolation.md`](./docs/namespace-isolation.md) for the full contract. [Full CHANGELOG →](./CHANGELOG.md#0422---2026-11-15)
71
+
72
+ <details>
73
+ <summary><b>Older releases</b> — 0.4.21 and earlier, one line each (the CHANGELOG has the full detail)</summary>
74
+
75
+ | Version | Date | What changed | Full notes |
76
+ | --- | --- | --- | --- |
77
+ | [0.4.21](./CHANGELOG.md#0421---2026-11-08) | 2026-11-08 | The remote SDK can authenticate, and it refuses to send memory data over a plaintext connection |
78
+ | [0.4.20](./CHANGELOG.md#0420---2026-11-01) | 2026-11-01 | The packaging test works on the oldest supported Python (no `tomllib` on 3.10) |
79
+ | [0.4.19](./CHANGELOG.md#0419---2026-11-01) | 2026-11-01 | The wheel ships the whole dashboard and the benchmark fixture (recursive package-data globs) |
80
+ | [0.4.18](./CHANGELOG.md#0418---2026-11-01) | 2026-11-01 | The BM25 channel no longer publishes a score that collapses to zero — plus a retrieval benchmark |
81
+ | [0.4.17](./CHANGELOG.md#0417---2026-10-25) | 2026-10-25 | A dry-run dream pass no longer writes to the database |
82
+ | [0.4.16](./CHANGELOG.md#0416---2026-10-25) | 2026-10-25 | Subject-key logical fact slots, `latest_only` recall, and the sleep-time "dream" pass |
83
+ | [0.4.15](./CHANGELOG.md#0415---2026-10-18) | 2026-10-18 | Content-hash dedup, bi-temporal validity window, evidence chain |
84
+ | [0.4.14](./CHANGELOG.md#0414---2026-10-12) | 2026-10-12 | `loop-memory doctor` — a deep store-health check |
85
+ | [0.4.13](./CHANGELOG.md#0413---2026-10-05) | 2026-10-05 | Recall token / char budget cap and a unified `level=0|1|2` tiered-loader knob |
86
+ | [0.4.12](./CHANGELOG.md#0412---2026-09-27) | 2026-09-27 | OKF bundle import and DB-level audit enforcement (12 SQLite triggers) |
87
+ | [0.4.11](./CHANGELOG.md#0411---2026-09-24) | 2026-09-24 | Operational probes (`/api/healthz`, `/api/readyz`) and the SQLite write-contention fix |
88
+ | [0.4.10](./CHANGELOG.md#0410---2026-09-23) | 2026-09-23 | Path-safety guard for the five export / snapshot endpoints, LLM cache cap, `subgraph_for(max_hops=)` |
89
+ | [0.4.9](./CHANGELOG.md#049---2026-09-20) | 2026-09-20 | OKF v0.2 export and bi-temporal `as_of` recall |
90
+ | [0.4.8](./CHANGELOG.md#048---2026-09-13) | 2026-09-13 | L0 outline recall and per-agent identity bootstrap |
91
+ | [0.4.7](./CHANGELOG.md#047---2026-09-06) | 2026-09-06 | Per-memory lifetime stats and a portable SQLite snapshot |
92
+ | [0.4.6](./CHANGELOG.md#046---2026-08-30) | 2026-08-30 | The memory supersession chain (no hard deletes) |
93
+ | [0.4.5](./CHANGELOG.md#045---2026-08-23) | 2026-08-23 | `loop_memory.__version__` was stale at `"0.2.0"` |
94
+ | [0.4.4](./CHANGELOG.md#044---2026-08-23) | 2026-08-23 | `loop-memory rules --agent <name> --write` |
95
+ | [0.4.3](./CHANGELOG.md#043---2026-08-16) | 2026-08-16 | The cognitive sweep N+1 fix |
96
+ | [0.4.2](./CHANGELOG.md#042---2026-08-09) | 2026-08-09 | README refresh |
97
+ | [0.4.1](./CHANGELOG.md#041---2026-08-09) | 2026-08-09 | CLI dispatch hardened against common `--help` crashes |
98
+ | [0.4.0](./CHANGELOG.md#040---2026-08-09) | 2026-08-09 | OpenAI base URL env-var fallback |
99
+ | [0.3.0](./CHANGELOG.md#030--2026-07-11) | 2026-07-11 | The run progress strip is back |
100
+ | [0.2.0](./CHANGELOG.md#020--2026-07-10) | 2026-07-10 | The SQLite-backed `MemoryStore` |
101
+ | [0.1.0](./CHANGELOG.md#010--2026-07-10) | 2026-07-10 | The dashboard tab empty-data regression |
102
+
103
+ </details>
146
104
 
147
105
  ---
148
106
 
@@ -310,6 +268,8 @@ Hermes, clawx) can read and write, we built this for you.
310
268
  | [docs/api.md](docs/api.md) | HTTP API reference — every route, request body, and response shape the UI consumes |
311
269
  | [docs/agent-memory-api.md](docs/agent-memory-api.md) | Stable four-verb SDK / HTTP / MCP contract for any Agent |
312
270
  | [docs/universal-agent-memory.md](docs/universal-agent-memory.md) | v7 graph memory, cognitive sleep, portable bundles, namespaces, and MCP/CLI extensions |
271
+ | [docs/transport-security.md](docs/transport-security.md) | How a remote client is allowed to talk to `loop-memory serve` — the transport guard, bearer-token auth, redirect hardening, and the `plaintext_exposure` doctor check (0.4.21) |
272
+ | [docs/namespace-isolation.md](docs/namespace-isolation.md) | Who may read which memory — the agent / user namespace predicate, why it lives in the SQL instead of on top of the answer, and the global-row semantics (0.4.22) |
313
273
  | [docs/providers.md](docs/providers.md) | LLM provider reference — built-in providers, defaults, base URLs, and how to add a new one |
314
274
  | [docs/auto-capture.md](docs/auto-capture.md) | Hooking Codex / Claude / Hermes / OpenClaw watchers (filesystem, launchd, systemd, cron) |
315
275
  | [CONTRIBUTING.md](CONTRIBUTING.md) | Local dev loop, pytest, where secrets live, how to add a provider/source |
@@ -663,7 +623,7 @@ loop_memory/
663
623
  backends/ # embedding (hashing / sentence-transformers) + vector store (memory / chroma)
664
624
  storage/ # SQLite-backed MemoryStore + migrations
665
625
  privacy/ # <private> stripping + regex redaction
666
- security/ # Keychain / 0600-file secrets wrapper
626
+ security/ # Keychain / 0600-file secrets wrapper + the transport guard
667
627
  mcp/ # stdio JSON-RPC MCP server
668
628
  serve/ # FastAPI app, watcher, web UI (Timeline / Dashboard / Wiki / Graph)
669
629
  export/ # markdown + v7 portable bundle export/import
@@ -673,16 +633,19 @@ loop_memory/
673
633
  memory/types.py # MemoryItem + 4 tiers
674
634
  examples/demo.py # runnable, zero-API-key demo
675
635
  py.typed
676
- tests/ # 475 unit tests across memory / SDK / serve / CLI / scripts
636
+ tests/ # 1084 unit tests across memory / SDK / serve / CLI / scripts
677
637
  docs/
678
638
  auto-capture.md # launchd / systemd / cron recipes
679
639
  architecture.md # layered view + 5-stage evolution pipeline
680
640
  api.md # HTTP API reference
641
+ transport-security.md # how a remote client may talk to `serve` (0.4.21)
642
+ namespace-isolation.md # who may read which memory (0.4.22)
681
643
  agent-memory-api.md # four-verb SDK / HTTP / MCP contract
682
644
  universal-agent-memory.md # v7 graph memory + cognitive sleep + bundles
683
645
  providers.md # LLM provider reference
684
646
  settings.md # settings table + secrets file
685
647
  weekly-research-automation.md # how the project auto-evolves from upstream research
648
+ research/ # the weekly ecosystem notes each release is based on
686
649
  ```
687
650
 
688
651
  ---
@@ -712,6 +675,34 @@ The web UI stores it in `localStorage` under `loop_auth_token` and attaches
712
675
  it automatically; non-browser clients (curl, MCP, SDK) must opt in by
713
676
  passing it explicitly.
714
677
 
678
+ **Remote clients (SDK):** the HTTP SDK can send the token for you, and it
679
+ refuses to send it — or any memory text — over an unencrypted connection
680
+ to a remote host:
681
+
682
+ ```python
683
+ client = MemoryClient.http(
684
+ "https://memory.example.com",
685
+ token=open("token.txt").read().strip(), # Authorization: Bearer …
686
+ )
687
+ client.remember("deploys go out on Tuesdays")
688
+ ```
689
+
690
+ Plain `http://` is still fine for `127.0.0.1` / `localhost` / `::1` (the
691
+ traffic never leaves the machine — that is the default deployment), but
692
+ plain `http://` to **any other host is refused**: the memory text and the
693
+ bearer token would cross the network in cleartext. A private tunnel or a
694
+ VPN'd LAN is a legitimate deployment, so `allow_insecure=True` (or
695
+ `LOOP_MEMORY_ALLOW_INSECURE=1`) exists — but it is never silent, it
696
+ warns on every construction. Redirects that downgrade `https://` →
697
+ `http://`, and cross-origin redirects that would replay the token, are
698
+ refused too. The full contract is in
699
+ [docs/transport-security.md](docs/transport-security.md).
700
+
701
+ **Server side:** `loop-memory serve --host <non-loopback>` prints a
702
+ warning and records the bind address, so `loop-memory doctor` reports it
703
+ as a `plaintext_exposure` warning — the API is reachable from the network
704
+ over plaintext.
705
+
715
706
  **When to set a token:**
716
707
 
717
708
  - ✅ Always, even on loopback. The default of "no token" exists only as a
@@ -728,6 +719,12 @@ passing it explicitly.
728
719
 
729
720
  - `install-hooks` runs Python that touches `~/.*` — it's gated by the
730
721
  bearer token like every other `POST /api/admin/*` route.
722
+ - A remote client cannot leak memory text to the network in cleartext:
723
+ the SDK refuses a plaintext `http://` base URL that is not loopback,
724
+ refuses a URL with embedded credentials, and refuses redirects that
725
+ downgrade the scheme or would replay the `Authorization` header
726
+ (audit 2026-11-08, 0.4.21 — see
727
+ [docs/transport-security.md](docs/transport-security.md)).
731
728
  - The `<private>...</private>` span stripping in the privacy layer keeps
732
729
  user-marked secrets out of long-term storage; the regex redaction layer
733
730
  then catches API keys / tokens / private keys / JWTs / generic
@@ -22,82 +22,40 @@
22
22
 
23
23
  ---
24
24
 
25
- > **What's new in 0.4.20** — **the packaging test works on the oldest supported Python** (audit 2026-11-01, patch follow-up to 0.4.19). The 0.4.19 packaging test parsed `pyproject.toml` with `tomllib`, which is stdlib only from Python **3.11** onwards, while the project declares `requires-python = ">=3.10"` — so the 3.10 CI job failed with `ModuleNotFoundError: No module named 'tomllib'` while 3.11 and 3.12 passed, the classic shape of a bug that only bites the oldest supported interpreter. The check now reads the `[tool.setuptools.package-data]` table directly (it is a flat mapping of package name to a list of glob strings), and a new case runs it in a subprocess with `tomllib` blocked at the import system level, so the 3.10 path is exercised on every version rather than only on the 3.10 runner. No behaviour change — the 0.4.19 distribution is unchanged and correct. 1 new regression case; suite at **1004 passed / 4 skipped**. [Full CHANGELOG →](./CHANGELOG.md#0420---2026-11-01)
26
-
27
- > **What's new in 0.4.19** — **the PyPI wheel now ships the whole dashboard and the benchmark fixture** (audit 2026-11-01, packaging follow-up to 0.4.18). The 0.4.18 wheel contained exactly two data files — `loop_memory/py.typed` and `loop_memory/serve/static/index.html` — because the package-data declaration was `loop_memory = ["py.typed", "serve/static/*.html"]`, and that glob is **not recursive**. Every CSS, JavaScript, i18n and image asset and the new benchmark fixture were **missing from the distribution**. Reproduced against the published wheel: `pip install loop-memory` started a server whose `GET /` returned `200` but whose `GET /static/js/main.js` and `GET /static/css/app.css` both returned **404** — a blank dashboard — and `loop-memory bench` crashed with `FileNotFoundError` on the packaged fixture. The declaration is now per package with recursive globs (`"loop_memory.serve.static" = ["**/*.html", "**/*.css", "**/*.js", "**/*.json", "**/*.svg"]`, `"loop_memory.bench" = ["fixtures/*.json"]`), so a new asset is picked up without anyone remembering to extend it, and the wheel contains all 32 non-Python files in the package tree. A new packaging test builds the real wheel and sdist **from a pristine source copy** — setuptools caches its file list in `egg-info/SOURCES.txt` and packages a stale `build/lib`, so building in the working tree would make the test pass for the wrong reason — and asserts every non-Python file actually reaches the distribution, then unpacks the wheel and runs the benchmark out of it. 8 new regression cases; suite at **1003 passed / 4 skipped**. [Full CHANGELOG →](./CHANGELOG.md#0419---2026-11-01)
28
-
29
- > **What's new in 0.4.18** — **the BM25 channel no longer publishes a score that collapses to zero, and there is finally a benchmark that measures it** (audit 2026-11-01, weekly ecosystem sync). SQLite's FTS5 `bm25()` computes the classic idf `log((N - n + 0.5) / (n + 0.5))` and **clamps every non-positive idf to `1e-6`** — so a query term present in 49 % of the corpus keeps a real idf while one present in 51 % has its weight annihilated, a factor of ~40 000 between two almost equally discriminative terms. With the `trigram` tokenizer that is the *normal* case for CJK (`知识图` alone occurs in more than half of any knowledge-graph-heavy corpus), and the visible symptom was the 0.4.15 evidence chain reporting **`bm25: 0.0`** — "this hit because bm25 0.0". The published score is now a textbook BM25 (`k1 = 1.2`, `b = 0.75`) with the Lucene / BM25+ idf `log(1 + (N - n + 0.5) / (n + 0.5))`, which is strictly positive for every term, so the scale cannot collapse. **A document that contains the query verbatim now outranks one that merely contains the same scattered trigrams** — the transferable half of `rohitg00/agentmemory` v0.9.31's prefix-score fix (2026-10-26: strict recall-all@5 **65.74 % → 84.68 %**, +18.94 pp), whose release note is careful to call its numbers "component retrieval results on inspected development data, not held-out answer accuracy". The `LIKE` fallback and the FTS5 hits are finally on the **same scale** (a `LIKE`-only hit used to publish the memory's `importance` under the name `bm25`). The candidate set is unchanged — the fix re-ranks, it does not add or drop documents. New **`loop-memory bench`**: a deterministic retrieval-quality benchmark over a fixed, versioned fixture (30 memories, 12 wiki pages, 11 judged queries) reporting **recall@1/3/5, MRR@10, nDCG@10** with the fixture's revision and the SHA-256 of its bytes; the run is fully offline (**no provider API is called, $0, no network**) and builds its store in a throwaway directory. On the shipped fixture the channel scored **MRR@10 = 0.6061, recall@1 = 0.2273, nDCG@10 = 0.7078** before and **1.0000 / 0.9545 / 1.0000** after — and the report keeps the raw FTS5 score next to the repaired one, so the defect stays visible instead of being quietly fixed and forgotten. Source: *agentmemory* v0.9.31 evaluation methodology + the FTS5 idf clamp in SQLite's `fts5_aux.c` (Apache-2.0 — *pattern only, no code copied*); honcho v3.4.1's "a filter must not silently drop a term" and v3.5.0's "a conclusion must cite its source" were reviewed and need no code (AGPL-3.0 — *pattern only*). 37 new regression cases; suite at **995 passed / 4 skipped**. [Full CHANGELOG →](./CHANGELOG.md#0418---2026-11-01)
30
- > **What's new in 0.4.17** — **a dry-run dream pass no longer writes to the database** (audit 2026-10-25). `loop-memory dream` and `POST /api/v1/dream` have always documented themselves as *"nothing is written unless `apply=True`"* — but the pass's decay stage called `MemoryStore.rescore_all()` unconditionally, which rewrote the `memories.score` column of **every** row while claiming to only inspect the store. The expiry and merge decisions were correctly left unapplied; the score decay was not. `rescore_all()` now takes a `dry_run` flag: it computes every new score and reports how many rows *would* change but issues no `UPDATE`, and the dream pass runs it with `dry_run=not apply`. A dry run still reports the size of the decay job through `report.rescored` while touching nothing. The contract wording is now exact rather than aspirational — a dry run changes **no memory data** (no expiry, no supersession, no rescore); the single write it still performs is one `cognitive_audit` row per proposed action with `action="suggest"` instead of `"applied"`, because that row *is* the reviewable plan the dashboard shows before you commit to it (`record_audit=False` suppresses even that). Also fixed in this release: `publish.yml` now sends `make_latest` as a JSON **boolean** (`gh api -F`) instead of a JSON **string** — GitHub's release API ignores the string form, so the "mark as latest" step had been returning 200 and reporting success while promoting nothing, leaving `/releases/latest` pointing at v0.4.14 through the 0.4.15 and 0.4.16 ships. The step now also re-reads `/releases/latest` and fails the job when the tag does not match, instead of trusting the exit code. 3 new regression cases (`tests/test_dream_2026_10_25.py`); suite at **958 passed / 4 skipped**. [Full CHANGELOG →](./CHANGELOG.md#0417---2026-10-25)
31
- > **What's new in 0.4.16** — **subject-key logical fact slots + `latest_only` recall + the "dream" sleep-time consolidation pass** (audit 2026-10-25, weekly ecosystem sync — Mem0 v2.2.1 "Dream", arXiv 2605.27366 "In Defense of Forgotten", Zep/Graphiti temporal edges, Letta memory blocks). New nullable `subject_key` column on `memories` names the *logical slot* a fact occupies (`"editor.preference"`); `upsert_memory(subject_key=...)` makes the new row the live version and soft-supersedes the previous one with a `superseded_by` pointer, so the version history survives and `recall_as_of()` can still answer "what did we believe last week?". The content-hash dedup path is now slot-aware — two different slots holding the same text stay two rows. `SCHEMA_VERSION` bumps 12 → 13 with an idempotent in-place migration. New `latest_only=False` flag on `recall()` / `recall_hybrid()` (Mem0 v2 `SearchMemoryOptions.latest_only` parity) collapses the answer in three layers: rows outside their validity window at *now* are dropped, a subject-key slot resolves to its newest **live** row (looked up in the store, so a stale version never comes back just because it matched the query better), and near-duplicate texts cluster by token Jaccard (default 0.85) with the newest row winning. Wired through `GET /api/recall?latest_only=1`, `GET /api/v1/recall?latest_only=1`, `loop-memory recall --latest-only` and `MemoryClient.recall(latest_only=True)`. New `loop_memory/jobs/dream.py` implements the sleep-time consolidation Mem0 v2 calls the "Dream" — deterministic, zero-dep and **non-destructive**: it decays scores, then judges every row past its deadline on **utility** (`0.45 × importance + 0.30 × usage + 0.25 × recency`) so a frequently-recalled old fact is *protected* while a never-used stale row is **soft-expired** (`invalid_at` stamped, row kept, history kept) instead of deleted, and finally merges near-duplicates into a supersession chain. Run it with `loop-memory dream [--apply] [--json]` or `POST /api/v1/dream` — it is a **dry run by default** and every decision lands in `cognitive_audit` so the dashboard can show and revert it. New store primitives `expire_memory()` / `revive_memory()`, and `memory_stats()` now reports `subject_key`, `ttl`, `valid_at`, `invalid_at` and a derived `ttl_state`. **Also fixed a real defect**: the hybrid recall path (BM25 / LIKE / semantic channels) did not filter `superseded_by`, so `recall_hybrid()` — the production path behind the CLI, MCP server and dashboard — returned both versions of a superseded fact. Source: Mem0 v2.2.1 Dream + ADD-only architecture + arXiv 2605.27366 utility-aware eviction + Zep/Graphiti bi-temporal edges (pattern only, no code copied). 77 new regression cases. [Full CHANGELOG →](./CHANGELOG.md#0416---2026-10-25)
32
- > **What's new in 0.4.15** — **content-hash dedup + bi-temporal validity window + evidence chain + `@agent_memory` decorator** (audit 2026-10-18, weekly ecosystem sync). Three new columns on `memories`: `content_hash` (SHA-256 of normalised text, indexed), `valid_at` (epoch the memory becomes active), `invalid_at` (epoch it expires — NULL means still open-ended). The schema migration bumps `SCHEMA_VERSION` 11 → 12 with an idempotent backfill so legacy DBs upgrade in-place. New `upsert_memory(valid_at=, invalid_at=, dedup=)` parameters let callers schedule future-dated or expiring memories and opt out of content-hash dedup for episodic event streams. `recall()` and `recall_hybrid()` now accept an optional `as_of_ts` (float epoch or ISO-8601) — when set, both methods drop memories whose `valid_at > as_of` or `invalid_at <= as_of`, complementing the existing `recall_as_of()` supersession-chain walk. Every recall hit now carries an `evidence: [{field, value, weight}]` array — BM25 / semantic / entity / rrf_total / high_importance channels are exposed individually so a UI can render "this hit because ... [bm25 1.0] [entity 0.7]" instead of a black-box score (Cognee 1.0 / Mem0 v2 evidence-citation pattern, zero-dep). New `loop_memory.decorators` exposes `@agent_memory(store, agent_id=, limit=)` — a one-line wrapper that recalls the top-k relevant memories before the body runs and writes a fresh `kind="interaction"` memory after. Strings returned from the wrapped function are stored verbatim; dict/list returns are JSON-serialised; the recall list is injected as a `memory_context` keyword when the function declares it, otherwise attached to dict results under `memories`. Sister `remember(store, text, ...)` helper for seeding. The BM25 `_like_fallback` now falls through to per-token OR-LIKE when the whole-string match returns nothing — natural-language questions like "What is the deploy process?" now hit stored facts even when FTS5 trigram can't match quoted exact phrases. Source: Cognee 1.0 ECL pipeline + Mem0 v2 hybrid fusion + Zep/Graphiti bi-temporal validity fields + Letta block-memory (pattern only, no code copied). 59 new regression cases. [Full CHANGELOG →](./CHANGELOG.md#0415---2026-10-18)
33
- > **What's new in 0.4.14** — **`loop-memory doctor` deep store-health check** (audit 2026-10-12). Ten diagnostic checks — `db_exists`, `db_integrity`, `schema_version`, `audit_triggers`, `audit_log_writable`, `audit_log_size`, `hooks_installed`, `version_consistent`, `wiki_no_orphans`, `recall_paths_works` — report `ok`/`info`/`warn`/`error` for the live `MemoryStore`. `--fix` auto-repairs the `audit_triggers` check. `--json` emits a stable shape for CI. `--list` prints the check function names without touching the store. `--verbose` shows `info`-level checks. Exit code `1` on any `error` status, `0` otherwise. Source: `cortexkit/magic-context doctor` auto-diagnostic pattern (MIT, 2026-10-12) + `dsh-memento` write-approval-gate pattern (Apache-2.0, 2026-10-12). Stdlib-only — we re-implement from scratch against our own `MemoryStore`. Also fixed a Python 3.14 bug: chained `store._conn().__enter__()` immediately triggers the contextmanager's `finally` and closes the connection mid-query — both call sites in `doctor.py` now use explicit `with` form. 27 new regression cases. [Full CHANGELOG →](./CHANGELOG.md#0414---2026-10-12)
34
-
35
- > **What's new in 0.4.13** — **recall token / char budget cap** + **unified `level=0|1|2` tiered-loader knob** (audit 2026-10-05). New `max_chars` parameter on `MemoryStore.recall()` / `recall_paths()` / `recall_hybrid()` / `recall_as_of()` caps the *total* payload across all returned hits — when the budget is exceeded the lowest-ranked hit shrinks first so the top-scoring hits always keep their full body. `recall()` / `recall_paths()` / `recall_as_of()` now also accept `level=0|1|2` so a caller can ask for the L0 abstract ladder rung inline (titles + tags + abstract only, never the full `text` / `body`) without routing through `recall_paths()` separately. Both knobs are wired through the HTTP routes (`GET /api/recall?max_chars=&level=`, `GET /api/recall/outline?max_chars=`), the CLI (`loop-memory recall --max-chars N --level 0|1|2`, `recall-paths --max-chars N`), the SDK (`LoopMemoryClient.recall(max_chars=, level=)`), and the MCP `tool_recall` server. Source: `vectorize-io/hindsight v0.10.2` "concise extraction by default" (Apache-2.0, 2026-09-29) + `aiming-lab/SimpleMem` ICML'26 semantic-lossless-compression (Apache-2.0, 2026-10-04) + `volcengine/OpenViking` tiered-loader (AGPLv3, 2026-10-02 — *pattern only, no code copied*). 26 new regression cases. [Full CHANGELOG →](./CHANGELOG.md#0413---2026-10-05)
36
-
37
- > **What's new in 0.4.12** — **OKF bundle import** + **DB-level audit enforcement** (audit 2026-09-27). The new `loop-memory import-okf <in_dir>` (and `POST /api/import/okf`) is the symmetric counterpart to `export-okf` (0.4.9) — bring an OKF v0.2 bundle back into Loop Memory as wiki pages, with idempotent upsert on `(scope, slug)`, optional `--dry-run` and `--skip-conflicts`. Source: `thecolourfoundation/rune` HN launch (MIT, 2026-09-25) + `okf-memory/okf-agent-memory` Issues #12-#15 + `Deja-Vu` (MIT, 2026-09-24) + Google OKF v0.2 spec (Apache-2.0, 2026-09-01). The new `MemoryStore._install_audit_triggers()` installs 12 SQLite triggers on `memories` / `wiki_pages` / `relations` / `consolidation_runs` so the `audit_log` table cannot be skipped — even a raw `sqlite3` writer that bypasses the Python wrapper still appends an audit row with `actor='db_trigger'`. New `MemoryStore.list_audit_log()` reads the log; `GET /api/stats` reports `audit_log` + `audit_triggers` counts. Source: `cogstate-eng/memory-eternal v0.4.3` `enforceAudit()` pattern (MIT, 2026-09-23). 31 new regression cases. [Full CHANGELOG →](./CHANGELOG.md#0412---2026-09-27)
38
-
39
- > **What's new in 0.4.11** — **operational probes** +
40
- **SQLite write-contention fix** (audit 2026-09-24). New
41
- ``GET /api/healthz`` (liveness) + ``GET /api/readyz``
42
- (readiness) endpoints let Kubernetes / launchd keepalive
43
- polls probe the serve without provisioning a Bearer token;
44
- ``/api/readyz`` returns 503 when the SQLite store is
45
- unreachable. Also fixed a real concurrency bug: every
46
- store connection now sets ``PRAGMA busy_timeout=5000``
47
- (was 0 ms) so the watcher + serve + scheduler threads no
48
- longer fail with ``database is locked`` when the
49
- consolidator writes concurrently. 7 new regression cases.
50
- [Full CHANGELOG →](./CHANGELOG.md#0411---2026-09-24)
51
-
52
- > **What's new in 0.4.10** — **path-safety guard** for the
53
- five export / snapshot endpoints. ``MemoryStore`` already
54
- validated query / wiki content; the new
55
- ``_safe_resolve_path`` helper (audit 2026-09-23) closes the
56
- remaining path-traversal vector by refusing any path that
57
- points at the live SQLite store, a system directory
58
- (``/etc`` / ``/usr`` / ``/System``), or a sensitive dotfile
59
- (``~/.ssh`` / ``~/.aws`` / ``~/.gnupg``). Also in this
60
- release: a hard cap on the in-process LLM response cache
61
- (was unbounded; now 256 entries with FIFO eviction) and a
62
- fix for ``subgraph_for(max_hops=...)`` which used to ignore
63
- the argument and always walk one hop (now BFS up to N hops,
64
- hard-capped at 8). 29 new regression cases. [Full
65
- CHANGELOG →](./CHANGELOG.md#0410---2026-09-23)
66
-
67
- > **What's new in 0.4.9** — **OKF v0.2 export** + **bi-temporal `as_of` recall**. `loop-memory export-okf <dir>` writes an [Open Knowledge Format](https://github.com/okf/SPEC) v0.2 bundle — one `.md` file per wiki page with YAML frontmatter + an `index.md` index — so a Loop Memory store can be handed to any OKF-aware tool (Grep, Obsidian, akitaonrails/ai-memory, okf-agent-memory, ...) without re-parsing the body (audit 2026-09-20, Google OKF v0.2 spec + akitaonrails/ai-memory 2.0 + okf-memory/okf-agent-memory, all Apache-2.0/MIT, 2026-09-01 → 2026-09-06). `loop-memory recall <q> --as-of <ISO|epoch>` answers the bi-temporal question "what did we know about this query at that moment?" — a memory written after the moment is invisible, and a superseded memory walks its chain to find whether the invalidation had happened by then (audit 2026-09-20, loomcycle v1.33-v1.49 RFCs BL/BS/BU/BV/BW, Apache-2.0). Both shapes ship with HTTP routes (`POST /api/export/okf`, `GET /api/recall?as_of=...`). 34 new regression cases pin both shapes. [Full CHANGELOG →](./CHANGELOG.md#049---2026-09-20)
68
-
69
- > **What's new in 0.4.8** — **L0 outline recall** +
70
- > **per-agent identity bootstrap**. `loop-memory recall-paths <q>`
71
- > (and the new `recall --outline` flag) returns only `id` +
72
- > `abstract` + `score` + `why` for each hit — never the full
73
- > body — so an agent can identify the top candidates cheaply
74
- > before deciding which bodies to fetch (audit 2026-09-13,
75
- > tigerless-labs/agent-memory v0.3.0 recall-ladder pattern).
76
- > `loop-memory init --agent <name>` registers an agent in the
77
- > new `agents` table and (with `--install-hooks`) also wires
78
- > the MCP + SessionStart hooks for codex/claude/hermes; the
79
- > index is bumped on every `install-hooks` run so a user can
80
- > always answer "which CLIs are wired into this store?" with
81
- > one SQL query (audit 2026-09-13, Mem0 CLI `init --agent`
82
- > pattern). Both shapes ship with HTTP routes
83
- > (`/api/recall/outline`, `/api/agents`, `/api/init/agent`).
84
- > 57 new regression cases pin both shapes. [Full
85
- > changelog →](CHANGELOG.md)
86
- >
87
- > **What's new in 0.4.7** — **per-memory lifetime stats** +
88
- > **portable SQLite snapshot**. `loop-memory memory-stats <id>`
89
- > returns a flat dict (recall_count, positive, negative, age,
90
- > etc.) so you can ask "is anyone still using this memory?"
91
- > without cracking open the SQLite file (audit 2026-09-06,
92
- > agentmemory v1.3.0 pattern). `loop-memory snapshot <out>` /
93
- > `loop-memory restore <in>` move a single-file, lossless
94
- > snapshot of the entire store — including recall-quality
95
- > signals and the supersession chain — to another machine
96
- > (audit 2026-09-06, codexa-memory v0.2.0 pattern). Both shapes
97
- > ship with HTTP routes (`/api/memories/{id}/stats`,
98
- > `/api/snapshot`, `/api/snapshot/restore`). 33 new regression
99
- > cases pin both shapes.
100
- > [Full changelog →](CHANGELOG.md)
25
+ > **What's new in 0.4.22** — **the agent-namespace filter now works, and it is enforced in the query instead of on top of the answer** (audit 2026-11-15, weekly ecosystem sync). The audit for this release (2026-11-15) turned up a real hole rather than a hypothetical one. `GET /api/v1/recall?agent_id=X` was documented as *"Unified recall filtered to a single Agent namespace"* — but the filter was applied to the store's **output**, and it had two separate failure modes. **On the legacy path it was a silent no-op:** `MemoryStore.recall()` never returned the `agent_id` / `user_id` columns, so the route's predicate read `None`/`None` for every row, decided every row was "global", and let all of them through — **an agent that explicitly asked for its own namespace was handed every other agent's memories as well.** **On the hybrid path it ran after the `LIMIT`:** the filter worked, but it ran on the store's already-trimmed result set, so a foreign namespace could spend the caller's *entire* result budget — an agent asking for 5 hits could get **zero**, while its own matching rows sat further down the ranking. Both failures have the same root cause: **the predicate was in the caller instead of in the query** — exactly what the upstream projects in this window converged on. `plastic-labs/honcho` v3.6.0 (2026-11-10, AGPL-3.0) made `tenant_id` lead every primary key, with Postgres row-level security scoping **every read and write**, and refused to hand a tenant an empty namespace; `EverMind-AI/EverOS` v1.6.0 (2026-11-11) made deletion owner-scoped and answered `503 SESSION_BUSY` rather than deleting half of a session; `thedotmack/claude-mem` (v15.16.x) gave each scope its own project by default and made *"an explicitly empty project selection show nothing instead of observations from every project"* (#5866); `vectorize-io/hindsight` v1.2.1 stopped a bank clone from taking over another bank's alias (#6280) — pattern references only, no code copied, and the two AGPL-3.0 projects contributed nothing but an idea. **What shipped:** a single predicate builder (`_scope_clause()`) that every retrieval channel builds its query from, so the namespace lives in the **SQL** of the BM25 channel (both the FTS5 query and the `LIKE` backstop), the semantic channel, the entity channel, `recall()`, `recall_hybrid()` and `recall_as_of()` — the fused ranking, and the `limit` that trims it, now only ever sees the caller's own rows; every hit now reports its `agent_id` / `user_id`, which is what makes the legacy no-op impossible to reintroduce silently; all four public surfaces expose it — `GET /api/v1/recall` and `GET /api/recall` accept `agent_id` / `user_id`, the MCP `recall` tool scopes to `LOOP_MEMORY_AGENT_ID` / `LOOP_MEMORY_USER_ID` (the MCP server is started once per client, so the environment already identifies the caller) and accepts explicit overrides, and `loop-memory recall` gained `--agent-id` / `--user-id`; and a client that **writes** into a namespace now **reads** from it — `MemoryClient.memory(store, agent_id="x")` and `MemoryClient.http(url, agent_id="x")` fall back to that identity on `recall()`, which is precisely the asymmetry that hid the leak (pass `agent_id=""` for the explicit escape hatch). A memory with no `agent_id` stays **global** and visible to everyone, because the ingest pipeline, the consolidator and the dream job all write global rows. 43 new regression cases; suite at **1084 passed / 4 skipped**. See [`docs/namespace-isolation.md`](./docs/namespace-isolation.md) for the full contract. [Full CHANGELOG →](./CHANGELOG.md#0422---2026-11-15)
26
+
27
+ <details>
28
+ <summary><b>Older releases</b> — 0.4.21 and earlier, one line each (the CHANGELOG has the full detail)</summary>
29
+
30
+ | Version | Date | What changed | Full notes |
31
+ | --- | --- | --- | --- |
32
+ | [0.4.21](./CHANGELOG.md#0421---2026-11-08) | 2026-11-08 | The remote SDK can authenticate, and it refuses to send memory data over a plaintext connection |
33
+ | [0.4.20](./CHANGELOG.md#0420---2026-11-01) | 2026-11-01 | The packaging test works on the oldest supported Python (no `tomllib` on 3.10) |
34
+ | [0.4.19](./CHANGELOG.md#0419---2026-11-01) | 2026-11-01 | The wheel ships the whole dashboard and the benchmark fixture (recursive package-data globs) |
35
+ | [0.4.18](./CHANGELOG.md#0418---2026-11-01) | 2026-11-01 | The BM25 channel no longer publishes a score that collapses to zero — plus a retrieval benchmark |
36
+ | [0.4.17](./CHANGELOG.md#0417---2026-10-25) | 2026-10-25 | A dry-run dream pass no longer writes to the database |
37
+ | [0.4.16](./CHANGELOG.md#0416---2026-10-25) | 2026-10-25 | Subject-key logical fact slots, `latest_only` recall, and the sleep-time "dream" pass |
38
+ | [0.4.15](./CHANGELOG.md#0415---2026-10-18) | 2026-10-18 | Content-hash dedup, bi-temporal validity window, evidence chain |
39
+ | [0.4.14](./CHANGELOG.md#0414---2026-10-12) | 2026-10-12 | `loop-memory doctor` — a deep store-health check |
40
+ | [0.4.13](./CHANGELOG.md#0413---2026-10-05) | 2026-10-05 | Recall token / char budget cap and a unified `level=0|1|2` tiered-loader knob |
41
+ | [0.4.12](./CHANGELOG.md#0412---2026-09-27) | 2026-09-27 | OKF bundle import and DB-level audit enforcement (12 SQLite triggers) |
42
+ | [0.4.11](./CHANGELOG.md#0411---2026-09-24) | 2026-09-24 | Operational probes (`/api/healthz`, `/api/readyz`) and the SQLite write-contention fix |
43
+ | [0.4.10](./CHANGELOG.md#0410---2026-09-23) | 2026-09-23 | Path-safety guard for the five export / snapshot endpoints, LLM cache cap, `subgraph_for(max_hops=)` |
44
+ | [0.4.9](./CHANGELOG.md#049---2026-09-20) | 2026-09-20 | OKF v0.2 export and bi-temporal `as_of` recall |
45
+ | [0.4.8](./CHANGELOG.md#048---2026-09-13) | 2026-09-13 | L0 outline recall and per-agent identity bootstrap |
46
+ | [0.4.7](./CHANGELOG.md#047---2026-09-06) | 2026-09-06 | Per-memory lifetime stats and a portable SQLite snapshot |
47
+ | [0.4.6](./CHANGELOG.md#046---2026-08-30) | 2026-08-30 | The memory supersession chain (no hard deletes) |
48
+ | [0.4.5](./CHANGELOG.md#045---2026-08-23) | 2026-08-23 | `loop_memory.__version__` was stale at `"0.2.0"` |
49
+ | [0.4.4](./CHANGELOG.md#044---2026-08-23) | 2026-08-23 | `loop-memory rules --agent <name> --write` |
50
+ | [0.4.3](./CHANGELOG.md#043---2026-08-16) | 2026-08-16 | The cognitive sweep N+1 fix |
51
+ | [0.4.2](./CHANGELOG.md#042---2026-08-09) | 2026-08-09 | README refresh |
52
+ | [0.4.1](./CHANGELOG.md#041---2026-08-09) | 2026-08-09 | CLI dispatch hardened against common `--help` crashes |
53
+ | [0.4.0](./CHANGELOG.md#040---2026-08-09) | 2026-08-09 | OpenAI base URL env-var fallback |
54
+ | [0.3.0](./CHANGELOG.md#030--2026-07-11) | 2026-07-11 | The run progress strip is back |
55
+ | [0.2.0](./CHANGELOG.md#020--2026-07-10) | 2026-07-10 | The SQLite-backed `MemoryStore` |
56
+ | [0.1.0](./CHANGELOG.md#010--2026-07-10) | 2026-07-10 | The dashboard tab empty-data regression |
57
+
58
+ </details>
101
59
 
102
60
  ---
103
61
 
@@ -265,6 +223,8 @@ Hermes, clawx) can read and write, we built this for you.
265
223
  | [docs/api.md](docs/api.md) | HTTP API reference — every route, request body, and response shape the UI consumes |
266
224
  | [docs/agent-memory-api.md](docs/agent-memory-api.md) | Stable four-verb SDK / HTTP / MCP contract for any Agent |
267
225
  | [docs/universal-agent-memory.md](docs/universal-agent-memory.md) | v7 graph memory, cognitive sleep, portable bundles, namespaces, and MCP/CLI extensions |
226
+ | [docs/transport-security.md](docs/transport-security.md) | How a remote client is allowed to talk to `loop-memory serve` — the transport guard, bearer-token auth, redirect hardening, and the `plaintext_exposure` doctor check (0.4.21) |
227
+ | [docs/namespace-isolation.md](docs/namespace-isolation.md) | Who may read which memory — the agent / user namespace predicate, why it lives in the SQL instead of on top of the answer, and the global-row semantics (0.4.22) |
268
228
  | [docs/providers.md](docs/providers.md) | LLM provider reference — built-in providers, defaults, base URLs, and how to add a new one |
269
229
  | [docs/auto-capture.md](docs/auto-capture.md) | Hooking Codex / Claude / Hermes / OpenClaw watchers (filesystem, launchd, systemd, cron) |
270
230
  | [CONTRIBUTING.md](CONTRIBUTING.md) | Local dev loop, pytest, where secrets live, how to add a provider/source |
@@ -618,7 +578,7 @@ loop_memory/
618
578
  backends/ # embedding (hashing / sentence-transformers) + vector store (memory / chroma)
619
579
  storage/ # SQLite-backed MemoryStore + migrations
620
580
  privacy/ # <private> stripping + regex redaction
621
- security/ # Keychain / 0600-file secrets wrapper
581
+ security/ # Keychain / 0600-file secrets wrapper + the transport guard
622
582
  mcp/ # stdio JSON-RPC MCP server
623
583
  serve/ # FastAPI app, watcher, web UI (Timeline / Dashboard / Wiki / Graph)
624
584
  export/ # markdown + v7 portable bundle export/import
@@ -628,16 +588,19 @@ loop_memory/
628
588
  memory/types.py # MemoryItem + 4 tiers
629
589
  examples/demo.py # runnable, zero-API-key demo
630
590
  py.typed
631
- tests/ # 475 unit tests across memory / SDK / serve / CLI / scripts
591
+ tests/ # 1084 unit tests across memory / SDK / serve / CLI / scripts
632
592
  docs/
633
593
  auto-capture.md # launchd / systemd / cron recipes
634
594
  architecture.md # layered view + 5-stage evolution pipeline
635
595
  api.md # HTTP API reference
596
+ transport-security.md # how a remote client may talk to `serve` (0.4.21)
597
+ namespace-isolation.md # who may read which memory (0.4.22)
636
598
  agent-memory-api.md # four-verb SDK / HTTP / MCP contract
637
599
  universal-agent-memory.md # v7 graph memory + cognitive sleep + bundles
638
600
  providers.md # LLM provider reference
639
601
  settings.md # settings table + secrets file
640
602
  weekly-research-automation.md # how the project auto-evolves from upstream research
603
+ research/ # the weekly ecosystem notes each release is based on
641
604
  ```
642
605
 
643
606
  ---
@@ -667,6 +630,34 @@ The web UI stores it in `localStorage` under `loop_auth_token` and attaches
667
630
  it automatically; non-browser clients (curl, MCP, SDK) must opt in by
668
631
  passing it explicitly.
669
632
 
633
+ **Remote clients (SDK):** the HTTP SDK can send the token for you, and it
634
+ refuses to send it — or any memory text — over an unencrypted connection
635
+ to a remote host:
636
+
637
+ ```python
638
+ client = MemoryClient.http(
639
+ "https://memory.example.com",
640
+ token=open("token.txt").read().strip(), # Authorization: Bearer …
641
+ )
642
+ client.remember("deploys go out on Tuesdays")
643
+ ```
644
+
645
+ Plain `http://` is still fine for `127.0.0.1` / `localhost` / `::1` (the
646
+ traffic never leaves the machine — that is the default deployment), but
647
+ plain `http://` to **any other host is refused**: the memory text and the
648
+ bearer token would cross the network in cleartext. A private tunnel or a
649
+ VPN'd LAN is a legitimate deployment, so `allow_insecure=True` (or
650
+ `LOOP_MEMORY_ALLOW_INSECURE=1`) exists — but it is never silent, it
651
+ warns on every construction. Redirects that downgrade `https://` →
652
+ `http://`, and cross-origin redirects that would replay the token, are
653
+ refused too. The full contract is in
654
+ [docs/transport-security.md](docs/transport-security.md).
655
+
656
+ **Server side:** `loop-memory serve --host <non-loopback>` prints a
657
+ warning and records the bind address, so `loop-memory doctor` reports it
658
+ as a `plaintext_exposure` warning — the API is reachable from the network
659
+ over plaintext.
660
+
670
661
  **When to set a token:**
671
662
 
672
663
  - ✅ Always, even on loopback. The default of "no token" exists only as a
@@ -683,6 +674,12 @@ passing it explicitly.
683
674
 
684
675
  - `install-hooks` runs Python that touches `~/.*` — it's gated by the
685
676
  bearer token like every other `POST /api/admin/*` route.
677
+ - A remote client cannot leak memory text to the network in cleartext:
678
+ the SDK refuses a plaintext `http://` base URL that is not loopback,
679
+ refuses a URL with embedded credentials, and refuses redirects that
680
+ downgrade the scheme or would replay the `Authorization` header
681
+ (audit 2026-11-08, 0.4.21 — see
682
+ [docs/transport-security.md](docs/transport-security.md)).
686
683
  - The `<private>...</private>` span stripping in the privacy layer keeps
687
684
  user-marked secrets out of long-term storage; the regex redaction layer
688
685
  then catches API keys / tokens / private keys / JWTs / generic
@@ -62,10 +62,14 @@ def run_recall(args) -> int:
62
62
  caller can ask "what did we know about this query at that
63
63
  moment?". The wiki and entity sections are unchanged — only
64
64
  the raw-memory block grows the new line.
65
+
66
+ ``--agent-id`` / ``--user-id`` (audit 2026-11-15, loop-memory
67
+ 0.4.22) restrict the answer to one namespace. Memories written
68
+ without an agent or user id are global and stay visible.
65
69
  """
66
70
  from ...storage.sqlite_store import MemoryStore
67
71
  if not args:
68
- return die("usage: loop-memory recall <query> [--verbose] [--limit N]")
72
+ return die("usage: loop-memory recall <query> [--verbose] [--limit N] [--agent-id <id>] [--user-id <id>]")
69
73
  # Parse the small flag subset we expose. Keep it dumb-on-purpose:
70
74
  # we don't want argparse to swallow a token that happens to start
71
75
  # with "--" inside the user's query (e.g. ``recall --foo bar``).
@@ -80,6 +84,11 @@ def run_recall(args) -> int:
80
84
  level = 1
81
85
  # Audit 2026-10-25 (loop-memory 0.4.16) -- latest-only mode.
82
86
  latest_only = False
87
+ # Audit 2026-11-15 (loop-memory 0.4.22) -- agent / user
88
+ # namespace. ``None`` means "no restriction", which keeps the
89
+ # legacy see-everything behaviour.
90
+ agent_id: str | None = None
91
+ user_id: str | None = None
83
92
  qargs: list[str] = []
84
93
  i = 0
85
94
  while i < len(args):
@@ -158,6 +167,17 @@ def run_recall(args) -> int:
158
167
  except ValueError:
159
168
  return die(f"--level must be 0, 1, or 2, got {args[i + 1]!r}")
160
169
  i += 2
170
+ elif a == "--agent-id" and i + 1 < len(args):
171
+ # Audit 2026-11-15 (loop-memory 0.4.22) -- restrict the
172
+ # answer to one agent namespace. Memories with no agent
173
+ # id are global and stay visible.
174
+ agent_id = args[i + 1].strip() or None
175
+ i += 2
176
+ elif a == "--user-id" and i + 1 < len(args):
177
+ # Audit 2026-11-15 (loop-memory 0.4.22) -- restrict the
178
+ # answer to one user namespace.
179
+ user_id = args[i + 1].strip() or None
180
+ i += 2
161
181
  elif a == "--limit" and i + 1 < len(args):
162
182
  try:
163
183
  limit = int(args[i + 1])
@@ -170,13 +190,14 @@ def run_recall(args) -> int:
170
190
  qargs.append(a)
171
191
  i += 1
172
192
  if not qargs:
173
- return die("usage: loop-memory recall <query> [--verbose] [--outline] [--as-of <ISO|epoch>] [--latest-only] [--limit N] [--max-chars N] [--level 0|1|2]")
193
+ return die("usage: loop-memory recall <query> [--verbose] [--outline] [--as-of <ISO|epoch>] [--latest-only] [--agent-id <id>] [--user-id <id>] [--limit N] [--max-chars N] [--level 0|1|2]")
174
194
  store = MemoryStore(default_db_path())
175
195
  query = " ".join(qargs)
176
196
  if as_of is not None:
177
197
  try:
178
198
  r = store.recall_as_of(query, as_of, limit=limit,
179
- max_chars=max_chars, level=level)
199
+ max_chars=max_chars, level=level,
200
+ agent_id=agent_id, user_id=user_id)
180
201
  except ValueError as e:
181
202
  return die(str(e))
182
203
  elif outline:
@@ -190,7 +211,11 @@ def run_recall(args) -> int:
190
211
  # "everything" semantics.
191
212
  as_of_ts=validity_at,
192
213
  # Audit 2026-10-25: one answer per fact.
193
- latest_only=latest_only)
214
+ latest_only=latest_only,
215
+ # Audit 2026-11-15: namespace predicate in
216
+ # SQL -- the limit counts only the caller's
217
+ # own rows.
218
+ agent_id=agent_id, user_id=user_id)
194
219
  has = False
195
220
  if r["wiki"]:
196
221
  has = True