Skip to content

v0.4.0: write validation, security fixes, decay and transcript parser fixes

Latest

Choose a tag to compare

@stevepridemore stevepridemore released this 11 Oct 17:43
559359d

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_entities and graph_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 overwrite id and confidence. Reserved keys are now stripped.
  • graph_entities is annotated read-only but ran in a write session. It and runReadQuery now use read transactions.
  • HTTP transport with TENANT_ID_SOURCE=static now 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. applyDecay measured from last_seen on every run, so decay compounded to rate^(N(N+1)/2) instead of rate^N. Weights fell far faster than the configured half-lives. Decay now accrues once per elapsed interval, anchored on a new last_decayed property.
  • 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 new graph_alias tool 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_relate reports created or strengthened instead of always created.
  • Superseding several predecessors no longer inflates the new edge's weight, and valid_at never supersedes multi-valued verbs such as FAMILY_OF or KNOWS.
  • 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_hops is 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 in proposed_relations.
  • An unknown type becomes Object, with the original kept in proposed_type.
  • Known same-direction aliases map directly (USES_TECHNOLOGY to USES_TECH, IMPACTS to AFFECTS, 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.0
  • ghcr.io/stevepridemore/graph-memory-mcp:latest (same digest)

Upgrade

docker compose pull
docker compose up -d --force-recreate graph-memory-mcp

Both 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.