echo-mem 0.5.2__tar.gz → 0.5.4__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.
- {echo_mem-0.5.2/src/echo_mem.egg-info → echo_mem-0.5.4}/PKG-INFO +107 -10
- {echo_mem-0.5.2 → echo_mem-0.5.4}/README.md +106 -9
- {echo_mem-0.5.2 → echo_mem-0.5.4}/pyproject.toml +1 -1
- {echo_mem-0.5.2 → echo_mem-0.5.4/src/echo_mem.egg-info}/PKG-INFO +107 -10
- {echo_mem-0.5.2 → echo_mem-0.5.4}/src/echo_mem.egg-info/SOURCES.txt +8 -0
- echo_mem-0.5.4/src/echo_memory/cli/infer_causal.py +737 -0
- echo_mem-0.5.4/src/echo_memory/cli/llm.py +132 -0
- {echo_mem-0.5.2 → echo_mem-0.5.4}/src/echo_memory/cli/main.py +167 -1
- echo_mem-0.5.4/src/echo_memory/eval/external.py +713 -0
- {echo_mem-0.5.2 → echo_mem-0.5.4}/src/echo_memory/ingestion/write_episode.py +7 -0
- echo_mem-0.5.4/src/echo_memory/migrations/versions/0025_lexical_statistics.py +72 -0
- echo_mem-0.5.4/src/echo_memory/migrations/versions/0026_late_causal_extraction.py +111 -0
- echo_mem-0.5.4/src/echo_memory/migrations/versions/0027_lexical_staleness_without_a_scan.py +47 -0
- echo_mem-0.5.4/src/echo_memory/retrieval/bm25.py +320 -0
- {echo_mem-0.5.2 → echo_mem-0.5.4}/src/echo_memory/retrieval/query_memory.py +397 -21
- echo_mem-0.5.4/src/echo_memory/retrieval/salience.py +98 -0
- {echo_mem-0.5.2 → echo_mem-0.5.4}/src/echo_memory/server.py +39 -26
- {echo_mem-0.5.2 → echo_mem-0.5.4}/src/echo_memory/skill/SKILL.md +6 -0
- {echo_mem-0.5.2 → echo_mem-0.5.4}/LICENSE +0 -0
- {echo_mem-0.5.2 → echo_mem-0.5.4}/LICENSE-APACHE-2.0 +0 -0
- {echo_mem-0.5.2 → echo_mem-0.5.4}/setup.cfg +0 -0
- {echo_mem-0.5.2 → echo_mem-0.5.4}/src/echo_mem.egg-info/dependency_links.txt +0 -0
- {echo_mem-0.5.2 → echo_mem-0.5.4}/src/echo_mem.egg-info/entry_points.txt +0 -0
- {echo_mem-0.5.2 → echo_mem-0.5.4}/src/echo_mem.egg-info/requires.txt +0 -0
- {echo_mem-0.5.2 → echo_mem-0.5.4}/src/echo_mem.egg-info/top_level.txt +0 -0
- {echo_mem-0.5.2 → echo_mem-0.5.4}/src/echo_memory/__init__.py +0 -0
- {echo_mem-0.5.2 → echo_mem-0.5.4}/src/echo_memory/audit/__init__.py +0 -0
- {echo_mem-0.5.2 → echo_mem-0.5.4}/src/echo_memory/audit/get_audit_log.py +0 -0
- {echo_mem-0.5.2 → echo_mem-0.5.4}/src/echo_memory/cli/__init__.py +0 -0
- {echo_mem-0.5.2 → echo_mem-0.5.4}/src/echo_memory/cli/adopt.py +0 -0
- {echo_mem-0.5.2 → echo_mem-0.5.4}/src/echo_memory/cli/analyse.py +0 -0
- {echo_mem-0.5.2 → echo_mem-0.5.4}/src/echo_memory/cli/benchmark.py +0 -0
- {echo_mem-0.5.2 → echo_mem-0.5.4}/src/echo_memory/cli/calibrate.py +0 -0
- {echo_mem-0.5.2 → echo_mem-0.5.4}/src/echo_memory/cli/connect.py +0 -0
- {echo_mem-0.5.2 → echo_mem-0.5.4}/src/echo_memory/cli/dashboard.py +0 -0
- {echo_mem-0.5.2 → echo_mem-0.5.4}/src/echo_memory/cli/dashboard_html.py +0 -0
- {echo_mem-0.5.2 → echo_mem-0.5.4}/src/echo_memory/cli/export.py +0 -0
- {echo_mem-0.5.2 → echo_mem-0.5.4}/src/echo_memory/cli/graph.py +0 -0
- {echo_mem-0.5.2 → echo_mem-0.5.4}/src/echo_memory/cli/health.py +0 -0
- {echo_mem-0.5.2 → echo_mem-0.5.4}/src/echo_memory/cli/hooks.py +0 -0
- {echo_mem-0.5.2 → echo_mem-0.5.4}/src/echo_memory/cli/initdb.py +0 -0
- {echo_mem-0.5.2 → echo_mem-0.5.4}/src/echo_memory/cli/install.py +0 -0
- {echo_mem-0.5.2 → echo_mem-0.5.4}/src/echo_memory/cli/judge.py +0 -0
- {echo_mem-0.5.2 → echo_mem-0.5.4}/src/echo_memory/cli/merge.py +0 -0
- {echo_mem-0.5.2 → echo_mem-0.5.4}/src/echo_memory/cli/queue.py +0 -0
- {echo_mem-0.5.2 → echo_mem-0.5.4}/src/echo_memory/cli/quickstart.py +0 -0
- {echo_mem-0.5.2 → echo_mem-0.5.4}/src/echo_memory/cli/reattribute.py +0 -0
- {echo_mem-0.5.2 → echo_mem-0.5.4}/src/echo_memory/cli/reattribute_cmd.py +0 -0
- {echo_mem-0.5.2 → echo_mem-0.5.4}/src/echo_memory/cli/recall.py +0 -0
- {echo_mem-0.5.2 → echo_mem-0.5.4}/src/echo_memory/cli/reconcile.py +0 -0
- {echo_mem-0.5.2 → echo_mem-0.5.4}/src/echo_memory/cli/reindex.py +0 -0
- {echo_mem-0.5.2 → echo_mem-0.5.4}/src/echo_memory/cli/session_start.py +0 -0
- {echo_mem-0.5.2 → echo_mem-0.5.4}/src/echo_memory/cli/skill.py +0 -0
- {echo_mem-0.5.2 → echo_mem-0.5.4}/src/echo_memory/cli/status.py +0 -0
- {echo_mem-0.5.2 → echo_mem-0.5.4}/src/echo_memory/cli/stop_gate.py +0 -0
- {echo_mem-0.5.2 → echo_mem-0.5.4}/src/echo_memory/cli/trial.py +0 -0
- {echo_mem-0.5.2 → echo_mem-0.5.4}/src/echo_memory/cli/unmerge.py +0 -0
- {echo_mem-0.5.2 → echo_mem-0.5.4}/src/echo_memory/cli/why.py +0 -0
- {echo_mem-0.5.2 → echo_mem-0.5.4}/src/echo_memory/client.py +0 -0
- {echo_mem-0.5.2 → echo_mem-0.5.4}/src/echo_memory/eval/__init__.py +0 -0
- {echo_mem-0.5.2 → echo_mem-0.5.4}/src/echo_memory/eval/independent.py +0 -0
- {echo_mem-0.5.2 → echo_mem-0.5.4}/src/echo_memory/eval/retrieval.py +0 -0
- {echo_mem-0.5.2 → echo_mem-0.5.4}/src/echo_memory/eval/sweep.py +0 -0
- {echo_mem-0.5.2 → echo_mem-0.5.4}/src/echo_memory/graph/__init__.py +0 -0
- {echo_mem-0.5.2 → echo_mem-0.5.4}/src/echo_memory/graph/communities.py +0 -0
- {echo_mem-0.5.2 → echo_mem-0.5.4}/src/echo_memory/infra/__init__.py +0 -0
- {echo_mem-0.5.2 → echo_mem-0.5.4}/src/echo_memory/infra/config.py +0 -0
- {echo_mem-0.5.2 → echo_mem-0.5.4}/src/echo_memory/infra/db.py +0 -0
- {echo_mem-0.5.2 → echo_mem-0.5.4}/src/echo_memory/infra/logging.py +0 -0
- {echo_mem-0.5.2 → echo_mem-0.5.4}/src/echo_memory/infra/pool.py +0 -0
- {echo_mem-0.5.2 → echo_mem-0.5.4}/src/echo_memory/infra/project.py +0 -0
- {echo_mem-0.5.2 → echo_mem-0.5.4}/src/echo_memory/ingestion/__init__.py +0 -0
- {echo_mem-0.5.2 → echo_mem-0.5.4}/src/echo_memory/ingestion/activity.py +0 -0
- {echo_mem-0.5.2 → echo_mem-0.5.4}/src/echo_memory/ingestion/bootstrap.py +0 -0
- {echo_mem-0.5.2 → echo_mem-0.5.4}/src/echo_memory/ingestion/capture.py +0 -0
- {echo_mem-0.5.2 → echo_mem-0.5.4}/src/echo_memory/ingestion/embeddings.py +0 -0
- {echo_mem-0.5.2 → echo_mem-0.5.4}/src/echo_memory/ingestion/neighbourhood.py +0 -0
- {echo_mem-0.5.2 → echo_mem-0.5.4}/src/echo_memory/ingestion/resolution.py +0 -0
- {echo_mem-0.5.2 → echo_mem-0.5.4}/src/echo_memory/migrations/env.py +0 -0
- {echo_mem-0.5.2 → echo_mem-0.5.4}/src/echo_memory/migrations/versions/0001_initial_schema.py +0 -0
- {echo_mem-0.5.2 → echo_mem-0.5.4}/src/echo_memory/migrations/versions/0002_trial_observations.py +0 -0
- {echo_mem-0.5.2 → echo_mem-0.5.4}/src/echo_memory/migrations/versions/0003_project_and_agent_provenance.py +0 -0
- {echo_mem-0.5.2 → echo_mem-0.5.4}/src/echo_memory/migrations/versions/0004_bootstrap_state.py +0 -0
- {echo_mem-0.5.2 → echo_mem-0.5.4}/src/echo_memory/migrations/versions/0005_recall_save_guards.py +0 -0
- {echo_mem-0.5.2 → echo_mem-0.5.4}/src/echo_memory/migrations/versions/0006_project_analysis.py +0 -0
- {echo_mem-0.5.2 → echo_mem-0.5.4}/src/echo_memory/migrations/versions/0007_backfill_unknown_agent.py +0 -0
- {echo_mem-0.5.2 → echo_mem-0.5.4}/src/echo_memory/migrations/versions/0008_read_events.py +0 -0
- {echo_mem-0.5.2 → echo_mem-0.5.4}/src/echo_memory/migrations/versions/0009_read_event_attribution.py +0 -0
- {echo_mem-0.5.2 → echo_mem-0.5.4}/src/echo_memory/migrations/versions/0010_stop_gate_fired.py +0 -0
- {echo_mem-0.5.2 → echo_mem-0.5.4}/src/echo_memory/migrations/versions/0011_backfill_absent_agent.py +0 -0
- {echo_mem-0.5.2 → echo_mem-0.5.4}/src/echo_memory/migrations/versions/0012_read_event_agent.py +0 -0
- {echo_mem-0.5.2 → echo_mem-0.5.4}/src/echo_memory/migrations/versions/0013_active_fact_indexes.py +0 -0
- {echo_mem-0.5.2 → echo_mem-0.5.4}/src/echo_memory/migrations/versions/0014_session_activity.py +0 -0
- {echo_mem-0.5.2 → echo_mem-0.5.4}/src/echo_memory/migrations/versions/0015_retracted_observations.py +0 -0
- {echo_mem-0.5.2 → echo_mem-0.5.4}/src/echo_memory/migrations/versions/0016_one_node_per_name.py +0 -0
- {echo_mem-0.5.2 → echo_mem-0.5.4}/src/echo_memory/migrations/versions/0017_writer_version.py +0 -0
- {echo_mem-0.5.2 → echo_mem-0.5.4}/src/echo_memory/migrations/versions/0018_trial_runs_are_history.py +0 -0
- {echo_mem-0.5.2 → echo_mem-0.5.4}/src/echo_memory/migrations/versions/0019_versions_expected_since.py +0 -0
- {echo_mem-0.5.2 → echo_mem-0.5.4}/src/echo_memory/migrations/versions/0020_who_closed_a_queued_document.py +0 -0
- {echo_mem-0.5.2 → echo_mem-0.5.4}/src/echo_memory/migrations/versions/0021_delivery_evidence.py +0 -0
- {echo_mem-0.5.2 → echo_mem-0.5.4}/src/echo_memory/migrations/versions/0022_independent_evaluation.py +0 -0
- {echo_mem-0.5.2 → echo_mem-0.5.4}/src/echo_memory/migrations/versions/0023_second_judge.py +0 -0
- {echo_mem-0.5.2 → echo_mem-0.5.4}/src/echo_memory/migrations/versions/0024_node_by_id.py +0 -0
- {echo_mem-0.5.2 → echo_mem-0.5.4}/src/echo_memory/retrieval/__init__.py +0 -0
- {echo_mem-0.5.2 → echo_mem-0.5.4}/src/echo_memory/retrieval/causality.py +0 -0
- {echo_mem-0.5.2 → echo_mem-0.5.4}/src/echo_memory/retrieval/fusion.py +0 -0
- {echo_mem-0.5.2 → echo_mem-0.5.4}/src/echo_memory/trial/__init__.py +0 -0
- {echo_mem-0.5.2 → echo_mem-0.5.4}/src/echo_memory/trial/check.py +0 -0
- {echo_mem-0.5.2 → echo_mem-0.5.4}/src/echo_memory/trial/observations.py +0 -0
- {echo_mem-0.5.2 → echo_mem-0.5.4}/src/echo_memory/trial/reads.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: echo-mem
|
|
3
|
-
Version: 0.5.
|
|
3
|
+
Version: 0.5.4
|
|
4
4
|
Summary: A long-horizon memory architecture for AI agents
|
|
5
5
|
License-Expression: BUSL-1.1
|
|
6
6
|
Requires-Python: >=3.11
|
|
@@ -80,6 +80,27 @@ echo-memory install --global
|
|
|
80
80
|
> hosted product. The import package and the CLI are both `echo_memory` / `echo-memory`;
|
|
81
81
|
> only the distribution name differs.
|
|
82
82
|
|
|
83
|
+
## Upgrading
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
pipx upgrade echo-mem
|
|
87
|
+
echo-memory init-db # apply any new migrations
|
|
88
|
+
echo-memory reindex # rebuild what the new code derives
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
Both steps, in that order, and neither is optional on a store that predates the version
|
|
92
|
+
you just installed.
|
|
93
|
+
|
|
94
|
+
**Skipping `init-db` fails loudly.** A read against a schema older than the code raises an
|
|
95
|
+
undefined column, which is the good case: it stops rather than answering from something it
|
|
96
|
+
half understands.
|
|
97
|
+
|
|
98
|
+
**Skipping `reindex` fails silently, which is worse.** Derived data that the new code reads
|
|
99
|
+
differently is still the old data, and nothing errors. Concretely, on an upgrade to 0.5.3
|
|
100
|
+
the lexical corpus statistics are treated as stale until rebuilt, so BM25 stays off and
|
|
101
|
+
your queries keep ranking by `ts_rank` while the setting reports as on. `reindex` rebuilds
|
|
102
|
+
the embeddings and those statistics together. `echo-memory health` tells you afterwards.
|
|
103
|
+
|
|
83
104
|
## Usage
|
|
84
105
|
|
|
85
106
|
```bash
|
|
@@ -94,9 +115,13 @@ echo-memory export # everything, as JSON
|
|
|
94
115
|
echo-memory install --for cursor # wire one client, project scoped
|
|
95
116
|
echo-memory adopt # wire every MCP client on the machine, each with its own id
|
|
96
117
|
|
|
118
|
+
echo-memory infer-causal-hints # type the facts whose own sentence states a cause (dry run)
|
|
119
|
+
|
|
97
120
|
echo-memory eval # retrieval quality against your own store
|
|
98
121
|
echo-memory eval --context # what a recall costs against injecting everything
|
|
99
122
|
echo-memory eval --context --sweep # the same, as a curve across corpus size
|
|
123
|
+
echo-memory eval-external locomo <path> # LoCoMo, the corpus the published figures use
|
|
124
|
+
echo-memory eval-external longmemeval <path> # LongMemEval, the same
|
|
100
125
|
echo-memory calibrate # is entity resolution trustworthy on your data
|
|
101
126
|
echo-memory benchmark # write, query and digest latency
|
|
102
127
|
```
|
|
@@ -106,7 +131,7 @@ echo-memory benchmark # write, query and digest latency
|
|
|
106
131
|
| Tool | What it does |
|
|
107
132
|
|---|---|
|
|
108
133
|
| `write_episode` | Store entities and the facts connecting them. No model call. |
|
|
109
|
-
| `query_memory` | Hybrid vector and full text retrieval, fused by reciprocal rank. |
|
|
134
|
+
| `query_memory` | Hybrid vector and full text retrieval, fused by reciprocal rank. Full text ranks by BM25. Takes `as_of` to read the store as it stood at an instant, and `about` to return the facts recorded against one or two named entities. |
|
|
110
135
|
| `trace_cause` | Causal chains through the entities a subject matches, not a ranked list. |
|
|
111
136
|
| `record_recall_save` | Mark that a recalled fact saved re explaining something. Refuses a fact no read returned. |
|
|
112
137
|
| `get_audit_log` | Every change to memory, with a plain language reason. |
|
|
@@ -126,6 +151,59 @@ no summarisation, and retrieval today walks every active fact in the scope. It i
|
|
|
126
151
|
described in [`docs/designs/`](docs/designs/) and listed below as v1c, and this paragraph
|
|
127
152
|
used to claim it in the present tense.
|
|
128
153
|
|
|
154
|
+
**A ranked answer that says why each fact is in it.** Every returned fact carries
|
|
155
|
+
`score`, its cosine similarity to the query, `rank`, its position, and `matched`, the
|
|
156
|
+
channels that found it. `score` is deliberately not the fusion number: reciprocal rank
|
|
157
|
+
values are sums of 1/(k+rank) and mean nothing from one query to the next, while a
|
|
158
|
+
similarity means the same thing every time, which is what a caller thresholding on it
|
|
159
|
+
needs. It is computed for every fact returned, including the ones only full text search
|
|
160
|
+
found, because which channel retrieved a fact is an implementation detail and has no
|
|
161
|
+
business reaching a field callers read as relevance.
|
|
162
|
+
|
|
163
|
+
**The full text channel ranks by BM25.** Postgres's `ts_rank` counts term occurrences and
|
|
164
|
+
nothing else: no inverse document frequency, so a word in every fact of a scope counts as
|
|
165
|
+
much as one in three; no saturation, so repetition scales linearly; no length
|
|
166
|
+
normalisation, so a long fact is punished for being long. On 324 real prompts the rank 3
|
|
167
|
+
and rank 4 scores were identical in 59% of them, and reciprocal rank fusion reads rank
|
|
168
|
+
position and never the score underneath it, so a channel ordered by scan order handed the
|
|
169
|
+
fusion a coin flip.
|
|
170
|
+
|
|
171
|
+
BM25 is computed in SQL rather than installed, so no extension is added to your database.
|
|
172
|
+
Corpus statistics are rebuilt amortised on write inside the transaction that already holds
|
|
173
|
+
the scope's lock, and by `echo-memory reindex`; a scope that has none falls back to
|
|
174
|
+
`ts_rank` rather than ranking on statistics it does not have. `ECHO_MEMORY_LEXICAL_BM25=0`
|
|
175
|
+
returns to `ts_rank` everywhere.
|
|
176
|
+
|
|
177
|
+
**Facts about an entity, which resemblance cannot express.** `query_memory` takes
|
|
178
|
+
`about`, one or two entity names, and returns only the facts recorded against them. One
|
|
179
|
+
name gives the edges incident to that node; two give the edges between them in either
|
|
180
|
+
direction, which is "both names in the same fact" without going near the fact text.
|
|
181
|
+
|
|
182
|
+
This is not a ranking improvement and it is not a substitute for one. A query about one
|
|
183
|
+
team returns a semantically close fact about a different team because the wrong fact
|
|
184
|
+
genuinely does resemble the question, and no amount of better ranking removes it.
|
|
185
|
+
Identity is not similarity, and a fact is an edge between two nodes, so asking which
|
|
186
|
+
entity a fact is about is structural and exact.
|
|
187
|
+
|
|
188
|
+
Name matching is exact and case insensitive, never a substring, because a short name is a
|
|
189
|
+
substring of longer unrelated words. A name nothing is recorded under returns no facts
|
|
190
|
+
rather than the nearest thing, and so does a pair with no fact joining them: that is real
|
|
191
|
+
absence and it means nobody recorded this, not here is something adjacent. More than two
|
|
192
|
+
names is refused rather than answered empty, because an edge has two endpoints and a fact
|
|
193
|
+
about three entities is a question this model cannot express.
|
|
194
|
+
|
|
195
|
+
**The history the store has always kept is readable.** Every fact has carried `t_valid`
|
|
196
|
+
and `t_invalid` since the first migration, and the read path had only ever asked whether a
|
|
197
|
+
fact is current, so the store paid to keep the whole record of what a scope believed and
|
|
198
|
+
could not answer the first question a post mortem asks. `query_memory` takes `as_of` in
|
|
199
|
+
unix seconds and every channel honours it: vector, full text, the graph hop, the digest.
|
|
200
|
+
|
|
201
|
+
Supersession is what makes that more than a curiosity. Writing the same source, target and
|
|
202
|
+
relation again does not edit the old fact, it ends it: the old edge takes a `t_invalid` and
|
|
203
|
+
stays queryable. Nothing is ever rewritten, so the history is real rather than
|
|
204
|
+
reconstructed, which is the property the memory reconsolidation literature gives up and
|
|
205
|
+
the reason this project refuses that idea.
|
|
206
|
+
|
|
129
207
|
**Provenance on every fact.** Who wrote it, which tool, which project, when, and which
|
|
130
208
|
reads returned it. A superseded fact is never deleted. It stops being drawn and stays
|
|
131
209
|
reachable with its history.
|
|
@@ -138,13 +216,30 @@ this happen" is an ordered chain and similarity cannot produce one. "A led_to B"
|
|
|
138
216
|
caused_by A" are one claim written from opposite ends, and both assemble into the same
|
|
139
217
|
chain.
|
|
140
218
|
|
|
141
|
-
A fact with no hint is associative, which is the default and usually correct.
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
219
|
+
A fact with no hint is associative, which is the default and usually correct. An empty
|
|
220
|
+
answer from `trace_cause` says which kind of empty it is: nobody recorded a cause here is
|
|
221
|
+
a different fact about a store than there are no chains.
|
|
222
|
+
|
|
223
|
+
**Filling it in, for a store that predates it.** Facts written before 0.5.0 carry no
|
|
224
|
+
hint, so on an existing store `trace_cause` has nothing to walk.
|
|
225
|
+
`echo-memory infer-causal-hints` re-reads the fact text already stored with your own
|
|
226
|
+
model (`ECHO_MEMORY_LLM_API_KEY`, `ECHO_MEMORY_LLM_MODEL`) and types the edges whose own
|
|
227
|
+
sentence states the relation. Dry run by default; `--write` applies what it printed and
|
|
228
|
+
`--clear --write` takes it back.
|
|
229
|
+
|
|
230
|
+
This is extraction done late, not causal discovery, and the difference is enforced rather
|
|
231
|
+
than promised. A sentence with no causal connective is never sent to a model, so
|
|
232
|
+
co-occurrence is refused before it costs anything. Every proposal has to quote the words
|
|
233
|
+
that state the relation, and a quote that is not literally in the fact is dropped, so a
|
|
234
|
+
model reasoning from the world instead of reading the sentence gets nothing stored. Each
|
|
235
|
+
hint written is audited and marked as extracted late, which is what makes `--clear`
|
|
236
|
+
able to remove exactly these and never one you wrote at write time.
|
|
145
237
|
|
|
146
238
|
**No inference on the write path.** Extraction happens in the calling agent, so storing
|
|
147
|
-
a memory invokes no model on the server.
|
|
239
|
+
a memory invokes no model on the server. `infer-causal-hints` above is not an exception
|
|
240
|
+
to that and is worth being precise about: it is a command a person types against facts
|
|
241
|
+
already stored, it never runs as part of a write, a migration or a hook, and a store that
|
|
242
|
+
never runs it never causes a model call. The cost moved rather than vanished: the agent
|
|
148
243
|
has to arrive with entities and facts already extracted, which is what the
|
|
149
244
|
[tool contract](docs/DEVELOPMENT.md) spells out. The comparison that makes this matter is
|
|
150
245
|
Zep/Graphiti, the closest architectural match, whose own description of ingestion is that
|
|
@@ -169,8 +264,9 @@ difference nobody sized is not a result.
|
|
|
169
264
|
|---|---|---|
|
|
170
265
|
| Context per recall vs injecting everything | **96.7% less**, hit@10 0.872 over 1,190 questions | `echo-memory eval --context` |
|
|
171
266
|
| The same saving across 8x of corpus growth | 75.5% at 32 facts rising to **96.4% at 261**, hit@10 0.900 to 0.946 | `echo-memory eval --context --sweep` |
|
|
172
|
-
| LoCoMo retrieval, 1,982 questions, 5,882 turns | recall@10 **0.601**, hit@10 0.658, MRR 0.460 | `
|
|
173
|
-
| LongMemEval retrieval, 90 questions, 15 per type | session@10 **0.
|
|
267
|
+
| LoCoMo retrieval, 1,982 questions, 5,882 turns | recall@10 **0.601**, hit@10 0.658, MRR 0.460, session@10 0.850 | `echo-memory eval-external locomo` |
|
|
268
|
+
| LongMemEval retrieval, 90 questions, 15 per type | session@10 **0.937**, recall@10 0.727, MRR 0.428 | `echo-memory eval-external longmemeval --per-type 15` |
|
|
269
|
+
| Lexical BM25 against `ts_rank` | **+0.0271** MRR on entity_pair [+0.0083, +0.0481] and on multihop [+0.0166, +0.0381], the other two shapes inside noise, none worse | `echo-memory eval --ablate`, the `without BM25` row with its sign reversed |
|
|
174
270
|
| Server side model calls per write | **0** | `echo-memory benchmark` |
|
|
175
271
|
| Write, query, digest latency (median) | 15ms, 8ms, 1ms | `echo-memory benchmark` |
|
|
176
272
|
| Entity resolution AUC | 0.666, 95% CI [0.421, 0.881] | `echo-memory calibrate` |
|
|
@@ -280,7 +376,8 @@ not a thing to withhold from the person whose data it is.
|
|
|
280
376
|
organisation wide shared graph, with no forced migration later. The novel work is the
|
|
281
377
|
memory structure and the read/write algorithm on top of it, not a new database engine.
|
|
282
378
|
|
|
283
|
-
**Retrieval** Hybrid vector and full text search fused by reciprocal rank in v1a
|
|
379
|
+
**Retrieval** Hybrid vector and full text search fused by reciprocal rank in v1a, with
|
|
380
|
+
the full text side ranked by BM25 computed in SQL over per scope corpus statistics.
|
|
284
381
|
Personalised PageRank via `networkx` lands in v1b for multi hop associative retrieval.
|
|
285
382
|
|
|
286
383
|
**Interface** [Model Context Protocol](https://modelcontextprotocol.io), so any compliant
|
|
@@ -57,6 +57,27 @@ echo-memory install --global
|
|
|
57
57
|
> hosted product. The import package and the CLI are both `echo_memory` / `echo-memory`;
|
|
58
58
|
> only the distribution name differs.
|
|
59
59
|
|
|
60
|
+
## Upgrading
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
pipx upgrade echo-mem
|
|
64
|
+
echo-memory init-db # apply any new migrations
|
|
65
|
+
echo-memory reindex # rebuild what the new code derives
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Both steps, in that order, and neither is optional on a store that predates the version
|
|
69
|
+
you just installed.
|
|
70
|
+
|
|
71
|
+
**Skipping `init-db` fails loudly.** A read against a schema older than the code raises an
|
|
72
|
+
undefined column, which is the good case: it stops rather than answering from something it
|
|
73
|
+
half understands.
|
|
74
|
+
|
|
75
|
+
**Skipping `reindex` fails silently, which is worse.** Derived data that the new code reads
|
|
76
|
+
differently is still the old data, and nothing errors. Concretely, on an upgrade to 0.5.3
|
|
77
|
+
the lexical corpus statistics are treated as stale until rebuilt, so BM25 stays off and
|
|
78
|
+
your queries keep ranking by `ts_rank` while the setting reports as on. `reindex` rebuilds
|
|
79
|
+
the embeddings and those statistics together. `echo-memory health` tells you afterwards.
|
|
80
|
+
|
|
60
81
|
## Usage
|
|
61
82
|
|
|
62
83
|
```bash
|
|
@@ -71,9 +92,13 @@ echo-memory export # everything, as JSON
|
|
|
71
92
|
echo-memory install --for cursor # wire one client, project scoped
|
|
72
93
|
echo-memory adopt # wire every MCP client on the machine, each with its own id
|
|
73
94
|
|
|
95
|
+
echo-memory infer-causal-hints # type the facts whose own sentence states a cause (dry run)
|
|
96
|
+
|
|
74
97
|
echo-memory eval # retrieval quality against your own store
|
|
75
98
|
echo-memory eval --context # what a recall costs against injecting everything
|
|
76
99
|
echo-memory eval --context --sweep # the same, as a curve across corpus size
|
|
100
|
+
echo-memory eval-external locomo <path> # LoCoMo, the corpus the published figures use
|
|
101
|
+
echo-memory eval-external longmemeval <path> # LongMemEval, the same
|
|
77
102
|
echo-memory calibrate # is entity resolution trustworthy on your data
|
|
78
103
|
echo-memory benchmark # write, query and digest latency
|
|
79
104
|
```
|
|
@@ -83,7 +108,7 @@ echo-memory benchmark # write, query and digest latency
|
|
|
83
108
|
| Tool | What it does |
|
|
84
109
|
|---|---|
|
|
85
110
|
| `write_episode` | Store entities and the facts connecting them. No model call. |
|
|
86
|
-
| `query_memory` | Hybrid vector and full text retrieval, fused by reciprocal rank. |
|
|
111
|
+
| `query_memory` | Hybrid vector and full text retrieval, fused by reciprocal rank. Full text ranks by BM25. Takes `as_of` to read the store as it stood at an instant, and `about` to return the facts recorded against one or two named entities. |
|
|
87
112
|
| `trace_cause` | Causal chains through the entities a subject matches, not a ranked list. |
|
|
88
113
|
| `record_recall_save` | Mark that a recalled fact saved re explaining something. Refuses a fact no read returned. |
|
|
89
114
|
| `get_audit_log` | Every change to memory, with a plain language reason. |
|
|
@@ -103,6 +128,59 @@ no summarisation, and retrieval today walks every active fact in the scope. It i
|
|
|
103
128
|
described in [`docs/designs/`](docs/designs/) and listed below as v1c, and this paragraph
|
|
104
129
|
used to claim it in the present tense.
|
|
105
130
|
|
|
131
|
+
**A ranked answer that says why each fact is in it.** Every returned fact carries
|
|
132
|
+
`score`, its cosine similarity to the query, `rank`, its position, and `matched`, the
|
|
133
|
+
channels that found it. `score` is deliberately not the fusion number: reciprocal rank
|
|
134
|
+
values are sums of 1/(k+rank) and mean nothing from one query to the next, while a
|
|
135
|
+
similarity means the same thing every time, which is what a caller thresholding on it
|
|
136
|
+
needs. It is computed for every fact returned, including the ones only full text search
|
|
137
|
+
found, because which channel retrieved a fact is an implementation detail and has no
|
|
138
|
+
business reaching a field callers read as relevance.
|
|
139
|
+
|
|
140
|
+
**The full text channel ranks by BM25.** Postgres's `ts_rank` counts term occurrences and
|
|
141
|
+
nothing else: no inverse document frequency, so a word in every fact of a scope counts as
|
|
142
|
+
much as one in three; no saturation, so repetition scales linearly; no length
|
|
143
|
+
normalisation, so a long fact is punished for being long. On 324 real prompts the rank 3
|
|
144
|
+
and rank 4 scores were identical in 59% of them, and reciprocal rank fusion reads rank
|
|
145
|
+
position and never the score underneath it, so a channel ordered by scan order handed the
|
|
146
|
+
fusion a coin flip.
|
|
147
|
+
|
|
148
|
+
BM25 is computed in SQL rather than installed, so no extension is added to your database.
|
|
149
|
+
Corpus statistics are rebuilt amortised on write inside the transaction that already holds
|
|
150
|
+
the scope's lock, and by `echo-memory reindex`; a scope that has none falls back to
|
|
151
|
+
`ts_rank` rather than ranking on statistics it does not have. `ECHO_MEMORY_LEXICAL_BM25=0`
|
|
152
|
+
returns to `ts_rank` everywhere.
|
|
153
|
+
|
|
154
|
+
**Facts about an entity, which resemblance cannot express.** `query_memory` takes
|
|
155
|
+
`about`, one or two entity names, and returns only the facts recorded against them. One
|
|
156
|
+
name gives the edges incident to that node; two give the edges between them in either
|
|
157
|
+
direction, which is "both names in the same fact" without going near the fact text.
|
|
158
|
+
|
|
159
|
+
This is not a ranking improvement and it is not a substitute for one. A query about one
|
|
160
|
+
team returns a semantically close fact about a different team because the wrong fact
|
|
161
|
+
genuinely does resemble the question, and no amount of better ranking removes it.
|
|
162
|
+
Identity is not similarity, and a fact is an edge between two nodes, so asking which
|
|
163
|
+
entity a fact is about is structural and exact.
|
|
164
|
+
|
|
165
|
+
Name matching is exact and case insensitive, never a substring, because a short name is a
|
|
166
|
+
substring of longer unrelated words. A name nothing is recorded under returns no facts
|
|
167
|
+
rather than the nearest thing, and so does a pair with no fact joining them: that is real
|
|
168
|
+
absence and it means nobody recorded this, not here is something adjacent. More than two
|
|
169
|
+
names is refused rather than answered empty, because an edge has two endpoints and a fact
|
|
170
|
+
about three entities is a question this model cannot express.
|
|
171
|
+
|
|
172
|
+
**The history the store has always kept is readable.** Every fact has carried `t_valid`
|
|
173
|
+
and `t_invalid` since the first migration, and the read path had only ever asked whether a
|
|
174
|
+
fact is current, so the store paid to keep the whole record of what a scope believed and
|
|
175
|
+
could not answer the first question a post mortem asks. `query_memory` takes `as_of` in
|
|
176
|
+
unix seconds and every channel honours it: vector, full text, the graph hop, the digest.
|
|
177
|
+
|
|
178
|
+
Supersession is what makes that more than a curiosity. Writing the same source, target and
|
|
179
|
+
relation again does not edit the old fact, it ends it: the old edge takes a `t_invalid` and
|
|
180
|
+
stays queryable. Nothing is ever rewritten, so the history is real rather than
|
|
181
|
+
reconstructed, which is the property the memory reconsolidation literature gives up and
|
|
182
|
+
the reason this project refuses that idea.
|
|
183
|
+
|
|
106
184
|
**Provenance on every fact.** Who wrote it, which tool, which project, when, and which
|
|
107
185
|
reads returned it. A superseded fact is never deleted. It stops being drawn and stays
|
|
108
186
|
reachable with its history.
|
|
@@ -115,13 +193,30 @@ this happen" is an ordered chain and similarity cannot produce one. "A led_to B"
|
|
|
115
193
|
caused_by A" are one claim written from opposite ends, and both assemble into the same
|
|
116
194
|
chain.
|
|
117
195
|
|
|
118
|
-
A fact with no hint is associative, which is the default and usually correct.
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
196
|
+
A fact with no hint is associative, which is the default and usually correct. An empty
|
|
197
|
+
answer from `trace_cause` says which kind of empty it is: nobody recorded a cause here is
|
|
198
|
+
a different fact about a store than there are no chains.
|
|
199
|
+
|
|
200
|
+
**Filling it in, for a store that predates it.** Facts written before 0.5.0 carry no
|
|
201
|
+
hint, so on an existing store `trace_cause` has nothing to walk.
|
|
202
|
+
`echo-memory infer-causal-hints` re-reads the fact text already stored with your own
|
|
203
|
+
model (`ECHO_MEMORY_LLM_API_KEY`, `ECHO_MEMORY_LLM_MODEL`) and types the edges whose own
|
|
204
|
+
sentence states the relation. Dry run by default; `--write` applies what it printed and
|
|
205
|
+
`--clear --write` takes it back.
|
|
206
|
+
|
|
207
|
+
This is extraction done late, not causal discovery, and the difference is enforced rather
|
|
208
|
+
than promised. A sentence with no causal connective is never sent to a model, so
|
|
209
|
+
co-occurrence is refused before it costs anything. Every proposal has to quote the words
|
|
210
|
+
that state the relation, and a quote that is not literally in the fact is dropped, so a
|
|
211
|
+
model reasoning from the world instead of reading the sentence gets nothing stored. Each
|
|
212
|
+
hint written is audited and marked as extracted late, which is what makes `--clear`
|
|
213
|
+
able to remove exactly these and never one you wrote at write time.
|
|
122
214
|
|
|
123
215
|
**No inference on the write path.** Extraction happens in the calling agent, so storing
|
|
124
|
-
a memory invokes no model on the server.
|
|
216
|
+
a memory invokes no model on the server. `infer-causal-hints` above is not an exception
|
|
217
|
+
to that and is worth being precise about: it is a command a person types against facts
|
|
218
|
+
already stored, it never runs as part of a write, a migration or a hook, and a store that
|
|
219
|
+
never runs it never causes a model call. The cost moved rather than vanished: the agent
|
|
125
220
|
has to arrive with entities and facts already extracted, which is what the
|
|
126
221
|
[tool contract](docs/DEVELOPMENT.md) spells out. The comparison that makes this matter is
|
|
127
222
|
Zep/Graphiti, the closest architectural match, whose own description of ingestion is that
|
|
@@ -146,8 +241,9 @@ difference nobody sized is not a result.
|
|
|
146
241
|
|---|---|---|
|
|
147
242
|
| Context per recall vs injecting everything | **96.7% less**, hit@10 0.872 over 1,190 questions | `echo-memory eval --context` |
|
|
148
243
|
| The same saving across 8x of corpus growth | 75.5% at 32 facts rising to **96.4% at 261**, hit@10 0.900 to 0.946 | `echo-memory eval --context --sweep` |
|
|
149
|
-
| LoCoMo retrieval, 1,982 questions, 5,882 turns | recall@10 **0.601**, hit@10 0.658, MRR 0.460 | `
|
|
150
|
-
| LongMemEval retrieval, 90 questions, 15 per type | session@10 **0.
|
|
244
|
+
| LoCoMo retrieval, 1,982 questions, 5,882 turns | recall@10 **0.601**, hit@10 0.658, MRR 0.460, session@10 0.850 | `echo-memory eval-external locomo` |
|
|
245
|
+
| LongMemEval retrieval, 90 questions, 15 per type | session@10 **0.937**, recall@10 0.727, MRR 0.428 | `echo-memory eval-external longmemeval --per-type 15` |
|
|
246
|
+
| Lexical BM25 against `ts_rank` | **+0.0271** MRR on entity_pair [+0.0083, +0.0481] and on multihop [+0.0166, +0.0381], the other two shapes inside noise, none worse | `echo-memory eval --ablate`, the `without BM25` row with its sign reversed |
|
|
151
247
|
| Server side model calls per write | **0** | `echo-memory benchmark` |
|
|
152
248
|
| Write, query, digest latency (median) | 15ms, 8ms, 1ms | `echo-memory benchmark` |
|
|
153
249
|
| Entity resolution AUC | 0.666, 95% CI [0.421, 0.881] | `echo-memory calibrate` |
|
|
@@ -257,7 +353,8 @@ not a thing to withhold from the person whose data it is.
|
|
|
257
353
|
organisation wide shared graph, with no forced migration later. The novel work is the
|
|
258
354
|
memory structure and the read/write algorithm on top of it, not a new database engine.
|
|
259
355
|
|
|
260
|
-
**Retrieval** Hybrid vector and full text search fused by reciprocal rank in v1a
|
|
356
|
+
**Retrieval** Hybrid vector and full text search fused by reciprocal rank in v1a, with
|
|
357
|
+
the full text side ranked by BM25 computed in SQL over per scope corpus statistics.
|
|
261
358
|
Personalised PageRank via `networkx` lands in v1b for multi hop associative retrieval.
|
|
262
359
|
|
|
263
360
|
**Interface** [Model Context Protocol](https://modelcontextprotocol.io), so any compliant
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
# (`-m echo_memory.server`) and every existing install reference it, and
|
|
7
7
|
# renaming the module would break live configuration to fix a cosmetic mismatch.
|
|
8
8
|
name = "echo-mem"
|
|
9
|
-
version = "0.5.
|
|
9
|
+
version = "0.5.4"
|
|
10
10
|
description = "A long-horizon memory architecture for AI agents"
|
|
11
11
|
readme = "README.md"
|
|
12
12
|
# BUSL-1.1 is the SPDX identifier for the Business Source License 1.1, so
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: echo-mem
|
|
3
|
-
Version: 0.5.
|
|
3
|
+
Version: 0.5.4
|
|
4
4
|
Summary: A long-horizon memory architecture for AI agents
|
|
5
5
|
License-Expression: BUSL-1.1
|
|
6
6
|
Requires-Python: >=3.11
|
|
@@ -80,6 +80,27 @@ echo-memory install --global
|
|
|
80
80
|
> hosted product. The import package and the CLI are both `echo_memory` / `echo-memory`;
|
|
81
81
|
> only the distribution name differs.
|
|
82
82
|
|
|
83
|
+
## Upgrading
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
pipx upgrade echo-mem
|
|
87
|
+
echo-memory init-db # apply any new migrations
|
|
88
|
+
echo-memory reindex # rebuild what the new code derives
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
Both steps, in that order, and neither is optional on a store that predates the version
|
|
92
|
+
you just installed.
|
|
93
|
+
|
|
94
|
+
**Skipping `init-db` fails loudly.** A read against a schema older than the code raises an
|
|
95
|
+
undefined column, which is the good case: it stops rather than answering from something it
|
|
96
|
+
half understands.
|
|
97
|
+
|
|
98
|
+
**Skipping `reindex` fails silently, which is worse.** Derived data that the new code reads
|
|
99
|
+
differently is still the old data, and nothing errors. Concretely, on an upgrade to 0.5.3
|
|
100
|
+
the lexical corpus statistics are treated as stale until rebuilt, so BM25 stays off and
|
|
101
|
+
your queries keep ranking by `ts_rank` while the setting reports as on. `reindex` rebuilds
|
|
102
|
+
the embeddings and those statistics together. `echo-memory health` tells you afterwards.
|
|
103
|
+
|
|
83
104
|
## Usage
|
|
84
105
|
|
|
85
106
|
```bash
|
|
@@ -94,9 +115,13 @@ echo-memory export # everything, as JSON
|
|
|
94
115
|
echo-memory install --for cursor # wire one client, project scoped
|
|
95
116
|
echo-memory adopt # wire every MCP client on the machine, each with its own id
|
|
96
117
|
|
|
118
|
+
echo-memory infer-causal-hints # type the facts whose own sentence states a cause (dry run)
|
|
119
|
+
|
|
97
120
|
echo-memory eval # retrieval quality against your own store
|
|
98
121
|
echo-memory eval --context # what a recall costs against injecting everything
|
|
99
122
|
echo-memory eval --context --sweep # the same, as a curve across corpus size
|
|
123
|
+
echo-memory eval-external locomo <path> # LoCoMo, the corpus the published figures use
|
|
124
|
+
echo-memory eval-external longmemeval <path> # LongMemEval, the same
|
|
100
125
|
echo-memory calibrate # is entity resolution trustworthy on your data
|
|
101
126
|
echo-memory benchmark # write, query and digest latency
|
|
102
127
|
```
|
|
@@ -106,7 +131,7 @@ echo-memory benchmark # write, query and digest latency
|
|
|
106
131
|
| Tool | What it does |
|
|
107
132
|
|---|---|
|
|
108
133
|
| `write_episode` | Store entities and the facts connecting them. No model call. |
|
|
109
|
-
| `query_memory` | Hybrid vector and full text retrieval, fused by reciprocal rank. |
|
|
134
|
+
| `query_memory` | Hybrid vector and full text retrieval, fused by reciprocal rank. Full text ranks by BM25. Takes `as_of` to read the store as it stood at an instant, and `about` to return the facts recorded against one or two named entities. |
|
|
110
135
|
| `trace_cause` | Causal chains through the entities a subject matches, not a ranked list. |
|
|
111
136
|
| `record_recall_save` | Mark that a recalled fact saved re explaining something. Refuses a fact no read returned. |
|
|
112
137
|
| `get_audit_log` | Every change to memory, with a plain language reason. |
|
|
@@ -126,6 +151,59 @@ no summarisation, and retrieval today walks every active fact in the scope. It i
|
|
|
126
151
|
described in [`docs/designs/`](docs/designs/) and listed below as v1c, and this paragraph
|
|
127
152
|
used to claim it in the present tense.
|
|
128
153
|
|
|
154
|
+
**A ranked answer that says why each fact is in it.** Every returned fact carries
|
|
155
|
+
`score`, its cosine similarity to the query, `rank`, its position, and `matched`, the
|
|
156
|
+
channels that found it. `score` is deliberately not the fusion number: reciprocal rank
|
|
157
|
+
values are sums of 1/(k+rank) and mean nothing from one query to the next, while a
|
|
158
|
+
similarity means the same thing every time, which is what a caller thresholding on it
|
|
159
|
+
needs. It is computed for every fact returned, including the ones only full text search
|
|
160
|
+
found, because which channel retrieved a fact is an implementation detail and has no
|
|
161
|
+
business reaching a field callers read as relevance.
|
|
162
|
+
|
|
163
|
+
**The full text channel ranks by BM25.** Postgres's `ts_rank` counts term occurrences and
|
|
164
|
+
nothing else: no inverse document frequency, so a word in every fact of a scope counts as
|
|
165
|
+
much as one in three; no saturation, so repetition scales linearly; no length
|
|
166
|
+
normalisation, so a long fact is punished for being long. On 324 real prompts the rank 3
|
|
167
|
+
and rank 4 scores were identical in 59% of them, and reciprocal rank fusion reads rank
|
|
168
|
+
position and never the score underneath it, so a channel ordered by scan order handed the
|
|
169
|
+
fusion a coin flip.
|
|
170
|
+
|
|
171
|
+
BM25 is computed in SQL rather than installed, so no extension is added to your database.
|
|
172
|
+
Corpus statistics are rebuilt amortised on write inside the transaction that already holds
|
|
173
|
+
the scope's lock, and by `echo-memory reindex`; a scope that has none falls back to
|
|
174
|
+
`ts_rank` rather than ranking on statistics it does not have. `ECHO_MEMORY_LEXICAL_BM25=0`
|
|
175
|
+
returns to `ts_rank` everywhere.
|
|
176
|
+
|
|
177
|
+
**Facts about an entity, which resemblance cannot express.** `query_memory` takes
|
|
178
|
+
`about`, one or two entity names, and returns only the facts recorded against them. One
|
|
179
|
+
name gives the edges incident to that node; two give the edges between them in either
|
|
180
|
+
direction, which is "both names in the same fact" without going near the fact text.
|
|
181
|
+
|
|
182
|
+
This is not a ranking improvement and it is not a substitute for one. A query about one
|
|
183
|
+
team returns a semantically close fact about a different team because the wrong fact
|
|
184
|
+
genuinely does resemble the question, and no amount of better ranking removes it.
|
|
185
|
+
Identity is not similarity, and a fact is an edge between two nodes, so asking which
|
|
186
|
+
entity a fact is about is structural and exact.
|
|
187
|
+
|
|
188
|
+
Name matching is exact and case insensitive, never a substring, because a short name is a
|
|
189
|
+
substring of longer unrelated words. A name nothing is recorded under returns no facts
|
|
190
|
+
rather than the nearest thing, and so does a pair with no fact joining them: that is real
|
|
191
|
+
absence and it means nobody recorded this, not here is something adjacent. More than two
|
|
192
|
+
names is refused rather than answered empty, because an edge has two endpoints and a fact
|
|
193
|
+
about three entities is a question this model cannot express.
|
|
194
|
+
|
|
195
|
+
**The history the store has always kept is readable.** Every fact has carried `t_valid`
|
|
196
|
+
and `t_invalid` since the first migration, and the read path had only ever asked whether a
|
|
197
|
+
fact is current, so the store paid to keep the whole record of what a scope believed and
|
|
198
|
+
could not answer the first question a post mortem asks. `query_memory` takes `as_of` in
|
|
199
|
+
unix seconds and every channel honours it: vector, full text, the graph hop, the digest.
|
|
200
|
+
|
|
201
|
+
Supersession is what makes that more than a curiosity. Writing the same source, target and
|
|
202
|
+
relation again does not edit the old fact, it ends it: the old edge takes a `t_invalid` and
|
|
203
|
+
stays queryable. Nothing is ever rewritten, so the history is real rather than
|
|
204
|
+
reconstructed, which is the property the memory reconsolidation literature gives up and
|
|
205
|
+
the reason this project refuses that idea.
|
|
206
|
+
|
|
129
207
|
**Provenance on every fact.** Who wrote it, which tool, which project, when, and which
|
|
130
208
|
reads returned it. A superseded fact is never deleted. It stops being drawn and stays
|
|
131
209
|
reachable with its history.
|
|
@@ -138,13 +216,30 @@ this happen" is an ordered chain and similarity cannot produce one. "A led_to B"
|
|
|
138
216
|
caused_by A" are one claim written from opposite ends, and both assemble into the same
|
|
139
217
|
chain.
|
|
140
218
|
|
|
141
|
-
A fact with no hint is associative, which is the default and usually correct.
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
219
|
+
A fact with no hint is associative, which is the default and usually correct. An empty
|
|
220
|
+
answer from `trace_cause` says which kind of empty it is: nobody recorded a cause here is
|
|
221
|
+
a different fact about a store than there are no chains.
|
|
222
|
+
|
|
223
|
+
**Filling it in, for a store that predates it.** Facts written before 0.5.0 carry no
|
|
224
|
+
hint, so on an existing store `trace_cause` has nothing to walk.
|
|
225
|
+
`echo-memory infer-causal-hints` re-reads the fact text already stored with your own
|
|
226
|
+
model (`ECHO_MEMORY_LLM_API_KEY`, `ECHO_MEMORY_LLM_MODEL`) and types the edges whose own
|
|
227
|
+
sentence states the relation. Dry run by default; `--write` applies what it printed and
|
|
228
|
+
`--clear --write` takes it back.
|
|
229
|
+
|
|
230
|
+
This is extraction done late, not causal discovery, and the difference is enforced rather
|
|
231
|
+
than promised. A sentence with no causal connective is never sent to a model, so
|
|
232
|
+
co-occurrence is refused before it costs anything. Every proposal has to quote the words
|
|
233
|
+
that state the relation, and a quote that is not literally in the fact is dropped, so a
|
|
234
|
+
model reasoning from the world instead of reading the sentence gets nothing stored. Each
|
|
235
|
+
hint written is audited and marked as extracted late, which is what makes `--clear`
|
|
236
|
+
able to remove exactly these and never one you wrote at write time.
|
|
145
237
|
|
|
146
238
|
**No inference on the write path.** Extraction happens in the calling agent, so storing
|
|
147
|
-
a memory invokes no model on the server.
|
|
239
|
+
a memory invokes no model on the server. `infer-causal-hints` above is not an exception
|
|
240
|
+
to that and is worth being precise about: it is a command a person types against facts
|
|
241
|
+
already stored, it never runs as part of a write, a migration or a hook, and a store that
|
|
242
|
+
never runs it never causes a model call. The cost moved rather than vanished: the agent
|
|
148
243
|
has to arrive with entities and facts already extracted, which is what the
|
|
149
244
|
[tool contract](docs/DEVELOPMENT.md) spells out. The comparison that makes this matter is
|
|
150
245
|
Zep/Graphiti, the closest architectural match, whose own description of ingestion is that
|
|
@@ -169,8 +264,9 @@ difference nobody sized is not a result.
|
|
|
169
264
|
|---|---|---|
|
|
170
265
|
| Context per recall vs injecting everything | **96.7% less**, hit@10 0.872 over 1,190 questions | `echo-memory eval --context` |
|
|
171
266
|
| The same saving across 8x of corpus growth | 75.5% at 32 facts rising to **96.4% at 261**, hit@10 0.900 to 0.946 | `echo-memory eval --context --sweep` |
|
|
172
|
-
| LoCoMo retrieval, 1,982 questions, 5,882 turns | recall@10 **0.601**, hit@10 0.658, MRR 0.460 | `
|
|
173
|
-
| LongMemEval retrieval, 90 questions, 15 per type | session@10 **0.
|
|
267
|
+
| LoCoMo retrieval, 1,982 questions, 5,882 turns | recall@10 **0.601**, hit@10 0.658, MRR 0.460, session@10 0.850 | `echo-memory eval-external locomo` |
|
|
268
|
+
| LongMemEval retrieval, 90 questions, 15 per type | session@10 **0.937**, recall@10 0.727, MRR 0.428 | `echo-memory eval-external longmemeval --per-type 15` |
|
|
269
|
+
| Lexical BM25 against `ts_rank` | **+0.0271** MRR on entity_pair [+0.0083, +0.0481] and on multihop [+0.0166, +0.0381], the other two shapes inside noise, none worse | `echo-memory eval --ablate`, the `without BM25` row with its sign reversed |
|
|
174
270
|
| Server side model calls per write | **0** | `echo-memory benchmark` |
|
|
175
271
|
| Write, query, digest latency (median) | 15ms, 8ms, 1ms | `echo-memory benchmark` |
|
|
176
272
|
| Entity resolution AUC | 0.666, 95% CI [0.421, 0.881] | `echo-memory calibrate` |
|
|
@@ -280,7 +376,8 @@ not a thing to withhold from the person whose data it is.
|
|
|
280
376
|
organisation wide shared graph, with no forced migration later. The novel work is the
|
|
281
377
|
memory structure and the read/write algorithm on top of it, not a new database engine.
|
|
282
378
|
|
|
283
|
-
**Retrieval** Hybrid vector and full text search fused by reciprocal rank in v1a
|
|
379
|
+
**Retrieval** Hybrid vector and full text search fused by reciprocal rank in v1a, with
|
|
380
|
+
the full text side ranked by BM25 computed in SQL over per scope corpus statistics.
|
|
284
381
|
Personalised PageRank via `networkx` lands in v1b for multi hop associative retrieval.
|
|
285
382
|
|
|
286
383
|
**Interface** [Model Context Protocol](https://modelcontextprotocol.io), so any compliant
|
|
@@ -25,9 +25,11 @@ src/echo_memory/cli/export.py
|
|
|
25
25
|
src/echo_memory/cli/graph.py
|
|
26
26
|
src/echo_memory/cli/health.py
|
|
27
27
|
src/echo_memory/cli/hooks.py
|
|
28
|
+
src/echo_memory/cli/infer_causal.py
|
|
28
29
|
src/echo_memory/cli/initdb.py
|
|
29
30
|
src/echo_memory/cli/install.py
|
|
30
31
|
src/echo_memory/cli/judge.py
|
|
32
|
+
src/echo_memory/cli/llm.py
|
|
31
33
|
src/echo_memory/cli/main.py
|
|
32
34
|
src/echo_memory/cli/merge.py
|
|
33
35
|
src/echo_memory/cli/queue.py
|
|
@@ -45,6 +47,7 @@ src/echo_memory/cli/trial.py
|
|
|
45
47
|
src/echo_memory/cli/unmerge.py
|
|
46
48
|
src/echo_memory/cli/why.py
|
|
47
49
|
src/echo_memory/eval/__init__.py
|
|
50
|
+
src/echo_memory/eval/external.py
|
|
48
51
|
src/echo_memory/eval/independent.py
|
|
49
52
|
src/echo_memory/eval/retrieval.py
|
|
50
53
|
src/echo_memory/eval/sweep.py
|
|
@@ -89,10 +92,15 @@ src/echo_memory/migrations/versions/0021_delivery_evidence.py
|
|
|
89
92
|
src/echo_memory/migrations/versions/0022_independent_evaluation.py
|
|
90
93
|
src/echo_memory/migrations/versions/0023_second_judge.py
|
|
91
94
|
src/echo_memory/migrations/versions/0024_node_by_id.py
|
|
95
|
+
src/echo_memory/migrations/versions/0025_lexical_statistics.py
|
|
96
|
+
src/echo_memory/migrations/versions/0026_late_causal_extraction.py
|
|
97
|
+
src/echo_memory/migrations/versions/0027_lexical_staleness_without_a_scan.py
|
|
92
98
|
src/echo_memory/retrieval/__init__.py
|
|
99
|
+
src/echo_memory/retrieval/bm25.py
|
|
93
100
|
src/echo_memory/retrieval/causality.py
|
|
94
101
|
src/echo_memory/retrieval/fusion.py
|
|
95
102
|
src/echo_memory/retrieval/query_memory.py
|
|
103
|
+
src/echo_memory/retrieval/salience.py
|
|
96
104
|
src/echo_memory/skill/SKILL.md
|
|
97
105
|
src/echo_memory/trial/__init__.py
|
|
98
106
|
src/echo_memory/trial/check.py
|