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 can't hop. We treated the four as roughly equal in weight, which made the argument easier to follow. In practice one of them, the forgetting of time, is doing most of the damage in production systems today, and it's the one the fewest teams take seriously. This essay takes it in depth: what it means for a knowledge system to model time, the difference between treating time as metadata and treating it as a primitive, and what a "valid_to" timestamp does for an agent's reasoning that no amount of clever prompting can replace.

Start with a thought experiment. Imagine you're 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, and he's said different things about scaling 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 can't 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 can't give that answer. 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 give it. 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 deal with a world that changes and with records of that world that 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 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.

Take a customer whose 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 hadn't yet been told about the change. Depending on which question you're 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.

The example sounds like accountant pedantry, but 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 and the system hadn't. 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 hadn't, 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. What sets it apart from a generic graph database is that every node and every edge it stores carries bi-temporal metadata as a first-class property. You don't have to remember to attach timestamps, and you don't 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 then does some bookkeeping for each new claim. Does the 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.

That arrival pattern matters because it 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. An atemporal vector store can't produce anything like it, 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 puts a discipline on your ingestion pipeline, and in practice it's the step that gets skipped. Every claim that enters the system needs both timestamps attached, and getting them right takes work. The transaction_time is easy: it's whenever your pipeline processed the source. The valid_time is harder, 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's the upload date of the video. For tweets and blog posts, it's the post date. For books, it's 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's 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 has to hold sources that are precise to the second next to sources that are precise 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's a 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.

Those two dates separate a system that can answer "what did Karpathy think in 2019?" with citation chains intact from 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, because it decides what happens when a new claim arrives that conflicts with an existing one. 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 has to figure 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 history is only part of the payoff, because 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 doesn't.

Supersession keeps the old claim 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. That's what makes a bi-temporal graph a time machine: pick any point in its history, ask what it looked like, and you get the actual graph state as it stood then, 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 doesn't 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.

A time cursor changes what's 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 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

Bi-temporal modeling has real costs, and they're operational ones.

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. The growth is recoverable, since you can periodically archive old superseded claims to cold storage if you no longer need fast querying against them, but it still costs something. 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's real engineering work 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, and deciding when a new claim supersedes an old one or coexists with it 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.

Each of these costs is a line item in the engineering budget. Itemizing them tells you what you're signing up for, and you shouldn't sign up for it on a system whose corpus is small, static, and uncontested. For those systems, atemporal storage is fine. The bi-temporal model pays for itself when the corpus is large, dynamic, and contested, which is the corpus an ambitious agentic platform is going to face.

Implementing it without Graphiti

You don't 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 that are tedious to implement correctly: add_episode, entity resolution, automatic supersession. 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 doesn't yet meet, rolling your own on top of Neo4j is 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

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 and the temporal evolution of every claim in the corpus are all there and all queryable, and they're the basis of any analytics, any audit, any "what changed and when" question.

A competitor who starts a year later can't synthesize that year. They can ingest the same sources, sure, but they'll only have the current state, because the evolution happened in real time and the competitor wasn't there to record it. They'll know where things ended up and nothing about how they got there. And for many of the most interesting questions (how the field changed, how a person's views evolved, how a market shifted), how things got there 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.

That recorded history is the moat bi-temporal modeling creates, because every day of operation adds value that can't 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. That moat, beyond the technical merits and the agent-quality merits, is the deep reason we're betting on this substrate, whose value increases monotonically with time, while every other substrate's value plateaus.

The next essay turns to contradiction directly. We've 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 →