nexusmem 0.2.0 → 0.3.1
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.
- package/CHANGELOG.md +218 -126
- package/README.md +117 -39
- package/dist/cli/index.js +818 -97
- package/dist/cli/index.js.map +1 -1
- package/package.json +7 -4
package/CHANGELOG.md
CHANGED
|
@@ -1,126 +1,218 @@
|
|
|
1
|
-
# Changelog
|
|
2
|
-
|
|
3
|
-
Notable changes per published version. Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/);
|
|
4
|
-
this project uses [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
5
|
-
|
|
6
|
-
Tags were added retroactively on 2026-08-11 and point at the exact commits the npm tarballs were
|
|
7
|
-
built from, matched by publish timestamp: `v0.1.0` → `67a4776`, `v0.1.1` → `809e62c`,
|
|
8
|
-
`v0.1.2` → `b22a3b0`.
|
|
9
|
-
|
|
10
|
-
## [Unreleased]
|
|
11
|
-
|
|
12
|
-
## [0.
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
Notable changes per published version. Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/);
|
|
4
|
+
this project uses [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
5
|
+
|
|
6
|
+
Tags were added retroactively on 2026-08-11 and point at the exact commits the npm tarballs were
|
|
7
|
+
built from, matched by publish timestamp: `v0.1.0` → `67a4776`, `v0.1.1` → `809e62c`,
|
|
8
|
+
`v0.1.2` → `b22a3b0`.
|
|
9
|
+
|
|
10
|
+
## [Unreleased]
|
|
11
|
+
|
|
12
|
+
## [0.3.1] — 2026-08-15
|
|
13
|
+
|
|
14
|
+
### Added
|
|
15
|
+
|
|
16
|
+
- **`scripts/benchmark.ts` (`npm run bench`) — a reproducible end-to-end token-saving benchmark.**
|
|
17
|
+
Compares `packed.tokensUsed` against two baselines (full-file-read and `git log -p`) for the same
|
|
18
|
+
files a query's packed nodes touch, over a query set derived mechanically from the corpus itself
|
|
19
|
+
rather than hand-picked. Used to measure the README's `## What it costs you` numbers against both
|
|
20
|
+
this repo (62 commits) and a 9,567-commit external corpus (`vitejs/vite`) — see README for the
|
|
21
|
+
numbers and their methodology caveats.
|
|
22
|
+
|
|
23
|
+
### Fixed
|
|
24
|
+
|
|
25
|
+
- **A generic 2-3 letter local model title, "id" as a search token, and one node's chunks flooding a
|
|
26
|
+
result set — three ranking/retrieval edge cases found by dogfooding, each verified with a red test
|
|
27
|
+
before the fix.**
|
|
28
|
+
- Session-summary titles: the local model sometimes wrote a role-framing line ("Role: Lead Systems
|
|
29
|
+
Engineer...") instead of a summary, in Thai and English alike. The existing generic-title filter
|
|
30
|
+
didn't catch either language, since it only matched English words like "summary"/"update".
|
|
31
|
+
- Search: the token `id` alone was prefix-matching unrelated shell commands like
|
|
32
|
+
`winget install --id ...`, because every query token was OR-ed with no floor and no stopword
|
|
33
|
+
list, and bm25 gives a rare-but-generic token an inflated score purely from scarcity.
|
|
34
|
+
- Packing: `conversation_turn` and `doc_section` both chunk one reply or file into several nodes
|
|
35
|
+
sharing the same timestamp; up to 2 may now appear in one packed result, down from unlimited.
|
|
36
|
+
|
|
37
|
+
- **A repo's memory could silently split in two if its git remote URL ever changed** (a GitHub
|
|
38
|
+
account rename, an org transfer). Project identity is derived from the remote URL on purpose — so
|
|
39
|
+
the same repo re-cloned to a new path or machine keeps sharing memory — but a changed URL on the
|
|
40
|
+
*same* path minted a new id and stranded every node synced under the old one, invisible to
|
|
41
|
+
`status`/`query`/MCP from then on. `sync` now detects a prior id already recorded in the repo's own
|
|
42
|
+
database and reconciles it forward: recomputable node kinds (session summaries, hook-sourced shell
|
|
43
|
+
history) are migrated under their correct new id and deduplicated against anything already synced;
|
|
44
|
+
conversation turns, whose identity can't be recomputed, are reassigned in place. Git commits,
|
|
45
|
+
diffs, and doc sections are left alone — a normal sync already re-derives them completely, so
|
|
46
|
+
there is nothing to migrate.
|
|
47
|
+
|
|
48
|
+
## [0.3.0] — 2026-08-13
|
|
49
|
+
|
|
50
|
+
**Upgrade note.** The first `sync` after upgrading drops every stored embedding and rebuilds it.
|
|
51
|
+
This is not optional and it is not a bug: embeddings now come from Ollama's `/api/embed`, which
|
|
52
|
+
returns L2-normalised vectors, while the previous `/api/embeddings` did not — measured norms of 1.0
|
|
53
|
+
and 20.7 for the same input. `nodes_vec` ranks by Euclidean distance and records no per-row
|
|
54
|
+
provenance, so a corpus holding both would separate by scale rather than by meaning. `sync` says
|
|
55
|
+
what it dropped, nodes are untouched, and BM25 keeps working while the rebuild runs. On this repo
|
|
56
|
+
the rebuild was 833 nodes in one pass, inside a 9-second sync.
|
|
57
|
+
|
|
58
|
+
### Added
|
|
59
|
+
|
|
60
|
+
- **Session summaries via a local model** (`sources.session`, opt-in, off by default). Each
|
|
61
|
+
finished session becomes one distilled `session_summary` node — decisions and their reasons —
|
|
62
|
+
alongside the raw exchanges. Runs a local Ollama chat model (`qwen2.5:3b` by default); nothing is
|
|
63
|
+
downloaded automatically and no transcript leaves the machine. New `scan-session` command, with
|
|
64
|
+
`--dry-run` to print the exact prompt a session would produce without calling the model.
|
|
65
|
+
Bounded three ways: a session must be quiet for `settleMinutes` (default 30) before it is
|
|
66
|
+
eligible, the prompt is hashed so an unchanged session never reaches the model again, and
|
|
67
|
+
`maxSessions` caps how many are summarized per sync. Every exchange is redacted before the model
|
|
68
|
+
sees it, and the model's own output is redacted again before it is stored.
|
|
69
|
+
- **`sync --embed-limit <n>`** to cap the embedding pass, for when draining the whole backlog is not
|
|
70
|
+
wanted.
|
|
71
|
+
|
|
72
|
+
### Changed
|
|
73
|
+
|
|
74
|
+
- **The embedding pass drains the backlog in one `sync`** instead of stopping after 200 nodes, and
|
|
75
|
+
sends texts to Ollama in batches of 32 — measured at 4.21x the throughput of one call per node
|
|
76
|
+
(20.3ms → 4.8ms per node, over 96 real nodes from this repo). Leaving it uncapped is safe because
|
|
77
|
+
paging walks rowids monotonically, so a node the provider failed on is passed over rather than
|
|
78
|
+
retried forever, and because three consecutive dead requests end the pass: an Ollama that is not
|
|
79
|
+
running now costs three requests rather than one timeout per node.
|
|
80
|
+
- **Embeddings carry a provider identity.** Changing the embedding model, or upgrading from a
|
|
81
|
+
release that recorded no identity, drops the vectors and re-embeds rather than ranking across a
|
|
82
|
+
mixture.
|
|
83
|
+
- **Ranking priors now share one budget instead of getting one each.** `signal` and `recency` are
|
|
84
|
+
query-independent, and each was separately capped at overturning a 2× relevance gap. The score
|
|
85
|
+
multiplies them, so together they could overturn 4× — which is not a corner case but a description
|
|
86
|
+
of every commit made during an active working day, fresh and high-signal at once. A query about the
|
|
87
|
+
PowerShell hook returned two unrelated same-day `fix:` commits at ranks 3 and 4 while the section
|
|
88
|
+
that answered it sat at rank 6. The 2× budget is now the bound on the priors *jointly*, split
|
|
89
|
+
between them (`signal^0.215 × recency^0.288`, down from `^0.431` and `^0.576`), and a third prior
|
|
90
|
+
would re-divide the same budget rather than enlarge it. Measured on four real queries against this
|
|
91
|
+
repository's memory: the answering section rose in three of them — the rationale for "why BM25
|
|
92
|
+
before vector search" went from rank 4 to rank 1 — and no query's correct top hit was displaced.
|
|
93
|
+
|
|
94
|
+
### Known limitation
|
|
95
|
+
|
|
96
|
+
- Session-summary *titles* depend on the model following a fixed output format, and a 3B model often
|
|
97
|
+
does not. Measured over 14 real sessions, roughly a third came back usable; the rest were
|
|
98
|
+
conversational preambles, stray bullets, or a bare "Summary of the Session". Those are rejected
|
|
99
|
+
and the title falls back to the first line of the question that opened the session — always
|
|
100
|
+
specific, not always elegant. Compliance was worst on long sessions and on transcripts not in
|
|
101
|
+
English. `sources.session.model` takes a larger model if it matters.
|
|
102
|
+
|
|
103
|
+
## [0.2.0] — 2026-08-12
|
|
104
|
+
|
|
105
|
+
**Upgrade note.** Both new sources are on by default, so the first `sync` after upgrading an
|
|
106
|
+
existing project ingests the patches of its 200 most recent commits and starts recording the
|
|
107
|
+
repository in `~/.nexusmem/projects.json`. Set `sources.diff.enabled` to `false` in
|
|
108
|
+
`.nexusmem/config.json` if you would rather not, and `NEXUSMEM_HOME` relocates the user-scoped
|
|
109
|
+
directory. Nothing existing is rewritten or lost.
|
|
110
|
+
|
|
111
|
+
### Added
|
|
112
|
+
|
|
113
|
+
- **Diff-level nodes.** Commit patches are now indexed, one node per changed file, so a question
|
|
114
|
+
about *what the change looked like* reaches the lines themselves rather than the commit message
|
|
115
|
+
and a `+41/-6` summary. New `code_diff` kind, `diff` source, `scan-diff` preview command, and a
|
|
116
|
+
`sources.diff` config block. Read by a second `git log --patch` walk with its own cursor: folding
|
|
117
|
+
it into the existing `--numstat` walk would put patch text and numstat rows in one field, where a
|
|
118
|
+
diff line reading `-1\t2\tfoo` is indistinguishable from a real file entry.
|
|
119
|
+
Bounded on purpose — 200 commits on a first sync, 20 files per commit, no merges (their patch
|
|
120
|
+
exists only in a combined format this parser does not read), and binaries, lockfiles and build
|
|
121
|
+
output skipped. Patches are redacted with the shape-matching rules only; the key/value rule that
|
|
122
|
+
serves prose would rewrite `const apiKey = process.env.SERVICE_API_KEY` into a redaction marker.
|
|
123
|
+
- **Cross-project recall.** `query --all-projects` (and `search_memory`'s `allProjects`) searches
|
|
124
|
+
every repository NexusMem has been run in on this machine, tagging each result with the repository
|
|
125
|
+
it came from. Databases stay per-repository — a shared global store was rejected for giving up the
|
|
126
|
+
property that deleting one repo's `.nexusmem/` removes that repo's memory and nothing else — so a
|
|
127
|
+
plain index at `~/.nexusmem/projects.json`, written by `init` and refreshed by `sync`, is what
|
|
128
|
+
makes the others findable. New `projects` command lists it; `--prune` forgets entries whose
|
|
129
|
+
database is gone. A stale or corrupt registry degrades the query, never fails it.
|
|
130
|
+
Ranking fuses each project's list by rank (RRF) instead of comparing raw BM25 costs, which are
|
|
131
|
+
computed against their own corpus and are not comparable across databases. The bias this leaves —
|
|
132
|
+
every project's rank-1 hit is worth the same, so recall favours breadth — is documented rather
|
|
133
|
+
than hidden.
|
|
134
|
+
- **Query-aware diff excerpts.** A packed summary is ~320 characters and a patch is thousands, so
|
|
135
|
+
the packer now picks the hunk whose tokens match the query and starts the excerpt at the changed
|
|
136
|
+
line. Found by dogfooding: "what flags are passed to every git invocation" retrieved the right
|
|
137
|
+
file and then spent the whole summary on a class definition seventy lines above the answer.
|
|
138
|
+
Matching splits identifiers on case and underscore boundaries, because `\bretry\b` does not match
|
|
139
|
+
`RETRY_DELAYS_MS` and a natural-language question otherwise never meets the code it is about.
|
|
140
|
+
- `CHANGELOG.md` now ships inside the npm tarball. npm's always-included list covers `package.json`,
|
|
141
|
+
`README` and `LICENSE` but not the changelog, so it previously reached GitHub readers only.
|
|
142
|
+
|
|
143
|
+
### Internal
|
|
144
|
+
|
|
145
|
+
- The test suite no longer writes to the developer's real `~/.nexusmem`. `sync` records the
|
|
146
|
+
repository it ingested in the project registry, and the suite syncs temporary repositories in
|
|
147
|
+
several places, so a green run left seven dead entries behind — found by running `nexusmem
|
|
148
|
+
projects` after the fact, not by any test. `tests/setup.ts` now points `NEXUSMEM_HOME` at a
|
|
149
|
+
throwaway directory for the whole suite, and one test fails if that guard is ever removed.
|
|
150
|
+
- `npm run smoke` drives the *packaged* artifact: build, pack, install into a throwaway directory,
|
|
151
|
+
then run the installed CLI, an end-to-end ingest/query against a fixture repository, and an
|
|
152
|
+
`initialize` handshake over real stdio. It also audits the manifest `npm publish` would send,
|
|
153
|
+
which is a different artifact from the tarball. Both defects that ever reached npm users passed a
|
|
154
|
+
green unit suite first; each is now pinned by a check verified to fail when the defect is
|
|
155
|
+
reintroduced. Runs in CI on Linux and Windows as its own job.
|
|
156
|
+
|
|
157
|
+
## [0.1.2] — 2026-08-10
|
|
158
|
+
|
|
159
|
+
### Fixed
|
|
160
|
+
|
|
161
|
+
- `nexusmem --version` printed `0.1.0` on 0.1.1. The version string in `src/cli/index.ts` was a
|
|
162
|
+
literal separate from `package.json`, and the 0.1.1 bump only touched the latter. `src/mcp/server.ts`
|
|
163
|
+
had the same problem in its `McpServer` constructor, so an MCP client's `initialize` handshake
|
|
164
|
+
would have reported the same stale version. Both now read the real version through
|
|
165
|
+
`readOwnVersion()` in `src/core/version.ts`, which resolves `package.json` via `import.meta.url`.
|
|
166
|
+
Found by running the published package end to end rather than trusting `npm publish --dry-run`
|
|
167
|
+
and the registry API, neither of which executes a `--version` flag.
|
|
168
|
+
|
|
169
|
+
The ingestion and retrieval pipeline was never affected — only the two places that report a version
|
|
170
|
+
independently of running a command.
|
|
171
|
+
|
|
172
|
+
## [0.1.1] — 2026-08-10
|
|
173
|
+
|
|
174
|
+
### Changed
|
|
175
|
+
|
|
176
|
+
- README rewritten for someone deciding whether to read the source: what it does, how retrieval
|
|
177
|
+
scores, what it costs, and where it breaks. `README.md` ships inside the package, so this is a
|
|
178
|
+
real change to what npm delivers — but no code changed between 0.1.0 and 0.1.1.
|
|
179
|
+
- Documented the ranking flaw the tool found in itself, and the fact that the conversation source
|
|
180
|
+
in the sample `status` output is opt-in rather than default.
|
|
181
|
+
- Dropped `&&` from the quickstart, which Windows PowerShell 5.1 cannot parse.
|
|
182
|
+
|
|
183
|
+
## [0.1.0] — 2026-08-10
|
|
184
|
+
|
|
185
|
+
First public release.
|
|
186
|
+
|
|
187
|
+
### Added
|
|
188
|
+
|
|
189
|
+
- **Collectors.** Git history (commit metadata and diff stats, not diff bodies), shell commands with
|
|
190
|
+
exit codes via an opt-in PowerShell hook, tracked markdown docs via `git ls-files -- '*.md'`, and
|
|
191
|
+
opt-in assistant transcripts.
|
|
192
|
+
- **Hybrid retrieval.** SQLite FTS5 BM25 and `sqlite-vec` KNN over 768-dim embeddings, fused with
|
|
193
|
+
reciprocal rank fusion, then ranked by relevance against signal and recency priors and packed into
|
|
194
|
+
an explicit token budget.
|
|
195
|
+
- **MCP server** over stdio (`nexusmem mcp`) exposing `search_memory`, `sync_project` and
|
|
196
|
+
`get_status`, for Claude Desktop, Cursor, Windsurf and other MCP clients.
|
|
197
|
+
- **CLI**: `init`, `sync`, `query`, `status`, `mcp`, and `hook install|remove|status`, plus four
|
|
198
|
+
dry-run previews — `scan-git`, `scan-shell`, `scan-docs`, `scan-conversation` — that write nothing
|
|
199
|
+
and print the nodes ingestion would create with their signal scores.
|
|
200
|
+
- Content-addressed node ids (`sha256(projectId + kind + naturalKey)`), so `sync` is idempotent and
|
|
201
|
+
two clones of one repository share a memory namespace.
|
|
202
|
+
- Everything stays on the machine: one SQLite database in WAL mode under `<repo>/.nexusmem/`.
|
|
203
|
+
|
|
204
|
+
### Notes
|
|
205
|
+
|
|
206
|
+
- Requires Node **>= 22**. `better-sqlite3` 12.11.1 publishes no prebuilt binary for Node 20 — its
|
|
207
|
+
prebuilds start at ABI 127 — so a lower floor would have been a promise the package could not keep.
|
|
208
|
+
Do not lower it without checking upstream prebuilds first.
|
|
209
|
+
- Not done at this release: diff bodies are not indexed, queries are scoped to a single project,
|
|
210
|
+
there is no local-model summarization pass, and the conversation collector has never been audited
|
|
211
|
+
for the stale-node bug that was found and fixed in the docs collector.
|
|
212
|
+
|
|
213
|
+
[Unreleased]: https://github.com/yaminbkk/NexusMem/compare/v0.3.0...HEAD
|
|
214
|
+
[0.3.0]: https://github.com/yaminbkk/NexusMem/compare/v0.2.0...v0.3.0
|
|
215
|
+
[0.2.0]: https://github.com/yaminbkk/NexusMem/compare/v0.1.2...v0.2.0
|
|
216
|
+
[0.1.2]: https://github.com/yaminbkk/NexusMem/compare/v0.1.1...v0.1.2
|
|
217
|
+
[0.1.1]: https://github.com/yaminbkk/NexusMem/compare/v0.1.0...v0.1.1
|
|
218
|
+
[0.1.0]: https://github.com/yaminbkk/NexusMem/releases/tag/v0.1.0
|
package/README.md
CHANGED
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
# NexusMem
|
|
2
2
|
|
|
3
|
+
[](https://github.com/yaminbkk/NexusMem/actions/workflows/ci.yml)
|
|
3
4
|
[](https://www.npmjs.com/package/nexusmem)
|
|
5
|
+
[](https://www.npmjs.com/package/nexusmem)
|
|
4
6
|
[](LICENSE)
|
|
5
7
|

|
|
6
8
|
|
|
9
|
+

|
|
10
|
+
|
|
7
11
|
Your coding agent can read `git log`. It cannot read the four things you tried last Tuesday that
|
|
8
12
|
didn't work.
|
|
9
13
|
|
|
@@ -42,7 +46,9 @@ Relevant history for: windows spawn failure
|
|
|
42
46
|
```
|
|
43
47
|
|
|
44
48
|
A commit and a docs section, ranked against each other, inside whatever token budget you gave it.
|
|
45
|
-
Nothing was summarized by a model on the way out; the ranker just decided what not to send.
|
|
49
|
+
Nothing was summarized by a model on the way out; the ranker just decided what not to send. (One
|
|
50
|
+
optional source, session summaries, does run a local model — but at ingest time, never on the way
|
|
51
|
+
out. What you query is always stored text.)
|
|
46
52
|
|
|
47
53
|
For a sense of what actually accumulates, here is `nexusmem status` on this repo after two days:
|
|
48
54
|
|
|
@@ -66,6 +72,21 @@ Requirements: Node 22 or newer, and git. Node 20 will not work, because `better-
|
|
|
66
72
|
prebuilt binary for it and Node 20 went end-of-life in April 2026. Ollama is optional and only
|
|
67
73
|
affects semantic search (see below).
|
|
68
74
|
|
|
75
|
+
## Optional: exact shell capture
|
|
76
|
+
|
|
77
|
+
Scraped history files (PSReadLine, `.bash_history`, `.zsh_history`) give you command text and not
|
|
78
|
+
much else. The hook gives you working directory, exit code and a real timestamp:
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
nexusmem hook install
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
It wraps your existing PowerShell prompt rather than replacing it, is idempotent, and
|
|
85
|
+
`nexusmem hook remove` undoes it cleanly.
|
|
86
|
+
|
|
87
|
+
Exit codes are what make this worth installing. A failed command is a stronger signal than a
|
|
88
|
+
successful one, and without the hook there is no way to tell them apart.
|
|
89
|
+
|
|
69
90
|
## How retrieval works
|
|
70
91
|
|
|
71
92
|
Every source normalizes to the same `MemoryNode` shape, so a commit, a shell command and a docs
|
|
@@ -79,7 +100,7 @@ they agree on. No hand-tuned normalization constant sits between them.
|
|
|
79
100
|
Ranking then multiplies three factors:
|
|
80
101
|
|
|
81
102
|
```
|
|
82
|
-
score = relevance × signal^0.
|
|
103
|
+
score = relevance × signal^0.215 × recency^0.288
|
|
83
104
|
```
|
|
84
105
|
|
|
85
106
|
`relevance` comes from the query. `signal` (a `fix:` commit outranks a `chore:`; a command that
|
|
@@ -89,13 +110,50 @@ zero out a strong match.
|
|
|
89
110
|
|
|
90
111
|
Those exponents are derived, not tuned. Priors kept overturning the query: on one real query a `fix:`
|
|
91
112
|
commit took rank 1 from a better-matching docs section on a 44% signal edge against a 15% relevance
|
|
92
|
-
deficit. So
|
|
93
|
-
gap
|
|
94
|
-
|
|
113
|
+
deficit. So the priors get a **shared** budget — across their whole range they may overturn at most a
|
|
114
|
+
2× relevance gap — split evenly between them, and each is raised to the power that makes its own span
|
|
115
|
+
worth exactly its share (`span^exponent = √2`). Priors still order equally-relevant hits exactly as
|
|
116
|
+
before, since the transform is monotonic. They just cannot outvote the question anymore.
|
|
117
|
+
|
|
118
|
+
The budget is shared rather than per-prior for a reason found by dogfooding, not by reading the
|
|
119
|
+
arithmetic: the score *multiplies* the priors, so capping each at 2× separately left the pair free to
|
|
120
|
+
overturn 4×. That describes every commit made during an active working day — fresh and high-signal at
|
|
121
|
+
once — so the failure landed on precisely the days with the most worth remembering. A query about the
|
|
122
|
+
PowerShell hook returned two unrelated same-day `fix:` commits at ranks 3 and 4 while the section that
|
|
123
|
+
answered it sat at rank 6. Adding a third prior now re-divides the same budget instead of enlarging it.
|
|
95
124
|
|
|
96
125
|
Without Ollama, vector search is skipped and you get BM25 only. That path is fully supported, not a
|
|
97
126
|
degraded error state; `sync` and `query` both succeed and simply do less.
|
|
98
127
|
|
|
128
|
+
## Session summaries (optional, local model)
|
|
129
|
+
|
|
130
|
+
With `sources.session.enabled`, each finished session becomes one distilled node next to the raw
|
|
131
|
+
exchanges — what was decided and why, rather than forty individual turns. It runs a local Ollama
|
|
132
|
+
chat model (`qwen2.5:3b` by default); nothing is downloaded automatically and nothing leaves the
|
|
133
|
+
machine.
|
|
134
|
+
|
|
135
|
+
```bash
|
|
136
|
+
nexusmem scan-session --dry-run
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
That prints the exact prompt a session would produce, after redaction and budget trimming, without
|
|
140
|
+
calling the model.
|
|
141
|
+
|
|
142
|
+
Three things bound the cost. A session is only summarized once it has been quiet for
|
|
143
|
+
`settleMinutes` (default 30), so a session in progress is not re-summarized on every sync. The
|
|
144
|
+
prompt is hashed, and an unchanged hash skips the model entirely — on this repo a steady-state sync
|
|
145
|
+
of 14 summarized sessions takes 0.25s and makes no model calls. And `maxSessions` (default 10) caps
|
|
146
|
+
how many reach the model per run; the rest are reported as queued and picked up next sync.
|
|
147
|
+
|
|
148
|
+
**What it is actually like, measured on 14 real sessions with `qwen2.5:3b`.** The summaries
|
|
149
|
+
themselves are good: decisions with their reasons, in the shape the prompt asks for. Titles are less
|
|
150
|
+
reliable — the model returned a usable one about a third of the time, and otherwise produced a
|
|
151
|
+
conversational preamble, a stray bullet, or a bare "Summary of the Session". Those are rejected and
|
|
152
|
+
the title falls back to the first line of the question that opened the session, which is always
|
|
153
|
+
specific even when it is not elegant. Compliance was worst on long sessions and on transcripts not
|
|
154
|
+
in English. A larger model (`qwen2.5:7b`) is the lever if the titles matter to you; set
|
|
155
|
+
`sources.session.model`.
|
|
156
|
+
|
|
99
157
|
## Use it from an agent
|
|
100
158
|
|
|
101
159
|
```json
|
|
@@ -114,21 +172,6 @@ Three tools over stdio: `search_memory` returns the packed context block, `sync_
|
|
|
114
172
|
MCP tool call carries no shell working directory. `sync_project` runs `init` for you if the
|
|
115
173
|
repository has not been set up.
|
|
116
174
|
|
|
117
|
-
## Optional: exact shell capture
|
|
118
|
-
|
|
119
|
-
Scraped history files (PSReadLine, `.bash_history`, `.zsh_history`) give you command text and not
|
|
120
|
-
much else. The hook gives you working directory, exit code and a real timestamp:
|
|
121
|
-
|
|
122
|
-
```bash
|
|
123
|
-
nexusmem hook install
|
|
124
|
-
```
|
|
125
|
-
|
|
126
|
-
It wraps your existing PowerShell prompt rather than replacing it, is idempotent, and
|
|
127
|
-
`nexusmem hook remove` undoes it cleanly.
|
|
128
|
-
|
|
129
|
-
Exit codes are what make this worth installing. A failed command is a stronger signal than a
|
|
130
|
-
successful one, and without the hook there is no way to tell them apart.
|
|
131
|
-
|
|
132
175
|
## What it costs you
|
|
133
176
|
|
|
134
177
|
Two numbers get conflated in tools like this, so they are kept apart here.
|
|
@@ -138,14 +181,40 @@ corpus it runs 81–84%. It is useful for tuning the ranker and useless as a cla
|
|
|
138
181
|
because the baseline is hypothetical: without NexusMem those candidates were never going into your
|
|
139
182
|
context window in the first place.
|
|
140
183
|
|
|
141
|
-
**End-to-end saving** compares packed context
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
184
|
+
**End-to-end saving** compares the packed context NexusMem actually sends against reading, in full,
|
|
185
|
+
the same files its own ranking identified as relevant to the query. Measured with
|
|
186
|
+
[`scripts/benchmark.ts`](scripts/benchmark.ts) (`npm run bench`), which anyone who clones this repo
|
|
187
|
+
and points it at a synced corpus can re-run from scratch:
|
|
188
|
+
|
|
189
|
+
| Corpus | Commits | Query set | vs. full file content | vs. `git log -p` on those files |
|
|
190
|
+
| --- | --- | --- | --- | --- |
|
|
191
|
+
| This repo | 62 | 16 real prompts, verbatim from this project's own history | 95% (median 94%) | 98% (median 97%) |
|
|
192
|
+
| [`vitejs/vite`](https://github.com/vitejs/vite) | 9,567 | 16, mechanically sampled — see below | 99% (median 98%) | ~100% (median ~100%) — see caveat |
|
|
193
|
+
|
|
194
|
+
Both clear the original >70% target ("cut API token spend versus sending full context"), and the vite
|
|
195
|
+
run is the first measurement at the scale that target was always described as applying to.
|
|
196
|
+
|
|
197
|
+
**Read the methodology before quoting either number, because it is a narrower claim than it looks:**
|
|
198
|
+
|
|
199
|
+
- The file set each query is graded against comes from NexusMem's *own* ranking — whichever files the
|
|
200
|
+
packed nodes for that query touch, not an outside judge's idea of the right answer. This isolates
|
|
201
|
+
what the pack step (rank → budget → excerpt) saves once retrieval has already picked a candidate
|
|
202
|
+
set; it does not independently verify that the candidate set was the right one to pick.
|
|
203
|
+
- The vite query set is not hand-picked: an even sample, across the full commit history, of
|
|
204
|
+
well-explained `fix`/`feat`/`perf`/`refactor` commits turned into "why does vite `<description>`"
|
|
205
|
+
from the commit's own conventional-commit text, plus rationale-bearing doc section headings. This
|
|
206
|
+
repo's own query set instead reuses real historical prompts verbatim from `conversation_turn`
|
|
207
|
+
nodes — several are broad task instructions rather than narrow questions, which pulls a wider file
|
|
208
|
+
set into scope and is part of why its number, while still high, sits below vite's. Both derivations
|
|
209
|
+
are mechanical and disclosed in `scripts/benchmark.ts`, neither is cherry-picked per-query.
|
|
210
|
+
- `git log -p` on a file touched by thousands of commits is enormous — one vite query's baseline hit
|
|
211
|
+
7.5M tokens because a file in its resolved set has that much history. That is itself a finding, not
|
|
212
|
+
noise: at this scale, "just read the file's history instead" stops being a viable alternative at
|
|
213
|
+
all, which is a big part of why that column rounds to ~100%.
|
|
214
|
+
- **This supersedes the previous ~40% figure**, which was hand-tallied from two hand-picked queries
|
|
215
|
+
against this repo alone, never instrumented, and used an unstated baseline. It was not wrong so much
|
|
216
|
+
as underspecified — this number replaces it with a stated method and a script that reproduces it,
|
|
217
|
+
rather than being a claim that the product got better.
|
|
149
218
|
|
|
150
219
|
One thing that is not a percentage: shell commands and conversation turns have no cheap `grep`
|
|
151
220
|
equivalent. Without something recording them, they are gone, not merely more expensive to find.
|
|
@@ -177,7 +246,13 @@ optimizing, and it is somebody else's process.
|
|
|
177
246
|
at the prompt is not reconstructed.
|
|
178
247
|
- **Scrape-fallback ids drift** if the history file is trimmed from the front between syncs.
|
|
179
248
|
Installing the hook fixes this.
|
|
180
|
-
- **
|
|
249
|
+
- **Session-summary titles depend on the model following instructions**, and a 3B model often does
|
|
250
|
+
not. The fallback keeps them specific rather than generic, but see the section above for what to
|
|
251
|
+
expect.
|
|
252
|
+
- **Changing the embedding model re-embeds everything.** Vectors from two models are not comparable
|
|
253
|
+
and `nodes_vec` records no per-row provenance, so `sync` drops the lot and rebuilds rather than
|
|
254
|
+
ranking across a mixture. It says so when it happens. Nodes are untouched and BM25 keeps working
|
|
255
|
+
throughout.
|
|
181
256
|
- **Diff indexing is bounded, and deliberately lossy.** A first sync indexes the patches of the most
|
|
182
257
|
recent 200 commits (later syncs only walk `cursor..HEAD`); merge commits contribute none, since
|
|
183
258
|
their patch exists only in a combined format this parser does not read; and binaries, lockfiles and
|
|
@@ -194,12 +269,18 @@ optimizing, and it is somebody else's process.
|
|
|
194
269
|
unmounted drive is not a deleted project.
|
|
195
270
|
- **Conversation chunking is unevaluated.** Splitting long replies at heading boundaries measurably
|
|
196
271
|
helped, but it has never been tested systematically.
|
|
197
|
-
- **A
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
272
|
+
- **A chunked node's sibling count in one result is capped, not tuned.** `conversation_turn` and
|
|
273
|
+
`doc_section` both split one reply or file into several nodes; at most 2 of them may appear
|
|
274
|
+
together in a packed result. Found live: a query for "token" returned 9 of its top 12 hits as
|
|
275
|
+
different pieces of one heavily-sectioned reply, crowding out the node that actually answered it.
|
|
276
|
+
The cap of 2 is a judgement call, not a measured optimum, same as the ranking priors' budget above.
|
|
277
|
+
- **The size of the prior budget is a judgement call, not a measured optimum.** Priors are now
|
|
278
|
+
bounded jointly rather than one at a time, which closed a real 4× hole (see the ranking section),
|
|
279
|
+
but the 2× budget itself has never been tuned against a labelled relevance set — there isn't one.
|
|
280
|
+
It is a defensible constant, not a result. What is measured is the direction: on four real queries
|
|
281
|
+
against this repo's own memory, switching to the joint cap moved the section that answered the
|
|
282
|
+
question up in three of them (the rationale section for "why BM25 before vector search" went from
|
|
283
|
+
rank 4 to rank 1) and displaced no query's correct top hit.
|
|
203
284
|
|
|
204
285
|
## Commands
|
|
205
286
|
|
|
@@ -267,12 +348,9 @@ Deleting `.nexusmem/` loses nothing that `sync` cannot rebuild.
|
|
|
267
348
|
|
|
268
349
|
## Status
|
|
269
350
|
|
|
270
|
-
Ingestion, hybrid retrieval, budgeted packing and the MCP server all work and are covered by
|
|
351
|
+
Ingestion, hybrid retrieval, budgeted packing and the MCP server all work and are covered by 342
|
|
271
352
|
tests running on Linux and Windows across Node 22 and 24.
|
|
272
353
|
|
|
273
|
-
Not done yet: there is no local-model summarization pass, and the embedding pass is still capped per
|
|
274
|
-
`sync`.
|
|
275
|
-
|
|
276
354
|
## Development
|
|
277
355
|
|
|
278
356
|
```bash
|