loop-memory 0.4.5__tar.gz → 0.4.6__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.5 → loop_memory-0.4.6}/PKG-INFO +10 -7
- {loop_memory-0.4.5 → loop_memory-0.4.6}/README.md +9 -6
- {loop_memory-0.4.5 → loop_memory-0.4.6}/loop_memory/cli/commands/cognitive.py +39 -0
- {loop_memory-0.4.5 → loop_memory-0.4.6}/loop_memory/cli/commands/read.py +41 -3
- {loop_memory-0.4.5 → loop_memory-0.4.6}/loop_memory/cli/main.py +3 -0
- {loop_memory-0.4.5 → loop_memory-0.4.6}/loop_memory/storage/sqlite_store.py +226 -17
- {loop_memory-0.4.5 → loop_memory-0.4.6}/loop_memory.egg-info/PKG-INFO +10 -7
- {loop_memory-0.4.5 → loop_memory-0.4.6}/loop_memory.egg-info/SOURCES.txt +4 -0
- {loop_memory-0.4.5 → loop_memory-0.4.6}/pyproject.toml +1 -1
- loop_memory-0.4.6/tests/test_cli_audit_supersede.py +162 -0
- loop_memory-0.4.6/tests/test_cli_recall_verbose.py +146 -0
- {loop_memory-0.4.5 → loop_memory-0.4.6}/tests/test_contradictions.py +19 -4
- loop_memory-0.4.6/tests/test_recall_provenance.py +197 -0
- loop_memory-0.4.6/tests/test_supersession_chain.py +370 -0
- {loop_memory-0.4.5 → loop_memory-0.4.6}/LICENSE +0 -0
- {loop_memory-0.4.5 → loop_memory-0.4.6}/loop_memory/__init__.py +0 -0
- {loop_memory-0.4.5 → loop_memory-0.4.6}/loop_memory/backends/__init__.py +0 -0
- {loop_memory-0.4.5 → loop_memory-0.4.6}/loop_memory/backends/embedding.py +0 -0
- {loop_memory-0.4.5 → loop_memory-0.4.6}/loop_memory/backends/sentence_embedder.py +0 -0
- {loop_memory-0.4.5 → loop_memory-0.4.6}/loop_memory/backends/vector_store.py +0 -0
- {loop_memory-0.4.5 → loop_memory-0.4.6}/loop_memory/cli/__init__.py +0 -0
- {loop_memory-0.4.5 → loop_memory-0.4.6}/loop_memory/cli/_common.py +0 -0
- {loop_memory-0.4.5 → loop_memory-0.4.6}/loop_memory/cli/commands/__init__.py +0 -0
- {loop_memory-0.4.5 → loop_memory-0.4.6}/loop_memory/cli/commands/diag.py +0 -0
- {loop_memory-0.4.5 → loop_memory-0.4.6}/loop_memory/cli/commands/graph.py +0 -0
- {loop_memory-0.4.5 → loop_memory-0.4.6}/loop_memory/cli/commands/hooks.py +0 -0
- {loop_memory-0.4.5 → loop_memory-0.4.6}/loop_memory/cli/commands/rules.py +0 -0
- {loop_memory-0.4.5 → loop_memory-0.4.6}/loop_memory/cli/commands/serve.py +0 -0
- {loop_memory-0.4.5 → loop_memory-0.4.6}/loop_memory/cli/commands/write.py +0 -0
- {loop_memory-0.4.5 → loop_memory-0.4.6}/loop_memory/engine/__init__.py +0 -0
- {loop_memory-0.4.5 → loop_memory-0.4.6}/loop_memory/engine/loop.py +0 -0
- {loop_memory-0.4.5 → loop_memory-0.4.6}/loop_memory/engine/reflect.py +0 -0
- {loop_memory-0.4.5 → loop_memory-0.4.6}/loop_memory/examples/__init__.py +0 -0
- {loop_memory-0.4.5 → loop_memory-0.4.6}/loop_memory/examples/demo.py +0 -0
- {loop_memory-0.4.5 → loop_memory-0.4.6}/loop_memory/export/__init__.py +0 -0
- {loop_memory-0.4.5 → loop_memory-0.4.6}/loop_memory/export/memory_md.py +0 -0
- {loop_memory-0.4.5 → loop_memory-0.4.6}/loop_memory/graph/__init__.py +0 -0
- {loop_memory-0.4.5 → loop_memory-0.4.6}/loop_memory/graph/build.py +0 -0
- {loop_memory-0.4.5 → loop_memory-0.4.6}/loop_memory/graph/extract.py +0 -0
- {loop_memory-0.4.5 → loop_memory-0.4.6}/loop_memory/ingest/__init__.py +0 -0
- {loop_memory-0.4.5 → loop_memory-0.4.6}/loop_memory/ingest/loader.py +0 -0
- {loop_memory-0.4.5 → loop_memory-0.4.6}/loop_memory/ingest/pipeline.py +0 -0
- {loop_memory-0.4.5 → loop_memory-0.4.6}/loop_memory/jobs/__init__.py +0 -0
- {loop_memory-0.4.5 → loop_memory-0.4.6}/loop_memory/jobs/cognitive.py +0 -0
- {loop_memory-0.4.5 → loop_memory-0.4.6}/loop_memory/jobs/compact.py +0 -0
- {loop_memory-0.4.5 → loop_memory-0.4.6}/loop_memory/jobs/consolidate.py +0 -0
- {loop_memory-0.4.5 → loop_memory-0.4.6}/loop_memory/jobs/contradiction.py +0 -0
- {loop_memory-0.4.5 → loop_memory-0.4.6}/loop_memory/jobs/evolution.py +0 -0
- {loop_memory-0.4.5 → loop_memory-0.4.6}/loop_memory/jobs/graph.py +0 -0
- {loop_memory-0.4.5 → loop_memory-0.4.6}/loop_memory/jobs/llm_compact_pass.py +0 -0
- {loop_memory-0.4.5 → loop_memory-0.4.6}/loop_memory/jobs/llm_consolidate.py +0 -0
- {loop_memory-0.4.5 → loop_memory-0.4.6}/loop_memory/jobs/scheduler.py +0 -0
- {loop_memory-0.4.5 → loop_memory-0.4.6}/loop_memory/llm/__init__.py +0 -0
- {loop_memory-0.4.5 → loop_memory-0.4.6}/loop_memory/llm/base.py +0 -0
- {loop_memory-0.4.5 → loop_memory-0.4.6}/loop_memory/llm/openai_adapter.py +0 -0
- {loop_memory-0.4.5 → loop_memory-0.4.6}/loop_memory/llm/providers.py +0 -0
- {loop_memory-0.4.5 → loop_memory-0.4.6}/loop_memory/mcp/__init__.py +0 -0
- {loop_memory-0.4.5 → loop_memory-0.4.6}/loop_memory/memory/__init__.py +0 -0
- {loop_memory-0.4.5 → loop_memory-0.4.6}/loop_memory/memory/types.py +0 -0
- {loop_memory-0.4.5 → loop_memory-0.4.6}/loop_memory/privacy/__init__.py +0 -0
- {loop_memory-0.4.5 → loop_memory-0.4.6}/loop_memory/privacy/private.py +0 -0
- {loop_memory-0.4.5 → loop_memory-0.4.6}/loop_memory/privacy/redact.py +0 -0
- {loop_memory-0.4.5 → loop_memory-0.4.6}/loop_memory/py.typed +0 -0
- {loop_memory-0.4.5 → loop_memory-0.4.6}/loop_memory/sdk.py +0 -0
- {loop_memory-0.4.5 → loop_memory-0.4.6}/loop_memory/sdk_extensions.py +0 -0
- {loop_memory-0.4.5 → loop_memory-0.4.6}/loop_memory/security/__init__.py +0 -0
- {loop_memory-0.4.5 → loop_memory-0.4.6}/loop_memory/security/secrets.py +0 -0
- {loop_memory-0.4.5 → loop_memory-0.4.6}/loop_memory/serve/__init__.py +0 -0
- {loop_memory-0.4.5 → loop_memory-0.4.6}/loop_memory/serve/app.py +0 -0
- {loop_memory-0.4.5 → loop_memory-0.4.6}/loop_memory/serve/handlers.py +0 -0
- {loop_memory-0.4.5 → loop_memory-0.4.6}/loop_memory/serve/routes/_shared.py +0 -0
- {loop_memory-0.4.5 → loop_memory-0.4.6}/loop_memory/serve/routes/admin.py +0 -0
- {loop_memory-0.4.5 → loop_memory-0.4.6}/loop_memory/serve/routes/cognitive.py +0 -0
- {loop_memory-0.4.5 → loop_memory-0.4.6}/loop_memory/serve/routes/export.py +0 -0
- {loop_memory-0.4.5 → loop_memory-0.4.6}/loop_memory/serve/routes/graph.py +0 -0
- {loop_memory-0.4.5 → loop_memory-0.4.6}/loop_memory/serve/routes/insights.py +0 -0
- {loop_memory-0.4.5 → loop_memory-0.4.6}/loop_memory/serve/routes/memories.py +0 -0
- {loop_memory-0.4.5 → loop_memory-0.4.6}/loop_memory/serve/routes/sessions.py +0 -0
- {loop_memory-0.4.5 → loop_memory-0.4.6}/loop_memory/serve/routes/system.py +0 -0
- {loop_memory-0.4.5 → loop_memory-0.4.6}/loop_memory/serve/routes/wiki.py +0 -0
- {loop_memory-0.4.5 → loop_memory-0.4.6}/loop_memory/serve/static/__init__.py +0 -0
- {loop_memory-0.4.5 → loop_memory-0.4.6}/loop_memory/serve/static/index.html +0 -0
- {loop_memory-0.4.5 → loop_memory-0.4.6}/loop_memory/serve/watcher.py +0 -0
- {loop_memory-0.4.5 → loop_memory-0.4.6}/loop_memory/storage/__init__.py +0 -0
- {loop_memory-0.4.5 → loop_memory-0.4.6}/loop_memory/storage/retrieval.py +0 -0
- {loop_memory-0.4.5 → loop_memory-0.4.6}/loop_memory/wiki/__init__.py +0 -0
- {loop_memory-0.4.5 → loop_memory-0.4.6}/loop_memory/wiki/backfill.py +0 -0
- {loop_memory-0.4.5 → loop_memory-0.4.6}/loop_memory/wiki/classifier.py +0 -0
- {loop_memory-0.4.5 → loop_memory-0.4.6}/loop_memory/wiki/prompts.py +0 -0
- {loop_memory-0.4.5 → loop_memory-0.4.6}/loop_memory/wiki/scope.py +0 -0
- {loop_memory-0.4.5 → loop_memory-0.4.6}/loop_memory.egg-info/dependency_links.txt +0 -0
- {loop_memory-0.4.5 → loop_memory-0.4.6}/loop_memory.egg-info/entry_points.txt +0 -0
- {loop_memory-0.4.5 → loop_memory-0.4.6}/loop_memory.egg-info/requires.txt +0 -0
- {loop_memory-0.4.5 → loop_memory-0.4.6}/loop_memory.egg-info/top_level.txt +0 -0
- {loop_memory-0.4.5 → loop_memory-0.4.6}/setup.cfg +0 -0
- {loop_memory-0.4.5 → loop_memory-0.4.6}/tests/test_admin_ingest_route.py +0 -0
- {loop_memory-0.4.5 → loop_memory-0.4.6}/tests/test_agent_memory_api.py +0 -0
- {loop_memory-0.4.5 → loop_memory-0.4.6}/tests/test_agent_memory_sdk.py +0 -0
- {loop_memory-0.4.5 → loop_memory-0.4.6}/tests/test_auth_token_rotate.py +0 -0
- {loop_memory-0.4.5 → loop_memory-0.4.6}/tests/test_cli_rules.py +0 -0
- {loop_memory-0.4.5 → loop_memory-0.4.6}/tests/test_cli_v7.py +0 -0
- {loop_memory-0.4.5 → loop_memory-0.4.6}/tests/test_cli_version.py +0 -0
- {loop_memory-0.4.5 → loop_memory-0.4.6}/tests/test_evolution.py +0 -0
- {loop_memory-0.4.5 → loop_memory-0.4.6}/tests/test_evolution_quality.py +0 -0
- {loop_memory-0.4.5 → loop_memory-0.4.6}/tests/test_expanduser.py +0 -0
- {loop_memory-0.4.5 → loop_memory-0.4.6}/tests/test_export_ask.py +0 -0
- {loop_memory-0.4.5 → loop_memory-0.4.6}/tests/test_graph.py +0 -0
- {loop_memory-0.4.5 → loop_memory-0.4.6}/tests/test_ingest.py +0 -0
- {loop_memory-0.4.5 → loop_memory-0.4.6}/tests/test_llm_consolidator.py +0 -0
- {loop_memory-0.4.5 → loop_memory-0.4.6}/tests/test_llm_fingerprint_not_persisted.py +0 -0
- {loop_memory-0.4.5 → loop_memory-0.4.6}/tests/test_llm_providers.py +0 -0
- {loop_memory-0.4.5 → loop_memory-0.4.6}/tests/test_llm_test_endpoint.py +0 -0
- {loop_memory-0.4.5 → loop_memory-0.4.6}/tests/test_loop.py +0 -0
- {loop_memory-0.4.5 → loop_memory-0.4.6}/tests/test_mcp.py +0 -0
- {loop_memory-0.4.5 → loop_memory-0.4.6}/tests/test_memories_pagination.py +0 -0
- {loop_memory-0.4.5 → loop_memory-0.4.6}/tests/test_openclaw_loader.py +0 -0
- {loop_memory-0.4.5 → loop_memory-0.4.6}/tests/test_package_version.py +0 -0
- {loop_memory-0.4.5 → loop_memory-0.4.6}/tests/test_reflection.py +0 -0
- {loop_memory-0.4.5 → loop_memory-0.4.6}/tests/test_repo_framing.py +0 -0
- {loop_memory-0.4.5 → loop_memory-0.4.6}/tests/test_score_api.py +0 -0
- {loop_memory-0.4.5 → loop_memory-0.4.6}/tests/test_scoring_v2.py +0 -0
- {loop_memory-0.4.5 → loop_memory-0.4.6}/tests/test_secrets.py +0 -0
- {loop_memory-0.4.5 → loop_memory-0.4.6}/tests/test_serve_app.py +0 -0
- {loop_memory-0.4.5 → loop_memory-0.4.6}/tests/test_serve_handlers.py +0 -0
- {loop_memory-0.4.5 → loop_memory-0.4.6}/tests/test_session_order.py +0 -0
- {loop_memory-0.4.5 → loop_memory-0.4.6}/tests/test_short_query.py +0 -0
- {loop_memory-0.4.5 → loop_memory-0.4.6}/tests/test_store.py +0 -0
- {loop_memory-0.4.5 → loop_memory-0.4.6}/tests/test_summarization.py +0 -0
- {loop_memory-0.4.5 → loop_memory-0.4.6}/tests/test_universal_memory.py +0 -0
- {loop_memory-0.4.5 → loop_memory-0.4.6}/tests/test_vector_store.py +0 -0
- {loop_memory-0.4.5 → loop_memory-0.4.6}/tests/test_watcher.py +0 -0
- {loop_memory-0.4.5 → loop_memory-0.4.6}/tests/test_wiki_classifier.py +0 -0
- {loop_memory-0.4.5 → loop_memory-0.4.6}/tests/test_wiki_export_escape.py +0 -0
- {loop_memory-0.4.5 → loop_memory-0.4.6}/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.6
|
|
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,12 +66,15 @@ Dynamic: license-file
|
|
|
66
66
|
|
|
67
67
|
---
|
|
68
68
|
|
|
69
|
-
> **What's new in 0.4.
|
|
70
|
-
>
|
|
71
|
-
>
|
|
72
|
-
>
|
|
73
|
-
> `
|
|
74
|
-
>
|
|
69
|
+
> **What's new in 0.4.6** — **memory supersession chain** + **recall
|
|
70
|
+
> provenance labels**. `merge_memories()` no longer silently
|
|
71
|
+
> `DELETE`s the loser — it writes an explicit `superseded_by`
|
|
72
|
+
> pointer (audit 2026-08-30, Mem0 v2.0.19 Dream pattern), the new
|
|
73
|
+
> `loop-memory audit-supersede` walks the chain, and every
|
|
74
|
+
> `recall()` hit now carries a `why: [...]` provenance list
|
|
75
|
+
> (adopted from agentmemory v1.2.0) that you can surface with
|
|
76
|
+
> `loop-memory recall --verbose`. 26 new regression cases pin
|
|
77
|
+
> both shapes.
|
|
75
78
|
> [Full changelog →](CHANGELOG.md)
|
|
76
79
|
|
|
77
80
|
---
|
|
@@ -22,12 +22,15 @@
|
|
|
22
22
|
|
|
23
23
|
---
|
|
24
24
|
|
|
25
|
-
> **What's new in 0.4.
|
|
26
|
-
>
|
|
27
|
-
>
|
|
28
|
-
>
|
|
29
|
-
> `
|
|
30
|
-
>
|
|
25
|
+
> **What's new in 0.4.6** — **memory supersession chain** + **recall
|
|
26
|
+
> provenance labels**. `merge_memories()` no longer silently
|
|
27
|
+
> `DELETE`s the loser — it writes an explicit `superseded_by`
|
|
28
|
+
> pointer (audit 2026-08-30, Mem0 v2.0.19 Dream pattern), the new
|
|
29
|
+
> `loop-memory audit-supersede` walks the chain, and every
|
|
30
|
+
> `recall()` hit now carries a `why: [...]` provenance list
|
|
31
|
+
> (adopted from agentmemory v1.2.0) that you can surface with
|
|
32
|
+
> `loop-memory recall --verbose`. 26 new regression cases pin
|
|
33
|
+
> both shapes.
|
|
31
34
|
> [Full changelog →](CHANGELOG.md)
|
|
32
35
|
|
|
33
36
|
---
|
|
@@ -116,6 +116,45 @@ def run_audit(args: list) -> int:
|
|
|
116
116
|
return _emit({"rows": s.list_audit(kind=ns.kind, action=ns.action, limit=ns.limit)})
|
|
117
117
|
|
|
118
118
|
|
|
119
|
+
def run_audit_supersede(args: list) -> int:
|
|
120
|
+
"""``loop-memory audit-supersede [--target ID] [--limit N] [--by ID]``
|
|
121
|
+
|
|
122
|
+
Walks the supersession chain (audit 2026-08-30, Mem0 v2.0.19
|
|
123
|
+
Dream pattern). Without ``--target`` it lists every superseded
|
|
124
|
+
memory (the loser side of every merge), most-recent first. With
|
|
125
|
+
``--target <id>`` it returns just the chain starting at that
|
|
126
|
+
memory (oldest to newest). With ``--by <id>`` it filters the
|
|
127
|
+
list to memories that were superseded by a specific winner.
|
|
128
|
+
|
|
129
|
+
The CLI prints JSON so a shell pipeline can consume the output.
|
|
130
|
+
"""
|
|
131
|
+
import argparse as _ap
|
|
132
|
+
p = _ap.ArgumentParser(prog="loop-memory audit-supersede")
|
|
133
|
+
p.add_argument("--target", default=None,
|
|
134
|
+
help="Walk the supersession chain starting at this memory id")
|
|
135
|
+
p.add_argument("--by", default=None,
|
|
136
|
+
help="List memories superseded by this specific winner id")
|
|
137
|
+
p.add_argument("--limit", type=int, default=200)
|
|
138
|
+
p.add_argument("--db", default=os.environ.get("LOOP_MEMORY_DB", DEFAULT_DB))
|
|
139
|
+
ns = p.parse_args(args)
|
|
140
|
+
s = MemoryStore(ns.db)
|
|
141
|
+
if ns.target:
|
|
142
|
+
chain = s.trace_supersession(ns.target)
|
|
143
|
+
return _emit({
|
|
144
|
+
"target": ns.target,
|
|
145
|
+
"chain": chain,
|
|
146
|
+
"chain_length": len(chain),
|
|
147
|
+
"winner": chain[-1] if chain else None,
|
|
148
|
+
})
|
|
149
|
+
rows = s.list_superseded(superseded_by=ns.by, limit=ns.limit)
|
|
150
|
+
return _emit({
|
|
151
|
+
"rows": rows,
|
|
152
|
+
"total": s.supersession_count(),
|
|
153
|
+
"filtered_by": ns.by,
|
|
154
|
+
"limit": ns.limit,
|
|
155
|
+
})
|
|
156
|
+
|
|
157
|
+
|
|
119
158
|
def run_export(args: list) -> int:
|
|
120
159
|
p = _build_parser()
|
|
121
160
|
db = os.environ.get("LOOP_MEMORY_DB", DEFAULT_DB)
|
|
@@ -51,12 +51,45 @@ 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]``.
|
|
55
|
+
|
|
56
|
+
With ``--verbose`` each memory hit is annotated with the
|
|
57
|
+
``why: [...]`` provenance labels produced by ``MemoryStore.recall``
|
|
58
|
+
(audit 2026-08-30, agentmemory v1.2.0 pattern). The wiki and
|
|
59
|
+
entity sections are unchanged — only the raw-memory block grows
|
|
60
|
+
the new line.
|
|
61
|
+
"""
|
|
54
62
|
from ...storage.sqlite_store import MemoryStore
|
|
55
63
|
if not args:
|
|
56
|
-
return die("usage: loop-memory recall <query>")
|
|
64
|
+
return die("usage: loop-memory recall <query> [--verbose] [--limit N]")
|
|
65
|
+
# Parse the small flag subset we expose. Keep it dumb-on-purpose:
|
|
66
|
+
# we don't want argparse to swallow a token that happens to start
|
|
67
|
+
# with "--" inside the user's query (e.g. ``recall --foo bar``).
|
|
68
|
+
verbose = False
|
|
69
|
+
limit = 10
|
|
70
|
+
qargs: list[str] = []
|
|
71
|
+
i = 0
|
|
72
|
+
while i < len(args):
|
|
73
|
+
a = args[i]
|
|
74
|
+
if a == "--verbose":
|
|
75
|
+
verbose = True
|
|
76
|
+
i += 1
|
|
77
|
+
elif a == "--limit" and i + 1 < len(args):
|
|
78
|
+
try:
|
|
79
|
+
limit = int(args[i + 1])
|
|
80
|
+
except ValueError:
|
|
81
|
+
return die(f"--limit must be an integer, got {args[i + 1]!r}")
|
|
82
|
+
i += 2
|
|
83
|
+
elif a.startswith("--"):
|
|
84
|
+
return die(f"unknown flag: {a}")
|
|
85
|
+
else:
|
|
86
|
+
qargs.append(a)
|
|
87
|
+
i += 1
|
|
88
|
+
if not qargs:
|
|
89
|
+
return die("usage: loop-memory recall <query> [--verbose] [--limit N]")
|
|
57
90
|
store = MemoryStore(default_db_path())
|
|
58
|
-
query = " ".join(
|
|
59
|
-
r = store.recall(query, limit=
|
|
91
|
+
query = " ".join(qargs)
|
|
92
|
+
r = store.recall(query, limit=limit)
|
|
60
93
|
has = False
|
|
61
94
|
if r["wiki"]:
|
|
62
95
|
has = True
|
|
@@ -73,6 +106,11 @@ def run_recall(args) -> int:
|
|
|
73
106
|
for m in r["memories"]:
|
|
74
107
|
tag_s = " [" + ", ".join(m.get("tags") or []) + "]" if m.get("tags") else ""
|
|
75
108
|
print(f"- [{m['kind']}] (imp={m['importance']:.2f}){tag_s}")
|
|
109
|
+
if verbose and m.get("why"):
|
|
110
|
+
# Stable order (built by MemoryStore.recall) so callers can
|
|
111
|
+
# render the list as a deterministic badge strip without
|
|
112
|
+
# re-sorting.
|
|
113
|
+
print(f" why: {', '.join(m['why'])}")
|
|
76
114
|
print(f" {m['text'][:240]}")
|
|
77
115
|
print()
|
|
78
116
|
if r["entities"]:
|
|
@@ -22,6 +22,7 @@ Usage:
|
|
|
22
22
|
loop-memory ask "what about…" # print a paste-ready context block for any LLM client
|
|
23
23
|
loop-memory cognitive-sleep [--apply] # dry-run / apply cognitive sweep (v7)
|
|
24
24
|
loop-memory audit [--kind X] [--action Y] # read the cognitive audit trail
|
|
25
|
+
loop-memory audit-supersede [--target ID] # walk / list the memory supersession chain
|
|
25
26
|
loop-memory export <out_dir> # write a MEMORY.md bundle (v7)
|
|
26
27
|
loop-memory export-bundle <out_dir> # explicit v7 bundle alias
|
|
27
28
|
loop-memory import <in_dir> # re-hydrate a bundle
|
|
@@ -109,6 +110,7 @@ COMMANDS = {
|
|
|
109
110
|
# Universal Agent Memory v7 — graph, cognitive, export, fork
|
|
110
111
|
"cognitive-sleep": cognitive_cmd.run_cognitive_sleep,
|
|
111
112
|
"audit": cognitive_cmd.run_audit,
|
|
113
|
+
"audit-supersede": cognitive_cmd.run_audit_supersede,
|
|
112
114
|
"export-bundle": cognitive_cmd.run_export,
|
|
113
115
|
"import": cognitive_cmd.run_import,
|
|
114
116
|
"fork": cognitive_cmd.run_fork,
|
|
@@ -128,6 +130,7 @@ COMMANDS = {
|
|
|
128
130
|
COMMAND_HELP: dict[str, str] = {
|
|
129
131
|
"ask": "loop-memory ask '<question>' # print a paste-ready context block for any LLM client.",
|
|
130
132
|
"audit": "loop-memory audit [--kind X] [--action Y] # read the cognitive audit trail.",
|
|
133
|
+
"audit-supersede": "loop-memory audit-supersede [--target ID] # walk / list supersession chain (Mem0 Dream pattern).",
|
|
131
134
|
"chat": "loop-memory chat # REPL with echo LLM.",
|
|
132
135
|
"cognitive-sleep": "loop-memory cognitive-sleep [--apply] # dry-run / apply cognitive sweep.",
|
|
133
136
|
"consolidate": "loop-memory consolidate # rescore + GC + dedupe.",
|
|
@@ -444,7 +444,7 @@ class MemoryStore:
|
|
|
444
444
|
The zero-dep claim holds — Python ships with sqlite3 and struct.
|
|
445
445
|
"""
|
|
446
446
|
|
|
447
|
-
SCHEMA_VERSION = "
|
|
447
|
+
SCHEMA_VERSION = "9"
|
|
448
448
|
|
|
449
449
|
def __init__(self, path: str | Path) -> None:
|
|
450
450
|
self.path = Path(path).expanduser()
|
|
@@ -511,6 +511,25 @@ class MemoryStore:
|
|
|
511
511
|
"WHERE external_id IS NOT NULL AND external_id != ''"
|
|
512
512
|
)
|
|
513
513
|
|
|
514
|
+
# Audit 2026-08-30 — supersession chain (Mem0 v2.0.19 Dream
|
|
515
|
+
# pattern, slimmed down). Adds a nullable FK ``superseded_by``
|
|
516
|
+
# so ``merge_memories()`` can write an explicit pointer on
|
|
517
|
+
# the loser rather than DELETE-ing the row; ``recall()``
|
|
518
|
+
# filters it out, and ``list_superseded()`` /
|
|
519
|
+
# ``trace_supersession()`` walk the chain. SCHEMA_VERSION
|
|
520
|
+
# bumped "8" -> "9".
|
|
521
|
+
mem_cols = {row["name"] for row in c.execute("PRAGMA table_info(memories)").fetchall()}
|
|
522
|
+
if "superseded_by" not in mem_cols:
|
|
523
|
+
c.execute("ALTER TABLE memories ADD COLUMN superseded_by TEXT")
|
|
524
|
+
# Partial index keeps the recall filter O(1) even on stores
|
|
525
|
+
# with millions of memories. ``superseded_by IS NULL`` is
|
|
526
|
+
# the hot path; the index only covers the rare "show me the
|
|
527
|
+
# chain" query.
|
|
528
|
+
c.execute(
|
|
529
|
+
"CREATE INDEX IF NOT EXISTS idx_mem_superseded_by "
|
|
530
|
+
"ON memories(superseded_by) WHERE superseded_by IS NOT NULL"
|
|
531
|
+
)
|
|
532
|
+
|
|
514
533
|
# One-shot FTS5 tokenizer migration. ``CREATE VIRTUAL TABLE
|
|
515
534
|
# IF NOT EXISTS`` will *not* rebuild an existing FTS5 table
|
|
516
535
|
# if its schema differs from the DDL — which is exactly
|
|
@@ -1272,10 +1291,16 @@ class MemoryStore:
|
|
|
1272
1291
|
sql = (
|
|
1273
1292
|
"SELECT m.id, m.kind, m.text, m.importance, m.score, m.source, "
|
|
1274
1293
|
"m.tags, m.created_at, m.updated_at, "
|
|
1275
|
-
"COALESCE(s.recall_count, 0) AS recall_count "
|
|
1294
|
+
"COALESCE(s.recall_count, 0) AS recall_count, "
|
|
1295
|
+
"m.superseded_by "
|
|
1276
1296
|
"FROM memories m "
|
|
1277
1297
|
"LEFT JOIN memory_signals s ON s.memory_id = m.id "
|
|
1278
|
-
|
|
1298
|
+
# Filter superseded memories out of the recall stream
|
|
1299
|
+
# (audit 2026-08-30, Mem0 v2.0.19 Dream pattern). The
|
|
1300
|
+
# partial index on ``superseded_by`` makes this an
|
|
1301
|
+
# O(1) skip; the chain itself stays queryable via
|
|
1302
|
+
# ``list_superseded()`` / ``trace_supersession()``.
|
|
1303
|
+
f"WHERE m.superseded_by IS NULL AND ({clause} OR {tag_clause}) "
|
|
1279
1304
|
"ORDER BY m.score DESC, m.importance DESC, m.created_at DESC "
|
|
1280
1305
|
"LIMIT ?"
|
|
1281
1306
|
)
|
|
@@ -1297,26 +1322,49 @@ class MemoryStore:
|
|
|
1297
1322
|
tag_lc = ",".join(tags).lower()
|
|
1298
1323
|
body_hits = sum(txt.count(t) for t in tokens)
|
|
1299
1324
|
tag_hits = sum(tag_lc.count(t) for t in tokens)
|
|
1325
|
+
importance = float(r["importance"] or 0)
|
|
1326
|
+
score_field = float(r["score"] or 0)
|
|
1300
1327
|
score = body_hits + 2 * tag_hits
|
|
1301
|
-
score *= 0.5 +
|
|
1302
|
-
score *= 0.7 +
|
|
1328
|
+
score *= 0.5 + importance * 0.8
|
|
1329
|
+
score *= 0.7 + score_field * 0.6
|
|
1330
|
+
# Build a small ``why: [...]`` provenance list
|
|
1331
|
+
# (audit 2026-08-30, agentmemory v1.2.0 pattern).
|
|
1332
|
+
# Each label names a scoring signal that actually
|
|
1333
|
+
# contributed to the hit; users see this through
|
|
1334
|
+
# ``loop-memory recall --verbose`` and the
|
|
1335
|
+
# dashboard tooltip. Order is stable so callers
|
|
1336
|
+
# can render it as a deterministic badge list.
|
|
1337
|
+
why: list[str] = []
|
|
1338
|
+
if body_hits > 0:
|
|
1339
|
+
why.append("keyword_match")
|
|
1340
|
+
if tag_hits > 0:
|
|
1341
|
+
why.append("tag_match")
|
|
1342
|
+
if importance >= 0.7:
|
|
1343
|
+
why.append("high_importance")
|
|
1344
|
+
if score_field >= 0.7:
|
|
1345
|
+
why.append("high_score_field")
|
|
1346
|
+
recall_count = int(r["recall_count"] or 0)
|
|
1303
1347
|
if short_query:
|
|
1304
|
-
recall_count = int(r["recall_count"] or 0)
|
|
1305
1348
|
# log1p saturates so a memory with thousands of
|
|
1306
1349
|
# recalls does not crowd out everything else.
|
|
1307
1350
|
score *= 1.0 + min(0.3, recall_count * 0.03)
|
|
1351
|
+
if recall_count > 0:
|
|
1352
|
+
why.append("short_query_boost")
|
|
1353
|
+
if recall_count >= 3:
|
|
1354
|
+
why.append("high_recall_count")
|
|
1308
1355
|
out["memories"].append({
|
|
1309
1356
|
"id": r["id"],
|
|
1310
1357
|
"kind": "memory",
|
|
1311
1358
|
"text": r["text"],
|
|
1312
|
-
"importance":
|
|
1313
|
-
"score_field":
|
|
1359
|
+
"importance": importance,
|
|
1360
|
+
"score_field": score_field,
|
|
1314
1361
|
"source": r["source"],
|
|
1315
1362
|
"tags": tags,
|
|
1316
1363
|
"created_at": float(r["created_at"] or 0),
|
|
1317
|
-
"recall_count":
|
|
1364
|
+
"recall_count": recall_count,
|
|
1318
1365
|
"score": round(score, 3),
|
|
1319
1366
|
"preview": (r["text"] or "")[:240],
|
|
1367
|
+
"why": why,
|
|
1320
1368
|
})
|
|
1321
1369
|
out["memories"].sort(key=lambda m: -m["score"])
|
|
1322
1370
|
out["memories"] = out["memories"][:limit]
|
|
@@ -3173,13 +3221,18 @@ class MemoryStore:
|
|
|
3173
3221
|
def merge_memories(self, a_id: str, b_id: str) -> dict:
|
|
3174
3222
|
"""True memory-pair merge.
|
|
3175
3223
|
|
|
3176
|
-
Behaviour
|
|
3224
|
+
Behaviour (audit 2026-08-30 — supersession chain, Mem0 v2.0.19
|
|
3225
|
+
Dream pattern, slimmed down):
|
|
3177
3226
|
- The higher-scored memory wins (ties go to ``a_id``).
|
|
3178
3227
|
- The loser's text is appended to the winner's text (de-duplicated
|
|
3179
3228
|
if the loser's text is already a substring of the winner's).
|
|
3180
3229
|
- The winner's ``importance`` and ``score`` are bumped to the max
|
|
3181
3230
|
of the two so the fused memory keeps the strongest signal.
|
|
3182
|
-
- The loser is deleted
|
|
3231
|
+
- The loser is **not** deleted: it gets ``superseded_by = winner_id``
|
|
3232
|
+
so the audit trail can walk the chain. ``recall()`` filters
|
|
3233
|
+
out rows with a non-null ``superseded_by`` so callers see the
|
|
3234
|
+
latest view. The chain is queryable via ``list_superseded()``
|
|
3235
|
+
and ``trace_supersession()``.
|
|
3183
3236
|
- The pair is recorded in ``contradiction_ignored`` so the pulse
|
|
3184
3237
|
does not surface it again.
|
|
3185
3238
|
|
|
@@ -3193,23 +3246,36 @@ class MemoryStore:
|
|
|
3193
3246
|
now = time.time()
|
|
3194
3247
|
with self._conn() as c:
|
|
3195
3248
|
a_row = c.execute(
|
|
3196
|
-
"SELECT id, text, importance, score FROM memories WHERE id=?",
|
|
3249
|
+
"SELECT id, text, importance, score, superseded_by FROM memories WHERE id=?",
|
|
3197
3250
|
(a_id,),
|
|
3198
3251
|
).fetchone()
|
|
3199
3252
|
b_row = c.execute(
|
|
3200
|
-
"SELECT id, text, importance, score FROM memories WHERE id=?",
|
|
3253
|
+
"SELECT id, text, importance, score, superseded_by FROM memories WHERE id=?",
|
|
3201
3254
|
(b_id,),
|
|
3202
3255
|
).fetchone()
|
|
3203
3256
|
if a_row is None and b_row is None:
|
|
3204
3257
|
return {"merged": False, "reason": "neither_exists"}
|
|
3205
3258
|
if a_row is None:
|
|
3206
|
-
# Only B exists — silently
|
|
3207
|
-
|
|
3259
|
+
# Only B exists — silently treat the missing A as a tombstone
|
|
3260
|
+
# and keep B. ``a_missing`` is rare (concurrent delete) so
|
|
3261
|
+
# we don't bother writing a supersede row.
|
|
3208
3262
|
return {"merged": False, "kept": b_id, "lost": a_id, "reason": "a_missing"}
|
|
3209
3263
|
if b_row is None:
|
|
3210
|
-
c.execute("DELETE FROM memories WHERE id=?", (b_id,))
|
|
3211
3264
|
return {"merged": False, "kept": a_id, "lost": b_id, "reason": "b_missing"}
|
|
3212
3265
|
|
|
3266
|
+
# If either side is already superseded, refuse to keep merging
|
|
3267
|
+
# to avoid creating a chain that's harder to reason about
|
|
3268
|
+
# (callers should walk the existing chain first via
|
|
3269
|
+
# ``trace_supersession``). The UI surfaces the existing chain
|
|
3270
|
+
# via ``audit supersede --target``.
|
|
3271
|
+
if a_row["superseded_by"] or b_row["superseded_by"]:
|
|
3272
|
+
return {
|
|
3273
|
+
"merged": False,
|
|
3274
|
+
"reason": "already_superseded",
|
|
3275
|
+
"a_superseded_by": a_row["superseded_by"],
|
|
3276
|
+
"b_superseded_by": b_row["superseded_by"],
|
|
3277
|
+
}
|
|
3278
|
+
|
|
3213
3279
|
# Pick winner = higher score; ties go to a_id.
|
|
3214
3280
|
a_score = a_row["score"] or 0.0
|
|
3215
3281
|
b_score = b_row["score"] or 0.0
|
|
@@ -3239,7 +3305,44 @@ class MemoryStore:
|
|
|
3239
3305
|
"WHERE id=?",
|
|
3240
3306
|
(merged_text, importance_max, score_max, now, winner_id),
|
|
3241
3307
|
)
|
|
3242
|
-
|
|
3308
|
+
# Write an explicit supersession pointer instead of DELETE
|
|
3309
|
+
# (audit 2026-08-30). The loser row stays so
|
|
3310
|
+
# ``list_superseded`` / ``trace_supersession`` can walk the
|
|
3311
|
+
# chain; ``recall()`` filters ``superseded_by IS NULL``.
|
|
3312
|
+
c.execute(
|
|
3313
|
+
"UPDATE memories SET superseded_by=?, score=0, updated_at=? "
|
|
3314
|
+
"WHERE id=?",
|
|
3315
|
+
(winner_id, now, loser_id),
|
|
3316
|
+
)
|
|
3317
|
+
|
|
3318
|
+
# Audit 2026-08-30 — emit a ``supersede`` row into
|
|
3319
|
+
# ``cognitive_audit`` so the dashboard / ``loop-memory audit
|
|
3320
|
+
# --kind supersede`` surface can show "memory X was
|
|
3321
|
+
# superseded by memory Y on date Z". We do this inside the
|
|
3322
|
+
# same transaction so a partial merge never leaves the
|
|
3323
|
+
# audit pointing at a missing row.
|
|
3324
|
+
c.execute(
|
|
3325
|
+
"""INSERT INTO cognitive_audit
|
|
3326
|
+
(id, ts, kind, action, target_kind, target_id,
|
|
3327
|
+
target_text, reason, score, payload)
|
|
3328
|
+
VALUES (?,?,?,?,?,?,?,?,?,?)""",
|
|
3329
|
+
(
|
|
3330
|
+
uuid.uuid4().hex,
|
|
3331
|
+
now,
|
|
3332
|
+
"supersede",
|
|
3333
|
+
"applied",
|
|
3334
|
+
"memory",
|
|
3335
|
+
loser_id,
|
|
3336
|
+
(loser_text or "")[:160],
|
|
3337
|
+
f"merged into {winner_id}",
|
|
3338
|
+
float(score_max),
|
|
3339
|
+
json.dumps({
|
|
3340
|
+
"winner_id": winner_id,
|
|
3341
|
+
"appended": needs_append,
|
|
3342
|
+
"winner_was_a": winner_is_a,
|
|
3343
|
+
}),
|
|
3344
|
+
),
|
|
3345
|
+
)
|
|
3243
3346
|
|
|
3244
3347
|
# Suppress the pair so the pulse does not resurface it.
|
|
3245
3348
|
lo, hi = sorted([a_id, b_id])
|
|
@@ -3259,6 +3362,112 @@ class MemoryStore:
|
|
|
3259
3362
|
"new_length": len(merged_text),
|
|
3260
3363
|
}
|
|
3261
3364
|
|
|
3365
|
+
# ------------------------------------------------------------------
|
|
3366
|
+
# Supersession chain (audit 2026-08-30, Mem0 v2.0.19 Dream pattern)
|
|
3367
|
+
# ------------------------------------------------------------------
|
|
3368
|
+
|
|
3369
|
+
def list_superseded(self, superseded_by: str | None = None,
|
|
3370
|
+
limit: int = 200) -> list[dict]:
|
|
3371
|
+
"""List memories that have been superseded.
|
|
3372
|
+
|
|
3373
|
+
``superseded_by=None`` (default) — return every superseded
|
|
3374
|
+
memory (the loser side of every merge). With
|
|
3375
|
+
``superseded_by=<id>`` — return only the memories that point
|
|
3376
|
+
at that specific winner. Results are ordered by ``updated_at
|
|
3377
|
+
DESC`` so the most recent supersession is first.
|
|
3378
|
+
|
|
3379
|
+
Returned dicts mirror the memory row + the ``superseded_by``
|
|
3380
|
+
pointer + the ``updated_at`` timestamp so the caller can
|
|
3381
|
+
render an audit view without an extra round trip.
|
|
3382
|
+
"""
|
|
3383
|
+
limit = max(1, min(int(limit), 5000))
|
|
3384
|
+
sql = (
|
|
3385
|
+
"SELECT id, kind, text, importance, source, session_id, "
|
|
3386
|
+
"tags, created_at, updated_at, score, ttl, "
|
|
3387
|
+
"agent_id, user_id, external_id, superseded_by "
|
|
3388
|
+
"FROM memories WHERE superseded_by IS NOT NULL "
|
|
3389
|
+
)
|
|
3390
|
+
params: list[Any] = []
|
|
3391
|
+
if superseded_by is not None:
|
|
3392
|
+
sql += "AND superseded_by = ? "
|
|
3393
|
+
params.append(str(superseded_by))
|
|
3394
|
+
sql += "ORDER BY updated_at DESC LIMIT ?"
|
|
3395
|
+
params.append(limit)
|
|
3396
|
+
with self._conn() as c:
|
|
3397
|
+
rows = c.execute(sql, params).fetchall()
|
|
3398
|
+
out: list[dict] = []
|
|
3399
|
+
for r in rows:
|
|
3400
|
+
tags_raw = r["tags"] or "[]"
|
|
3401
|
+
try:
|
|
3402
|
+
tags = json.loads(tags_raw)
|
|
3403
|
+
except Exception:
|
|
3404
|
+
tags = []
|
|
3405
|
+
out.append({
|
|
3406
|
+
"id": r["id"],
|
|
3407
|
+
"kind": r["kind"],
|
|
3408
|
+
"text": r["text"] or "",
|
|
3409
|
+
"importance": float(r["importance"] or 0),
|
|
3410
|
+
"source": r["source"],
|
|
3411
|
+
"session_id": r["session_id"],
|
|
3412
|
+
"tags": tags,
|
|
3413
|
+
"created_at": float(r["created_at"] or 0),
|
|
3414
|
+
"updated_at": float(r["updated_at"] or 0),
|
|
3415
|
+
"score": float(r["score"] or 0),
|
|
3416
|
+
"ttl": r["ttl"],
|
|
3417
|
+
"agent_id": r["agent_id"],
|
|
3418
|
+
"user_id": r["user_id"],
|
|
3419
|
+
"external_id": r["external_id"],
|
|
3420
|
+
"superseded_by": r["superseded_by"],
|
|
3421
|
+
})
|
|
3422
|
+
return out
|
|
3423
|
+
|
|
3424
|
+
def trace_supersession(self, target_id: str) -> list[str]:
|
|
3425
|
+
"""Walk the supersession chain starting from ``target_id``.
|
|
3426
|
+
|
|
3427
|
+
Returns the **list of memory ids** in the chain from oldest to
|
|
3428
|
+
newest (the last entry is the current "winner"). If the
|
|
3429
|
+
target is itself a winner, the list is just ``[target_id]``.
|
|
3430
|
+
If the target is not in the store, the list is empty.
|
|
3431
|
+
|
|
3432
|
+
Bounded to 64 hops so a corrupted FK cannot spin forever.
|
|
3433
|
+
"""
|
|
3434
|
+
target_id = str(target_id or "")
|
|
3435
|
+
if not target_id:
|
|
3436
|
+
return []
|
|
3437
|
+
seen: set[str] = set()
|
|
3438
|
+
out: list[str] = []
|
|
3439
|
+
current = target_id
|
|
3440
|
+
with self._conn() as c:
|
|
3441
|
+
for _ in range(64):
|
|
3442
|
+
if current in seen:
|
|
3443
|
+
break
|
|
3444
|
+
seen.add(current)
|
|
3445
|
+
row = c.execute(
|
|
3446
|
+
"SELECT id, superseded_by FROM memories WHERE id=?",
|
|
3447
|
+
(current,),
|
|
3448
|
+
).fetchone()
|
|
3449
|
+
if row is None:
|
|
3450
|
+
break
|
|
3451
|
+
out.append(row["id"])
|
|
3452
|
+
nxt = row["superseded_by"]
|
|
3453
|
+
if not nxt:
|
|
3454
|
+
break
|
|
3455
|
+
current = nxt
|
|
3456
|
+
return out
|
|
3457
|
+
|
|
3458
|
+
def supersession_count(self) -> int:
|
|
3459
|
+
"""Total number of superseded memories in the store.
|
|
3460
|
+
|
|
3461
|
+
Used by the dashboard so the user can see how much history
|
|
3462
|
+
the supersession chain carries. Cheap because the partial
|
|
3463
|
+
index covers it.
|
|
3464
|
+
"""
|
|
3465
|
+
with self._conn() as c:
|
|
3466
|
+
row = c.execute(
|
|
3467
|
+
"SELECT COUNT(*) c FROM memories WHERE superseded_by IS NOT NULL"
|
|
3468
|
+
).fetchone()
|
|
3469
|
+
return int(row["c"] or 0)
|
|
3470
|
+
|
|
3262
3471
|
def delete_session(self, session_id: str) -> int:
|
|
3263
3472
|
with self._conn() as c:
|
|
3264
3473
|
cur = c.execute("DELETE FROM memories WHERE session_id=?", (session_id,))
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: loop-memory
|
|
3
|
-
Version: 0.4.
|
|
3
|
+
Version: 0.4.6
|
|
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,12 +66,15 @@ Dynamic: license-file
|
|
|
66
66
|
|
|
67
67
|
---
|
|
68
68
|
|
|
69
|
-
> **What's new in 0.4.
|
|
70
|
-
>
|
|
71
|
-
>
|
|
72
|
-
>
|
|
73
|
-
> `
|
|
74
|
-
>
|
|
69
|
+
> **What's new in 0.4.6** — **memory supersession chain** + **recall
|
|
70
|
+
> provenance labels**. `merge_memories()` no longer silently
|
|
71
|
+
> `DELETE`s the loser — it writes an explicit `superseded_by`
|
|
72
|
+
> pointer (audit 2026-08-30, Mem0 v2.0.19 Dream pattern), the new
|
|
73
|
+
> `loop-memory audit-supersede` walks the chain, and every
|
|
74
|
+
> `recall()` hit now carries a `why: [...]` provenance list
|
|
75
|
+
> (adopted from agentmemory v1.2.0) that you can surface with
|
|
76
|
+
> `loop-memory recall --verbose`. 26 new regression cases pin
|
|
77
|
+
> both shapes.
|
|
75
78
|
> [Full changelog →](CHANGELOG.md)
|
|
76
79
|
|
|
77
80
|
---
|
|
@@ -90,6 +90,8 @@ tests/test_admin_ingest_route.py
|
|
|
90
90
|
tests/test_agent_memory_api.py
|
|
91
91
|
tests/test_agent_memory_sdk.py
|
|
92
92
|
tests/test_auth_token_rotate.py
|
|
93
|
+
tests/test_cli_audit_supersede.py
|
|
94
|
+
tests/test_cli_recall_verbose.py
|
|
93
95
|
tests/test_cli_rules.py
|
|
94
96
|
tests/test_cli_v7.py
|
|
95
97
|
tests/test_cli_version.py
|
|
@@ -109,6 +111,7 @@ tests/test_mcp.py
|
|
|
109
111
|
tests/test_memories_pagination.py
|
|
110
112
|
tests/test_openclaw_loader.py
|
|
111
113
|
tests/test_package_version.py
|
|
114
|
+
tests/test_recall_provenance.py
|
|
112
115
|
tests/test_reflection.py
|
|
113
116
|
tests/test_repo_framing.py
|
|
114
117
|
tests/test_score_api.py
|
|
@@ -120,6 +123,7 @@ tests/test_session_order.py
|
|
|
120
123
|
tests/test_short_query.py
|
|
121
124
|
tests/test_store.py
|
|
122
125
|
tests/test_summarization.py
|
|
126
|
+
tests/test_supersession_chain.py
|
|
123
127
|
tests/test_universal_memory.py
|
|
124
128
|
tests/test_vector_store.py
|
|
125
129
|
tests/test_watcher.py
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "loop-memory"
|
|
7
|
-
version = "0.4.
|
|
7
|
+
version = "0.4.6"
|
|
8
8
|
description = "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."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.10"
|