WikiDesignCo THE GIGA LIBRARY · ∞ STACKS Request a stack
← The Library
Essay 02 of 12 · Epistemics

Bi-Temporal Knowledge: When the Author of a Claim Disagrees With Themselves

Why every fact needs two timestamps, and what happens to your agent when one of them is missing.

The first essay in this series argued that vector retrieval fails structurally on four axes: it averages contradictions, it forgets time, it loses attribution, and it cannot hop. We treated each failure as roughly equal in weight. This was a rhetorical convenience. In practice one of those failures, the forgetting of time, is doing most of the damage in production systems today, and is the failure mode that the fewest teams take seriously. So take that mode in depth. What it really means for a knowledge system to model time. The difference between thinking time is metadata and treating time as a primitive. What a "valid_to" timestamp does for an agent's ability to reason that no amount of clever prompting can replace.

Start with a thought experiment that anyone building agentic systems has run into. Imagine you are building a research assistant for a working scientist. Your assistant has ingested every paper, every transcript, every blog post, every tweet from the field of, say, deep learning architectures over the last decade. A user asks it a question that should be simple: "What does Andrej Karpathy think about whether scaling is sufficient for general intelligence?"

Karpathy has been one of the most public voices in the field for a long time. He has said things about scaling. He has said different things at different times. In 2019, when GPT-2 had just landed and the scaling laws were the most exciting thing anyone had seen in years, he leaned in hard on the scaling story. By 2022, with GPT-3 in deployment and the limitations of pure autoregressive prediction starting to bite, he was more measured. By 2024, in his now-famous YouTube lectures, he was explicit that pure scaling has limitations and that structured world models are increasingly necessary for reliable reasoning. By 2026 his position has shifted further, in ways we can debate but cannot ignore.

So what does Karpathy think? The accurate answer is: it depends on when you ask, and the system's answer should explicitly include that dependence. An atemporal retrieval system cannot do this. It will surface whichever of the five quotes happens to have the highest cosine similarity to the user's phrasing, present it as Karpathy's view, and confidently mislead. A temporally aware system can do this. It can return all five, ordered by date, with their validity windows marked. It can return the most recent one and explicitly note that earlier positions existed and were revised. It can answer the meta-question, "how has Karpathy's position evolved?", natively, because the evolution is in the structure.

What "bi-temporal" actually means

The term "bi-temporal" comes from database theory and predates the current AI moment by decades. Database researchers, who have to deal with the fact that the world changes and records of the world also change, figured out a long time ago that a serious data model needs two distinct time dimensions. The first is valid time: when the thing the record describes was actually true in the world. The second is transaction time: when the record itself was created, updated, or invalidated in the database. These two times are different things, and conflating them is a category error that produces wrong answers.

Live Follow the read on both sides. On the left the planner reaches the index and the date sits off to one side, unconsulted, so any era can come back. On the right the read clears the validity window first, which makes the era part of the question.

Consider a simple example. A customer's address changes from A to B on March 1. Your system finds out about it on March 15. The valid_from of the new address is March 1. The transaction_from is March 15. If on March 10 you ran a query "where does this customer live?", what should the system return? An atemporal database would just store the latest known address, and on March 10 would still return A. A bi-temporal database stores both, knows that A was valid until March 1 and B is valid from March 1, but also knows that on March 10 the system had not yet been informed of the change. Depending on which question you are asking, "where did the customer actually live on March 10?" vs. "where did our records say the customer lived on March 10?", you get different correct answers.

Live One customer, one move on March 1, one system that learns of it on March 15. Stand on March 10 and the two axes hand back two different letters. Which one is right depends only on which of the two questions was asked.

This sounds like accountant pedantry until you realize that every claim in a knowledge graph has this same structure. Karpathy held position X. He changed his mind, internally, at some point in 2022. He stated the new position publicly in 2023. Your system ingested the 2023 statement in 2024. The valid_time of "Karpathy holds position X" closes somewhere in 2022. The transaction_time of "the system knows Karpathy no longer holds X" opens in 2024. Between 2022 and 2024, the world had moved on; the system had not. The system needs to be able to represent that gap, and to query around it.

Live The blue bar is when the position was held in the world. The amber bar is when the graph knew it. Between them sits the stretch where the world had moved and the system had not, and a query landing inside it still answers with position X at full confidence.

Graphiti's bi-temporal contribution

Graphiti is a temporal knowledge graph framework built on top of Neo4j. Its central contribution, the thing that makes it different from a generic graph database, is that every node and every edge it stores carries bi-temporal metadata as a first-class property. You do not have to remember to attach timestamps. You do not have to write custom queries that filter on validity. The framework's API requires you to provide the temporal context at write time, and every read goes through a temporal lens by default.

Concretely, when you add an episode to Graphiti, an episode being a chunk of new information entering the system, the framework runs entity extraction and relation extraction on the episode, identifies which entities and which relationships are mentioned, looks up whether those entities already exist in the graph, and for each new claim, performs the following bookkeeping. Does this claim agree with existing graph state? If so, the existing claim's evidence count increments. Does it disagree? If so, the existing claim's valid_to may be closed, and the new claim opened. Is it ambiguous, talking about a different aspect, or a different context? Then both are kept, with appropriate context tags. The framework does this work for you, episode by episode, and the result is a graph whose temporal structure reflects the real arrival pattern of information rather than the convenient fiction that all facts arrived at once.

Live One question per claim, three writes behind the answer. Agreement bumps an evidence count. Disagreement closes the old window at the instant the new one opens and writes a SUPERSEDES edge. Ambiguity keeps both, each tagged with the context it belongs to.

This matters because the arrival pattern is the information. When Karpathy's 2024 position arrives in your system, the fact that it closed his 2019 position signals that the field has been moving, that the prior consensus has shifted, that downstream agents who were relying on the old position need to be invalidated. Graphiti makes this signal queryable: "show me all claims whose validity windows were closed in the last six months." That query, run periodically, is a structural change-detection system for your domain. It is the kind of thing an atemporal vector store cannot dream of producing, because the information was never there in the first place.

Live Every bar is a claim and its window. The scan runs the length of the history and lifts the ones whose valid_to landed inside the recent band. The count in the corner is how much of the domain moved, and it comes out of the data model with no separate change-tracking system to build.

The two-timestamp discipline at ingestion

Bi-temporal modeling places a discipline on your ingestion pipeline that is, in practice, the part most teams skip. Every claim that enters the system needs both timestamps attached, and getting them right is non-trivial. The transaction_time is easy, it is whenever your pipeline processed the source. The valid_time is the hard part, because the source itself may not state it cleanly.

For research papers, the valid_time is usually the publication date, which is right there in the metadata. For YouTube transcripts, it is the upload date of the video. For tweets and blog posts, it is the post date. For books, it is the publication date, which can be tricky if the edition matters (a textbook revised in 2020 has different claims than its 2014 first edition). For interview transcripts, it is the date the interview happened, which may differ from the date the transcript was published. For each source type, your pipeline needs to know where to find the valid_time, and your schema needs to accommodate the fact that some sources are precise to the second and others only to the year.

Where valid_time lives, by source type
Source typeWhere the pipeline reads valid_timeThe catch
Research paperThe publication date, sitting in the metadataThe easy case, and the one people generalise from
YouTube transcriptThe upload date of the videoPrecise to the day, and the talk may predate it
Tweet or blog postThe post datePrecise to the second, which the schema has to allow
BookThe publication date of the edition in handA textbook revised in 2020 carries different claims than its 2014 first edition
Interview transcriptThe date the interview happenedDiffers from the date the transcript was published, and only one of the two is the answer

Then there is the second-order problem: a claim made at time T may be about something at a different time. When Karpathy in 2024 says "in 2019 I thought scaling was all we needed," there are two valid_times in play. The claim was asserted in 2024 (valid_from of the assertion). The claim is about Karpathy's 2019 state (valid_from of the referenced position). A serious bi-temporal model represents both. The current state of the graph is that the 2019 position existed, and we know about it because of a 2024 statement. The 2024 statement itself is also a current claim, with its own validity window, and it remains valid until something causes it to be closed.

Live Two valid_from values on one axis. The lower bar is the assertion, opening in 2024 and still open. The upper bar is the state it describes, which opened in 2019 and closed years before the sentence was ever said. The arrow between them is the relationship a single timestamp has nowhere to put.

This sounds like representational hairsplitting. It is not. It is the difference between a system that can answer "what did Karpathy think in 2019?" with citation chains intact, and a system that flattens everything into a single timestamp and gets confused when the question gets specific.

Validity windows and the supersession protocol

The supersession protocol is the heart of bi-temporal knowledge management. It is the answer to the question: when a new claim arrives that conflicts with an existing claim, what happens? An atemporal system overwrites. The old fact is destroyed, the new fact takes its place, and the history is lost. A naive temporal system appends, both facts coexist, and the application layer is responsible for figuring out which is current. A bi-temporal system with supersession closes the old claim's validity window at the moment the new claim's window opens, and links the two with an explicit SUPERSEDES edge.

The benefits of this are not just historical. The SUPERSEDES edge is itself queryable. You can ask: "show me all positions this author has revised." Or: "which claims in domain D have been most volatile over the last year?" Or: "when did the field's consensus on topic T shift?" These queries are trivial against a graph that maintains supersession chains, and impossible against a graph that does not.

Supersession is not deletion. The old claim remains in the graph, marked as no longer valid. Queries that ask about "the current state of the world" filter it out. Queries that ask about "the historical state of the world as of date D" include it if D falls within its old validity window. This is the bi-temporal time machine: at any point in history, you can ask the graph what it looked like, and get an accurate snapshot. Not an approximation. Not a reconstruction. The actual graph state, because the graph never forgot.

The agent's time horizon

Agents that operate on top of a bi-temporal graph have a fundamentally different relationship to time than agents that operate on top of a flat vector store. The vector-store agent lives in an eternal present, every query returns the same kind of results regardless of when it was asked, because the index does not change in meaningful ways. The graph agent operates with a time cursor. By default, the cursor is "now," and queries return current state. But the cursor can be moved. A query can be issued as-of a past date, and the graph responds with what was known then. The agent can therefore reason about its own evolution. "What did I believe about this customer six months ago, and what new information has changed my belief?" is a question the agent can answer, because the graph remembers both states.

Live The cursor starts at now and slides back. Edges whose windows contain it stay lit; the rest drop to history grey. Park it on the decision and the graph on screen is the graph the agent was looking at when it decided.

This capability changes what is possible at the application layer. A customer-success agent that has been deployed for a year and that has been ingesting customer feedback continuously can be asked: "this customer is unhappy, what did we know about their preferences when we made the decision that led to this?" The agent runs the query as-of the decision date, gets the historical snapshot, and can produce a meaningful answer. The same query against an atemporal system returns the current state, which may bear no resemblance to what the system actually believed when the decision was made. The customer-success agent on a bi-temporal substrate can do real root-cause analysis. The one on the atemporal substrate can only confabulate.

What you give up for bi-temporal modeling

The clear-eyed accounting. Bi-temporal modeling is not free, and the costs are real and operational.

The three line items, and where each one lands
CostWhat growsWhere it is paid
StorageEvery superseded claim stays, with its old window intact, so the graph accumulates historical state year on yearThe storage budget, recoverable by archiving old superseded claims to cold storage once fast querying against them stops mattering
Query complexityEvery read either takes the default temporal filter or names a cursor, and the time dimension turns up in every analytics and audit queryLonger Cypher, harder query optimisation, and an engineering surface that has to be staffed
Ingestion complexitySourcing valid_time from heterogeneous content, handling references to past states, deciding supersession against coexistenceEvery episode, and getting it wrong corrupts the graph in ways that are hard to detect and hard to roll back

Storage grows. Every claim that gets superseded remains in the graph, with its old validity window intact. Over years, the graph accumulates significant historical state. This is recoverable, you can periodically archive old superseded claims to cold storage if you no longer need fast querying against them, but it is not nothing. The disk usage of a bi-temporal graph after two years of operation is meaningfully larger than the disk usage of an atemporal one, and the storage budget needs to account for that.

Query complexity grows. Every read against the graph either uses the default temporal filter (current state) or explicitly specifies a time cursor. Application developers have to think about which they want. Most reads will be "current," which is fine, but the moment you start writing analytics queries or audit queries, the time dimension becomes pervasive. Cypher queries get longer. Query optimization gets harder. The Neo4j community has good tooling for this, but it is a real engineering surface that has to be staffed.

Ingestion pipeline complexity grows. The two-timestamp discipline described above is, by itself, a project. Sourcing valid_time correctly from heterogeneous content types, handling references to past states, deciding when a new claim supersedes vs. coexists with an old one, these are decisions the pipeline has to make on every episode, and getting them wrong corrupts the graph in ways that are hard to detect and hard to roll back.

None of these costs are deal-breakers. They are line items in the engineering budget. The point of itemizing them is that you should know what you are signing up for, and you should not sign up for it for a system whose corpus is small, static, and uncontested. For those systems, atemporal storage is genuinely fine. The bi-temporal model pays for itself when the corpus is large, dynamic, and contested, which is exactly the corpus an ambitious agentic platform is going to face.

Implementing it without Graphiti

You do not need Graphiti to get bi-temporal behavior. You can implement the patterns directly in Neo4j, or in any property graph database. The cost is that you do the bookkeeping yourself. Every node gets valid_from and valid_to and tx_from and tx_to properties. Every edge gets the same. Every query that asks about "current state" filters for valid_to IS NULL OR valid_to > now(). Every write that supersedes an existing claim is a two-step transaction: close the old claim's valid_to, write the new claim. Get this wrong and your graph is inconsistent.

Live Four properties on every node and every edge, one filter on every current-state read, and a superseding write that is two steps inside one transaction. Watch the second step land. A transaction that commits the first and misses the second leaves two claims current where one belongs.

Graphiti automates the bookkeeping and provides higher-level abstractions, add_episode, entity resolution, automatic supersession, that are tedious to implement correctly. For a small team that wants the bi-temporal benefits without staffing a full graph engineering function, Graphiti is the pragmatic choice. For a team with strong graph engineering and specific requirements that Graphiti does not yet meet, rolling your own on top of Neo4j is entirely reasonable. The choice is operational, not philosophical. Either way, the data model is what matters.

Two routes to the same data model
Graphiti on Neo4jRolling your own on a property graph
Temporal metadataCarried on every node and edge as a first-class property, required at write timeFour properties you attach and maintain yourself on every node and every edge
ReadsPass through a temporal lens by defaultEach one filters explicitly on valid_to IS NULL OR valid_to > now()
SupersessionAutomatic, alongside add_episode and entity resolutionA two-step transaction you write, and an inconsistent graph when it half-lands
Who it suitsA small team that wants the bi-temporal benefits without staffing a graph engineering functionA team with strong graph engineering and requirements the framework does not yet meet

The deepest payoff: time as a moat

Here is the structural insight that nobody talks about enough. A bi-temporal knowledge graph that has been running for a year contains something that is structurally impossible for a competitor to replicate, no matter how much capital they raise or how many GPUs they buy: a year's worth of historical state, accurately recorded. The supersession chains, the validity windows, the temporal evolution of every claim in the corpus, these are all there. They are queryable. They are the basis of any analytics, any audit, any "what changed and when" question.

A competitor who starts a year later cannot synthesize this. They can ingest the same sources, sure, but they will only have the current state, they will not have the temporal evolution, because the evolution happened in real time and the competitor was not there to record it. They will have the destination but not the journey. And for many of the most interesting questions, questions about how the field changed, how a person's views evolved, how a market shifted, the journey is the answer.

Live Each layer is an interval of recorded state, laid down as it happened. The stack on the left is a year of operation. The one on the right ingested the same sources today and received the top layer, because the layers under it were written in real time by a system that was already running.

This is the structural moat that bi-temporal modeling creates. Every day of operation accumulates value that cannot be back-filled. Teams that start now, even with imperfect implementations, are pulling ahead of teams that will start in 2027. Teams that start in 2027 will be pulling ahead of teams that start in 2030. The advantage compounds. This is the deep reason, beyond the technical merits, beyond the agent-quality merits, that we are betting on this substrate. It is the substrate whose value increases monotonically with time, while every other substrate's value plateaus.

The next essay turns to contradiction directly. We have hinted at the contradiction engine throughout these first two essays. In essay three, we open it up: what it is, how it detects, how it classifies, how it routes, and what it does when three experts walk into a graph and refuse to agree.


END OF ESSAY 02 · CONTINUE TO ESSAY 03 →