ruvnet / ruvnet/RuVector

mcp-server.js still saves intelligence.json non-atomically and swallows corrupt loads — #634 / #698 fixed cli.js only

Open
#995 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
Rust
Stars
4.5k
Forks
603
Avg merge
23h 32m
Merged PRs (30d)
59

Description

Describe the bug

#634 reported that Intelligence.save() overwrites .ruvector/intelligence.json with a plain fs.writeFileSync and that load() swallows a parse error into empty defaults, so a concurrent reader that lands mid-write silently restarts from an empty store. #698 fixed both in bin/cli.js (atomicWriteFileSync, readIntelStoreSafe quarantining to .corrupt-<epoch>), but bin/mcp-server.js has its own copy of the Intelligence class that was not touched:

  • mcp-server.js Intelligence.save() still ends in fs.writeFileSync(this.intelPath, JSON.stringify(this.data, null, 2)) — line 396 in both 0.2.41 and 0.3.1 (line 320 in 0.2.34).
  • mcp-server.js Intelligence.load() still wraps the read in try { … } catch {} and returns { patterns: {}, memories: [], … } on any parse failure, with no quarantine.

Under Claude Code the MCP server is the long-lived writer (every hooks_remember call goes through it) while each PreToolUse/PostToolUse/SessionStart hook is a separate short-lived ruvector hooks … CLI process reading the same file. The server's save therefore still opens the torn-file window #634 described; the fixed CLI side now detects the torn read and quarantines the file, but the store the user ends up with is whatever the hook wrote next.

What we observed (ruvector 0.2.34 server, 2026-09-17): a 9 MB onnx-minilm-stamped store with 757 memories was being saved by the MCP server while ~12 parallel subagents fired PostToolUse hooks. A hook-side CLI read the half-written file, moved it to intelligence.json.corrupt-1789669812640, and created a fresh store — hash-stamped, 64-dim, because the hook path resolved the hash embedder — so hooks reembed --dry-run then reported an embedder-provenance MISMATCH against a store that had been fine an hour earlier. The quarantined file was complete and parsed with all 757 memories; the server still had the good snapshot in memory. The same sequence is reachable on 0.3.1 because the server-side write is unchanged.

To Reproduce

  1. Start the MCP server (ruvector mcp start, or via a Claude Code plugin) against a store of a few MB with vector memories.
  2. In a loop, call hooks_remember through the server while running ~20 parallel npx ruvector@0.3.1 hooks route "x" / hooks remember "y" -t test CLI processes against the same .ruvector/.
  3. Eventually a CLI process reads mid-write: with 0.2.41+ it prints the quarantine error and intelligence.json.corrupt-<epoch> appears; the server's next save() then overwrites intelligence.json from memory, or a CLI process creates a fresh store first, depending on timing.

Deterministic variant (no race): while the server is running, truncate intelligence.json mid-file, then trigger a server-side operation that calls load() (e.g. restart the server) — load() returns empty defaults with no error and the next save() persists the empty store.

Expected behavior

  • mcp-server.js Intelligence.save() writes to a unique temp file in the same directory and rename()s over the target, as cli.js does since #698 (ideally by sharing that helper rather than a second copy).
  • mcp-server.js Intelligence.load() fails loud (or quarantines, like readIntelStoreSafe) on an unparseable store instead of returning empty defaults that the next save() persists.
  • Reader side: rename() alone does not protect a reader that already opened the old inode before the rename (see npm/write-file-atomic#64), so a parse-retry once (re-open + re-read) before quarantining would avoid quarantining a file that was simply replaced under the reader.

Environment

  • OS: Linux 6.18 (WSL2, Ubuntu 24.04)
  • Node: 24.15.0 (also reproduced reading with 22.22.0)
  • Ruvector version: 0.2.34 (observed); 0.2.41 and 0.3.1 verified to still have the plain writeFileSync at bin/mcp-server.js:396

Additional context

  • #634, #698 (cli.js fix), #635 / #697 (same class in the rvf id-map sidecar).
  • Downstream write-up with recovery steps: yellow-plugins docs/solutions/integration-issues/ruvector-adr210-embedding-provenance-refusal.md, "Non-atomic store write race".

Contributor guide

No contributing guide indexed for this repository

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start by comparing the Intelligence.save() and Intelligence.load() implementations in bin/mcp-server.js with atomicWriteFileSync and readIntelStoreSafe in bin/cli.js. Reproduce the concurrent or truncated-file scenarios described, then verify that the MCP server uses atomic replacement, retries or quarantines unreadable stores, and does not silently persist empty defaults.

Written by the indexing model from the issue text.

Assessment

Tech stack
javascript, nodejs
Domain
backend, cli
Issue type
Bug
Difficulty
3/5
Estimated time
1-2 days
Activity status
Active
Clarity
Clearly specified
Newbie friendliness
72/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.