Time and truth
Supersession, valid time, hedged claims and episodes — how louvain decides what is true now, what was true then, and who said so.
A fact in a company is not a value. It is a value, a date it started applying, a date it stopped, a person who said it, a place they said it, and usually an earlier value it replaced. Throw any of those away and the memory can still answer questions — it just cannot be checked, which for a system of record is the same as being unable to be trusted.
louvain keeps all of it. This page covers how that shows up in answers.
Two clocks
Every claim carries two independent notions of time, and confusing them is how memory systems quietly lie.
Knowledge time (recorded_at) is when louvain learned the thing — taken from
when the source observed it, not from when the row was inserted, so a backfilled
year of history is time-travelable rather than all dated to the import.
Valid time (valid_from, invalid_at) is when the thing was true in the
world. A claim also records where its valid-from came from: extracted when
the source actually stated a date, said-at when it is only a defaulted lower
bound — the message's own timestamp standing in for a date nobody gave. That
distinction is surfaced rather than smoothed over, because "true since 14 March"
and "we heard about it on 14 March" are different assertions and only one of
them is evidence.
An as-of query respects both: it excludes anything louvain had not yet learned by that date, and anything that had already stopped being true by it.
POST /v1/answer
{ "question": "What was the Acme contract value in June 2026?" }The bound applies across every tier, not just claims. A point-in-time question answered from a passage recorded after the date being asked about is a system quietly time-travelling its own evidence.
Supersession
Whether a new claim replaces an old one is decided by the ontology, never by a
constant in the code. A predicate declared cardinality: "single" supersedes —
a deal has one stage, a person has one manager. A predicate declared "multi"
accumulates — an account can carry several risks, and collapsing them would
delete information.
2026-06-20 drive/renewals "Acme Corp contract value to $340k/yr."
→ claim Acme Corp | deal.value = $340k/yr
2026-07-02 slack/sales "they came back at 410k a year"
→ claim Acme Corp | deal.value = $410k/yr (current)
the $340k claim is marked superseded, not deletedThe old row stays. /v1/entities/:name/timeline renders the full visible
history in system-time order, with superseded rows shown and marked rather than
hidden — marked only when this viewer can see the claim that replaced them,
which is the rule in the next section. That is what makes "walk me through what
happened on this account" a rendering of dated rows rather than a story a model
reassembled.
Two refinements matter in practice.
Corroboration does not create a new row. If a message asserts a claim that
already exists and is current, it gains an attestation rather than a duplicate
claim. One fact, two sources — which is also what makes the visibility union in
Permissions meaningful. A re-statement also moves
the claim's asserted_at forward, so a fact said again on 10 September is a
fact about 10 September, however long it has been true.
Supersession compares the claim's moment, not its arrival. Each claim has a
moment: the later of its start (valid_from) and the newest statement that
asserted it (asserted_at) — unless it arrived already over, a reminiscence,
whose moment is its start alone. A claim closes the incumbent only if its moment
is later, with recorded time as the tiebreak for two claims about the same
instant. On a live stream that is invisible: messages arrive roughly when they
happen. A backfill hands history over in whatever order it likes, and before
this rule the current value was whichever row was processed last — an
imported mailbox showed a role from 15 August as current because it was
ingested after four claims from 24 August. An older fact that arrives last is
recorded born-superseded: written, attributable, never current.
2026-08-24 gmail/payslips "Rena Okafor — Global Implementation Lead"
→ claim Rena | person.role = Global Implementation Lead (current)
2026-08-15 gmail/inbox "Rena, Business Analyst, is on leave" ← ingested later
→ claim Rena | person.role = Business Analyst born-superseded:
its moment (15 Aug) is before the incumbent's (24 Aug)Supersession is trust-ranked. A claim only closes the incumbent if its source ranks at least as trustworthy. Ranks are per connector and configurable; the defaults put deliberate human entry above chat, chat above AI-agent assertions, those above documents, and documents above continuous CRM sync — conversations decide, documents restate, and bulk re-assertion is volume rather than judgment. A lower-ranked newcomer is recorded born-superseded: written, attributable, visible in an audit, and never current. Without this, a CRM sync running every fifteen minutes buries a human statement under sheer volume, and the graph's opinion becomes whatever system is noisiest.
Supersession follows the supersessor
This is the rule that makes truth per-principal, and it is the one that most often surprises people.
If the claim that supersedes yours is in a container you cannot see, your version stays current, and unstruck, for you.
2026-07-02 slack/sales "Acme moved to legal review this morning"
→ claim Acme Corp | deal.stage = legal review
2026-08-01 slack/exec-private "Acme is closed lost, we're not renewing"
→ claim Acme Corp | deal.stage = closed lost (supersedes)
Priya (in slack/exec-private) → closed lost
Marcus (not in it) → legal review: current, unstruck, no footnoteThe tempting alternative — show Marcus the old value with a line through it, or a "superseded" badge, or "there is a more recent value you cannot see" — leaks the existence of the correction. Marcus now knows something happened in a room he is not in, on this account, on that date. That is frequently the sensitive part.
So the rule is stated in the negative and holds absolutely: louvain never tells you something false, and never reveals that more exists.
The same logic decides which of several visible claims is current. For a single-valued predicate, a viewer's current value is the latest claim they can see that is not struck for them — latest by the same moment supersession uses, so the write side and a partially-sighted reader never disagree about which fact is newer — and a later claim only suppresses an earlier one while that later claim is itself effectively current for this viewer. Re-assertions after an invisible override do not stack up as duplicates, and a born-superseded low-trust row cannot suppress the incumbent it failed to displace.
Speculation never wins
Certainty is extracted from the source, never inferred from a score. A claim is
asserted or hedged.
Hedged claims are stored, because a memory that drops "we might lose Nimbus" has thrown away the most useful sentence in the channel. But a hedged claim never supersedes an asserted one and never takes the current slot: "we might move them to closed lost" must not displace "they moved to negotiation".
The reverse confirms. An asserted statement of a value that was only hedged before lands on that claim as an attestation — one current row per value — and flips it to asserted, so "we lost them" takes the slot that "we might lose them" could not. A hedged claim about a later moment never records an assertion born-superseded either; speculation holds nothing against a statement.
An answer only falls back to speculation when nothing was ever asserted, and when it does, it says so:
Nimbus deal.stage: closed lost — unconfirmed, said as a possibility
(priya in slack/exec-private)Things that stop being true
Supersession handles a fact being overwritten. It does not handle a fact that merely stopped applying, and those are different. "Rena is the champion" is not replaced by anything when Rena leaves the company; it just ends.
Templates mark some predicates terminal — person.departure is the clearest
case. Asserting one writes invalid_at, and it ends more than its own subject:
- the subject's other current claims end as of the stated date; and
- claims that pointed at the subject end too — the champion claim on an account, the owner claim on a service.
The second half is matched by name as well as by id, because entity resolution scopes ids by kind: the person named as a champion and the person whose departure is announced can be two rows describing one human, and an ending that only followed ids would miss exactly the case it exists for.
2026-03-14 slack/sales "Dana Voss is the champion at Acme Corp."
→ claim Acme Corp | champion = Dana Voss
2026-06-02 slack/people "Dana Voss's last day is 30 June."
→ claim Dana Voss | person.departure = 2026-06-30 (terminal)
→ Acme Corp | champion = Dana Voss ends: invalid_at = 2026-06-30The read path honours invalid_at on every query and on every tier. A claim
that stopped being true is not current, however recently it was recorded.
Provenance as an answer
"Who said that?" is a first-class question, not a debugging feature. A provenance plan returns the claim with the sources this viewer can see:
Acme Corp contract value is $410k/yr — 2 source(s) you can see:
marcus in slack/sales (2026-07-02); priya in drive/renewals (2026-07-03)The count is per viewer, for the reason given in Permissions: a corroboration badge computed from attestations the viewer cannot see would be a leak wearing a number.
Episodes
Time also organizes the input side. Ingest assigns every message to an episode — the conversation it belongs to — before extraction runs.
Assignment is one of three lineages:
- stay — continues the newest open episode in that container;
- route — joins an older open one, because the message is about that topic;
- branch — starts a new episode.
The scoring is multi-signal on purpose, because embedding similarity alone is inverted for this job: a pronoun follow-up scores low against its own conversation, while a different account's same-template status update scores high against it. So entity overlap is the decisive signal — messages that each name entities and share none are different conversations, however similar the phrasing, and no amount of recency rescues them. Same-author fast-follows and reply momentum carry entity-less utterances ("they pushed it") that would otherwise lose their antecedent. Centroid similarity is the soft signal in between, and an episode stops being a candidate after a few idle hours.
The bias is toward joining: chatter that wrongly joins an episode is harmless, because the admission gate drops it anyway, whereas a pronoun follow-up wrongly split loses its antecedent forever.
Episodes matter because they are where extraction gets its context. Reading a message against the last few messages of its own conversation resolves pronouns correctly; reading it against raw channel recency in a busy channel mixes three interleaved conversations and puts the wrong antecedent behind "they". Assignment takes a single advisory lock per conversation, so messages racing to join the same channel's episode are ordered — which is required to decide correctly — while different channels proceed fully in parallel.
Reprocessing is deterministic
Extraction reads a message as a delta against what its container already believes, and that state is cut at the message's own timestamp. A message is extracted against what was known when it was sent, so replaying a year-old backlog produces what it would have produced then, rather than being quietly informed by its own future.
One consequence is stated openly rather than buried: under concurrent workers, a message can be processed before its predecessors and see less state than it eventually would, so two runs over one corpus can produce different graphs. It fails soft in the only acceptable direction — a message extracted early resolves fewer subjects and produces fewer claims, never different ones, because every claim still has to come from the message's own words and still needs a grounded quote. Reprocessing converges it.
Permissions
How louvain computes what one person is allowed to see, why two people can correctly get different answers to the same question, and why a refusal looks exactly like a missing record.
Connections and connectors
How a data source becomes a permissioned feed — connection rows, sealed credentials, supervised runners, and per-connection ingest tokens.