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.
Files changed (110) hide show
  1. {echo_mem-0.5.2/src/echo_mem.egg-info → echo_mem-0.5.4}/PKG-INFO +107 -10
  2. {echo_mem-0.5.2 → echo_mem-0.5.4}/README.md +106 -9
  3. {echo_mem-0.5.2 → echo_mem-0.5.4}/pyproject.toml +1 -1
  4. {echo_mem-0.5.2 → echo_mem-0.5.4/src/echo_mem.egg-info}/PKG-INFO +107 -10
  5. {echo_mem-0.5.2 → echo_mem-0.5.4}/src/echo_mem.egg-info/SOURCES.txt +8 -0
  6. echo_mem-0.5.4/src/echo_memory/cli/infer_causal.py +737 -0
  7. echo_mem-0.5.4/src/echo_memory/cli/llm.py +132 -0
  8. {echo_mem-0.5.2 → echo_mem-0.5.4}/src/echo_memory/cli/main.py +167 -1
  9. echo_mem-0.5.4/src/echo_memory/eval/external.py +713 -0
  10. {echo_mem-0.5.2 → echo_mem-0.5.4}/src/echo_memory/ingestion/write_episode.py +7 -0
  11. echo_mem-0.5.4/src/echo_memory/migrations/versions/0025_lexical_statistics.py +72 -0
  12. echo_mem-0.5.4/src/echo_memory/migrations/versions/0026_late_causal_extraction.py +111 -0
  13. echo_mem-0.5.4/src/echo_memory/migrations/versions/0027_lexical_staleness_without_a_scan.py +47 -0
  14. echo_mem-0.5.4/src/echo_memory/retrieval/bm25.py +320 -0
  15. {echo_mem-0.5.2 → echo_mem-0.5.4}/src/echo_memory/retrieval/query_memory.py +397 -21
  16. echo_mem-0.5.4/src/echo_memory/retrieval/salience.py +98 -0
  17. {echo_mem-0.5.2 → echo_mem-0.5.4}/src/echo_memory/server.py +39 -26
  18. {echo_mem-0.5.2 → echo_mem-0.5.4}/src/echo_memory/skill/SKILL.md +6 -0
  19. {echo_mem-0.5.2 → echo_mem-0.5.4}/LICENSE +0 -0
  20. {echo_mem-0.5.2 → echo_mem-0.5.4}/LICENSE-APACHE-2.0 +0 -0
  21. {echo_mem-0.5.2 → echo_mem-0.5.4}/setup.cfg +0 -0
  22. {echo_mem-0.5.2 → echo_mem-0.5.4}/src/echo_mem.egg-info/dependency_links.txt +0 -0
  23. {echo_mem-0.5.2 → echo_mem-0.5.4}/src/echo_mem.egg-info/entry_points.txt +0 -0
  24. {echo_mem-0.5.2 → echo_mem-0.5.4}/src/echo_mem.egg-info/requires.txt +0 -0
  25. {echo_mem-0.5.2 → echo_mem-0.5.4}/src/echo_mem.egg-info/top_level.txt +0 -0
  26. {echo_mem-0.5.2 → echo_mem-0.5.4}/src/echo_memory/__init__.py +0 -0
  27. {echo_mem-0.5.2 → echo_mem-0.5.4}/src/echo_memory/audit/__init__.py +0 -0
  28. {echo_mem-0.5.2 → echo_mem-0.5.4}/src/echo_memory/audit/get_audit_log.py +0 -0
  29. {echo_mem-0.5.2 → echo_mem-0.5.4}/src/echo_memory/cli/__init__.py +0 -0
  30. {echo_mem-0.5.2 → echo_mem-0.5.4}/src/echo_memory/cli/adopt.py +0 -0
  31. {echo_mem-0.5.2 → echo_mem-0.5.4}/src/echo_memory/cli/analyse.py +0 -0
  32. {echo_mem-0.5.2 → echo_mem-0.5.4}/src/echo_memory/cli/benchmark.py +0 -0
  33. {echo_mem-0.5.2 → echo_mem-0.5.4}/src/echo_memory/cli/calibrate.py +0 -0
  34. {echo_mem-0.5.2 → echo_mem-0.5.4}/src/echo_memory/cli/connect.py +0 -0
  35. {echo_mem-0.5.2 → echo_mem-0.5.4}/src/echo_memory/cli/dashboard.py +0 -0
  36. {echo_mem-0.5.2 → echo_mem-0.5.4}/src/echo_memory/cli/dashboard_html.py +0 -0
  37. {echo_mem-0.5.2 → echo_mem-0.5.4}/src/echo_memory/cli/export.py +0 -0
  38. {echo_mem-0.5.2 → echo_mem-0.5.4}/src/echo_memory/cli/graph.py +0 -0
  39. {echo_mem-0.5.2 → echo_mem-0.5.4}/src/echo_memory/cli/health.py +0 -0
  40. {echo_mem-0.5.2 → echo_mem-0.5.4}/src/echo_memory/cli/hooks.py +0 -0
  41. {echo_mem-0.5.2 → echo_mem-0.5.4}/src/echo_memory/cli/initdb.py +0 -0
  42. {echo_mem-0.5.2 → echo_mem-0.5.4}/src/echo_memory/cli/install.py +0 -0
  43. {echo_mem-0.5.2 → echo_mem-0.5.4}/src/echo_memory/cli/judge.py +0 -0
  44. {echo_mem-0.5.2 → echo_mem-0.5.4}/src/echo_memory/cli/merge.py +0 -0
  45. {echo_mem-0.5.2 → echo_mem-0.5.4}/src/echo_memory/cli/queue.py +0 -0
  46. {echo_mem-0.5.2 → echo_mem-0.5.4}/src/echo_memory/cli/quickstart.py +0 -0
  47. {echo_mem-0.5.2 → echo_mem-0.5.4}/src/echo_memory/cli/reattribute.py +0 -0
  48. {echo_mem-0.5.2 → echo_mem-0.5.4}/src/echo_memory/cli/reattribute_cmd.py +0 -0
  49. {echo_mem-0.5.2 → echo_mem-0.5.4}/src/echo_memory/cli/recall.py +0 -0
  50. {echo_mem-0.5.2 → echo_mem-0.5.4}/src/echo_memory/cli/reconcile.py +0 -0
  51. {echo_mem-0.5.2 → echo_mem-0.5.4}/src/echo_memory/cli/reindex.py +0 -0
  52. {echo_mem-0.5.2 → echo_mem-0.5.4}/src/echo_memory/cli/session_start.py +0 -0
  53. {echo_mem-0.5.2 → echo_mem-0.5.4}/src/echo_memory/cli/skill.py +0 -0
  54. {echo_mem-0.5.2 → echo_mem-0.5.4}/src/echo_memory/cli/status.py +0 -0
  55. {echo_mem-0.5.2 → echo_mem-0.5.4}/src/echo_memory/cli/stop_gate.py +0 -0
  56. {echo_mem-0.5.2 → echo_mem-0.5.4}/src/echo_memory/cli/trial.py +0 -0
  57. {echo_mem-0.5.2 → echo_mem-0.5.4}/src/echo_memory/cli/unmerge.py +0 -0
  58. {echo_mem-0.5.2 → echo_mem-0.5.4}/src/echo_memory/cli/why.py +0 -0
  59. {echo_mem-0.5.2 → echo_mem-0.5.4}/src/echo_memory/client.py +0 -0
  60. {echo_mem-0.5.2 → echo_mem-0.5.4}/src/echo_memory/eval/__init__.py +0 -0
  61. {echo_mem-0.5.2 → echo_mem-0.5.4}/src/echo_memory/eval/independent.py +0 -0
  62. {echo_mem-0.5.2 → echo_mem-0.5.4}/src/echo_memory/eval/retrieval.py +0 -0
  63. {echo_mem-0.5.2 → echo_mem-0.5.4}/src/echo_memory/eval/sweep.py +0 -0
  64. {echo_mem-0.5.2 → echo_mem-0.5.4}/src/echo_memory/graph/__init__.py +0 -0
  65. {echo_mem-0.5.2 → echo_mem-0.5.4}/src/echo_memory/graph/communities.py +0 -0
  66. {echo_mem-0.5.2 → echo_mem-0.5.4}/src/echo_memory/infra/__init__.py +0 -0
  67. {echo_mem-0.5.2 → echo_mem-0.5.4}/src/echo_memory/infra/config.py +0 -0
  68. {echo_mem-0.5.2 → echo_mem-0.5.4}/src/echo_memory/infra/db.py +0 -0
  69. {echo_mem-0.5.2 → echo_mem-0.5.4}/src/echo_memory/infra/logging.py +0 -0
  70. {echo_mem-0.5.2 → echo_mem-0.5.4}/src/echo_memory/infra/pool.py +0 -0
  71. {echo_mem-0.5.2 → echo_mem-0.5.4}/src/echo_memory/infra/project.py +0 -0
  72. {echo_mem-0.5.2 → echo_mem-0.5.4}/src/echo_memory/ingestion/__init__.py +0 -0
  73. {echo_mem-0.5.2 → echo_mem-0.5.4}/src/echo_memory/ingestion/activity.py +0 -0
  74. {echo_mem-0.5.2 → echo_mem-0.5.4}/src/echo_memory/ingestion/bootstrap.py +0 -0
  75. {echo_mem-0.5.2 → echo_mem-0.5.4}/src/echo_memory/ingestion/capture.py +0 -0
  76. {echo_mem-0.5.2 → echo_mem-0.5.4}/src/echo_memory/ingestion/embeddings.py +0 -0
  77. {echo_mem-0.5.2 → echo_mem-0.5.4}/src/echo_memory/ingestion/neighbourhood.py +0 -0
  78. {echo_mem-0.5.2 → echo_mem-0.5.4}/src/echo_memory/ingestion/resolution.py +0 -0
  79. {echo_mem-0.5.2 → echo_mem-0.5.4}/src/echo_memory/migrations/env.py +0 -0
  80. {echo_mem-0.5.2 → echo_mem-0.5.4}/src/echo_memory/migrations/versions/0001_initial_schema.py +0 -0
  81. {echo_mem-0.5.2 → echo_mem-0.5.4}/src/echo_memory/migrations/versions/0002_trial_observations.py +0 -0
  82. {echo_mem-0.5.2 → echo_mem-0.5.4}/src/echo_memory/migrations/versions/0003_project_and_agent_provenance.py +0 -0
  83. {echo_mem-0.5.2 → echo_mem-0.5.4}/src/echo_memory/migrations/versions/0004_bootstrap_state.py +0 -0
  84. {echo_mem-0.5.2 → echo_mem-0.5.4}/src/echo_memory/migrations/versions/0005_recall_save_guards.py +0 -0
  85. {echo_mem-0.5.2 → echo_mem-0.5.4}/src/echo_memory/migrations/versions/0006_project_analysis.py +0 -0
  86. {echo_mem-0.5.2 → echo_mem-0.5.4}/src/echo_memory/migrations/versions/0007_backfill_unknown_agent.py +0 -0
  87. {echo_mem-0.5.2 → echo_mem-0.5.4}/src/echo_memory/migrations/versions/0008_read_events.py +0 -0
  88. {echo_mem-0.5.2 → echo_mem-0.5.4}/src/echo_memory/migrations/versions/0009_read_event_attribution.py +0 -0
  89. {echo_mem-0.5.2 → echo_mem-0.5.4}/src/echo_memory/migrations/versions/0010_stop_gate_fired.py +0 -0
  90. {echo_mem-0.5.2 → echo_mem-0.5.4}/src/echo_memory/migrations/versions/0011_backfill_absent_agent.py +0 -0
  91. {echo_mem-0.5.2 → echo_mem-0.5.4}/src/echo_memory/migrations/versions/0012_read_event_agent.py +0 -0
  92. {echo_mem-0.5.2 → echo_mem-0.5.4}/src/echo_memory/migrations/versions/0013_active_fact_indexes.py +0 -0
  93. {echo_mem-0.5.2 → echo_mem-0.5.4}/src/echo_memory/migrations/versions/0014_session_activity.py +0 -0
  94. {echo_mem-0.5.2 → echo_mem-0.5.4}/src/echo_memory/migrations/versions/0015_retracted_observations.py +0 -0
  95. {echo_mem-0.5.2 → echo_mem-0.5.4}/src/echo_memory/migrations/versions/0016_one_node_per_name.py +0 -0
  96. {echo_mem-0.5.2 → echo_mem-0.5.4}/src/echo_memory/migrations/versions/0017_writer_version.py +0 -0
  97. {echo_mem-0.5.2 → echo_mem-0.5.4}/src/echo_memory/migrations/versions/0018_trial_runs_are_history.py +0 -0
  98. {echo_mem-0.5.2 → echo_mem-0.5.4}/src/echo_memory/migrations/versions/0019_versions_expected_since.py +0 -0
  99. {echo_mem-0.5.2 → echo_mem-0.5.4}/src/echo_memory/migrations/versions/0020_who_closed_a_queued_document.py +0 -0
  100. {echo_mem-0.5.2 → echo_mem-0.5.4}/src/echo_memory/migrations/versions/0021_delivery_evidence.py +0 -0
  101. {echo_mem-0.5.2 → echo_mem-0.5.4}/src/echo_memory/migrations/versions/0022_independent_evaluation.py +0 -0
  102. {echo_mem-0.5.2 → echo_mem-0.5.4}/src/echo_memory/migrations/versions/0023_second_judge.py +0 -0
  103. {echo_mem-0.5.2 → echo_mem-0.5.4}/src/echo_memory/migrations/versions/0024_node_by_id.py +0 -0
  104. {echo_mem-0.5.2 → echo_mem-0.5.4}/src/echo_memory/retrieval/__init__.py +0 -0
  105. {echo_mem-0.5.2 → echo_mem-0.5.4}/src/echo_memory/retrieval/causality.py +0 -0
  106. {echo_mem-0.5.2 → echo_mem-0.5.4}/src/echo_memory/retrieval/fusion.py +0 -0
  107. {echo_mem-0.5.2 → echo_mem-0.5.4}/src/echo_memory/trial/__init__.py +0 -0
  108. {echo_mem-0.5.2 → echo_mem-0.5.4}/src/echo_memory/trial/check.py +0 -0
  109. {echo_mem-0.5.2 → echo_mem-0.5.4}/src/echo_memory/trial/observations.py +0 -0
  110. {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.2
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. Facts
142
- written before 0.5.0 have no hint and are not a gap to fix. An empty answer from
143
- `trace_cause` says which kind of empty it is: nobody recorded a cause here is a
144
- different fact about a store than there are no chains.
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. The cost moved rather than vanished: the agent
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 | `scripts/locomo-bench.py` |
173
- | LongMemEval retrieval, 90 questions, 15 per type | session@10 **0.940**, turn@10 0.727 | `scripts/longmemeval-bench.py --per-type 15` |
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. Facts
119
- written before 0.5.0 have no hint and are not a gap to fix. An empty answer from
120
- `trace_cause` says which kind of empty it is: nobody recorded a cause here is a
121
- different fact about a store than there are no chains.
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. The cost moved rather than vanished: the agent
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 | `scripts/locomo-bench.py` |
150
- | LongMemEval retrieval, 90 questions, 15 per type | session@10 **0.940**, turn@10 0.727 | `scripts/longmemeval-bench.py --per-type 15` |
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.2"
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.2
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. Facts
142
- written before 0.5.0 have no hint and are not a gap to fix. An empty answer from
143
- `trace_cause` says which kind of empty it is: nobody recorded a cause here is a
144
- different fact about a store than there are no chains.
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. The cost moved rather than vanished: the agent
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 | `scripts/locomo-bench.py` |
173
- | LongMemEval retrieval, 90 questions, 15 per type | session@10 **0.940**, turn@10 0.727 | `scripts/longmemeval-bench.py --per-type 15` |
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