Repository navigation
Bug: graph-reset leaves pre-reset rows queryable and races graph-extract #1189
Description
User problem
After POST /agentmemory/graph/reset reports success, Graph queries can still return nodes and relationships from before the reset. A concurrent mem::graph-extract can also overwrite the reset snapshot with a snapshot derived from the previous Graph state.
This is a correctness problem on small Graphs. It is independent of the large-corpus worker failure reported in #825.
Environment
- Published agentmemory:
@agentmemory/agentmemory@0.9.28 - Upstream
main: package version0.9.29, commit2973e4e - Node:
v26.4.0 - OS: macOS
26.6.1arm64
Reproduction
- Create one Graph node and one relationship through normal extraction.
- Confirm that
POST /agentmemory/graph/querywith a matchingqueryor the node'sstartNodeIdreturns them. - Call
POST /agentmemory/graph/reset. - Repeat the same
queryorstartNodeIdrequest. - For the race case, use a controlled provider or test double to pause
mem::graph-extractafter it reads the current Graph snapshot. Runmem::graph-reset, then allow extraction to continue.
Actual result
mem::graph-reset writes only an empty KV.graphSnapshot with resetAt. The source explicitly leaves KV.graphNodes and KV.graphEdges on disk as orphans:
The query and startNodeId paths enumerate the raw node and relationship scopes and filter only on stale. They do not exclude records older than snapshot.resetAt:
GraphRetrieval.searchByEntities, expandFromChunks, and temporalQuery have the same behavior:
src/functions/graph-retrieval.ts#L41-L50src/functions/graph-retrieval.ts#L117-L123src/functions/graph-retrieval.ts#L158-L167
The extraction persist path reads the snapshot, writes nodes, relationships, lookup indexes, and degrees, and writes the snapshot at the end. Reset does not share one operation-wide serialization boundary with that sequence:
This permits the following ordering:
- Extraction reads the pre-reset snapshot.
- Reset writes the empty snapshot and returns success.
- Extraction writes its nodes and relationships.
- Extraction writes its pre-reset-derived snapshot after the reset.
Expected result
A successful reset must establish one clear Graph generation boundary:
- No Graph read returns pre-reset nodes or relationships.
- All Graph read paths apply the same generation rule.
- Reset and extraction are ordered atomically.
- An extraction completed before reset is hidden or removed by reset.
- An extraction committed after reset writes only into the new generation.
- Reset does not report success while an in-flight extraction can restore the old snapshot.
The implementation can use physical deletion, generation-aware filtering, or another design. The required result is consistent isolation across every Graph read and write path.
Related work
- Bug: no path to rebuild or reset a >25K legacy graph — snapshot-rebuild + graph-reset both heartbeat-crash on state::list (follow-up to #816) #825 covers large-corpus reset/rebuild worker failure. This report reproduces on a small Graph and concerns reset isolation.
- Bug: entity-graph query/traversal (graph-query query + startNodeId) heartbeat-crashes past ~25K nodes on FRESH corpora — needs the per-node edge index #816 references #828 covers large-corpus
queryandstartNodeIdenumeration failure, not post-reset visibility. - Request to add available APIs for batch session deletion and graph cleanup #907 requests batch Session deletion and Graph cleanup APIs but does not define reset-generation semantics.
- Observation eviction leaves session counters and graph provenance stale #1157 covers Observation-deletion consistency.
- Unbounded sourceObservationIds / sourceMemoryIds are what push collections past the worker limit in #1142 / #1124 #1168 and graph nodes: sourceObservationIds capped at creation but unbounded on merge #1171 cover unbounded provenance.
- Closed, unmerged PR fix(knowledge-graph): deduplication, validation, GC, and locking (6 fixes) #813 proposed stale Graph garbage collection but did not serialize reset with extraction.
Activity