Repository navigation
feat(memory): opt-in write limits on graph size - #5534
Conversation
Add MEMORY_MAX_ENTITIES, MEMORY_MAX_RELATIONS and MEMORY_MAX_FILE_BYTES. When set, create_entities, create_relations and add_observations refuse a call that would leave the graph over a limit and larger than it was, with a tool error naming the variable; nothing is saved. Delete tools are never refused, so a graph over a lowered limit can always shrink. An invalid value stops startup. The README documents the limits and that every write rewrites the whole memory file under the lock, so its cost grows linearly with the file. Closes #5525 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: cliffhall <cliff@futurescale.com>
🦋 Changeset detectedLatest commit: fba4d6b 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 fba4d6be541b51a45359eef9eef1d4cfaf1e3472.
There was a problem hiding this comment.
🟡 Changes recommended
Additive no-ops can incorrectly fail the file-size cap when saving normalizes existing content, and blank configured limits silently disable the cap contrary to the documented validation rules.
2 open issues
What changed in this PR
Adds opt-in graph-growth limits to the memory server for #5525, complementing its existing read-side controls and helping operators manage whole-file write costs.
Changes:
- Introduces environment-configured limits on entity count, relation count, and saved file bytes.
- Checks additive writes before saving, while leaving delete operations unrestricted.
- Adds limit-validation and protocol-level tests, operator documentation, and a minor-release changeset.
| File | Description |
|---|---|
src/memory/README.md |
Documents the limits, refusal behavior, and linear write cost. |
src/memory/index.ts |
Parses configuration and enforces growth limits before saving. |
src/memory/__tests__/write-limits.test.ts |
Tests parsing, limit enforcement, unchanged files after refusal, and startup wiring. |
src/memory/__tests__/helpers.ts |
Allows test connections to configure graph limits. |
.changeset/memory-write-limits.md |
Records the feature for a minor package release. |
Review effort: Balanced
A call that adds nothing is no longer checked against the limits, since re-saving can grow a file not written in canonical form. An empty or blank limit variable now stops startup instead of leaving the limit off, as the README says. 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 c4f8da4.
|
|
| 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 c4f8da452da3008fb435c0c0404aef7b7348e281.
There was a problem hiding this comment.
🟡 Changes recommended
The no-op regression test fixture stays the same byte size after serialization, so it would not catch incorrectly rechecking no-op calls against the byte limit.
1 open issue
2 resolved since last review
Review effort: Balanced
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: cliffhall <cliff@futurescale.com>
|
Copilot round 2: one finding, fixed in a54816d. The no-op byte-limit fixture's extra space offset its missing final newline, so the test could not catch a regression; it now grows on save, and was confirmed to fail without the fix. Round 1's two findings show as resolved. |
…y-write-caps Signed-off-by: cliffhall <cliff@futurescale.com> # Conflicts: # src/memory/index.ts
…y-write-caps Resolve the createServer signature against #5536's Streamable HTTP transport: limits becomes the fourth parameter, after publishGraphUpdated. run() reads the limits for the HTTP path too, and startHttpServer passes them to each request's server, with a test. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: cliffhall <cliff@futurescale.com>
|
Round 3 was clean. Since then v2/main gained Streamable HTTP for memory (#5536), which conflicted with this PR's |

Closes #5525
Description
Adds three opt-in write limits to the memory server, and documents the linear per-write cost the issue measured.
MEMORY_MAX_ENTITIES,MEMORY_MAX_RELATIONS,MEMORY_MAX_FILE_BYTES: each unset by default (no change in behavior). When one is set,create_entities,create_relationsandadd_observationsrefuse a call that would leave the graph over the limit and larger than it was on that dimension, with a tool error naming the variable. The whole call is refused and nothing is saved.MEMORY_MAX_FILE_BYTEScounts UTF-8 bytes of the file as it would be saved (unreadable lines included), so it bounds each write's cost directly.Server Details
interface-diff: no interface changes).Motivation and Context
#5525: the read side gained opt-in limits in #3953; the write side had no operator-set ceiling while every mutation's cost grows linearly with the file. The issue asked for parity (opt-in caps on entities, relations, bytes) and/or a README note; this does both.
How Has This Been Tested?
Unit and protocol-level tests in
src/memory/__tests__/write-limits.test.ts(env parsing, each limit, whole-call refusal with the file left byte-for-byte unchanged, no-op calls at the limit, shrinking a graph over a lowered limit, bytes vs characters, andmain()reading the environment).npm run local:gateexits 0.Inspector CLI 2.12.0,
node src/memory/dist/index.js -e MEMORY_MAX_ENTITIES=2, the same sequence in--protocol-era legacy(2025-11-25) and--protocol-era modern(2026-07-28, result_metacarriesio.modelcontextprotocol/serverInfo), with identical results:create_entitiesA, Bcreate_entitiesCisError: true, "Refused: this change would bring the knowledge graph to 3 entities, over the limit of 2 set by MEMORY_MAX_ENTITIES. Nothing was saved. …"delete_entitiesBcreate_entitiesCtools/listwithMEMORY_MAX_FILE_BYTES=10MBMEMORY_MAX_FILE_BYTES must be a positive whole number, got "10MB"LLM client: Claude Code 2.1.322 (
claude -p --strict-mcp-config, local build,MEMORY_MAX_ENTITIES=2; the era cannot be selected in this client). Prompt: "Using the memory server, create three entities of type person named Ada, Grace and Linus, in one create_entities call. If the call fails, report the error message verbatim and stop." The model calledcreate_entities, got the refusal, and reported it verbatim; the graph file was never created.Breaking Changes
None. Every limit is off unless its variable is set.
Types of changes
Checklist
npm run changeset) if this changes what a TypeScript server publishes (minor)Additional context
The limits are checked in
saveGraphagainst the sizeloadGraphWithUnreadableread, after the serialized content is built and before anything is written, so a refusal leaves the file untouched. Only the three additive tools pass a baseline; the delete tools skip the check.🤖 Generated with Claude Code