nexusmem 0.1.2 → 0.3.0

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/README.md CHANGED
@@ -1,254 +1,347 @@
1
- # NexusMem
2
-
3
- [![npm](https://img.shields.io/npm/v/nexusmem)](https://www.npmjs.com/package/nexusmem)
4
- [![License: MIT](https://img.shields.io/badge/license-MIT-informational)](LICENSE)
5
- ![Node](https://img.shields.io/badge/node-%3E%3D22-brightgreen)
6
-
7
- Your coding agent can read `git log`. It cannot read the four things you tried last Tuesday that
8
- didn't work.
9
-
10
- NexusMem records what actually happened on your machine (shell commands and their exit codes, git
11
- history, project docs, optionally your assistant transcripts) into a local SQLite database, and
12
- serves back a ranked, token-budgeted slice of it on demand. Everything stays on disk. No account, no
13
- cloud, no telemetry.
14
-
15
- The shell history is the part worth caring about. Git tells an agent what shipped. Shell history
16
- tells it what was attempted, in what order, and which commands exited non-zero. That information
17
- exists nowhere else, and it disappears when your terminal scrollback rolls over.
18
-
19
- ## Try it
20
-
21
- From inside any git repository:
22
-
23
- ```
24
- npx nexusmem init
25
- npx nexusmem sync
26
- ```
27
-
28
- Then ask it something. Real output from this repository, top 2 of 5 hits:
29
-
30
- ```
31
- $ nexusmem query "windows spawn failure"
32
-
33
- Relevant history for: windows spawn failure
34
-
35
- - 2026-08-09 fix: distinguish a failed git spawn from "not a git repository"
36
- readRepoInfo collapsed three unrelated failures into one error: git running and reporting
37
- the path is not a work tree, git not being installed, and the process failing to spawn at
38
- all. Dogfooding hit the third case in two separate sessions...
39
- - 2026-08-09 README.md Before a tagged release
40
- - [ ] Retry on transient process-spawn failures on Windows
41
- ```
42
-
43
- A commit and a docs section, ranked against each other, inside whatever token budget you gave it.
44
- Nothing was summarized by a model on the way out; the ranker just decided what not to send.
45
-
46
- For a sense of what actually accumulates, here is `nexusmem status` on this repo after two days:
47
-
48
- ```
49
- 527 node(s) 2026-08-08 .. 2026-08-09
50
- 321 shell_command
51
- 130 conversation_turn
52
- 60 doc_section
53
- 16 git_commit
54
- ```
55
-
56
- Sixteen commits. Three hundred and twenty-one shell commands. The commits were already retrievable
57
- by any agent with a terminal. The rest was not.
58
-
59
- That `conversation_turn` row only appears because this corpus was synced with `--conversation`.
60
- Assistant transcripts are the one source that is off by default and stays off until you opt in, since
61
- they are the likeliest place for a pasted credential to be sitting. A default install indexes git,
62
- shell and docs.
63
-
64
- Requirements: Node 22 or newer, and git. Node 20 will not work, because `better-sqlite3` ships no
65
- prebuilt binary for it and Node 20 went end-of-life in April 2026. Ollama is optional and only
66
- affects semantic search (see below).
67
-
68
- ## How retrieval works
69
-
70
- Every source normalizes to the same `MemoryNode` shape, so a commit, a shell command and a docs
71
- section compete on equal terms. Retrieval runs BM25 over FTS5 and, if an embedding model is
72
- reachable, a vector search over `sqlite-vec`, then fuses the two with Reciprocal Rank Fusion.
73
-
74
- RRF fuses on rank *position* only, never on raw scores. That is the entire reason it is safe here: a
75
- BM25 cost and a vector distance live on unrelated, unbounded scales, and position is the only thing
76
- they agree on. No hand-tuned normalization constant sits between them.
77
-
78
- Ranking then multiplies three factors:
79
-
80
- ```
81
- score = relevance × signal^0.431 × recency^0.576
82
- ```
83
-
84
- `relevance` comes from the query. `signal` (a `fix:` commit outranks a `chore:`; a command that
85
- exited non-zero outranks one that succeeded) and `recency` are priors that hold before any query
86
- exists. Each factor is floored into `[floor, 1]` rather than `[0, 1]`, so one weak dimension cannot
87
- zero out a strong match.
88
-
89
- Those exponents are derived, not tuned. Priors kept overturning the query: on one real query a `fix:`
90
- commit took rank 1 from a better-matching docs section on a 44% signal edge against a 15% relevance
91
- deficit. So each prior is raised to the power that caps its entire range at overturning a 2× relevance
92
- gap, by solving `span^exponent = 2`. Priors still order equally-relevant hits exactly as before, since
93
- the transform is monotonic. They just cannot outvote the question anymore.
94
-
95
- Without Ollama, vector search is skipped and you get BM25 only. That path is fully supported, not a
96
- degraded error state; `sync` and `query` both succeed and simply do less.
97
-
98
- ## Use it from an agent
99
-
100
- ```json
101
- {
102
- "mcpServers": {
103
- "nexusmem": {
104
- "command": "npx",
105
- "args": ["-y", "nexusmem", "mcp"]
106
- }
107
- }
108
- }
109
- ```
110
-
111
- Three tools over stdio: `search_memory` returns the packed context block, `sync_project` ingests, and
112
- `get_status` reports what is currently remembered. Each takes an explicit `projectRoot`, because an
113
- MCP tool call carries no shell working directory. `sync_project` runs `init` for you if the
114
- repository has not been set up.
115
-
116
- ## Optional: exact shell capture
117
-
118
- Scraped history files (PSReadLine, `.bash_history`, `.zsh_history`) give you command text and not
119
- much else. The hook gives you working directory, exit code and a real timestamp:
120
-
121
- ```bash
122
- nexusmem hook install
123
- ```
124
-
125
- It wraps your existing PowerShell prompt rather than replacing it, is idempotent, and
126
- `nexusmem hook remove` undoes it cleanly.
127
-
128
- Exit codes are what make this worth installing. A failed command is a stronger signal than a
129
- successful one, and without the hook there is no way to tell them apart.
130
-
131
- ## What it costs you
132
-
133
- Two numbers get conflated in tools like this, so they are kept apart here.
134
-
135
- **Packer efficiency** is how much the ranker trims from its own candidate set. On this repository's
136
- corpus it runs 81–84%. It is useful for tuning the ranker and useless as a claim about your bill,
137
- because the baseline is hypothetical: without NexusMem those candidates were never going into your
138
- context window in the first place.
139
-
140
- **End-to-end saving** compares packed context against reading the equivalent files in full. Measured
141
- at **~40%** on design queries against this codebase, hand-tallied from one real session rather than
142
- instrumented. Treat it as an order of magnitude.
143
-
144
- The long-term target is >70%, and this repository cannot demonstrate it. That figure describes repos
145
- with thousands of commits, where the win comes from omitting hundreds of unrelated items rather than
146
- shaving a handful. A benchmark at that size is still outstanding, and until it exists the honest
147
- number is 40%.
148
-
149
- One thing that is not a percentage: shell commands and conversation turns have no cheap `grep`
150
- equivalent. Without something recording them, they are gone, not merely more expensive to find.
151
-
152
- Latency on a ~530-node corpus, warm, p50 over 10 runs:
153
-
154
- | Operation | |
155
- | --- | --- |
156
- | BM25 retrieval (FTS5) | ~1.1 ms |
157
- | Vector KNN (`sqlite-vec`) | ~3.2 ms |
158
- | Fuse, rank, pack | ~0.6 ms |
159
- | Query embedding (local Ollama) | ~55–77 ms |
160
- | **End-to-end hybrid** | **~56 ms** |
161
-
162
- All the SQLite work totals about 5 ms. The embedding call is the only thing on this path worth
163
- optimizing, and it is somebody else's process.
164
-
165
- ## Where it breaks
166
-
167
- - **Shell history without the hook is unscoped.** Scraped history has no directory context, so it is
168
- attributed to whichever repository you ran `sync` from. Bounded to a tail window, and an
169
- approximation rather than a guarantee.
170
- - **Thai, Japanese and Chinese depend on the vector pass.** FTS5's `unicode61` tokenizer splits on
171
- whitespace, so languages without space boundaries get no useful BM25 recall.
172
- - **Rebasing strands nodes.** Rewritten history leaves nodes for unreachable commits. They describe
173
- real events so they are not wrong, but a targeted prune does not exist yet. `sync --rebuild`
174
- forces a clean re-scan.
175
- - **Multi-line PowerShell input is read as separate commands.** A function typed across several lines
176
- at the prompt is not reconstructed.
177
- - **Scrape-fallback ids drift** if the history file is trimmed from the front between syncs.
178
- Installing the hook fixes this.
179
- - **The embedding pass is capped per `sync`**, so a large corpus needs a few runs to embed fully.
180
- - **Conversation chunking is unevaluated.** Splitting long replies at heading boundaries measurably
181
- helped, but it has never been tested systematically.
182
- - **A burst of recent, high-signal commits crowds unrelated queries.** Each prior is individually
183
- capped at overturning a relevance gap, but the caps are per-prior, not joint, so a node that is
184
- both very fresh and highly scored can overturn roughly 4×. Found by dogfooding: a query about the
185
- PowerShell hook returned two same-day `fix:` commits with nothing to do with it at ranks 3 and 4,
186
- while the section that actually answered the question sat at rank 6. Gets worse on days with a lot
187
- of commits, which are exactly the days you have most to remember.
188
-
189
- ## Commands
190
-
191
- `init`, `sync`, `query <text>`, `status`, `mcp`, and `hook install|remove|status`.
192
-
193
- There are also four dry-run previews (`scan-git`, `scan-shell`, `scan-docs`, `scan-conversation`)
194
- that write nothing and print the nodes ingestion *would* create along with their signal scores. That
195
- is the intended way to tune scoring against a real repository before committing to a change. Add
196
- `--json` to pipe them somewhere.
197
-
198
- Every command takes `-C <path>` to target another repository. On `sync`, `--conversation` opts the
199
- transcript source in for one run without persisting it, `--no-embed` skips the vector pass, and
200
- `--rebuild` drops the project's nodes and re-ingests from scratch.
201
-
202
- ## On disk
203
-
204
- ```
205
- <repo>/.nexusmem/
206
- .gitignore '*' — the workspace ignores itself, so init never edits a file it doesn't own
207
- config.json validated on read; a corrupt config fails loudly rather than silently
208
- memory.db SQLite in WAL mode
209
- ```
210
-
211
- Node ids are content-addressed from `sha256(projectId + kind + naturalKey)`, so running `sync` twice
212
- cannot produce duplicates and ingestion stays correct even if a cursor is lost. Project identity
213
- comes from the normalized origin URL when there is one, falling back to the absolute path, so two
214
- clones of the same repo share one memory namespace.
215
-
216
- Deleting `.nexusmem/` loses nothing that `sync` cannot rebuild.
217
-
218
- ## Status
219
-
220
- Ingestion, hybrid retrieval, budgeted packing and the MCP server all work and are covered by 210
221
- tests running on Linux and Windows across Node 22 and 24.
222
-
223
- Not done yet: diff bodies are not indexed (commits stop at metadata and diff stats), queries are
224
- scoped to a single project, there is no local-model summarization pass, and the conversation
225
- collector has never been audited for the stale-node bug that was found and fixed in the docs
226
- collector.
227
-
228
- ## Development
229
-
230
- ```bash
231
- npm install
232
- npm run typecheck
233
- npm test
234
- npm run build
235
- ```
236
-
237
- Tests are behavioral rather than snapshot-based, and several are regressions tied to specific
238
- observed failures. `tests/git-errors.test.ts` injects a fake `spawn` to exercise the Windows
239
- process-spawn faults, which cannot be provoked on demand.
240
-
241
- ## On how this was built
242
-
243
- This started as an experiment in whether a local context-memory engine for coding agents was viable,
244
- prototyped with Claude Code. The code was written through AI-assisted workflows; the architecture,
245
- the design decisions and the specifications were human-directed.
246
-
247
- That is worth stating plainly because it should change how you read the code, not whether you trust
248
- it. Audits, corrections and PRs are genuinely welcome, and the commit history is deliberately
249
- detailed about *why* things are the way they are, including the times an earlier assumption turned
250
- out to be wrong.
251
-
252
- ## License
253
-
254
- MIT
1
+ # NexusMem
2
+
3
+ [![npm](https://img.shields.io/npm/v/nexusmem)](https://www.npmjs.com/package/nexusmem)
4
+ [![License: MIT](https://img.shields.io/badge/license-MIT-informational)](LICENSE)
5
+ ![Node](https://img.shields.io/badge/node-%3E%3D22-brightgreen)
6
+
7
+ ![NexusMem: init, sync, status, and a query against this repo's own history](docs/demo.gif)
8
+
9
+ Your coding agent can read `git log`. It cannot read the four things you tried last Tuesday that
10
+ didn't work.
11
+
12
+ NexusMem records what actually happened on your machine (shell commands and their exit codes, git
13
+ history down to the patch of each changed file, project docs, optionally your assistant transcripts)
14
+ into a local SQLite database, and
15
+ serves back a ranked, token-budgeted slice of it on demand. Everything stays on disk. No account, no
16
+ cloud, no telemetry.
17
+
18
+ The shell history is the part worth caring about. Git tells an agent what shipped. Shell history
19
+ tells it what was attempted, in what order, and which commands exited non-zero. That information
20
+ exists nowhere else, and it disappears when your terminal scrollback rolls over.
21
+
22
+ ## Try it
23
+
24
+ From inside any git repository:
25
+
26
+ ```
27
+ npx nexusmem init
28
+ npx nexusmem sync
29
+ ```
30
+
31
+ Then ask it something. Real output from this repository, top 2 of 5 hits:
32
+
33
+ ```
34
+ $ nexusmem query "windows spawn failure"
35
+
36
+ Relevant history for: windows spawn failure
37
+
38
+ - 2026-08-09 fix: distinguish a failed git spawn from "not a git repository"
39
+ readRepoInfo collapsed three unrelated failures into one error: git running and reporting
40
+ the path is not a work tree, git not being installed, and the process failing to spawn at
41
+ all. Dogfooding hit the third case in two separate sessions...
42
+ - 2026-08-09 README.md — Before a tagged release
43
+ - [ ] Retry on transient process-spawn failures on Windows
44
+ ```
45
+
46
+ A commit and a docs section, ranked against each other, inside whatever token budget you gave it.
47
+ Nothing was summarized by a model on the way out; the ranker just decided what not to send. (One
48
+ optional source, session summaries, does run a local model — but at ingest time, never on the way
49
+ out. What you query is always stored text.)
50
+
51
+ For a sense of what actually accumulates, here is `nexusmem status` on this repo after two days:
52
+
53
+ ```
54
+ 527 node(s) 2026-08-08 .. 2026-08-09
55
+ 321 shell_command
56
+ 130 conversation_turn
57
+ 60 doc_section
58
+ 16 git_commit
59
+ ```
60
+
61
+ Sixteen commits. Three hundred and twenty-one shell commands. The commits were already retrievable
62
+ by any agent with a terminal. The rest was not.
63
+
64
+ That `conversation_turn` row only appears because this corpus was synced with `--conversation`.
65
+ Assistant transcripts are the one source that is off by default and stays off until you opt in, since
66
+ they are the likeliest place for a pasted credential to be sitting. A default install indexes git
67
+ commits, their diffs, shell and docs.
68
+
69
+ Requirements: Node 22 or newer, and git. Node 20 will not work, because `better-sqlite3` ships no
70
+ prebuilt binary for it and Node 20 went end-of-life in April 2026. Ollama is optional and only
71
+ affects semantic search (see below).
72
+
73
+ ## How retrieval works
74
+
75
+ Every source normalizes to the same `MemoryNode` shape, so a commit, a shell command and a docs
76
+ section compete on equal terms. Retrieval runs BM25 over FTS5 and, if an embedding model is
77
+ reachable, a vector search over `sqlite-vec`, then fuses the two with Reciprocal Rank Fusion.
78
+
79
+ RRF fuses on rank *position* only, never on raw scores. That is the entire reason it is safe here: a
80
+ BM25 cost and a vector distance live on unrelated, unbounded scales, and position is the only thing
81
+ they agree on. No hand-tuned normalization constant sits between them.
82
+
83
+ Ranking then multiplies three factors:
84
+
85
+ ```
86
+ score = relevance × signal^0.215 × recency^0.288
87
+ ```
88
+
89
+ `relevance` comes from the query. `signal` (a `fix:` commit outranks a `chore:`; a command that
90
+ exited non-zero outranks one that succeeded) and `recency` are priors that hold before any query
91
+ exists. Each factor is floored into `[floor, 1]` rather than `[0, 1]`, so one weak dimension cannot
92
+ zero out a strong match.
93
+
94
+ Those exponents are derived, not tuned. Priors kept overturning the query: on one real query a `fix:`
95
+ commit took rank 1 from a better-matching docs section on a 44% signal edge against a 15% relevance
96
+ deficit. So the priors get a **shared** budget across their whole range they may overturn at most a
97
+ 2× relevance gap — split evenly between them, and each is raised to the power that makes its own span
98
+ worth exactly its share (`span^exponent = √2`). Priors still order equally-relevant hits exactly as
99
+ before, since the transform is monotonic. They just cannot outvote the question anymore.
100
+
101
+ The budget is shared rather than per-prior for a reason found by dogfooding, not by reading the
102
+ arithmetic: the score *multiplies* the priors, so capping each at 2× separately left the pair free to
103
+ overturn 4×. That describes every commit made during an active working day — fresh and high-signal at
104
+ once — so the failure landed on precisely the days with the most worth remembering. A query about the
105
+ PowerShell hook returned two unrelated same-day `fix:` commits at ranks 3 and 4 while the section that
106
+ answered it sat at rank 6. Adding a third prior now re-divides the same budget instead of enlarging it.
107
+
108
+ Without Ollama, vector search is skipped and you get BM25 only. That path is fully supported, not a
109
+ degraded error state; `sync` and `query` both succeed and simply do less.
110
+
111
+ ## Session summaries (optional, local model)
112
+
113
+ With `sources.session.enabled`, each finished session becomes one distilled node next to the raw
114
+ exchanges what was decided and why, rather than forty individual turns. It runs a local Ollama
115
+ chat model (`qwen2.5:3b` by default); nothing is downloaded automatically and nothing leaves the
116
+ machine.
117
+
118
+ ```bash
119
+ nexusmem scan-session --dry-run
120
+ ```
121
+
122
+ That prints the exact prompt a session would produce, after redaction and budget trimming, without
123
+ calling the model.
124
+
125
+ Three things bound the cost. A session is only summarized once it has been quiet for
126
+ `settleMinutes` (default 30), so a session in progress is not re-summarized on every sync. The
127
+ prompt is hashed, and an unchanged hash skips the model entirely — on this repo a steady-state sync
128
+ of 14 summarized sessions takes 0.25s and makes no model calls. And `maxSessions` (default 10) caps
129
+ how many reach the model per run; the rest are reported as queued and picked up next sync.
130
+
131
+ **What it is actually like, measured on 14 real sessions with `qwen2.5:3b`.** The summaries
132
+ themselves are good: decisions with their reasons, in the shape the prompt asks for. Titles are less
133
+ reliable the model returned a usable one about a third of the time, and otherwise produced a
134
+ conversational preamble, a stray bullet, or a bare "Summary of the Session". Those are rejected and
135
+ the title falls back to the first line of the question that opened the session, which is always
136
+ specific even when it is not elegant. Compliance was worst on long sessions and on transcripts not
137
+ in English. A larger model (`qwen2.5:7b`) is the lever if the titles matter to you; set
138
+ `sources.session.model`.
139
+
140
+ ## Use it from an agent
141
+
142
+ ```json
143
+ {
144
+ "mcpServers": {
145
+ "nexusmem": {
146
+ "command": "npx",
147
+ "args": ["-y", "nexusmem", "mcp"]
148
+ }
149
+ }
150
+ }
151
+ ```
152
+
153
+ Three tools over stdio: `search_memory` returns the packed context block, `sync_project` ingests, and
154
+ `get_status` reports what is currently remembered. Each takes an explicit `projectRoot`, because an
155
+ MCP tool call carries no shell working directory. `sync_project` runs `init` for you if the
156
+ repository has not been set up.
157
+
158
+ ## Optional: exact shell capture
159
+
160
+ Scraped history files (PSReadLine, `.bash_history`, `.zsh_history`) give you command text and not
161
+ much else. The hook gives you working directory, exit code and a real timestamp:
162
+
163
+ ```bash
164
+ nexusmem hook install
165
+ ```
166
+
167
+ It wraps your existing PowerShell prompt rather than replacing it, is idempotent, and
168
+ `nexusmem hook remove` undoes it cleanly.
169
+
170
+ Exit codes are what make this worth installing. A failed command is a stronger signal than a
171
+ successful one, and without the hook there is no way to tell them apart.
172
+
173
+ ## What it costs you
174
+
175
+ Two numbers get conflated in tools like this, so they are kept apart here.
176
+
177
+ **Packer efficiency** is how much the ranker trims from its own candidate set. On this repository's
178
+ corpus it runs 81–84%. It is useful for tuning the ranker and useless as a claim about your bill,
179
+ because the baseline is hypothetical: without NexusMem those candidates were never going into your
180
+ context window in the first place.
181
+
182
+ **End-to-end saving** compares packed context against reading the equivalent files in full. Measured
183
+ at **~40%** on design queries against this codebase, hand-tallied from one real session rather than
184
+ instrumented. Treat it as an order of magnitude.
185
+
186
+ The long-term target is >70%, and this repository cannot demonstrate it. That figure describes repos
187
+ with thousands of commits, where the win comes from omitting hundreds of unrelated items rather than
188
+ shaving a handful. A benchmark at that size is still outstanding, and until it exists the honest
189
+ number is 40%.
190
+
191
+ One thing that is not a percentage: shell commands and conversation turns have no cheap `grep`
192
+ equivalent. Without something recording them, they are gone, not merely more expensive to find.
193
+
194
+ Latency on a ~530-node corpus, warm, p50 over 10 runs:
195
+
196
+ | Operation | |
197
+ | --- | --- |
198
+ | BM25 retrieval (FTS5) | ~1.1 ms |
199
+ | Vector KNN (`sqlite-vec`) | ~3.2 ms |
200
+ | Fuse, rank, pack | ~0.6 ms |
201
+ | Query embedding (local Ollama) | ~55–77 ms |
202
+ | **End-to-end hybrid** | **~56 ms** |
203
+
204
+ All the SQLite work totals about 5 ms. The embedding call is the only thing on this path worth
205
+ optimizing, and it is somebody else's process.
206
+
207
+ ## Where it breaks
208
+
209
+ - **Shell history without the hook is unscoped.** Scraped history has no directory context, so it is
210
+ attributed to whichever repository you ran `sync` from. Bounded to a tail window, and an
211
+ approximation rather than a guarantee.
212
+ - **Japanese and Chinese depend on the vector pass.** FTS5's `unicode61` tokenizer splits on
213
+ whitespace, so languages without space boundaries get no useful BM25 recall.
214
+ - **Rebasing strands nodes.** Rewritten history leaves nodes for unreachable commits. They describe
215
+ real events so they are not wrong, but a targeted prune does not exist yet. `sync --rebuild`
216
+ forces a clean re-scan.
217
+ - **Multi-line PowerShell input is read as separate commands.** A function typed across several lines
218
+ at the prompt is not reconstructed.
219
+ - **Scrape-fallback ids drift** if the history file is trimmed from the front between syncs.
220
+ Installing the hook fixes this.
221
+ - **Session-summary titles depend on the model following instructions**, and a 3B model often does
222
+ not. The fallback keeps them specific rather than generic, but see the section above for what to
223
+ expect.
224
+ - **Changing the embedding model re-embeds everything.** Vectors from two models are not comparable
225
+ and `nodes_vec` records no per-row provenance, so `sync` drops the lot and rebuilds rather than
226
+ ranking across a mixture. It says so when it happens. Nodes are untouched and BM25 keeps working
227
+ throughout.
228
+ - **Diff indexing is bounded, and deliberately lossy.** A first sync indexes the patches of the most
229
+ recent 200 commits (later syncs only walk `cursor..HEAD`); merge commits contribute none, since
230
+ their patch exists only in a combined format this parser does not read; and binaries, lockfiles and
231
+ build output are skipped so a dependency bump cannot bury the corpus. All of it is still recorded
232
+ as a `git_commit` node. A patch longer than `limits.maxBodyChars` is truncated, so the tail of a
233
+ very large change is not indexed. The caps live under `sources.diff` in `config.json`.
234
+ - **Cross-project recall favours breadth.** Each repository's hits are fused by rank, so a project
235
+ whose best match is mediocre still contributes a rank-1 item, and rank 1 is worth the same in
236
+ every list. Adding a repository that has little to say about your question still pushes a few of
237
+ its results into the budget. Signal, recency and the budget are what hold that in check; there is
238
+ no per-project quality weight.
239
+ - **The project registry is an index, not a source of truth.** It can point at a database that has
240
+ moved or been deleted; those are reported and skipped, never silently pruned, because an
241
+ unmounted drive is not a deleted project.
242
+ - **Conversation chunking is unevaluated.** Splitting long replies at heading boundaries measurably
243
+ helped, but it has never been tested systematically.
244
+ - **The size of the prior budget is a judgement call, not a measured optimum.** Priors are now
245
+ bounded jointly rather than one at a time, which closed a real 4× hole (see the ranking section),
246
+ but the 2× budget itself has never been tuned against a labelled relevance set — there isn't one.
247
+ It is a defensible constant, not a result. What is measured is the direction: on four real queries
248
+ against this repo's own memory, switching to the joint cap moved the section that answered the
249
+ question up in three of them (the rationale section for "why BM25 before vector search" went from
250
+ rank 4 to rank 1) and displaced no query's correct top hit.
251
+
252
+ ## Commands
253
+
254
+ `init`, `sync`, `query <text>`, `status`, `projects`, `mcp`, and `hook install|remove|status`.
255
+
256
+ There are also five dry-run previews (`scan-git`, `scan-diff`, `scan-shell`, `scan-docs`,
257
+ `scan-conversation`)
258
+ that write nothing and print the nodes ingestion *would* create along with their signal scores. That
259
+ is the intended way to tune scoring against a real repository before committing to a change. Add
260
+ `--json` to pipe them somewhere.
261
+
262
+ Every command takes `-C <path>` to target another repository. On `sync`, `--conversation` opts the
263
+ transcript source in for one run without persisting it, `--no-embed` skips the vector pass, and
264
+ `--rebuild` drops the project's nodes and re-ingests from scratch.
265
+
266
+ ## Recall across projects
267
+
268
+ `query --all-projects` searches every repository you have run NexusMem in, not just the current one,
269
+ and tags each result with the repository it came from:
270
+
271
+ ```
272
+ $ nexusmem query --all-projects "why was the retry budget raised"
273
+ scope 2 project(s): NexusMem, uploader
274
+
275
+ - 2026-08-12 [uploader] fix: raise the retry budget after the S3 upload timeouts
276
+ - 2026-08-12 [uploader] retry.ts @ 8d0f98b — fix: raise the retry budget after the S3 upload timeouts
277
+ @@ -1 +1 @@
278
+ -export const RETRY_BUDGET = 3;
279
+ +export const RETRY_BUDGET = 5;
280
+ - 2026-08-09 [NexusMem] fix(git): retry a transient failure to spawn git
281
+ ```
282
+
283
+ Databases stay per-repository — there is no shared global store, and deleting one repo's
284
+ `.nexusmem/` still removes exactly that repo's memory. What makes the others findable is a plain
285
+ index at `~/.nexusmem/projects.json`, written by `init` and refreshed by every `sync`. `nexusmem
286
+ projects` shows what is in it, and `--prune` forgets entries whose database is gone.
287
+
288
+ Ranking across repositories uses reciprocal rank fusion per project rather than raw BM25, because a
289
+ BM25 cost is computed against its own corpus and means different things in a 50-node and a
290
+ 50,000-node database. The trade is stated in *Where it breaks*.
291
+
292
+ The MCP `search_memory` tool takes the same switch as `allProjects: true`.
293
+
294
+ ## On disk
295
+
296
+ ```
297
+ <repo>/.nexusmem/
298
+ .gitignore '*' — the workspace ignores itself, so init never edits a file it doesn't own
299
+ config.json validated on read; a corrupt config fails loudly rather than silently
300
+ memory.db SQLite in WAL mode
301
+
302
+ ~/.nexusmem/
303
+ projects.json which repositories exist, for cross-project recall; a corrupt one reads as empty
304
+ shell-history.jsonl the hook's log, if you installed it
305
+ ```
306
+
307
+ `NEXUSMEM_HOME` overrides the user-scoped directory.
308
+
309
+ Node ids are content-addressed from `sha256(projectId + kind + naturalKey)`, so running `sync` twice
310
+ cannot produce duplicates and ingestion stays correct even if a cursor is lost. Project identity
311
+ comes from the normalized origin URL when there is one, falling back to the absolute path, so two
312
+ clones of the same repo share one memory namespace.
313
+
314
+ Deleting `.nexusmem/` loses nothing that `sync` cannot rebuild.
315
+
316
+ ## Status
317
+
318
+ Ingestion, hybrid retrieval, budgeted packing and the MCP server all work and are covered by 315
319
+ tests running on Linux and Windows across Node 22 and 24. Phase 3 is complete.
320
+
321
+ ## Development
322
+
323
+ ```bash
324
+ npm install
325
+ npm run typecheck
326
+ npm test
327
+ npm run build
328
+ ```
329
+
330
+ Tests are behavioral rather than snapshot-based, and several are regressions tied to specific
331
+ observed failures. `tests/git-errors.test.ts` injects a fake `spawn` to exercise the Windows
332
+ process-spawn faults, which cannot be provoked on demand.
333
+
334
+ ## On how this was built
335
+
336
+ This started as an experiment in whether a local context-memory engine for coding agents was viable,
337
+ prototyped with Claude Code. The code was written through AI-assisted workflows; the architecture,
338
+ the design decisions and the specifications were human-directed.
339
+
340
+ That is worth stating plainly because it should change how you read the code, not whether you trust
341
+ it. Audits, corrections and PRs are genuinely welcome, and the commit history is deliberately
342
+ detailed about *why* things are the way they are, including the times an earlier assumption turned
343
+ out to be wrong.
344
+
345
+ ## License
346
+
347
+ MIT