Skip to content

fix(memory): add a cross-process file lock so separate server processes do not overwrite each other - #4744

Open
daichiyasunami-vottia wants to merge 1 commit into
modelcontextprotocol:mainfrom
daichiyasunami-vottia:fix/memory-cross-process-lock
Open

fix(memory): add a cross-process file lock so separate server processes do not overwrite each other#4744
daichiyasunami-vottia wants to merge 1 commit into
modelcontextprotocol:mainfrom
daichiyasunami-vottia:fix/memory-cross-process-lock

Conversation

@daichiyasunami-vottia

Copy link
Copy Markdown

Refs #1819, #3286. Builds on #4555.

#4555 serialises mutations within one process, and it fully fixes that case. The server is stdio-only, so every client is its own process with its own queue: two editor windows, or two git worktrees pointed at one MEMORY_FILE_PATH, still overwrite each other's load→mutate→save.

Measured on d73f99e (this branch's base), 20 create_entities calls:

serial   kept=20 lost=0
single   kept=20 lost=0     one process, 20 in flight   <- #4555
multi    kept=10 lost=10    two processes, one file      <- 5 runs, exactly 10 each time

Each process serialises its own half correctly and the last one to write the file wins. Every call returns success.

Change (one method, no new dependency): wrap each mutation in a sidecar lock MEMORY_FILE_PATH.lock created with fs.open(..., 'wx') — O_EXCL is atomic on POSIX and Windows. Retry with a short backoff; a lock older than 30 s is treated as left by a crashed process and reclaimed. The in-process queue is kept, so a process never contends with itself. With this branch the multi row is 20/20.

Relation to #3286: same goal. That PR is now CONFLICTING against main after #4555 and adds proper-lockfile (+1007 lines). This is the minimal version on top of the current queue; happy to close in favour of a rebased #3286 if that is preferred.

Tests: __tests__/cross-process-lock.test.ts — two instances on one file (a faithful stand-in for two processes, since the queue is per instance) keep 20/20; the lock file is removed after each mutation; a stale lock is reclaimed. Full suite: 90 passed.

…es do not overwrite each other

modelcontextprotocol#4555 serialises mutations within one process. The server is stdio-only, so
each client is its own process with its own queue: two editor windows or two
git worktrees pointed at one MEMORY_FILE_PATH still overwrite each other's
load->mutate->save. Measured on d73f99e with 20 writes split across two
processes: exactly 10 survive, every time - each process serialises its own
half correctly and the last one to write the file wins.

Wrap each mutation in a sidecar lock (MEMORY_FILE_PATH.lock) created with
O_EXCL, which is atomic on POSIX and Windows and needs no dependency. A lock
older than 30s is treated as left by a crashed process and reclaimed. The
in-process queue stays, so one process never contends with itself.

Refs modelcontextprotocol#1819, modelcontextprotocol#3286
Copilot AI balanced review requested due to automatic review settings September 3, 2026 11:01

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Stale-lock reclamation can admit multiple holders and reintroduce lost updates.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Pull request overview

Adds cross-process locking to prevent concurrent memory-server instances from overwriting shared graph data.

Changes:

  • Wraps mutations with a sidecar file lock.
  • Adds contention, cleanup, and stale-lock tests.
File summaries
File Description
src/memory/index.ts Implements cross-process mutation locking.
src/memory/__tests__/cross-process-lock.test.ts Tests lock behavior across manager instances.
Review details
  • Files reviewed: 2/2 changed files
  • Comments generated: 1
  • Review effort level: Balanced

💡 Add a code-review agent skill for context-aware, tailored reviews. Learn more in the docs.

Comment thread src/memory/index.ts
Comment on lines +137 to +140
const stat = await fs.stat(this.lockFilePath);
if (Date.now() - stat.mtimeMs > KnowledgeGraphManager.LOCK_STALE_MS) {
await fs.unlink(this.lockFilePath).catch(() => {});
continue;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants