loop-memory 0.4.7__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.
Files changed (144) hide show
  1. {loop_memory-0.4.7 → loop_memory-0.4.8}/PKG-INFO +19 -1
  2. {loop_memory-0.4.7 → loop_memory-0.4.8}/README.md +18 -0
  3. {loop_memory-0.4.7 → loop_memory-0.4.8}/loop_memory/cli/commands/hooks.py +74 -6
  4. {loop_memory-0.4.7 → loop_memory-0.4.8}/loop_memory/cli/commands/read.py +82 -4
  5. {loop_memory-0.4.7 → loop_memory-0.4.8}/loop_memory/cli/main.py +7 -0
  6. {loop_memory-0.4.7 → loop_memory-0.4.8}/loop_memory/serve/routes/system.py +92 -0
  7. {loop_memory-0.4.7 → loop_memory-0.4.8}/loop_memory/storage/sqlite_store.py +304 -1
  8. {loop_memory-0.4.7 → loop_memory-0.4.8}/loop_memory.egg-info/PKG-INFO +19 -1
  9. {loop_memory-0.4.7 → loop_memory-0.4.8}/loop_memory.egg-info/SOURCES.txt +5 -0
  10. {loop_memory-0.4.7 → loop_memory-0.4.8}/pyproject.toml +1 -1
  11. loop_memory-0.4.8/tests/test_agents_init.py +154 -0
  12. loop_memory-0.4.8/tests/test_cli_init.py +106 -0
  13. loop_memory-0.4.8/tests/test_cli_recall_paths.py +155 -0
  14. loop_memory-0.4.8/tests/test_recall_paths.py +213 -0
  15. loop_memory-0.4.8/tests/test_routes_audit_2026_09_13.py +141 -0
  16. {loop_memory-0.4.7 → loop_memory-0.4.8}/tests/test_supersession_chain.py +1 -1
  17. {loop_memory-0.4.7 → loop_memory-0.4.8}/LICENSE +0 -0
  18. {loop_memory-0.4.7 → loop_memory-0.4.8}/loop_memory/__init__.py +0 -0
  19. {loop_memory-0.4.7 → loop_memory-0.4.8}/loop_memory/backends/__init__.py +0 -0
  20. {loop_memory-0.4.7 → loop_memory-0.4.8}/loop_memory/backends/embedding.py +0 -0
  21. {loop_memory-0.4.7 → loop_memory-0.4.8}/loop_memory/backends/sentence_embedder.py +0 -0
  22. {loop_memory-0.4.7 → loop_memory-0.4.8}/loop_memory/backends/vector_store.py +0 -0
  23. {loop_memory-0.4.7 → loop_memory-0.4.8}/loop_memory/cli/__init__.py +0 -0
  24. {loop_memory-0.4.7 → loop_memory-0.4.8}/loop_memory/cli/_common.py +0 -0
  25. {loop_memory-0.4.7 → loop_memory-0.4.8}/loop_memory/cli/commands/__init__.py +0 -0
  26. {loop_memory-0.4.7 → loop_memory-0.4.8}/loop_memory/cli/commands/cognitive.py +0 -0
  27. {loop_memory-0.4.7 → loop_memory-0.4.8}/loop_memory/cli/commands/diag.py +0 -0
  28. {loop_memory-0.4.7 → loop_memory-0.4.8}/loop_memory/cli/commands/graph.py +0 -0
  29. {loop_memory-0.4.7 → loop_memory-0.4.8}/loop_memory/cli/commands/rules.py +0 -0
  30. {loop_memory-0.4.7 → loop_memory-0.4.8}/loop_memory/cli/commands/serve.py +0 -0
  31. {loop_memory-0.4.7 → loop_memory-0.4.8}/loop_memory/cli/commands/write.py +0 -0
  32. {loop_memory-0.4.7 → loop_memory-0.4.8}/loop_memory/engine/__init__.py +0 -0
  33. {loop_memory-0.4.7 → loop_memory-0.4.8}/loop_memory/engine/loop.py +0 -0
  34. {loop_memory-0.4.7 → loop_memory-0.4.8}/loop_memory/engine/reflect.py +0 -0
  35. {loop_memory-0.4.7 → loop_memory-0.4.8}/loop_memory/examples/__init__.py +0 -0
  36. {loop_memory-0.4.7 → loop_memory-0.4.8}/loop_memory/examples/demo.py +0 -0
  37. {loop_memory-0.4.7 → loop_memory-0.4.8}/loop_memory/export/__init__.py +0 -0
  38. {loop_memory-0.4.7 → loop_memory-0.4.8}/loop_memory/export/memory_md.py +0 -0
  39. {loop_memory-0.4.7 → loop_memory-0.4.8}/loop_memory/graph/__init__.py +0 -0
  40. {loop_memory-0.4.7 → loop_memory-0.4.8}/loop_memory/graph/build.py +0 -0
  41. {loop_memory-0.4.7 → loop_memory-0.4.8}/loop_memory/graph/extract.py +0 -0
  42. {loop_memory-0.4.7 → loop_memory-0.4.8}/loop_memory/ingest/__init__.py +0 -0
  43. {loop_memory-0.4.7 → loop_memory-0.4.8}/loop_memory/ingest/loader.py +0 -0
  44. {loop_memory-0.4.7 → loop_memory-0.4.8}/loop_memory/ingest/pipeline.py +0 -0
  45. {loop_memory-0.4.7 → loop_memory-0.4.8}/loop_memory/jobs/__init__.py +0 -0
  46. {loop_memory-0.4.7 → loop_memory-0.4.8}/loop_memory/jobs/cognitive.py +0 -0
  47. {loop_memory-0.4.7 → loop_memory-0.4.8}/loop_memory/jobs/compact.py +0 -0
  48. {loop_memory-0.4.7 → loop_memory-0.4.8}/loop_memory/jobs/consolidate.py +0 -0
  49. {loop_memory-0.4.7 → loop_memory-0.4.8}/loop_memory/jobs/contradiction.py +0 -0
  50. {loop_memory-0.4.7 → loop_memory-0.4.8}/loop_memory/jobs/evolution.py +0 -0
  51. {loop_memory-0.4.7 → loop_memory-0.4.8}/loop_memory/jobs/graph.py +0 -0
  52. {loop_memory-0.4.7 → loop_memory-0.4.8}/loop_memory/jobs/llm_compact_pass.py +0 -0
  53. {loop_memory-0.4.7 → loop_memory-0.4.8}/loop_memory/jobs/llm_consolidate.py +0 -0
  54. {loop_memory-0.4.7 → loop_memory-0.4.8}/loop_memory/jobs/scheduler.py +0 -0
  55. {loop_memory-0.4.7 → loop_memory-0.4.8}/loop_memory/llm/__init__.py +0 -0
  56. {loop_memory-0.4.7 → loop_memory-0.4.8}/loop_memory/llm/base.py +0 -0
  57. {loop_memory-0.4.7 → loop_memory-0.4.8}/loop_memory/llm/openai_adapter.py +0 -0
  58. {loop_memory-0.4.7 → loop_memory-0.4.8}/loop_memory/llm/providers.py +0 -0
  59. {loop_memory-0.4.7 → loop_memory-0.4.8}/loop_memory/mcp/__init__.py +0 -0
  60. {loop_memory-0.4.7 → loop_memory-0.4.8}/loop_memory/memory/__init__.py +0 -0
  61. {loop_memory-0.4.7 → loop_memory-0.4.8}/loop_memory/memory/types.py +0 -0
  62. {loop_memory-0.4.7 → loop_memory-0.4.8}/loop_memory/privacy/__init__.py +0 -0
  63. {loop_memory-0.4.7 → loop_memory-0.4.8}/loop_memory/privacy/private.py +0 -0
  64. {loop_memory-0.4.7 → loop_memory-0.4.8}/loop_memory/privacy/redact.py +0 -0
  65. {loop_memory-0.4.7 → loop_memory-0.4.8}/loop_memory/py.typed +0 -0
  66. {loop_memory-0.4.7 → loop_memory-0.4.8}/loop_memory/sdk.py +0 -0
  67. {loop_memory-0.4.7 → loop_memory-0.4.8}/loop_memory/sdk_extensions.py +0 -0
  68. {loop_memory-0.4.7 → loop_memory-0.4.8}/loop_memory/security/__init__.py +0 -0
  69. {loop_memory-0.4.7 → loop_memory-0.4.8}/loop_memory/security/secrets.py +0 -0
  70. {loop_memory-0.4.7 → loop_memory-0.4.8}/loop_memory/serve/__init__.py +0 -0
  71. {loop_memory-0.4.7 → loop_memory-0.4.8}/loop_memory/serve/app.py +0 -0
  72. {loop_memory-0.4.7 → loop_memory-0.4.8}/loop_memory/serve/handlers.py +0 -0
  73. {loop_memory-0.4.7 → loop_memory-0.4.8}/loop_memory/serve/routes/_shared.py +0 -0
  74. {loop_memory-0.4.7 → loop_memory-0.4.8}/loop_memory/serve/routes/admin.py +0 -0
  75. {loop_memory-0.4.7 → loop_memory-0.4.8}/loop_memory/serve/routes/cognitive.py +0 -0
  76. {loop_memory-0.4.7 → loop_memory-0.4.8}/loop_memory/serve/routes/export.py +0 -0
  77. {loop_memory-0.4.7 → loop_memory-0.4.8}/loop_memory/serve/routes/graph.py +0 -0
  78. {loop_memory-0.4.7 → loop_memory-0.4.8}/loop_memory/serve/routes/insights.py +0 -0
  79. {loop_memory-0.4.7 → loop_memory-0.4.8}/loop_memory/serve/routes/memories.py +0 -0
  80. {loop_memory-0.4.7 → loop_memory-0.4.8}/loop_memory/serve/routes/sessions.py +0 -0
  81. {loop_memory-0.4.7 → loop_memory-0.4.8}/loop_memory/serve/routes/wiki.py +0 -0
  82. {loop_memory-0.4.7 → loop_memory-0.4.8}/loop_memory/serve/static/__init__.py +0 -0
  83. {loop_memory-0.4.7 → loop_memory-0.4.8}/loop_memory/serve/static/index.html +0 -0
  84. {loop_memory-0.4.7 → loop_memory-0.4.8}/loop_memory/serve/watcher.py +0 -0
  85. {loop_memory-0.4.7 → loop_memory-0.4.8}/loop_memory/storage/__init__.py +0 -0
  86. {loop_memory-0.4.7 → loop_memory-0.4.8}/loop_memory/storage/retrieval.py +0 -0
  87. {loop_memory-0.4.7 → loop_memory-0.4.8}/loop_memory/storage/snapshot.py +0 -0
  88. {loop_memory-0.4.7 → loop_memory-0.4.8}/loop_memory/wiki/__init__.py +0 -0
  89. {loop_memory-0.4.7 → loop_memory-0.4.8}/loop_memory/wiki/backfill.py +0 -0
  90. {loop_memory-0.4.7 → loop_memory-0.4.8}/loop_memory/wiki/classifier.py +0 -0
  91. {loop_memory-0.4.7 → loop_memory-0.4.8}/loop_memory/wiki/prompts.py +0 -0
  92. {loop_memory-0.4.7 → loop_memory-0.4.8}/loop_memory/wiki/scope.py +0 -0
  93. {loop_memory-0.4.7 → loop_memory-0.4.8}/loop_memory.egg-info/dependency_links.txt +0 -0
  94. {loop_memory-0.4.7 → loop_memory-0.4.8}/loop_memory.egg-info/entry_points.txt +0 -0
  95. {loop_memory-0.4.7 → loop_memory-0.4.8}/loop_memory.egg-info/requires.txt +0 -0
  96. {loop_memory-0.4.7 → loop_memory-0.4.8}/loop_memory.egg-info/top_level.txt +0 -0
  97. {loop_memory-0.4.7 → loop_memory-0.4.8}/setup.cfg +0 -0
  98. {loop_memory-0.4.7 → loop_memory-0.4.8}/tests/test_admin_ingest_route.py +0 -0
  99. {loop_memory-0.4.7 → loop_memory-0.4.8}/tests/test_agent_memory_api.py +0 -0
  100. {loop_memory-0.4.7 → loop_memory-0.4.8}/tests/test_agent_memory_sdk.py +0 -0
  101. {loop_memory-0.4.7 → loop_memory-0.4.8}/tests/test_auth_token_rotate.py +0 -0
  102. {loop_memory-0.4.7 → loop_memory-0.4.8}/tests/test_cli_audit_supersede.py +0 -0
  103. {loop_memory-0.4.7 → loop_memory-0.4.8}/tests/test_cli_memory_stats.py +0 -0
  104. {loop_memory-0.4.7 → loop_memory-0.4.8}/tests/test_cli_recall_verbose.py +0 -0
  105. {loop_memory-0.4.7 → loop_memory-0.4.8}/tests/test_cli_rules.py +0 -0
  106. {loop_memory-0.4.7 → loop_memory-0.4.8}/tests/test_cli_snapshot.py +0 -0
  107. {loop_memory-0.4.7 → loop_memory-0.4.8}/tests/test_cli_v7.py +0 -0
  108. {loop_memory-0.4.7 → loop_memory-0.4.8}/tests/test_cli_version.py +0 -0
  109. {loop_memory-0.4.7 → loop_memory-0.4.8}/tests/test_contradictions.py +0 -0
  110. {loop_memory-0.4.7 → loop_memory-0.4.8}/tests/test_evolution.py +0 -0
  111. {loop_memory-0.4.7 → loop_memory-0.4.8}/tests/test_evolution_quality.py +0 -0
  112. {loop_memory-0.4.7 → loop_memory-0.4.8}/tests/test_expanduser.py +0 -0
  113. {loop_memory-0.4.7 → loop_memory-0.4.8}/tests/test_export_ask.py +0 -0
  114. {loop_memory-0.4.7 → loop_memory-0.4.8}/tests/test_graph.py +0 -0
  115. {loop_memory-0.4.7 → loop_memory-0.4.8}/tests/test_ingest.py +0 -0
  116. {loop_memory-0.4.7 → loop_memory-0.4.8}/tests/test_llm_consolidator.py +0 -0
  117. {loop_memory-0.4.7 → loop_memory-0.4.8}/tests/test_llm_fingerprint_not_persisted.py +0 -0
  118. {loop_memory-0.4.7 → loop_memory-0.4.8}/tests/test_llm_providers.py +0 -0
  119. {loop_memory-0.4.7 → loop_memory-0.4.8}/tests/test_llm_test_endpoint.py +0 -0
  120. {loop_memory-0.4.7 → loop_memory-0.4.8}/tests/test_loop.py +0 -0
  121. {loop_memory-0.4.7 → loop_memory-0.4.8}/tests/test_mcp.py +0 -0
  122. {loop_memory-0.4.7 → loop_memory-0.4.8}/tests/test_memories_pagination.py +0 -0
  123. {loop_memory-0.4.7 → loop_memory-0.4.8}/tests/test_memory_stats.py +0 -0
  124. {loop_memory-0.4.7 → loop_memory-0.4.8}/tests/test_openclaw_loader.py +0 -0
  125. {loop_memory-0.4.7 → loop_memory-0.4.8}/tests/test_package_version.py +0 -0
  126. {loop_memory-0.4.7 → loop_memory-0.4.8}/tests/test_recall_provenance.py +0 -0
  127. {loop_memory-0.4.7 → loop_memory-0.4.8}/tests/test_reflection.py +0 -0
  128. {loop_memory-0.4.7 → loop_memory-0.4.8}/tests/test_repo_framing.py +0 -0
  129. {loop_memory-0.4.7 → loop_memory-0.4.8}/tests/test_score_api.py +0 -0
  130. {loop_memory-0.4.7 → loop_memory-0.4.8}/tests/test_scoring_v2.py +0 -0
  131. {loop_memory-0.4.7 → loop_memory-0.4.8}/tests/test_secrets.py +0 -0
  132. {loop_memory-0.4.7 → loop_memory-0.4.8}/tests/test_serve_app.py +0 -0
  133. {loop_memory-0.4.7 → loop_memory-0.4.8}/tests/test_serve_handlers.py +0 -0
  134. {loop_memory-0.4.7 → loop_memory-0.4.8}/tests/test_session_order.py +0 -0
  135. {loop_memory-0.4.7 → loop_memory-0.4.8}/tests/test_short_query.py +0 -0
  136. {loop_memory-0.4.7 → loop_memory-0.4.8}/tests/test_snapshot.py +0 -0
  137. {loop_memory-0.4.7 → loop_memory-0.4.8}/tests/test_store.py +0 -0
  138. {loop_memory-0.4.7 → loop_memory-0.4.8}/tests/test_summarization.py +0 -0
  139. {loop_memory-0.4.7 → loop_memory-0.4.8}/tests/test_universal_memory.py +0 -0
  140. {loop_memory-0.4.7 → loop_memory-0.4.8}/tests/test_vector_store.py +0 -0
  141. {loop_memory-0.4.7 → loop_memory-0.4.8}/tests/test_watcher.py +0 -0
  142. {loop_memory-0.4.7 → loop_memory-0.4.8}/tests/test_wiki_classifier.py +0 -0
  143. {loop_memory-0.4.7 → loop_memory-0.4.8}/tests/test_wiki_export_escape.py +0 -0
  144. {loop_memory-0.4.7 → 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.7
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,6 +66,24 @@ Dynamic: license-file
66
66
 
67
67
  ---
68
68
 
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
+ >
69
87
  > **What's new in 0.4.7** — **per-memory lifetime stats** +
70
88
  > **portable SQLite snapshot**. `loop-memory memory-stats <id>`
71
89
  > returns a flat dict (recall_count, positive, negative, age,
@@ -22,6 +22,24 @@
22
22
 
23
23
  ---
24
24
 
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
+ >
25
43
  > **What's new in 0.4.7** — **per-memory lifetime stats** +
26
44
  > **portable SQLite snapshot**. `loop-memory memory-stats <id>`
27
45
  > returns a flat dict (recall_count, positive, negative, age,
@@ -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
- _install_codex(home, actions)
204
- _install_claude(home, actions)
205
- _install_hermes(home, actions)
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
- r = store.recall(query, limit=limit)
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 w.get("summary"):
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
- print(f" {m['text'][:240]}")
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
@@ -122,6 +122,9 @@ 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,
125
128
  "rules": rules_cmd.run_rules,
126
129
  "version": _run_version,
127
130
  }
@@ -166,6 +169,10 @@ COMMAND_HELP: dict[str, str] = {
166
169
  "memory-stats": "loop-memory memory-stats <id> [--prefix] # per-memory lifetime stats (audit 2026-09-06, agentmemory v1.3.0).",
167
170
  "snapshot": "loop-memory snapshot <out.memory.sqlite> # write a portable SQLite snapshot (audit 2026-09-06, codexa-memory v0.2.0).",
168
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.",
169
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.",
170
177
  }
171
178
 
@@ -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."""