loop-memory 0.4.12__tar.gz → 0.4.14__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 (160) hide show
  1. {loop_memory-0.4.12 → loop_memory-0.4.14}/PKG-INFO +5 -1
  2. {loop_memory-0.4.12 → loop_memory-0.4.14}/README.md +4 -0
  3. {loop_memory-0.4.12 → loop_memory-0.4.14}/loop_memory/cli/commands/diag.py +11 -88
  4. loop_memory-0.4.14/loop_memory/cli/commands/doctor.py +144 -0
  5. {loop_memory-0.4.12 → loop_memory-0.4.14}/loop_memory/cli/commands/read.py +44 -7
  6. {loop_memory-0.4.12 → loop_memory-0.4.14}/loop_memory/cli/main.py +3 -3
  7. loop_memory-0.4.14/loop_memory/doctor.py +678 -0
  8. {loop_memory-0.4.12 → loop_memory-0.4.14}/loop_memory/mcp/__init__.py +21 -1
  9. {loop_memory-0.4.12 → loop_memory-0.4.14}/loop_memory/sdk.py +19 -2
  10. {loop_memory-0.4.12 → loop_memory-0.4.14}/loop_memory/serve/routes/system.py +48 -2
  11. {loop_memory-0.4.12 → loop_memory-0.4.14}/loop_memory/storage/sqlite_store.py +314 -2
  12. {loop_memory-0.4.12 → loop_memory-0.4.14}/loop_memory.egg-info/PKG-INFO +5 -1
  13. {loop_memory-0.4.12 → loop_memory-0.4.14}/loop_memory.egg-info/SOURCES.txt +4 -0
  14. {loop_memory-0.4.12 → loop_memory-0.4.14}/pyproject.toml +1 -1
  15. loop_memory-0.4.14/tests/test_doctor_2026_10_12.py +319 -0
  16. loop_memory-0.4.14/tests/test_recall_budget_2026_10_05.py +407 -0
  17. {loop_memory-0.4.12 → loop_memory-0.4.14}/LICENSE +0 -0
  18. {loop_memory-0.4.12 → loop_memory-0.4.14}/loop_memory/__init__.py +0 -0
  19. {loop_memory-0.4.12 → loop_memory-0.4.14}/loop_memory/backends/__init__.py +0 -0
  20. {loop_memory-0.4.12 → loop_memory-0.4.14}/loop_memory/backends/embedding.py +0 -0
  21. {loop_memory-0.4.12 → loop_memory-0.4.14}/loop_memory/backends/sentence_embedder.py +0 -0
  22. {loop_memory-0.4.12 → loop_memory-0.4.14}/loop_memory/backends/vector_store.py +0 -0
  23. {loop_memory-0.4.12 → loop_memory-0.4.14}/loop_memory/cli/__init__.py +0 -0
  24. {loop_memory-0.4.12 → loop_memory-0.4.14}/loop_memory/cli/_common.py +0 -0
  25. {loop_memory-0.4.12 → loop_memory-0.4.14}/loop_memory/cli/commands/__init__.py +0 -0
  26. {loop_memory-0.4.12 → loop_memory-0.4.14}/loop_memory/cli/commands/cognitive.py +0 -0
  27. {loop_memory-0.4.12 → loop_memory-0.4.14}/loop_memory/cli/commands/graph.py +0 -0
  28. {loop_memory-0.4.12 → loop_memory-0.4.14}/loop_memory/cli/commands/hooks.py +0 -0
  29. {loop_memory-0.4.12 → loop_memory-0.4.14}/loop_memory/cli/commands/rules.py +0 -0
  30. {loop_memory-0.4.12 → loop_memory-0.4.14}/loop_memory/cli/commands/serve.py +0 -0
  31. {loop_memory-0.4.12 → loop_memory-0.4.14}/loop_memory/cli/commands/write.py +0 -0
  32. {loop_memory-0.4.12 → loop_memory-0.4.14}/loop_memory/engine/__init__.py +0 -0
  33. {loop_memory-0.4.12 → loop_memory-0.4.14}/loop_memory/engine/loop.py +0 -0
  34. {loop_memory-0.4.12 → loop_memory-0.4.14}/loop_memory/engine/reflect.py +0 -0
  35. {loop_memory-0.4.12 → loop_memory-0.4.14}/loop_memory/examples/__init__.py +0 -0
  36. {loop_memory-0.4.12 → loop_memory-0.4.14}/loop_memory/examples/demo.py +0 -0
  37. {loop_memory-0.4.12 → loop_memory-0.4.14}/loop_memory/export/__init__.py +0 -0
  38. {loop_memory-0.4.12 → loop_memory-0.4.14}/loop_memory/export/memory_md.py +0 -0
  39. {loop_memory-0.4.12 → loop_memory-0.4.14}/loop_memory/graph/__init__.py +0 -0
  40. {loop_memory-0.4.12 → loop_memory-0.4.14}/loop_memory/graph/build.py +0 -0
  41. {loop_memory-0.4.12 → loop_memory-0.4.14}/loop_memory/graph/extract.py +0 -0
  42. {loop_memory-0.4.12 → loop_memory-0.4.14}/loop_memory/ingest/__init__.py +0 -0
  43. {loop_memory-0.4.12 → loop_memory-0.4.14}/loop_memory/ingest/loader.py +0 -0
  44. {loop_memory-0.4.12 → loop_memory-0.4.14}/loop_memory/ingest/pipeline.py +0 -0
  45. {loop_memory-0.4.12 → loop_memory-0.4.14}/loop_memory/jobs/__init__.py +0 -0
  46. {loop_memory-0.4.12 → loop_memory-0.4.14}/loop_memory/jobs/cognitive.py +0 -0
  47. {loop_memory-0.4.12 → loop_memory-0.4.14}/loop_memory/jobs/compact.py +0 -0
  48. {loop_memory-0.4.12 → loop_memory-0.4.14}/loop_memory/jobs/consolidate.py +0 -0
  49. {loop_memory-0.4.12 → loop_memory-0.4.14}/loop_memory/jobs/contradiction.py +0 -0
  50. {loop_memory-0.4.12 → loop_memory-0.4.14}/loop_memory/jobs/evolution.py +0 -0
  51. {loop_memory-0.4.12 → loop_memory-0.4.14}/loop_memory/jobs/graph.py +0 -0
  52. {loop_memory-0.4.12 → loop_memory-0.4.14}/loop_memory/jobs/llm_compact_pass.py +0 -0
  53. {loop_memory-0.4.12 → loop_memory-0.4.14}/loop_memory/jobs/llm_consolidate.py +0 -0
  54. {loop_memory-0.4.12 → loop_memory-0.4.14}/loop_memory/jobs/scheduler.py +0 -0
  55. {loop_memory-0.4.12 → loop_memory-0.4.14}/loop_memory/llm/__init__.py +0 -0
  56. {loop_memory-0.4.12 → loop_memory-0.4.14}/loop_memory/llm/base.py +0 -0
  57. {loop_memory-0.4.12 → loop_memory-0.4.14}/loop_memory/llm/openai_adapter.py +0 -0
  58. {loop_memory-0.4.12 → loop_memory-0.4.14}/loop_memory/llm/providers.py +0 -0
  59. {loop_memory-0.4.12 → loop_memory-0.4.14}/loop_memory/memory/__init__.py +0 -0
  60. {loop_memory-0.4.12 → loop_memory-0.4.14}/loop_memory/memory/types.py +0 -0
  61. {loop_memory-0.4.12 → loop_memory-0.4.14}/loop_memory/privacy/__init__.py +0 -0
  62. {loop_memory-0.4.12 → loop_memory-0.4.14}/loop_memory/privacy/private.py +0 -0
  63. {loop_memory-0.4.12 → loop_memory-0.4.14}/loop_memory/privacy/redact.py +0 -0
  64. {loop_memory-0.4.12 → loop_memory-0.4.14}/loop_memory/py.typed +0 -0
  65. {loop_memory-0.4.12 → loop_memory-0.4.14}/loop_memory/sdk_extensions.py +0 -0
  66. {loop_memory-0.4.12 → loop_memory-0.4.14}/loop_memory/security/__init__.py +0 -0
  67. {loop_memory-0.4.12 → loop_memory-0.4.14}/loop_memory/security/secrets.py +0 -0
  68. {loop_memory-0.4.12 → loop_memory-0.4.14}/loop_memory/serve/__init__.py +0 -0
  69. {loop_memory-0.4.12 → loop_memory-0.4.14}/loop_memory/serve/app.py +0 -0
  70. {loop_memory-0.4.12 → loop_memory-0.4.14}/loop_memory/serve/handlers.py +0 -0
  71. {loop_memory-0.4.12 → loop_memory-0.4.14}/loop_memory/serve/routes/_shared.py +0 -0
  72. {loop_memory-0.4.12 → loop_memory-0.4.14}/loop_memory/serve/routes/admin.py +0 -0
  73. {loop_memory-0.4.12 → loop_memory-0.4.14}/loop_memory/serve/routes/cognitive.py +0 -0
  74. {loop_memory-0.4.12 → loop_memory-0.4.14}/loop_memory/serve/routes/export.py +0 -0
  75. {loop_memory-0.4.12 → loop_memory-0.4.14}/loop_memory/serve/routes/graph.py +0 -0
  76. {loop_memory-0.4.12 → loop_memory-0.4.14}/loop_memory/serve/routes/insights.py +0 -0
  77. {loop_memory-0.4.12 → loop_memory-0.4.14}/loop_memory/serve/routes/memories.py +0 -0
  78. {loop_memory-0.4.12 → loop_memory-0.4.14}/loop_memory/serve/routes/sessions.py +0 -0
  79. {loop_memory-0.4.12 → loop_memory-0.4.14}/loop_memory/serve/routes/wiki.py +0 -0
  80. {loop_memory-0.4.12 → loop_memory-0.4.14}/loop_memory/serve/static/__init__.py +0 -0
  81. {loop_memory-0.4.12 → loop_memory-0.4.14}/loop_memory/serve/static/index.html +0 -0
  82. {loop_memory-0.4.12 → loop_memory-0.4.14}/loop_memory/serve/watcher.py +0 -0
  83. {loop_memory-0.4.12 → loop_memory-0.4.14}/loop_memory/storage/__init__.py +0 -0
  84. {loop_memory-0.4.12 → loop_memory-0.4.14}/loop_memory/storage/retrieval.py +0 -0
  85. {loop_memory-0.4.12 → loop_memory-0.4.14}/loop_memory/storage/snapshot.py +0 -0
  86. {loop_memory-0.4.12 → loop_memory-0.4.14}/loop_memory/wiki/__init__.py +0 -0
  87. {loop_memory-0.4.12 → loop_memory-0.4.14}/loop_memory/wiki/backfill.py +0 -0
  88. {loop_memory-0.4.12 → loop_memory-0.4.14}/loop_memory/wiki/classifier.py +0 -0
  89. {loop_memory-0.4.12 → loop_memory-0.4.14}/loop_memory/wiki/prompts.py +0 -0
  90. {loop_memory-0.4.12 → loop_memory-0.4.14}/loop_memory/wiki/scope.py +0 -0
  91. {loop_memory-0.4.12 → loop_memory-0.4.14}/loop_memory.egg-info/dependency_links.txt +0 -0
  92. {loop_memory-0.4.12 → loop_memory-0.4.14}/loop_memory.egg-info/entry_points.txt +0 -0
  93. {loop_memory-0.4.12 → loop_memory-0.4.14}/loop_memory.egg-info/requires.txt +0 -0
  94. {loop_memory-0.4.12 → loop_memory-0.4.14}/loop_memory.egg-info/top_level.txt +0 -0
  95. {loop_memory-0.4.12 → loop_memory-0.4.14}/setup.cfg +0 -0
  96. {loop_memory-0.4.12 → loop_memory-0.4.14}/tests/test_admin_ingest_route.py +0 -0
  97. {loop_memory-0.4.12 → loop_memory-0.4.14}/tests/test_agent_memory_api.py +0 -0
  98. {loop_memory-0.4.12 → loop_memory-0.4.14}/tests/test_agent_memory_sdk.py +0 -0
  99. {loop_memory-0.4.12 → loop_memory-0.4.14}/tests/test_agents_init.py +0 -0
  100. {loop_memory-0.4.12 → loop_memory-0.4.14}/tests/test_audit_triggers_2026_09_27.py +0 -0
  101. {loop_memory-0.4.12 → loop_memory-0.4.14}/tests/test_auth_token_rotate.py +0 -0
  102. {loop_memory-0.4.12 → loop_memory-0.4.14}/tests/test_busy_timeout_2026_09_24.py +0 -0
  103. {loop_memory-0.4.12 → loop_memory-0.4.14}/tests/test_cache_cap_2026_09_23.py +0 -0
  104. {loop_memory-0.4.12 → loop_memory-0.4.14}/tests/test_cli_audit_supersede.py +0 -0
  105. {loop_memory-0.4.12 → loop_memory-0.4.14}/tests/test_cli_export_okf.py +0 -0
  106. {loop_memory-0.4.12 → loop_memory-0.4.14}/tests/test_cli_init.py +0 -0
  107. {loop_memory-0.4.12 → loop_memory-0.4.14}/tests/test_cli_memory_stats.py +0 -0
  108. {loop_memory-0.4.12 → loop_memory-0.4.14}/tests/test_cli_recall_as_of.py +0 -0
  109. {loop_memory-0.4.12 → loop_memory-0.4.14}/tests/test_cli_recall_paths.py +0 -0
  110. {loop_memory-0.4.12 → loop_memory-0.4.14}/tests/test_cli_recall_verbose.py +0 -0
  111. {loop_memory-0.4.12 → loop_memory-0.4.14}/tests/test_cli_rules.py +0 -0
  112. {loop_memory-0.4.12 → loop_memory-0.4.14}/tests/test_cli_snapshot.py +0 -0
  113. {loop_memory-0.4.12 → loop_memory-0.4.14}/tests/test_cli_v7.py +0 -0
  114. {loop_memory-0.4.12 → loop_memory-0.4.14}/tests/test_cli_version.py +0 -0
  115. {loop_memory-0.4.12 → loop_memory-0.4.14}/tests/test_contradictions.py +0 -0
  116. {loop_memory-0.4.12 → loop_memory-0.4.14}/tests/test_evolution.py +0 -0
  117. {loop_memory-0.4.12 → loop_memory-0.4.14}/tests/test_evolution_quality.py +0 -0
  118. {loop_memory-0.4.12 → loop_memory-0.4.14}/tests/test_expanduser.py +0 -0
  119. {loop_memory-0.4.12 → loop_memory-0.4.14}/tests/test_export_ask.py +0 -0
  120. {loop_memory-0.4.12 → loop_memory-0.4.14}/tests/test_export_okf.py +0 -0
  121. {loop_memory-0.4.12 → loop_memory-0.4.14}/tests/test_graph.py +0 -0
  122. {loop_memory-0.4.12 → loop_memory-0.4.14}/tests/test_healthz_readyz_2026_09_24.py +0 -0
  123. {loop_memory-0.4.12 → loop_memory-0.4.14}/tests/test_import_okf_2026_09_27.py +0 -0
  124. {loop_memory-0.4.12 → loop_memory-0.4.14}/tests/test_ingest.py +0 -0
  125. {loop_memory-0.4.12 → loop_memory-0.4.14}/tests/test_llm_consolidator.py +0 -0
  126. {loop_memory-0.4.12 → loop_memory-0.4.14}/tests/test_llm_fingerprint_not_persisted.py +0 -0
  127. {loop_memory-0.4.12 → loop_memory-0.4.14}/tests/test_llm_providers.py +0 -0
  128. {loop_memory-0.4.12 → loop_memory-0.4.14}/tests/test_llm_test_endpoint.py +0 -0
  129. {loop_memory-0.4.12 → loop_memory-0.4.14}/tests/test_loop.py +0 -0
  130. {loop_memory-0.4.12 → loop_memory-0.4.14}/tests/test_mcp.py +0 -0
  131. {loop_memory-0.4.12 → loop_memory-0.4.14}/tests/test_memories_pagination.py +0 -0
  132. {loop_memory-0.4.12 → loop_memory-0.4.14}/tests/test_memory_stats.py +0 -0
  133. {loop_memory-0.4.12 → loop_memory-0.4.14}/tests/test_openclaw_loader.py +0 -0
  134. {loop_memory-0.4.12 → loop_memory-0.4.14}/tests/test_package_version.py +0 -0
  135. {loop_memory-0.4.12 → loop_memory-0.4.14}/tests/test_path_safety_2026_09_23.py +0 -0
  136. {loop_memory-0.4.12 → loop_memory-0.4.14}/tests/test_recall_as_of.py +0 -0
  137. {loop_memory-0.4.12 → loop_memory-0.4.14}/tests/test_recall_paths.py +0 -0
  138. {loop_memory-0.4.12 → loop_memory-0.4.14}/tests/test_recall_provenance.py +0 -0
  139. {loop_memory-0.4.12 → loop_memory-0.4.14}/tests/test_reflection.py +0 -0
  140. {loop_memory-0.4.12 → loop_memory-0.4.14}/tests/test_repo_framing.py +0 -0
  141. {loop_memory-0.4.12 → loop_memory-0.4.14}/tests/test_routes_audit_2026_09_13.py +0 -0
  142. {loop_memory-0.4.12 → loop_memory-0.4.14}/tests/test_routes_audit_2026_09_20.py +0 -0
  143. {loop_memory-0.4.12 → loop_memory-0.4.14}/tests/test_score_api.py +0 -0
  144. {loop_memory-0.4.12 → loop_memory-0.4.14}/tests/test_scoring_v2.py +0 -0
  145. {loop_memory-0.4.12 → loop_memory-0.4.14}/tests/test_secrets.py +0 -0
  146. {loop_memory-0.4.12 → loop_memory-0.4.14}/tests/test_serve_app.py +0 -0
  147. {loop_memory-0.4.12 → loop_memory-0.4.14}/tests/test_serve_handlers.py +0 -0
  148. {loop_memory-0.4.12 → loop_memory-0.4.14}/tests/test_session_order.py +0 -0
  149. {loop_memory-0.4.12 → loop_memory-0.4.14}/tests/test_short_query.py +0 -0
  150. {loop_memory-0.4.12 → loop_memory-0.4.14}/tests/test_snapshot.py +0 -0
  151. {loop_memory-0.4.12 → loop_memory-0.4.14}/tests/test_store.py +0 -0
  152. {loop_memory-0.4.12 → loop_memory-0.4.14}/tests/test_subgraph_hops_2026_09_23.py +0 -0
  153. {loop_memory-0.4.12 → loop_memory-0.4.14}/tests/test_summarization.py +0 -0
  154. {loop_memory-0.4.12 → loop_memory-0.4.14}/tests/test_supersession_chain.py +0 -0
  155. {loop_memory-0.4.12 → loop_memory-0.4.14}/tests/test_universal_memory.py +0 -0
  156. {loop_memory-0.4.12 → loop_memory-0.4.14}/tests/test_vector_store.py +0 -0
  157. {loop_memory-0.4.12 → loop_memory-0.4.14}/tests/test_watcher.py +0 -0
  158. {loop_memory-0.4.12 → loop_memory-0.4.14}/tests/test_wiki_classifier.py +0 -0
  159. {loop_memory-0.4.12 → loop_memory-0.4.14}/tests/test_wiki_export_escape.py +0 -0
  160. {loop_memory-0.4.12 → loop_memory-0.4.14}/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.12
3
+ Version: 0.4.14
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
@@ -66,6 +66,10 @@ Dynamic: license-file
66
66
 
67
67
  ---
68
68
 
69
+ > **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)
70
+
71
+ > **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)
72
+
69
73
  > **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)
70
74
 
71
75
  > **What's new in 0.4.11** — **operational probes** +
@@ -22,6 +22,10 @@
22
22
 
23
23
  ---
24
24
 
25
+ > **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)
26
+
27
+ > **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)
28
+
25
29
  > **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)
26
30
 
27
31
  > **What's new in 0.4.11** — **operational probes** +
@@ -133,95 +133,18 @@ def _detect_watcher() -> bool:
133
133
 
134
134
 
135
135
  def run_doctor(_args) -> int:
136
- """``loop-memory doctor`` — green/red diagnostic screen.
137
-
138
- Tells the user what's installed, what's wired, what's broken, and
139
- gives copy-pasteable fix commands for anything red.
136
+ """`loop-memory doctor` — deep store-health check.
137
+
138
+ Audit 2026-10-12. Delegates to :mod:`loop_memory.doctor` which runs
139
+ a small library of health checks against the live
140
+ `MemoryStore` and renders a human-readable report. Exits
141
+ non-zero if any check is in 'error' state. Source:
142
+ cortexkit/magic-context doctor (MIT, 2026-10-12) +
143
+ PerryLink/dsh-memento dsh-plugin-doctor (Apache-2.0,
144
+ 2026-10-12) — pattern only.
140
145
  """
141
- print("loop-memory doctor\n")
142
- # 1) Installation
143
- cli_path = shutil.which("loop-memory")
144
- print(_dot(cli_path is not None,
145
- f"CLI on PATH: {'yes (' + cli_path + ')' if cli_path else 'no'}"))
146
- # 2) Database
147
- db_path = Path(DEFAULT_DB)
148
- print(_dot(db_path.exists(),
149
- f"database: {db_path}" + ("" if db_path.exists() else " (will be created on first run)")))
150
- # 3) Server
151
- server_running = False
152
- try:
153
- import urllib.request
154
- with urllib.request.urlopen("http://127.0.0.1:7767/api/stats", timeout=1) as r:
155
- server_running = r.status == 200
156
- except Exception:
157
- server_running = False
158
- print(_dot(server_running,
159
- "web UI server: http://127.0.0.1:7767" + (" (running)" if server_running else " (not running — start with `loop-memory serve &`)")))
160
-
161
- # 4) Clients
162
- print()
163
- print("clients:")
164
- clients = _detect_clients()
165
- for name in ("codex", "claude", "hermes", "openclaw"):
166
- info = clients[name]
167
- if name == "openclaw":
168
- print(_dot(info["installed"],
169
- f" openclaw: {'installed' if info['installed'] else 'not installed'}"))
170
- if info["installed"]:
171
- paths = ", ".join(Path(p).name for p in info["watch_paths"]) or "no transcript paths found"
172
- print(f" {paths}")
173
- print(_dot(_detect_watcher(),
174
- " watcher running" if _detect_watcher() else " watcher not running — start with `loop-memory openclaw-setup`"))
175
- else:
176
- cfg = info["config"]
177
- installed = info["installed"]
178
- cfg_str = f"@ {cfg}" if installed else ""
179
- print(_dot(installed,
180
- f" {name}: {'installed' if installed else 'not installed'} {cfg_str}"))
181
- if installed:
182
- print(_dot(info["mcp_configured"],
183
- f" MCP server wired: {'yes' if info['mcp_configured'] else 'no — run `loop-memory install-hooks`'}"))
184
-
185
- # 5) LLM provider
186
- print()
187
- print("LLM provider:")
188
- try:
189
- from ...storage.sqlite_store import MemoryStore
190
- from ...llm.providers import default_config
191
- from ...security import backend_display_name, has_secret
192
- store = MemoryStore(DEFAULT_DB)
193
- cfg = store.get_setting("llm_consolidator", default_config())
194
- provider = cfg.get("provider") or "echo"
195
- print(f" provider: {provider}")
196
- print(f" model: {cfg.get('model', '—')}")
197
- print(f" secret backend: {backend_display_name()}")
198
- if provider != "echo":
199
- from ...security import account_for
200
- account = cfg.get("api_key_account") or account_for(provider)
201
- has = has_secret(account)
202
- print(_dot(has,
203
- f" API key: {'configured' if has else 'MISSING — open the web UI → ⚙ Model → paste your key'}"))
204
- except Exception as e:
205
- print(_dot(False, f" could not read LLM config: {e}"))
206
-
207
- # 6) Data summary
208
- print()
209
- print("data:")
210
- try:
211
- from ...storage.sqlite_store import MemoryStore
212
- store = MemoryStore(DEFAULT_DB)
213
- n_mem = store.count_memories()
214
- n_sess = store.count_sessions()
215
- n_wiki = store.count_wiki_pages()
216
- n_ent = store.count_entities()
217
- print(f" {n_mem} memories · {n_sess} sessions · {n_wiki} wiki pages · {n_ent} entities")
218
- except Exception as e:
219
- print(f" could not read counters: {e}")
220
-
221
- print()
222
- print(_DIM + "Tip: run `loop-memory status` for a one-screen summary, "
223
- "or `loop-memory install-hooks` to auto-configure your CLIs." + _RESET)
224
- return 0
146
+ from . import doctor as _doctor_cli
147
+ return _doctor_cli.run_doctor(list(_args))
225
148
 
226
149
 
227
150
  def run_status(_args) -> int:
@@ -0,0 +1,144 @@
1
+ """``loop-memory doctor`` subcommand.
2
+
3
+ Audit 2026-10-12. Wraps :mod:`loop_memory.doctor` and renders the
4
+ result for a human or as JSON. See ``loop_memory/doctor.py`` for
5
+ the check library and the source attribution.
6
+
7
+ Usage examples::
8
+
9
+ loop-memory doctor # human-readable report
10
+ loop-memory doctor --json # machine-readable report
11
+ loop-memory doctor --verbose # also show INFO-level checks
12
+ loop-memory doctor --fix # auto-repair auto_fixable checks
13
+ loop-memory doctor --db /path/to/store.db # run against a specific DB
14
+ """
15
+ from __future__ import annotations
16
+
17
+ import argparse
18
+ import sys
19
+ from pathlib import Path
20
+ from typing import Any
21
+
22
+ from .._common import die
23
+ from ... import doctor as _doctor
24
+ from ...storage.sqlite_store import MemoryStore
25
+
26
+
27
+ def _build_parser() -> argparse.ArgumentParser:
28
+ p = argparse.ArgumentParser(
29
+ prog="loop-memory doctor",
30
+ description=(
31
+ "Run a small library of health checks against the loop-memory "
32
+ "store. Reports ok / info / warn / error per check and exits "
33
+ "non-zero if any check is in 'error' state. Source: cortexkit/"
34
+ "magic-context doctor (MIT, 2026-10-12) + PerryLink/dsh-memento "
35
+ "dsh-plugin-doctor (Apache-2.0, 2026-10-12) — pattern only."
36
+ ),
37
+ )
38
+ p.add_argument(
39
+ "--db",
40
+ default=None,
41
+ help="path to the SQLite file (default: $LOOP_MEMORY_DB or ~/.loop_memory/loop_memory.db)",
42
+ )
43
+ p.add_argument(
44
+ "--json",
45
+ action="store_true",
46
+ help="emit the result as JSON for scripting",
47
+ )
48
+ p.add_argument(
49
+ "--verbose",
50
+ action="store_true",
51
+ help="also show INFO-level checks in human output",
52
+ )
53
+ p.add_argument(
54
+ "--fix",
55
+ action="store_true",
56
+ help="auto-repair any auto_fixable check that fails (idempotent; safe to re-run)",
57
+ )
58
+ p.add_argument(
59
+ "--list",
60
+ action="store_true",
61
+ help="list every check the doctor would run and exit (no DB access)",
62
+ )
63
+ return p
64
+
65
+
66
+ def _resolve_db_path(arg: str | None) -> Path:
67
+ """Pick the store path: ``--db`` wins, else ``$LOOP_MEMORY_DB``,
68
+ else the default ``~/.loop_memory/loop_memory.db``."""
69
+ if arg:
70
+ return Path(arg).expanduser()
71
+ import os
72
+ env = os.environ.get("LOOP_MEMORY_DB")
73
+ if env:
74
+ return Path(env).expanduser()
75
+ from .._common import default_db_path
76
+ return Path(default_db_path())
77
+
78
+
79
+ def run_doctor(args: list[str]) -> int:
80
+ parser = _build_parser()
81
+ parsed = parser.parse_args(args)
82
+
83
+ if parsed.list:
84
+ for fn in _doctor.CHECKS:
85
+ print(fn.__name__)
86
+ return 0
87
+
88
+ db_path = _resolve_db_path(parsed.db)
89
+ if not db_path.exists():
90
+ msg = (
91
+ f"loop-memory doctor: store file not found at {db_path}. "
92
+ "Run `loop-memory init` first, or pass --db to point at an existing store."
93
+ )
94
+ if parsed.json:
95
+ print(_doctor.render_json([
96
+ _doctor.DoctorCheck(
97
+ name="db_exists",
98
+ status=_doctor.STATUS_ERROR,
99
+ message=msg,
100
+ fix_hint="run `loop-memory init` to create the store",
101
+ ),
102
+ ]))
103
+ return 1
104
+ return die(msg)
105
+
106
+ # ``MemoryStore(<missing_path>)`` creates a fresh DB on open.
107
+ # We want doctor to FAIL when the path is missing rather than
108
+ # silently materialise an empty store, so the missing-path
109
+ # check above is the contract.
110
+ try:
111
+ store = MemoryStore(str(db_path))
112
+ except Exception as e:
113
+ msg = f"loop-memory doctor: could not open store at {db_path}: {e}"
114
+ if parsed.json:
115
+ print(_doctor.render_json([
116
+ _doctor.DoctorCheck(
117
+ name="db_exists",
118
+ status=_doctor.STATUS_ERROR,
119
+ message=msg,
120
+ ),
121
+ ]))
122
+ return 1
123
+ return die(msg)
124
+
125
+ checks = _doctor.run_checks(store)
126
+
127
+ if parsed.fix:
128
+ # Apply auto-fixes in a single pass. We re-run the checks
129
+ # so the user sees the post-fix state in the same command.
130
+ for c in checks:
131
+ if c.status in (_doctor.STATUS_ERROR, _doctor.STATUS_WARN) and c.auto_fixable:
132
+ post = _doctor.apply_fix(store, c.name)
133
+ # Replace the failing check with the post-fix result
134
+ # so the human / JSON output reflects reality.
135
+ idx = checks.index(c)
136
+ checks[idx] = post
137
+ checks = _doctor.run_checks(store)
138
+
139
+ if parsed.json:
140
+ sys.stdout.write(_doctor.render_json(checks))
141
+ else:
142
+ sys.stdout.write(_doctor.render_human(checks, verbose=parsed.verbose))
143
+
144
+ return _doctor.exit_code(checks)
@@ -51,7 +51,7 @@ def run_stats(_args) -> int:
51
51
 
52
52
 
53
53
  def run_recall(args) -> int:
54
- """Run ``loop-memory recall <query> [--verbose] [--outline] [--as-of T] [--limit N]``.
54
+ """Run ``loop-memory recall <query> [--verbose] [--outline] [--as-of T] [--limit N] [--max-chars N] [--level 0|1|2]``.
55
55
 
56
56
  With ``--verbose`` each memory hit is annotated with the
57
57
  ``why: [...]`` provenance labels produced by ``MemoryStore.recall``
@@ -73,6 +73,8 @@ def run_recall(args) -> int:
73
73
  outline = False
74
74
  limit = 10
75
75
  as_of: float | None = None
76
+ max_chars: int | None = None
77
+ level = 1
76
78
  qargs: list[str] = []
77
79
  i = 0
78
80
  while i < len(args):
@@ -104,6 +106,29 @@ def run_recall(args) -> int:
104
106
  f"--as-of must be float epoch or ISO-8601; got {raw!r}"
105
107
  )
106
108
  i += 2
109
+ elif a == "--max-chars" and i + 1 < len(args):
110
+ # Audit 2026-10-05 (vectorize-io/hindsight v0.10.2 +
111
+ # aiming-lab/SimpleMem ICML'26) — cap the total payload
112
+ # across all hits so an agent loop never has to hand-trim
113
+ # a giant response. ``0`` / negative = no cap.
114
+ try:
115
+ max_chars = int(args[i + 1])
116
+ if max_chars <= 0:
117
+ max_chars = None
118
+ except ValueError:
119
+ return die(f"--max-chars must be an integer, got {args[i + 1]!r}")
120
+ i += 2
121
+ elif a == "--level" and i + 1 < len(args):
122
+ # Audit 2026-10-05 (volcengine/OpenViking tiered-loader
123
+ # pattern) — 0 = L0 abstract ladder rung, 1 = L1 default,
124
+ # 2 = L2 full body.
125
+ try:
126
+ level = int(args[i + 1])
127
+ if level not in (0, 1, 2):
128
+ return die("--level must be 0, 1, or 2")
129
+ except ValueError:
130
+ return die(f"--level must be 0, 1, or 2, got {args[i + 1]!r}")
131
+ i += 2
107
132
  elif a == "--limit" and i + 1 < len(args):
108
133
  try:
109
134
  limit = int(args[i + 1])
@@ -116,18 +141,20 @@ def run_recall(args) -> int:
116
141
  qargs.append(a)
117
142
  i += 1
118
143
  if not qargs:
119
- return die("usage: loop-memory recall <query> [--verbose] [--outline] [--as-of <ISO|epoch>] [--limit N]")
144
+ return die("usage: loop-memory recall <query> [--verbose] [--outline] [--as-of <ISO|epoch>] [--limit N] [--max-chars N] [--level 0|1|2]")
120
145
  store = MemoryStore(default_db_path())
121
146
  query = " ".join(qargs)
122
147
  if as_of is not None:
123
148
  try:
124
- r = store.recall_as_of(query, as_of, limit=limit)
149
+ r = store.recall_as_of(query, as_of, limit=limit,
150
+ max_chars=max_chars, level=level)
125
151
  except ValueError as e:
126
152
  return die(str(e))
127
153
  elif outline:
128
- r = store.recall_paths(query, limit=limit)
154
+ r = store.recall_paths(query, limit=limit, max_chars=max_chars)
129
155
  else:
130
- r = store.recall(query, limit=limit)
156
+ r = store.recall(query, limit=limit,
157
+ max_chars=max_chars, level=level)
131
158
  has = False
132
159
  if r["wiki"]:
133
160
  has = True
@@ -458,6 +485,7 @@ def run_recall_paths(args) -> int:
458
485
  if not args:
459
486
  return die("usage: loop-memory recall-paths <query> [--limit N]")
460
487
  limit = 12
488
+ max_chars: int | None = None
461
489
  qargs: list[str] = []
462
490
  i = 0
463
491
  while i < len(args):
@@ -468,16 +496,25 @@ def run_recall_paths(args) -> int:
468
496
  except ValueError:
469
497
  return die(f"--limit must be an integer, got {args[i + 1]!r}")
470
498
  i += 2
499
+ elif a == "--max-chars" and i + 1 < len(args):
500
+ # Audit 2026-10-05 — cap the abstract chip-stream size.
501
+ try:
502
+ max_chars = int(args[i + 1])
503
+ if max_chars <= 0:
504
+ max_chars = None
505
+ except ValueError:
506
+ return die(f"--max-chars must be an integer, got {args[i + 1]!r}")
507
+ i += 2
471
508
  elif a.startswith("--"):
472
509
  return die(f"unknown flag: {a}")
473
510
  else:
474
511
  qargs.append(a)
475
512
  i += 1
476
513
  if not qargs:
477
- return die("usage: loop-memory recall-paths <query> [--limit N]")
514
+ return die("usage: loop-memory recall-paths <query> [--limit N] [--max-chars N]")
478
515
  store = MemoryStore(default_db_path())
479
516
  query = " ".join(qargs)
480
- r = store.recall_paths(query, limit=limit)
517
+ r = store.recall_paths(query, limit=limit, max_chars=max_chars)
481
518
  has = False
482
519
  if r["memories"]:
483
520
  has = True
@@ -149,7 +149,7 @@ COMMAND_HELP: dict[str, str] = {
149
149
  "consolidate": "loop-memory consolidate # rescore + GC + dedupe.",
150
150
  "consolidate-now": "loop-memory consolidate-now # ask the running server to trigger a pass right now.",
151
151
  "digest": "loop-memory digest [--out PATH] # compact knowledge digest for AGENTS.md.",
152
- "doctor": "loop-memory doctor # diagnose local install (db, hooks, optional deps).",
152
+ "doctor": "loop-memory doctor [--json] [--verbose] [--fix] [--db PATH] # deep store-health check (db integrity + audit triggers + hooks + version + wiki orphans). Auto-repair with --fix (audit 2026-10-12; pattern adopted from cortexkit/magic-context + dsh-memento).",
153
153
  "export": "loop-memory export [--out FILE | <out_dir>] # markdown dump or v7 bundle export.",
154
154
  "export-bundle": "loop-memory export-bundle <out_dir> # explicit v7 bundle alias.",
155
155
  "flush": "loop-memory flush # drop volatile caches; keep persisted data.",
@@ -164,7 +164,7 @@ COMMAND_HELP: dict[str, str] = {
164
164
  "install-hooks": "loop-memory install-hooks # auto-write MCP + SessionStart hooks for known clients.",
165
165
  "mcp": "loop-memory mcp # stdio MCP server (for codex/claude/hermes). Requires ``[serve]`` for HTTP transport.",
166
166
  "openclaw-setup": "loop-memory openclaw-setup # install the openclaw/clawx auto-ingest watcher (launchd on macOS).",
167
- "recall": "loop-memory recall <text> [--as-of <ISO|epoch>] # show top memories (--as-of = bi-temporal recall).",
167
+ "recall": "loop-memory recall <text> [--as-of <ISO|epoch>] [--max-chars N] [--level 0|1|2] # show top memories (--as-of = bi-temporal recall; --max-chars = total payload cap; --level 0 = L0 abstract ladder rung; audit 2026-10-05).",
168
168
  "rescore": "loop-memory rescore [--half-life 30] # recompute memory scores.",
169
169
  "serve": "loop-memory serve [--port 7767] [--no-browser] # start the local web UI. Requires the ``[serve]`` extra (fastapi + uvicorn).",
170
170
  "stats": "loop-memory stats # counters.",
@@ -178,7 +178,7 @@ COMMAND_HELP: dict[str, str] = {
178
178
  "import-okf": "loop-memory import-okf <in_dir> [--scope S] [--dry-run] [--skip-conflicts] # ingest an OKF v0.2 bundle as wiki pages (audit 2026-09-27).",
179
179
  "restore": "loop-memory restore <in.memory.sqlite> # re-hydrate a portable SQLite snapshot.",
180
180
  # Audit 2026-09-13 — L0 outline recall (tigerless-labs/agent-memory v0.3.0).
181
- "recall-paths": "loop-memory recall-paths <query> [--limit N] # cheap L0 outline (id+abstract+score) instead of full text (audit 2026-09-13).",
181
+ "recall-paths": "loop-memory recall-paths <query> [--limit N] [--max-chars N] # cheap L0 outline (id+abstract+score) instead of full text (audit 2026-09-13; --max-chars caps the chip stream, audit 2026-10-05).",
182
182
  # Audit 2026-09-13 — per-agent identity bootstrap (Mem0 CLI init --agent pattern).
183
183
  "init": "loop-memory init --agent <name> [--install-hooks] # register an agent + (optionally) install hooks.",
184
184
  "rules": "loop-memory rules [--agent codex|claude|hermes|openclaw] [--write] [--force]\n Print, or append into the agent's rule file, the three-phase memory discipline (task start / mid-task / wrap-up). Never overwrites user content.",