loop-memory 0.4.11__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.11 → loop_memory-0.4.13}/PKG-INFO +5 -1
  2. {loop_memory-0.4.11 → loop_memory-0.4.13}/README.md +4 -0
  3. {loop_memory-0.4.11 → loop_memory-0.4.13}/loop_memory/cli/commands/cognitive.py +46 -0
  4. {loop_memory-0.4.11 → loop_memory-0.4.13}/loop_memory/cli/commands/read.py +44 -7
  5. {loop_memory-0.4.11 → loop_memory-0.4.13}/loop_memory/cli/main.py +6 -2
  6. {loop_memory-0.4.11 → loop_memory-0.4.13}/loop_memory/mcp/__init__.py +21 -1
  7. {loop_memory-0.4.11 → loop_memory-0.4.13}/loop_memory/sdk.py +19 -2
  8. {loop_memory-0.4.11 → loop_memory-0.4.13}/loop_memory/serve/routes/system.py +91 -2
  9. {loop_memory-0.4.11 → loop_memory-0.4.13}/loop_memory/storage/sqlite_store.py +886 -3
  10. {loop_memory-0.4.11 → loop_memory-0.4.13}/loop_memory.egg-info/PKG-INFO +5 -1
  11. {loop_memory-0.4.11 → loop_memory-0.4.13}/loop_memory.egg-info/SOURCES.txt +3 -0
  12. {loop_memory-0.4.11 → loop_memory-0.4.13}/pyproject.toml +1 -1
  13. {loop_memory-0.4.11 → loop_memory-0.4.13}/tests/test_agents_init.py +3 -3
  14. loop_memory-0.4.13/tests/test_audit_triggers_2026_09_27.py +188 -0
  15. loop_memory-0.4.13/tests/test_import_okf_2026_09_27.py +425 -0
  16. loop_memory-0.4.13/tests/test_recall_budget_2026_10_05.py +407 -0
  17. {loop_memory-0.4.11 → loop_memory-0.4.13}/tests/test_supersession_chain.py +1 -1
  18. {loop_memory-0.4.11 → loop_memory-0.4.13}/LICENSE +0 -0
  19. {loop_memory-0.4.11 → loop_memory-0.4.13}/loop_memory/__init__.py +0 -0
  20. {loop_memory-0.4.11 → loop_memory-0.4.13}/loop_memory/backends/__init__.py +0 -0
  21. {loop_memory-0.4.11 → loop_memory-0.4.13}/loop_memory/backends/embedding.py +0 -0
  22. {loop_memory-0.4.11 → loop_memory-0.4.13}/loop_memory/backends/sentence_embedder.py +0 -0
  23. {loop_memory-0.4.11 → loop_memory-0.4.13}/loop_memory/backends/vector_store.py +0 -0
  24. {loop_memory-0.4.11 → loop_memory-0.4.13}/loop_memory/cli/__init__.py +0 -0
  25. {loop_memory-0.4.11 → loop_memory-0.4.13}/loop_memory/cli/_common.py +0 -0
  26. {loop_memory-0.4.11 → loop_memory-0.4.13}/loop_memory/cli/commands/__init__.py +0 -0
  27. {loop_memory-0.4.11 → loop_memory-0.4.13}/loop_memory/cli/commands/diag.py +0 -0
  28. {loop_memory-0.4.11 → loop_memory-0.4.13}/loop_memory/cli/commands/graph.py +0 -0
  29. {loop_memory-0.4.11 → loop_memory-0.4.13}/loop_memory/cli/commands/hooks.py +0 -0
  30. {loop_memory-0.4.11 → loop_memory-0.4.13}/loop_memory/cli/commands/rules.py +0 -0
  31. {loop_memory-0.4.11 → loop_memory-0.4.13}/loop_memory/cli/commands/serve.py +0 -0
  32. {loop_memory-0.4.11 → loop_memory-0.4.13}/loop_memory/cli/commands/write.py +0 -0
  33. {loop_memory-0.4.11 → loop_memory-0.4.13}/loop_memory/engine/__init__.py +0 -0
  34. {loop_memory-0.4.11 → loop_memory-0.4.13}/loop_memory/engine/loop.py +0 -0
  35. {loop_memory-0.4.11 → loop_memory-0.4.13}/loop_memory/engine/reflect.py +0 -0
  36. {loop_memory-0.4.11 → loop_memory-0.4.13}/loop_memory/examples/__init__.py +0 -0
  37. {loop_memory-0.4.11 → loop_memory-0.4.13}/loop_memory/examples/demo.py +0 -0
  38. {loop_memory-0.4.11 → loop_memory-0.4.13}/loop_memory/export/__init__.py +0 -0
  39. {loop_memory-0.4.11 → loop_memory-0.4.13}/loop_memory/export/memory_md.py +0 -0
  40. {loop_memory-0.4.11 → loop_memory-0.4.13}/loop_memory/graph/__init__.py +0 -0
  41. {loop_memory-0.4.11 → loop_memory-0.4.13}/loop_memory/graph/build.py +0 -0
  42. {loop_memory-0.4.11 → loop_memory-0.4.13}/loop_memory/graph/extract.py +0 -0
  43. {loop_memory-0.4.11 → loop_memory-0.4.13}/loop_memory/ingest/__init__.py +0 -0
  44. {loop_memory-0.4.11 → loop_memory-0.4.13}/loop_memory/ingest/loader.py +0 -0
  45. {loop_memory-0.4.11 → loop_memory-0.4.13}/loop_memory/ingest/pipeline.py +0 -0
  46. {loop_memory-0.4.11 → loop_memory-0.4.13}/loop_memory/jobs/__init__.py +0 -0
  47. {loop_memory-0.4.11 → loop_memory-0.4.13}/loop_memory/jobs/cognitive.py +0 -0
  48. {loop_memory-0.4.11 → loop_memory-0.4.13}/loop_memory/jobs/compact.py +0 -0
  49. {loop_memory-0.4.11 → loop_memory-0.4.13}/loop_memory/jobs/consolidate.py +0 -0
  50. {loop_memory-0.4.11 → loop_memory-0.4.13}/loop_memory/jobs/contradiction.py +0 -0
  51. {loop_memory-0.4.11 → loop_memory-0.4.13}/loop_memory/jobs/evolution.py +0 -0
  52. {loop_memory-0.4.11 → loop_memory-0.4.13}/loop_memory/jobs/graph.py +0 -0
  53. {loop_memory-0.4.11 → loop_memory-0.4.13}/loop_memory/jobs/llm_compact_pass.py +0 -0
  54. {loop_memory-0.4.11 → loop_memory-0.4.13}/loop_memory/jobs/llm_consolidate.py +0 -0
  55. {loop_memory-0.4.11 → loop_memory-0.4.13}/loop_memory/jobs/scheduler.py +0 -0
  56. {loop_memory-0.4.11 → loop_memory-0.4.13}/loop_memory/llm/__init__.py +0 -0
  57. {loop_memory-0.4.11 → loop_memory-0.4.13}/loop_memory/llm/base.py +0 -0
  58. {loop_memory-0.4.11 → loop_memory-0.4.13}/loop_memory/llm/openai_adapter.py +0 -0
  59. {loop_memory-0.4.11 → loop_memory-0.4.13}/loop_memory/llm/providers.py +0 -0
  60. {loop_memory-0.4.11 → loop_memory-0.4.13}/loop_memory/memory/__init__.py +0 -0
  61. {loop_memory-0.4.11 → loop_memory-0.4.13}/loop_memory/memory/types.py +0 -0
  62. {loop_memory-0.4.11 → loop_memory-0.4.13}/loop_memory/privacy/__init__.py +0 -0
  63. {loop_memory-0.4.11 → loop_memory-0.4.13}/loop_memory/privacy/private.py +0 -0
  64. {loop_memory-0.4.11 → loop_memory-0.4.13}/loop_memory/privacy/redact.py +0 -0
  65. {loop_memory-0.4.11 → loop_memory-0.4.13}/loop_memory/py.typed +0 -0
  66. {loop_memory-0.4.11 → loop_memory-0.4.13}/loop_memory/sdk_extensions.py +0 -0
  67. {loop_memory-0.4.11 → loop_memory-0.4.13}/loop_memory/security/__init__.py +0 -0
  68. {loop_memory-0.4.11 → loop_memory-0.4.13}/loop_memory/security/secrets.py +0 -0
  69. {loop_memory-0.4.11 → loop_memory-0.4.13}/loop_memory/serve/__init__.py +0 -0
  70. {loop_memory-0.4.11 → loop_memory-0.4.13}/loop_memory/serve/app.py +0 -0
  71. {loop_memory-0.4.11 → loop_memory-0.4.13}/loop_memory/serve/handlers.py +0 -0
  72. {loop_memory-0.4.11 → loop_memory-0.4.13}/loop_memory/serve/routes/_shared.py +0 -0
  73. {loop_memory-0.4.11 → loop_memory-0.4.13}/loop_memory/serve/routes/admin.py +0 -0
  74. {loop_memory-0.4.11 → loop_memory-0.4.13}/loop_memory/serve/routes/cognitive.py +0 -0
  75. {loop_memory-0.4.11 → loop_memory-0.4.13}/loop_memory/serve/routes/export.py +0 -0
  76. {loop_memory-0.4.11 → loop_memory-0.4.13}/loop_memory/serve/routes/graph.py +0 -0
  77. {loop_memory-0.4.11 → loop_memory-0.4.13}/loop_memory/serve/routes/insights.py +0 -0
  78. {loop_memory-0.4.11 → loop_memory-0.4.13}/loop_memory/serve/routes/memories.py +0 -0
  79. {loop_memory-0.4.11 → loop_memory-0.4.13}/loop_memory/serve/routes/sessions.py +0 -0
  80. {loop_memory-0.4.11 → loop_memory-0.4.13}/loop_memory/serve/routes/wiki.py +0 -0
  81. {loop_memory-0.4.11 → loop_memory-0.4.13}/loop_memory/serve/static/__init__.py +0 -0
  82. {loop_memory-0.4.11 → loop_memory-0.4.13}/loop_memory/serve/static/index.html +0 -0
  83. {loop_memory-0.4.11 → loop_memory-0.4.13}/loop_memory/serve/watcher.py +0 -0
  84. {loop_memory-0.4.11 → loop_memory-0.4.13}/loop_memory/storage/__init__.py +0 -0
  85. {loop_memory-0.4.11 → loop_memory-0.4.13}/loop_memory/storage/retrieval.py +0 -0
  86. {loop_memory-0.4.11 → loop_memory-0.4.13}/loop_memory/storage/snapshot.py +0 -0
  87. {loop_memory-0.4.11 → loop_memory-0.4.13}/loop_memory/wiki/__init__.py +0 -0
  88. {loop_memory-0.4.11 → loop_memory-0.4.13}/loop_memory/wiki/backfill.py +0 -0
  89. {loop_memory-0.4.11 → loop_memory-0.4.13}/loop_memory/wiki/classifier.py +0 -0
  90. {loop_memory-0.4.11 → loop_memory-0.4.13}/loop_memory/wiki/prompts.py +0 -0
  91. {loop_memory-0.4.11 → loop_memory-0.4.13}/loop_memory/wiki/scope.py +0 -0
  92. {loop_memory-0.4.11 → loop_memory-0.4.13}/loop_memory.egg-info/dependency_links.txt +0 -0
  93. {loop_memory-0.4.11 → loop_memory-0.4.13}/loop_memory.egg-info/entry_points.txt +0 -0
  94. {loop_memory-0.4.11 → loop_memory-0.4.13}/loop_memory.egg-info/requires.txt +0 -0
  95. {loop_memory-0.4.11 → loop_memory-0.4.13}/loop_memory.egg-info/top_level.txt +0 -0
  96. {loop_memory-0.4.11 → loop_memory-0.4.13}/setup.cfg +0 -0
  97. {loop_memory-0.4.11 → loop_memory-0.4.13}/tests/test_admin_ingest_route.py +0 -0
  98. {loop_memory-0.4.11 → loop_memory-0.4.13}/tests/test_agent_memory_api.py +0 -0
  99. {loop_memory-0.4.11 → loop_memory-0.4.13}/tests/test_agent_memory_sdk.py +0 -0
  100. {loop_memory-0.4.11 → loop_memory-0.4.13}/tests/test_auth_token_rotate.py +0 -0
  101. {loop_memory-0.4.11 → loop_memory-0.4.13}/tests/test_busy_timeout_2026_09_24.py +0 -0
  102. {loop_memory-0.4.11 → loop_memory-0.4.13}/tests/test_cache_cap_2026_09_23.py +0 -0
  103. {loop_memory-0.4.11 → loop_memory-0.4.13}/tests/test_cli_audit_supersede.py +0 -0
  104. {loop_memory-0.4.11 → loop_memory-0.4.13}/tests/test_cli_export_okf.py +0 -0
  105. {loop_memory-0.4.11 → loop_memory-0.4.13}/tests/test_cli_init.py +0 -0
  106. {loop_memory-0.4.11 → loop_memory-0.4.13}/tests/test_cli_memory_stats.py +0 -0
  107. {loop_memory-0.4.11 → loop_memory-0.4.13}/tests/test_cli_recall_as_of.py +0 -0
  108. {loop_memory-0.4.11 → loop_memory-0.4.13}/tests/test_cli_recall_paths.py +0 -0
  109. {loop_memory-0.4.11 → loop_memory-0.4.13}/tests/test_cli_recall_verbose.py +0 -0
  110. {loop_memory-0.4.11 → loop_memory-0.4.13}/tests/test_cli_rules.py +0 -0
  111. {loop_memory-0.4.11 → loop_memory-0.4.13}/tests/test_cli_snapshot.py +0 -0
  112. {loop_memory-0.4.11 → loop_memory-0.4.13}/tests/test_cli_v7.py +0 -0
  113. {loop_memory-0.4.11 → loop_memory-0.4.13}/tests/test_cli_version.py +0 -0
  114. {loop_memory-0.4.11 → loop_memory-0.4.13}/tests/test_contradictions.py +0 -0
  115. {loop_memory-0.4.11 → loop_memory-0.4.13}/tests/test_evolution.py +0 -0
  116. {loop_memory-0.4.11 → loop_memory-0.4.13}/tests/test_evolution_quality.py +0 -0
  117. {loop_memory-0.4.11 → loop_memory-0.4.13}/tests/test_expanduser.py +0 -0
  118. {loop_memory-0.4.11 → loop_memory-0.4.13}/tests/test_export_ask.py +0 -0
  119. {loop_memory-0.4.11 → loop_memory-0.4.13}/tests/test_export_okf.py +0 -0
  120. {loop_memory-0.4.11 → loop_memory-0.4.13}/tests/test_graph.py +0 -0
  121. {loop_memory-0.4.11 → loop_memory-0.4.13}/tests/test_healthz_readyz_2026_09_24.py +0 -0
  122. {loop_memory-0.4.11 → loop_memory-0.4.13}/tests/test_ingest.py +0 -0
  123. {loop_memory-0.4.11 → loop_memory-0.4.13}/tests/test_llm_consolidator.py +0 -0
  124. {loop_memory-0.4.11 → loop_memory-0.4.13}/tests/test_llm_fingerprint_not_persisted.py +0 -0
  125. {loop_memory-0.4.11 → loop_memory-0.4.13}/tests/test_llm_providers.py +0 -0
  126. {loop_memory-0.4.11 → loop_memory-0.4.13}/tests/test_llm_test_endpoint.py +0 -0
  127. {loop_memory-0.4.11 → loop_memory-0.4.13}/tests/test_loop.py +0 -0
  128. {loop_memory-0.4.11 → loop_memory-0.4.13}/tests/test_mcp.py +0 -0
  129. {loop_memory-0.4.11 → loop_memory-0.4.13}/tests/test_memories_pagination.py +0 -0
  130. {loop_memory-0.4.11 → loop_memory-0.4.13}/tests/test_memory_stats.py +0 -0
  131. {loop_memory-0.4.11 → loop_memory-0.4.13}/tests/test_openclaw_loader.py +0 -0
  132. {loop_memory-0.4.11 → loop_memory-0.4.13}/tests/test_package_version.py +0 -0
  133. {loop_memory-0.4.11 → loop_memory-0.4.13}/tests/test_path_safety_2026_09_23.py +0 -0
  134. {loop_memory-0.4.11 → loop_memory-0.4.13}/tests/test_recall_as_of.py +0 -0
  135. {loop_memory-0.4.11 → loop_memory-0.4.13}/tests/test_recall_paths.py +0 -0
  136. {loop_memory-0.4.11 → loop_memory-0.4.13}/tests/test_recall_provenance.py +0 -0
  137. {loop_memory-0.4.11 → loop_memory-0.4.13}/tests/test_reflection.py +0 -0
  138. {loop_memory-0.4.11 → loop_memory-0.4.13}/tests/test_repo_framing.py +0 -0
  139. {loop_memory-0.4.11 → loop_memory-0.4.13}/tests/test_routes_audit_2026_09_13.py +0 -0
  140. {loop_memory-0.4.11 → loop_memory-0.4.13}/tests/test_routes_audit_2026_09_20.py +0 -0
  141. {loop_memory-0.4.11 → loop_memory-0.4.13}/tests/test_score_api.py +0 -0
  142. {loop_memory-0.4.11 → loop_memory-0.4.13}/tests/test_scoring_v2.py +0 -0
  143. {loop_memory-0.4.11 → loop_memory-0.4.13}/tests/test_secrets.py +0 -0
  144. {loop_memory-0.4.11 → loop_memory-0.4.13}/tests/test_serve_app.py +0 -0
  145. {loop_memory-0.4.11 → loop_memory-0.4.13}/tests/test_serve_handlers.py +0 -0
  146. {loop_memory-0.4.11 → loop_memory-0.4.13}/tests/test_session_order.py +0 -0
  147. {loop_memory-0.4.11 → loop_memory-0.4.13}/tests/test_short_query.py +0 -0
  148. {loop_memory-0.4.11 → loop_memory-0.4.13}/tests/test_snapshot.py +0 -0
  149. {loop_memory-0.4.11 → loop_memory-0.4.13}/tests/test_store.py +0 -0
  150. {loop_memory-0.4.11 → loop_memory-0.4.13}/tests/test_subgraph_hops_2026_09_23.py +0 -0
  151. {loop_memory-0.4.11 → loop_memory-0.4.13}/tests/test_summarization.py +0 -0
  152. {loop_memory-0.4.11 → loop_memory-0.4.13}/tests/test_universal_memory.py +0 -0
  153. {loop_memory-0.4.11 → loop_memory-0.4.13}/tests/test_vector_store.py +0 -0
  154. {loop_memory-0.4.11 → loop_memory-0.4.13}/tests/test_watcher.py +0 -0
  155. {loop_memory-0.4.11 → loop_memory-0.4.13}/tests/test_wiki_classifier.py +0 -0
  156. {loop_memory-0.4.11 → loop_memory-0.4.13}/tests/test_wiki_export_escape.py +0 -0
  157. {loop_memory-0.4.11 → 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.11
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,10 @@ 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
+
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)
72
+
69
73
  > **What's new in 0.4.11** — **operational probes** +
70
74
  **SQLite write-contention fix** (audit 2026-09-24). New
71
75
  ``GET /api/healthz`` (liveness) + ``GET /api/readyz``
@@ -22,6 +22,10 @@
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
+
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)
28
+
25
29
  > **What's new in 0.4.11** — **operational probes** +
26
30
  **SQLite write-contention fix** (audit 2026-09-24). New
27
31
  ``GET /api/healthz`` (liveness) + ``GET /api/readyz``
@@ -119,6 +119,23 @@ def _build_parser() -> argparse.ArgumentParser:
119
119
  eo.add_argument("--scope", default=None,
120
120
  help="Filter: 'global' or a single source token (e.g. codex)")
121
121
 
122
+ # Audit 2026-09-27: OKF v0.2 bundle import
123
+ # (thecolourfoundation/rune + okf-memory/okf-agent-memory Issues
124
+ # #12-#15 + Deja-Vu HN front-page). Symmetric counterpart to
125
+ # ``export-okf`` (0.4.9): bring an OKF bundle back into Loop
126
+ # Memory as wiki pages. Idempotent upsert on (scope, slug).
127
+ io = sub.add_parser(
128
+ "import-okf",
129
+ help="Import an OKF v0.2 bundle directory as wiki pages")
130
+ io.add_argument("in_dir",
131
+ help="Bundle directory containing index.md and pages/")
132
+ io.add_argument("--scope", default=None,
133
+ help="Scope tag for imported pages (default: bundle's index.md, then 'global')")
134
+ io.add_argument("--dry-run", action="store_true",
135
+ help="Parse every file but do not write")
136
+ io.add_argument("--skip-conflicts", action="store_true",
137
+ help="Don't overwrite an existing (scope, slug) row")
138
+
122
139
  return p
123
140
 
124
141
 
@@ -375,3 +392,32 @@ def run_export_okf(args) -> int:
375
392
  s = MemoryStore(ns.db)
376
393
  r = s.export_okf(ns.out_dir, scope_filter=ns.scope)
377
394
  return _emit(r)
395
+
396
+
397
+ def run_import_okf(args) -> int:
398
+ """``loop-memory import-okf <in_dir> [--scope S] [--dry-run] [--skip-conflicts]``
399
+
400
+ Audit 2026-09-27: OKF v0.2 bundle import
401
+ (thecolourfoundation/rune + okf-memory/okf-agent-memory Issues
402
+ #12-#15 + Deja-Vu HN front-page). Returns the summary dict
403
+ from ``MemoryStore.import_okf`` so the caller can confirm what
404
+ landed where. ``--dry-run`` parses every file but writes nothing;
405
+ ``--skip-conflicts`` turns INSERT-vs-existing conflicts into
406
+ a ``skipped`` counter rather than an UPDATE.
407
+ """
408
+ import argparse as _ap
409
+ p = _ap.ArgumentParser(prog="loop-memory import-okf")
410
+ p.add_argument("in_dir")
411
+ p.add_argument("--scope", default=None)
412
+ p.add_argument("--dry-run", action="store_true")
413
+ p.add_argument("--skip-conflicts", action="store_true")
414
+ p.add_argument("--db", default=os.environ.get("LOOP_MEMORY_DB", DEFAULT_DB))
415
+ ns = p.parse_args(args)
416
+ s = MemoryStore(ns.db)
417
+ r = s.import_okf(
418
+ ns.in_dir,
419
+ scope=ns.scope,
420
+ dry_run=ns.dry_run,
421
+ skip_conflicts=ns.skip_conflicts,
422
+ )
423
+ return _emit(r)
@@ -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
@@ -26,6 +26,7 @@ Usage:
26
26
  loop-memory export <out_dir> # write a MEMORY.md bundle (v7)
27
27
  loop-memory export-bundle <out_dir> # explicit v7 bundle alias
28
28
  loop-memory import <in_dir> # re-hydrate a bundle
29
+ 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)
29
30
  loop-memory fork [--branch-tag T] # snapshot every wiki page
30
31
  loop-memory graph-edge <src> <dst> [--kind K] [--weight W] # push a relation
31
32
  loop-memory subgraph <query> # print a small subgraph
@@ -128,6 +129,8 @@ COMMANDS = {
128
129
  # Audit 2026-09-20 — OKF v0.2 export (akitaonrails/ai-memory 2.0
129
130
  # + okf-agent-memory + Google OKF spec).
130
131
  "export-okf": cognitive_cmd.run_export_okf,
132
+ # Audit 2026-09-27: OKF v0.2 bundle import (rune + okf-agent-memory #12-#15 + Deja-Vu).
133
+ "import-okf": cognitive_cmd.run_import_okf,
131
134
  "rules": rules_cmd.run_rules,
132
135
  "version": _run_version,
133
136
  }
@@ -161,7 +164,7 @@ COMMAND_HELP: dict[str, str] = {
161
164
  "install-hooks": "loop-memory install-hooks # auto-write MCP + SessionStart hooks for known clients.",
162
165
  "mcp": "loop-memory mcp # stdio MCP server (for codex/claude/hermes). Requires ``[serve]`` for HTTP transport.",
163
166
  "openclaw-setup": "loop-memory openclaw-setup # install the openclaw/clawx auto-ingest watcher (launchd on macOS).",
164
- "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).",
165
168
  "rescore": "loop-memory rescore [--half-life 30] # recompute memory scores.",
166
169
  "serve": "loop-memory serve [--port 7767] [--no-browser] # start the local web UI. Requires the ``[serve]`` extra (fastapi + uvicorn).",
167
170
  "stats": "loop-memory stats # counters.",
@@ -172,9 +175,10 @@ COMMAND_HELP: dict[str, str] = {
172
175
  "memory-stats": "loop-memory memory-stats <id> [--prefix] # per-memory lifetime stats (audit 2026-09-06, agentmemory v1.3.0).",
173
176
  "snapshot": "loop-memory snapshot <out.memory.sqlite> # write a portable SQLite snapshot (audit 2026-09-06, codexa-memory v0.2.0).",
174
177
  "export-okf": "loop-memory export-okf <out_dir> [--scope S] # write an OKF v0.2 bundle (audit 2026-09-20).",
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).",
175
179
  "restore": "loop-memory restore <in.memory.sqlite> # re-hydrate a portable SQLite snapshot.",
176
180
  # Audit 2026-09-13 — L0 outline recall (tigerless-labs/agent-memory v0.3.0).
177
- "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).",
178
182
  # Audit 2026-09-13 — per-agent identity bootstrap (Mem0 CLI init --agent pattern).
179
183
  "init": "loop-memory init --agent <name> [--install-hooks] # register an agent + (optionally) install hooks.",
180
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
 
@@ -485,10 +515,54 @@ def register(app: FastAPI, store: MemoryStore, scheduler: Optional[Any] = None,
485
515
  raise HTTPException(500, f"export-okf failed: {e}")
486
516
  return r
487
517
 
518
+ @app.post("/api/import/okf")
519
+ def import_okf(body: dict):
520
+ """Audit 2026-09-27: OKF v0.2 bundle import.
521
+
522
+ Body shape::
523
+
524
+ {
525
+ "in_dir": "/abs/path/to/bundle",
526
+ "scope": "global", # optional
527
+ "dry_run": false, # optional
528
+ "skip_conflicts": false # optional
529
+ }
530
+
531
+ Walks ``<in_dir>/index.md`` + ``<in_dir>/pages/*.md`` and
532
+ upserts each page as a wiki row keyed on ``(scope, slug)``.
533
+ ``in_dir`` is validated by ``_safe_resolve_path`` (live DB
534
+ + system dirs are still refused; per-user temp is OK).
535
+ Returns the same summary dict as the CLI handler.
536
+ """
537
+ from ._shared import _safe_resolve_path as _safe_path
538
+ in_dir = (body.get("in_dir") or "").strip()
539
+ if not in_dir:
540
+ raise HTTPException(400, "in_dir is required")
541
+ scope = body.get("scope")
542
+ if scope is not None:
543
+ scope = str(scope).strip() or None
544
+ dry_run = bool(body.get("dry_run", False))
545
+ skip_conflicts = bool(body.get("skip_conflicts", False))
546
+ try:
547
+ safe_dir = _safe_path(in_dir, kind="read", live_db_path=str(store.path))
548
+ except ValueError as e:
549
+ raise HTTPException(400, str(e))
550
+ try:
551
+ r = store.import_okf(
552
+ safe_dir,
553
+ scope=scope,
554
+ dry_run=dry_run,
555
+ skip_conflicts=skip_conflicts,
556
+ )
557
+ except Exception as e:
558
+ raise HTTPException(500, f"import-okf failed: {e}")
559
+ return r
560
+
488
561
  @app.get("/api/recall/outline")
489
562
  def recall_outline(query: str = "", limit: int = 12,
490
563
  include: str = "memories,wiki,entities",
491
- bump: int = 0, source: str | None = None):
564
+ bump: int = 0, source: str | None = None,
565
+ max_chars: int | None = None):
492
566
  """Audit 2026-09-13 — L0 outline recall (tigerless-labs/agent-memory
493
567
  v0.3.0 recall ladder pattern).
494
568
 
@@ -503,18 +577,32 @@ def register(app: FastAPI, store: MemoryStore, scheduler: Optional[Any] = None,
503
577
  ``recall_count`` before the agent has actually decided to read
504
578
  the body. Set ``bump=1`` only when the agent commits to the
505
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.
506
586
  """
507
587
  if not query:
508
588
  raise HTTPException(status_code=400, detail="query is required")
509
589
  wanted = tuple(s.strip() for s in include.split(",") if s.strip())
510
590
  if not wanted:
511
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
512
599
  r = store.recall_paths(
513
600
  query,
514
601
  limit=limit,
515
602
  include=wanted,
516
603
  bump_signals=bool(bump),
517
604
  source=source,
605
+ max_chars=max_chars,
518
606
  )
519
607
  return {
520
608
  "query": query,
@@ -524,6 +612,7 @@ def register(app: FastAPI, store: MemoryStore, scheduler: Optional[Any] = None,
524
612
  "entities": r.get("entities", []),
525
613
  "mode": "outline",
526
614
  "source": source,
615
+ "max_chars": max_chars,
527
616
  }
528
617
 
529
618