Summary
client.graph.add_fact_triple(...) returns a successful AddTripleResponse(edge=None, task_id=<id>) but the resulting edge is not discoverable through any client API for at least 30 seconds:
client.graph.edge.get(client_fact_uuid) returns 404
client.graph.edge.get_by_graph_id(graph_id) does not include the edge (polled 30 times at 1s intervals)
- Fact-text scan across the same paginated list does not find any matching edge
This reproduces against a fresh test graph with no custom ontology using only the public zep-cloud Python SDK.
Environment
- SDK:
zep-cloud (Python)
- Python: 3.14 (also observed on 3.11 in production)
- Platform: cloud Zep
Minimal reproducer
Attached at the end of this issue. Self-contained, 118 lines, no external dependencies beyond zep-cloud. Creates a fresh throwaway graph (bug_repro_<utc_stamp>), runs the failing sequence, prints a verdict, and deletes the test graph on exit.
Run with:
ZEP_API_KEY=<your-key> python repro_minimal_add_fact_triple_undiscoverable.py
Observed run (verdict: BUG_REPRODUCED)
creating graph: bug_repro_20260516T035254661020Z
client_fact_uuid: 6650fa7f-db6f-46c6-9d70-9fcda81e4725
add_fact_triple response: AddTripleResponse(edge=None, source_node=None, target_node=None, task_id='a6b74acb-e076-425b-98e5-6f52b4392fb6')
edge.get: NotFoundError: status_code: 404, body: message='not found' request_id='5819d4dc-5450-493d-aea9-bb5e74f991c8'
get_by_graph_id: poll 1..30 not_found
fact scan: not_found
REPRO: BUG_REPRODUCED
deleted graph: bug_repro_20260516T035254661020Z
The task_id (a6b74acb-e076-425b-98e5-6f52b4392fb6) is available for server-side trace if helpful.
Second observed manifestation
While constructing this reproducer, a second names-only add_fact_triple call (intended to bootstrap an additional target node on the same fresh graph) also produced a task response without materializing a discoverable edge. The committed minimal reproducer uses the smallest sequence that reliably triggers the bug; the broader pattern suggests this is not specific to UUID-addressed creation.
Hypotheses tested and falsified before filing
We independently tested four mechanisms locally before reducing this to a minimal reproducer:
- Client-provided
fact_uuid not honored ? falsified. A diagnostic probe confirmed edge.get_by_graph_id discovers the edge at the client-provided UUID when discovery does eventually succeed (in a different reproducer variant).
- By-UUID consistency lag relative to list-index ? falsified. Polling
edge.get_by_graph_id first, then edge.update(uuid), still failed within a 15-retry budget.
- Historical
created_at hides edge from discovery ? falsified. Forcing created_at=datetime.now(timezone.utc) did not change the outcome.
- Newly-created target node UUID not yet propagated ? falsified. Adding a 30-second
asyncio.sleep between bootstrap and the failing call did not change the outcome.
The minimal reproducer attached here strips all four of those variables and still reproduces the bug.
Reproducer source
"""Minimal public-SDK reproducer for UUID-addressed edge discovery.
Fresh graph, no custom ontology. Calls add_fact_triple with source_node_uuid,
target_node_uuid, and fact_uuid, then checks whether that UUID is discoverable.
"""
import asyncio
import os
import sys
import uuid
from datetime import datetime, timezone
from zep_cloud.client import AsyncZep
def uid(edge):
return getattr(edge, "uuid_", None) or getattr(edge, "uuid", None)
async def scan_edges(client, graph_id, uuid_=None, fact=None):
found_uuid = found_fact = None
cursor = None
while True:
kwargs = {"limit": 100}
if cursor:
kwargs["uuid_cursor"] = cursor
edges = await client.graph.edge.get_by_graph_id(graph_id, **kwargs)
for edge in edges:
if uuid_ and uid(edge) == uuid_:
found_uuid = edge
if fact and fact in (getattr(edge, "fact", None) or ""):
found_fact = edge
if found_uuid or len(edges) < 100:
return found_uuid, found_fact
cursor = uid(edges[-1])
if not cursor:
return found_uuid, found_fact
async def wait_fact(client, graph_id, fact, seconds=60):
for _ in range(seconds):
_, edge = await scan_edges(client, graph_id, fact=fact)
if edge:
return edge
await asyncio.sleep(1)
return None
async def bootstrap(client, graph_id, fact, source, target):
await client.graph.add_fact_triple(
graph_id=graph_id, fact=fact, fact_name="GOVERNS",
source_node_name=source, source_node_labels=["Person"],
target_node_name=target, target_node_labels=["Organization"],
)
edge = await wait_fact(client, graph_id, fact)
if not edge:
raise RuntimeError(f"{fact} was not discoverable")
return edge
async def poll_repro(client, graph_id, edge_uuid, fact, seconds=30):
fact_match = None
for poll in range(1, seconds + 1):
by_uuid, by_fact = await scan_edges(client, graph_id, uuid_=edge_uuid, fact=fact)
fact_match = fact_match or by_fact
if by_uuid:
print(f"get_by_graph_id: found uuid on poll {poll}")
return by_uuid, fact_match
print(f"get_by_graph_id: poll {poll} not_found")
await asyncio.sleep(1)
return None, fact_match
async def main():
api_key = os.getenv("ZEP_API_KEY")
if not api_key:
print("ERROR: ZEP_API_KEY is required in the environment.")
return 2
stamp = datetime.now(timezone.utc).strftime("%Y%m%dT%H%M%S%fZ")
graph_id = f"bug_repro_{stamp}"
repro_fact = f"repro-recreated-{stamp}"
client = AsyncZep(api_key=api_key)
try:
print(f"creating graph: {graph_id}")
await client.graph.create(graph_id=graph_id, name=graph_id, description="Minimal add_fact_triple discovery reproducer.")
await asyncio.sleep(2)
edge1 = await bootstrap(client, graph_id, f"bootstrap-1-{stamp}", "SourceA", "TargetB")
client_fact_uuid = str(uuid.uuid4())
print(f"client_fact_uuid: {client_fact_uuid}")
response = await client.graph.add_fact_triple(
graph_id=graph_id, fact=repro_fact, fact_name="GOVERNS",
fact_uuid=client_fact_uuid,
source_node_uuid=edge1.source_node_uuid, source_node_name="SourceA", source_node_labels=["Person"],
target_node_uuid=edge1.target_node_uuid, target_node_name="TargetB", target_node_labels=["Organization"],
)
print(f"add_fact_triple response: {response!r}")
try:
direct = await client.graph.edge.get(client_fact_uuid)
print(f"edge.get: found uuid={uid(direct)}")
direct_found = True
except Exception as exc:
print(f"edge.get: {type(exc).__name__}: {exc}")
direct_found = False
listed, by_fact = await poll_repro(client, graph_id, client_fact_uuid, repro_fact)
if by_fact:
print(f"fact scan: found uuid={uid(by_fact)} source={by_fact.source_node_uuid} target={by_fact.target_node_uuid}")
else:
print("fact scan: not_found")
if listed or direct_found:
print("REPRO: BUG_NOT_REPRODUCED")
return 1
if by_fact:
print("REPRO: PARTIAL")
return 2
print("REPRO: BUG_REPRODUCED")
return 0
except Exception as exc:
print(f"ERROR: {type(exc).__name__}: {exc}")
return 2
finally:
try:
await client.graph.delete(graph_id)
print(f"deleted graph: {graph_id}")
except Exception as exc:
print(f"cleanup failed for graph {graph_id}: {type(exc).__name__}: {exc}")
if __name__ == "__main__":
sys.exit(asyncio.run(main()))
Impact
Any caller of add_fact_triple with source_node_uuid + target_node_uuid + fact_uuid is currently unable to subsequently locate the created edge through the client SDK. This affects cleanup/merge workflows that need to update edge attributes after creation. Server-side trace of the failing task_id would help confirm whether the edge was actually persisted server-side or whether the task silently dropped it.
Request
Server-side investigation of task_id=a6b74acb-e076-425b-98e5-6f52b4392fb6 (run at 2026-05-16T03:52:54Z UTC against test graph bug_repro_20260516T035254661020Z ? the graph was deleted after the run, but the task trace should still be available). Reproducer can be run on demand to generate a fresh task_id if needed.
Happy to provide additional diagnostic output or run modified versions of the reproducer.
Summary
client.graph.add_fact_triple(...)returns a successfulAddTripleResponse(edge=None, task_id=<id>)but the resulting edge is not discoverable through any client API for at least 30 seconds:client.graph.edge.get(client_fact_uuid)returns 404client.graph.edge.get_by_graph_id(graph_id)does not include the edge (polled 30 times at 1s intervals)This reproduces against a fresh test graph with no custom ontology using only the public
zep-cloudPython SDK.Environment
zep-cloud(Python)Minimal reproducer
Attached at the end of this issue. Self-contained, 118 lines, no external dependencies beyond
zep-cloud. Creates a fresh throwaway graph (bug_repro_<utc_stamp>), runs the failing sequence, prints a verdict, and deletes the test graph on exit.Run with:
Observed run (verdict: BUG_REPRODUCED)
The
task_id(a6b74acb-e076-425b-98e5-6f52b4392fb6) is available for server-side trace if helpful.Second observed manifestation
While constructing this reproducer, a second names-only
add_fact_triplecall (intended to bootstrap an additional target node on the same fresh graph) also produced a task response without materializing a discoverable edge. The committed minimal reproducer uses the smallest sequence that reliably triggers the bug; the broader pattern suggests this is not specific to UUID-addressed creation.Hypotheses tested and falsified before filing
We independently tested four mechanisms locally before reducing this to a minimal reproducer:
fact_uuidnot honored ? falsified. A diagnostic probe confirmededge.get_by_graph_iddiscovers the edge at the client-provided UUID when discovery does eventually succeed (in a different reproducer variant).edge.get_by_graph_idfirst, thenedge.update(uuid), still failed within a 15-retry budget.created_athides edge from discovery ? falsified. Forcingcreated_at=datetime.now(timezone.utc)did not change the outcome.asyncio.sleepbetween bootstrap and the failing call did not change the outcome.The minimal reproducer attached here strips all four of those variables and still reproduces the bug.
Reproducer source
Impact
Any caller of
add_fact_triplewithsource_node_uuid+target_node_uuid+fact_uuidis currently unable to subsequently locate the created edge through the client SDK. This affects cleanup/merge workflows that need to update edge attributes after creation. Server-side trace of the failingtask_idwould help confirm whether the edge was actually persisted server-side or whether the task silently dropped it.Request
Server-side investigation of
task_id=a6b74acb-e076-425b-98e5-6f52b4392fb6(run at 2026-05-16T03:52:54Z UTC against test graphbug_repro_20260516T035254661020Z? the graph was deleted after the run, but the task trace should still be available). Reproducer can be run on demand to generate a freshtask_idif needed.Happy to provide additional diagnostic output or run modified versions of the reproducer.