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