loop-memory 0.4.14__tar.gz → 0.4.16__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.14 → loop_memory-0.4.16}/PKG-INFO +45 -2
- {loop_memory-0.4.14 → loop_memory-0.4.16}/README.md +44 -1
- {loop_memory-0.4.14 → loop_memory-0.4.16}/loop_memory/cli/commands/read.py +38 -2
- {loop_memory-0.4.14 → loop_memory-0.4.16}/loop_memory/cli/commands/write.py +94 -1
- {loop_memory-0.4.14 → loop_memory-0.4.16}/loop_memory/cli/main.py +6 -1
- loop_memory-0.4.16/loop_memory/decorators.py +243 -0
- loop_memory-0.4.16/loop_memory/jobs/dream.py +532 -0
- {loop_memory-0.4.14 → loop_memory-0.4.16}/loop_memory/sdk.py +33 -0
- {loop_memory-0.4.14 → loop_memory-0.4.16}/loop_memory/serve/handlers.py +5 -0
- {loop_memory-0.4.14 → loop_memory-0.4.16}/loop_memory/serve/routes/cognitive.py +39 -0
- {loop_memory-0.4.14 → loop_memory-0.4.16}/loop_memory/serve/routes/memories.py +22 -3
- {loop_memory-0.4.14 → loop_memory-0.4.16}/loop_memory/serve/routes/system.py +50 -15
- {loop_memory-0.4.14 → loop_memory-0.4.16}/loop_memory/storage/retrieval.py +105 -6
- {loop_memory-0.4.14 → loop_memory-0.4.16}/loop_memory/storage/sqlite_store.py +793 -25
- {loop_memory-0.4.14 → loop_memory-0.4.16}/loop_memory.egg-info/PKG-INFO +45 -2
- {loop_memory-0.4.14 → loop_memory-0.4.16}/loop_memory.egg-info/SOURCES.txt +10 -0
- {loop_memory-0.4.14 → loop_memory-0.4.16}/pyproject.toml +1 -1
- {loop_memory-0.4.14 → loop_memory-0.4.16}/tests/test_agents_init.py +2 -2
- loop_memory-0.4.16/tests/test_content_hash_dedup_2026_10_18.py +437 -0
- loop_memory-0.4.16/tests/test_decorators_2026_10_18.py +279 -0
- loop_memory-0.4.16/tests/test_dream_2026_10_25.py +310 -0
- loop_memory-0.4.16/tests/test_dream_cli_http_2026_10_25.py +287 -0
- {loop_memory-0.4.14 → loop_memory-0.4.16}/tests/test_evolution_quality.py +12 -2
- loop_memory-0.4.16/tests/test_latest_only_2026_10_25.py +208 -0
- {loop_memory-0.4.14 → loop_memory-0.4.16}/tests/test_memory_stats.py +3 -0
- loop_memory-0.4.16/tests/test_recall_validity_http_2026_10_18.py +169 -0
- loop_memory-0.4.16/tests/test_retrieval_channels_2026_10_18.py +216 -0
- {loop_memory-0.4.14 → loop_memory-0.4.16}/tests/test_store.py +11 -2
- loop_memory-0.4.16/tests/test_subject_key_2026_10_25.py +248 -0
- {loop_memory-0.4.14 → loop_memory-0.4.16}/tests/test_supersession_chain.py +5 -2
- {loop_memory-0.4.14 → loop_memory-0.4.16}/LICENSE +0 -0
- {loop_memory-0.4.14 → loop_memory-0.4.16}/loop_memory/__init__.py +0 -0
- {loop_memory-0.4.14 → loop_memory-0.4.16}/loop_memory/backends/__init__.py +0 -0
- {loop_memory-0.4.14 → loop_memory-0.4.16}/loop_memory/backends/embedding.py +0 -0
- {loop_memory-0.4.14 → loop_memory-0.4.16}/loop_memory/backends/sentence_embedder.py +0 -0
- {loop_memory-0.4.14 → loop_memory-0.4.16}/loop_memory/backends/vector_store.py +0 -0
- {loop_memory-0.4.14 → loop_memory-0.4.16}/loop_memory/cli/__init__.py +0 -0
- {loop_memory-0.4.14 → loop_memory-0.4.16}/loop_memory/cli/_common.py +0 -0
- {loop_memory-0.4.14 → loop_memory-0.4.16}/loop_memory/cli/commands/__init__.py +0 -0
- {loop_memory-0.4.14 → loop_memory-0.4.16}/loop_memory/cli/commands/cognitive.py +0 -0
- {loop_memory-0.4.14 → loop_memory-0.4.16}/loop_memory/cli/commands/diag.py +0 -0
- {loop_memory-0.4.14 → loop_memory-0.4.16}/loop_memory/cli/commands/doctor.py +0 -0
- {loop_memory-0.4.14 → loop_memory-0.4.16}/loop_memory/cli/commands/graph.py +0 -0
- {loop_memory-0.4.14 → loop_memory-0.4.16}/loop_memory/cli/commands/hooks.py +0 -0
- {loop_memory-0.4.14 → loop_memory-0.4.16}/loop_memory/cli/commands/rules.py +0 -0
- {loop_memory-0.4.14 → loop_memory-0.4.16}/loop_memory/cli/commands/serve.py +0 -0
- {loop_memory-0.4.14 → loop_memory-0.4.16}/loop_memory/doctor.py +0 -0
- {loop_memory-0.4.14 → loop_memory-0.4.16}/loop_memory/engine/__init__.py +0 -0
- {loop_memory-0.4.14 → loop_memory-0.4.16}/loop_memory/engine/loop.py +0 -0
- {loop_memory-0.4.14 → loop_memory-0.4.16}/loop_memory/engine/reflect.py +0 -0
- {loop_memory-0.4.14 → loop_memory-0.4.16}/loop_memory/examples/__init__.py +0 -0
- {loop_memory-0.4.14 → loop_memory-0.4.16}/loop_memory/examples/demo.py +0 -0
- {loop_memory-0.4.14 → loop_memory-0.4.16}/loop_memory/export/__init__.py +0 -0
- {loop_memory-0.4.14 → loop_memory-0.4.16}/loop_memory/export/memory_md.py +0 -0
- {loop_memory-0.4.14 → loop_memory-0.4.16}/loop_memory/graph/__init__.py +0 -0
- {loop_memory-0.4.14 → loop_memory-0.4.16}/loop_memory/graph/build.py +0 -0
- {loop_memory-0.4.14 → loop_memory-0.4.16}/loop_memory/graph/extract.py +0 -0
- {loop_memory-0.4.14 → loop_memory-0.4.16}/loop_memory/ingest/__init__.py +0 -0
- {loop_memory-0.4.14 → loop_memory-0.4.16}/loop_memory/ingest/loader.py +0 -0
- {loop_memory-0.4.14 → loop_memory-0.4.16}/loop_memory/ingest/pipeline.py +0 -0
- {loop_memory-0.4.14 → loop_memory-0.4.16}/loop_memory/jobs/__init__.py +0 -0
- {loop_memory-0.4.14 → loop_memory-0.4.16}/loop_memory/jobs/cognitive.py +0 -0
- {loop_memory-0.4.14 → loop_memory-0.4.16}/loop_memory/jobs/compact.py +0 -0
- {loop_memory-0.4.14 → loop_memory-0.4.16}/loop_memory/jobs/consolidate.py +0 -0
- {loop_memory-0.4.14 → loop_memory-0.4.16}/loop_memory/jobs/contradiction.py +0 -0
- {loop_memory-0.4.14 → loop_memory-0.4.16}/loop_memory/jobs/evolution.py +0 -0
- {loop_memory-0.4.14 → loop_memory-0.4.16}/loop_memory/jobs/graph.py +0 -0
- {loop_memory-0.4.14 → loop_memory-0.4.16}/loop_memory/jobs/llm_compact_pass.py +0 -0
- {loop_memory-0.4.14 → loop_memory-0.4.16}/loop_memory/jobs/llm_consolidate.py +0 -0
- {loop_memory-0.4.14 → loop_memory-0.4.16}/loop_memory/jobs/scheduler.py +0 -0
- {loop_memory-0.4.14 → loop_memory-0.4.16}/loop_memory/llm/__init__.py +0 -0
- {loop_memory-0.4.14 → loop_memory-0.4.16}/loop_memory/llm/base.py +0 -0
- {loop_memory-0.4.14 → loop_memory-0.4.16}/loop_memory/llm/openai_adapter.py +0 -0
- {loop_memory-0.4.14 → loop_memory-0.4.16}/loop_memory/llm/providers.py +0 -0
- {loop_memory-0.4.14 → loop_memory-0.4.16}/loop_memory/mcp/__init__.py +0 -0
- {loop_memory-0.4.14 → loop_memory-0.4.16}/loop_memory/memory/__init__.py +0 -0
- {loop_memory-0.4.14 → loop_memory-0.4.16}/loop_memory/memory/types.py +0 -0
- {loop_memory-0.4.14 → loop_memory-0.4.16}/loop_memory/privacy/__init__.py +0 -0
- {loop_memory-0.4.14 → loop_memory-0.4.16}/loop_memory/privacy/private.py +0 -0
- {loop_memory-0.4.14 → loop_memory-0.4.16}/loop_memory/privacy/redact.py +0 -0
- {loop_memory-0.4.14 → loop_memory-0.4.16}/loop_memory/py.typed +0 -0
- {loop_memory-0.4.14 → loop_memory-0.4.16}/loop_memory/sdk_extensions.py +0 -0
- {loop_memory-0.4.14 → loop_memory-0.4.16}/loop_memory/security/__init__.py +0 -0
- {loop_memory-0.4.14 → loop_memory-0.4.16}/loop_memory/security/secrets.py +0 -0
- {loop_memory-0.4.14 → loop_memory-0.4.16}/loop_memory/serve/__init__.py +0 -0
- {loop_memory-0.4.14 → loop_memory-0.4.16}/loop_memory/serve/app.py +0 -0
- {loop_memory-0.4.14 → loop_memory-0.4.16}/loop_memory/serve/routes/_shared.py +0 -0
- {loop_memory-0.4.14 → loop_memory-0.4.16}/loop_memory/serve/routes/admin.py +0 -0
- {loop_memory-0.4.14 → loop_memory-0.4.16}/loop_memory/serve/routes/export.py +0 -0
- {loop_memory-0.4.14 → loop_memory-0.4.16}/loop_memory/serve/routes/graph.py +0 -0
- {loop_memory-0.4.14 → loop_memory-0.4.16}/loop_memory/serve/routes/insights.py +0 -0
- {loop_memory-0.4.14 → loop_memory-0.4.16}/loop_memory/serve/routes/sessions.py +0 -0
- {loop_memory-0.4.14 → loop_memory-0.4.16}/loop_memory/serve/routes/wiki.py +0 -0
- {loop_memory-0.4.14 → loop_memory-0.4.16}/loop_memory/serve/static/__init__.py +0 -0
- {loop_memory-0.4.14 → loop_memory-0.4.16}/loop_memory/serve/static/index.html +0 -0
- {loop_memory-0.4.14 → loop_memory-0.4.16}/loop_memory/serve/watcher.py +0 -0
- {loop_memory-0.4.14 → loop_memory-0.4.16}/loop_memory/storage/__init__.py +0 -0
- {loop_memory-0.4.14 → loop_memory-0.4.16}/loop_memory/storage/snapshot.py +0 -0
- {loop_memory-0.4.14 → loop_memory-0.4.16}/loop_memory/wiki/__init__.py +0 -0
- {loop_memory-0.4.14 → loop_memory-0.4.16}/loop_memory/wiki/backfill.py +0 -0
- {loop_memory-0.4.14 → loop_memory-0.4.16}/loop_memory/wiki/classifier.py +0 -0
- {loop_memory-0.4.14 → loop_memory-0.4.16}/loop_memory/wiki/prompts.py +0 -0
- {loop_memory-0.4.14 → loop_memory-0.4.16}/loop_memory/wiki/scope.py +0 -0
- {loop_memory-0.4.14 → loop_memory-0.4.16}/loop_memory.egg-info/dependency_links.txt +0 -0
- {loop_memory-0.4.14 → loop_memory-0.4.16}/loop_memory.egg-info/entry_points.txt +0 -0
- {loop_memory-0.4.14 → loop_memory-0.4.16}/loop_memory.egg-info/requires.txt +0 -0
- {loop_memory-0.4.14 → loop_memory-0.4.16}/loop_memory.egg-info/top_level.txt +0 -0
- {loop_memory-0.4.14 → loop_memory-0.4.16}/setup.cfg +0 -0
- {loop_memory-0.4.14 → loop_memory-0.4.16}/tests/test_admin_ingest_route.py +0 -0
- {loop_memory-0.4.14 → loop_memory-0.4.16}/tests/test_agent_memory_api.py +0 -0
- {loop_memory-0.4.14 → loop_memory-0.4.16}/tests/test_agent_memory_sdk.py +0 -0
- {loop_memory-0.4.14 → loop_memory-0.4.16}/tests/test_audit_triggers_2026_09_27.py +0 -0
- {loop_memory-0.4.14 → loop_memory-0.4.16}/tests/test_auth_token_rotate.py +0 -0
- {loop_memory-0.4.14 → loop_memory-0.4.16}/tests/test_busy_timeout_2026_09_24.py +0 -0
- {loop_memory-0.4.14 → loop_memory-0.4.16}/tests/test_cache_cap_2026_09_23.py +0 -0
- {loop_memory-0.4.14 → loop_memory-0.4.16}/tests/test_cli_audit_supersede.py +0 -0
- {loop_memory-0.4.14 → loop_memory-0.4.16}/tests/test_cli_export_okf.py +0 -0
- {loop_memory-0.4.14 → loop_memory-0.4.16}/tests/test_cli_init.py +0 -0
- {loop_memory-0.4.14 → loop_memory-0.4.16}/tests/test_cli_memory_stats.py +0 -0
- {loop_memory-0.4.14 → loop_memory-0.4.16}/tests/test_cli_recall_as_of.py +0 -0
- {loop_memory-0.4.14 → loop_memory-0.4.16}/tests/test_cli_recall_paths.py +0 -0
- {loop_memory-0.4.14 → loop_memory-0.4.16}/tests/test_cli_recall_verbose.py +0 -0
- {loop_memory-0.4.14 → loop_memory-0.4.16}/tests/test_cli_rules.py +0 -0
- {loop_memory-0.4.14 → loop_memory-0.4.16}/tests/test_cli_snapshot.py +0 -0
- {loop_memory-0.4.14 → loop_memory-0.4.16}/tests/test_cli_v7.py +0 -0
- {loop_memory-0.4.14 → loop_memory-0.4.16}/tests/test_cli_version.py +0 -0
- {loop_memory-0.4.14 → loop_memory-0.4.16}/tests/test_contradictions.py +0 -0
- {loop_memory-0.4.14 → loop_memory-0.4.16}/tests/test_doctor_2026_10_12.py +0 -0
- {loop_memory-0.4.14 → loop_memory-0.4.16}/tests/test_evolution.py +0 -0
- {loop_memory-0.4.14 → loop_memory-0.4.16}/tests/test_expanduser.py +0 -0
- {loop_memory-0.4.14 → loop_memory-0.4.16}/tests/test_export_ask.py +0 -0
- {loop_memory-0.4.14 → loop_memory-0.4.16}/tests/test_export_okf.py +0 -0
- {loop_memory-0.4.14 → loop_memory-0.4.16}/tests/test_graph.py +0 -0
- {loop_memory-0.4.14 → loop_memory-0.4.16}/tests/test_healthz_readyz_2026_09_24.py +0 -0
- {loop_memory-0.4.14 → loop_memory-0.4.16}/tests/test_import_okf_2026_09_27.py +0 -0
- {loop_memory-0.4.14 → loop_memory-0.4.16}/tests/test_ingest.py +0 -0
- {loop_memory-0.4.14 → loop_memory-0.4.16}/tests/test_llm_consolidator.py +0 -0
- {loop_memory-0.4.14 → loop_memory-0.4.16}/tests/test_llm_fingerprint_not_persisted.py +0 -0
- {loop_memory-0.4.14 → loop_memory-0.4.16}/tests/test_llm_providers.py +0 -0
- {loop_memory-0.4.14 → loop_memory-0.4.16}/tests/test_llm_test_endpoint.py +0 -0
- {loop_memory-0.4.14 → loop_memory-0.4.16}/tests/test_loop.py +0 -0
- {loop_memory-0.4.14 → loop_memory-0.4.16}/tests/test_mcp.py +0 -0
- {loop_memory-0.4.14 → loop_memory-0.4.16}/tests/test_memories_pagination.py +0 -0
- {loop_memory-0.4.14 → loop_memory-0.4.16}/tests/test_openclaw_loader.py +0 -0
- {loop_memory-0.4.14 → loop_memory-0.4.16}/tests/test_package_version.py +0 -0
- {loop_memory-0.4.14 → loop_memory-0.4.16}/tests/test_path_safety_2026_09_23.py +0 -0
- {loop_memory-0.4.14 → loop_memory-0.4.16}/tests/test_recall_as_of.py +0 -0
- {loop_memory-0.4.14 → loop_memory-0.4.16}/tests/test_recall_budget_2026_10_05.py +0 -0
- {loop_memory-0.4.14 → loop_memory-0.4.16}/tests/test_recall_paths.py +0 -0
- {loop_memory-0.4.14 → loop_memory-0.4.16}/tests/test_recall_provenance.py +0 -0
- {loop_memory-0.4.14 → loop_memory-0.4.16}/tests/test_reflection.py +0 -0
- {loop_memory-0.4.14 → loop_memory-0.4.16}/tests/test_repo_framing.py +0 -0
- {loop_memory-0.4.14 → loop_memory-0.4.16}/tests/test_routes_audit_2026_09_13.py +0 -0
- {loop_memory-0.4.14 → loop_memory-0.4.16}/tests/test_routes_audit_2026_09_20.py +0 -0
- {loop_memory-0.4.14 → loop_memory-0.4.16}/tests/test_score_api.py +0 -0
- {loop_memory-0.4.14 → loop_memory-0.4.16}/tests/test_scoring_v2.py +0 -0
- {loop_memory-0.4.14 → loop_memory-0.4.16}/tests/test_secrets.py +0 -0
- {loop_memory-0.4.14 → loop_memory-0.4.16}/tests/test_serve_app.py +0 -0
- {loop_memory-0.4.14 → loop_memory-0.4.16}/tests/test_serve_handlers.py +0 -0
- {loop_memory-0.4.14 → loop_memory-0.4.16}/tests/test_session_order.py +0 -0
- {loop_memory-0.4.14 → loop_memory-0.4.16}/tests/test_short_query.py +0 -0
- {loop_memory-0.4.14 → loop_memory-0.4.16}/tests/test_snapshot.py +0 -0
- {loop_memory-0.4.14 → loop_memory-0.4.16}/tests/test_subgraph_hops_2026_09_23.py +0 -0
- {loop_memory-0.4.14 → loop_memory-0.4.16}/tests/test_summarization.py +0 -0
- {loop_memory-0.4.14 → loop_memory-0.4.16}/tests/test_universal_memory.py +0 -0
- {loop_memory-0.4.14 → loop_memory-0.4.16}/tests/test_vector_store.py +0 -0
- {loop_memory-0.4.14 → loop_memory-0.4.16}/tests/test_watcher.py +0 -0
- {loop_memory-0.4.14 → loop_memory-0.4.16}/tests/test_wiki_classifier.py +0 -0
- {loop_memory-0.4.14 → loop_memory-0.4.16}/tests/test_wiki_export_escape.py +0 -0
- {loop_memory-0.4.14 → loop_memory-0.4.16}/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.16
|
|
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.16** — **subject-key logical fact slots + `latest_only` recall + the "dream" sleep-time consolidation pass** (audit 2026-10-25, weekly ecosystem sync — Mem0 v2.2.1 "Dream", arXiv 2605.27366 "In Defense of Forgotten", Zep/Graphiti temporal edges, Letta memory blocks). New nullable `subject_key` column on `memories` names the *logical slot* a fact occupies (`"editor.preference"`); `upsert_memory(subject_key=...)` makes the new row the live version and soft-supersedes the previous one with a `superseded_by` pointer, so the version history survives and `recall_as_of()` can still answer "what did we believe last week?". The content-hash dedup path is now slot-aware — two different slots holding the same text stay two rows. `SCHEMA_VERSION` bumps 12 → 13 with an idempotent in-place migration. New `latest_only=False` flag on `recall()` / `recall_hybrid()` (Mem0 v2 `SearchMemoryOptions.latest_only` parity) collapses the answer in three layers: rows outside their validity window at *now* are dropped, a subject-key slot resolves to its newest **live** row (looked up in the store, so a stale version never comes back just because it matched the query better), and near-duplicate texts cluster by token Jaccard (default 0.85) with the newest row winning. Wired through `GET /api/recall?latest_only=1`, `GET /api/v1/recall?latest_only=1`, `loop-memory recall --latest-only` and `MemoryClient.recall(latest_only=True)`. New `loop_memory/jobs/dream.py` implements the sleep-time consolidation Mem0 v2 calls the "Dream" — deterministic, zero-dep and **non-destructive**: it decays scores, then judges every row past its deadline on **utility** (`0.45 × importance + 0.30 × usage + 0.25 × recency`) so a frequently-recalled old fact is *protected* while a never-used stale row is **soft-expired** (`invalid_at` stamped, row kept, history kept) instead of deleted, and finally merges near-duplicates into a supersession chain. Run it with `loop-memory dream [--apply] [--json]` or `POST /api/v1/dream` — it is a **dry run by default** and every decision lands in `cognitive_audit` so the dashboard can show and revert it. New store primitives `expire_memory()` / `revive_memory()`, and `memory_stats()` now reports `subject_key`, `ttl`, `valid_at`, `invalid_at` and a derived `ttl_state`. **Also fixed a real defect**: the hybrid recall path (BM25 / LIKE / semantic channels) did not filter `superseded_by`, so `recall_hybrid()` — the production path behind the CLI, MCP server and dashboard — returned both versions of a superseded fact. Source: Mem0 v2.2.1 Dream + ADD-only architecture + arXiv 2605.27366 utility-aware eviction + Zep/Graphiti bi-temporal edges (pattern only, no code copied). 77 new regression cases. [Full CHANGELOG →](./CHANGELOG.md#0416---2026-10-25)
|
|
70
|
+
> **What's new in 0.4.15** — **content-hash dedup + bi-temporal validity window + evidence chain + `@agent_memory` decorator** (audit 2026-10-18, weekly ecosystem sync). Three new columns on `memories`: `content_hash` (SHA-256 of normalised text, indexed), `valid_at` (epoch the memory becomes active), `invalid_at` (epoch it expires — NULL means still open-ended). The schema migration bumps `SCHEMA_VERSION` 11 → 12 with an idempotent backfill so legacy DBs upgrade in-place. New `upsert_memory(valid_at=, invalid_at=, dedup=)` parameters let callers schedule future-dated or expiring memories and opt out of content-hash dedup for episodic event streams. `recall()` and `recall_hybrid()` now accept an optional `as_of_ts` (float epoch or ISO-8601) — when set, both methods drop memories whose `valid_at > as_of` or `invalid_at <= as_of`, complementing the existing `recall_as_of()` supersession-chain walk. Every recall hit now carries an `evidence: [{field, value, weight}]` array — BM25 / semantic / entity / rrf_total / high_importance channels are exposed individually so a UI can render "this hit because ... [bm25 1.0] [entity 0.7]" instead of a black-box score (Cognee 1.0 / Mem0 v2 evidence-citation pattern, zero-dep). New `loop_memory.decorators` exposes `@agent_memory(store, agent_id=, limit=)` — a one-line wrapper that recalls the top-k relevant memories before the body runs and writes a fresh `kind="interaction"` memory after. Strings returned from the wrapped function are stored verbatim; dict/list returns are JSON-serialised; the recall list is injected as a `memory_context` keyword when the function declares it, otherwise attached to dict results under `memories`. Sister `remember(store, text, ...)` helper for seeding. The BM25 `_like_fallback` now falls through to per-token OR-LIKE when the whole-string match returns nothing — natural-language questions like "What is the deploy process?" now hit stored facts even when FTS5 trigram can't match quoted exact phrases. Source: Cognee 1.0 ECL pipeline + Mem0 v2 hybrid fusion + Zep/Graphiti bi-temporal validity fields + Letta block-memory (pattern only, no code copied). 59 new regression cases. [Full CHANGELOG →](./CHANGELOG.md#0415---2026-10-18)
|
|
69
71
|
> **What's new in 0.4.14** — **`loop-memory doctor` deep store-health check** (audit 2026-10-12). Ten diagnostic checks — `db_exists`, `db_integrity`, `schema_version`, `audit_triggers`, `audit_log_writable`, `audit_log_size`, `hooks_installed`, `version_consistent`, `wiki_no_orphans`, `recall_paths_works` — report `ok`/`info`/`warn`/`error` for the live `MemoryStore`. `--fix` auto-repairs the `audit_triggers` check. `--json` emits a stable shape for CI. `--list` prints the check function names without touching the store. `--verbose` shows `info`-level checks. Exit code `1` on any `error` status, `0` otherwise. Source: `cortexkit/magic-context doctor` auto-diagnostic pattern (MIT, 2026-10-12) + `dsh-memento` write-approval-gate pattern (Apache-2.0, 2026-10-12). Stdlib-only — we re-implement from scratch against our own `MemoryStore`. Also fixed a Python 3.14 bug: chained `store._conn().__enter__()` immediately triggers the contextmanager's `finally` and closes the connection mid-query — both call sites in `doctor.py` now use explicit `with` form. 27 new regression cases. [Full CHANGELOG →](./CHANGELOG.md#0414---2026-10-12)
|
|
70
72
|
|
|
71
73
|
> **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)
|
|
@@ -245,6 +247,10 @@ loop-memory serve --port 7767 # open http://127.0.0.1:7767
|
|
|
245
247
|
# 3. Make it run on a timer
|
|
246
248
|
# (see docs/auto-capture.md for launchd / systemd / cron snippets)
|
|
247
249
|
loop-memory consolidate # rescore + GC + dedupe
|
|
250
|
+
|
|
251
|
+
# 4. Optional: sleep-time consolidation (soft expiry, never deletes)
|
|
252
|
+
loop-memory dream # dry run -- what would expire / merge
|
|
253
|
+
loop-memory dream --apply # write it (soft-expire + supersede)
|
|
248
254
|
```
|
|
249
255
|
|
|
250
256
|
---
|
|
@@ -446,6 +452,43 @@ User signals close the loop:
|
|
|
446
452
|
- Every `recall()` / search bumps `recall_count` on the returned
|
|
447
453
|
rows so the next Stage-1 ranks them higher.
|
|
448
454
|
|
|
455
|
+
### Dream: sleep-time consolidation (0.4.16)
|
|
456
|
+
|
|
457
|
+
`loop-memory dream [--apply]` is the **non-destructive** counterpart to
|
|
458
|
+
`loop-memory cognitive-sleep --apply`:
|
|
459
|
+
|
|
460
|
+
| | `cognitive-sleep --apply` | `dream --apply` |
|
|
461
|
+
|---|---|---|
|
|
462
|
+
| forgets a stale row | `DELETE` — the row is gone | **soft expiry** — `invalid_at` is stamped, the row and its history stay |
|
|
463
|
+
| decides by age alone | yes | no — **utility** decides |
|
|
464
|
+
| near-duplicates | merged | merged into a **supersession chain** |
|
|
465
|
+
| reversible | no | yes (`revive_memory()` / `recall --as-of`) |
|
|
466
|
+
|
|
467
|
+
The utility blend is `0.45 × importance + 0.30 × usage + 0.25 × recency`,
|
|
468
|
+
so a rarely-stated-but-constantly-recalled fact is *protected* while a
|
|
469
|
+
never-used stale row is expired. Age alone never decides — that is the
|
|
470
|
+
whole point of "In Defense of Forgotten" (arXiv 2605.27366).
|
|
471
|
+
|
|
472
|
+
The pass is a **dry run by default**: `--apply` (CLI) or `"apply": true`
|
|
473
|
+
(HTTP) is what actually writes.
|
|
474
|
+
|
|
475
|
+
```bash
|
|
476
|
+
loop-memory dream --json # machine-readable dry run
|
|
477
|
+
loop-memory dream --apply --ttl-days 60 # tighter horizon
|
|
478
|
+
loop-memory dream --apply --utility 0.2 # expire more aggressively
|
|
479
|
+
curl -X POST http://127.0.0.1:7767/api/v1/dream -d '{"apply": true}'
|
|
480
|
+
```
|
|
481
|
+
|
|
482
|
+
After a dream pass, ask for the *current* truth explicitly:
|
|
483
|
+
|
|
484
|
+
```bash
|
|
485
|
+
loop-memory recall "editor preference" --latest-only
|
|
486
|
+
```
|
|
487
|
+
|
|
488
|
+
`--latest-only` (Mem0 v2 `latest_only` parity) drops rows outside their
|
|
489
|
+
validity window at *now*, collapses a subject-key slot to its newest live
|
|
490
|
+
row, and collapses near-duplicate paraphrases to the newest one.
|
|
491
|
+
|
|
449
492
|
### Cognitive sleep (v7)
|
|
450
493
|
|
|
451
494
|
`loop-memory cognitive-sleep [--apply]` runs an **auditable cleanup
|
|
@@ -602,7 +645,7 @@ loop_memory/
|
|
|
602
645
|
ingest/ # Codex / Claude / Hermes / OpenClaw / generic loaders
|
|
603
646
|
wiki/ # distillation, classifier, scope auto-promotion
|
|
604
647
|
graph/ # entity extraction + knowledge-graph build
|
|
605
|
-
jobs/ # consolidate / evolve / cognitive-sleep / scheduler / contradiction / graph
|
|
648
|
+
jobs/ # consolidate / evolve / cognitive-sleep / dream / scheduler / contradiction / graph
|
|
606
649
|
llm/ # provider protocol + OpenAI / Anthropic / Ollama / rule-based
|
|
607
650
|
backends/ # embedding (hashing / sentence-transformers) + vector store (memory / chroma)
|
|
608
651
|
storage/ # SQLite-backed MemoryStore + migrations
|
|
@@ -22,6 +22,8 @@
|
|
|
22
22
|
|
|
23
23
|
---
|
|
24
24
|
|
|
25
|
+
> **What's new in 0.4.16** — **subject-key logical fact slots + `latest_only` recall + the "dream" sleep-time consolidation pass** (audit 2026-10-25, weekly ecosystem sync — Mem0 v2.2.1 "Dream", arXiv 2605.27366 "In Defense of Forgotten", Zep/Graphiti temporal edges, Letta memory blocks). New nullable `subject_key` column on `memories` names the *logical slot* a fact occupies (`"editor.preference"`); `upsert_memory(subject_key=...)` makes the new row the live version and soft-supersedes the previous one with a `superseded_by` pointer, so the version history survives and `recall_as_of()` can still answer "what did we believe last week?". The content-hash dedup path is now slot-aware — two different slots holding the same text stay two rows. `SCHEMA_VERSION` bumps 12 → 13 with an idempotent in-place migration. New `latest_only=False` flag on `recall()` / `recall_hybrid()` (Mem0 v2 `SearchMemoryOptions.latest_only` parity) collapses the answer in three layers: rows outside their validity window at *now* are dropped, a subject-key slot resolves to its newest **live** row (looked up in the store, so a stale version never comes back just because it matched the query better), and near-duplicate texts cluster by token Jaccard (default 0.85) with the newest row winning. Wired through `GET /api/recall?latest_only=1`, `GET /api/v1/recall?latest_only=1`, `loop-memory recall --latest-only` and `MemoryClient.recall(latest_only=True)`. New `loop_memory/jobs/dream.py` implements the sleep-time consolidation Mem0 v2 calls the "Dream" — deterministic, zero-dep and **non-destructive**: it decays scores, then judges every row past its deadline on **utility** (`0.45 × importance + 0.30 × usage + 0.25 × recency`) so a frequently-recalled old fact is *protected* while a never-used stale row is **soft-expired** (`invalid_at` stamped, row kept, history kept) instead of deleted, and finally merges near-duplicates into a supersession chain. Run it with `loop-memory dream [--apply] [--json]` or `POST /api/v1/dream` — it is a **dry run by default** and every decision lands in `cognitive_audit` so the dashboard can show and revert it. New store primitives `expire_memory()` / `revive_memory()`, and `memory_stats()` now reports `subject_key`, `ttl`, `valid_at`, `invalid_at` and a derived `ttl_state`. **Also fixed a real defect**: the hybrid recall path (BM25 / LIKE / semantic channels) did not filter `superseded_by`, so `recall_hybrid()` — the production path behind the CLI, MCP server and dashboard — returned both versions of a superseded fact. Source: Mem0 v2.2.1 Dream + ADD-only architecture + arXiv 2605.27366 utility-aware eviction + Zep/Graphiti bi-temporal edges (pattern only, no code copied). 77 new regression cases. [Full CHANGELOG →](./CHANGELOG.md#0416---2026-10-25)
|
|
26
|
+
> **What's new in 0.4.15** — **content-hash dedup + bi-temporal validity window + evidence chain + `@agent_memory` decorator** (audit 2026-10-18, weekly ecosystem sync). Three new columns on `memories`: `content_hash` (SHA-256 of normalised text, indexed), `valid_at` (epoch the memory becomes active), `invalid_at` (epoch it expires — NULL means still open-ended). The schema migration bumps `SCHEMA_VERSION` 11 → 12 with an idempotent backfill so legacy DBs upgrade in-place. New `upsert_memory(valid_at=, invalid_at=, dedup=)` parameters let callers schedule future-dated or expiring memories and opt out of content-hash dedup for episodic event streams. `recall()` and `recall_hybrid()` now accept an optional `as_of_ts` (float epoch or ISO-8601) — when set, both methods drop memories whose `valid_at > as_of` or `invalid_at <= as_of`, complementing the existing `recall_as_of()` supersession-chain walk. Every recall hit now carries an `evidence: [{field, value, weight}]` array — BM25 / semantic / entity / rrf_total / high_importance channels are exposed individually so a UI can render "this hit because ... [bm25 1.0] [entity 0.7]" instead of a black-box score (Cognee 1.0 / Mem0 v2 evidence-citation pattern, zero-dep). New `loop_memory.decorators` exposes `@agent_memory(store, agent_id=, limit=)` — a one-line wrapper that recalls the top-k relevant memories before the body runs and writes a fresh `kind="interaction"` memory after. Strings returned from the wrapped function are stored verbatim; dict/list returns are JSON-serialised; the recall list is injected as a `memory_context` keyword when the function declares it, otherwise attached to dict results under `memories`. Sister `remember(store, text, ...)` helper for seeding. The BM25 `_like_fallback` now falls through to per-token OR-LIKE when the whole-string match returns nothing — natural-language questions like "What is the deploy process?" now hit stored facts even when FTS5 trigram can't match quoted exact phrases. Source: Cognee 1.0 ECL pipeline + Mem0 v2 hybrid fusion + Zep/Graphiti bi-temporal validity fields + Letta block-memory (pattern only, no code copied). 59 new regression cases. [Full CHANGELOG →](./CHANGELOG.md#0415---2026-10-18)
|
|
25
27
|
> **What's new in 0.4.14** — **`loop-memory doctor` deep store-health check** (audit 2026-10-12). Ten diagnostic checks — `db_exists`, `db_integrity`, `schema_version`, `audit_triggers`, `audit_log_writable`, `audit_log_size`, `hooks_installed`, `version_consistent`, `wiki_no_orphans`, `recall_paths_works` — report `ok`/`info`/`warn`/`error` for the live `MemoryStore`. `--fix` auto-repairs the `audit_triggers` check. `--json` emits a stable shape for CI. `--list` prints the check function names without touching the store. `--verbose` shows `info`-level checks. Exit code `1` on any `error` status, `0` otherwise. Source: `cortexkit/magic-context doctor` auto-diagnostic pattern (MIT, 2026-10-12) + `dsh-memento` write-approval-gate pattern (Apache-2.0, 2026-10-12). Stdlib-only — we re-implement from scratch against our own `MemoryStore`. Also fixed a Python 3.14 bug: chained `store._conn().__enter__()` immediately triggers the contextmanager's `finally` and closes the connection mid-query — both call sites in `doctor.py` now use explicit `with` form. 27 new regression cases. [Full CHANGELOG →](./CHANGELOG.md#0414---2026-10-12)
|
|
26
28
|
|
|
27
29
|
> **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)
|
|
@@ -201,6 +203,10 @@ loop-memory serve --port 7767 # open http://127.0.0.1:7767
|
|
|
201
203
|
# 3. Make it run on a timer
|
|
202
204
|
# (see docs/auto-capture.md for launchd / systemd / cron snippets)
|
|
203
205
|
loop-memory consolidate # rescore + GC + dedupe
|
|
206
|
+
|
|
207
|
+
# 4. Optional: sleep-time consolidation (soft expiry, never deletes)
|
|
208
|
+
loop-memory dream # dry run -- what would expire / merge
|
|
209
|
+
loop-memory dream --apply # write it (soft-expire + supersede)
|
|
204
210
|
```
|
|
205
211
|
|
|
206
212
|
---
|
|
@@ -402,6 +408,43 @@ User signals close the loop:
|
|
|
402
408
|
- Every `recall()` / search bumps `recall_count` on the returned
|
|
403
409
|
rows so the next Stage-1 ranks them higher.
|
|
404
410
|
|
|
411
|
+
### Dream: sleep-time consolidation (0.4.16)
|
|
412
|
+
|
|
413
|
+
`loop-memory dream [--apply]` is the **non-destructive** counterpart to
|
|
414
|
+
`loop-memory cognitive-sleep --apply`:
|
|
415
|
+
|
|
416
|
+
| | `cognitive-sleep --apply` | `dream --apply` |
|
|
417
|
+
|---|---|---|
|
|
418
|
+
| forgets a stale row | `DELETE` — the row is gone | **soft expiry** — `invalid_at` is stamped, the row and its history stay |
|
|
419
|
+
| decides by age alone | yes | no — **utility** decides |
|
|
420
|
+
| near-duplicates | merged | merged into a **supersession chain** |
|
|
421
|
+
| reversible | no | yes (`revive_memory()` / `recall --as-of`) |
|
|
422
|
+
|
|
423
|
+
The utility blend is `0.45 × importance + 0.30 × usage + 0.25 × recency`,
|
|
424
|
+
so a rarely-stated-but-constantly-recalled fact is *protected* while a
|
|
425
|
+
never-used stale row is expired. Age alone never decides — that is the
|
|
426
|
+
whole point of "In Defense of Forgotten" (arXiv 2605.27366).
|
|
427
|
+
|
|
428
|
+
The pass is a **dry run by default**: `--apply` (CLI) or `"apply": true`
|
|
429
|
+
(HTTP) is what actually writes.
|
|
430
|
+
|
|
431
|
+
```bash
|
|
432
|
+
loop-memory dream --json # machine-readable dry run
|
|
433
|
+
loop-memory dream --apply --ttl-days 60 # tighter horizon
|
|
434
|
+
loop-memory dream --apply --utility 0.2 # expire more aggressively
|
|
435
|
+
curl -X POST http://127.0.0.1:7767/api/v1/dream -d '{"apply": true}'
|
|
436
|
+
```
|
|
437
|
+
|
|
438
|
+
After a dream pass, ask for the *current* truth explicitly:
|
|
439
|
+
|
|
440
|
+
```bash
|
|
441
|
+
loop-memory recall "editor preference" --latest-only
|
|
442
|
+
```
|
|
443
|
+
|
|
444
|
+
`--latest-only` (Mem0 v2 `latest_only` parity) drops rows outside their
|
|
445
|
+
validity window at *now*, collapses a subject-key slot to its newest live
|
|
446
|
+
row, and collapses near-duplicate paraphrases to the newest one.
|
|
447
|
+
|
|
405
448
|
### Cognitive sleep (v7)
|
|
406
449
|
|
|
407
450
|
`loop-memory cognitive-sleep [--apply]` runs an **auditable cleanup
|
|
@@ -558,7 +601,7 @@ loop_memory/
|
|
|
558
601
|
ingest/ # Codex / Claude / Hermes / OpenClaw / generic loaders
|
|
559
602
|
wiki/ # distillation, classifier, scope auto-promotion
|
|
560
603
|
graph/ # entity extraction + knowledge-graph build
|
|
561
|
-
jobs/ # consolidate / evolve / cognitive-sleep / scheduler / contradiction / graph
|
|
604
|
+
jobs/ # consolidate / evolve / cognitive-sleep / dream / scheduler / contradiction / graph
|
|
562
605
|
llm/ # provider protocol + OpenAI / Anthropic / Ollama / rule-based
|
|
563
606
|
backends/ # embedding (hashing / sentence-transformers) + vector store (memory / chroma)
|
|
564
607
|
storage/ # SQLite-backed MemoryStore + migrations
|
|
@@ -73,8 +73,13 @@ def run_recall(args) -> int:
|
|
|
73
73
|
outline = False
|
|
74
74
|
limit = 10
|
|
75
75
|
as_of: float | None = None
|
|
76
|
+
# Audit 2026-10-18: bi-temporal *validity* window (distinct
|
|
77
|
+
# from ``as_of`` which routes through the supersession chain).
|
|
78
|
+
validity_at: float | None = None
|
|
76
79
|
max_chars: int | None = None
|
|
77
80
|
level = 1
|
|
81
|
+
# Audit 2026-10-25 (loop-memory 0.4.16) -- latest-only mode.
|
|
82
|
+
latest_only = False
|
|
78
83
|
qargs: list[str] = []
|
|
79
84
|
i = 0
|
|
80
85
|
while i < len(args):
|
|
@@ -82,6 +87,12 @@ def run_recall(args) -> int:
|
|
|
82
87
|
if a == "--verbose":
|
|
83
88
|
verbose = True
|
|
84
89
|
i += 1
|
|
90
|
+
elif a == "--latest-only":
|
|
91
|
+
# Audit 2026-10-25: Mem0 v2 parity -- show one answer per
|
|
92
|
+
# logical fact (drops rows outside their validity window
|
|
93
|
+
# and collapses subject-key / near-duplicate versions).
|
|
94
|
+
latest_only = True
|
|
95
|
+
i += 1
|
|
85
96
|
elif a == "--outline":
|
|
86
97
|
# Audit 2026-09-13 (tigerless-labs/agent-memory v0.3.0) —
|
|
87
98
|
# cheap L0 list instead of full text. Sets a flag and
|
|
@@ -106,6 +117,24 @@ def run_recall(args) -> int:
|
|
|
106
117
|
f"--as-of must be float epoch or ISO-8601; got {raw!r}"
|
|
107
118
|
)
|
|
108
119
|
i += 2
|
|
120
|
+
elif a == "--validity-at" and i + 1 < len(args):
|
|
121
|
+
# Audit 2026-10-18 (loop-memory 0.4.15) -- cheaper
|
|
122
|
+
# validity-window filter (vs --as-of which walks the
|
|
123
|
+
# supersession chain). Accepts the same float-or-ISO.
|
|
124
|
+
raw = args[i + 1].strip()
|
|
125
|
+
try:
|
|
126
|
+
validity_at = float(raw)
|
|
127
|
+
except ValueError:
|
|
128
|
+
try:
|
|
129
|
+
from datetime import datetime
|
|
130
|
+
validity_at = datetime.fromisoformat(
|
|
131
|
+
raw.replace("Z", "+00:00")
|
|
132
|
+
).timestamp()
|
|
133
|
+
except Exception:
|
|
134
|
+
return die(
|
|
135
|
+
f"--validity-at must be float epoch or ISO-8601; got {raw!r}"
|
|
136
|
+
)
|
|
137
|
+
i += 2
|
|
109
138
|
elif a == "--max-chars" and i + 1 < len(args):
|
|
110
139
|
# Audit 2026-10-05 (vectorize-io/hindsight v0.10.2 +
|
|
111
140
|
# aiming-lab/SimpleMem ICML'26) — cap the total payload
|
|
@@ -141,7 +170,7 @@ def run_recall(args) -> int:
|
|
|
141
170
|
qargs.append(a)
|
|
142
171
|
i += 1
|
|
143
172
|
if not qargs:
|
|
144
|
-
return die("usage: loop-memory recall <query> [--verbose] [--outline] [--as-of <ISO|epoch>] [--limit N] [--max-chars N] [--level 0|1|2]")
|
|
173
|
+
return die("usage: loop-memory recall <query> [--verbose] [--outline] [--as-of <ISO|epoch>] [--latest-only] [--limit N] [--max-chars N] [--level 0|1|2]")
|
|
145
174
|
store = MemoryStore(default_db_path())
|
|
146
175
|
query = " ".join(qargs)
|
|
147
176
|
if as_of is not None:
|
|
@@ -154,7 +183,14 @@ def run_recall(args) -> int:
|
|
|
154
183
|
r = store.recall_paths(query, limit=limit, max_chars=max_chars)
|
|
155
184
|
else:
|
|
156
185
|
r = store.recall(query, limit=limit,
|
|
157
|
-
max_chars=max_chars, level=level
|
|
186
|
+
max_chars=max_chars, level=level,
|
|
187
|
+
# Audit 2026-10-18: pass validity_at through
|
|
188
|
+
# to recall() so the new bi-temporal window
|
|
189
|
+
# filter applies. ``None`` keeps the legacy
|
|
190
|
+
# "everything" semantics.
|
|
191
|
+
as_of_ts=validity_at,
|
|
192
|
+
# Audit 2026-10-25: one answer per fact.
|
|
193
|
+
latest_only=latest_only)
|
|
158
194
|
has = False
|
|
159
195
|
if r["wiki"]:
|
|
160
196
|
has = True
|
|
@@ -7,7 +7,7 @@ import os
|
|
|
7
7
|
import urllib.error
|
|
8
8
|
import urllib.request
|
|
9
9
|
|
|
10
|
-
from .._common import DEFAULT_DB, die, parse_int_flag
|
|
10
|
+
from .._common import DEFAULT_DB, default_db_path, die, parse_int_flag
|
|
11
11
|
|
|
12
12
|
|
|
13
13
|
def run_ingest(args) -> int:
|
|
@@ -115,6 +115,99 @@ def run_rescore(args) -> int:
|
|
|
115
115
|
return 0
|
|
116
116
|
|
|
117
117
|
|
|
118
|
+
def run_dream(args) -> int:
|
|
119
|
+
"""loop-memory dream [--apply] [--json] [tuning flags]
|
|
120
|
+
|
|
121
|
+
The sleep-time consolidation pass (audit 2026-10-25, loop-memory
|
|
122
|
+
0.4.16). Runs a **dry sweep** by default and prints what it
|
|
123
|
+
*would* do; pass ``--apply`` to actually soft-expire stale rows and
|
|
124
|
+
merge near-duplicates.
|
|
125
|
+
|
|
126
|
+
Unlike ``loop-memory consolidate`` (which hard-deletes rows past
|
|
127
|
+
their TTL), the dream pass never deletes: it stamps ``invalid_at``
|
|
128
|
+
so the fact leaves the current view while its history stays
|
|
129
|
+
queryable through ``loop-memory recall --as-of``.
|
|
130
|
+
"""
|
|
131
|
+
import sys
|
|
132
|
+
|
|
133
|
+
from ...jobs.dream import dream as dream_job
|
|
134
|
+
from ...storage.sqlite_store import MemoryStore
|
|
135
|
+
|
|
136
|
+
args = list(args)
|
|
137
|
+
apply_changes = False
|
|
138
|
+
as_json = False
|
|
139
|
+
half_life_days = 30.0
|
|
140
|
+
ttl_days = 90.0
|
|
141
|
+
utility_threshold = 0.30
|
|
142
|
+
similarity = 0.92
|
|
143
|
+
limit = 2000
|
|
144
|
+
i = 0
|
|
145
|
+
while i < len(args):
|
|
146
|
+
a = args[i]
|
|
147
|
+
if a == "--apply":
|
|
148
|
+
apply_changes = True
|
|
149
|
+
i += 1
|
|
150
|
+
elif a == "--json":
|
|
151
|
+
as_json = True
|
|
152
|
+
i += 1
|
|
153
|
+
elif a in ("--half-life", "--ttl-days", "--utility", "--similarity", "--limit"):
|
|
154
|
+
if i + 1 >= len(args):
|
|
155
|
+
return die(f"{a} needs a value")
|
|
156
|
+
raw = args[i + 1]
|
|
157
|
+
try:
|
|
158
|
+
if a == "--limit":
|
|
159
|
+
limit = int(raw)
|
|
160
|
+
else:
|
|
161
|
+
value = float(raw)
|
|
162
|
+
except ValueError:
|
|
163
|
+
return die(f"{a} must be a number, got {raw!r}")
|
|
164
|
+
if a == "--half-life":
|
|
165
|
+
half_life_days = value
|
|
166
|
+
elif a == "--ttl-days":
|
|
167
|
+
ttl_days = value
|
|
168
|
+
elif a == "--utility":
|
|
169
|
+
utility_threshold = value
|
|
170
|
+
else:
|
|
171
|
+
similarity = value
|
|
172
|
+
i += 2
|
|
173
|
+
elif a.startswith("--"):
|
|
174
|
+
return die(f"unknown flag: {a}")
|
|
175
|
+
else:
|
|
176
|
+
return die(f"unexpected argument: {a!r}")
|
|
177
|
+
|
|
178
|
+
# ``default_db_path()`` (not the import-time constant) so a host
|
|
179
|
+
# that changes $LOOP_MEMORY_DB after import is honoured.
|
|
180
|
+
store = MemoryStore(default_db_path())
|
|
181
|
+
report = dream_job(
|
|
182
|
+
store,
|
|
183
|
+
apply=apply_changes,
|
|
184
|
+
half_life_days=half_life_days,
|
|
185
|
+
ttl_days=ttl_days,
|
|
186
|
+
utility_threshold=utility_threshold,
|
|
187
|
+
similarity=similarity,
|
|
188
|
+
limit=limit,
|
|
189
|
+
)
|
|
190
|
+
if as_json:
|
|
191
|
+
print(json.dumps(report.to_dict(), indent=2, ensure_ascii=False))
|
|
192
|
+
return 0
|
|
193
|
+
|
|
194
|
+
mode = "applied" if apply_changes else "dry-run"
|
|
195
|
+
print(
|
|
196
|
+
f"dream ({mode}) — considered {report.considered}, "
|
|
197
|
+
f"expired {report.expired}, kept {report.kept}, "
|
|
198
|
+
f"superseded {report.superseded} in {report.elapsed_ms:.0f} ms"
|
|
199
|
+
)
|
|
200
|
+
if not apply_changes and (report.expired or report.superseded):
|
|
201
|
+
print(" (dry run — re-run with --apply to write these changes)")
|
|
202
|
+
for action in report.actions:
|
|
203
|
+
if action.kind == "keep":
|
|
204
|
+
continue
|
|
205
|
+
marker = "✔" if action.action == "applied" else "•"
|
|
206
|
+
print(f" {marker} {action.kind}: {action.target_text[:72]}")
|
|
207
|
+
print(f" {action.reason}")
|
|
208
|
+
return 0
|
|
209
|
+
|
|
210
|
+
|
|
118
211
|
def run_consolidate_now(_args) -> int:
|
|
119
212
|
"""Ask the running server to trigger a consolidation pass right now."""
|
|
120
213
|
import json as _json
|
|
@@ -15,6 +15,7 @@ Usage:
|
|
|
15
15
|
loop-memory inject [query] # dump long-term context block (for SessionStart hooks)
|
|
16
16
|
loop-memory install-hooks # auto-write MCP + SessionStart hooks for known clients
|
|
17
17
|
loop-memory consolidate-now # ask the running server to trigger a pass right now
|
|
18
|
+
loop-memory dream [--apply] # sleep-time consolidation: utility-aware soft expiry + dedupe
|
|
18
19
|
loop-memory export # legacy markdown export (no positional path)
|
|
19
20
|
loop-memory digest [--out PATH] # compact knowledge digest for AGENTS.md (≤ max-chars bytes)
|
|
20
21
|
loop-memory rules [--agent codex|claude|hermes|openclaw] [--write] [--force]
|
|
@@ -94,6 +95,9 @@ COMMANDS = {
|
|
|
94
95
|
"ingest": write_cmd.run_ingest,
|
|
95
96
|
"consolidate": write_cmd.run_consolidate,
|
|
96
97
|
"consolidate-now": write_cmd.run_consolidate_now,
|
|
98
|
+
# Audit 2026-10-25 (loop-memory 0.4.16) -- sleep-time "dream"
|
|
99
|
+
# consolidation: utility-aware soft expiry + near-duplicate merge.
|
|
100
|
+
"dream": write_cmd.run_dream,
|
|
97
101
|
"export": _run_export,
|
|
98
102
|
"digest": read_cmd.run_digest,
|
|
99
103
|
"ask": read_cmd.run_ask,
|
|
@@ -147,6 +151,7 @@ COMMAND_HELP: dict[str, str] = {
|
|
|
147
151
|
"chat": "loop-memory chat # REPL with echo LLM.",
|
|
148
152
|
"cognitive-sleep": "loop-memory cognitive-sleep [--apply] # dry-run / apply cognitive sweep.",
|
|
149
153
|
"consolidate": "loop-memory consolidate # rescore + GC + dedupe.",
|
|
154
|
+
"dream": "loop-memory dream [--apply] [--json] [--ttl-days D] [--utility T] # sleep-time consolidation: utility-aware soft expiry + near-duplicate merge (audit 2026-10-25). Dry run unless --apply.",
|
|
150
155
|
"consolidate-now": "loop-memory consolidate-now # ask the running server to trigger a pass right now.",
|
|
151
156
|
"digest": "loop-memory digest [--out PATH] # compact knowledge digest for AGENTS.md.",
|
|
152
157
|
"doctor": "loop-memory doctor [--json] [--verbose] [--fix] [--db PATH] # deep store-health check (db integrity + audit triggers + hooks + version + wiki orphans). Auto-repair with --fix (audit 2026-10-12; pattern adopted from cortexkit/magic-context + dsh-memento).",
|
|
@@ -164,7 +169,7 @@ COMMAND_HELP: dict[str, str] = {
|
|
|
164
169
|
"install-hooks": "loop-memory install-hooks # auto-write MCP + SessionStart hooks for known clients.",
|
|
165
170
|
"mcp": "loop-memory mcp # stdio MCP server (for codex/claude/hermes). Requires ``[serve]`` for HTTP transport.",
|
|
166
171
|
"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>] [--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).",
|
|
172
|
+
"recall": "loop-memory recall <text> [--as-of <ISO|epoch>] [--latest-only] [--max-chars N] [--level 0|1|2] # show top memories (--as-of = bi-temporal recall; --latest-only = one answer per fact; --max-chars = total payload cap; --level 0 = L0 abstract ladder rung; audit 2026-10-05).",
|
|
168
173
|
"rescore": "loop-memory rescore [--half-life 30] # recompute memory scores.",
|
|
169
174
|
"serve": "loop-memory serve [--port 7767] [--no-browser] # start the local web UI. Requires the ``[serve]`` extra (fastapi + uvicorn).",
|
|
170
175
|
"stats": "loop-memory stats # counters.",
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
"""Lightweight decorators for one-line agent memory integration.
|
|
2
|
+
|
|
3
|
+
Audit 2026-10-18 (loop-memory 0.4.15): the decorator pattern is the
|
|
4
|
+
adoption-shortcut inspired by LangChain ``@tool`` / Mem0 v2's
|
|
5
|
+
``@remember`` -- a single line of boilerplate turns any plain Python
|
|
6
|
+
function into an "agent-callable" that reads from long-term memory
|
|
7
|
+
before it runs and writes a fresh memory after it returns.
|
|
8
|
+
|
|
9
|
+
Two decorators ship here:
|
|
10
|
+
|
|
11
|
+
* :func:`agent_memory` -- wraps a callable so every invocation:
|
|
12
|
+
- **recalls** the top-k relevant memories for the function's first
|
|
13
|
+
string argument (the "query") *before* running the body, and
|
|
14
|
+
- either exposes the recall as a ``memory_context`` keyword
|
|
15
|
+
argument (when the wrapped function's signature declares it),
|
|
16
|
+
OR surfaces it on the return value as ``result["memories"]``;
|
|
17
|
+
- **stores** the (query, return-value) pair as a single
|
|
18
|
+
``kind="interaction"`` memory row *after* the body finishes, so
|
|
19
|
+
the next invocation can recall it.
|
|
20
|
+
|
|
21
|
+
* :func:`remember` -- a tiny sugar that just calls
|
|
22
|
+
``store.upsert_memory(kind="fact", text=text, ...)`` once at
|
|
23
|
+
definition time. Useful for facts the agent wants to hard-code into
|
|
24
|
+
memory at import time ("I am a code assistant", "My user prefers
|
|
25
|
+
dark mode", etc.).
|
|
26
|
+
|
|
27
|
+
Both decorators are intentionally zero-dep and free of any LLM/agent
|
|
28
|
+
framework dependency -- they sit on top of the public
|
|
29
|
+
``MemoryStore`` API only.
|
|
30
|
+
"""
|
|
31
|
+
from __future__ import annotations
|
|
32
|
+
|
|
33
|
+
import functools
|
|
34
|
+
import inspect
|
|
35
|
+
import json
|
|
36
|
+
from collections.abc import Callable
|
|
37
|
+
from typing import Any, ParamSpec, TypeVar
|
|
38
|
+
|
|
39
|
+
from .storage.sqlite_store import MemoryStore
|
|
40
|
+
|
|
41
|
+
P = ParamSpec("P")
|
|
42
|
+
R = TypeVar("R")
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _stringify(value: Any, *, max_len: int = 2000) -> str:
|
|
46
|
+
"""Coerce a Python return value to a memory-friendly string.
|
|
47
|
+
|
|
48
|
+
* ``str`` -> as-is (truncated to ``max_len``).
|
|
49
|
+
* ``dict`` / ``list`` -> JSON-serialised then truncated.
|
|
50
|
+
* ``bytes``-> hex preview.
|
|
51
|
+
* everything else -> ``repr(value)``.
|
|
52
|
+
|
|
53
|
+
The truncation is the only place this helper swallows information;
|
|
54
|
+
downstream recall still works because the original ``text`` is on
|
|
55
|
+
the row, and the full structured payload is also stored verbatim
|
|
56
|
+
in ``tags`` when the caller passes a dict/list.
|
|
57
|
+
"""
|
|
58
|
+
if isinstance(value, str):
|
|
59
|
+
return value if len(value) <= max_len else value[: max_len - 1] + "\u2026"
|
|
60
|
+
if isinstance(value, (dict, list)):
|
|
61
|
+
s = json.dumps(value, ensure_ascii=False, default=str)
|
|
62
|
+
return s if len(s) <= max_len else s[: max_len - 1] + "\u2026"
|
|
63
|
+
if isinstance(value, bytes):
|
|
64
|
+
return value[:64].hex() + (f" (len={len(value)})" if len(value) > 64 else "")
|
|
65
|
+
return repr(value)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _first_string_arg(
|
|
69
|
+
args: tuple, kwargs: dict, signature: inspect.Signature
|
|
70
|
+
) -> str | None:
|
|
71
|
+
"""Return the first positional str argument from the call.
|
|
72
|
+
|
|
73
|
+
Falls back to ``None`` when the call carries no string-y input
|
|
74
|
+
(e.g. ``@agent_memory`` wrapping an internal ``tick()`` that
|
|
75
|
+
takes only objects).
|
|
76
|
+
"""
|
|
77
|
+
if args:
|
|
78
|
+
first = args[0]
|
|
79
|
+
if isinstance(first, str) and first.strip():
|
|
80
|
+
return first
|
|
81
|
+
return None
|
|
82
|
+
for name, p in signature.parameters.items():
|
|
83
|
+
if name in kwargs:
|
|
84
|
+
v = kwargs[name]
|
|
85
|
+
if isinstance(v, str) and v.strip():
|
|
86
|
+
return v
|
|
87
|
+
return None
|
|
88
|
+
return None
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def agent_memory(
|
|
92
|
+
store: MemoryStore,
|
|
93
|
+
*,
|
|
94
|
+
agent_id: str | None = None,
|
|
95
|
+
user_id: str | None = None,
|
|
96
|
+
kind: str = "interaction",
|
|
97
|
+
limit: int = 5,
|
|
98
|
+
recall_kwargs_name: str = "memory_context",
|
|
99
|
+
) -> Callable[[Callable[P, R]], Callable[P, R]]:
|
|
100
|
+
"""Wrap ``fn`` so every call reads + writes persistent memory.
|
|
101
|
+
|
|
102
|
+
Parameters
|
|
103
|
+
----------
|
|
104
|
+
store:
|
|
105
|
+
An open :class:`~loop_memory.storage.sqlite_store.MemoryStore`.
|
|
106
|
+
Required -- the decorator has nowhere to put memory otherwise.
|
|
107
|
+
agent_id, user_id:
|
|
108
|
+
Optional identity columns. When ``None`` the wrapped call
|
|
109
|
+
pushes a memory that is global to the store (the same scope
|
|
110
|
+
that pre-0.4.15 callers used).
|
|
111
|
+
kind:
|
|
112
|
+
Memory row kind. Default ``"interaction"`` matches the
|
|
113
|
+
dashboard's existing tag.
|
|
114
|
+
limit:
|
|
115
|
+
Number of top-k memories to recall *before* the body runs.
|
|
116
|
+
Capped at 50 to keep the prompt tiny.
|
|
117
|
+
recall_kwargs_name:
|
|
118
|
+
Name of the optional kwarg the wrapped function can declare to
|
|
119
|
+
receive the recall as a string. Defaults to ``"memory_context"``.
|
|
120
|
+
If the function does NOT declare that kwarg the recall is
|
|
121
|
+
silently dropped (the body still runs) but the row is still
|
|
122
|
+
stored after the call returns.
|
|
123
|
+
|
|
124
|
+
Example
|
|
125
|
+
-------
|
|
126
|
+
>>> store = MemoryStore("loop.db")
|
|
127
|
+
>>> @agent_memory(store, agent_id="codex")
|
|
128
|
+
... def ask(question: str, memory_context: str = "") -> str:
|
|
129
|
+
... # memory_context arrives pre-formatted from the recall
|
|
130
|
+
... return f"{question} -> {memory_context[:40]}"
|
|
131
|
+
>>> ask("What is the deploy process?")
|
|
132
|
+
"""
|
|
133
|
+
if not isinstance(store, MemoryStore):
|
|
134
|
+
raise TypeError(
|
|
135
|
+
f"@agent_memory requires a MemoryStore, got {type(store).__name__}"
|
|
136
|
+
)
|
|
137
|
+
if limit <= 0 or limit > 50:
|
|
138
|
+
raise ValueError(f"limit must be in [1, 50], got {limit}")
|
|
139
|
+
|
|
140
|
+
def _decorator(fn: Callable[P, R]) -> Callable[P, R]:
|
|
141
|
+
sig = inspect.signature(fn)
|
|
142
|
+
_ = sig # pre-bound for _first_string_arg
|
|
143
|
+
recall_kw = recall_kwargs_name
|
|
144
|
+
|
|
145
|
+
@functools.wraps(fn)
|
|
146
|
+
def _wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
|
|
147
|
+
query = _first_string_arg(args, kwargs, sig)
|
|
148
|
+
# --- pre-call: recall ---------------------------------
|
|
149
|
+
recall_hits: list[dict] = []
|
|
150
|
+
context_text = ""
|
|
151
|
+
if query:
|
|
152
|
+
try:
|
|
153
|
+
res = store.recall_hybrid(
|
|
154
|
+
query, limit=limit, as_of_ts=None,
|
|
155
|
+
)
|
|
156
|
+
recall_hits = list(res.get("memories", []))
|
|
157
|
+
except Exception:
|
|
158
|
+
recall_hits = []
|
|
159
|
+
if recall_hits:
|
|
160
|
+
lines = []
|
|
161
|
+
for h in recall_hits:
|
|
162
|
+
prev = (h.get("text") or h.get("preview") or "").strip()
|
|
163
|
+
if prev:
|
|
164
|
+
lines.append(f"- {prev[:240]}")
|
|
165
|
+
context_text = "\n".join(lines)
|
|
166
|
+
|
|
167
|
+
# Inject memory_context only when the function actually
|
|
168
|
+
# declares the kwarg -- otherwise the decorator stays
|
|
169
|
+
# invisible to legacy callables that don't know about it.
|
|
170
|
+
injected = False
|
|
171
|
+
if recall_kw in sig.parameters and context_text:
|
|
172
|
+
kwargs = {**kwargs, recall_kw: context_text}
|
|
173
|
+
injected = True
|
|
174
|
+
|
|
175
|
+
# --- body ----------------------------------------------
|
|
176
|
+
result = fn(*args, **kwargs)
|
|
177
|
+
|
|
178
|
+
# --- post-call: store ----------------------------------
|
|
179
|
+
if query:
|
|
180
|
+
try:
|
|
181
|
+
text = _stringify(result)
|
|
182
|
+
tags = ["agent_memory"]
|
|
183
|
+
if isinstance(result, (dict, list)):
|
|
184
|
+
tags.append("structured")
|
|
185
|
+
store.upsert_memory(
|
|
186
|
+
kind=kind,
|
|
187
|
+
text=text,
|
|
188
|
+
importance=0.6,
|
|
189
|
+
agent_id=agent_id,
|
|
190
|
+
user_id=user_id,
|
|
191
|
+
tags=tags,
|
|
192
|
+
source=f"agent_memory:{fn.__name__}",
|
|
193
|
+
)
|
|
194
|
+
except Exception:
|
|
195
|
+
# Never let a store failure break the wrapped call.
|
|
196
|
+
pass
|
|
197
|
+
|
|
198
|
+
# Attach the recall to a dict result so callers can
|
|
199
|
+
# surface "I knew this because ..." in the UI. Plain
|
|
200
|
+
# string/int/float returns stay untouched.
|
|
201
|
+
if isinstance(result, dict) and recall_hits and not injected:
|
|
202
|
+
result.setdefault("memories", recall_hits)
|
|
203
|
+
return result
|
|
204
|
+
|
|
205
|
+
# Friendly metadata on the wrapper so ``help()`` is useful.
|
|
206
|
+
_wrapper.__wrapped_agent_memory__ = True # type: ignore[attr-defined]
|
|
207
|
+
return _wrapper
|
|
208
|
+
|
|
209
|
+
return _decorator
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def remember(
|
|
213
|
+
store: MemoryStore,
|
|
214
|
+
text: str,
|
|
215
|
+
*,
|
|
216
|
+
kind: str = "fact",
|
|
217
|
+
importance: float = 0.7,
|
|
218
|
+
tags: list[str] | None = None,
|
|
219
|
+
agent_id: str | None = None,
|
|
220
|
+
user_id: str | None = None,
|
|
221
|
+
) -> str:
|
|
222
|
+
"""Insert ``text`` into the store as a single memory and return its id.
|
|
223
|
+
|
|
224
|
+
Sugar for callers that want to seed memory at import time without
|
|
225
|
+
having to instantiate ``StoredMemory`` / ``upsert_memory``:
|
|
226
|
+
|
|
227
|
+
>>> store = MemoryStore("loop.db")
|
|
228
|
+
>>> remember(store, "User prefers dark mode",
|
|
229
|
+
... tags=["ui", "preference"], agent_id="codex")
|
|
230
|
+
"""
|
|
231
|
+
if not isinstance(store, MemoryStore):
|
|
232
|
+
raise TypeError(
|
|
233
|
+
f"remember() requires a MemoryStore, got {type(store).__name__}"
|
|
234
|
+
)
|
|
235
|
+
item = store.upsert_memory(
|
|
236
|
+
kind=kind, text=text, importance=importance,
|
|
237
|
+
tags=tags or ["remember"], agent_id=agent_id, user_id=user_id,
|
|
238
|
+
source="remember",
|
|
239
|
+
)
|
|
240
|
+
return item.id
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
__all__ = ["agent_memory", "remember"]
|