loop-memory 0.4.12__tar.gz → 0.4.13__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 (157) hide show
  1. {loop_memory-0.4.12 → loop_memory-0.4.13}/PKG-INFO +3 -1
  2. {loop_memory-0.4.12 → loop_memory-0.4.13}/README.md +2 -0
  3. {loop_memory-0.4.12 → loop_memory-0.4.13}/loop_memory/cli/commands/read.py +44 -7
  4. {loop_memory-0.4.12 → loop_memory-0.4.13}/loop_memory/cli/main.py +2 -2
  5. {loop_memory-0.4.12 → loop_memory-0.4.13}/loop_memory/mcp/__init__.py +21 -1
  6. {loop_memory-0.4.12 → loop_memory-0.4.13}/loop_memory/sdk.py +19 -2
  7. {loop_memory-0.4.12 → loop_memory-0.4.13}/loop_memory/serve/routes/system.py +48 -2
  8. {loop_memory-0.4.12 → loop_memory-0.4.13}/loop_memory/storage/sqlite_store.py +314 -2
  9. {loop_memory-0.4.12 → loop_memory-0.4.13}/loop_memory.egg-info/PKG-INFO +3 -1
  10. {loop_memory-0.4.12 → loop_memory-0.4.13}/loop_memory.egg-info/SOURCES.txt +1 -0
  11. {loop_memory-0.4.12 → loop_memory-0.4.13}/pyproject.toml +1 -1
  12. loop_memory-0.4.13/tests/test_recall_budget_2026_10_05.py +407 -0
  13. {loop_memory-0.4.12 → loop_memory-0.4.13}/LICENSE +0 -0
  14. {loop_memory-0.4.12 → loop_memory-0.4.13}/loop_memory/__init__.py +0 -0
  15. {loop_memory-0.4.12 → loop_memory-0.4.13}/loop_memory/backends/__init__.py +0 -0
  16. {loop_memory-0.4.12 → loop_memory-0.4.13}/loop_memory/backends/embedding.py +0 -0
  17. {loop_memory-0.4.12 → loop_memory-0.4.13}/loop_memory/backends/sentence_embedder.py +0 -0
  18. {loop_memory-0.4.12 → loop_memory-0.4.13}/loop_memory/backends/vector_store.py +0 -0
  19. {loop_memory-0.4.12 → loop_memory-0.4.13}/loop_memory/cli/__init__.py +0 -0
  20. {loop_memory-0.4.12 → loop_memory-0.4.13}/loop_memory/cli/_common.py +0 -0
  21. {loop_memory-0.4.12 → loop_memory-0.4.13}/loop_memory/cli/commands/__init__.py +0 -0
  22. {loop_memory-0.4.12 → loop_memory-0.4.13}/loop_memory/cli/commands/cognitive.py +0 -0
  23. {loop_memory-0.4.12 → loop_memory-0.4.13}/loop_memory/cli/commands/diag.py +0 -0
  24. {loop_memory-0.4.12 → loop_memory-0.4.13}/loop_memory/cli/commands/graph.py +0 -0
  25. {loop_memory-0.4.12 → loop_memory-0.4.13}/loop_memory/cli/commands/hooks.py +0 -0
  26. {loop_memory-0.4.12 → loop_memory-0.4.13}/loop_memory/cli/commands/rules.py +0 -0
  27. {loop_memory-0.4.12 → loop_memory-0.4.13}/loop_memory/cli/commands/serve.py +0 -0
  28. {loop_memory-0.4.12 → loop_memory-0.4.13}/loop_memory/cli/commands/write.py +0 -0
  29. {loop_memory-0.4.12 → loop_memory-0.4.13}/loop_memory/engine/__init__.py +0 -0
  30. {loop_memory-0.4.12 → loop_memory-0.4.13}/loop_memory/engine/loop.py +0 -0
  31. {loop_memory-0.4.12 → loop_memory-0.4.13}/loop_memory/engine/reflect.py +0 -0
  32. {loop_memory-0.4.12 → loop_memory-0.4.13}/loop_memory/examples/__init__.py +0 -0
  33. {loop_memory-0.4.12 → loop_memory-0.4.13}/loop_memory/examples/demo.py +0 -0
  34. {loop_memory-0.4.12 → loop_memory-0.4.13}/loop_memory/export/__init__.py +0 -0
  35. {loop_memory-0.4.12 → loop_memory-0.4.13}/loop_memory/export/memory_md.py +0 -0
  36. {loop_memory-0.4.12 → loop_memory-0.4.13}/loop_memory/graph/__init__.py +0 -0
  37. {loop_memory-0.4.12 → loop_memory-0.4.13}/loop_memory/graph/build.py +0 -0
  38. {loop_memory-0.4.12 → loop_memory-0.4.13}/loop_memory/graph/extract.py +0 -0
  39. {loop_memory-0.4.12 → loop_memory-0.4.13}/loop_memory/ingest/__init__.py +0 -0
  40. {loop_memory-0.4.12 → loop_memory-0.4.13}/loop_memory/ingest/loader.py +0 -0
  41. {loop_memory-0.4.12 → loop_memory-0.4.13}/loop_memory/ingest/pipeline.py +0 -0
  42. {loop_memory-0.4.12 → loop_memory-0.4.13}/loop_memory/jobs/__init__.py +0 -0
  43. {loop_memory-0.4.12 → loop_memory-0.4.13}/loop_memory/jobs/cognitive.py +0 -0
  44. {loop_memory-0.4.12 → loop_memory-0.4.13}/loop_memory/jobs/compact.py +0 -0
  45. {loop_memory-0.4.12 → loop_memory-0.4.13}/loop_memory/jobs/consolidate.py +0 -0
  46. {loop_memory-0.4.12 → loop_memory-0.4.13}/loop_memory/jobs/contradiction.py +0 -0
  47. {loop_memory-0.4.12 → loop_memory-0.4.13}/loop_memory/jobs/evolution.py +0 -0
  48. {loop_memory-0.4.12 → loop_memory-0.4.13}/loop_memory/jobs/graph.py +0 -0
  49. {loop_memory-0.4.12 → loop_memory-0.4.13}/loop_memory/jobs/llm_compact_pass.py +0 -0
  50. {loop_memory-0.4.12 → loop_memory-0.4.13}/loop_memory/jobs/llm_consolidate.py +0 -0
  51. {loop_memory-0.4.12 → loop_memory-0.4.13}/loop_memory/jobs/scheduler.py +0 -0
  52. {loop_memory-0.4.12 → loop_memory-0.4.13}/loop_memory/llm/__init__.py +0 -0
  53. {loop_memory-0.4.12 → loop_memory-0.4.13}/loop_memory/llm/base.py +0 -0
  54. {loop_memory-0.4.12 → loop_memory-0.4.13}/loop_memory/llm/openai_adapter.py +0 -0
  55. {loop_memory-0.4.12 → loop_memory-0.4.13}/loop_memory/llm/providers.py +0 -0
  56. {loop_memory-0.4.12 → loop_memory-0.4.13}/loop_memory/memory/__init__.py +0 -0
  57. {loop_memory-0.4.12 → loop_memory-0.4.13}/loop_memory/memory/types.py +0 -0
  58. {loop_memory-0.4.12 → loop_memory-0.4.13}/loop_memory/privacy/__init__.py +0 -0
  59. {loop_memory-0.4.12 → loop_memory-0.4.13}/loop_memory/privacy/private.py +0 -0
  60. {loop_memory-0.4.12 → loop_memory-0.4.13}/loop_memory/privacy/redact.py +0 -0
  61. {loop_memory-0.4.12 → loop_memory-0.4.13}/loop_memory/py.typed +0 -0
  62. {loop_memory-0.4.12 → loop_memory-0.4.13}/loop_memory/sdk_extensions.py +0 -0
  63. {loop_memory-0.4.12 → loop_memory-0.4.13}/loop_memory/security/__init__.py +0 -0
  64. {loop_memory-0.4.12 → loop_memory-0.4.13}/loop_memory/security/secrets.py +0 -0
  65. {loop_memory-0.4.12 → loop_memory-0.4.13}/loop_memory/serve/__init__.py +0 -0
  66. {loop_memory-0.4.12 → loop_memory-0.4.13}/loop_memory/serve/app.py +0 -0
  67. {loop_memory-0.4.12 → loop_memory-0.4.13}/loop_memory/serve/handlers.py +0 -0
  68. {loop_memory-0.4.12 → loop_memory-0.4.13}/loop_memory/serve/routes/_shared.py +0 -0
  69. {loop_memory-0.4.12 → loop_memory-0.4.13}/loop_memory/serve/routes/admin.py +0 -0
  70. {loop_memory-0.4.12 → loop_memory-0.4.13}/loop_memory/serve/routes/cognitive.py +0 -0
  71. {loop_memory-0.4.12 → loop_memory-0.4.13}/loop_memory/serve/routes/export.py +0 -0
  72. {loop_memory-0.4.12 → loop_memory-0.4.13}/loop_memory/serve/routes/graph.py +0 -0
  73. {loop_memory-0.4.12 → loop_memory-0.4.13}/loop_memory/serve/routes/insights.py +0 -0
  74. {loop_memory-0.4.12 → loop_memory-0.4.13}/loop_memory/serve/routes/memories.py +0 -0
  75. {loop_memory-0.4.12 → loop_memory-0.4.13}/loop_memory/serve/routes/sessions.py +0 -0
  76. {loop_memory-0.4.12 → loop_memory-0.4.13}/loop_memory/serve/routes/wiki.py +0 -0
  77. {loop_memory-0.4.12 → loop_memory-0.4.13}/loop_memory/serve/static/__init__.py +0 -0
  78. {loop_memory-0.4.12 → loop_memory-0.4.13}/loop_memory/serve/static/index.html +0 -0
  79. {loop_memory-0.4.12 → loop_memory-0.4.13}/loop_memory/serve/watcher.py +0 -0
  80. {loop_memory-0.4.12 → loop_memory-0.4.13}/loop_memory/storage/__init__.py +0 -0
  81. {loop_memory-0.4.12 → loop_memory-0.4.13}/loop_memory/storage/retrieval.py +0 -0
  82. {loop_memory-0.4.12 → loop_memory-0.4.13}/loop_memory/storage/snapshot.py +0 -0
  83. {loop_memory-0.4.12 → loop_memory-0.4.13}/loop_memory/wiki/__init__.py +0 -0
  84. {loop_memory-0.4.12 → loop_memory-0.4.13}/loop_memory/wiki/backfill.py +0 -0
  85. {loop_memory-0.4.12 → loop_memory-0.4.13}/loop_memory/wiki/classifier.py +0 -0
  86. {loop_memory-0.4.12 → loop_memory-0.4.13}/loop_memory/wiki/prompts.py +0 -0
  87. {loop_memory-0.4.12 → loop_memory-0.4.13}/loop_memory/wiki/scope.py +0 -0
  88. {loop_memory-0.4.12 → loop_memory-0.4.13}/loop_memory.egg-info/dependency_links.txt +0 -0
  89. {loop_memory-0.4.12 → loop_memory-0.4.13}/loop_memory.egg-info/entry_points.txt +0 -0
  90. {loop_memory-0.4.12 → loop_memory-0.4.13}/loop_memory.egg-info/requires.txt +0 -0
  91. {loop_memory-0.4.12 → loop_memory-0.4.13}/loop_memory.egg-info/top_level.txt +0 -0
  92. {loop_memory-0.4.12 → loop_memory-0.4.13}/setup.cfg +0 -0
  93. {loop_memory-0.4.12 → loop_memory-0.4.13}/tests/test_admin_ingest_route.py +0 -0
  94. {loop_memory-0.4.12 → loop_memory-0.4.13}/tests/test_agent_memory_api.py +0 -0
  95. {loop_memory-0.4.12 → loop_memory-0.4.13}/tests/test_agent_memory_sdk.py +0 -0
  96. {loop_memory-0.4.12 → loop_memory-0.4.13}/tests/test_agents_init.py +0 -0
  97. {loop_memory-0.4.12 → loop_memory-0.4.13}/tests/test_audit_triggers_2026_09_27.py +0 -0
  98. {loop_memory-0.4.12 → loop_memory-0.4.13}/tests/test_auth_token_rotate.py +0 -0
  99. {loop_memory-0.4.12 → loop_memory-0.4.13}/tests/test_busy_timeout_2026_09_24.py +0 -0
  100. {loop_memory-0.4.12 → loop_memory-0.4.13}/tests/test_cache_cap_2026_09_23.py +0 -0
  101. {loop_memory-0.4.12 → loop_memory-0.4.13}/tests/test_cli_audit_supersede.py +0 -0
  102. {loop_memory-0.4.12 → loop_memory-0.4.13}/tests/test_cli_export_okf.py +0 -0
  103. {loop_memory-0.4.12 → loop_memory-0.4.13}/tests/test_cli_init.py +0 -0
  104. {loop_memory-0.4.12 → loop_memory-0.4.13}/tests/test_cli_memory_stats.py +0 -0
  105. {loop_memory-0.4.12 → loop_memory-0.4.13}/tests/test_cli_recall_as_of.py +0 -0
  106. {loop_memory-0.4.12 → loop_memory-0.4.13}/tests/test_cli_recall_paths.py +0 -0
  107. {loop_memory-0.4.12 → loop_memory-0.4.13}/tests/test_cli_recall_verbose.py +0 -0
  108. {loop_memory-0.4.12 → loop_memory-0.4.13}/tests/test_cli_rules.py +0 -0
  109. {loop_memory-0.4.12 → loop_memory-0.4.13}/tests/test_cli_snapshot.py +0 -0
  110. {loop_memory-0.4.12 → loop_memory-0.4.13}/tests/test_cli_v7.py +0 -0
  111. {loop_memory-0.4.12 → loop_memory-0.4.13}/tests/test_cli_version.py +0 -0
  112. {loop_memory-0.4.12 → loop_memory-0.4.13}/tests/test_contradictions.py +0 -0
  113. {loop_memory-0.4.12 → loop_memory-0.4.13}/tests/test_evolution.py +0 -0
  114. {loop_memory-0.4.12 → loop_memory-0.4.13}/tests/test_evolution_quality.py +0 -0
  115. {loop_memory-0.4.12 → loop_memory-0.4.13}/tests/test_expanduser.py +0 -0
  116. {loop_memory-0.4.12 → loop_memory-0.4.13}/tests/test_export_ask.py +0 -0
  117. {loop_memory-0.4.12 → loop_memory-0.4.13}/tests/test_export_okf.py +0 -0
  118. {loop_memory-0.4.12 → loop_memory-0.4.13}/tests/test_graph.py +0 -0
  119. {loop_memory-0.4.12 → loop_memory-0.4.13}/tests/test_healthz_readyz_2026_09_24.py +0 -0
  120. {loop_memory-0.4.12 → loop_memory-0.4.13}/tests/test_import_okf_2026_09_27.py +0 -0
  121. {loop_memory-0.4.12 → loop_memory-0.4.13}/tests/test_ingest.py +0 -0
  122. {loop_memory-0.4.12 → loop_memory-0.4.13}/tests/test_llm_consolidator.py +0 -0
  123. {loop_memory-0.4.12 → loop_memory-0.4.13}/tests/test_llm_fingerprint_not_persisted.py +0 -0
  124. {loop_memory-0.4.12 → loop_memory-0.4.13}/tests/test_llm_providers.py +0 -0
  125. {loop_memory-0.4.12 → loop_memory-0.4.13}/tests/test_llm_test_endpoint.py +0 -0
  126. {loop_memory-0.4.12 → loop_memory-0.4.13}/tests/test_loop.py +0 -0
  127. {loop_memory-0.4.12 → loop_memory-0.4.13}/tests/test_mcp.py +0 -0
  128. {loop_memory-0.4.12 → loop_memory-0.4.13}/tests/test_memories_pagination.py +0 -0
  129. {loop_memory-0.4.12 → loop_memory-0.4.13}/tests/test_memory_stats.py +0 -0
  130. {loop_memory-0.4.12 → loop_memory-0.4.13}/tests/test_openclaw_loader.py +0 -0
  131. {loop_memory-0.4.12 → loop_memory-0.4.13}/tests/test_package_version.py +0 -0
  132. {loop_memory-0.4.12 → loop_memory-0.4.13}/tests/test_path_safety_2026_09_23.py +0 -0
  133. {loop_memory-0.4.12 → loop_memory-0.4.13}/tests/test_recall_as_of.py +0 -0
  134. {loop_memory-0.4.12 → loop_memory-0.4.13}/tests/test_recall_paths.py +0 -0
  135. {loop_memory-0.4.12 → loop_memory-0.4.13}/tests/test_recall_provenance.py +0 -0
  136. {loop_memory-0.4.12 → loop_memory-0.4.13}/tests/test_reflection.py +0 -0
  137. {loop_memory-0.4.12 → loop_memory-0.4.13}/tests/test_repo_framing.py +0 -0
  138. {loop_memory-0.4.12 → loop_memory-0.4.13}/tests/test_routes_audit_2026_09_13.py +0 -0
  139. {loop_memory-0.4.12 → loop_memory-0.4.13}/tests/test_routes_audit_2026_09_20.py +0 -0
  140. {loop_memory-0.4.12 → loop_memory-0.4.13}/tests/test_score_api.py +0 -0
  141. {loop_memory-0.4.12 → loop_memory-0.4.13}/tests/test_scoring_v2.py +0 -0
  142. {loop_memory-0.4.12 → loop_memory-0.4.13}/tests/test_secrets.py +0 -0
  143. {loop_memory-0.4.12 → loop_memory-0.4.13}/tests/test_serve_app.py +0 -0
  144. {loop_memory-0.4.12 → loop_memory-0.4.13}/tests/test_serve_handlers.py +0 -0
  145. {loop_memory-0.4.12 → loop_memory-0.4.13}/tests/test_session_order.py +0 -0
  146. {loop_memory-0.4.12 → loop_memory-0.4.13}/tests/test_short_query.py +0 -0
  147. {loop_memory-0.4.12 → loop_memory-0.4.13}/tests/test_snapshot.py +0 -0
  148. {loop_memory-0.4.12 → loop_memory-0.4.13}/tests/test_store.py +0 -0
  149. {loop_memory-0.4.12 → loop_memory-0.4.13}/tests/test_subgraph_hops_2026_09_23.py +0 -0
  150. {loop_memory-0.4.12 → loop_memory-0.4.13}/tests/test_summarization.py +0 -0
  151. {loop_memory-0.4.12 → loop_memory-0.4.13}/tests/test_supersession_chain.py +0 -0
  152. {loop_memory-0.4.12 → loop_memory-0.4.13}/tests/test_universal_memory.py +0 -0
  153. {loop_memory-0.4.12 → loop_memory-0.4.13}/tests/test_vector_store.py +0 -0
  154. {loop_memory-0.4.12 → loop_memory-0.4.13}/tests/test_watcher.py +0 -0
  155. {loop_memory-0.4.12 → loop_memory-0.4.13}/tests/test_wiki_classifier.py +0 -0
  156. {loop_memory-0.4.12 → loop_memory-0.4.13}/tests/test_wiki_export_escape.py +0 -0
  157. {loop_memory-0.4.12 → loop_memory-0.4.13}/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.13
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.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
+
69
71
  > **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
72
 
71
73
  > **What's new in 0.4.11** — **operational probes** +
@@ -22,6 +22,8 @@
22
22
 
23
23
  ---
24
24
 
25
+ > **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
+
25
27
  > **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
28
 
27
29
  > **What's new in 0.4.11** — **operational probes** +
@@ -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
@@ -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.",
@@ -78,7 +78,27 @@ def tool_recall(arguments: dict[str, Any]) -> list[dict[str, Any]]:
78
78
  return [_err("missing 'query' argument")]
79
79
  store = _store()
80
80
  source = arguments.get("source") or _agent_context()[0]
81
- r = store.recall(query, limit=limit, source=source)
81
+ # Audit 2026-10-05: thread ``max_chars`` (token / char budget cap)
82
+ # + ``level`` (tiered-loader) through the MCP tool so an MCP-aware
83
+ # agent loop can ask for a bounded retrieval inline. ``None`` and
84
+ # the default ``1`` preserve the byte-identical legacy payload.
85
+ raw_max = arguments.get("max_chars")
86
+ max_chars: int | None = None
87
+ if raw_max is not None:
88
+ try:
89
+ max_chars = int(raw_max)
90
+ if max_chars <= 0:
91
+ max_chars = None
92
+ except (TypeError, ValueError):
93
+ max_chars = None
94
+ try:
95
+ level = int(arguments.get("level") or 1)
96
+ if level not in (0, 1, 2):
97
+ level = 1
98
+ except (TypeError, ValueError):
99
+ level = 1
100
+ r = store.recall(query, limit=limit, source=source,
101
+ max_chars=max_chars, level=level)
82
102
  n_mem = len(r["memories"])
83
103
  n_wiki = len(r["wiki"])
84
104
  n_ent = len(r["entities"])
@@ -243,8 +243,15 @@ class MemoryClient(MemoryClientExt, AbstractContextManager):
243
243
  agent_id: str | None = None,
244
244
  user_id: str | None = None,
245
245
  include: str = "memories,wiki,entities",
246
+ max_chars: int | None = None,
247
+ level: int = 1,
246
248
  ) -> RecallResult:
247
- """Unified search across the user's memory store."""
249
+ """Unified search across the user's memory store.
250
+
251
+ ``max_chars`` (audit 2026-10-05) caps the total payload so an
252
+ MCP-loop caller never has to hand-trim a giant response.
253
+ ``level`` (0..2) is the tiered-loader knob.
254
+ """
248
255
  raise NotImplementedError
249
256
 
250
257
  def forget(
@@ -351,6 +358,8 @@ class _InProcessClient(MemoryClient):
351
358
  agent_id: str | None = None,
352
359
  user_id: str | None = None,
353
360
  include: str = "memories,wiki,entities",
361
+ max_chars: int | None = None,
362
+ level: int = 1,
354
363
  ) -> RecallResult:
355
364
  # ``recall_hybrid`` is the production code path; fall back to
356
365
  # the legacy LIKE-based ``recall`` if a stale DB doesn't have
@@ -359,7 +368,8 @@ class _InProcessClient(MemoryClient):
359
368
  if hasattr(self._store, "recall_hybrid"):
360
369
  r = self._store.recall_hybrid(
361
370
  query, limit=limit, include=wanted,
362
- bump_signals=True, source=source, level=1,
371
+ bump_signals=True, source=source, level=level,
372
+ max_chars=max_chars,
363
373
  )
364
374
  else:
365
375
  r = self._store.recall(
@@ -367,6 +377,8 @@ class _InProcessClient(MemoryClient):
367
377
  limit=limit,
368
378
  include=wanted,
369
379
  source=source,
380
+ max_chars=max_chars,
381
+ level=level,
370
382
  )
371
383
  # Optional post-filter: even if the store returned hits, the
372
384
  # caller may want only their agent's namespace.
@@ -661,13 +673,18 @@ class _HttpClient(MemoryClient):
661
673
  agent_id: str | None = None,
662
674
  user_id: str | None = None,
663
675
  include: str = "memories,wiki,entities",
676
+ max_chars: int | None = None,
677
+ level: int = 1,
664
678
  ) -> RecallResult:
665
679
  params = {
666
680
  "q": query,
667
681
  "limit": int(limit),
668
682
  "include": include,
669
683
  "source": source or "",
684
+ "level": int(level),
670
685
  }
686
+ if max_chars is not None:
687
+ params["max_chars"] = int(max_chars)
671
688
  if agent_id is not None:
672
689
  params["agent_id"] = agent_id
673
690
  if user_id is not None:
@@ -386,7 +386,8 @@ def register(app: FastAPI, store: MemoryStore, scheduler: Optional[Any] = None,
386
386
  def recall(query: str, limit: int = 10, include: str = "memories,wiki,entities",
387
387
  bump: int = 1, source: str | None = None,
388
388
  mode: str = "hybrid", level: int = 1,
389
- as_of: str | None = None):
389
+ as_of: str | None = None,
390
+ max_chars: int | None = None):
390
391
  """Unified recall — wiki + memories + entities in one ranked stream.
391
392
 
392
393
  ``mode`` is 'hybrid' (default) for BM25+semantic+entity RRF,
@@ -406,10 +407,33 @@ def register(app: FastAPI, store: MemoryStore, scheduler: Optional[Any] = None,
406
407
  When unset (default) the call returns the latest answer
407
408
  (preserves the pre-0.4.9 contract). When set, the call
408
409
  routes through ``MemoryStore.recall_as_of``.
410
+
411
+ ``level`` (audit 2026-10-05) is the OpenViking-style
412
+ tiered-loader knob: 0 = L0 abstract ladder rung (titles +
413
+ tags + abstract only, never the full ``text`` / ``body``),
414
+ 1 = L1 default (full ``text``, ``body`` capped at 800),
415
+ 2 = L2 (full body, full text).
416
+
417
+ ``max_chars`` (audit 2026-10-05) caps the total payload
418
+ (sum of ``text`` + ``body`` across all hits) so an agent
419
+ loop can ask for a bounded retrieval. ``None`` (default)
420
+ preserves the byte-identical legacy payload. Source:
421
+ vectorize-io/hindsight v0.10.2 "concise extraction by
422
+ default" + aiming-lab/SimpleMem ICML'26 semantic-lossless-
423
+ compression.
409
424
  """
410
425
  wanted = tuple(s.strip() for s in include.split(",") if s.strip())
411
426
  if not wanted:
412
427
  wanted = ("memories", "wiki", "entities")
428
+ # Coerce max_chars to ``int | None`` so the route accepts
429
+ # ``?max_chars=`` (empty) and ``?max_chars=0`` as "no cap".
430
+ if max_chars is not None:
431
+ try:
432
+ max_chars = int(max_chars)
433
+ except (TypeError, ValueError):
434
+ max_chars = None
435
+ if max_chars is not None and max_chars <= 0:
436
+ max_chars = None
413
437
  if as_of is not None and str(as_of).strip():
414
438
  try:
415
439
  r = store.recall_as_of(
@@ -419,6 +443,8 @@ def register(app: FastAPI, store: MemoryStore, scheduler: Optional[Any] = None,
419
443
  include=wanted,
420
444
  bump_signals=bool(bump),
421
445
  source=source,
446
+ max_chars=max_chars,
447
+ level=level,
422
448
  )
423
449
  recall_mode = "as_of"
424
450
  except ValueError as e:
@@ -430,12 +456,15 @@ def register(app: FastAPI, store: MemoryStore, scheduler: Optional[Any] = None,
430
456
  include=wanted,
431
457
  bump_signals=bool(bump),
432
458
  source=source,
459
+ max_chars=max_chars,
460
+ level=level,
433
461
  )
434
462
  recall_mode = mode
435
463
  else:
436
464
  r = store.recall_hybrid(
437
465
  query, limit=limit, include=wanted,
438
466
  bump_signals=bool(bump), source=source, level=level,
467
+ max_chars=max_chars,
439
468
  )
440
469
  recall_mode = mode
441
470
  return {
@@ -448,6 +477,7 @@ def register(app: FastAPI, store: MemoryStore, scheduler: Optional[Any] = None,
448
477
  "source": source,
449
478
  "level": level,
450
479
  "as_of": as_of,
480
+ "max_chars": max_chars,
451
481
  }
452
482
 
453
483
 
@@ -531,7 +561,8 @@ def register(app: FastAPI, store: MemoryStore, scheduler: Optional[Any] = None,
531
561
  @app.get("/api/recall/outline")
532
562
  def recall_outline(query: str = "", limit: int = 12,
533
563
  include: str = "memories,wiki,entities",
534
- bump: int = 0, source: str | None = None):
564
+ bump: int = 0, source: str | None = None,
565
+ max_chars: int | None = None):
535
566
  """Audit 2026-09-13 — L0 outline recall (tigerless-labs/agent-memory
536
567
  v0.3.0 recall ladder pattern).
537
568
 
@@ -546,18 +577,32 @@ def register(app: FastAPI, store: MemoryStore, scheduler: Optional[Any] = None,
546
577
  ``recall_count`` before the agent has actually decided to read
547
578
  the body. Set ``bump=1`` only when the agent commits to the
548
579
  candidate set.
580
+
581
+ ``max_chars`` (audit 2026-10-05) caps the total payload of
582
+ the *abstracts* across all returned hits so a downstream UI
583
+ never has to hand-trim a giant chip stream. ``None``
584
+ (default) keeps the legacy byte-identical abstract ladder
585
+ rung.
549
586
  """
550
587
  if not query:
551
588
  raise HTTPException(status_code=400, detail="query is required")
552
589
  wanted = tuple(s.strip() for s in include.split(",") if s.strip())
553
590
  if not wanted:
554
591
  wanted = ("memories", "wiki", "entities")
592
+ if max_chars is not None:
593
+ try:
594
+ max_chars = int(max_chars)
595
+ except (TypeError, ValueError):
596
+ max_chars = None
597
+ if max_chars is not None and max_chars <= 0:
598
+ max_chars = None
555
599
  r = store.recall_paths(
556
600
  query,
557
601
  limit=limit,
558
602
  include=wanted,
559
603
  bump_signals=bool(bump),
560
604
  source=source,
605
+ max_chars=max_chars,
561
606
  )
562
607
  return {
563
608
  "query": query,
@@ -567,6 +612,7 @@ def register(app: FastAPI, store: MemoryStore, scheduler: Optional[Any] = None,
567
612
  "entities": r.get("entities", []),
568
613
  "mode": "outline",
569
614
  "source": source,
615
+ "max_chars": max_chars,
570
616
  }
571
617
 
572
618