Repository navigation
fix(memory): release held lock files on exit and SIGINT/SIGTERM - #5530
Conversation
withFileLock releases its lock in a finally, which never runs when a signal ends the process, so a stopped server stranded memory.jsonl.lock and the next writer waited for it to go stale (the full 30 s from another container). Track held locks and remove those still ours synchronously on exit; SIGINT and SIGTERM handlers exit with the default status so the exit hook runs. The Dockerfile stops with SIGINT, as everything's does. Closes #5485 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: cliffhall <cliff@futurescale.com>
🦋 Changeset detectedLatest commit: 4efde40 The changes in this PR will be included in the next version bump. This PR includes changesets to release 1 package
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
|
| Tools | Prompts | Resources | Resource templates | |
|---|---|---|---|---|
| Base | 9 | 0 | 1 | 0 |
| Head | 9 | 0 | 1 | 0 |
Updated by this run for 4efde404fe4757fe2c825f5801a64c565a0ac38d.
There was a problem hiding this comment.
🟡 Changes recommended
Lock tracking gaps during acquisition and release can leave stale locks behind if SIGINT or SIGTERM arrives during filesystem operations, undermining the intended cleanup.
2 open issues
What changed in this PR
This PR addresses #5485 by adding shutdown cleanup for the memory server’s lock files, aiming to prevent stale-lock delays after termination.
Changes:
- Tracks held lock tokens and installs synchronous cleanup on exit, SIGINT, and SIGTERM.
- Sets Docker’s stop signal to SIGINT and documents shutdown behavior.
- Adds lock ownership and signal-handling tests, plus a patch changeset.
| File | Description |
|---|---|
src/memory/README.md |
Documents lock cleanup during shutdown. |
src/memory/index.ts |
Adds held-lock tracking and process cleanup handlers. |
src/memory/Dockerfile |
Sets SIGINT as the container stop signal. |
src/memory/__tests__/file-lock.test.ts |
Tests tracking, ownership preservation, and signal cleanup. |
.changeset/memory-lock-exit-cleanup.md |
Records the memory server patch release change. |
Review effort: Balanced
Copilot round 1 on #5530: a signal during the token write or during the release found no tracked entry and left the lock. Track it as soon as the exclusive open returns, treat a file holding a prefix of our token as ours at exit, and untrack only after the release's unlink. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: cliffhall <cliff@futurescale.com>
|
Copilot round 1: two findings, both fixed in 72d35db (each is a signal window in what this PR added).
Gate re-run: exit 0. Requesting round 2. |
|
| Tools | Prompts | Resources | Resource templates | |
|---|---|---|---|---|
| Base | 1 | 1 | 0 | 0 |
| Head | 1 | 1 | 0 | 0 |
Updated by this run for 4efde404fe4757fe2c825f5801a64c565a0ac38d.
|
| Tools | Prompts | Resources | Resource templates | |
|---|---|---|---|---|
| Base | 22 | 4 | 8 | 5 |
| Head | 22 | 4 | 8 | 5 |
Updated by this run for 4efde404fe4757fe2c825f5801a64c565a0ac38d.
|
| Tools | Prompts | Resources | Resource templates | |
|---|---|---|---|---|
| Base | 14 | 2 | 0 | 0 |
| Head | 14 | 2 | 0 | 0 |
tools
--- base/tools.json
+++ target/tools.json
- tools[git_remote].description: "Lists the repository's remotes with their fetch and push URLs, like `git remote -v`. Credentials emb..."
+ tools[git_remote].description: "Lists the repository's remotes with their fetch and push URLs, like `git remote -v`. Credentials emb..."Updated by this run for 4efde404fe4757fe2c825f5801a64c565a0ac38d.
Copilot round 2 on #5530: once our token is written, a prefix match also matched another writer's empty replacement lock, letting exit cleanup remove it. Record whether the token write finished; only an unwritten lock, which cannot yet have been judged stale and replaced, takes the prefix match. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: cliffhall <cliff@futurescale.com>
|
Copilot round 2: one finding, fixed in 4efde40.
Gate re-run: exit 0. Requesting round 3. |
|
Copilot round 3 (on 4efde40): clean, no findings. Review loop converged. |


Closes #5485
Description
withFileLockreleases its lock in afinally, which never runs when a signal ends the process. A memory server stopped mid-write (Ctrl-C,docker stop, a client shutting it down) therefore leftmemory.jsonl.lockbehind, and the next writer waited for it to go stale: the full 30 s when that writer is in another container and cannot see that the owner's PID is gone.withFileLocknow records each lock it holds (path → its token) for as long as it holds it.releaseHeldLocks()removes, synchronously, every held lock that still carries this process's token, so a lock another process broke and re-took is left alone, as the normal release already does.installLockCleanup(), called once frommain()(not at import, so tests that import the module keep their own signals), runs that onexit, and handlesSIGINT/SIGTERMby exiting with the status the default action would have given (130/143), which fires theexithook.src/memory/DockerfilesetsSTOPSIGNAL SIGINT, matchingsrc/everything/Dockerfile. Node as PID 1 ignores a signal it has no handler for; with the handlers above both signals now work, and SIGINT keeps the two images consistent.Server Details
@modelcontextprotocol/server-memory)Motivation and Context
See #5485. The fix follows the prompt the reporter supplied there (credit to @Faris-as for the report and prototype), written fresh against
v2/main.How Has This Been Tested?
Unit tests (
src/memory/__tests__/file-lock.test.ts, new describe block):releaseHeldLocks()removes a lock still ours, leaves one another process re-took, and is quiet about one already gone;installLockCleanup()registersexit,SIGINTandSIGTERMonce, and the signal handlers exit 130 / 143;SIGTERM: it exits 143 and the lock is gone (skipped on Windows, which has no deliverable SIGTERM). WithoutinstallLockCleanup()the same script exits 143 and leaves the lock, so the test discriminates.Inspector CLI (2.12.0), both eras, before/after. To stop the real bin mid-write,
MEMORY_FILE_PATHis a FIFO, socreate_entitiesblocks in its load while holding the lock; the server's PID is read from the lock file and signalled:memory.jsonl memory.jsonl.lockmemory.jsonl memory.jsonl.lockmemory.jsonl memory.jsonl.lockmemory.jsonl memory.jsonl.lockmemory.jsonlmemory.jsonlmemory.jsonlmemory.jsonlA normal
create_entitiesstill succeeds in both eras (exit 0; the modern result carriesio.modelcontextprotocol/serverInfomemory-server2.1.1) and leaves onlymemory.jsonl.LLM client: Claude Code 2.1.322 (
claude -p --strict-mcp-config, the branch'sdist/index.js); the era cannot be selected from this client. Prompt: "Using the memory server, create an entity named 'Ada' of type 'person' with the observation 'wrote the first program', then read the graph and reply with just the entity's observations." →- wrote the first program. The graph file held the entity and, after the client shut the server down, no lock file remained.Gate:
npm run local:gateexit 0.Breaking Changes
None. Client configuration is unchanged. The server process now exits with status 130 on SIGINT and 143 on SIGTERM, which is what the default signal action reported to a shell before.
Types of changes
Checklist
npm run changeset) if this changes what a TypeScript server publishesAdditional context
SIGHUPis not handled: the issue asked forSIGINT/SIGTERM, and a stdio server whose client goes away sees its stdin close rather than a hangup. A hard kill (SIGKILL, OOM) still cannot run any handler; that case remains covered by the existing stale-lock breaking.🤖 Generated with Claude Code