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.
- {loop_memory-0.4.11 → loop_memory-0.4.13}/PKG-INFO +5 -1
- {loop_memory-0.4.11 → loop_memory-0.4.13}/README.md +4 -0
- {loop_memory-0.4.11 → loop_memory-0.4.13}/loop_memory/cli/commands/cognitive.py +46 -0
- {loop_memory-0.4.11 → loop_memory-0.4.13}/loop_memory/cli/commands/read.py +44 -7
- {loop_memory-0.4.11 → loop_memory-0.4.13}/loop_memory/cli/main.py +6 -2
- {loop_memory-0.4.11 → loop_memory-0.4.13}/loop_memory/mcp/__init__.py +21 -1
- {loop_memory-0.4.11 → loop_memory-0.4.13}/loop_memory/sdk.py +19 -2
- {loop_memory-0.4.11 → loop_memory-0.4.13}/loop_memory/serve/routes/system.py +91 -2
- {loop_memory-0.4.11 → loop_memory-0.4.13}/loop_memory/storage/sqlite_store.py +886 -3
- {loop_memory-0.4.11 → loop_memory-0.4.13}/loop_memory.egg-info/PKG-INFO +5 -1
- {loop_memory-0.4.11 → loop_memory-0.4.13}/loop_memory.egg-info/SOURCES.txt +3 -0
- {loop_memory-0.4.11 → loop_memory-0.4.13}/pyproject.toml +1 -1
- {loop_memory-0.4.11 → loop_memory-0.4.13}/tests/test_agents_init.py +3 -3
- loop_memory-0.4.13/tests/test_audit_triggers_2026_09_27.py +188 -0
- loop_memory-0.4.13/tests/test_import_okf_2026_09_27.py +425 -0
- loop_memory-0.4.13/tests/test_recall_budget_2026_10_05.py +407 -0
- {loop_memory-0.4.11 → loop_memory-0.4.13}/tests/test_supersession_chain.py +1 -1
- {loop_memory-0.4.11 → loop_memory-0.4.13}/LICENSE +0 -0
- {loop_memory-0.4.11 → loop_memory-0.4.13}/loop_memory/__init__.py +0 -0
- {loop_memory-0.4.11 → loop_memory-0.4.13}/loop_memory/backends/__init__.py +0 -0
- {loop_memory-0.4.11 → loop_memory-0.4.13}/loop_memory/backends/embedding.py +0 -0
- {loop_memory-0.4.11 → loop_memory-0.4.13}/loop_memory/backends/sentence_embedder.py +0 -0
- {loop_memory-0.4.11 → loop_memory-0.4.13}/loop_memory/backends/vector_store.py +0 -0
- {loop_memory-0.4.11 → loop_memory-0.4.13}/loop_memory/cli/__init__.py +0 -0
- {loop_memory-0.4.11 → loop_memory-0.4.13}/loop_memory/cli/_common.py +0 -0
- {loop_memory-0.4.11 → loop_memory-0.4.13}/loop_memory/cli/commands/__init__.py +0 -0
- {loop_memory-0.4.11 → loop_memory-0.4.13}/loop_memory/cli/commands/diag.py +0 -0
- {loop_memory-0.4.11 → loop_memory-0.4.13}/loop_memory/cli/commands/graph.py +0 -0
- {loop_memory-0.4.11 → loop_memory-0.4.13}/loop_memory/cli/commands/hooks.py +0 -0
- {loop_memory-0.4.11 → loop_memory-0.4.13}/loop_memory/cli/commands/rules.py +0 -0
- {loop_memory-0.4.11 → loop_memory-0.4.13}/loop_memory/cli/commands/serve.py +0 -0
- {loop_memory-0.4.11 → loop_memory-0.4.13}/loop_memory/cli/commands/write.py +0 -0
- {loop_memory-0.4.11 → loop_memory-0.4.13}/loop_memory/engine/__init__.py +0 -0
- {loop_memory-0.4.11 → loop_memory-0.4.13}/loop_memory/engine/loop.py +0 -0
- {loop_memory-0.4.11 → loop_memory-0.4.13}/loop_memory/engine/reflect.py +0 -0
- {loop_memory-0.4.11 → loop_memory-0.4.13}/loop_memory/examples/__init__.py +0 -0
- {loop_memory-0.4.11 → loop_memory-0.4.13}/loop_memory/examples/demo.py +0 -0
- {loop_memory-0.4.11 → loop_memory-0.4.13}/loop_memory/export/__init__.py +0 -0
- {loop_memory-0.4.11 → loop_memory-0.4.13}/loop_memory/export/memory_md.py +0 -0
- {loop_memory-0.4.11 → loop_memory-0.4.13}/loop_memory/graph/__init__.py +0 -0
- {loop_memory-0.4.11 → loop_memory-0.4.13}/loop_memory/graph/build.py +0 -0
- {loop_memory-0.4.11 → loop_memory-0.4.13}/loop_memory/graph/extract.py +0 -0
- {loop_memory-0.4.11 → loop_memory-0.4.13}/loop_memory/ingest/__init__.py +0 -0
- {loop_memory-0.4.11 → loop_memory-0.4.13}/loop_memory/ingest/loader.py +0 -0
- {loop_memory-0.4.11 → loop_memory-0.4.13}/loop_memory/ingest/pipeline.py +0 -0
- {loop_memory-0.4.11 → loop_memory-0.4.13}/loop_memory/jobs/__init__.py +0 -0
- {loop_memory-0.4.11 → loop_memory-0.4.13}/loop_memory/jobs/cognitive.py +0 -0
- {loop_memory-0.4.11 → loop_memory-0.4.13}/loop_memory/jobs/compact.py +0 -0
- {loop_memory-0.4.11 → loop_memory-0.4.13}/loop_memory/jobs/consolidate.py +0 -0
- {loop_memory-0.4.11 → loop_memory-0.4.13}/loop_memory/jobs/contradiction.py +0 -0
- {loop_memory-0.4.11 → loop_memory-0.4.13}/loop_memory/jobs/evolution.py +0 -0
- {loop_memory-0.4.11 → loop_memory-0.4.13}/loop_memory/jobs/graph.py +0 -0
- {loop_memory-0.4.11 → loop_memory-0.4.13}/loop_memory/jobs/llm_compact_pass.py +0 -0
- {loop_memory-0.4.11 → loop_memory-0.4.13}/loop_memory/jobs/llm_consolidate.py +0 -0
- {loop_memory-0.4.11 → loop_memory-0.4.13}/loop_memory/jobs/scheduler.py +0 -0
- {loop_memory-0.4.11 → loop_memory-0.4.13}/loop_memory/llm/__init__.py +0 -0
- {loop_memory-0.4.11 → loop_memory-0.4.13}/loop_memory/llm/base.py +0 -0
- {loop_memory-0.4.11 → loop_memory-0.4.13}/loop_memory/llm/openai_adapter.py +0 -0
- {loop_memory-0.4.11 → loop_memory-0.4.13}/loop_memory/llm/providers.py +0 -0
- {loop_memory-0.4.11 → loop_memory-0.4.13}/loop_memory/memory/__init__.py +0 -0
- {loop_memory-0.4.11 → loop_memory-0.4.13}/loop_memory/memory/types.py +0 -0
- {loop_memory-0.4.11 → loop_memory-0.4.13}/loop_memory/privacy/__init__.py +0 -0
- {loop_memory-0.4.11 → loop_memory-0.4.13}/loop_memory/privacy/private.py +0 -0
- {loop_memory-0.4.11 → loop_memory-0.4.13}/loop_memory/privacy/redact.py +0 -0
- {loop_memory-0.4.11 → loop_memory-0.4.13}/loop_memory/py.typed +0 -0
- {loop_memory-0.4.11 → loop_memory-0.4.13}/loop_memory/sdk_extensions.py +0 -0
- {loop_memory-0.4.11 → loop_memory-0.4.13}/loop_memory/security/__init__.py +0 -0
- {loop_memory-0.4.11 → loop_memory-0.4.13}/loop_memory/security/secrets.py +0 -0
- {loop_memory-0.4.11 → loop_memory-0.4.13}/loop_memory/serve/__init__.py +0 -0
- {loop_memory-0.4.11 → loop_memory-0.4.13}/loop_memory/serve/app.py +0 -0
- {loop_memory-0.4.11 → loop_memory-0.4.13}/loop_memory/serve/handlers.py +0 -0
- {loop_memory-0.4.11 → loop_memory-0.4.13}/loop_memory/serve/routes/_shared.py +0 -0
- {loop_memory-0.4.11 → loop_memory-0.4.13}/loop_memory/serve/routes/admin.py +0 -0
- {loop_memory-0.4.11 → loop_memory-0.4.13}/loop_memory/serve/routes/cognitive.py +0 -0
- {loop_memory-0.4.11 → loop_memory-0.4.13}/loop_memory/serve/routes/export.py +0 -0
- {loop_memory-0.4.11 → loop_memory-0.4.13}/loop_memory/serve/routes/graph.py +0 -0
- {loop_memory-0.4.11 → loop_memory-0.4.13}/loop_memory/serve/routes/insights.py +0 -0
- {loop_memory-0.4.11 → loop_memory-0.4.13}/loop_memory/serve/routes/memories.py +0 -0
- {loop_memory-0.4.11 → loop_memory-0.4.13}/loop_memory/serve/routes/sessions.py +0 -0
- {loop_memory-0.4.11 → loop_memory-0.4.13}/loop_memory/serve/routes/wiki.py +0 -0
- {loop_memory-0.4.11 → loop_memory-0.4.13}/loop_memory/serve/static/__init__.py +0 -0
- {loop_memory-0.4.11 → loop_memory-0.4.13}/loop_memory/serve/static/index.html +0 -0
- {loop_memory-0.4.11 → loop_memory-0.4.13}/loop_memory/serve/watcher.py +0 -0
- {loop_memory-0.4.11 → loop_memory-0.4.13}/loop_memory/storage/__init__.py +0 -0
- {loop_memory-0.4.11 → loop_memory-0.4.13}/loop_memory/storage/retrieval.py +0 -0
- {loop_memory-0.4.11 → loop_memory-0.4.13}/loop_memory/storage/snapshot.py +0 -0
- {loop_memory-0.4.11 → loop_memory-0.4.13}/loop_memory/wiki/__init__.py +0 -0
- {loop_memory-0.4.11 → loop_memory-0.4.13}/loop_memory/wiki/backfill.py +0 -0
- {loop_memory-0.4.11 → loop_memory-0.4.13}/loop_memory/wiki/classifier.py +0 -0
- {loop_memory-0.4.11 → loop_memory-0.4.13}/loop_memory/wiki/prompts.py +0 -0
- {loop_memory-0.4.11 → loop_memory-0.4.13}/loop_memory/wiki/scope.py +0 -0
- {loop_memory-0.4.11 → loop_memory-0.4.13}/loop_memory.egg-info/dependency_links.txt +0 -0
- {loop_memory-0.4.11 → loop_memory-0.4.13}/loop_memory.egg-info/entry_points.txt +0 -0
- {loop_memory-0.4.11 → loop_memory-0.4.13}/loop_memory.egg-info/requires.txt +0 -0
- {loop_memory-0.4.11 → loop_memory-0.4.13}/loop_memory.egg-info/top_level.txt +0 -0
- {loop_memory-0.4.11 → loop_memory-0.4.13}/setup.cfg +0 -0
- {loop_memory-0.4.11 → loop_memory-0.4.13}/tests/test_admin_ingest_route.py +0 -0
- {loop_memory-0.4.11 → loop_memory-0.4.13}/tests/test_agent_memory_api.py +0 -0
- {loop_memory-0.4.11 → loop_memory-0.4.13}/tests/test_agent_memory_sdk.py +0 -0
- {loop_memory-0.4.11 → loop_memory-0.4.13}/tests/test_auth_token_rotate.py +0 -0
- {loop_memory-0.4.11 → loop_memory-0.4.13}/tests/test_busy_timeout_2026_09_24.py +0 -0
- {loop_memory-0.4.11 → loop_memory-0.4.13}/tests/test_cache_cap_2026_09_23.py +0 -0
- {loop_memory-0.4.11 → loop_memory-0.4.13}/tests/test_cli_audit_supersede.py +0 -0
- {loop_memory-0.4.11 → loop_memory-0.4.13}/tests/test_cli_export_okf.py +0 -0
- {loop_memory-0.4.11 → loop_memory-0.4.13}/tests/test_cli_init.py +0 -0
- {loop_memory-0.4.11 → loop_memory-0.4.13}/tests/test_cli_memory_stats.py +0 -0
- {loop_memory-0.4.11 → loop_memory-0.4.13}/tests/test_cli_recall_as_of.py +0 -0
- {loop_memory-0.4.11 → loop_memory-0.4.13}/tests/test_cli_recall_paths.py +0 -0
- {loop_memory-0.4.11 → loop_memory-0.4.13}/tests/test_cli_recall_verbose.py +0 -0
- {loop_memory-0.4.11 → loop_memory-0.4.13}/tests/test_cli_rules.py +0 -0
- {loop_memory-0.4.11 → loop_memory-0.4.13}/tests/test_cli_snapshot.py +0 -0
- {loop_memory-0.4.11 → loop_memory-0.4.13}/tests/test_cli_v7.py +0 -0
- {loop_memory-0.4.11 → loop_memory-0.4.13}/tests/test_cli_version.py +0 -0
- {loop_memory-0.4.11 → loop_memory-0.4.13}/tests/test_contradictions.py +0 -0
- {loop_memory-0.4.11 → loop_memory-0.4.13}/tests/test_evolution.py +0 -0
- {loop_memory-0.4.11 → loop_memory-0.4.13}/tests/test_evolution_quality.py +0 -0
- {loop_memory-0.4.11 → loop_memory-0.4.13}/tests/test_expanduser.py +0 -0
- {loop_memory-0.4.11 → loop_memory-0.4.13}/tests/test_export_ask.py +0 -0
- {loop_memory-0.4.11 → loop_memory-0.4.13}/tests/test_export_okf.py +0 -0
- {loop_memory-0.4.11 → loop_memory-0.4.13}/tests/test_graph.py +0 -0
- {loop_memory-0.4.11 → loop_memory-0.4.13}/tests/test_healthz_readyz_2026_09_24.py +0 -0
- {loop_memory-0.4.11 → loop_memory-0.4.13}/tests/test_ingest.py +0 -0
- {loop_memory-0.4.11 → loop_memory-0.4.13}/tests/test_llm_consolidator.py +0 -0
- {loop_memory-0.4.11 → loop_memory-0.4.13}/tests/test_llm_fingerprint_not_persisted.py +0 -0
- {loop_memory-0.4.11 → loop_memory-0.4.13}/tests/test_llm_providers.py +0 -0
- {loop_memory-0.4.11 → loop_memory-0.4.13}/tests/test_llm_test_endpoint.py +0 -0
- {loop_memory-0.4.11 → loop_memory-0.4.13}/tests/test_loop.py +0 -0
- {loop_memory-0.4.11 → loop_memory-0.4.13}/tests/test_mcp.py +0 -0
- {loop_memory-0.4.11 → loop_memory-0.4.13}/tests/test_memories_pagination.py +0 -0
- {loop_memory-0.4.11 → loop_memory-0.4.13}/tests/test_memory_stats.py +0 -0
- {loop_memory-0.4.11 → loop_memory-0.4.13}/tests/test_openclaw_loader.py +0 -0
- {loop_memory-0.4.11 → loop_memory-0.4.13}/tests/test_package_version.py +0 -0
- {loop_memory-0.4.11 → loop_memory-0.4.13}/tests/test_path_safety_2026_09_23.py +0 -0
- {loop_memory-0.4.11 → loop_memory-0.4.13}/tests/test_recall_as_of.py +0 -0
- {loop_memory-0.4.11 → loop_memory-0.4.13}/tests/test_recall_paths.py +0 -0
- {loop_memory-0.4.11 → loop_memory-0.4.13}/tests/test_recall_provenance.py +0 -0
- {loop_memory-0.4.11 → loop_memory-0.4.13}/tests/test_reflection.py +0 -0
- {loop_memory-0.4.11 → loop_memory-0.4.13}/tests/test_repo_framing.py +0 -0
- {loop_memory-0.4.11 → loop_memory-0.4.13}/tests/test_routes_audit_2026_09_13.py +0 -0
- {loop_memory-0.4.11 → loop_memory-0.4.13}/tests/test_routes_audit_2026_09_20.py +0 -0
- {loop_memory-0.4.11 → loop_memory-0.4.13}/tests/test_score_api.py +0 -0
- {loop_memory-0.4.11 → loop_memory-0.4.13}/tests/test_scoring_v2.py +0 -0
- {loop_memory-0.4.11 → loop_memory-0.4.13}/tests/test_secrets.py +0 -0
- {loop_memory-0.4.11 → loop_memory-0.4.13}/tests/test_serve_app.py +0 -0
- {loop_memory-0.4.11 → loop_memory-0.4.13}/tests/test_serve_handlers.py +0 -0
- {loop_memory-0.4.11 → loop_memory-0.4.13}/tests/test_session_order.py +0 -0
- {loop_memory-0.4.11 → loop_memory-0.4.13}/tests/test_short_query.py +0 -0
- {loop_memory-0.4.11 → loop_memory-0.4.13}/tests/test_snapshot.py +0 -0
- {loop_memory-0.4.11 → loop_memory-0.4.13}/tests/test_store.py +0 -0
- {loop_memory-0.4.11 → loop_memory-0.4.13}/tests/test_subgraph_hops_2026_09_23.py +0 -0
- {loop_memory-0.4.11 → loop_memory-0.4.13}/tests/test_summarization.py +0 -0
- {loop_memory-0.4.11 → loop_memory-0.4.13}/tests/test_universal_memory.py +0 -0
- {loop_memory-0.4.11 → loop_memory-0.4.13}/tests/test_vector_store.py +0 -0
- {loop_memory-0.4.11 → loop_memory-0.4.13}/tests/test_watcher.py +0 -0
- {loop_memory-0.4.11 → loop_memory-0.4.13}/tests/test_wiki_classifier.py +0 -0
- {loop_memory-0.4.11 → loop_memory-0.4.13}/tests/test_wiki_export_escape.py +0 -0
- {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.
|
|
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
|
-
|
|
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=
|
|
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
|
|