Issue #680. Promote temporal supersession to a first-class fact lifecycle that callers can query historically.
Remnic now persists when a fact is "true" with two explicit ISO 8601 frontmatter fields:
| Field | Meaning |
|---|---|
validAt |
When the fact begins being authoritative. |
invalidAt |
When the fact stops being authoritative (exclusive). |
A fact is considered authoritative for the half-open interval
[validAt, invalidAt). Both fields are optional. When validAt is
absent, the fact's created timestamp is used as a read-time fallback
so legacy memories written before #680 participate in as_of filtering
without a backfill migration. When invalidAt is absent, the fact is
considered authoritative through "now".
For memories with status: "superseded" written before #680 — which
have supersededAt populated but no invalidAt — the read-time
filter falls back to supersededAt as effectiveInvalidAt. This
keeps legacy predecessors out of historical recall once they were
clearly retired, without requiring a backfill.
The fallback is approximate: supersededAt reflects when the
supersession write fired, which can post-date the successor's true
validAt if consolidation ran on a delayed cadence. So a legacy
predecessor may appear in as_of recall slightly longer than its
successor's actual replacement instant. New data (post-#680) writes
invalidAt directly from the successor's validAt, so this
imprecision applies only to data written before the temporal-lifecycle
schema landed.
The temporal supersession pipeline (temporal-supersession.ts) already
detects when a newer fact replaces an older one on the same
(entityRef, attribute) pair. As of #680 it now also stamps the
predecessor's invalidAt:
- The successor's
validAtis copied verbatim onto the predecessor'sinvalidAtso the two facts dovetail at exactly the same instant. - When the successor has no explicit
validAt, the predecessor'sinvalidAtis set to the successor's persistedcreatedtimestamp. - An existing
invalidAton the predecessor is preserved (idempotent), so manual or earlier supersession events are not overwritten.
Every recall surface accepts an as_of ISO 8601 timestamp:
| Surface | How to pass as_of |
|---|---|
| CLI | remnic recall "<query>" --as-of <iso> |
| HTTP | ?as_of=<iso> on POST /engram/v1/recall, or asOf in the JSON body |
| MCP | asOf field on the engram.recall tool |
Each surface validates the timestamp at the input boundary
(Date.parse) and rejects malformed values with a structured error.
There is no silent fallback (CLAUDE.md rule 51).
When as_of is set, recall drops candidates that were not
authoritative at that instant — i.e. those where
effectiveValidAt(fm) > asOf OR
effectiveInvalidAt(fm) !== undefined && effectiveInvalidAt(fm) <= asOf.
The upper bound is exclusive so a fact's exact end-of-life timestamp
hides it (CLAUDE.md gotcha #35: time-range filters use exclusive
upper bounds).
A user moved from Austin to NYC on 2026-04-01.
# facts/preferences.md
---
id: 01HXY...
created: 2025-01-15T10:00:00.000Z
validAt: 2025-01-15T10:00:00.000Z
invalidAt: 2026-04-01T00:00:00.000Z
status: superseded
supersededBy: 01J0AB...
entityRef: project-x
structuredAttributes:
city: Austin
---
project X is based in Austin# facts/preferences.md (newer fact, after supersession)
---
id: 01J0AB...
created: 2026-04-01T00:00:00.000Z
validAt: 2026-04-01T00:00:00.000Z
entityRef: project-x
structuredAttributes:
city: NYC
---
project X relocated to NYCA normal remnic recall "where is project X based?" returns the NYC
fact (the Austin fact is filtered as superseded).
A historical recall pinned to the day before the move returns the Austin fact:
remnic recall "where is project X based?" --as-of 2026-03-31T00:00:00Z
# → "project X is based in Austin" (validAt <= asOf < invalidAt)A recall pinned exactly at the supersession instant returns the NYC
fact (because invalidAt is exclusive):
remnic recall "where is project X based?" --as-of 2026-04-01T00:00:00Z
# → "project X relocated to NYC"This slice does NOT include:
- Mass migration / backfill of
validAtonto existing files. The read-time fallback tocreatedcovers legacy data. - Vector clocks, branching timelines, or cross-fact causality.
- A new
temporal-supersessionpolicy. The existing pipeline is unchanged apart from emittinginvalidAton the predecessor.
See the test suite at
packages/remnic-core/src/temporal-validity.test.ts for the canonical
boundary cases.