neuralmind.uk / NeuralMind v4.6.0 — one keyword index for docs and code, and the documented features wired in Edit this page on GitHub ↗

NeuralMind v4.6.0 — one keyword index for docs and code, and the documented features wired in

Type: Minor release Themes: retrieval ranking, measured (sections 1–5) · what the docs describe, the product does (sections 6–10)

Retrieval ranking, measured

v4.5.0 gave every project a way to measure retrieval on its own questions. v4.6.0 pointed that measurement at NeuralMind’s own ranking: six candidate changes to how the four L3 search slots are spent, 30 questions on each of six repositories — pre-registered and committed for the five public repositories, plus a private 383-file repository — and a keep rule written down before the first run. One change passed it.

  1. One BM25 index for docs and code, on by default. Mean hit@5 across the six repositories went from 72.8% to 79.4% (MRR 0.589 → 0.654).
  2. The public 4-repo benchmark’s gold-file recall went from 93.75% to 95%, at 46–263× fewer tokens than pasting every source file.
  3. Five more ranking changes (six flags — the intent pool is a variant of the intent rules) were measured and not kept. They ship off by default, behind flags, with the numbers that sank them.

Evidence: reproducible on demand, not a CI gate. The raw output is committed in bench/retrieval/; pip install -e . tiktoken, then NEURALMIND_ORT_THREADS=1 python -m evals.retrieval.run --public-benchmark re-runs it. The question sets for the five public repositories are pre-registered and committed; the private 383-file repository uses its own local question set, which readers can’t re-run. On the five public repositories alone, mean hit@5 went from 75.3% to 80.7% (MRR 0.614 → 0.664).

Run neuralmind build once after upgrading: the build writes the new index, and a query never builds anything.

Wired in, or gone

The v4.3.4 public-copy pass checked every claim against the code and found five features that existed as modules but weren’t connected to the running product. The site, README and wiki relabelled them “not yet wired in” or “roadmap”. v4.6.0 settles each one: three are wired in, one is deleted, and one is rebuilt and stays on demand.

Feature Before (v4.5) Now (v4.6)
Read dedup (read_dedup.py) Not imported anywhere A repeat read of an unchanged file comes back as a short stub (Claude Code)
Co-access edges (graph_traversal.py) Not imported; called a store method that doesn’t exist Deleted — queries already reinforce what they retrieve together
Decision invalidation (InvalidationEngine) Exercised only by tests; decisions went stale only by hand Runs after every commit through the init-hook post-commit hook
Team governance Scope and weight threshold stored and audited, never enforced; remove-edge only logged; list-shared printed [] Enforced by memory publish; remove-edge removes and retracts; list-shared lists; imports and reviews audited
Cognition loop On demand; its own SQL deleted most learned memory Rebuilt on the store’s own decay; still on demand, now safe for cron

1. One BM25 index for docs and code

L3 fuses vector search with a BM25 keyword list by Reciprocal Rank Fusion. On the default turbovec backend, that keyword index held only document nodes. A question whose words appeared in a README or a wiki page gave the docs a second signal that code never got — so on “how does X work” questions a doc kept taking a slot the implementation should have had.

Against v4.5.0 — hit@5 / MRR, 30 questions per repository (bench/retrieval/vs-v4.5):

Repository v4.5.0 v4.6.0 (unified BM25)
requests 90% / 0.76 87% / 0.75
click 93% / 0.69 93% / 0.79
flask 73% / 0.63 83% / 0.71
rich 80% / 0.71 80% / 0.60
neuralmind (this repository, docs indexed) 40% / 0.27 60% / 0.47
a private 383-file repository 60% / 0.47 73% / 0.60
mean 72.8% / 0.589 79.4% / 0.654

The losses, published

2. query --explain says which intent L3 ranked with

L3 re-weights its hits by query intent — a docs question multiplies doc hits by 2.0 and code hits by 0.7 — and nothing showed which intent a query got. --explain now prints it, and how it was decided:

  Query intent     : docs (by classifier)

The Top search hits list now shows each hit’s label and file; it printed raw node ids before.

3. How the decision was made — python -m evals.retrieval.run

A multi-repo retrieval eval ships in the source tree, with its raw output committed in bench/retrieval/.

4. Measured and not kept

Each stays available behind its flag, off by default:

Flag What it does Why it wasn’t kept
NEURALMIND_L3_PER_FILE=2 at most N L3 hits per file +1.1 pts mean hit@5, 2 repos up, none down on hit@5, but mean MRR fell 0.654 → 0.629 (requests 0.75 → 0.69, flask 0.71 → 0.65); public recall 96.25% — the rule needs 3 repos
NEURALMIND_DOC_HANDOFF=1 a doc hit that names code pulls that code in helps only where docs name code (private +3, neuralmind +1 question vs v4.5.0); a wash on top of v4.6.0
NEURALMIND_HUB_DAMPEN=1 down-weights files returned far more often than chance cost click 3–4 questions
NEURALMIND_INTENT_RULES=1 “how does X… / where is X… / which X is…” → code intent moves MRR (0.654 → 0.672), never hit@5: it only re-orders the 4 hits L3 already chose
NEURALMIND_INTENT_POOL=1 (with NEURALMIND_INTENT_RULES=1) intent ranks all 10 candidates click −8 questions, public recall 83.75% (vs v4.5.0)
NEURALMIND_BM25_CODE=1 a separate code-only keyword list requests −3 (vs v4.5.0); rich −4, click −3 (on v4.6.0)

One finding is worth keeping even though its fix wasn’t. The intent classifier — still the default, since the intent rules weren’t kept — sends many “how does X…” and “where is X…” behaviour questions to docs intent, which multiplies doc hits ×2.0 and code ×0.7. Four such shapes are now regression tests that the intent rules must send to code (tests/test_l3_slots_v460.py): “How does the session pick which adapter sends a request?”, “Where is the redirect followed after a POST?”, “Which hook runs before a request is sent?” and “How does the cache avoid storing the same response twice?”. Sending questions like these to code intent instead only re-orders the four hits L3 already chose, which is why the intent rules moved MRR and never hit@5.

5. The public benchmark, regenerated

bench/public/results.json was regenerated on v4.6.0 (tiktoken o200k_base; reproduce with NEURALMIND_ORT_THREADS=1 python -m evals.public.run):

6. Read dedup — a repeat read becomes a stub

When your agent reads a file it already read in this session, and the content hasn’t changed, the Read hook replaces the repeat with this:

[neuralmind read-dedup] auth/session.py is unchanged since you read it at 14:03:22 in this session, so that earlier result is still current and this repeat read was shortened. If the earlier result is no longer in your context, read the file again: the next read returns it in full.

It uses Claude Code’s PostToolUse updatedToolOutput, which replaces a tool result before the model sees it. The replacement keeps the Read tool’s own output shape ({"type": "text", "file": {...}}, only the text swapped), and Claude Code ignores a replacement that doesn’t match that shape and delivers the original read. Read dedup is the only hook action that replaces a tool result; everything else in the hook block only adds context or writes state.

What counts as a repeat, and what keeps it safe:

Where it applies: projects that already have a .neuralmind/ directory (a globally installed hook doesn’t create one in every repository it sees), with the hooks from neuralmind install-hooks. Off with NEURALMIND_READ_DEDUP=0; also off under NEURALMIND_BYPASS=1 and NEURALMIND_NO_LEARN=1 (which promises the hooks write nothing). State lives in .neuralmind/read_cache.db.

Not benchmarked yet, so these notes quote no token figure. The savings depend on how often your sessions re-read large unchanged files.

The v3.13 description also mentioned “preloading related files”. That part was never built, and putting extra file contents into context is the opposite of what read dedup is for, so it’s dropped rather than deferred.

7. Co-access edges — removed

graph_traversal.py described edges reinforced “when files appear together in query results”. Every query already does that: reinforce_from_query strengthens the edges between the nodes a query returns together, in the namespaces recall reads. The module would have written a second, file-level copy into a traversal namespace that no recall path ever read. Its get_related_files called a SynapseStore method that doesn’t exist, and its decay multiplied weights by a negative factor. Wiring it in would have double-counted learning into a namespace nothing reads, so it’s deleted.

8. Decisions go stale when a commit changes their files

neuralmind init-hook . now installs a post-commit hook that runs neuralmind decisions scan before rebuilding the index. When a decision went stale, the commit says so:

$ git commit -m "Switch sessions to Redis"
[neuralmind] 1 decision(s) marked STALE by commit 3f9c2ab:
  - Sessions live in Postgres (5b1e0c7a-…) — commit 3f9c2ab changed auth/session.py since this decision was recorded
  Review: neuralmind decisions audit --stale   Still valid? neuralmind decisions restore <id>
[neuralmind] Rebuilding neural index...

The rule is the engine’s, deliberately conservative: any change to a file a decision names marks it STALE. There’s no diff analysis. Two exemptions keep a fresh decision from going stale on the commit that carries it:

The scan diffs the new commit against its first parent, so a merge commit counts everything it brought in. A rename counts both the old and the new path. A project in a subdirectory of its repository, or in a linked worktree, works too: init-hook installs into the repository’s hooks and names the project by its path from the repository root (one NeuralMind project per repository’s hooks). Dependent decisions cascade, and the reason is kept on each decision (Marked STALE: commit 3f9c2ab changed auth/session.py after this decision was recorded in its evidence).

The PreToolUse stale-decision guard now shows that reason, the decision’s full id, and how to re-anchor one that still holds:

[neuralmind stale-guard] 1 decision(s) governing auth/session.py are no longer ACTIVE. Their rationale may not hold — verify before relying on them:
- [STALE] Sessions live in Postgres (id 5b1e0c7a-…, confidence 0.90, updated 2026-10-03): … — commit 3f9c2ab changed auth/session.py since this decision was recorded
If a STALE decision still holds after you check the code, `neuralmind decisions restore <id>` re-anchors it to HEAD.

9. Team governance is enforced, not just recorded

Governance applies once an operator has set it up: neuralmind onboarding, or any neuralmind team governance command that saves settings, writes the tier2 config. Without that config, nothing below changes anything.

neuralmind memory publish honours the policy:

publishing_scope What memory publish writes
personal Nothing. It refuses, exits 1 and names the admin command that changes the scope
shared Only the team baseline (the shared namespace). Your personal memory stays on your machine
both (default) Personal + shared, as before

Synapse edges below weight_threshold (default 0.1) are left out of the bundle and counted in the output. Transitions use a different scale and aren’t weight-filtered. The bundle’s provenance records the policy it was published under. If the governance config exists but can’t be read (unreadable, malformed YAML, or an invalid scope, for example), publish refuses rather than publishing ungoverned memory. team governance set-governance-enabled false turns these gates off; events are still audited.

team governance remove-edge SOURCE TARGET stops sharing an association:

The bundle is written first, atomically. If that write fails, nothing has changed. If the local store update fails after it, the bundle already carries the retraction and the next session’s import finishes it. A failed removal is audited too.

Admin-only; a non-admin gets Permission denied and exit 1. Breaking: the command now takes the two nodes (remove-edge SOURCE TARGET [--project PATH]) instead of one edge_id. The old form never removed anything.

team governance list-shared [--project PATH] [--json] lists the project’s shared-memory associations, strongest first. It used to print [].

Audited: publishes (including refused ones), imports of a teammate’s bundle, memory review-approve / review-reject, and removals now write to the hash-chained audit log, alongside the configuration changes they already covered. Appends are serialized across processes (a lock file beside the log), so hooks and commands writing at the same moment can’t fork the hash chain. neuralmind team audit verify checks the chain; neuralmind team audit export --format csv --output audit.csv hands it to an auditor. The actor is NEURALMIND_ACTOR_EMAIL, else the repository’s git config user.email, else the OS user.

10. The cognition loop, rebuilt — and still on demand

Before v4.6.0, neuralmind cognition-loop ran its own SQL, and it destroyed learned memory:

It now runs one idempotent pass on the machinery the hooks already use:

  1. Decay — the synapse store’s half-life decay: 30 days for personal and branch memory, 60 for shared, 1 for ephemeral, with long-term edges kept at their floor. It charges only the time since the previous decay, so running it often never decays anything twice.
  2. Read-dedup cleanup — rows untouched for a day are dropped.
neuralmind cognition-loop .          # one pass
neuralmind cognition-loop . --json   # edges_pruned, edges_remaining, transitions_*, read_cache_pruned

No timer ships, on purpose. Claude Code’s session-start hook already decays at every session start, and neuralmind watch decays every 10 minutes. The pass is for setups that run neither, such as an MCP-only client. It’s safe for cron:

0 * * * * cd /path/to/repo && neuralmind cognition-loop . >/dev/null

Hub normalization stays with the pre-compact hook: it scales a hub’s edges down on every call, so a frequent schedule would compound it. If you ran the old pass, its leftover rows are inert; neuralmind memory reset --namespace traversal removes them.

What the agent actually sees post-install

Retrieval ranking (sections 1–5)

Agent Before (v4.5.0) After (v4.6.0)
Claude Code (MCP + hooks) neuralmind_query ranked L3 with a keyword list only docs were in Same tool and the same four slots, ranked with one keyword index over docs and code after the next build; hooks unchanged
Cursor (MCP) Same as Claude Code Same change through neuralmind_query; neuralmind_search (plain vector search) is unchanged
Cline / Continue (MCP) Same as Claude Code Same as Cursor
Generic MCP client — No schema change: same tools, same arguments, re-ranked L3 hits
CI neuralmind eval . scored v4.5.0 ranking Same command; run it with and without NEURALMIND_BM25_UNIFIED=0 to see what v4.6.0 changed on your repo

Wired in, or gone (sections 6–10)

Agent Before (v4.5) After (v4.6)
Claude Code (hooks + MCP) Every re-read of a file arrives in full. The stale-guard fires only on decisions someone invalidated by hand A repeat read of an unchanged file arrives as a stub, with the next read always full. The stale-guard also fires after commits change a decision’s files, and shows why, the id, and decisions restore <id>
Cursor / Cline / Continue (MCP) Decision tools report what was invalidated by hand Decisions also go STALE on commit (git hook), so neuralmind_audit_decisions and neuralmind_query_decisions reflect it. No read dedup (no PostToolUse hooks)
Generic MCP client Same as above Same as above
git (after neuralmind init-hook .) post-commit rebuilt the index post-commit first marks stale decisions and prints them, then rebuilds
Team admins Scope, threshold and remove-edge recorded only Enforced on publish; removals retract through the bundle; every team-memory event audited

Environment variables

Variable Default Effect
NEURALMIND_BM25_UNIFIED on One BM25 index over docs and code, written by build. 0 restores v4.5.0’s docs-only keyword list
NEURALMIND_L3_PER_FILE off N caps L3 hits per file (measured at 2), refilling from the next-best candidates
NEURALMIND_DOC_HANDOFF off 1: a doc hit that names a code file or symbol brings that code into contention
NEURALMIND_HUB_DAMPEN off 1: down-weights files returned far more often than chance
NEURALMIND_INTENT_RULES off 1: “how does X… / where is X… / which X is…” questions get code intent; questions that name a document or ask how to install get docs
NEURALMIND_INTENT_POOL off 1: intent ranks all 10 candidates instead of re-ordering the four L3 chose; measured with NEURALMIND_INTENT_RULES=1
NEURALMIND_BM25_CODE off 1: a second, code-only keyword list fused by RRF
NEURALMIND_READ_DEDUP on 0: every re-read of a file arrives in full (no stub)
NEURALMIND_DECISION_SCAN on 0: the post-commit hook skips the decision scan

Five more ranking changes (six flags — the intent pool is a variant of the intent rules) were measured and not kept. Those flags are research settings: each one lost on the eval above. Measure one on your own repository before turning it on — see A/B-test a ranking change on your own repo.

Upgrade notes