loop-memory 0.4.20__tar.gz → 0.4.21__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 (210) hide show
  1. {loop_memory-0.4.20 → loop_memory-0.4.21}/PKG-INFO +73 -79
  2. {loop_memory-0.4.20 → loop_memory-0.4.21}/README.md +72 -78
  3. {loop_memory-0.4.20 → loop_memory-0.4.21}/loop_memory/cli/commands/serve.py +19 -0
  4. {loop_memory-0.4.20 → loop_memory-0.4.21}/loop_memory/doctor.py +75 -0
  5. {loop_memory-0.4.20 → loop_memory-0.4.21}/loop_memory/sdk.py +67 -5
  6. {loop_memory-0.4.20 → loop_memory-0.4.21}/loop_memory/sdk_extensions.py +55 -13
  7. loop_memory-0.4.21/loop_memory/security/__init__.py +39 -0
  8. loop_memory-0.4.21/loop_memory/security/transport.py +339 -0
  9. {loop_memory-0.4.20 → loop_memory-0.4.21}/loop_memory.egg-info/PKG-INFO +73 -79
  10. {loop_memory-0.4.20 → loop_memory-0.4.21}/loop_memory.egg-info/SOURCES.txt +2 -0
  11. {loop_memory-0.4.20 → loop_memory-0.4.21}/pyproject.toml +1 -1
  12. {loop_memory-0.4.20 → loop_memory-0.4.21}/tests/test_agent_memory_sdk.py +5 -1
  13. {loop_memory-0.4.20 → loop_memory-0.4.21}/tests/test_doctor_2026_10_12.py +8 -7
  14. loop_memory-0.4.21/tests/test_transport_security_2026_11_08.py +456 -0
  15. loop_memory-0.4.20/loop_memory/security/__init__.py +0 -20
  16. {loop_memory-0.4.20 → loop_memory-0.4.21}/LICENSE +0 -0
  17. {loop_memory-0.4.20 → loop_memory-0.4.21}/loop_memory/__init__.py +0 -0
  18. {loop_memory-0.4.20 → loop_memory-0.4.21}/loop_memory/backends/__init__.py +0 -0
  19. {loop_memory-0.4.20 → loop_memory-0.4.21}/loop_memory/backends/embedding.py +0 -0
  20. {loop_memory-0.4.20 → loop_memory-0.4.21}/loop_memory/backends/sentence_embedder.py +0 -0
  21. {loop_memory-0.4.20 → loop_memory-0.4.21}/loop_memory/backends/vector_store.py +0 -0
  22. {loop_memory-0.4.20 → loop_memory-0.4.21}/loop_memory/bench/__init__.py +0 -0
  23. {loop_memory-0.4.20 → loop_memory-0.4.21}/loop_memory/bench/fixtures/default.json +0 -0
  24. {loop_memory-0.4.20 → loop_memory-0.4.21}/loop_memory/bench/runner.py +0 -0
  25. {loop_memory-0.4.20 → loop_memory-0.4.21}/loop_memory/cli/__init__.py +0 -0
  26. {loop_memory-0.4.20 → loop_memory-0.4.21}/loop_memory/cli/_common.py +0 -0
  27. {loop_memory-0.4.20 → loop_memory-0.4.21}/loop_memory/cli/commands/__init__.py +0 -0
  28. {loop_memory-0.4.20 → loop_memory-0.4.21}/loop_memory/cli/commands/bench.py +0 -0
  29. {loop_memory-0.4.20 → loop_memory-0.4.21}/loop_memory/cli/commands/cognitive.py +0 -0
  30. {loop_memory-0.4.20 → loop_memory-0.4.21}/loop_memory/cli/commands/diag.py +0 -0
  31. {loop_memory-0.4.20 → loop_memory-0.4.21}/loop_memory/cli/commands/doctor.py +0 -0
  32. {loop_memory-0.4.20 → loop_memory-0.4.21}/loop_memory/cli/commands/graph.py +0 -0
  33. {loop_memory-0.4.20 → loop_memory-0.4.21}/loop_memory/cli/commands/hooks.py +0 -0
  34. {loop_memory-0.4.20 → loop_memory-0.4.21}/loop_memory/cli/commands/read.py +0 -0
  35. {loop_memory-0.4.20 → loop_memory-0.4.21}/loop_memory/cli/commands/rules.py +0 -0
  36. {loop_memory-0.4.20 → loop_memory-0.4.21}/loop_memory/cli/commands/write.py +0 -0
  37. {loop_memory-0.4.20 → loop_memory-0.4.21}/loop_memory/cli/main.py +0 -0
  38. {loop_memory-0.4.20 → loop_memory-0.4.21}/loop_memory/decorators.py +0 -0
  39. {loop_memory-0.4.20 → loop_memory-0.4.21}/loop_memory/engine/__init__.py +0 -0
  40. {loop_memory-0.4.20 → loop_memory-0.4.21}/loop_memory/engine/loop.py +0 -0
  41. {loop_memory-0.4.20 → loop_memory-0.4.21}/loop_memory/engine/reflect.py +0 -0
  42. {loop_memory-0.4.20 → loop_memory-0.4.21}/loop_memory/examples/__init__.py +0 -0
  43. {loop_memory-0.4.20 → loop_memory-0.4.21}/loop_memory/examples/demo.py +0 -0
  44. {loop_memory-0.4.20 → loop_memory-0.4.21}/loop_memory/export/__init__.py +0 -0
  45. {loop_memory-0.4.20 → loop_memory-0.4.21}/loop_memory/export/memory_md.py +0 -0
  46. {loop_memory-0.4.20 → loop_memory-0.4.21}/loop_memory/graph/__init__.py +0 -0
  47. {loop_memory-0.4.20 → loop_memory-0.4.21}/loop_memory/graph/build.py +0 -0
  48. {loop_memory-0.4.20 → loop_memory-0.4.21}/loop_memory/graph/extract.py +0 -0
  49. {loop_memory-0.4.20 → loop_memory-0.4.21}/loop_memory/ingest/__init__.py +0 -0
  50. {loop_memory-0.4.20 → loop_memory-0.4.21}/loop_memory/ingest/loader.py +0 -0
  51. {loop_memory-0.4.20 → loop_memory-0.4.21}/loop_memory/ingest/pipeline.py +0 -0
  52. {loop_memory-0.4.20 → loop_memory-0.4.21}/loop_memory/jobs/__init__.py +0 -0
  53. {loop_memory-0.4.20 → loop_memory-0.4.21}/loop_memory/jobs/cognitive.py +0 -0
  54. {loop_memory-0.4.20 → loop_memory-0.4.21}/loop_memory/jobs/compact.py +0 -0
  55. {loop_memory-0.4.20 → loop_memory-0.4.21}/loop_memory/jobs/consolidate.py +0 -0
  56. {loop_memory-0.4.20 → loop_memory-0.4.21}/loop_memory/jobs/contradiction.py +0 -0
  57. {loop_memory-0.4.20 → loop_memory-0.4.21}/loop_memory/jobs/dream.py +0 -0
  58. {loop_memory-0.4.20 → loop_memory-0.4.21}/loop_memory/jobs/evolution.py +0 -0
  59. {loop_memory-0.4.20 → loop_memory-0.4.21}/loop_memory/jobs/graph.py +0 -0
  60. {loop_memory-0.4.20 → loop_memory-0.4.21}/loop_memory/jobs/llm_compact_pass.py +0 -0
  61. {loop_memory-0.4.20 → loop_memory-0.4.21}/loop_memory/jobs/llm_consolidate.py +0 -0
  62. {loop_memory-0.4.20 → loop_memory-0.4.21}/loop_memory/jobs/scheduler.py +0 -0
  63. {loop_memory-0.4.20 → loop_memory-0.4.21}/loop_memory/llm/__init__.py +0 -0
  64. {loop_memory-0.4.20 → loop_memory-0.4.21}/loop_memory/llm/base.py +0 -0
  65. {loop_memory-0.4.20 → loop_memory-0.4.21}/loop_memory/llm/openai_adapter.py +0 -0
  66. {loop_memory-0.4.20 → loop_memory-0.4.21}/loop_memory/llm/providers.py +0 -0
  67. {loop_memory-0.4.20 → loop_memory-0.4.21}/loop_memory/mcp/__init__.py +0 -0
  68. {loop_memory-0.4.20 → loop_memory-0.4.21}/loop_memory/memory/__init__.py +0 -0
  69. {loop_memory-0.4.20 → loop_memory-0.4.21}/loop_memory/memory/types.py +0 -0
  70. {loop_memory-0.4.20 → loop_memory-0.4.21}/loop_memory/privacy/__init__.py +0 -0
  71. {loop_memory-0.4.20 → loop_memory-0.4.21}/loop_memory/privacy/private.py +0 -0
  72. {loop_memory-0.4.20 → loop_memory-0.4.21}/loop_memory/privacy/redact.py +0 -0
  73. {loop_memory-0.4.20 → loop_memory-0.4.21}/loop_memory/py.typed +0 -0
  74. {loop_memory-0.4.20 → loop_memory-0.4.21}/loop_memory/security/secrets.py +0 -0
  75. {loop_memory-0.4.20 → loop_memory-0.4.21}/loop_memory/serve/__init__.py +0 -0
  76. {loop_memory-0.4.20 → loop_memory-0.4.21}/loop_memory/serve/app.py +0 -0
  77. {loop_memory-0.4.20 → loop_memory-0.4.21}/loop_memory/serve/handlers.py +0 -0
  78. {loop_memory-0.4.20 → loop_memory-0.4.21}/loop_memory/serve/routes/_shared.py +0 -0
  79. {loop_memory-0.4.20 → loop_memory-0.4.21}/loop_memory/serve/routes/admin.py +0 -0
  80. {loop_memory-0.4.20 → loop_memory-0.4.21}/loop_memory/serve/routes/cognitive.py +0 -0
  81. {loop_memory-0.4.20 → loop_memory-0.4.21}/loop_memory/serve/routes/export.py +0 -0
  82. {loop_memory-0.4.20 → loop_memory-0.4.21}/loop_memory/serve/routes/graph.py +0 -0
  83. {loop_memory-0.4.20 → loop_memory-0.4.21}/loop_memory/serve/routes/insights.py +0 -0
  84. {loop_memory-0.4.20 → loop_memory-0.4.21}/loop_memory/serve/routes/memories.py +0 -0
  85. {loop_memory-0.4.20 → loop_memory-0.4.21}/loop_memory/serve/routes/sessions.py +0 -0
  86. {loop_memory-0.4.20 → loop_memory-0.4.21}/loop_memory/serve/routes/system.py +0 -0
  87. {loop_memory-0.4.20 → loop_memory-0.4.21}/loop_memory/serve/routes/wiki.py +0 -0
  88. {loop_memory-0.4.20 → loop_memory-0.4.21}/loop_memory/serve/static/__init__.py +0 -0
  89. {loop_memory-0.4.20 → loop_memory-0.4.21}/loop_memory/serve/static/css/app.css +0 -0
  90. {loop_memory-0.4.20 → loop_memory-0.4.21}/loop_memory/serve/static/css/layout.css +0 -0
  91. {loop_memory-0.4.20 → loop_memory-0.4.21}/loop_memory/serve/static/i18n/en.json +0 -0
  92. {loop_memory-0.4.20 → loop_memory-0.4.21}/loop_memory/serve/static/i18n/zh.json +0 -0
  93. {loop_memory-0.4.20 → loop_memory-0.4.21}/loop_memory/serve/static/img/logo-light.svg +0 -0
  94. {loop_memory-0.4.20 → loop_memory-0.4.21}/loop_memory/serve/static/img/logo-mark.svg +0 -0
  95. {loop_memory-0.4.20 → loop_memory-0.4.21}/loop_memory/serve/static/img/logo.svg +0 -0
  96. {loop_memory-0.4.20 → loop_memory-0.4.21}/loop_memory/serve/static/index.html +0 -0
  97. {loop_memory-0.4.20 → loop_memory-0.4.21}/loop_memory/serve/static/js/App.js +0 -0
  98. {loop_memory-0.4.20 → loop_memory-0.4.21}/loop_memory/serve/static/js/api.js +0 -0
  99. {loop_memory-0.4.20 → loop_memory-0.4.21}/loop_memory/serve/static/js/components/Dashboard.js +0 -0
  100. {loop_memory-0.4.20 → loop_memory-0.4.21}/loop_memory/serve/static/js/components/Diagnostic.js +0 -0
  101. {loop_memory-0.4.20 → loop_memory-0.4.21}/loop_memory/serve/static/js/components/IngestPopover.js +0 -0
  102. {loop_memory-0.4.20 → loop_memory-0.4.21}/loop_memory/serve/static/js/components/KnowledgeGraph.js +0 -0
  103. {loop_memory-0.4.20 → loop_memory-0.4.21}/loop_memory/serve/static/js/components/RingMeter.js +0 -0
  104. {loop_memory-0.4.20 → loop_memory-0.4.21}/loop_memory/serve/static/js/components/RunStrip.js +0 -0
  105. {loop_memory-0.4.20 → loop_memory-0.4.21}/loop_memory/serve/static/js/components/SectionTitle.js +0 -0
  106. {loop_memory-0.4.20 → loop_memory-0.4.21}/loop_memory/serve/static/js/components/Settings.js +0 -0
  107. {loop_memory-0.4.20 → loop_memory-0.4.21}/loop_memory/serve/static/js/components/Sidebar.js +0 -0
  108. {loop_memory-0.4.20 → loop_memory-0.4.21}/loop_memory/serve/static/js/components/Tabs.js +0 -0
  109. {loop_memory-0.4.20 → loop_memory-0.4.21}/loop_memory/serve/static/js/components/Timeline.js +0 -0
  110. {loop_memory-0.4.20 → loop_memory-0.4.21}/loop_memory/serve/static/js/components/Toast.js +0 -0
  111. {loop_memory-0.4.20 → loop_memory-0.4.21}/loop_memory/serve/static/js/components/TopBar.js +0 -0
  112. {loop_memory-0.4.20 → loop_memory-0.4.21}/loop_memory/serve/static/js/components/Wiki.js +0 -0
  113. {loop_memory-0.4.20 → loop_memory-0.4.21}/loop_memory/serve/static/js/components/WikiEditor.js +0 -0
  114. {loop_memory-0.4.20 → loop_memory-0.4.21}/loop_memory/serve/static/js/composables/useI18n.js +0 -0
  115. {loop_memory-0.4.20 → loop_memory-0.4.21}/loop_memory/serve/static/js/lib/sanitize.js +0 -0
  116. {loop_memory-0.4.20 → loop_memory-0.4.21}/loop_memory/serve/static/js/lib/vue.esm-browser.prod.js +0 -0
  117. {loop_memory-0.4.20 → loop_memory-0.4.21}/loop_memory/serve/static/js/main.js +0 -0
  118. {loop_memory-0.4.20 → loop_memory-0.4.21}/loop_memory/serve/static/js/store.js +0 -0
  119. {loop_memory-0.4.20 → loop_memory-0.4.21}/loop_memory/serve/watcher.py +0 -0
  120. {loop_memory-0.4.20 → loop_memory-0.4.21}/loop_memory/storage/__init__.py +0 -0
  121. {loop_memory-0.4.20 → loop_memory-0.4.21}/loop_memory/storage/lexical.py +0 -0
  122. {loop_memory-0.4.20 → loop_memory-0.4.21}/loop_memory/storage/retrieval.py +0 -0
  123. {loop_memory-0.4.20 → loop_memory-0.4.21}/loop_memory/storage/snapshot.py +0 -0
  124. {loop_memory-0.4.20 → loop_memory-0.4.21}/loop_memory/storage/sqlite_store.py +0 -0
  125. {loop_memory-0.4.20 → loop_memory-0.4.21}/loop_memory/wiki/__init__.py +0 -0
  126. {loop_memory-0.4.20 → loop_memory-0.4.21}/loop_memory/wiki/backfill.py +0 -0
  127. {loop_memory-0.4.20 → loop_memory-0.4.21}/loop_memory/wiki/classifier.py +0 -0
  128. {loop_memory-0.4.20 → loop_memory-0.4.21}/loop_memory/wiki/prompts.py +0 -0
  129. {loop_memory-0.4.20 → loop_memory-0.4.21}/loop_memory/wiki/scope.py +0 -0
  130. {loop_memory-0.4.20 → loop_memory-0.4.21}/loop_memory.egg-info/dependency_links.txt +0 -0
  131. {loop_memory-0.4.20 → loop_memory-0.4.21}/loop_memory.egg-info/entry_points.txt +0 -0
  132. {loop_memory-0.4.20 → loop_memory-0.4.21}/loop_memory.egg-info/requires.txt +0 -0
  133. {loop_memory-0.4.20 → loop_memory-0.4.21}/loop_memory.egg-info/top_level.txt +0 -0
  134. {loop_memory-0.4.20 → loop_memory-0.4.21}/setup.cfg +0 -0
  135. {loop_memory-0.4.20 → loop_memory-0.4.21}/tests/test_admin_ingest_route.py +0 -0
  136. {loop_memory-0.4.20 → loop_memory-0.4.21}/tests/test_agent_memory_api.py +0 -0
  137. {loop_memory-0.4.20 → loop_memory-0.4.21}/tests/test_agents_init.py +0 -0
  138. {loop_memory-0.4.20 → loop_memory-0.4.21}/tests/test_audit_triggers_2026_09_27.py +0 -0
  139. {loop_memory-0.4.20 → loop_memory-0.4.21}/tests/test_auth_token_rotate.py +0 -0
  140. {loop_memory-0.4.20 → loop_memory-0.4.21}/tests/test_bench_2026_11_01.py +0 -0
  141. {loop_memory-0.4.20 → loop_memory-0.4.21}/tests/test_busy_timeout_2026_09_24.py +0 -0
  142. {loop_memory-0.4.20 → loop_memory-0.4.21}/tests/test_cache_cap_2026_09_23.py +0 -0
  143. {loop_memory-0.4.20 → loop_memory-0.4.21}/tests/test_cli_audit_supersede.py +0 -0
  144. {loop_memory-0.4.20 → loop_memory-0.4.21}/tests/test_cli_export_okf.py +0 -0
  145. {loop_memory-0.4.20 → loop_memory-0.4.21}/tests/test_cli_init.py +0 -0
  146. {loop_memory-0.4.20 → loop_memory-0.4.21}/tests/test_cli_memory_stats.py +0 -0
  147. {loop_memory-0.4.20 → loop_memory-0.4.21}/tests/test_cli_recall_as_of.py +0 -0
  148. {loop_memory-0.4.20 → loop_memory-0.4.21}/tests/test_cli_recall_paths.py +0 -0
  149. {loop_memory-0.4.20 → loop_memory-0.4.21}/tests/test_cli_recall_verbose.py +0 -0
  150. {loop_memory-0.4.20 → loop_memory-0.4.21}/tests/test_cli_rules.py +0 -0
  151. {loop_memory-0.4.20 → loop_memory-0.4.21}/tests/test_cli_snapshot.py +0 -0
  152. {loop_memory-0.4.20 → loop_memory-0.4.21}/tests/test_cli_v7.py +0 -0
  153. {loop_memory-0.4.20 → loop_memory-0.4.21}/tests/test_cli_version.py +0 -0
  154. {loop_memory-0.4.20 → loop_memory-0.4.21}/tests/test_content_hash_dedup_2026_10_18.py +0 -0
  155. {loop_memory-0.4.20 → loop_memory-0.4.21}/tests/test_contradictions.py +0 -0
  156. {loop_memory-0.4.20 → loop_memory-0.4.21}/tests/test_decorators_2026_10_18.py +0 -0
  157. {loop_memory-0.4.20 → loop_memory-0.4.21}/tests/test_dream_2026_10_25.py +0 -0
  158. {loop_memory-0.4.20 → loop_memory-0.4.21}/tests/test_dream_cli_http_2026_10_25.py +0 -0
  159. {loop_memory-0.4.20 → loop_memory-0.4.21}/tests/test_evolution.py +0 -0
  160. {loop_memory-0.4.20 → loop_memory-0.4.21}/tests/test_evolution_quality.py +0 -0
  161. {loop_memory-0.4.20 → loop_memory-0.4.21}/tests/test_expanduser.py +0 -0
  162. {loop_memory-0.4.20 → loop_memory-0.4.21}/tests/test_export_ask.py +0 -0
  163. {loop_memory-0.4.20 → loop_memory-0.4.21}/tests/test_export_okf.py +0 -0
  164. {loop_memory-0.4.20 → loop_memory-0.4.21}/tests/test_graph.py +0 -0
  165. {loop_memory-0.4.20 → loop_memory-0.4.21}/tests/test_healthz_readyz_2026_09_24.py +0 -0
  166. {loop_memory-0.4.20 → loop_memory-0.4.21}/tests/test_import_okf_2026_09_27.py +0 -0
  167. {loop_memory-0.4.20 → loop_memory-0.4.21}/tests/test_ingest.py +0 -0
  168. {loop_memory-0.4.20 → loop_memory-0.4.21}/tests/test_latest_only_2026_10_25.py +0 -0
  169. {loop_memory-0.4.20 → loop_memory-0.4.21}/tests/test_lexical_ranking_2026_11_01.py +0 -0
  170. {loop_memory-0.4.20 → loop_memory-0.4.21}/tests/test_llm_consolidator.py +0 -0
  171. {loop_memory-0.4.20 → loop_memory-0.4.21}/tests/test_llm_fingerprint_not_persisted.py +0 -0
  172. {loop_memory-0.4.20 → loop_memory-0.4.21}/tests/test_llm_providers.py +0 -0
  173. {loop_memory-0.4.20 → loop_memory-0.4.21}/tests/test_llm_test_endpoint.py +0 -0
  174. {loop_memory-0.4.20 → loop_memory-0.4.21}/tests/test_loop.py +0 -0
  175. {loop_memory-0.4.20 → loop_memory-0.4.21}/tests/test_mcp.py +0 -0
  176. {loop_memory-0.4.20 → loop_memory-0.4.21}/tests/test_memories_pagination.py +0 -0
  177. {loop_memory-0.4.20 → loop_memory-0.4.21}/tests/test_memory_stats.py +0 -0
  178. {loop_memory-0.4.20 → loop_memory-0.4.21}/tests/test_openclaw_loader.py +0 -0
  179. {loop_memory-0.4.20 → loop_memory-0.4.21}/tests/test_package_version.py +0 -0
  180. {loop_memory-0.4.20 → loop_memory-0.4.21}/tests/test_packaging_2026_11_01.py +0 -0
  181. {loop_memory-0.4.20 → loop_memory-0.4.21}/tests/test_path_safety_2026_09_23.py +0 -0
  182. {loop_memory-0.4.20 → loop_memory-0.4.21}/tests/test_recall_as_of.py +0 -0
  183. {loop_memory-0.4.20 → loop_memory-0.4.21}/tests/test_recall_budget_2026_10_05.py +0 -0
  184. {loop_memory-0.4.20 → loop_memory-0.4.21}/tests/test_recall_paths.py +0 -0
  185. {loop_memory-0.4.20 → loop_memory-0.4.21}/tests/test_recall_provenance.py +0 -0
  186. {loop_memory-0.4.20 → loop_memory-0.4.21}/tests/test_recall_validity_http_2026_10_18.py +0 -0
  187. {loop_memory-0.4.20 → loop_memory-0.4.21}/tests/test_reflection.py +0 -0
  188. {loop_memory-0.4.20 → loop_memory-0.4.21}/tests/test_repo_framing.py +0 -0
  189. {loop_memory-0.4.20 → loop_memory-0.4.21}/tests/test_retrieval_channels_2026_10_18.py +0 -0
  190. {loop_memory-0.4.20 → loop_memory-0.4.21}/tests/test_routes_audit_2026_09_13.py +0 -0
  191. {loop_memory-0.4.20 → loop_memory-0.4.21}/tests/test_routes_audit_2026_09_20.py +0 -0
  192. {loop_memory-0.4.20 → loop_memory-0.4.21}/tests/test_score_api.py +0 -0
  193. {loop_memory-0.4.20 → loop_memory-0.4.21}/tests/test_scoring_v2.py +0 -0
  194. {loop_memory-0.4.20 → loop_memory-0.4.21}/tests/test_secrets.py +0 -0
  195. {loop_memory-0.4.20 → loop_memory-0.4.21}/tests/test_serve_app.py +0 -0
  196. {loop_memory-0.4.20 → loop_memory-0.4.21}/tests/test_serve_handlers.py +0 -0
  197. {loop_memory-0.4.20 → loop_memory-0.4.21}/tests/test_session_order.py +0 -0
  198. {loop_memory-0.4.20 → loop_memory-0.4.21}/tests/test_short_query.py +0 -0
  199. {loop_memory-0.4.20 → loop_memory-0.4.21}/tests/test_snapshot.py +0 -0
  200. {loop_memory-0.4.20 → loop_memory-0.4.21}/tests/test_store.py +0 -0
  201. {loop_memory-0.4.20 → loop_memory-0.4.21}/tests/test_subgraph_hops_2026_09_23.py +0 -0
  202. {loop_memory-0.4.20 → loop_memory-0.4.21}/tests/test_subject_key_2026_10_25.py +0 -0
  203. {loop_memory-0.4.20 → loop_memory-0.4.21}/tests/test_summarization.py +0 -0
  204. {loop_memory-0.4.20 → loop_memory-0.4.21}/tests/test_supersession_chain.py +0 -0
  205. {loop_memory-0.4.20 → loop_memory-0.4.21}/tests/test_universal_memory.py +0 -0
  206. {loop_memory-0.4.20 → loop_memory-0.4.21}/tests/test_vector_store.py +0 -0
  207. {loop_memory-0.4.20 → loop_memory-0.4.21}/tests/test_watcher.py +0 -0
  208. {loop_memory-0.4.20 → loop_memory-0.4.21}/tests/test_wiki_classifier.py +0 -0
  209. {loop_memory-0.4.20 → loop_memory-0.4.21}/tests/test_wiki_export_escape.py +0 -0
  210. {loop_memory-0.4.20 → loop_memory-0.4.21}/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.21
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,39 @@ 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.21** — **the remote SDK can finally authenticate, and it refuses to send memory data over a plaintext connection** (audit 2026-11-08, weekly ecosystem sync). The audit for this release (2026-11-08) turned up a real hole rather than a hypothetical one. `MemoryClient.http(base_url=...)` had **no transport guard at all**: point it at `http://mem.example.com` and it opened a plain socket and shipped the user's memory text across the network — and because the HTTP backend had **no way to send the bearer token** (`sqlite_store.py` even carried the comment *"header when `MemoryClient.http(..., token=...)` is given"* for a parameter that did not exist), the only way for a remote client to talk to a token-protected `loop-memory serve` was to turn auth **off** — exactly the configuration in which both the memory text and the token cross the network in cleartext. Two upstream projects closed the same hole in the same week: `rohitg00/agentmemory` v0.9.32 (2026-11-04, Apache-2.0) made *"hooks send nothing to a remote plain `http://` server; use HTTPS for a remote server"* a **breaking change**, and `topoteretes/cognee` v1.6.7 (2026-11-06, Apache-2.0) refuses *"server file paths over HTTP"* by default — both are pattern references only, no code copied. **What shipped:** a new `loop_memory.security.transport` that classifies a base URL and refuses plaintext `http://` to a **non-loopback** host (plaintext to a **loopback** address stays allowed — that traffic never leaves the host, and it is the default deployment), refuses a URL with embedded credentials, and refuses an unsupported scheme; `Authorization: Bearer` support in the SDK and in the shared `http_*_json` helpers, so the documented remote path works against a protected server instead of requiring auth to be turned off; a redirect guard that refuses an `https://` → `http://` downgrade (urllib's stock handler follows it, which would undo the TLS guarantee one hop from the server) and refuses a cross-origin redirect while a token is attached (urllib replays the `Authorization` header onto the redirect request); and the server half — `loop-memory serve` records its bind address and a new `loop-memory doctor` check (`plaintext_exposure`) warns when the API is reachable from the network over plaintext. The rule is about the *destination*, not the scheme: a private tunnel or a VPN'd LAN is a legitimate deployment, so `allow_insecure=True` (or `LOOP_MEMORY_ALLOW_INSECURE=1`) exists — but it is never silent, it warns on every construction. 37 new regression cases; suite at **1041 passed / 4 skipped**. See [`docs/transport-security.md`](./docs/transport-security.md) for the full contract. [Full CHANGELOG →](./CHANGELOG.md#0421---2026-11-08)
71
+
72
+ <details>
73
+ <summary><b>Older releases</b> — 0.4.20 and earlier, one line each (the CHANGELOG has the full detail)</summary>
74
+
75
+ | Version | Date | What changed | Full notes |
76
+ | --- | --- | --- | --- |
77
+ | [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) |
78
+ | [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) |
79
+ | [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 |
80
+ | [0.4.17](./CHANGELOG.md#0417---2026-10-25) | 2026-10-25 | A dry-run dream pass no longer writes to the database |
81
+ | [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 |
82
+ | [0.4.15](./CHANGELOG.md#0415---2026-10-18) | 2026-10-18 | Content-hash dedup, bi-temporal validity window, evidence chain |
83
+ | [0.4.14](./CHANGELOG.md#0414---2026-10-12) | 2026-10-12 | `loop-memory doctor` — a deep store-health check |
84
+ | [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 |
85
+ | [0.4.12](./CHANGELOG.md#0412---2026-09-27) | 2026-09-27 | OKF bundle import and DB-level audit enforcement (12 SQLite triggers) |
86
+ | [0.4.11](./CHANGELOG.md#0411---2026-09-24) | 2026-09-24 | Operational probes (`/api/healthz`, `/api/readyz`) and the SQLite write-contention fix |
87
+ | [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=)` |
88
+ | [0.4.9](./CHANGELOG.md#049---2026-09-20) | 2026-09-20 | OKF v0.2 export and bi-temporal `as_of` recall |
89
+ | [0.4.8](./CHANGELOG.md#048---2026-09-13) | 2026-09-13 | L0 outline recall and per-agent identity bootstrap |
90
+ | [0.4.7](./CHANGELOG.md#047---2026-09-06) | 2026-09-06 | Per-memory lifetime stats and a portable SQLite snapshot |
91
+ | [0.4.6](./CHANGELOG.md#046---2026-08-30) | 2026-08-30 | The memory supersession chain (no hard deletes) |
92
+ | [0.4.5](./CHANGELOG.md#045---2026-08-23) | 2026-08-23 | `loop_memory.__version__` was stale at `"0.2.0"` |
93
+ | [0.4.4](./CHANGELOG.md#044---2026-08-23) | 2026-08-23 | `loop-memory rules --agent <name> --write` |
94
+ | [0.4.3](./CHANGELOG.md#043---2026-08-16) | 2026-08-16 | The cognitive sweep N+1 fix |
95
+ | [0.4.2](./CHANGELOG.md#042---2026-08-09) | 2026-08-09 | README refresh |
96
+ | [0.4.1](./CHANGELOG.md#041---2026-08-09) | 2026-08-09 | CLI dispatch hardened against common `--help` crashes |
97
+ | [0.4.0](./CHANGELOG.md#040---2026-08-09) | 2026-08-09 | OpenAI base URL env-var fallback |
98
+ | [0.3.0](./CHANGELOG.md#030--2026-07-11) | 2026-07-11 | The run progress strip is back |
99
+ | [0.2.0](./CHANGELOG.md#020--2026-07-10) | 2026-07-10 | The SQLite-backed `MemoryStore` |
100
+ | [0.1.0](./CHANGELOG.md#010--2026-07-10) | 2026-07-10 | The dashboard tab empty-data regression |
101
+
102
+ </details>
146
103
 
147
104
  ---
148
105
 
@@ -310,6 +267,7 @@ Hermes, clawx) can read and write, we built this for you.
310
267
  | [docs/api.md](docs/api.md) | HTTP API reference — every route, request body, and response shape the UI consumes |
311
268
  | [docs/agent-memory-api.md](docs/agent-memory-api.md) | Stable four-verb SDK / HTTP / MCP contract for any Agent |
312
269
  | [docs/universal-agent-memory.md](docs/universal-agent-memory.md) | v7 graph memory, cognitive sleep, portable bundles, namespaces, and MCP/CLI extensions |
270
+ | [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) |
313
271
  | [docs/providers.md](docs/providers.md) | LLM provider reference — built-in providers, defaults, base URLs, and how to add a new one |
314
272
  | [docs/auto-capture.md](docs/auto-capture.md) | Hooking Codex / Claude / Hermes / OpenClaw watchers (filesystem, launchd, systemd, cron) |
315
273
  | [CONTRIBUTING.md](CONTRIBUTING.md) | Local dev loop, pytest, where secrets live, how to add a provider/source |
@@ -663,7 +621,7 @@ loop_memory/
663
621
  backends/ # embedding (hashing / sentence-transformers) + vector store (memory / chroma)
664
622
  storage/ # SQLite-backed MemoryStore + migrations
665
623
  privacy/ # <private> stripping + regex redaction
666
- security/ # Keychain / 0600-file secrets wrapper
624
+ security/ # Keychain / 0600-file secrets wrapper + the transport guard
667
625
  mcp/ # stdio JSON-RPC MCP server
668
626
  serve/ # FastAPI app, watcher, web UI (Timeline / Dashboard / Wiki / Graph)
669
627
  export/ # markdown + v7 portable bundle export/import
@@ -673,16 +631,18 @@ loop_memory/
673
631
  memory/types.py # MemoryItem + 4 tiers
674
632
  examples/demo.py # runnable, zero-API-key demo
675
633
  py.typed
676
- tests/ # 475 unit tests across memory / SDK / serve / CLI / scripts
634
+ tests/ # 1041 unit tests across memory / SDK / serve / CLI / scripts
677
635
  docs/
678
636
  auto-capture.md # launchd / systemd / cron recipes
679
637
  architecture.md # layered view + 5-stage evolution pipeline
680
638
  api.md # HTTP API reference
639
+ transport-security.md # how a remote client may talk to `serve` (0.4.21)
681
640
  agent-memory-api.md # four-verb SDK / HTTP / MCP contract
682
641
  universal-agent-memory.md # v7 graph memory + cognitive sleep + bundles
683
642
  providers.md # LLM provider reference
684
643
  settings.md # settings table + secrets file
685
644
  weekly-research-automation.md # how the project auto-evolves from upstream research
645
+ research/ # the weekly ecosystem notes each release is based on
686
646
  ```
687
647
 
688
648
  ---
@@ -712,6 +672,34 @@ The web UI stores it in `localStorage` under `loop_auth_token` and attaches
712
672
  it automatically; non-browser clients (curl, MCP, SDK) must opt in by
713
673
  passing it explicitly.
714
674
 
675
+ **Remote clients (SDK):** the HTTP SDK can send the token for you, and it
676
+ refuses to send it — or any memory text — over an unencrypted connection
677
+ to a remote host:
678
+
679
+ ```python
680
+ client = MemoryClient.http(
681
+ "https://memory.example.com",
682
+ token=open("token.txt").read().strip(), # Authorization: Bearer …
683
+ )
684
+ client.remember("deploys go out on Tuesdays")
685
+ ```
686
+
687
+ Plain `http://` is still fine for `127.0.0.1` / `localhost` / `::1` (the
688
+ traffic never leaves the machine — that is the default deployment), but
689
+ plain `http://` to **any other host is refused**: the memory text and the
690
+ bearer token would cross the network in cleartext. A private tunnel or a
691
+ VPN'd LAN is a legitimate deployment, so `allow_insecure=True` (or
692
+ `LOOP_MEMORY_ALLOW_INSECURE=1`) exists — but it is never silent, it
693
+ warns on every construction. Redirects that downgrade `https://` →
694
+ `http://`, and cross-origin redirects that would replay the token, are
695
+ refused too. The full contract is in
696
+ [docs/transport-security.md](docs/transport-security.md).
697
+
698
+ **Server side:** `loop-memory serve --host <non-loopback>` prints a
699
+ warning and records the bind address, so `loop-memory doctor` reports it
700
+ as a `plaintext_exposure` warning — the API is reachable from the network
701
+ over plaintext.
702
+
715
703
  **When to set a token:**
716
704
 
717
705
  - ✅ Always, even on loopback. The default of "no token" exists only as a
@@ -728,6 +716,12 @@ passing it explicitly.
728
716
 
729
717
  - `install-hooks` runs Python that touches `~/.*` — it's gated by the
730
718
  bearer token like every other `POST /api/admin/*` route.
719
+ - A remote client cannot leak memory text to the network in cleartext:
720
+ the SDK refuses a plaintext `http://` base URL that is not loopback,
721
+ refuses a URL with embedded credentials, and refuses redirects that
722
+ downgrade the scheme or would replay the `Authorization` header
723
+ (audit 2026-11-08, 0.4.21 — see
724
+ [docs/transport-security.md](docs/transport-security.md)).
731
725
  - The `<private>...</private>` span stripping in the privacy layer keeps
732
726
  user-marked secrets out of long-term storage; the regex redaction layer
733
727
  then catches API keys / tokens / private keys / JWTs / generic
@@ -22,82 +22,39 @@
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.21** — **the remote SDK can finally authenticate, and it refuses to send memory data over a plaintext connection** (audit 2026-11-08, weekly ecosystem sync). The audit for this release (2026-11-08) turned up a real hole rather than a hypothetical one. `MemoryClient.http(base_url=...)` had **no transport guard at all**: point it at `http://mem.example.com` and it opened a plain socket and shipped the user's memory text across the network — and because the HTTP backend had **no way to send the bearer token** (`sqlite_store.py` even carried the comment *"header when `MemoryClient.http(..., token=...)` is given"* for a parameter that did not exist), the only way for a remote client to talk to a token-protected `loop-memory serve` was to turn auth **off** — exactly the configuration in which both the memory text and the token cross the network in cleartext. Two upstream projects closed the same hole in the same week: `rohitg00/agentmemory` v0.9.32 (2026-11-04, Apache-2.0) made *"hooks send nothing to a remote plain `http://` server; use HTTPS for a remote server"* a **breaking change**, and `topoteretes/cognee` v1.6.7 (2026-11-06, Apache-2.0) refuses *"server file paths over HTTP"* by default — both are pattern references only, no code copied. **What shipped:** a new `loop_memory.security.transport` that classifies a base URL and refuses plaintext `http://` to a **non-loopback** host (plaintext to a **loopback** address stays allowed — that traffic never leaves the host, and it is the default deployment), refuses a URL with embedded credentials, and refuses an unsupported scheme; `Authorization: Bearer` support in the SDK and in the shared `http_*_json` helpers, so the documented remote path works against a protected server instead of requiring auth to be turned off; a redirect guard that refuses an `https://` → `http://` downgrade (urllib's stock handler follows it, which would undo the TLS guarantee one hop from the server) and refuses a cross-origin redirect while a token is attached (urllib replays the `Authorization` header onto the redirect request); and the server half — `loop-memory serve` records its bind address and a new `loop-memory doctor` check (`plaintext_exposure`) warns when the API is reachable from the network over plaintext. The rule is about the *destination*, not the scheme: a private tunnel or a VPN'd LAN is a legitimate deployment, so `allow_insecure=True` (or `LOOP_MEMORY_ALLOW_INSECURE=1`) exists — but it is never silent, it warns on every construction. 37 new regression cases; suite at **1041 passed / 4 skipped**. See [`docs/transport-security.md`](./docs/transport-security.md) for the full contract. [Full CHANGELOG →](./CHANGELOG.md#0421---2026-11-08)
26
+
27
+ <details>
28
+ <summary><b>Older releases</b> — 0.4.20 and earlier, one line each (the CHANGELOG has the full detail)</summary>
29
+
30
+ | Version | Date | What changed | Full notes |
31
+ | --- | --- | --- | --- |
32
+ | [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) |
33
+ | [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) |
34
+ | [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 |
35
+ | [0.4.17](./CHANGELOG.md#0417---2026-10-25) | 2026-10-25 | A dry-run dream pass no longer writes to the database |
36
+ | [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 |
37
+ | [0.4.15](./CHANGELOG.md#0415---2026-10-18) | 2026-10-18 | Content-hash dedup, bi-temporal validity window, evidence chain |
38
+ | [0.4.14](./CHANGELOG.md#0414---2026-10-12) | 2026-10-12 | `loop-memory doctor` — a deep store-health check |
39
+ | [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 |
40
+ | [0.4.12](./CHANGELOG.md#0412---2026-09-27) | 2026-09-27 | OKF bundle import and DB-level audit enforcement (12 SQLite triggers) |
41
+ | [0.4.11](./CHANGELOG.md#0411---2026-09-24) | 2026-09-24 | Operational probes (`/api/healthz`, `/api/readyz`) and the SQLite write-contention fix |
42
+ | [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=)` |
43
+ | [0.4.9](./CHANGELOG.md#049---2026-09-20) | 2026-09-20 | OKF v0.2 export and bi-temporal `as_of` recall |
44
+ | [0.4.8](./CHANGELOG.md#048---2026-09-13) | 2026-09-13 | L0 outline recall and per-agent identity bootstrap |
45
+ | [0.4.7](./CHANGELOG.md#047---2026-09-06) | 2026-09-06 | Per-memory lifetime stats and a portable SQLite snapshot |
46
+ | [0.4.6](./CHANGELOG.md#046---2026-08-30) | 2026-08-30 | The memory supersession chain (no hard deletes) |
47
+ | [0.4.5](./CHANGELOG.md#045---2026-08-23) | 2026-08-23 | `loop_memory.__version__` was stale at `"0.2.0"` |
48
+ | [0.4.4](./CHANGELOG.md#044---2026-08-23) | 2026-08-23 | `loop-memory rules --agent <name> --write` |
49
+ | [0.4.3](./CHANGELOG.md#043---2026-08-16) | 2026-08-16 | The cognitive sweep N+1 fix |
50
+ | [0.4.2](./CHANGELOG.md#042---2026-08-09) | 2026-08-09 | README refresh |
51
+ | [0.4.1](./CHANGELOG.md#041---2026-08-09) | 2026-08-09 | CLI dispatch hardened against common `--help` crashes |
52
+ | [0.4.0](./CHANGELOG.md#040---2026-08-09) | 2026-08-09 | OpenAI base URL env-var fallback |
53
+ | [0.3.0](./CHANGELOG.md#030--2026-07-11) | 2026-07-11 | The run progress strip is back |
54
+ | [0.2.0](./CHANGELOG.md#020--2026-07-10) | 2026-07-10 | The SQLite-backed `MemoryStore` |
55
+ | [0.1.0](./CHANGELOG.md#010--2026-07-10) | 2026-07-10 | The dashboard tab empty-data regression |
56
+
57
+ </details>
101
58
 
102
59
  ---
103
60
 
@@ -265,6 +222,7 @@ Hermes, clawx) can read and write, we built this for you.
265
222
  | [docs/api.md](docs/api.md) | HTTP API reference — every route, request body, and response shape the UI consumes |
266
223
  | [docs/agent-memory-api.md](docs/agent-memory-api.md) | Stable four-verb SDK / HTTP / MCP contract for any Agent |
267
224
  | [docs/universal-agent-memory.md](docs/universal-agent-memory.md) | v7 graph memory, cognitive sleep, portable bundles, namespaces, and MCP/CLI extensions |
225
+ | [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) |
268
226
  | [docs/providers.md](docs/providers.md) | LLM provider reference — built-in providers, defaults, base URLs, and how to add a new one |
269
227
  | [docs/auto-capture.md](docs/auto-capture.md) | Hooking Codex / Claude / Hermes / OpenClaw watchers (filesystem, launchd, systemd, cron) |
270
228
  | [CONTRIBUTING.md](CONTRIBUTING.md) | Local dev loop, pytest, where secrets live, how to add a provider/source |
@@ -618,7 +576,7 @@ loop_memory/
618
576
  backends/ # embedding (hashing / sentence-transformers) + vector store (memory / chroma)
619
577
  storage/ # SQLite-backed MemoryStore + migrations
620
578
  privacy/ # <private> stripping + regex redaction
621
- security/ # Keychain / 0600-file secrets wrapper
579
+ security/ # Keychain / 0600-file secrets wrapper + the transport guard
622
580
  mcp/ # stdio JSON-RPC MCP server
623
581
  serve/ # FastAPI app, watcher, web UI (Timeline / Dashboard / Wiki / Graph)
624
582
  export/ # markdown + v7 portable bundle export/import
@@ -628,16 +586,18 @@ loop_memory/
628
586
  memory/types.py # MemoryItem + 4 tiers
629
587
  examples/demo.py # runnable, zero-API-key demo
630
588
  py.typed
631
- tests/ # 475 unit tests across memory / SDK / serve / CLI / scripts
589
+ tests/ # 1041 unit tests across memory / SDK / serve / CLI / scripts
632
590
  docs/
633
591
  auto-capture.md # launchd / systemd / cron recipes
634
592
  architecture.md # layered view + 5-stage evolution pipeline
635
593
  api.md # HTTP API reference
594
+ transport-security.md # how a remote client may talk to `serve` (0.4.21)
636
595
  agent-memory-api.md # four-verb SDK / HTTP / MCP contract
637
596
  universal-agent-memory.md # v7 graph memory + cognitive sleep + bundles
638
597
  providers.md # LLM provider reference
639
598
  settings.md # settings table + secrets file
640
599
  weekly-research-automation.md # how the project auto-evolves from upstream research
600
+ research/ # the weekly ecosystem notes each release is based on
641
601
  ```
642
602
 
643
603
  ---
@@ -667,6 +627,34 @@ The web UI stores it in `localStorage` under `loop_auth_token` and attaches
667
627
  it automatically; non-browser clients (curl, MCP, SDK) must opt in by
668
628
  passing it explicitly.
669
629
 
630
+ **Remote clients (SDK):** the HTTP SDK can send the token for you, and it
631
+ refuses to send it — or any memory text — over an unencrypted connection
632
+ to a remote host:
633
+
634
+ ```python
635
+ client = MemoryClient.http(
636
+ "https://memory.example.com",
637
+ token=open("token.txt").read().strip(), # Authorization: Bearer …
638
+ )
639
+ client.remember("deploys go out on Tuesdays")
640
+ ```
641
+
642
+ Plain `http://` is still fine for `127.0.0.1` / `localhost` / `::1` (the
643
+ traffic never leaves the machine — that is the default deployment), but
644
+ plain `http://` to **any other host is refused**: the memory text and the
645
+ bearer token would cross the network in cleartext. A private tunnel or a
646
+ VPN'd LAN is a legitimate deployment, so `allow_insecure=True` (or
647
+ `LOOP_MEMORY_ALLOW_INSECURE=1`) exists — but it is never silent, it
648
+ warns on every construction. Redirects that downgrade `https://` →
649
+ `http://`, and cross-origin redirects that would replay the token, are
650
+ refused too. The full contract is in
651
+ [docs/transport-security.md](docs/transport-security.md).
652
+
653
+ **Server side:** `loop-memory serve --host <non-loopback>` prints a
654
+ warning and records the bind address, so `loop-memory doctor` reports it
655
+ as a `plaintext_exposure` warning — the API is reachable from the network
656
+ over plaintext.
657
+
670
658
  **When to set a token:**
671
659
 
672
660
  - ✅ Always, even on loopback. The default of "no token" exists only as a
@@ -683,6 +671,12 @@ passing it explicitly.
683
671
 
684
672
  - `install-hooks` runs Python that touches `~/.*` — it's gated by the
685
673
  bearer token like every other `POST /api/admin/*` route.
674
+ - A remote client cannot leak memory text to the network in cleartext:
675
+ the SDK refuses a plaintext `http://` base URL that is not loopback,
676
+ refuses a URL with embedded credentials, and refuses redirects that
677
+ downgrade the scheme or would replay the `Authorization` header
678
+ (audit 2026-11-08, 0.4.21 — see
679
+ [docs/transport-security.md](docs/transport-security.md)).
686
680
  - The `<private>...</private>` span stripping in the privacy layer keeps
687
681
  user-marked secrets out of long-term storage; the regex redaction layer
688
682
  then catches API keys / tokens / private keys / JWTs / generic
@@ -40,6 +40,25 @@ def run_serve(args) -> int:
40
40
  " Press Ctrl-C now unless you know what you are doing.\n",
41
41
  file=sys.stderr,
42
42
  )
43
+ # Audit 2026-11-08 (loop-memory 0.4.21): record the bind address
44
+ # in the store so `loop-memory doctor` can report the network
45
+ # exposure of a non-loopback bind. The client-side transport
46
+ # guard (loop_memory.security.transport) refuses a plaintext
47
+ # remote URL; this setting is the server-side half of the same
48
+ # story — it tells the operator that the API they just exposed
49
+ # is reachable in cleartext from the network.
50
+ try:
51
+ from ...storage.sqlite_store import MemoryStore
52
+ import time as _time
53
+ MemoryStore(DEFAULT_DB).set_setting(
54
+ "loop_memory_serve_bind",
55
+ {"host": host, "port": int(port), "scheme": "http",
56
+ "recorded_at": _time.time()},
57
+ )
58
+ except Exception as _exc: # pragma: no cover - never block startup
59
+ print(f"[loop-memory] warning: could not record the bind address: {_exc}",
60
+ file=sys.stderr)
61
+
43
62
  from ...serve.app import serve as _serve
44
63
  print(f"[loop-memory] serving UI on http://{host}:{port}")
45
64
  print(f"[loop-memory] db = {DEFAULT_DB}")