Repository navigation
Minor release. It changes default write behavior (see "Behavior change") and fixes two security issues, so read the upgrade notes before pulling.
What's new
Security (#39)
- Cypher injection through relation and type names. Labels and relationship types were interpolated inside backticks without validation, so a backtick in a relation or type closed the identifier and the rest of the string ran as Cypher. Tenant isolation is enforced by predicates in the query text, so this bypassed it. It was reachable through
graph_relate(single and batch),graph_boost,graph_weaken,graph_entitiesandgraph_unmerge. All 23 interpolation sites are now validated. - Caller-supplied properties could overwrite
tenant_id, moving a node into another tenant with no injection needed, and could also overwriteidandconfidence. Reserved keys are now stripped. graph_entitiesis annotated read-only but ran in a write session. It andrunReadQuerynow use read transactions.- HTTP transport with
TENANT_ID_SOURCE=staticnow logs a loud warning at startup. The default is unchanged, so existing installs keep working.
If untrusted content flows into extraction (ingested pages, email), update: the injection is reachable from extracted relation names.
Data integrity (#39)
- Decay compounding.
applyDecaymeasured fromlast_seenon every run, so decay compounded torate^(N(N+1)/2)instead ofrate^N. Weights fell far faster than the configured half-lives. Decay now accrues once per elapsed interval, anchored on a newlast_decayedproperty. - One entity resolver for every write path (exact name, case-insensitive name,
aliases, slug). Batch mode previously skipped name lookup and created duplicate nodes. The newgraph_aliastool manages aliases. - Batch mode is atomic, as its description already claimed. It runs in one transaction; invalid items are skipped and reported instead of aborting the batch.
- Re-asserting an edge could raise its weight past 1.0. It is now clamped, and
graph_relatereportscreatedorstrengthenedinstead of alwayscreated. - Superseding several predecessors no longer inflates the new edge's weight, and
valid_atnever supersedes multi-valued verbs such asFAMILY_OForKNOWS. - Re-mentioning an entity under a different type no longer collides with the uniqueness constraint, and a match no longer overwrites the stored name.
max_hopsis truncated and capped at 4.
Transcript format detection (#40)
detectFormatVersion looked at only the first 5 lines of a transcript. Current Claude Code transcripts open with five or more non-message records (queue-operation, attachment), so a valid transcript was reported as "unknown format". The nightly dream skips unknown-format transcripts and marks them processed, so those sessions were never extracted.
Detection now scans up to 500 lines and stops at the first record with the expected fields. A file with no message-bearing record still reports "unknown". This also adds the first tests for the parser.
Behavior change
Types and relations are now checked against the documented vocabulary (src/shared/types.ts, matching GRAPH_SCHEMA.md).
- A well-formed but undocumented relation is stored as
RELATED_TO, with the original kept inproposed_relations. - An unknown type becomes
Object, with the original kept inproposed_type. - Known same-direction aliases map directly (
USES_TECHNOLOGYtoUSES_TECH,IMPACTStoAFFECTS, and so on).
Nothing is lost, and no new label or relationship type is created. Set SCHEMA_ENFORCEMENT=lenient for the previous behavior, or strict to reject. The default is coerce.
Image
ghcr.io/stevepridemore/graph-memory-mcp:v0.4.0ghcr.io/stevepridemore/graph-memory-mcp:latest(same digest)
Upgrade
docker compose pull
docker compose up -d --force-recreate graph-memory-mcpBoth the MCP server image and the host dist/ (used by the session-start hook) contain the transcript parser. If you run the hook from a clone, pull and run npm run build there too.
Upgrade notes
Weights already over-decayed by the old bug are not restored. Upgrading stops the compounding; it does not repair values that were already driven down. Nodes and edges written before this release have no last_decayed, so the first decay run after upgrading measures from last_seen and applies one more catch-up step. To skip that step and have decay start from the upgrade date, stamp last_decayed once before the next run:
MATCH (n:Entity {tenant_id: '<your-tenant>'})
WHERE n.confidence IS NOT NULL AND n.last_decayed IS NULL
SET n.last_decayed = datetime();
MATCH (:Entity {tenant_id: '<your-tenant>'})-[r]->(:Entity {tenant_id: '<your-tenant>'})
WHERE r.weight IS NOT NULL AND r.last_decayed IS NULL
SET r.last_decayed = datetime();Transcripts skipped as "unknown format" stay marked as processed. If a skipped transcript still exists on disk, remove its entry from processed/manifest.json and the next dream run will pick it up.
Schema enforcement applies to new writes only. Existing nodes and edges are not rewritten.
Testing
176 tests against Neo4j 5.20 as of #39, plus 5 parser tests in #40. Each new test was checked by reverting its fix and confirming the test fails.