Summary
On the FalkorDB backend, each group gets its own graph, and add_episode /
add_triplet permanently switch the shared Graphiti client's driver to the
group they just wrote:
graphiti_core/graphiti.py:1081 — self.driver = self.driver.clone(database=group_id)
graphiti_core/graphiti.py:1309 — same in the triplet path
(line numbers from main @ 993e081a; behavior identical in 0.29.2)
Any long-lived multi-group client — the MCP server is exactly that — then runs
all subsequent reads inside whichever group wrote last. get_episodes /
searches for another group execute in the wrong graph with a
group_id property filter that matches nothing, and return empty with no
error. Scoped memory appears to work until a second group writes; after that,
cross-group reads silently return nothing.
Environment
- graphiti-core 0.29.2 (code path unchanged on current
main @ 993e081a)
- MCP server (streamable HTTP), FalkorDB v4.20.4, falkordb-py 1.2.0
Reproduction
- One MCP server / one
Graphiti client, FalkorDB backend.
add_memory(..., group_id="a") and wait for it to land.
get_episodes(group_ids="a") → returns the episode (driver sits on a).
add_memory(..., group_id="b") (or just let queued group-b work run).
get_episodes(group_ids="a") → empty, though the data exists.
Evidence
Captured live with redis-cli MONITOR while calling
get_episodes(group_ids="quarantine") right after group helpscout-sync had
been written:
GRAPH.QUERY "helpscout-sync" "MATCH (e:Episodic) WHERE e.group_id IN $group_ids ..."
— the Episodic query for group quarantine executed against the
helpscout-sync graph. Direct inspection confirmed the data exists where it
was written:
GRAPH.QUERY quarantine "MATCH (e:Episodic) RETURN e.name" → smoke-temp-fix
Two related design problems
- Mutating shared state:
clone() returning a rerouted driver is fine;
assigning it to self.driver on a shared client is the bug. Reads have no
corresponding per-call routing.
- Per-group graphs make
group_ids lists unsatisfiable: a search with
group_ids=["a","b"] can only ever execute inside one graph, so
cross-group search silently degrades to single-group even when routing is
"correct".
Suggested fix
Route per operation instead of mutating: use a local
driver = self.driver.clone(database=group_id) for writes, and route reads by
their requested group_ids. For the list-search case, either fan out across
graphs and merge, or offer a single-graph mode for FalkorDB in which all
groups live in the configured graph and scoping relies on the existing
group_id property filters (this is how the Neo4j path already behaves).
We deployed the single-graph variant locally (make FalkorDriver.clone() a
no-op so _database stays fixed) and all semantics — scalar reads after
interleaved writes, list searches, isolation between groups — behave correctly
since. The 0.29.3 "namespace operations routing" fixes appear related but do
not cover this.
Workaround
Pin all groups to one graph by neutering FalkorDriver.clone() (return
self), accepting the loss of graph-level isolation; scoping via the
group_id property filters already present in every query.
Summary
On the FalkorDB backend, each group gets its own graph, and
add_episode/add_tripletpermanently switch the sharedGraphiticlient's driver to thegroup they just wrote:
graphiti_core/graphiti.py:1081—self.driver = self.driver.clone(database=group_id)graphiti_core/graphiti.py:1309— same in the triplet path(line numbers from
main@993e081a; behavior identical in 0.29.2)Any long-lived multi-group client — the MCP server is exactly that — then runs
all subsequent reads inside whichever group wrote last.
get_episodes/searches for another group execute in the wrong graph with a
group_idproperty filter that matches nothing, and return empty with noerror. Scoped memory appears to work until a second group writes; after that,
cross-group reads silently return nothing.
Environment
main@993e081a)Reproduction
Graphiticlient, FalkorDB backend.add_memory(..., group_id="a")and wait for it to land.get_episodes(group_ids="a")→ returns the episode (driver sits ona).add_memory(..., group_id="b")(or just let queued group-bwork run).get_episodes(group_ids="a")→ empty, though the data exists.Evidence
Captured live with
redis-cli MONITORwhile callingget_episodes(group_ids="quarantine")right after grouphelpscout-synchadbeen written:
— the Episodic query for group
quarantineexecuted against thehelpscout-syncgraph. Direct inspection confirmed the data exists where itwas written:
Two related design problems
clone()returning a rerouted driver is fine;assigning it to
self.driveron a shared client is the bug. Reads have nocorresponding per-call routing.
group_idslists unsatisfiable: a search withgroup_ids=["a","b"]can only ever execute inside one graph, socross-group search silently degrades to single-group even when routing is
"correct".
Suggested fix
Route per operation instead of mutating: use a local
driver = self.driver.clone(database=group_id)for writes, and route reads bytheir requested
group_ids. For the list-search case, either fan out acrossgraphs and merge, or offer a single-graph mode for FalkorDB in which all
groups live in the configured graph and scoping relies on the existing
group_idproperty filters (this is how the Neo4j path already behaves).We deployed the single-graph variant locally (make
FalkorDriver.clone()ano-op so
_databasestays fixed) and all semantics — scalar reads afterinterleaved writes, list searches, isolation between groups — behave correctly
since. The 0.29.3 "namespace operations routing" fixes appear related but do
not cover this.
Workaround
Pin all groups to one graph by neutering
FalkorDriver.clone()(returnself), accepting the loss of graph-level isolation; scoping via thegroup_idproperty filters already present in every query.