What Graphiti records, and what it does not

Read ata6e026f
Repositorygithub.com/getzep/graphiti
LicenceApache-2.0
Assessed asstores, derives
Reachesno level yet
Read byTroy Brandon Clifford
LevelMeetsWhat the level asks
TR-1 Recorded4/5The record exists and is append-only.
TR-2 Explained3/5Every belief resolves to its evidence, and disagreements survive.
TR-3 Gatedn/aActions carry a verdict, and approvals carry a name.
TR-4 Verifiable0/3The record can be shown not to have changed.

A count is requirements fully met out of those that apply. This system is assessed as stores, derives, and requirements outside that are not counted against it.

Graphiti is bi-temporal by design and it shows in almost every answer here. A fact that stops being true is not removed: resolve_edge_contradictions stamps invalid_at and expired_at on the existing edge and keeps it, so both sides of a disagreement remain in the graph and a query about a past moment can still be answered. Facts also point at the episodes they were extracted from, and the raw episode content is retained as a separate node type, which means the told and the inferred can never be confused for one another. This is the strongest showing of the systems assessed so far and it was arrived at independently of this specification, which is the most useful kind of agreement. The single thing standing between it and TR-1 is that deletion, when it does happen, leaves no record that it happened.

Every verdict, and what it rests on

Twenty requirements, each stated as a capability rather than a format, so a system that holds the information in its own shape counts as having it. An absent verdict cites where the assessor looked and did not find it, which is the difference between a measurement and an accusation.

TR-1 Recorded

The record exists and is append-only.

R1.1 · present. When the system stores a fact, does it durably record when that happened?

The separation of transaction time from valid time is exactly the distinction the specification draws between when an entry was written and when the fact held, and graphiti keeps both.

  • source graphiti_core/edges.py:54
    created_at is a required field on the base Edge, stored with the edge itself
  • source graphiti_core/edges.py:271
    expired_at records when the system learned a fact was no longer valid, which is transaction time kept separately from valid_at and invalid_at, the world time

R1.2 · present. When a stored fact changes, is the previous version still readable?

R1.3 · present. Does the ordinary write path ever destroy what was previously recorded?

A correction is a new edge plus a closed interval on the old one, which is what the specification asks for. Hard deletion exists but is an explicit administrative call rather than part of this path, and is assessed at R1.5.

R1.4 · present. Are entries distinguishable by kind, or is everything one undifferentiated blob of text?

R1.5 · absent. When data must be destroyed for a legal reason, is the destruction itself recorded?

This is the one requirement keeping graphiti below TR-1, and it is narrow. Everything about the ordinary write path is append-only and bi-temporal. What is missing is that when a deletion does occur, whether for privacy or housekeeping, the graph afterwards is indistinguishable from one where the data never existed. A tombstone carrying the deleted uuid, the time and the requester, without the content, would close it.

  • source graphiti_core/nodes.py:111 Node.delete
    issues DETACH DELETE against the node and its edges
  • source graphiti_core/nodes.py:178 delete_by_group_id
    removes an entire partition in batches, which is the natural call for a per-tenant or per-subject erasure request
  • searched grep -rniE 'erasure|deletion_log|tombstone|deleted_at' graphiti_core/ --include=*.py
    no match; there is no record type, column or table that survives a delete to say one happened

TR-2 Explained

Every belief resolves to its evidence, and disagreements survive.

R2.1 · present. Can the source a stored fact came from be recovered from the store, by following a link rather than by guessing?

The link is bidirectional and followable, and the raw material is retained rather than discarded after extraction. This is the requirement most systems miss and it is the one graphiti answers best.

  • source graphiti_core/edges.py:267-270
    EntityEdge.episodes is a list of the episode ids the fact was extracted from
  • source graphiti_core/nodes.py:318-328
    EpisodicNode retains content, the raw episode data, along with source, source_description and valid_at; entity_edges points back at the facts drawn from it

R2.2 · present. Can a fact the system inferred be told apart from one it was told?

The distinction is structural rather than a flag, which is stronger: a consumer cannot accidentally read an extracted fact as something a person said, because the two are not the same kind of object.

  • source graphiti_core/nodes.py:318-321
    an EpisodicNode holds content, described as 'raw episode data', with a source of EpisodeType and a source_description
  • source graphiti_core/edges.py:264-266
    an EntityEdge holds fact, produced by LLM extraction. It is a different type in a different part of the schema

R2.3 · present. When two stored facts about the same proposition disagree, do both survive?

R2.4 · partial. Is the disagreement itself queryable, or must a reader diff rows to notice it?

A disagreement is discoverable: query the edges between one pair of nodes and look for a closed interval. It is not recorded as such, so an edge invalidated by a contradiction and one invalidated because the fact simply lapsed look identical, and nothing points from the loser to the winner.

  • source graphiti_core/utils/maintenance/edge_operations.py:538
    resolve_edge_contradictions returns the invalidated edges to its caller but writes no record that a contradiction was the cause
  • searched grep -rniE 'class Conflict|conflict_id|contradiction' graphiti_core/ --include=*.py
    matches are function names and prompt text only; there is no conflict object, and no field on an edge naming what invalidated it

R2.5 · partial. When a conflict is resolved, does the record say who resolved it and by what method?

That the conflict was resolved is recorded. Who resolved it is not. The decision runs through an LLM extraction step and then the temporal comparison in this function, and the graph afterwards carries neither the model, the prompt version nor the reason.

  • source graphiti_core/utils/maintenance/edge_operations.py:562-570
    the invalidation is written down as a temporal bound
  • searched grep -rn 'invalidated_by\|resolved_by\|decided_by\|model_name' graphiti_core/edges.py graphiti_core/utils/maintenance/edge_operations.py
    no match; neither the model that extracted the contradicting fact nor the reason is recorded on either edge

TR-3 Gated

Actions carry a verdict, and approvals carry a name.

R3.1, R3.2, R3.3, R3.4, R3.5, R3.6, R3.7 are not assessed here, because this system is not in that business and marking it down for that would be dishonest.

TR-4 Verifiable

The record can be shown not to have changed.

R4.1 · absent. Does the system publish a scheme under which the record's past state can be verified?

  • searched grep -rniE 'hash_chain|merkle|signature|tamper|checksum|digest' graphiti_core/ --include=*.py
    the only hash is an md5 used to key the LLM response cache at llm_client/client.py:157; nothing relates to record integrity
  • docs graphiti README.md
    no integrity, verification or tamper-evidence claim is made

R4.2 · absent. Can an independent party run that verification without the vendor's cooperation, and without the operator's?

  • searched grep -rniE 'def verify|verification' graphiti_core/ --include=*.py
    no verification surface exists, so there is nothing for an independent party to run

R4.3 · absent. Would alteration of a past entry be detectable after the fact?

The bi-temporal design means the ordinary code path does not lose history, which is a different and weaker property than being able to show that nobody went around the code path.

  • searched grep -rniE 'hash_chain|merkle|signature|tamper|checksum|digest' graphiti_core/ --include=*.py
    no match; nothing binds one record to the next, so an edit made directly against the graph database leaves no evidence
  • source graphiti_core/edges.py:49-54
    an edge carries a uuid and timestamps but no digest of itself or of anything preceding it

How this was read

Read of graphiti_core/edges.py, graphiti_core/nodes.py and graphiti_core/utils/maintenance/edge_operations.py at a6e026f, cloned from the public repository. Line numbers are that commit's.

Found an error? Challenge a finding

Then it is wrong in the ordinary way readings are wrong, and every verdict cites a file and a line at a pinned commit precisely so that being wrong is cheap to demonstrate. The remedy is a pull request against the subject file, and it does not involve persuading anybody. Nobody applied for this and it is not a certification.

The standing register carries every system side by side, and the rubric is the twenty requirements in full, free to apply to anything, including to this assessment.