Skip to content

add_fact_triple returns task_id but produces undiscoverable edge (fresh graph, no ontology) #313

@FC-FUZ

Description

@FC-FUZ

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:

  1. 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).
  2. 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.
  3. Historical created_at hides edge from discovery ? falsified. Forcing created_at=datetime.now(timezone.utc) did not change the outcome.
  4. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions