loop-memory 0.4.7__tar.gz → 0.4.9__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.7 → loop_memory-0.4.9}/PKG-INFO +21 -1
- {loop_memory-0.4.7 → loop_memory-0.4.9}/README.md +20 -0
- {loop_memory-0.4.7 → loop_memory-0.4.9}/loop_memory/cli/commands/cognitive.py +33 -0
- {loop_memory-0.4.7 → loop_memory-0.4.9}/loop_memory/cli/commands/hooks.py +74 -6
- {loop_memory-0.4.7 → loop_memory-0.4.9}/loop_memory/cli/commands/read.py +114 -8
- {loop_memory-0.4.7 → loop_memory-0.4.9}/loop_memory/cli/main.py +12 -1
- {loop_memory-0.4.7 → loop_memory-0.4.9}/loop_memory/serve/routes/system.py +147 -3
- {loop_memory-0.4.7 → loop_memory-0.4.9}/loop_memory/storage/sqlite_store.py +766 -1
- {loop_memory-0.4.7 → loop_memory-0.4.9}/loop_memory.egg-info/PKG-INFO +21 -1
- {loop_memory-0.4.7 → loop_memory-0.4.9}/loop_memory.egg-info/SOURCES.txt +10 -0
- {loop_memory-0.4.7 → loop_memory-0.4.9}/pyproject.toml +1 -1
- loop_memory-0.4.9/tests/test_agents_init.py +154 -0
- loop_memory-0.4.9/tests/test_cli_export_okf.py +89 -0
- loop_memory-0.4.9/tests/test_cli_init.py +106 -0
- loop_memory-0.4.9/tests/test_cli_recall_as_of.py +90 -0
- loop_memory-0.4.9/tests/test_cli_recall_paths.py +155 -0
- loop_memory-0.4.9/tests/test_export_okf.py +162 -0
- loop_memory-0.4.9/tests/test_recall_as_of.py +154 -0
- loop_memory-0.4.9/tests/test_recall_paths.py +213 -0
- loop_memory-0.4.9/tests/test_routes_audit_2026_09_13.py +141 -0
- loop_memory-0.4.9/tests/test_routes_audit_2026_09_20.py +119 -0
- {loop_memory-0.4.7 → loop_memory-0.4.9}/tests/test_supersession_chain.py +1 -1
- {loop_memory-0.4.7 → loop_memory-0.4.9}/LICENSE +0 -0
- {loop_memory-0.4.7 → loop_memory-0.4.9}/loop_memory/__init__.py +0 -0
- {loop_memory-0.4.7 → loop_memory-0.4.9}/loop_memory/backends/__init__.py +0 -0
- {loop_memory-0.4.7 → loop_memory-0.4.9}/loop_memory/backends/embedding.py +0 -0
- {loop_memory-0.4.7 → loop_memory-0.4.9}/loop_memory/backends/sentence_embedder.py +0 -0
- {loop_memory-0.4.7 → loop_memory-0.4.9}/loop_memory/backends/vector_store.py +0 -0
- {loop_memory-0.4.7 → loop_memory-0.4.9}/loop_memory/cli/__init__.py +0 -0
- {loop_memory-0.4.7 → loop_memory-0.4.9}/loop_memory/cli/_common.py +0 -0
- {loop_memory-0.4.7 → loop_memory-0.4.9}/loop_memory/cli/commands/__init__.py +0 -0
- {loop_memory-0.4.7 → loop_memory-0.4.9}/loop_memory/cli/commands/diag.py +0 -0
- {loop_memory-0.4.7 → loop_memory-0.4.9}/loop_memory/cli/commands/graph.py +0 -0
- {loop_memory-0.4.7 → loop_memory-0.4.9}/loop_memory/cli/commands/rules.py +0 -0
- {loop_memory-0.4.7 → loop_memory-0.4.9}/loop_memory/cli/commands/serve.py +0 -0
- {loop_memory-0.4.7 → loop_memory-0.4.9}/loop_memory/cli/commands/write.py +0 -0
- {loop_memory-0.4.7 → loop_memory-0.4.9}/loop_memory/engine/__init__.py +0 -0
- {loop_memory-0.4.7 → loop_memory-0.4.9}/loop_memory/engine/loop.py +0 -0
- {loop_memory-0.4.7 → loop_memory-0.4.9}/loop_memory/engine/reflect.py +0 -0
- {loop_memory-0.4.7 → loop_memory-0.4.9}/loop_memory/examples/__init__.py +0 -0
- {loop_memory-0.4.7 → loop_memory-0.4.9}/loop_memory/examples/demo.py +0 -0
- {loop_memory-0.4.7 → loop_memory-0.4.9}/loop_memory/export/__init__.py +0 -0
- {loop_memory-0.4.7 → loop_memory-0.4.9}/loop_memory/export/memory_md.py +0 -0
- {loop_memory-0.4.7 → loop_memory-0.4.9}/loop_memory/graph/__init__.py +0 -0
- {loop_memory-0.4.7 → loop_memory-0.4.9}/loop_memory/graph/build.py +0 -0
- {loop_memory-0.4.7 → loop_memory-0.4.9}/loop_memory/graph/extract.py +0 -0
- {loop_memory-0.4.7 → loop_memory-0.4.9}/loop_memory/ingest/__init__.py +0 -0
- {loop_memory-0.4.7 → loop_memory-0.4.9}/loop_memory/ingest/loader.py +0 -0
- {loop_memory-0.4.7 → loop_memory-0.4.9}/loop_memory/ingest/pipeline.py +0 -0
- {loop_memory-0.4.7 → loop_memory-0.4.9}/loop_memory/jobs/__init__.py +0 -0
- {loop_memory-0.4.7 → loop_memory-0.4.9}/loop_memory/jobs/cognitive.py +0 -0
- {loop_memory-0.4.7 → loop_memory-0.4.9}/loop_memory/jobs/compact.py +0 -0
- {loop_memory-0.4.7 → loop_memory-0.4.9}/loop_memory/jobs/consolidate.py +0 -0
- {loop_memory-0.4.7 → loop_memory-0.4.9}/loop_memory/jobs/contradiction.py +0 -0
- {loop_memory-0.4.7 → loop_memory-0.4.9}/loop_memory/jobs/evolution.py +0 -0
- {loop_memory-0.4.7 → loop_memory-0.4.9}/loop_memory/jobs/graph.py +0 -0
- {loop_memory-0.4.7 → loop_memory-0.4.9}/loop_memory/jobs/llm_compact_pass.py +0 -0
- {loop_memory-0.4.7 → loop_memory-0.4.9}/loop_memory/jobs/llm_consolidate.py +0 -0
- {loop_memory-0.4.7 → loop_memory-0.4.9}/loop_memory/jobs/scheduler.py +0 -0
- {loop_memory-0.4.7 → loop_memory-0.4.9}/loop_memory/llm/__init__.py +0 -0
- {loop_memory-0.4.7 → loop_memory-0.4.9}/loop_memory/llm/base.py +0 -0
- {loop_memory-0.4.7 → loop_memory-0.4.9}/loop_memory/llm/openai_adapter.py +0 -0
- {loop_memory-0.4.7 → loop_memory-0.4.9}/loop_memory/llm/providers.py +0 -0
- {loop_memory-0.4.7 → loop_memory-0.4.9}/loop_memory/mcp/__init__.py +0 -0
- {loop_memory-0.4.7 → loop_memory-0.4.9}/loop_memory/memory/__init__.py +0 -0
- {loop_memory-0.4.7 → loop_memory-0.4.9}/loop_memory/memory/types.py +0 -0
- {loop_memory-0.4.7 → loop_memory-0.4.9}/loop_memory/privacy/__init__.py +0 -0
- {loop_memory-0.4.7 → loop_memory-0.4.9}/loop_memory/privacy/private.py +0 -0
- {loop_memory-0.4.7 → loop_memory-0.4.9}/loop_memory/privacy/redact.py +0 -0
- {loop_memory-0.4.7 → loop_memory-0.4.9}/loop_memory/py.typed +0 -0
- {loop_memory-0.4.7 → loop_memory-0.4.9}/loop_memory/sdk.py +0 -0
- {loop_memory-0.4.7 → loop_memory-0.4.9}/loop_memory/sdk_extensions.py +0 -0
- {loop_memory-0.4.7 → loop_memory-0.4.9}/loop_memory/security/__init__.py +0 -0
- {loop_memory-0.4.7 → loop_memory-0.4.9}/loop_memory/security/secrets.py +0 -0
- {loop_memory-0.4.7 → loop_memory-0.4.9}/loop_memory/serve/__init__.py +0 -0
- {loop_memory-0.4.7 → loop_memory-0.4.9}/loop_memory/serve/app.py +0 -0
- {loop_memory-0.4.7 → loop_memory-0.4.9}/loop_memory/serve/handlers.py +0 -0
- {loop_memory-0.4.7 → loop_memory-0.4.9}/loop_memory/serve/routes/_shared.py +0 -0
- {loop_memory-0.4.7 → loop_memory-0.4.9}/loop_memory/serve/routes/admin.py +0 -0
- {loop_memory-0.4.7 → loop_memory-0.4.9}/loop_memory/serve/routes/cognitive.py +0 -0
- {loop_memory-0.4.7 → loop_memory-0.4.9}/loop_memory/serve/routes/export.py +0 -0
- {loop_memory-0.4.7 → loop_memory-0.4.9}/loop_memory/serve/routes/graph.py +0 -0
- {loop_memory-0.4.7 → loop_memory-0.4.9}/loop_memory/serve/routes/insights.py +0 -0
- {loop_memory-0.4.7 → loop_memory-0.4.9}/loop_memory/serve/routes/memories.py +0 -0
- {loop_memory-0.4.7 → loop_memory-0.4.9}/loop_memory/serve/routes/sessions.py +0 -0
- {loop_memory-0.4.7 → loop_memory-0.4.9}/loop_memory/serve/routes/wiki.py +0 -0
- {loop_memory-0.4.7 → loop_memory-0.4.9}/loop_memory/serve/static/__init__.py +0 -0
- {loop_memory-0.4.7 → loop_memory-0.4.9}/loop_memory/serve/static/index.html +0 -0
- {loop_memory-0.4.7 → loop_memory-0.4.9}/loop_memory/serve/watcher.py +0 -0
- {loop_memory-0.4.7 → loop_memory-0.4.9}/loop_memory/storage/__init__.py +0 -0
- {loop_memory-0.4.7 → loop_memory-0.4.9}/loop_memory/storage/retrieval.py +0 -0
- {loop_memory-0.4.7 → loop_memory-0.4.9}/loop_memory/storage/snapshot.py +0 -0
- {loop_memory-0.4.7 → loop_memory-0.4.9}/loop_memory/wiki/__init__.py +0 -0
- {loop_memory-0.4.7 → loop_memory-0.4.9}/loop_memory/wiki/backfill.py +0 -0
- {loop_memory-0.4.7 → loop_memory-0.4.9}/loop_memory/wiki/classifier.py +0 -0
- {loop_memory-0.4.7 → loop_memory-0.4.9}/loop_memory/wiki/prompts.py +0 -0
- {loop_memory-0.4.7 → loop_memory-0.4.9}/loop_memory/wiki/scope.py +0 -0
- {loop_memory-0.4.7 → loop_memory-0.4.9}/loop_memory.egg-info/dependency_links.txt +0 -0
- {loop_memory-0.4.7 → loop_memory-0.4.9}/loop_memory.egg-info/entry_points.txt +0 -0
- {loop_memory-0.4.7 → loop_memory-0.4.9}/loop_memory.egg-info/requires.txt +0 -0
- {loop_memory-0.4.7 → loop_memory-0.4.9}/loop_memory.egg-info/top_level.txt +0 -0
- {loop_memory-0.4.7 → loop_memory-0.4.9}/setup.cfg +0 -0
- {loop_memory-0.4.7 → loop_memory-0.4.9}/tests/test_admin_ingest_route.py +0 -0
- {loop_memory-0.4.7 → loop_memory-0.4.9}/tests/test_agent_memory_api.py +0 -0
- {loop_memory-0.4.7 → loop_memory-0.4.9}/tests/test_agent_memory_sdk.py +0 -0
- {loop_memory-0.4.7 → loop_memory-0.4.9}/tests/test_auth_token_rotate.py +0 -0
- {loop_memory-0.4.7 → loop_memory-0.4.9}/tests/test_cli_audit_supersede.py +0 -0
- {loop_memory-0.4.7 → loop_memory-0.4.9}/tests/test_cli_memory_stats.py +0 -0
- {loop_memory-0.4.7 → loop_memory-0.4.9}/tests/test_cli_recall_verbose.py +0 -0
- {loop_memory-0.4.7 → loop_memory-0.4.9}/tests/test_cli_rules.py +0 -0
- {loop_memory-0.4.7 → loop_memory-0.4.9}/tests/test_cli_snapshot.py +0 -0
- {loop_memory-0.4.7 → loop_memory-0.4.9}/tests/test_cli_v7.py +0 -0
- {loop_memory-0.4.7 → loop_memory-0.4.9}/tests/test_cli_version.py +0 -0
- {loop_memory-0.4.7 → loop_memory-0.4.9}/tests/test_contradictions.py +0 -0
- {loop_memory-0.4.7 → loop_memory-0.4.9}/tests/test_evolution.py +0 -0
- {loop_memory-0.4.7 → loop_memory-0.4.9}/tests/test_evolution_quality.py +0 -0
- {loop_memory-0.4.7 → loop_memory-0.4.9}/tests/test_expanduser.py +0 -0
- {loop_memory-0.4.7 → loop_memory-0.4.9}/tests/test_export_ask.py +0 -0
- {loop_memory-0.4.7 → loop_memory-0.4.9}/tests/test_graph.py +0 -0
- {loop_memory-0.4.7 → loop_memory-0.4.9}/tests/test_ingest.py +0 -0
- {loop_memory-0.4.7 → loop_memory-0.4.9}/tests/test_llm_consolidator.py +0 -0
- {loop_memory-0.4.7 → loop_memory-0.4.9}/tests/test_llm_fingerprint_not_persisted.py +0 -0
- {loop_memory-0.4.7 → loop_memory-0.4.9}/tests/test_llm_providers.py +0 -0
- {loop_memory-0.4.7 → loop_memory-0.4.9}/tests/test_llm_test_endpoint.py +0 -0
- {loop_memory-0.4.7 → loop_memory-0.4.9}/tests/test_loop.py +0 -0
- {loop_memory-0.4.7 → loop_memory-0.4.9}/tests/test_mcp.py +0 -0
- {loop_memory-0.4.7 → loop_memory-0.4.9}/tests/test_memories_pagination.py +0 -0
- {loop_memory-0.4.7 → loop_memory-0.4.9}/tests/test_memory_stats.py +0 -0
- {loop_memory-0.4.7 → loop_memory-0.4.9}/tests/test_openclaw_loader.py +0 -0
- {loop_memory-0.4.7 → loop_memory-0.4.9}/tests/test_package_version.py +0 -0
- {loop_memory-0.4.7 → loop_memory-0.4.9}/tests/test_recall_provenance.py +0 -0
- {loop_memory-0.4.7 → loop_memory-0.4.9}/tests/test_reflection.py +0 -0
- {loop_memory-0.4.7 → loop_memory-0.4.9}/tests/test_repo_framing.py +0 -0
- {loop_memory-0.4.7 → loop_memory-0.4.9}/tests/test_score_api.py +0 -0
- {loop_memory-0.4.7 → loop_memory-0.4.9}/tests/test_scoring_v2.py +0 -0
- {loop_memory-0.4.7 → loop_memory-0.4.9}/tests/test_secrets.py +0 -0
- {loop_memory-0.4.7 → loop_memory-0.4.9}/tests/test_serve_app.py +0 -0
- {loop_memory-0.4.7 → loop_memory-0.4.9}/tests/test_serve_handlers.py +0 -0
- {loop_memory-0.4.7 → loop_memory-0.4.9}/tests/test_session_order.py +0 -0
- {loop_memory-0.4.7 → loop_memory-0.4.9}/tests/test_short_query.py +0 -0
- {loop_memory-0.4.7 → loop_memory-0.4.9}/tests/test_snapshot.py +0 -0
- {loop_memory-0.4.7 → loop_memory-0.4.9}/tests/test_store.py +0 -0
- {loop_memory-0.4.7 → loop_memory-0.4.9}/tests/test_summarization.py +0 -0
- {loop_memory-0.4.7 → loop_memory-0.4.9}/tests/test_universal_memory.py +0 -0
- {loop_memory-0.4.7 → loop_memory-0.4.9}/tests/test_vector_store.py +0 -0
- {loop_memory-0.4.7 → loop_memory-0.4.9}/tests/test_watcher.py +0 -0
- {loop_memory-0.4.7 → loop_memory-0.4.9}/tests/test_wiki_classifier.py +0 -0
- {loop_memory-0.4.7 → loop_memory-0.4.9}/tests/test_wiki_export_escape.py +0 -0
- {loop_memory-0.4.7 → loop_memory-0.4.9}/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.9
|
|
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,26 @@ Dynamic: license-file
|
|
|
66
66
|
|
|
67
67
|
---
|
|
68
68
|
|
|
69
|
+
> **What's new in 0.4.9** — **OKF v0.2 export** + **bi-temporal `as_of` recall**. `loop-memory export-okf <dir>` writes an [Open Knowledge Format](https://github.com/okf/SPEC) v0.2 bundle — one `.md` file per wiki page with YAML frontmatter + an `index.md` index — so a Loop Memory store can be handed to any OKF-aware tool (Grep, Obsidian, akitaonrails/ai-memory, okf-agent-memory, ...) without re-parsing the body (audit 2026-09-20, Google OKF v0.2 spec + akitaonrails/ai-memory 2.0 + okf-memory/okf-agent-memory, all Apache-2.0/MIT, 2026-09-01 → 2026-09-06). `loop-memory recall <q> --as-of <ISO|epoch>` answers the bi-temporal question "what did we know about this query at that moment?" — a memory written after the moment is invisible, and a superseded memory walks its chain to find whether the invalidation had happened by then (audit 2026-09-20, loomcycle v1.33-v1.49 RFCs BL/BS/BU/BV/BW, Apache-2.0). Both shapes ship with HTTP routes (`POST /api/export/okf`, `GET /api/recall?as_of=...`). 34 new regression cases pin both shapes. [Full CHANGELOG →](./CHANGELOG.md#049---2026-09-20)
|
|
70
|
+
|
|
71
|
+
> **What's new in 0.4.8** — **L0 outline recall** +
|
|
72
|
+
> **per-agent identity bootstrap**. `loop-memory recall-paths <q>`
|
|
73
|
+
> (and the new `recall --outline` flag) returns only `id` +
|
|
74
|
+
> `abstract` + `score` + `why` for each hit — never the full
|
|
75
|
+
> body — so an agent can identify the top candidates cheaply
|
|
76
|
+
> before deciding which bodies to fetch (audit 2026-09-13,
|
|
77
|
+
> tigerless-labs/agent-memory v0.3.0 recall-ladder pattern).
|
|
78
|
+
> `loop-memory init --agent <name>` registers an agent in the
|
|
79
|
+
> new `agents` table and (with `--install-hooks`) also wires
|
|
80
|
+
> the MCP + SessionStart hooks for codex/claude/hermes; the
|
|
81
|
+
> index is bumped on every `install-hooks` run so a user can
|
|
82
|
+
> always answer "which CLIs are wired into this store?" with
|
|
83
|
+
> one SQL query (audit 2026-09-13, Mem0 CLI `init --agent`
|
|
84
|
+
> pattern). Both shapes ship with HTTP routes
|
|
85
|
+
> (`/api/recall/outline`, `/api/agents`, `/api/init/agent`).
|
|
86
|
+
> 57 new regression cases pin both shapes. [Full
|
|
87
|
+
> changelog →](CHANGELOG.md)
|
|
88
|
+
>
|
|
69
89
|
> **What's new in 0.4.7** — **per-memory lifetime stats** +
|
|
70
90
|
> **portable SQLite snapshot**. `loop-memory memory-stats <id>`
|
|
71
91
|
> returns a flat dict (recall_count, positive, negative, age,
|
|
@@ -22,6 +22,26 @@
|
|
|
22
22
|
|
|
23
23
|
---
|
|
24
24
|
|
|
25
|
+
> **What's new in 0.4.9** — **OKF v0.2 export** + **bi-temporal `as_of` recall**. `loop-memory export-okf <dir>` writes an [Open Knowledge Format](https://github.com/okf/SPEC) v0.2 bundle — one `.md` file per wiki page with YAML frontmatter + an `index.md` index — so a Loop Memory store can be handed to any OKF-aware tool (Grep, Obsidian, akitaonrails/ai-memory, okf-agent-memory, ...) without re-parsing the body (audit 2026-09-20, Google OKF v0.2 spec + akitaonrails/ai-memory 2.0 + okf-memory/okf-agent-memory, all Apache-2.0/MIT, 2026-09-01 → 2026-09-06). `loop-memory recall <q> --as-of <ISO|epoch>` answers the bi-temporal question "what did we know about this query at that moment?" — a memory written after the moment is invisible, and a superseded memory walks its chain to find whether the invalidation had happened by then (audit 2026-09-20, loomcycle v1.33-v1.49 RFCs BL/BS/BU/BV/BW, Apache-2.0). Both shapes ship with HTTP routes (`POST /api/export/okf`, `GET /api/recall?as_of=...`). 34 new regression cases pin both shapes. [Full CHANGELOG →](./CHANGELOG.md#049---2026-09-20)
|
|
26
|
+
|
|
27
|
+
> **What's new in 0.4.8** — **L0 outline recall** +
|
|
28
|
+
> **per-agent identity bootstrap**. `loop-memory recall-paths <q>`
|
|
29
|
+
> (and the new `recall --outline` flag) returns only `id` +
|
|
30
|
+
> `abstract` + `score` + `why` for each hit — never the full
|
|
31
|
+
> body — so an agent can identify the top candidates cheaply
|
|
32
|
+
> before deciding which bodies to fetch (audit 2026-09-13,
|
|
33
|
+
> tigerless-labs/agent-memory v0.3.0 recall-ladder pattern).
|
|
34
|
+
> `loop-memory init --agent <name>` registers an agent in the
|
|
35
|
+
> new `agents` table and (with `--install-hooks`) also wires
|
|
36
|
+
> the MCP + SessionStart hooks for codex/claude/hermes; the
|
|
37
|
+
> index is bumped on every `install-hooks` run so a user can
|
|
38
|
+
> always answer "which CLIs are wired into this store?" with
|
|
39
|
+
> one SQL query (audit 2026-09-13, Mem0 CLI `init --agent`
|
|
40
|
+
> pattern). Both shapes ship with HTTP routes
|
|
41
|
+
> (`/api/recall/outline`, `/api/agents`, `/api/init/agent`).
|
|
42
|
+
> 57 new regression cases pin both shapes. [Full
|
|
43
|
+
> changelog →](CHANGELOG.md)
|
|
44
|
+
>
|
|
25
45
|
> **What's new in 0.4.7** — **per-memory lifetime stats** +
|
|
26
46
|
> **portable SQLite snapshot**. `loop-memory memory-stats <id>`
|
|
27
47
|
> returns a flat dict (recall_count, positive, negative, age,
|
|
@@ -108,6 +108,17 @@ def _build_parser() -> argparse.ArgumentParser:
|
|
|
108
108
|
rs.add_argument("in_path",
|
|
109
109
|
help="Source .memory.sqlite file")
|
|
110
110
|
|
|
111
|
+
# Audit 2026-09-20: OKF v0.2 bundle export
|
|
112
|
+
# (akitaonrails/ai-memory 2.0 + okf-memory/okf-agent-memory +
|
|
113
|
+
# Google OKF v0.2 spec).
|
|
114
|
+
eo = sub.add_parser(
|
|
115
|
+
"export-okf",
|
|
116
|
+
help="Export wiki pages as an OKF v0.2 markdown bundle")
|
|
117
|
+
eo.add_argument("out_dir",
|
|
118
|
+
help="Destination directory for the bundle")
|
|
119
|
+
eo.add_argument("--scope", default=None,
|
|
120
|
+
help="Filter: 'global' or a single source token (e.g. codex)")
|
|
121
|
+
|
|
111
122
|
return p
|
|
112
123
|
|
|
113
124
|
|
|
@@ -342,3 +353,25 @@ def run_restore(args: list) -> int:
|
|
|
342
353
|
return _emit(_restore(s, ns.in_path))
|
|
343
354
|
except (FileNotFoundError, ValueError) as e:
|
|
344
355
|
return _emit({"error": str(e)})
|
|
356
|
+
|
|
357
|
+
|
|
358
|
+
def run_export_okf(args) -> int:
|
|
359
|
+
"""``loop-memory export-okf <out_dir> [--scope SCOPE]``
|
|
360
|
+
|
|
361
|
+
Audit 2026-09-20: OKF v0.2 bundle export
|
|
362
|
+
(akitaonrails/ai-memory 2.0 + okf-memory/okf-agent-memory +
|
|
363
|
+
Google OKF v0.2 spec). Returns a summary dict with the bundle
|
|
364
|
+
root + per-page list so the caller can confirm the bundle is
|
|
365
|
+
sane. ``--scope`` accepts ``global`` (only globally-scoped
|
|
366
|
+
pages) or a single source token (only pages whose scope list
|
|
367
|
+
contains the token); omit the flag to emit every page.
|
|
368
|
+
"""
|
|
369
|
+
import argparse as _ap
|
|
370
|
+
p = _ap.ArgumentParser(prog="loop-memory export-okf")
|
|
371
|
+
p.add_argument("out_dir")
|
|
372
|
+
p.add_argument("--scope", default=None)
|
|
373
|
+
p.add_argument("--db", default=os.environ.get("LOOP_MEMORY_DB", DEFAULT_DB))
|
|
374
|
+
ns = p.parse_args(args)
|
|
375
|
+
s = MemoryStore(ns.db)
|
|
376
|
+
r = s.export_okf(ns.out_dir, scope_filter=ns.scope)
|
|
377
|
+
return _emit(r)
|
|
@@ -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
|
|
@@ -51,13 +51,17 @@ def run_stats(_args) -> int:
|
|
|
51
51
|
|
|
52
52
|
|
|
53
53
|
def run_recall(args) -> int:
|
|
54
|
-
"""Run ``loop-memory recall <query> [--verbose] [--limit N]``.
|
|
54
|
+
"""Run ``loop-memory recall <query> [--verbose] [--outline] [--as-of T] [--limit N]``.
|
|
55
55
|
|
|
56
56
|
With ``--verbose`` each memory hit is annotated with the
|
|
57
57
|
``why: [...]`` provenance labels produced by ``MemoryStore.recall``
|
|
58
|
-
(audit 2026-08-30, agentmemory v1.2.0 pattern).
|
|
59
|
-
|
|
60
|
-
|
|
58
|
+
(audit 2026-08-30, agentmemory v1.2.0 pattern). ``--outline``
|
|
59
|
+
switches to the L0 list (audit 2026-09-13, tigerless-labs/
|
|
60
|
+
agent-memory v0.3.0). ``--as-of <float-or-ISO>`` enables
|
|
61
|
+
bi-temporal recall (audit 2026-09-20, loomcycle v1.33+) so a
|
|
62
|
+
caller can ask "what did we know about this query at that
|
|
63
|
+
moment?". The wiki and entity sections are unchanged — only
|
|
64
|
+
the raw-memory block grows the new line.
|
|
61
65
|
"""
|
|
62
66
|
from ...storage.sqlite_store import MemoryStore
|
|
63
67
|
if not args:
|
|
@@ -66,7 +70,9 @@ def run_recall(args) -> int:
|
|
|
66
70
|
# we don't want argparse to swallow a token that happens to start
|
|
67
71
|
# with "--" inside the user's query (e.g. ``recall --foo bar``).
|
|
68
72
|
verbose = False
|
|
73
|
+
outline = False
|
|
69
74
|
limit = 10
|
|
75
|
+
as_of: float | None = None
|
|
70
76
|
qargs: list[str] = []
|
|
71
77
|
i = 0
|
|
72
78
|
while i < len(args):
|
|
@@ -74,6 +80,30 @@ def run_recall(args) -> int:
|
|
|
74
80
|
if a == "--verbose":
|
|
75
81
|
verbose = True
|
|
76
82
|
i += 1
|
|
83
|
+
elif a == "--outline":
|
|
84
|
+
# Audit 2026-09-13 (tigerless-labs/agent-memory v0.3.0) —
|
|
85
|
+
# cheap L0 list instead of full text. Sets a flag and
|
|
86
|
+
# routes the call to ``recall_paths`` below.
|
|
87
|
+
outline = True
|
|
88
|
+
i += 1
|
|
89
|
+
elif a == "--as-of" and i + 1 < len(args):
|
|
90
|
+
# Audit 2026-09-20 (loomcycle v1.33+) — bi-temporal
|
|
91
|
+
# recall: "what did we know about this query at that
|
|
92
|
+
# moment?". Accepts float epoch OR ISO-8601 string.
|
|
93
|
+
raw = args[i + 1].strip()
|
|
94
|
+
try:
|
|
95
|
+
as_of = float(raw)
|
|
96
|
+
except ValueError:
|
|
97
|
+
try:
|
|
98
|
+
from datetime import datetime
|
|
99
|
+
as_of = datetime.fromisoformat(
|
|
100
|
+
raw.replace("Z", "+00:00")
|
|
101
|
+
).timestamp()
|
|
102
|
+
except Exception:
|
|
103
|
+
return die(
|
|
104
|
+
f"--as-of must be float epoch or ISO-8601; got {raw!r}"
|
|
105
|
+
)
|
|
106
|
+
i += 2
|
|
77
107
|
elif a == "--limit" and i + 1 < len(args):
|
|
78
108
|
try:
|
|
79
109
|
limit = int(args[i + 1])
|
|
@@ -86,10 +116,18 @@ def run_recall(args) -> int:
|
|
|
86
116
|
qargs.append(a)
|
|
87
117
|
i += 1
|
|
88
118
|
if not qargs:
|
|
89
|
-
return die("usage: loop-memory recall <query> [--verbose] [--limit N]")
|
|
119
|
+
return die("usage: loop-memory recall <query> [--verbose] [--outline] [--as-of <ISO|epoch>] [--limit N]")
|
|
90
120
|
store = MemoryStore(default_db_path())
|
|
91
121
|
query = " ".join(qargs)
|
|
92
|
-
|
|
122
|
+
if as_of is not None:
|
|
123
|
+
try:
|
|
124
|
+
r = store.recall_as_of(query, as_of, limit=limit)
|
|
125
|
+
except ValueError as e:
|
|
126
|
+
return die(str(e))
|
|
127
|
+
elif outline:
|
|
128
|
+
r = store.recall_paths(query, limit=limit)
|
|
129
|
+
else:
|
|
130
|
+
r = store.recall(query, limit=limit)
|
|
93
131
|
has = False
|
|
94
132
|
if r["wiki"]:
|
|
95
133
|
has = True
|
|
@@ -97,7 +135,9 @@ def run_recall(args) -> int:
|
|
|
97
135
|
for w in r["wiki"]:
|
|
98
136
|
tag_s = " [" + ", ".join(w.get("tags") or []) + "]" if w.get("tags") else ""
|
|
99
137
|
print(f"- **{w['title']}** (`{w['slug']}`) — imp {w['importance']:.2f}{tag_s}")
|
|
100
|
-
if
|
|
138
|
+
if outline:
|
|
139
|
+
print(f" > {w.get('abstract','')}")
|
|
140
|
+
elif w.get("summary"):
|
|
101
141
|
print(f" > {w['summary'][:240]}")
|
|
102
142
|
print()
|
|
103
143
|
if r["memories"]:
|
|
@@ -111,7 +151,11 @@ def run_recall(args) -> int:
|
|
|
111
151
|
# render the list as a deterministic badge strip without
|
|
112
152
|
# re-sorting.
|
|
113
153
|
print(f" why: {', '.join(m['why'])}")
|
|
114
|
-
|
|
154
|
+
if outline:
|
|
155
|
+
# L0 outline mode: print only the short abstract.
|
|
156
|
+
print(f" {m.get('abstract','')}")
|
|
157
|
+
else:
|
|
158
|
+
print(f" {m['text'][:240]}")
|
|
115
159
|
print()
|
|
116
160
|
if r["entities"]:
|
|
117
161
|
has = True
|
|
@@ -398,3 +442,65 @@ def run_inject(args) -> int:
|
|
|
398
442
|
out.write(f" {text}\n")
|
|
399
443
|
sys.stdout.write(out.getvalue())
|
|
400
444
|
return 0
|
|
445
|
+
|
|
446
|
+
|
|
447
|
+
|
|
448
|
+
def run_recall_paths(args) -> int:
|
|
449
|
+
"""``loop-memory recall-paths <query> [--limit N]``.
|
|
450
|
+
|
|
451
|
+
Audit 2026-09-13 (tigerless-labs/agent-memory v0.3.0). Returns
|
|
452
|
+
only ``id`` + ``abstract`` + ``score`` + ``why`` for each hit —
|
|
453
|
+
no full text, no body. Use this when an agent needs to *decide*
|
|
454
|
+
which candidate to open; once it has picked an id, fetch the
|
|
455
|
+
body via ``GET /api/memories/{id}`` or ``GET /api/wiki/{slug}``.
|
|
456
|
+
"""
|
|
457
|
+
from ...storage.sqlite_store import MemoryStore
|
|
458
|
+
if not args:
|
|
459
|
+
return die("usage: loop-memory recall-paths <query> [--limit N]")
|
|
460
|
+
limit = 12
|
|
461
|
+
qargs: list[str] = []
|
|
462
|
+
i = 0
|
|
463
|
+
while i < len(args):
|
|
464
|
+
a = args[i]
|
|
465
|
+
if a == "--limit" and i + 1 < len(args):
|
|
466
|
+
try:
|
|
467
|
+
limit = int(args[i + 1])
|
|
468
|
+
except ValueError:
|
|
469
|
+
return die(f"--limit must be an integer, got {args[i + 1]!r}")
|
|
470
|
+
i += 2
|
|
471
|
+
elif a.startswith("--"):
|
|
472
|
+
return die(f"unknown flag: {a}")
|
|
473
|
+
else:
|
|
474
|
+
qargs.append(a)
|
|
475
|
+
i += 1
|
|
476
|
+
if not qargs:
|
|
477
|
+
return die("usage: loop-memory recall-paths <query> [--limit N]")
|
|
478
|
+
store = MemoryStore(default_db_path())
|
|
479
|
+
query = " ".join(qargs)
|
|
480
|
+
r = store.recall_paths(query, limit=limit)
|
|
481
|
+
has = False
|
|
482
|
+
if r["memories"]:
|
|
483
|
+
has = True
|
|
484
|
+
print(f"## Outline (memories, {len(r['memories'])} match{'es' if len(r['memories'])!=1 else ''})")
|
|
485
|
+
for m in r["memories"]:
|
|
486
|
+
why = f" why: {', '.join(m['why'])}" if m.get("why") else ""
|
|
487
|
+
print(f"- [{m['kind']}] score={m['score']:.2f} id={m['id']}{why}")
|
|
488
|
+
print(f" > {m.get('abstract','')}")
|
|
489
|
+
print()
|
|
490
|
+
if r["wiki"]:
|
|
491
|
+
has = True
|
|
492
|
+
print(f"## Outline (wiki, {len(r['wiki'])} match{'es' if len(r['wiki'])!=1 else ''})")
|
|
493
|
+
for w in r["wiki"]:
|
|
494
|
+
why = f" why: {', '.join(w['why'])}" if w.get("why") else ""
|
|
495
|
+
print(f"- score={w['score']:.2f} slug={w['slug']}{why}")
|
|
496
|
+
print(f" > {w.get('abstract','')}")
|
|
497
|
+
print()
|
|
498
|
+
if r["entities"]:
|
|
499
|
+
has = True
|
|
500
|
+
print(f"## Outline (entities, {len(r['entities'])})")
|
|
501
|
+
for e in r["entities"]:
|
|
502
|
+
print(f"- {e['name']} _({e['entity_kind']}, w={e['weight']:.2f})_")
|
|
503
|
+
print()
|
|
504
|
+
if not has:
|
|
505
|
+
print(f"_( nothing matched {query!r})_")
|
|
506
|
+
return 0
|
|
@@ -122,6 +122,12 @@ COMMANDS = {
|
|
|
122
122
|
"memory-stats": cognitive_cmd.run_memory_stats,
|
|
123
123
|
"snapshot": cognitive_cmd.run_snapshot,
|
|
124
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,
|
|
128
|
+
# Audit 2026-09-20 — OKF v0.2 export (akitaonrails/ai-memory 2.0
|
|
129
|
+
# + okf-agent-memory + Google OKF spec).
|
|
130
|
+
"export-okf": cognitive_cmd.run_export_okf,
|
|
125
131
|
"rules": rules_cmd.run_rules,
|
|
126
132
|
"version": _run_version,
|
|
127
133
|
}
|
|
@@ -155,7 +161,7 @@ COMMAND_HELP: dict[str, str] = {
|
|
|
155
161
|
"install-hooks": "loop-memory install-hooks # auto-write MCP + SessionStart hooks for known clients.",
|
|
156
162
|
"mcp": "loop-memory mcp # stdio MCP server (for codex/claude/hermes). Requires ``[serve]`` for HTTP transport.",
|
|
157
163
|
"openclaw-setup": "loop-memory openclaw-setup # install the openclaw/clawx auto-ingest watcher (launchd on macOS).",
|
|
158
|
-
"recall": "loop-memory recall <text> # show top memories.",
|
|
164
|
+
"recall": "loop-memory recall <text> [--as-of <ISO|epoch>] # show top memories (--as-of = bi-temporal recall).",
|
|
159
165
|
"rescore": "loop-memory rescore [--half-life 30] # recompute memory scores.",
|
|
160
166
|
"serve": "loop-memory serve [--port 7767] [--no-browser] # start the local web UI. Requires the ``[serve]`` extra (fastapi + uvicorn).",
|
|
161
167
|
"stats": "loop-memory stats # counters.",
|
|
@@ -165,7 +171,12 @@ COMMAND_HELP: dict[str, str] = {
|
|
|
165
171
|
"wiki-reclassify-legacy": "loop-memory wiki-reclassify-legacy # back-fill scope + scope_filter for H4+H5 pages.",
|
|
166
172
|
"memory-stats": "loop-memory memory-stats <id> [--prefix] # per-memory lifetime stats (audit 2026-09-06, agentmemory v1.3.0).",
|
|
167
173
|
"snapshot": "loop-memory snapshot <out.memory.sqlite> # write a portable SQLite snapshot (audit 2026-09-06, codexa-memory v0.2.0).",
|
|
174
|
+
"export-okf": "loop-memory export-okf <out_dir> [--scope S] # write an OKF v0.2 bundle (audit 2026-09-20).",
|
|
168
175
|
"restore": "loop-memory restore <in.memory.sqlite> # re-hydrate a portable SQLite snapshot.",
|
|
176
|
+
# Audit 2026-09-13 — L0 outline recall (tigerless-labs/agent-memory v0.3.0).
|
|
177
|
+
"recall-paths": "loop-memory recall-paths <query> [--limit N] # cheap L0 outline (id+abstract+score) instead of full text (audit 2026-09-13).",
|
|
178
|
+
# Audit 2026-09-13 — per-agent identity bootstrap (Mem0 CLI init --agent pattern).
|
|
179
|
+
"init": "loop-memory init --agent <name> [--install-hooks] # register an agent + (optionally) install hooks.",
|
|
169
180
|
"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.",
|
|
170
181
|
}
|
|
171
182
|
|
|
@@ -350,7 +350,8 @@ def register(app: FastAPI, store: MemoryStore, scheduler: Optional[Any] = None,
|
|
|
350
350
|
@app.get("/api/recall")
|
|
351
351
|
def recall(query: str, limit: int = 10, include: str = "memories,wiki,entities",
|
|
352
352
|
bump: int = 1, source: str | None = None,
|
|
353
|
-
mode: str = "hybrid", level: int = 1
|
|
353
|
+
mode: str = "hybrid", level: int = 1,
|
|
354
|
+
as_of: str | None = None):
|
|
354
355
|
"""Unified recall — wiki + memories + entities in one ranked stream.
|
|
355
356
|
|
|
356
357
|
``mode`` is 'hybrid' (default) for BM25+semantic+entity RRF,
|
|
@@ -363,11 +364,31 @@ def register(app: FastAPI, store: MemoryStore, scheduler: Optional[Any] = None,
|
|
|
363
364
|
and memories whose source matches this token are boosted. The
|
|
364
365
|
dashboard passes the currently-active client (codex/claude/
|
|
365
366
|
hermes/openclaw); the MCP/CLI tools pass the calling client.
|
|
367
|
+
|
|
368
|
+
``as_of`` (audit 2026-09-20, loomcycle v1.33+) enables
|
|
369
|
+
bi-temporal recall: a float epoch OR an ISO-8601 string that
|
|
370
|
+
answers "what did we know about this query at that moment?".
|
|
371
|
+
When unset (default) the call returns the latest answer
|
|
372
|
+
(preserves the pre-0.4.9 contract). When set, the call
|
|
373
|
+
routes through ``MemoryStore.recall_as_of``.
|
|
366
374
|
"""
|
|
367
375
|
wanted = tuple(s.strip() for s in include.split(",") if s.strip())
|
|
368
376
|
if not wanted:
|
|
369
377
|
wanted = ("memories", "wiki", "entities")
|
|
370
|
-
if
|
|
378
|
+
if as_of is not None and str(as_of).strip():
|
|
379
|
+
try:
|
|
380
|
+
r = store.recall_as_of(
|
|
381
|
+
query,
|
|
382
|
+
as_of,
|
|
383
|
+
limit=limit,
|
|
384
|
+
include=wanted,
|
|
385
|
+
bump_signals=bool(bump),
|
|
386
|
+
source=source,
|
|
387
|
+
)
|
|
388
|
+
recall_mode = "as_of"
|
|
389
|
+
except ValueError as e:
|
|
390
|
+
raise HTTPException(400, str(e))
|
|
391
|
+
elif mode == "legacy" or not hasattr(store, "recall_hybrid"):
|
|
371
392
|
r = store.recall(
|
|
372
393
|
query,
|
|
373
394
|
limit=limit,
|
|
@@ -375,23 +396,146 @@ def register(app: FastAPI, store: MemoryStore, scheduler: Optional[Any] = None,
|
|
|
375
396
|
bump_signals=bool(bump),
|
|
376
397
|
source=source,
|
|
377
398
|
)
|
|
399
|
+
recall_mode = mode
|
|
378
400
|
else:
|
|
379
401
|
r = store.recall_hybrid(
|
|
380
402
|
query, limit=limit, include=wanted,
|
|
381
403
|
bump_signals=bool(bump), source=source, level=level,
|
|
382
404
|
)
|
|
405
|
+
recall_mode = mode
|
|
383
406
|
return {
|
|
384
407
|
"query": query,
|
|
385
408
|
"tokens": r.get("tokens", []),
|
|
386
409
|
"memories": r.get("memories", []),
|
|
387
410
|
"wiki": r.get("wiki", []),
|
|
388
411
|
"entities": r.get("entities", []),
|
|
389
|
-
"mode":
|
|
412
|
+
"mode": recall_mode,
|
|
390
413
|
"source": source,
|
|
391
414
|
"level": level,
|
|
415
|
+
"as_of": as_of,
|
|
392
416
|
}
|
|
393
417
|
|
|
394
418
|
|
|
419
|
+
@app.post("/api/export/okf")
|
|
420
|
+
def export_okf(body: dict):
|
|
421
|
+
"""Audit 2026-09-20: OKF v0.2 bundle export.
|
|
422
|
+
|
|
423
|
+
Body shape::
|
|
424
|
+
|
|
425
|
+
{
|
|
426
|
+
"out_dir": "/abs/path/to/out",
|
|
427
|
+
"scope": "global" # optional: "global" or a source token
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
Writes one ``.md`` file per wiki page under
|
|
431
|
+
``<out_dir>/pages/<slug>.md`` plus an ``index.md`` index.
|
|
432
|
+
See ``MemoryStore.export_okf`` for the field-mapping
|
|
433
|
+
contract.
|
|
434
|
+
"""
|
|
435
|
+
out_dir = (body.get("out_dir") or "").strip()
|
|
436
|
+
if not out_dir:
|
|
437
|
+
raise HTTPException(400, "out_dir is required")
|
|
438
|
+
scope = body.get("scope")
|
|
439
|
+
if scope is not None:
|
|
440
|
+
scope = str(scope).strip() or None
|
|
441
|
+
try:
|
|
442
|
+
r = store.export_okf(out_dir, scope_filter=scope)
|
|
443
|
+
except Exception as e:
|
|
444
|
+
raise HTTPException(500, f"export-okf failed: {e}")
|
|
445
|
+
return r
|
|
446
|
+
|
|
447
|
+
@app.get("/api/recall/outline")
|
|
448
|
+
def recall_outline(query: str = "", limit: int = 12,
|
|
449
|
+
include: str = "memories,wiki,entities",
|
|
450
|
+
bump: int = 0, source: str | None = None):
|
|
451
|
+
"""Audit 2026-09-13 — L0 outline recall (tigerless-labs/agent-memory
|
|
452
|
+
v0.3.0 recall ladder pattern).
|
|
453
|
+
|
|
454
|
+
Returns the same ranked lists as ``GET /api/recall`` but each
|
|
455
|
+
hit carries only ``id`` + ``kind`` + ``abstract`` (≤80 chars)
|
|
456
|
+
+ ``score`` + ``why`` -- never the full ``text`` / ``body``.
|
|
457
|
+
Use this when an agent needs to *decide* which candidate to
|
|
458
|
+
open; once it has picked an id, fetch the body via
|
|
459
|
+
``GET /api/memories/{id}`` or ``GET /api/wiki/{slug}``.
|
|
460
|
+
|
|
461
|
+
``bump`` defaults to 0 so the L0 listing does not inflate
|
|
462
|
+
``recall_count`` before the agent has actually decided to read
|
|
463
|
+
the body. Set ``bump=1`` only when the agent commits to the
|
|
464
|
+
candidate set.
|
|
465
|
+
"""
|
|
466
|
+
if not query:
|
|
467
|
+
raise HTTPException(status_code=400, detail="query is required")
|
|
468
|
+
wanted = tuple(s.strip() for s in include.split(",") if s.strip())
|
|
469
|
+
if not wanted:
|
|
470
|
+
wanted = ("memories", "wiki", "entities")
|
|
471
|
+
r = store.recall_paths(
|
|
472
|
+
query,
|
|
473
|
+
limit=limit,
|
|
474
|
+
include=wanted,
|
|
475
|
+
bump_signals=bool(bump),
|
|
476
|
+
source=source,
|
|
477
|
+
)
|
|
478
|
+
return {
|
|
479
|
+
"query": query,
|
|
480
|
+
"tokens": r.get("tokens", []),
|
|
481
|
+
"memories": r.get("memories", []),
|
|
482
|
+
"wiki": r.get("wiki", []),
|
|
483
|
+
"entities": r.get("entities", []),
|
|
484
|
+
"mode": "outline",
|
|
485
|
+
"source": source,
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
|
|
489
|
+
@app.get("/api/agents")
|
|
490
|
+
def list_agents_route():
|
|
491
|
+
"""Audit 2026-09-13 — list all registered agents (Mem0 CLI
|
|
492
|
+
``init --agent`` pattern). Each entry includes ``name``,
|
|
493
|
+
``scope``, ``created_at``, ``last_seen_at``,
|
|
494
|
+
``hooks_installed`` so a user can answer "which CLIs are
|
|
495
|
+
wired into this store?" with a single HTTP GET.
|
|
496
|
+
"""
|
|
497
|
+
return {"agents": store.list_agents()}
|
|
498
|
+
|
|
499
|
+
|
|
500
|
+
@app.post("/api/init/agent")
|
|
501
|
+
def init_agent_route(body: dict):
|
|
502
|
+
"""Audit 2026-09-13 — register (or refresh) a named agent.
|
|
503
|
+
|
|
504
|
+
Body keys (all optional except ``name``):
|
|
505
|
+
- ``name`` (str, required) — e.g. ``"codex"``, ``"claude"``,
|
|
506
|
+
``"hermes"``, ``"openclaw"``, or a custom name.
|
|
507
|
+
- ``scope`` (str, default ``"global"``) — reserved for
|
|
508
|
+
future per-source isolation; today always ``"global"``.
|
|
509
|
+
- ``install_hooks`` (bool, default ``false``) — when true,
|
|
510
|
+
also runs the equivalent of ``install-hooks`` so MCP +
|
|
511
|
+
SessionStart files are written for codex/claude/hermes if
|
|
512
|
+
those clients are installed locally. Best-effort: a hook
|
|
513
|
+
failure does not fail the registration.
|
|
514
|
+
|
|
515
|
+
Idempotent — re-registering an existing name just bumps
|
|
516
|
+
``last_seen_at``. Returns the persisted row.
|
|
517
|
+
"""
|
|
518
|
+
if not isinstance(body, dict):
|
|
519
|
+
raise HTTPException(status_code=400, detail="body must be a JSON object")
|
|
520
|
+
name = (body.get("name") or "").strip()
|
|
521
|
+
if not name:
|
|
522
|
+
raise HTTPException(status_code=400, detail="name is required")
|
|
523
|
+
scope = (body.get("scope") or "global").strip() or "global"
|
|
524
|
+
want_hooks = bool(body.get("install_hooks", False))
|
|
525
|
+
rec = store.register_agent(name, scope=scope, hooks_installed=want_hooks)
|
|
526
|
+
if want_hooks:
|
|
527
|
+
# Best-effort: write MCP + SessionStart hooks for whatever
|
|
528
|
+
# local CLI clients the user has installed. We don't
|
|
529
|
+
# surface the per-client summary here; ``install-hooks``
|
|
530
|
+
# already prints its own summary when run from the CLI.
|
|
531
|
+
try:
|
|
532
|
+
from ...cli.commands.hooks import run_install_hooks
|
|
533
|
+
run_install_hooks([])
|
|
534
|
+
except Exception:
|
|
535
|
+
pass
|
|
536
|
+
return rec
|
|
537
|
+
|
|
538
|
+
|
|
395
539
|
@app.get("/api/pipeline")
|
|
396
540
|
def pipeline_dashboard_route():
|
|
397
541
|
"""5-stage data flow dashboard. See handlers.pipeline_dashboard."""
|