{"path":"research/supersession-and-bitemporal-lifecycles.md","content":"# Edge Supersession and Graphiti's Bitemporal Model: Same Move, Different Axis\n\n**Date**: 2026-08-22 · **Occasion**: founder question the day edge supersession shipped — how does it relate to Graphiti's bitemporal model? · **Grounding**: read from Graphiti's source, not its marketing — `graphiti_core/edges.py` (the four temporal fields and their docstrings) and `graphiti_core/utils/maintenance/edge_operations.py` (`resolve_edge_contradictions`), in the fork this fleet already runs. Zep's paper describes the design (arXiv:2501.13956).\n\n## What Graphiti actually does\n\nEvery entity edge carries **four timestamps on two independent axes** — the classic bitemporal scheme from database theory (Snodgrass; SQL:2011 temporal tables):\n\n| Axis | Fields | The question it answers |\n|---|---|---|\n| **Valid time** (world time) | `valid_at` / `invalid_at` | *when was this fact true in the world?* |\n| **Transaction time** (system time) | `created_at` / `expired_at` | *when did the system learn it, and when did it retire the record?* |\n\nThe invalidation mechanism (`resolve_edge_contradictions`): when a newly ingested fact is judged (by an LLM pass) to contradict existing edges, each contradicted edge gets `invalid_at = the new fact's valid_at` — the new fact's validity start ends the old one's — and `expired_at = now()`, the system-time stamp of retirement. **The edge is never deleted**: historical queries still see it; current-state queries filter it out.\n\n## The relation: the same architectural move, on a different axis\n\nSupersession and Graphiti invalidation are the **same move** — mark-and-filter, never delete; the record survives for history while the \"current\" computation ignores it. Deprecation-over-deletion made queryable, in both systems.\n\nBut the mark answers a **different question in each**, and the difference is exactly the three-way lifecycle taxonomy this project keeps circling:\n\n1. **The world changed** — the fact stopped holding. Graphiti's *valid time*. Deliberus has **nothing** here: this is the unbuilt temporal rung (the doctor's note ages, a terminus expires, and — the founder's observation on shipping day — a state-deficit answer is time-indexed by nature).\n2. **Our knowledge changed** — we learned the record was wrong. Graphiti's *transaction time*, applied automatically by the contradiction pass. Deliberus **deliberately does not handle this with lifecycle at all**: learning-better is an argument move — an ATTACKS edge, a strength shift — kept visible and contestable in the graph, because contest is the product. Graphiti can expire silently and automatically because it is a *memory substrate* with no ratification loop; a deliberation graph that auto-expired contradicted claims would be the machine adjudicating, which propose-never-assert forbids.\n3. **The representation sharpened** — the edge was never wrong and never stopped holding; a finer-grained rendering of the same content replaced it. This is supersession, and it sits on **neither time axis**. Its family in the standards world is not bitemporality but provenance lineage — W3C PROV's `wasRevisionOf`.\n\nSo: Graphiti is bitemporal with no refinement lineage; Deliberus (as of today) has refinement lineage with no temporal axis. Orthogonal, complementary, and each system's gap is the other's strength.\n\n## Two asymmetries worth keeping\n\n**Deliberus's supersession names its successor; Graphiti's expiry does not.** A contradicted Graphiti edge records *that* it expired, not *which* edge expired it (verified in `resolve_edge_contradictions`: the timestamps are set, no successor reference is stored). Our `superseded_by` requires the successor edge to exist and points at it — the lineage is walkable and auditable. For a memory system this hardly matters; for a system whose product is inspectable reasoning, the pointer is load-bearing.\n\n**Graphiti's valid-time pair is the ready-made pattern for the temporal rung.** The staleness daemon (the ranked first daemon precisely because its adversary is entropy, not strategy) would in Graphiti vocabulary be the process that *proposes* `invalid_at` on aging support — with the Deliberus twist that the proposal is ratified by a human, never auto-applied. A claim's evidence could carry `valid_at`/`invalid_at` fields tomorrow without any redesign of the strength layer: an invalidated-support filter would slot in exactly where the superseded-edge filter went this morning — the same `WHERE` seam, a fourth condition. That the two filters would sit side by side and mean different things is the taxonomy point: **stale, refuted, and superseded are three different states, and a lifecycle that conflates them files the penguin under large flightless duck.**\n\n## Built the same day (founder: \"We should build it, unless you see a reason not to?\")\n\nThe check for reasons-not-to came back green — the daemon doctrine's preconditions were all newly met (strength layer settled this week, supersession as the undo-shaped lifecycle, and a deterministic detector needs no LLM, so the spend gate is moot). Shipped: `recorded_at` on every claim creation, backfilled onto all 4,768 existing claims from their source dates (datable fraction 0.985 — the honest remainder is verdict claims with no source); `valid_at`/`invalid_at` exactly as this note prescribed; the staleness daemon (`deliberus/staleness.py`, flag-tier, entropy-class, per-kind decay horizons as named provisional parameters, no-basis-never-flags); ratification via `POST /claims/{id}/validity` minting a challengeable verdict-claim ground; the dashboard `GET /graph/staleness`; and the invalid-evidence filter at the same WHERE seams as the supersession filter — the fourth condition, landing exactly where this note predicted it would. First full loop live-verified on the synthetic corpus: a three-year-old testimonial swept (one flag from 3,728 candidates, zero noise), ratified, publicly declared LAPSED with its ground linked, and its support edge stopped weighing.\n\n**The founder's challenge, same day, and it stands: the DETECTOR is deterministic; its INPUTS are not.** Traced: the evidence-kind label is assigned by the extraction LLM (a judgment, made once, upstream — `evidence_type` on the extraction's claim model); and content time (`valid_at`) is currently populated by NOTHING — zero claims carry it — so every sweep so far aged *transaction* time, when the graph learned the claim, not when the underlying observation happened. Those come apart in the dangerous direction: the van den Haag essay (c. 1969) entered the graph July 2026, so its citation-kind claims read as fresh until 2030 — half-century-old scholarship wearing this year's date. The design copes honestly at the margins (every proposal names which date it aged; a human ratifies; no basis means no flag), but the accurate statement is that **judgment was moved upstream and made ratifiable, not eliminated**: the arsenal-and-harness split, with the clock arithmetic in the arsenal and the kind-label plus any content-date owed to judgment — an extraction-time LLM pass for `valid_at` (stated dates, relative dates against the source's own date, fieldwork ranges) is the missing half, and two whole families resist dating entirely: event-bound validity (\"the sitting president supports…\" expires on an election, not a duration) and timeless claims (exempt today via kind-absence, correctly).\n\n**The machine-readable tier shipped the same evening** (`deliberus/temporal_extraction.py` + `scripts/temporal_backfill.py`): URL date paths, copyright and published lines, datelines — under a conservative agreement rule (candidates disagreeing on the year propose nothing; a year-only signal resolves to Dec 31, the youngest defensible reading, so the pass can fix fresh-forever but never overstate age). Run against the live corpus: **3 of 31 sources machine-datable, 28 honest misses** — and the two that matter both landed: the van den Haag essay dated **1986** by its copyright line (correcting not only the graph but the operator's own from-memory \"1969\"), and the 2014 Swedish blog post dated by its URL path, whose anecdotal claims the re-sweep now flags at their true twelve-year age. The LLM tier (`llm_content_dates`, stated + relative dates with null-beats-a-guess instructions) is written, schema-typed and gated off until quota returns. Event-bound validity remains a watcher design: a registered condition plus a trigger source, feeding the SAME ratification pipe as the clock daemon — the ratification machinery is proposer-agnostic, which is what makes the watcher an add-on rather than a redesign.\n\n## Consequence\n\nNo integration is implied (the fleet's Graphiti instance serves session memory, a different job). The yield is conceptual: when the temporal rung gets built, borrow the bitemporal *shape* — two named axes, never a single \"deprecated\" flag — and keep supersession as the third, a-temporal axis it already is. The edge lifecycle then has its full vocabulary: `superseded_by` (sharper rendering), `invalid_at` (stopped holding, human-ratified), and the argument layer itself for \"we learned better,\" which never becomes metadata at all.\n\n## Cross-references\n\n[synthetic-stress-suite-and-ontology-reflection.md](synthetic-stress-suite-and-ontology-reflection.md) § Wave two (why supersession exists) · [fractal-scales-and-temporal-frame.md](fractal-scales-and-temporal-frame.md) (the temporal rung; the staleness daemon's entropy-class adversary) · [decomposing-value.md](decomposing-value.md) §6 (the state-deficit answer as a time-indexed fact) · [taxonomy-gaps-and-the-closed-enum.md](taxonomy-gaps-and-the-closed-enum.md) (why three states must not share one flag)\n"}