loop-memory 0.4.6__tar.gz → 0.4.8__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.6 → loop_memory-0.4.8}/PKG-INFO +32 -10
- {loop_memory-0.4.6 → loop_memory-0.4.8}/README.md +31 -9
- {loop_memory-0.4.6 → loop_memory-0.4.8}/loop_memory/cli/commands/cognitive.py +100 -0
- {loop_memory-0.4.6 → loop_memory-0.4.8}/loop_memory/cli/commands/hooks.py +74 -6
- {loop_memory-0.4.6 → loop_memory-0.4.8}/loop_memory/cli/commands/read.py +82 -4
- {loop_memory-0.4.6 → loop_memory-0.4.8}/loop_memory/cli/main.py +14 -0
- {loop_memory-0.4.6 → loop_memory-0.4.8}/loop_memory/serve/routes/export.py +36 -0
- {loop_memory-0.4.6 → loop_memory-0.4.8}/loop_memory/serve/routes/memories.py +16 -0
- {loop_memory-0.4.6 → loop_memory-0.4.8}/loop_memory/serve/routes/system.py +92 -0
- loop_memory-0.4.8/loop_memory/storage/snapshot.py +231 -0
- {loop_memory-0.4.6 → loop_memory-0.4.8}/loop_memory/storage/sqlite_store.py +367 -1
- {loop_memory-0.4.6 → loop_memory-0.4.8}/loop_memory.egg-info/PKG-INFO +32 -10
- {loop_memory-0.4.6 → loop_memory-0.4.8}/loop_memory.egg-info/SOURCES.txt +10 -0
- {loop_memory-0.4.6 → loop_memory-0.4.8}/pyproject.toml +1 -1
- loop_memory-0.4.8/tests/test_agents_init.py +154 -0
- loop_memory-0.4.8/tests/test_cli_init.py +106 -0
- loop_memory-0.4.8/tests/test_cli_memory_stats.py +82 -0
- loop_memory-0.4.8/tests/test_cli_recall_paths.py +155 -0
- {loop_memory-0.4.6 → loop_memory-0.4.8}/tests/test_cli_recall_verbose.py +11 -8
- loop_memory-0.4.8/tests/test_cli_snapshot.py +112 -0
- loop_memory-0.4.8/tests/test_memory_stats.py +155 -0
- loop_memory-0.4.8/tests/test_recall_paths.py +213 -0
- loop_memory-0.4.8/tests/test_routes_audit_2026_09_13.py +141 -0
- loop_memory-0.4.8/tests/test_snapshot.py +215 -0
- {loop_memory-0.4.6 → loop_memory-0.4.8}/tests/test_supersession_chain.py +1 -1
- {loop_memory-0.4.6 → loop_memory-0.4.8}/LICENSE +0 -0
- {loop_memory-0.4.6 → loop_memory-0.4.8}/loop_memory/__init__.py +0 -0
- {loop_memory-0.4.6 → loop_memory-0.4.8}/loop_memory/backends/__init__.py +0 -0
- {loop_memory-0.4.6 → loop_memory-0.4.8}/loop_memory/backends/embedding.py +0 -0
- {loop_memory-0.4.6 → loop_memory-0.4.8}/loop_memory/backends/sentence_embedder.py +0 -0
- {loop_memory-0.4.6 → loop_memory-0.4.8}/loop_memory/backends/vector_store.py +0 -0
- {loop_memory-0.4.6 → loop_memory-0.4.8}/loop_memory/cli/__init__.py +0 -0
- {loop_memory-0.4.6 → loop_memory-0.4.8}/loop_memory/cli/_common.py +0 -0
- {loop_memory-0.4.6 → loop_memory-0.4.8}/loop_memory/cli/commands/__init__.py +0 -0
- {loop_memory-0.4.6 → loop_memory-0.4.8}/loop_memory/cli/commands/diag.py +0 -0
- {loop_memory-0.4.6 → loop_memory-0.4.8}/loop_memory/cli/commands/graph.py +0 -0
- {loop_memory-0.4.6 → loop_memory-0.4.8}/loop_memory/cli/commands/rules.py +0 -0
- {loop_memory-0.4.6 → loop_memory-0.4.8}/loop_memory/cli/commands/serve.py +0 -0
- {loop_memory-0.4.6 → loop_memory-0.4.8}/loop_memory/cli/commands/write.py +0 -0
- {loop_memory-0.4.6 → loop_memory-0.4.8}/loop_memory/engine/__init__.py +0 -0
- {loop_memory-0.4.6 → loop_memory-0.4.8}/loop_memory/engine/loop.py +0 -0
- {loop_memory-0.4.6 → loop_memory-0.4.8}/loop_memory/engine/reflect.py +0 -0
- {loop_memory-0.4.6 → loop_memory-0.4.8}/loop_memory/examples/__init__.py +0 -0
- {loop_memory-0.4.6 → loop_memory-0.4.8}/loop_memory/examples/demo.py +0 -0
- {loop_memory-0.4.6 → loop_memory-0.4.8}/loop_memory/export/__init__.py +0 -0
- {loop_memory-0.4.6 → loop_memory-0.4.8}/loop_memory/export/memory_md.py +0 -0
- {loop_memory-0.4.6 → loop_memory-0.4.8}/loop_memory/graph/__init__.py +0 -0
- {loop_memory-0.4.6 → loop_memory-0.4.8}/loop_memory/graph/build.py +0 -0
- {loop_memory-0.4.6 → loop_memory-0.4.8}/loop_memory/graph/extract.py +0 -0
- {loop_memory-0.4.6 → loop_memory-0.4.8}/loop_memory/ingest/__init__.py +0 -0
- {loop_memory-0.4.6 → loop_memory-0.4.8}/loop_memory/ingest/loader.py +0 -0
- {loop_memory-0.4.6 → loop_memory-0.4.8}/loop_memory/ingest/pipeline.py +0 -0
- {loop_memory-0.4.6 → loop_memory-0.4.8}/loop_memory/jobs/__init__.py +0 -0
- {loop_memory-0.4.6 → loop_memory-0.4.8}/loop_memory/jobs/cognitive.py +0 -0
- {loop_memory-0.4.6 → loop_memory-0.4.8}/loop_memory/jobs/compact.py +0 -0
- {loop_memory-0.4.6 → loop_memory-0.4.8}/loop_memory/jobs/consolidate.py +0 -0
- {loop_memory-0.4.6 → loop_memory-0.4.8}/loop_memory/jobs/contradiction.py +0 -0
- {loop_memory-0.4.6 → loop_memory-0.4.8}/loop_memory/jobs/evolution.py +0 -0
- {loop_memory-0.4.6 → loop_memory-0.4.8}/loop_memory/jobs/graph.py +0 -0
- {loop_memory-0.4.6 → loop_memory-0.4.8}/loop_memory/jobs/llm_compact_pass.py +0 -0
- {loop_memory-0.4.6 → loop_memory-0.4.8}/loop_memory/jobs/llm_consolidate.py +0 -0
- {loop_memory-0.4.6 → loop_memory-0.4.8}/loop_memory/jobs/scheduler.py +0 -0
- {loop_memory-0.4.6 → loop_memory-0.4.8}/loop_memory/llm/__init__.py +0 -0
- {loop_memory-0.4.6 → loop_memory-0.4.8}/loop_memory/llm/base.py +0 -0
- {loop_memory-0.4.6 → loop_memory-0.4.8}/loop_memory/llm/openai_adapter.py +0 -0
- {loop_memory-0.4.6 → loop_memory-0.4.8}/loop_memory/llm/providers.py +0 -0
- {loop_memory-0.4.6 → loop_memory-0.4.8}/loop_memory/mcp/__init__.py +0 -0
- {loop_memory-0.4.6 → loop_memory-0.4.8}/loop_memory/memory/__init__.py +0 -0
- {loop_memory-0.4.6 → loop_memory-0.4.8}/loop_memory/memory/types.py +0 -0
- {loop_memory-0.4.6 → loop_memory-0.4.8}/loop_memory/privacy/__init__.py +0 -0
- {loop_memory-0.4.6 → loop_memory-0.4.8}/loop_memory/privacy/private.py +0 -0
- {loop_memory-0.4.6 → loop_memory-0.4.8}/loop_memory/privacy/redact.py +0 -0
- {loop_memory-0.4.6 → loop_memory-0.4.8}/loop_memory/py.typed +0 -0
- {loop_memory-0.4.6 → loop_memory-0.4.8}/loop_memory/sdk.py +0 -0
- {loop_memory-0.4.6 → loop_memory-0.4.8}/loop_memory/sdk_extensions.py +0 -0
- {loop_memory-0.4.6 → loop_memory-0.4.8}/loop_memory/security/__init__.py +0 -0
- {loop_memory-0.4.6 → loop_memory-0.4.8}/loop_memory/security/secrets.py +0 -0
- {loop_memory-0.4.6 → loop_memory-0.4.8}/loop_memory/serve/__init__.py +0 -0
- {loop_memory-0.4.6 → loop_memory-0.4.8}/loop_memory/serve/app.py +0 -0
- {loop_memory-0.4.6 → loop_memory-0.4.8}/loop_memory/serve/handlers.py +0 -0
- {loop_memory-0.4.6 → loop_memory-0.4.8}/loop_memory/serve/routes/_shared.py +0 -0
- {loop_memory-0.4.6 → loop_memory-0.4.8}/loop_memory/serve/routes/admin.py +0 -0
- {loop_memory-0.4.6 → loop_memory-0.4.8}/loop_memory/serve/routes/cognitive.py +0 -0
- {loop_memory-0.4.6 → loop_memory-0.4.8}/loop_memory/serve/routes/graph.py +0 -0
- {loop_memory-0.4.6 → loop_memory-0.4.8}/loop_memory/serve/routes/insights.py +0 -0
- {loop_memory-0.4.6 → loop_memory-0.4.8}/loop_memory/serve/routes/sessions.py +0 -0
- {loop_memory-0.4.6 → loop_memory-0.4.8}/loop_memory/serve/routes/wiki.py +0 -0
- {loop_memory-0.4.6 → loop_memory-0.4.8}/loop_memory/serve/static/__init__.py +0 -0
- {loop_memory-0.4.6 → loop_memory-0.4.8}/loop_memory/serve/static/index.html +0 -0
- {loop_memory-0.4.6 → loop_memory-0.4.8}/loop_memory/serve/watcher.py +0 -0
- {loop_memory-0.4.6 → loop_memory-0.4.8}/loop_memory/storage/__init__.py +0 -0
- {loop_memory-0.4.6 → loop_memory-0.4.8}/loop_memory/storage/retrieval.py +0 -0
- {loop_memory-0.4.6 → loop_memory-0.4.8}/loop_memory/wiki/__init__.py +0 -0
- {loop_memory-0.4.6 → loop_memory-0.4.8}/loop_memory/wiki/backfill.py +0 -0
- {loop_memory-0.4.6 → loop_memory-0.4.8}/loop_memory/wiki/classifier.py +0 -0
- {loop_memory-0.4.6 → loop_memory-0.4.8}/loop_memory/wiki/prompts.py +0 -0
- {loop_memory-0.4.6 → loop_memory-0.4.8}/loop_memory/wiki/scope.py +0 -0
- {loop_memory-0.4.6 → loop_memory-0.4.8}/loop_memory.egg-info/dependency_links.txt +0 -0
- {loop_memory-0.4.6 → loop_memory-0.4.8}/loop_memory.egg-info/entry_points.txt +0 -0
- {loop_memory-0.4.6 → loop_memory-0.4.8}/loop_memory.egg-info/requires.txt +0 -0
- {loop_memory-0.4.6 → loop_memory-0.4.8}/loop_memory.egg-info/top_level.txt +0 -0
- {loop_memory-0.4.6 → loop_memory-0.4.8}/setup.cfg +0 -0
- {loop_memory-0.4.6 → loop_memory-0.4.8}/tests/test_admin_ingest_route.py +0 -0
- {loop_memory-0.4.6 → loop_memory-0.4.8}/tests/test_agent_memory_api.py +0 -0
- {loop_memory-0.4.6 → loop_memory-0.4.8}/tests/test_agent_memory_sdk.py +0 -0
- {loop_memory-0.4.6 → loop_memory-0.4.8}/tests/test_auth_token_rotate.py +0 -0
- {loop_memory-0.4.6 → loop_memory-0.4.8}/tests/test_cli_audit_supersede.py +0 -0
- {loop_memory-0.4.6 → loop_memory-0.4.8}/tests/test_cli_rules.py +0 -0
- {loop_memory-0.4.6 → loop_memory-0.4.8}/tests/test_cli_v7.py +0 -0
- {loop_memory-0.4.6 → loop_memory-0.4.8}/tests/test_cli_version.py +0 -0
- {loop_memory-0.4.6 → loop_memory-0.4.8}/tests/test_contradictions.py +0 -0
- {loop_memory-0.4.6 → loop_memory-0.4.8}/tests/test_evolution.py +0 -0
- {loop_memory-0.4.6 → loop_memory-0.4.8}/tests/test_evolution_quality.py +0 -0
- {loop_memory-0.4.6 → loop_memory-0.4.8}/tests/test_expanduser.py +0 -0
- {loop_memory-0.4.6 → loop_memory-0.4.8}/tests/test_export_ask.py +0 -0
- {loop_memory-0.4.6 → loop_memory-0.4.8}/tests/test_graph.py +0 -0
- {loop_memory-0.4.6 → loop_memory-0.4.8}/tests/test_ingest.py +0 -0
- {loop_memory-0.4.6 → loop_memory-0.4.8}/tests/test_llm_consolidator.py +0 -0
- {loop_memory-0.4.6 → loop_memory-0.4.8}/tests/test_llm_fingerprint_not_persisted.py +0 -0
- {loop_memory-0.4.6 → loop_memory-0.4.8}/tests/test_llm_providers.py +0 -0
- {loop_memory-0.4.6 → loop_memory-0.4.8}/tests/test_llm_test_endpoint.py +0 -0
- {loop_memory-0.4.6 → loop_memory-0.4.8}/tests/test_loop.py +0 -0
- {loop_memory-0.4.6 → loop_memory-0.4.8}/tests/test_mcp.py +0 -0
- {loop_memory-0.4.6 → loop_memory-0.4.8}/tests/test_memories_pagination.py +0 -0
- {loop_memory-0.4.6 → loop_memory-0.4.8}/tests/test_openclaw_loader.py +0 -0
- {loop_memory-0.4.6 → loop_memory-0.4.8}/tests/test_package_version.py +0 -0
- {loop_memory-0.4.6 → loop_memory-0.4.8}/tests/test_recall_provenance.py +0 -0
- {loop_memory-0.4.6 → loop_memory-0.4.8}/tests/test_reflection.py +0 -0
- {loop_memory-0.4.6 → loop_memory-0.4.8}/tests/test_repo_framing.py +0 -0
- {loop_memory-0.4.6 → loop_memory-0.4.8}/tests/test_score_api.py +0 -0
- {loop_memory-0.4.6 → loop_memory-0.4.8}/tests/test_scoring_v2.py +0 -0
- {loop_memory-0.4.6 → loop_memory-0.4.8}/tests/test_secrets.py +0 -0
- {loop_memory-0.4.6 → loop_memory-0.4.8}/tests/test_serve_app.py +0 -0
- {loop_memory-0.4.6 → loop_memory-0.4.8}/tests/test_serve_handlers.py +0 -0
- {loop_memory-0.4.6 → loop_memory-0.4.8}/tests/test_session_order.py +0 -0
- {loop_memory-0.4.6 → loop_memory-0.4.8}/tests/test_short_query.py +0 -0
- {loop_memory-0.4.6 → loop_memory-0.4.8}/tests/test_store.py +0 -0
- {loop_memory-0.4.6 → loop_memory-0.4.8}/tests/test_summarization.py +0 -0
- {loop_memory-0.4.6 → loop_memory-0.4.8}/tests/test_universal_memory.py +0 -0
- {loop_memory-0.4.6 → loop_memory-0.4.8}/tests/test_vector_store.py +0 -0
- {loop_memory-0.4.6 → loop_memory-0.4.8}/tests/test_watcher.py +0 -0
- {loop_memory-0.4.6 → loop_memory-0.4.8}/tests/test_wiki_classifier.py +0 -0
- {loop_memory-0.4.6 → loop_memory-0.4.8}/tests/test_wiki_export_escape.py +0 -0
- {loop_memory-0.4.6 → loop_memory-0.4.8}/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.8
|
|
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,15 +66,37 @@ Dynamic: license-file
|
|
|
66
66
|
|
|
67
67
|
---
|
|
68
68
|
|
|
69
|
-
> **What's new in 0.4.
|
|
70
|
-
>
|
|
71
|
-
>
|
|
72
|
-
>
|
|
73
|
-
>
|
|
74
|
-
>
|
|
75
|
-
>
|
|
76
|
-
> `loop-memory
|
|
77
|
-
>
|
|
69
|
+
> **What's new in 0.4.8** — **L0 outline recall** +
|
|
70
|
+
> **per-agent identity bootstrap**. `loop-memory recall-paths <q>`
|
|
71
|
+
> (and the new `recall --outline` flag) returns only `id` +
|
|
72
|
+
> `abstract` + `score` + `why` for each hit — never the full
|
|
73
|
+
> body — so an agent can identify the top candidates cheaply
|
|
74
|
+
> before deciding which bodies to fetch (audit 2026-09-13,
|
|
75
|
+
> tigerless-labs/agent-memory v0.3.0 recall-ladder pattern).
|
|
76
|
+
> `loop-memory init --agent <name>` registers an agent in the
|
|
77
|
+
> new `agents` table and (with `--install-hooks`) also wires
|
|
78
|
+
> the MCP + SessionStart hooks for codex/claude/hermes; the
|
|
79
|
+
> index is bumped on every `install-hooks` run so a user can
|
|
80
|
+
> always answer "which CLIs are wired into this store?" with
|
|
81
|
+
> one SQL query (audit 2026-09-13, Mem0 CLI `init --agent`
|
|
82
|
+
> pattern). Both shapes ship with HTTP routes
|
|
83
|
+
> (`/api/recall/outline`, `/api/agents`, `/api/init/agent`).
|
|
84
|
+
> 57 new regression cases pin both shapes. [Full
|
|
85
|
+
> changelog →](CHANGELOG.md)
|
|
86
|
+
>
|
|
87
|
+
> **What's new in 0.4.7** — **per-memory lifetime stats** +
|
|
88
|
+
> **portable SQLite snapshot**. `loop-memory memory-stats <id>`
|
|
89
|
+
> returns a flat dict (recall_count, positive, negative, age,
|
|
90
|
+
> etc.) so you can ask "is anyone still using this memory?"
|
|
91
|
+
> without cracking open the SQLite file (audit 2026-09-06,
|
|
92
|
+
> agentmemory v1.3.0 pattern). `loop-memory snapshot <out>` /
|
|
93
|
+
> `loop-memory restore <in>` move a single-file, lossless
|
|
94
|
+
> snapshot of the entire store — including recall-quality
|
|
95
|
+
> signals and the supersession chain — to another machine
|
|
96
|
+
> (audit 2026-09-06, codexa-memory v0.2.0 pattern). Both shapes
|
|
97
|
+
> ship with HTTP routes (`/api/memories/{id}/stats`,
|
|
98
|
+
> `/api/snapshot`, `/api/snapshot/restore`). 33 new regression
|
|
99
|
+
> cases pin both shapes.
|
|
78
100
|
> [Full changelog →](CHANGELOG.md)
|
|
79
101
|
|
|
80
102
|
---
|
|
@@ -22,15 +22,37 @@
|
|
|
22
22
|
|
|
23
23
|
---
|
|
24
24
|
|
|
25
|
-
> **What's new in 0.4.
|
|
26
|
-
>
|
|
27
|
-
>
|
|
28
|
-
>
|
|
29
|
-
>
|
|
30
|
-
>
|
|
31
|
-
>
|
|
32
|
-
> `loop-memory
|
|
33
|
-
>
|
|
25
|
+
> **What's new in 0.4.8** — **L0 outline recall** +
|
|
26
|
+
> **per-agent identity bootstrap**. `loop-memory recall-paths <q>`
|
|
27
|
+
> (and the new `recall --outline` flag) returns only `id` +
|
|
28
|
+
> `abstract` + `score` + `why` for each hit — never the full
|
|
29
|
+
> body — so an agent can identify the top candidates cheaply
|
|
30
|
+
> before deciding which bodies to fetch (audit 2026-09-13,
|
|
31
|
+
> tigerless-labs/agent-memory v0.3.0 recall-ladder pattern).
|
|
32
|
+
> `loop-memory init --agent <name>` registers an agent in the
|
|
33
|
+
> new `agents` table and (with `--install-hooks`) also wires
|
|
34
|
+
> the MCP + SessionStart hooks for codex/claude/hermes; the
|
|
35
|
+
> index is bumped on every `install-hooks` run so a user can
|
|
36
|
+
> always answer "which CLIs are wired into this store?" with
|
|
37
|
+
> one SQL query (audit 2026-09-13, Mem0 CLI `init --agent`
|
|
38
|
+
> pattern). Both shapes ship with HTTP routes
|
|
39
|
+
> (`/api/recall/outline`, `/api/agents`, `/api/init/agent`).
|
|
40
|
+
> 57 new regression cases pin both shapes. [Full
|
|
41
|
+
> changelog →](CHANGELOG.md)
|
|
42
|
+
>
|
|
43
|
+
> **What's new in 0.4.7** — **per-memory lifetime stats** +
|
|
44
|
+
> **portable SQLite snapshot**. `loop-memory memory-stats <id>`
|
|
45
|
+
> returns a flat dict (recall_count, positive, negative, age,
|
|
46
|
+
> etc.) so you can ask "is anyone still using this memory?"
|
|
47
|
+
> without cracking open the SQLite file (audit 2026-09-06,
|
|
48
|
+
> agentmemory v1.3.0 pattern). `loop-memory snapshot <out>` /
|
|
49
|
+
> `loop-memory restore <in>` move a single-file, lossless
|
|
50
|
+
> snapshot of the entire store — including recall-quality
|
|
51
|
+
> signals and the supersession chain — to another machine
|
|
52
|
+
> (audit 2026-09-06, codexa-memory v0.2.0 pattern). Both shapes
|
|
53
|
+
> ship with HTTP routes (`/api/memories/{id}/stats`,
|
|
54
|
+
> `/api/snapshot`, `/api/snapshot/restore`). 33 new regression
|
|
55
|
+
> cases pin both shapes.
|
|
34
56
|
> [Full changelog →](CHANGELOG.md)
|
|
35
57
|
|
|
36
58
|
---
|
|
@@ -85,6 +85,29 @@ def _build_parser() -> argparse.ArgumentParser:
|
|
|
85
85
|
rc.add_argument("--dry-run", action="store_true",
|
|
86
86
|
help="Report what would change without persisting")
|
|
87
87
|
|
|
88
|
+
# Audit 2026-09-06: per-memory lifetime stats
|
|
89
|
+
# (agentmemory v1.3.0 pattern, slimmed down).
|
|
90
|
+
ms = sub.add_parser(
|
|
91
|
+
"memory-stats",
|
|
92
|
+
help="Per-memory lifetime stats (audit 2026-09-06)")
|
|
93
|
+
ms.add_argument("memory_id",
|
|
94
|
+
help="Memory id (or short prefix) to look up")
|
|
95
|
+
ms.add_argument("--prefix", action="store_true",
|
|
96
|
+
help="Allow a short prefix match (first 8+ chars)")
|
|
97
|
+
|
|
98
|
+
# Audit 2026-09-06: portable SQLite snapshot
|
|
99
|
+
# (codexa-memory v0.2.0 pattern).
|
|
100
|
+
sn = sub.add_parser(
|
|
101
|
+
"snapshot",
|
|
102
|
+
help="Write a portable SQLite snapshot of the live store")
|
|
103
|
+
sn.add_argument("out_path",
|
|
104
|
+
help="Destination .memory.sqlite file")
|
|
105
|
+
rs = sub.add_parser(
|
|
106
|
+
"restore",
|
|
107
|
+
help="Re-hydrate a portable SQLite snapshot into the live store")
|
|
108
|
+
rs.add_argument("in_path",
|
|
109
|
+
help="Source .memory.sqlite file")
|
|
110
|
+
|
|
88
111
|
return p
|
|
89
112
|
|
|
90
113
|
|
|
@@ -242,3 +265,80 @@ def run_wiki_reclassify_legacy(args: list) -> int:
|
|
|
242
265
|
})
|
|
243
266
|
summary = reclassify_legacy_pages(s, batch=ns.batch)
|
|
244
267
|
return _emit(summary)
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
def run_memory_stats(args: list) -> int:
|
|
271
|
+
"""``loop-memory memory-stats <id> [--prefix]``
|
|
272
|
+
|
|
273
|
+
Per-memory lifetime stats (audit 2026-09-06, agentmemory v1.3.0).
|
|
274
|
+
Returns a flat dict suitable for ``jq`` and the dashboard.
|
|
275
|
+
|
|
276
|
+
With ``--prefix`` the caller can pass the first 8+ chars of the id
|
|
277
|
+
(handy in a shell pipeline where the full UUID is awkward).
|
|
278
|
+
"""
|
|
279
|
+
import argparse as _ap
|
|
280
|
+
p = _ap.ArgumentParser(prog="loop-memory memory-stats")
|
|
281
|
+
p.add_argument("memory_id")
|
|
282
|
+
p.add_argument("--prefix", action="store_true")
|
|
283
|
+
p.add_argument("--db", default=os.environ.get("LOOP_MEMORY_DB", DEFAULT_DB))
|
|
284
|
+
ns = p.parse_args(args)
|
|
285
|
+
s = MemoryStore(ns.db)
|
|
286
|
+
mid = ns.memory_id.strip()
|
|
287
|
+
if not mid:
|
|
288
|
+
return _emit({"error": "memory_id is required"})
|
|
289
|
+
if ns.prefix:
|
|
290
|
+
# Resolve a prefix to a full id. The store indexes by full
|
|
291
|
+
# id so we walk a list_memories scan (cheap because prefix
|
|
292
|
+
# queries are rare / human-driven).
|
|
293
|
+
candidates = [m.id for m in s.list_memories(limit=10000)
|
|
294
|
+
if m.id.startswith(mid)]
|
|
295
|
+
if not candidates:
|
|
296
|
+
return _emit({"error": f"no memory matches prefix {mid!r}"})
|
|
297
|
+
if len(candidates) > 1:
|
|
298
|
+
return _emit({
|
|
299
|
+
"error": f"prefix {mid!r} matches {len(candidates)} memories",
|
|
300
|
+
"candidates": candidates[:8],
|
|
301
|
+
})
|
|
302
|
+
mid = candidates[0]
|
|
303
|
+
res = s.memory_stats(mid)
|
|
304
|
+
if res is None:
|
|
305
|
+
return _emit({"error": f"memory {mid!r} not found"})
|
|
306
|
+
return _emit(res)
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
def run_snapshot(args: list) -> int:
|
|
310
|
+
"""``loop-memory snapshot <out_path>``
|
|
311
|
+
|
|
312
|
+
Write a portable SQLite snapshot (audit 2026-09-06,
|
|
313
|
+
codexa-memory v0.2.0). Returns a summary dict with size + table
|
|
314
|
+
counts so the caller can confirm the snapshot is sane.
|
|
315
|
+
"""
|
|
316
|
+
import argparse as _ap
|
|
317
|
+
p = _ap.ArgumentParser(prog="loop-memory snapshot")
|
|
318
|
+
p.add_argument("out_path")
|
|
319
|
+
p.add_argument("--db", default=os.environ.get("LOOP_MEMORY_DB", DEFAULT_DB))
|
|
320
|
+
ns = p.parse_args(args)
|
|
321
|
+
s = MemoryStore(ns.db)
|
|
322
|
+
from ...storage.snapshot import snapshot as _snapshot
|
|
323
|
+
return _emit(_snapshot(s, ns.out_path))
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
def run_restore(args: list) -> int:
|
|
327
|
+
"""``loop-memory restore <in_path>``
|
|
328
|
+
|
|
329
|
+
Re-hydrate a portable SQLite snapshot (audit 2026-09-06,
|
|
330
|
+
codexa-memory v0.2.0). Returns a summary dict with table counts
|
|
331
|
+
so the caller can confirm the restore is sane. Refuses
|
|
332
|
+
wrong-magic files loudly instead of corrupting the live store.
|
|
333
|
+
"""
|
|
334
|
+
import argparse as _ap
|
|
335
|
+
p = _ap.ArgumentParser(prog="loop-memory restore")
|
|
336
|
+
p.add_argument("in_path")
|
|
337
|
+
p.add_argument("--db", default=os.environ.get("LOOP_MEMORY_DB", DEFAULT_DB))
|
|
338
|
+
ns = p.parse_args(args)
|
|
339
|
+
s = MemoryStore(ns.db)
|
|
340
|
+
from ...storage.snapshot import restore as _restore
|
|
341
|
+
try:
|
|
342
|
+
return _emit(_restore(s, ns.in_path))
|
|
343
|
+
except (FileNotFoundError, ValueError) as e:
|
|
344
|
+
return _emit({"error": str(e)})
|
|
@@ -46,7 +46,7 @@ def _upsert_block(text: str, block: str, marker: str) -> str:
|
|
|
46
46
|
return "".join(lines[:start]) + block + "".join(lines[end:])
|
|
47
47
|
|
|
48
48
|
|
|
49
|
-
def _install_codex(home: Path, actions: list) -> None:
|
|
49
|
+
def _install_codex(home: Path, actions: list, store=None) -> None:
|
|
50
50
|
codex_dir = home / ".codex"
|
|
51
51
|
codex_cfg = codex_dir / "config.toml"
|
|
52
52
|
# Codex's TOML schema: `hooks` is a struct (HooksToml) keyed by event
|
|
@@ -104,11 +104,13 @@ def _install_codex(home: Path, actions: list) -> None:
|
|
|
104
104
|
except Exception: pass
|
|
105
105
|
return
|
|
106
106
|
actions.append(f"codex → {codex_cfg}" + ("" if parse_ok else " (unverified)"))
|
|
107
|
+
if store is not None:
|
|
108
|
+
store.register_agent("codex", hooks_installed=True)
|
|
107
109
|
except Exception as e:
|
|
108
110
|
actions.append(f"codex → SKIP ({e})")
|
|
109
111
|
|
|
110
112
|
|
|
111
|
-
def _install_claude(home: Path, actions: list) -> None:
|
|
113
|
+
def _install_claude(home: Path, actions: list, store=None) -> None:
|
|
112
114
|
claude_dir = home / ".claude"
|
|
113
115
|
claude_mcp = claude_dir / "mcp.json"
|
|
114
116
|
claude_settings = claude_dir / "settings.json"
|
|
@@ -148,9 +150,11 @@ def _install_claude(home: Path, actions: list) -> None:
|
|
|
148
150
|
})
|
|
149
151
|
claude_settings.write_text(json.dumps(existing_s, ensure_ascii=False, indent=2), encoding="utf-8")
|
|
150
152
|
actions.append(f"claude (SessionStart hook) → {claude_settings}")
|
|
153
|
+
if store is not None:
|
|
154
|
+
store.register_agent("claude", hooks_installed=True)
|
|
151
155
|
|
|
152
156
|
|
|
153
|
-
def _install_hermes(home: Path, actions: list) -> None:
|
|
157
|
+
def _install_hermes(home: Path, actions: list, store=None) -> None:
|
|
154
158
|
hermes_dir = home / ".hermes"
|
|
155
159
|
hermes_cfg = hermes_dir / "mcp.json"
|
|
156
160
|
if not hermes_dir.exists():
|
|
@@ -168,6 +172,8 @@ def _install_hermes(home: Path, actions: list) -> None:
|
|
|
168
172
|
hermes_cfg.parent.mkdir(parents=True, exist_ok=True)
|
|
169
173
|
hermes_cfg.write_text(json.dumps(existing, ensure_ascii=False, indent=2), encoding="utf-8")
|
|
170
174
|
actions.append(f"hermes → {hermes_cfg}")
|
|
175
|
+
if store is not None:
|
|
176
|
+
store.register_agent("hermes", hooks_installed=True)
|
|
171
177
|
|
|
172
178
|
|
|
173
179
|
def _openclaw_hint(home: Path, actions: list) -> None:
|
|
@@ -200,13 +206,75 @@ def run_install_hooks(_args) -> int:
|
|
|
200
206
|
"""
|
|
201
207
|
home = Path.home()
|
|
202
208
|
actions: list = []
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
209
|
+
# Audit 2026-09-13 (Mem0 CLI init --agent pattern) — record which
|
|
210
|
+
# agents we touched so the per-agent index stays accurate. The
|
|
211
|
+
# store is best-effort: if the SQLite path can't open, we fall
|
|
212
|
+
# back to "install only" so the user is never blocked by a DB
|
|
213
|
+
# hiccup during a one-line CLI install.
|
|
214
|
+
store = None
|
|
215
|
+
try:
|
|
216
|
+
from ...storage.sqlite_store import MemoryStore
|
|
217
|
+
from ...cli._common import default_db_path
|
|
218
|
+
store = MemoryStore(default_db_path())
|
|
219
|
+
except Exception:
|
|
220
|
+
store = None
|
|
221
|
+
_install_codex(home, actions, store=store)
|
|
222
|
+
_install_claude(home, actions, store=store)
|
|
223
|
+
_install_hermes(home, actions, store=store)
|
|
206
224
|
_openclaw_hint(home, actions)
|
|
225
|
+
if store is not None:
|
|
226
|
+
# OpenClaw is hinted (no auto-install) so we only touch it.
|
|
227
|
+
store.touch_agent("openclaw")
|
|
207
228
|
print("[loop-memory] install-hooks results:")
|
|
208
229
|
for a in actions:
|
|
209
230
|
print(f" · {a}")
|
|
210
231
|
print()
|
|
211
232
|
print("Restart your CLI to pick up the new MCP server + hooks.")
|
|
212
233
|
return 0
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def run_init(args) -> int:
|
|
238
|
+
"""``loop-memory init --agent <name> [--install-hooks]``.
|
|
239
|
+
|
|
240
|
+
Audit 2026-09-13 (Mem0 CLI ``mem0 init --agent <name>`` pattern).
|
|
241
|
+
Registers an agent in the per-agent identity index and, when
|
|
242
|
+
``--install-hooks`` is passed, also runs the standard
|
|
243
|
+
``install-hooks`` flow so MCP + SessionStart hooks are written
|
|
244
|
+
for that agent's CLI client. Idempotent — re-running for the
|
|
245
|
+
same name just bumps ``last_seen_at``.
|
|
246
|
+
"""
|
|
247
|
+
from ...storage.sqlite_store import MemoryStore
|
|
248
|
+
from ...cli._common import default_db_path, die
|
|
249
|
+
agent_name: str | None = None
|
|
250
|
+
install_hooks = False
|
|
251
|
+
i = 0
|
|
252
|
+
while i < len(args):
|
|
253
|
+
a = args[i]
|
|
254
|
+
if a == "--agent" and i + 1 < len(args):
|
|
255
|
+
agent_name = args[i + 1].strip()
|
|
256
|
+
i += 2
|
|
257
|
+
elif a == "--install-hooks":
|
|
258
|
+
install_hooks = True
|
|
259
|
+
i += 1
|
|
260
|
+
elif a == "--help" or a == "-h":
|
|
261
|
+
print("usage: loop-memory init --agent <name> [--install-hooks]")
|
|
262
|
+
return 0
|
|
263
|
+
elif a.startswith("--"):
|
|
264
|
+
return die(f"[loop-memory] unknown flag: {a}")
|
|
265
|
+
else:
|
|
266
|
+
return die(f"[loop-memory] unexpected positional: {a}")
|
|
267
|
+
if not agent_name:
|
|
268
|
+
return die("usage: loop-memory init --agent <name> [--install-hooks]")
|
|
269
|
+
store = MemoryStore(default_db_path())
|
|
270
|
+
rec = store.register_agent(agent_name, hooks_installed=False)
|
|
271
|
+
verb = "registered" if rec["created"] else "refreshed"
|
|
272
|
+
print(f"[loop-memory] {verb} agent {agent_name!r} (scope={rec['scope']}, "
|
|
273
|
+
f"hooks_installed={rec['hooks_installed']}, "
|
|
274
|
+
f"last_seen_at={rec['last_seen_at']:.0f}).")
|
|
275
|
+
if install_hooks:
|
|
276
|
+
# Delegate to the standard install-hooks flow so the MCP + inject
|
|
277
|
+
# hook files get written for codex/claude/hermes if any of those
|
|
278
|
+
# clients are installed.
|
|
279
|
+
run_install_hooks([])
|
|
280
|
+
return 0
|
|
@@ -66,6 +66,7 @@ def run_recall(args) -> int:
|
|
|
66
66
|
# we don't want argparse to swallow a token that happens to start
|
|
67
67
|
# with "--" inside the user's query (e.g. ``recall --foo bar``).
|
|
68
68
|
verbose = False
|
|
69
|
+
outline = False
|
|
69
70
|
limit = 10
|
|
70
71
|
qargs: list[str] = []
|
|
71
72
|
i = 0
|
|
@@ -74,6 +75,12 @@ def run_recall(args) -> int:
|
|
|
74
75
|
if a == "--verbose":
|
|
75
76
|
verbose = True
|
|
76
77
|
i += 1
|
|
78
|
+
elif a == "--outline":
|
|
79
|
+
# Audit 2026-09-13 (tigerless-labs/agent-memory v0.3.0) —
|
|
80
|
+
# cheap L0 list instead of full text. Sets a flag and
|
|
81
|
+
# routes the call to ``recall_paths`` below.
|
|
82
|
+
outline = True
|
|
83
|
+
i += 1
|
|
77
84
|
elif a == "--limit" and i + 1 < len(args):
|
|
78
85
|
try:
|
|
79
86
|
limit = int(args[i + 1])
|
|
@@ -86,10 +93,13 @@ def run_recall(args) -> int:
|
|
|
86
93
|
qargs.append(a)
|
|
87
94
|
i += 1
|
|
88
95
|
if not qargs:
|
|
89
|
-
return die("usage: loop-memory recall <query> [--verbose] [--limit N]")
|
|
96
|
+
return die("usage: loop-memory recall <query> [--verbose] [--outline] [--limit N]")
|
|
90
97
|
store = MemoryStore(default_db_path())
|
|
91
98
|
query = " ".join(qargs)
|
|
92
|
-
|
|
99
|
+
if outline:
|
|
100
|
+
r = store.recall_paths(query, limit=limit)
|
|
101
|
+
else:
|
|
102
|
+
r = store.recall(query, limit=limit)
|
|
93
103
|
has = False
|
|
94
104
|
if r["wiki"]:
|
|
95
105
|
has = True
|
|
@@ -97,7 +107,9 @@ def run_recall(args) -> int:
|
|
|
97
107
|
for w in r["wiki"]:
|
|
98
108
|
tag_s = " [" + ", ".join(w.get("tags") or []) + "]" if w.get("tags") else ""
|
|
99
109
|
print(f"- **{w['title']}** (`{w['slug']}`) — imp {w['importance']:.2f}{tag_s}")
|
|
100
|
-
if
|
|
110
|
+
if outline:
|
|
111
|
+
print(f" > {w.get('abstract','')}")
|
|
112
|
+
elif w.get("summary"):
|
|
101
113
|
print(f" > {w['summary'][:240]}")
|
|
102
114
|
print()
|
|
103
115
|
if r["memories"]:
|
|
@@ -111,7 +123,11 @@ def run_recall(args) -> int:
|
|
|
111
123
|
# render the list as a deterministic badge strip without
|
|
112
124
|
# re-sorting.
|
|
113
125
|
print(f" why: {', '.join(m['why'])}")
|
|
114
|
-
|
|
126
|
+
if outline:
|
|
127
|
+
# L0 outline mode: print only the short abstract.
|
|
128
|
+
print(f" {m.get('abstract','')}")
|
|
129
|
+
else:
|
|
130
|
+
print(f" {m['text'][:240]}")
|
|
115
131
|
print()
|
|
116
132
|
if r["entities"]:
|
|
117
133
|
has = True
|
|
@@ -398,3 +414,65 @@ def run_inject(args) -> int:
|
|
|
398
414
|
out.write(f" {text}\n")
|
|
399
415
|
sys.stdout.write(out.getvalue())
|
|
400
416
|
return 0
|
|
417
|
+
|
|
418
|
+
|
|
419
|
+
|
|
420
|
+
def run_recall_paths(args) -> int:
|
|
421
|
+
"""``loop-memory recall-paths <query> [--limit N]``.
|
|
422
|
+
|
|
423
|
+
Audit 2026-09-13 (tigerless-labs/agent-memory v0.3.0). Returns
|
|
424
|
+
only ``id`` + ``abstract`` + ``score`` + ``why`` for each hit —
|
|
425
|
+
no full text, no body. Use this when an agent needs to *decide*
|
|
426
|
+
which candidate to open; once it has picked an id, fetch the
|
|
427
|
+
body via ``GET /api/memories/{id}`` or ``GET /api/wiki/{slug}``.
|
|
428
|
+
"""
|
|
429
|
+
from ...storage.sqlite_store import MemoryStore
|
|
430
|
+
if not args:
|
|
431
|
+
return die("usage: loop-memory recall-paths <query> [--limit N]")
|
|
432
|
+
limit = 12
|
|
433
|
+
qargs: list[str] = []
|
|
434
|
+
i = 0
|
|
435
|
+
while i < len(args):
|
|
436
|
+
a = args[i]
|
|
437
|
+
if a == "--limit" and i + 1 < len(args):
|
|
438
|
+
try:
|
|
439
|
+
limit = int(args[i + 1])
|
|
440
|
+
except ValueError:
|
|
441
|
+
return die(f"--limit must be an integer, got {args[i + 1]!r}")
|
|
442
|
+
i += 2
|
|
443
|
+
elif a.startswith("--"):
|
|
444
|
+
return die(f"unknown flag: {a}")
|
|
445
|
+
else:
|
|
446
|
+
qargs.append(a)
|
|
447
|
+
i += 1
|
|
448
|
+
if not qargs:
|
|
449
|
+
return die("usage: loop-memory recall-paths <query> [--limit N]")
|
|
450
|
+
store = MemoryStore(default_db_path())
|
|
451
|
+
query = " ".join(qargs)
|
|
452
|
+
r = store.recall_paths(query, limit=limit)
|
|
453
|
+
has = False
|
|
454
|
+
if r["memories"]:
|
|
455
|
+
has = True
|
|
456
|
+
print(f"## Outline (memories, {len(r['memories'])} match{'es' if len(r['memories'])!=1 else ''})")
|
|
457
|
+
for m in r["memories"]:
|
|
458
|
+
why = f" why: {', '.join(m['why'])}" if m.get("why") else ""
|
|
459
|
+
print(f"- [{m['kind']}] score={m['score']:.2f} id={m['id']}{why}")
|
|
460
|
+
print(f" > {m.get('abstract','')}")
|
|
461
|
+
print()
|
|
462
|
+
if r["wiki"]:
|
|
463
|
+
has = True
|
|
464
|
+
print(f"## Outline (wiki, {len(r['wiki'])} match{'es' if len(r['wiki'])!=1 else ''})")
|
|
465
|
+
for w in r["wiki"]:
|
|
466
|
+
why = f" why: {', '.join(w['why'])}" if w.get("why") else ""
|
|
467
|
+
print(f"- score={w['score']:.2f} slug={w['slug']}{why}")
|
|
468
|
+
print(f" > {w.get('abstract','')}")
|
|
469
|
+
print()
|
|
470
|
+
if r["entities"]:
|
|
471
|
+
has = True
|
|
472
|
+
print(f"## Outline (entities, {len(r['entities'])})")
|
|
473
|
+
for e in r["entities"]:
|
|
474
|
+
print(f"- {e['name']} _({e['entity_kind']}, w={e['weight']:.2f})_")
|
|
475
|
+
print()
|
|
476
|
+
if not has:
|
|
477
|
+
print(f"_( nothing matched {query!r})_")
|
|
478
|
+
return 0
|
|
@@ -118,6 +118,13 @@ COMMANDS = {
|
|
|
118
118
|
"subgraph": cognitive_cmd.run_subgraph,
|
|
119
119
|
"graph-rebuild": cognitive_cmd.run_graph_rebuild,
|
|
120
120
|
"wiki-reclassify-legacy": cognitive_cmd.run_wiki_reclassify_legacy,
|
|
121
|
+
# Audit 2026-09-06 — per-memory stats + portable snapshot.
|
|
122
|
+
"memory-stats": cognitive_cmd.run_memory_stats,
|
|
123
|
+
"snapshot": cognitive_cmd.run_snapshot,
|
|
124
|
+
"restore": cognitive_cmd.run_restore,
|
|
125
|
+
# Audit 2026-09-13 — L0 outline recall + per-agent identity bootstrap.
|
|
126
|
+
"recall-paths": read_cmd.run_recall_paths,
|
|
127
|
+
"init": hooks_cmd.run_init,
|
|
121
128
|
"rules": rules_cmd.run_rules,
|
|
122
129
|
"version": _run_version,
|
|
123
130
|
}
|
|
@@ -159,6 +166,13 @@ COMMAND_HELP: dict[str, str] = {
|
|
|
159
166
|
"subgraph": "loop-memory subgraph <query> # print a small subgraph.",
|
|
160
167
|
"version": "loop-memory version # print the installed distribution version.",
|
|
161
168
|
"wiki-reclassify-legacy": "loop-memory wiki-reclassify-legacy # back-fill scope + scope_filter for H4+H5 pages.",
|
|
169
|
+
"memory-stats": "loop-memory memory-stats <id> [--prefix] # per-memory lifetime stats (audit 2026-09-06, agentmemory v1.3.0).",
|
|
170
|
+
"snapshot": "loop-memory snapshot <out.memory.sqlite> # write a portable SQLite snapshot (audit 2026-09-06, codexa-memory v0.2.0).",
|
|
171
|
+
"restore": "loop-memory restore <in.memory.sqlite> # re-hydrate a portable SQLite snapshot.",
|
|
172
|
+
# Audit 2026-09-13 — L0 outline recall (tigerless-labs/agent-memory v0.3.0).
|
|
173
|
+
"recall-paths": "loop-memory recall-paths <query> [--limit N] # cheap L0 outline (id+abstract+score) instead of full text (audit 2026-09-13).",
|
|
174
|
+
# Audit 2026-09-13 — per-agent identity bootstrap (Mem0 CLI init --agent pattern).
|
|
175
|
+
"init": "loop-memory init --agent <name> [--install-hooks] # register an agent + (optionally) install hooks.",
|
|
162
176
|
"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.",
|
|
163
177
|
}
|
|
164
178
|
|
|
@@ -62,4 +62,40 @@ def register(app: FastAPI, store: MemoryStore, scheduler: Optional[Any] = None)
|
|
|
62
62
|
raise HTTPException(500, f"import failed: {e}")
|
|
63
63
|
return r.to_dict()
|
|
64
64
|
|
|
65
|
+
@app.post("/api/snapshot")
|
|
66
|
+
def snapshot_create(body: dict):
|
|
67
|
+
"""Write a portable SQLite snapshot of the live store.
|
|
65
68
|
|
|
69
|
+
Body: ``{"out_path": "/path/to/file.memory.sqlite"}``. Returns
|
|
70
|
+
the summary dict from ``loop_memory.storage.snapshot.snapshot``.
|
|
71
|
+
"""
|
|
72
|
+
from ...storage.snapshot import snapshot as _snapshot
|
|
73
|
+
out = (body.get("out_path") or "").strip()
|
|
74
|
+
if not out:
|
|
75
|
+
raise HTTPException(400, "out_path is required")
|
|
76
|
+
try:
|
|
77
|
+
return _snapshot(store, out)
|
|
78
|
+
except Exception as e:
|
|
79
|
+
raise HTTPException(500, f"snapshot failed: {e}")
|
|
80
|
+
|
|
81
|
+
@app.post("/api/snapshot/restore")
|
|
82
|
+
def snapshot_restore(body: dict):
|
|
83
|
+
"""Re-hydrate a portable SQLite snapshot into the live store.
|
|
84
|
+
|
|
85
|
+
Body: ``{"in_path": "/path/to/file.memory.sqlite"}``. Returns
|
|
86
|
+
the summary dict from ``loop_memory.storage.snapshot.restore``.
|
|
87
|
+
Refuses wrong-magic files with a 400 so the dashboard surfaces
|
|
88
|
+
the error clearly instead of corrupting the live store.
|
|
89
|
+
"""
|
|
90
|
+
from ...storage.snapshot import restore as _restore
|
|
91
|
+
in_path = (body.get("in_path") or "").strip()
|
|
92
|
+
if not in_path:
|
|
93
|
+
raise HTTPException(400, "in_path is required")
|
|
94
|
+
try:
|
|
95
|
+
return _restore(store, in_path)
|
|
96
|
+
except FileNotFoundError as e:
|
|
97
|
+
raise HTTPException(404, str(e))
|
|
98
|
+
except ValueError as e:
|
|
99
|
+
raise HTTPException(400, str(e))
|
|
100
|
+
except Exception as e:
|
|
101
|
+
raise HTTPException(500, f"restore failed: {e}")
|
|
@@ -414,6 +414,22 @@ def register(app: FastAPI, store: MemoryStore, scheduler: Optional[Any] = None)
|
|
|
414
414
|
return {"deleted": n}
|
|
415
415
|
|
|
416
416
|
|
|
417
|
+
@app.get("/api/memories/{mid}/stats")
|
|
418
|
+
def memory_stats_route(mid: str):
|
|
419
|
+
"""Per-memory lifetime stats (audit 2026-09-06).
|
|
420
|
+
|
|
421
|
+
Returns the flat dict produced by ``MemoryStore.memory_stats``
|
|
422
|
+
— see that method for the field list. 404 if the memory id
|
|
423
|
+
is unknown. The dashboard uses this to surface
|
|
424
|
+
recall / positive / negative counters alongside each memory
|
|
425
|
+
row without a second round-trip.
|
|
426
|
+
"""
|
|
427
|
+
res = store.memory_stats(mid)
|
|
428
|
+
if res is None:
|
|
429
|
+
from fastapi import HTTPException
|
|
430
|
+
raise HTTPException(404, "memory not found")
|
|
431
|
+
return res
|
|
432
|
+
|
|
417
433
|
@app.get("/api/memories/{mid}", name="memory_drill_down")
|
|
418
434
|
def memory_drill_down(mid: str):
|
|
419
435
|
"""L2 drill-down for a single memory row (full text)."""
|
|
@@ -392,6 +392,98 @@ def register(app: FastAPI, store: MemoryStore, scheduler: Optional[Any] = None,
|
|
|
392
392
|
}
|
|
393
393
|
|
|
394
394
|
|
|
395
|
+
@app.get("/api/recall/outline")
|
|
396
|
+
def recall_outline(query: str = "", limit: int = 12,
|
|
397
|
+
include: str = "memories,wiki,entities",
|
|
398
|
+
bump: int = 0, source: str | None = None):
|
|
399
|
+
"""Audit 2026-09-13 — L0 outline recall (tigerless-labs/agent-memory
|
|
400
|
+
v0.3.0 recall ladder pattern).
|
|
401
|
+
|
|
402
|
+
Returns the same ranked lists as ``GET /api/recall`` but each
|
|
403
|
+
hit carries only ``id`` + ``kind`` + ``abstract`` (≤80 chars)
|
|
404
|
+
+ ``score`` + ``why`` -- never the full ``text`` / ``body``.
|
|
405
|
+
Use this when an agent needs to *decide* which candidate to
|
|
406
|
+
open; once it has picked an id, fetch the body via
|
|
407
|
+
``GET /api/memories/{id}`` or ``GET /api/wiki/{slug}``.
|
|
408
|
+
|
|
409
|
+
``bump`` defaults to 0 so the L0 listing does not inflate
|
|
410
|
+
``recall_count`` before the agent has actually decided to read
|
|
411
|
+
the body. Set ``bump=1`` only when the agent commits to the
|
|
412
|
+
candidate set.
|
|
413
|
+
"""
|
|
414
|
+
if not query:
|
|
415
|
+
raise HTTPException(status_code=400, detail="query is required")
|
|
416
|
+
wanted = tuple(s.strip() for s in include.split(",") if s.strip())
|
|
417
|
+
if not wanted:
|
|
418
|
+
wanted = ("memories", "wiki", "entities")
|
|
419
|
+
r = store.recall_paths(
|
|
420
|
+
query,
|
|
421
|
+
limit=limit,
|
|
422
|
+
include=wanted,
|
|
423
|
+
bump_signals=bool(bump),
|
|
424
|
+
source=source,
|
|
425
|
+
)
|
|
426
|
+
return {
|
|
427
|
+
"query": query,
|
|
428
|
+
"tokens": r.get("tokens", []),
|
|
429
|
+
"memories": r.get("memories", []),
|
|
430
|
+
"wiki": r.get("wiki", []),
|
|
431
|
+
"entities": r.get("entities", []),
|
|
432
|
+
"mode": "outline",
|
|
433
|
+
"source": source,
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
|
|
437
|
+
@app.get("/api/agents")
|
|
438
|
+
def list_agents_route():
|
|
439
|
+
"""Audit 2026-09-13 — list all registered agents (Mem0 CLI
|
|
440
|
+
``init --agent`` pattern). Each entry includes ``name``,
|
|
441
|
+
``scope``, ``created_at``, ``last_seen_at``,
|
|
442
|
+
``hooks_installed`` so a user can answer "which CLIs are
|
|
443
|
+
wired into this store?" with a single HTTP GET.
|
|
444
|
+
"""
|
|
445
|
+
return {"agents": store.list_agents()}
|
|
446
|
+
|
|
447
|
+
|
|
448
|
+
@app.post("/api/init/agent")
|
|
449
|
+
def init_agent_route(body: dict):
|
|
450
|
+
"""Audit 2026-09-13 — register (or refresh) a named agent.
|
|
451
|
+
|
|
452
|
+
Body keys (all optional except ``name``):
|
|
453
|
+
- ``name`` (str, required) — e.g. ``"codex"``, ``"claude"``,
|
|
454
|
+
``"hermes"``, ``"openclaw"``, or a custom name.
|
|
455
|
+
- ``scope`` (str, default ``"global"``) — reserved for
|
|
456
|
+
future per-source isolation; today always ``"global"``.
|
|
457
|
+
- ``install_hooks`` (bool, default ``false``) — when true,
|
|
458
|
+
also runs the equivalent of ``install-hooks`` so MCP +
|
|
459
|
+
SessionStart files are written for codex/claude/hermes if
|
|
460
|
+
those clients are installed locally. Best-effort: a hook
|
|
461
|
+
failure does not fail the registration.
|
|
462
|
+
|
|
463
|
+
Idempotent — re-registering an existing name just bumps
|
|
464
|
+
``last_seen_at``. Returns the persisted row.
|
|
465
|
+
"""
|
|
466
|
+
if not isinstance(body, dict):
|
|
467
|
+
raise HTTPException(status_code=400, detail="body must be a JSON object")
|
|
468
|
+
name = (body.get("name") or "").strip()
|
|
469
|
+
if not name:
|
|
470
|
+
raise HTTPException(status_code=400, detail="name is required")
|
|
471
|
+
scope = (body.get("scope") or "global").strip() or "global"
|
|
472
|
+
want_hooks = bool(body.get("install_hooks", False))
|
|
473
|
+
rec = store.register_agent(name, scope=scope, hooks_installed=want_hooks)
|
|
474
|
+
if want_hooks:
|
|
475
|
+
# Best-effort: write MCP + SessionStart hooks for whatever
|
|
476
|
+
# local CLI clients the user has installed. We don't
|
|
477
|
+
# surface the per-client summary here; ``install-hooks``
|
|
478
|
+
# already prints its own summary when run from the CLI.
|
|
479
|
+
try:
|
|
480
|
+
from ...cli.commands.hooks import run_install_hooks
|
|
481
|
+
run_install_hooks([])
|
|
482
|
+
except Exception:
|
|
483
|
+
pass
|
|
484
|
+
return rec
|
|
485
|
+
|
|
486
|
+
|
|
395
487
|
@app.get("/api/pipeline")
|
|
396
488
|
def pipeline_dashboard_route():
|
|
397
489
|
"""5-stage data flow dashboard. See handlers.pipeline_dashboard."""
|