loop-memory 0.4.13__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.13 → loop_memory-0.4.14}/PKG-INFO +3 -1
  2. {loop_memory-0.4.13 → loop_memory-0.4.14}/README.md +2 -0
  3. {loop_memory-0.4.13 → 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.13 → loop_memory-0.4.14}/loop_memory/cli/main.py +1 -1
  6. loop_memory-0.4.14/loop_memory/doctor.py +678 -0
  7. {loop_memory-0.4.13 → loop_memory-0.4.14}/loop_memory.egg-info/PKG-INFO +3 -1
  8. {loop_memory-0.4.13 → loop_memory-0.4.14}/loop_memory.egg-info/SOURCES.txt +3 -0
  9. {loop_memory-0.4.13 → loop_memory-0.4.14}/pyproject.toml +1 -1
  10. loop_memory-0.4.14/tests/test_doctor_2026_10_12.py +319 -0
  11. {loop_memory-0.4.13 → loop_memory-0.4.14}/LICENSE +0 -0
  12. {loop_memory-0.4.13 → loop_memory-0.4.14}/loop_memory/__init__.py +0 -0
  13. {loop_memory-0.4.13 → loop_memory-0.4.14}/loop_memory/backends/__init__.py +0 -0
  14. {loop_memory-0.4.13 → loop_memory-0.4.14}/loop_memory/backends/embedding.py +0 -0
  15. {loop_memory-0.4.13 → loop_memory-0.4.14}/loop_memory/backends/sentence_embedder.py +0 -0
  16. {loop_memory-0.4.13 → loop_memory-0.4.14}/loop_memory/backends/vector_store.py +0 -0
  17. {loop_memory-0.4.13 → loop_memory-0.4.14}/loop_memory/cli/__init__.py +0 -0
  18. {loop_memory-0.4.13 → loop_memory-0.4.14}/loop_memory/cli/_common.py +0 -0
  19. {loop_memory-0.4.13 → loop_memory-0.4.14}/loop_memory/cli/commands/__init__.py +0 -0
  20. {loop_memory-0.4.13 → loop_memory-0.4.14}/loop_memory/cli/commands/cognitive.py +0 -0
  21. {loop_memory-0.4.13 → loop_memory-0.4.14}/loop_memory/cli/commands/graph.py +0 -0
  22. {loop_memory-0.4.13 → loop_memory-0.4.14}/loop_memory/cli/commands/hooks.py +0 -0
  23. {loop_memory-0.4.13 → loop_memory-0.4.14}/loop_memory/cli/commands/read.py +0 -0
  24. {loop_memory-0.4.13 → loop_memory-0.4.14}/loop_memory/cli/commands/rules.py +0 -0
  25. {loop_memory-0.4.13 → loop_memory-0.4.14}/loop_memory/cli/commands/serve.py +0 -0
  26. {loop_memory-0.4.13 → loop_memory-0.4.14}/loop_memory/cli/commands/write.py +0 -0
  27. {loop_memory-0.4.13 → loop_memory-0.4.14}/loop_memory/engine/__init__.py +0 -0
  28. {loop_memory-0.4.13 → loop_memory-0.4.14}/loop_memory/engine/loop.py +0 -0
  29. {loop_memory-0.4.13 → loop_memory-0.4.14}/loop_memory/engine/reflect.py +0 -0
  30. {loop_memory-0.4.13 → loop_memory-0.4.14}/loop_memory/examples/__init__.py +0 -0
  31. {loop_memory-0.4.13 → loop_memory-0.4.14}/loop_memory/examples/demo.py +0 -0
  32. {loop_memory-0.4.13 → loop_memory-0.4.14}/loop_memory/export/__init__.py +0 -0
  33. {loop_memory-0.4.13 → loop_memory-0.4.14}/loop_memory/export/memory_md.py +0 -0
  34. {loop_memory-0.4.13 → loop_memory-0.4.14}/loop_memory/graph/__init__.py +0 -0
  35. {loop_memory-0.4.13 → loop_memory-0.4.14}/loop_memory/graph/build.py +0 -0
  36. {loop_memory-0.4.13 → loop_memory-0.4.14}/loop_memory/graph/extract.py +0 -0
  37. {loop_memory-0.4.13 → loop_memory-0.4.14}/loop_memory/ingest/__init__.py +0 -0
  38. {loop_memory-0.4.13 → loop_memory-0.4.14}/loop_memory/ingest/loader.py +0 -0
  39. {loop_memory-0.4.13 → loop_memory-0.4.14}/loop_memory/ingest/pipeline.py +0 -0
  40. {loop_memory-0.4.13 → loop_memory-0.4.14}/loop_memory/jobs/__init__.py +0 -0
  41. {loop_memory-0.4.13 → loop_memory-0.4.14}/loop_memory/jobs/cognitive.py +0 -0
  42. {loop_memory-0.4.13 → loop_memory-0.4.14}/loop_memory/jobs/compact.py +0 -0
  43. {loop_memory-0.4.13 → loop_memory-0.4.14}/loop_memory/jobs/consolidate.py +0 -0
  44. {loop_memory-0.4.13 → loop_memory-0.4.14}/loop_memory/jobs/contradiction.py +0 -0
  45. {loop_memory-0.4.13 → loop_memory-0.4.14}/loop_memory/jobs/evolution.py +0 -0
  46. {loop_memory-0.4.13 → loop_memory-0.4.14}/loop_memory/jobs/graph.py +0 -0
  47. {loop_memory-0.4.13 → loop_memory-0.4.14}/loop_memory/jobs/llm_compact_pass.py +0 -0
  48. {loop_memory-0.4.13 → loop_memory-0.4.14}/loop_memory/jobs/llm_consolidate.py +0 -0
  49. {loop_memory-0.4.13 → loop_memory-0.4.14}/loop_memory/jobs/scheduler.py +0 -0
  50. {loop_memory-0.4.13 → loop_memory-0.4.14}/loop_memory/llm/__init__.py +0 -0
  51. {loop_memory-0.4.13 → loop_memory-0.4.14}/loop_memory/llm/base.py +0 -0
  52. {loop_memory-0.4.13 → loop_memory-0.4.14}/loop_memory/llm/openai_adapter.py +0 -0
  53. {loop_memory-0.4.13 → loop_memory-0.4.14}/loop_memory/llm/providers.py +0 -0
  54. {loop_memory-0.4.13 → loop_memory-0.4.14}/loop_memory/mcp/__init__.py +0 -0
  55. {loop_memory-0.4.13 → loop_memory-0.4.14}/loop_memory/memory/__init__.py +0 -0
  56. {loop_memory-0.4.13 → loop_memory-0.4.14}/loop_memory/memory/types.py +0 -0
  57. {loop_memory-0.4.13 → loop_memory-0.4.14}/loop_memory/privacy/__init__.py +0 -0
  58. {loop_memory-0.4.13 → loop_memory-0.4.14}/loop_memory/privacy/private.py +0 -0
  59. {loop_memory-0.4.13 → loop_memory-0.4.14}/loop_memory/privacy/redact.py +0 -0
  60. {loop_memory-0.4.13 → loop_memory-0.4.14}/loop_memory/py.typed +0 -0
  61. {loop_memory-0.4.13 → loop_memory-0.4.14}/loop_memory/sdk.py +0 -0
  62. {loop_memory-0.4.13 → loop_memory-0.4.14}/loop_memory/sdk_extensions.py +0 -0
  63. {loop_memory-0.4.13 → loop_memory-0.4.14}/loop_memory/security/__init__.py +0 -0
  64. {loop_memory-0.4.13 → loop_memory-0.4.14}/loop_memory/security/secrets.py +0 -0
  65. {loop_memory-0.4.13 → loop_memory-0.4.14}/loop_memory/serve/__init__.py +0 -0
  66. {loop_memory-0.4.13 → loop_memory-0.4.14}/loop_memory/serve/app.py +0 -0
  67. {loop_memory-0.4.13 → loop_memory-0.4.14}/loop_memory/serve/handlers.py +0 -0
  68. {loop_memory-0.4.13 → loop_memory-0.4.14}/loop_memory/serve/routes/_shared.py +0 -0
  69. {loop_memory-0.4.13 → loop_memory-0.4.14}/loop_memory/serve/routes/admin.py +0 -0
  70. {loop_memory-0.4.13 → loop_memory-0.4.14}/loop_memory/serve/routes/cognitive.py +0 -0
  71. {loop_memory-0.4.13 → loop_memory-0.4.14}/loop_memory/serve/routes/export.py +0 -0
  72. {loop_memory-0.4.13 → loop_memory-0.4.14}/loop_memory/serve/routes/graph.py +0 -0
  73. {loop_memory-0.4.13 → loop_memory-0.4.14}/loop_memory/serve/routes/insights.py +0 -0
  74. {loop_memory-0.4.13 → loop_memory-0.4.14}/loop_memory/serve/routes/memories.py +0 -0
  75. {loop_memory-0.4.13 → loop_memory-0.4.14}/loop_memory/serve/routes/sessions.py +0 -0
  76. {loop_memory-0.4.13 → loop_memory-0.4.14}/loop_memory/serve/routes/system.py +0 -0
  77. {loop_memory-0.4.13 → loop_memory-0.4.14}/loop_memory/serve/routes/wiki.py +0 -0
  78. {loop_memory-0.4.13 → loop_memory-0.4.14}/loop_memory/serve/static/__init__.py +0 -0
  79. {loop_memory-0.4.13 → loop_memory-0.4.14}/loop_memory/serve/static/index.html +0 -0
  80. {loop_memory-0.4.13 → loop_memory-0.4.14}/loop_memory/serve/watcher.py +0 -0
  81. {loop_memory-0.4.13 → loop_memory-0.4.14}/loop_memory/storage/__init__.py +0 -0
  82. {loop_memory-0.4.13 → loop_memory-0.4.14}/loop_memory/storage/retrieval.py +0 -0
  83. {loop_memory-0.4.13 → loop_memory-0.4.14}/loop_memory/storage/snapshot.py +0 -0
  84. {loop_memory-0.4.13 → loop_memory-0.4.14}/loop_memory/storage/sqlite_store.py +0 -0
  85. {loop_memory-0.4.13 → loop_memory-0.4.14}/loop_memory/wiki/__init__.py +0 -0
  86. {loop_memory-0.4.13 → loop_memory-0.4.14}/loop_memory/wiki/backfill.py +0 -0
  87. {loop_memory-0.4.13 → loop_memory-0.4.14}/loop_memory/wiki/classifier.py +0 -0
  88. {loop_memory-0.4.13 → loop_memory-0.4.14}/loop_memory/wiki/prompts.py +0 -0
  89. {loop_memory-0.4.13 → loop_memory-0.4.14}/loop_memory/wiki/scope.py +0 -0
  90. {loop_memory-0.4.13 → loop_memory-0.4.14}/loop_memory.egg-info/dependency_links.txt +0 -0
  91. {loop_memory-0.4.13 → loop_memory-0.4.14}/loop_memory.egg-info/entry_points.txt +0 -0
  92. {loop_memory-0.4.13 → loop_memory-0.4.14}/loop_memory.egg-info/requires.txt +0 -0
  93. {loop_memory-0.4.13 → loop_memory-0.4.14}/loop_memory.egg-info/top_level.txt +0 -0
  94. {loop_memory-0.4.13 → loop_memory-0.4.14}/setup.cfg +0 -0
  95. {loop_memory-0.4.13 → loop_memory-0.4.14}/tests/test_admin_ingest_route.py +0 -0
  96. {loop_memory-0.4.13 → loop_memory-0.4.14}/tests/test_agent_memory_api.py +0 -0
  97. {loop_memory-0.4.13 → loop_memory-0.4.14}/tests/test_agent_memory_sdk.py +0 -0
  98. {loop_memory-0.4.13 → loop_memory-0.4.14}/tests/test_agents_init.py +0 -0
  99. {loop_memory-0.4.13 → loop_memory-0.4.14}/tests/test_audit_triggers_2026_09_27.py +0 -0
  100. {loop_memory-0.4.13 → loop_memory-0.4.14}/tests/test_auth_token_rotate.py +0 -0
  101. {loop_memory-0.4.13 → loop_memory-0.4.14}/tests/test_busy_timeout_2026_09_24.py +0 -0
  102. {loop_memory-0.4.13 → loop_memory-0.4.14}/tests/test_cache_cap_2026_09_23.py +0 -0
  103. {loop_memory-0.4.13 → loop_memory-0.4.14}/tests/test_cli_audit_supersede.py +0 -0
  104. {loop_memory-0.4.13 → loop_memory-0.4.14}/tests/test_cli_export_okf.py +0 -0
  105. {loop_memory-0.4.13 → loop_memory-0.4.14}/tests/test_cli_init.py +0 -0
  106. {loop_memory-0.4.13 → loop_memory-0.4.14}/tests/test_cli_memory_stats.py +0 -0
  107. {loop_memory-0.4.13 → loop_memory-0.4.14}/tests/test_cli_recall_as_of.py +0 -0
  108. {loop_memory-0.4.13 → loop_memory-0.4.14}/tests/test_cli_recall_paths.py +0 -0
  109. {loop_memory-0.4.13 → loop_memory-0.4.14}/tests/test_cli_recall_verbose.py +0 -0
  110. {loop_memory-0.4.13 → loop_memory-0.4.14}/tests/test_cli_rules.py +0 -0
  111. {loop_memory-0.4.13 → loop_memory-0.4.14}/tests/test_cli_snapshot.py +0 -0
  112. {loop_memory-0.4.13 → loop_memory-0.4.14}/tests/test_cli_v7.py +0 -0
  113. {loop_memory-0.4.13 → loop_memory-0.4.14}/tests/test_cli_version.py +0 -0
  114. {loop_memory-0.4.13 → loop_memory-0.4.14}/tests/test_contradictions.py +0 -0
  115. {loop_memory-0.4.13 → loop_memory-0.4.14}/tests/test_evolution.py +0 -0
  116. {loop_memory-0.4.13 → loop_memory-0.4.14}/tests/test_evolution_quality.py +0 -0
  117. {loop_memory-0.4.13 → loop_memory-0.4.14}/tests/test_expanduser.py +0 -0
  118. {loop_memory-0.4.13 → loop_memory-0.4.14}/tests/test_export_ask.py +0 -0
  119. {loop_memory-0.4.13 → loop_memory-0.4.14}/tests/test_export_okf.py +0 -0
  120. {loop_memory-0.4.13 → loop_memory-0.4.14}/tests/test_graph.py +0 -0
  121. {loop_memory-0.4.13 → loop_memory-0.4.14}/tests/test_healthz_readyz_2026_09_24.py +0 -0
  122. {loop_memory-0.4.13 → loop_memory-0.4.14}/tests/test_import_okf_2026_09_27.py +0 -0
  123. {loop_memory-0.4.13 → loop_memory-0.4.14}/tests/test_ingest.py +0 -0
  124. {loop_memory-0.4.13 → loop_memory-0.4.14}/tests/test_llm_consolidator.py +0 -0
  125. {loop_memory-0.4.13 → loop_memory-0.4.14}/tests/test_llm_fingerprint_not_persisted.py +0 -0
  126. {loop_memory-0.4.13 → loop_memory-0.4.14}/tests/test_llm_providers.py +0 -0
  127. {loop_memory-0.4.13 → loop_memory-0.4.14}/tests/test_llm_test_endpoint.py +0 -0
  128. {loop_memory-0.4.13 → loop_memory-0.4.14}/tests/test_loop.py +0 -0
  129. {loop_memory-0.4.13 → loop_memory-0.4.14}/tests/test_mcp.py +0 -0
  130. {loop_memory-0.4.13 → loop_memory-0.4.14}/tests/test_memories_pagination.py +0 -0
  131. {loop_memory-0.4.13 → loop_memory-0.4.14}/tests/test_memory_stats.py +0 -0
  132. {loop_memory-0.4.13 → loop_memory-0.4.14}/tests/test_openclaw_loader.py +0 -0
  133. {loop_memory-0.4.13 → loop_memory-0.4.14}/tests/test_package_version.py +0 -0
  134. {loop_memory-0.4.13 → loop_memory-0.4.14}/tests/test_path_safety_2026_09_23.py +0 -0
  135. {loop_memory-0.4.13 → loop_memory-0.4.14}/tests/test_recall_as_of.py +0 -0
  136. {loop_memory-0.4.13 → loop_memory-0.4.14}/tests/test_recall_budget_2026_10_05.py +0 -0
  137. {loop_memory-0.4.13 → loop_memory-0.4.14}/tests/test_recall_paths.py +0 -0
  138. {loop_memory-0.4.13 → loop_memory-0.4.14}/tests/test_recall_provenance.py +0 -0
  139. {loop_memory-0.4.13 → loop_memory-0.4.14}/tests/test_reflection.py +0 -0
  140. {loop_memory-0.4.13 → loop_memory-0.4.14}/tests/test_repo_framing.py +0 -0
  141. {loop_memory-0.4.13 → loop_memory-0.4.14}/tests/test_routes_audit_2026_09_13.py +0 -0
  142. {loop_memory-0.4.13 → loop_memory-0.4.14}/tests/test_routes_audit_2026_09_20.py +0 -0
  143. {loop_memory-0.4.13 → loop_memory-0.4.14}/tests/test_score_api.py +0 -0
  144. {loop_memory-0.4.13 → loop_memory-0.4.14}/tests/test_scoring_v2.py +0 -0
  145. {loop_memory-0.4.13 → loop_memory-0.4.14}/tests/test_secrets.py +0 -0
  146. {loop_memory-0.4.13 → loop_memory-0.4.14}/tests/test_serve_app.py +0 -0
  147. {loop_memory-0.4.13 → loop_memory-0.4.14}/tests/test_serve_handlers.py +0 -0
  148. {loop_memory-0.4.13 → loop_memory-0.4.14}/tests/test_session_order.py +0 -0
  149. {loop_memory-0.4.13 → loop_memory-0.4.14}/tests/test_short_query.py +0 -0
  150. {loop_memory-0.4.13 → loop_memory-0.4.14}/tests/test_snapshot.py +0 -0
  151. {loop_memory-0.4.13 → loop_memory-0.4.14}/tests/test_store.py +0 -0
  152. {loop_memory-0.4.13 → loop_memory-0.4.14}/tests/test_subgraph_hops_2026_09_23.py +0 -0
  153. {loop_memory-0.4.13 → loop_memory-0.4.14}/tests/test_summarization.py +0 -0
  154. {loop_memory-0.4.13 → loop_memory-0.4.14}/tests/test_supersession_chain.py +0 -0
  155. {loop_memory-0.4.13 → loop_memory-0.4.14}/tests/test_universal_memory.py +0 -0
  156. {loop_memory-0.4.13 → loop_memory-0.4.14}/tests/test_vector_store.py +0 -0
  157. {loop_memory-0.4.13 → loop_memory-0.4.14}/tests/test_watcher.py +0 -0
  158. {loop_memory-0.4.13 → loop_memory-0.4.14}/tests/test_wiki_classifier.py +0 -0
  159. {loop_memory-0.4.13 → loop_memory-0.4.14}/tests/test_wiki_export_escape.py +0 -0
  160. {loop_memory-0.4.13 → 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.13
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,8 @@ 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
+
69
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)
70
72
 
71
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)
@@ -22,6 +22,8 @@
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
+
25
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)
26
28
 
27
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)
@@ -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)
@@ -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.",