Operon v0.19: Bi-Temporal Memory
Append-Only Facts, Dual Time Axes, and Belief-State Reconstruction for Auditable Agent Systems
Release: v0.19.0
v0.18 gave operon a thinner front door. v0.19 goes deeper in
the opposite direction: it adds a memory subsystem that tracks
not just what is true, but when it became true and when the
system learned about it. These are two different questions, and
conflating them is the source of a whole class of bugs in
agent systems that ingest delayed or corrected information.
The new BiTemporalMemory is append-only —
corrections close old records and create new ones, so any
past belief state can always be reconstructed. This is the
foundation for compliance auditing, temporal reasoning, and
eventually richer forms of agent self-knowledge.
1. The Problem with a Single Timestamp
Most agent memory systems give each fact a single timestamp: when was this stored? That works fine as long as the system learns about things exactly when they happen. In practice, that is almost never the case.
A client’s risk tier changes on Monday. The CRM sync runs on Wednesday. A manual review on Friday discovers the tier should have been “high,” not “medium.” Now there are three different moments in play, and a single timestamp cannot distinguish between them.
This is not a niche scenario. It is the norm in compliance, healthcare, finance, and anywhere else where facts arrive with delay or get corrected retroactively. And it matters for agents because the question “what did the system believe when it made that decision?” is not the same as “what is true now?”
The Core Distinction
Valid time is when a fact is true in the world. Record time is when the system learned about it. These diverge whenever information arrives late or gets corrected — and an agent that conflates them cannot explain its own past decisions.
2. Bi-Temporal Data in Databases
The idea is not new. Snodgrass formalized bi-temporal data
models in the 1990s, and SQL:2011 added standard syntax for
temporal tables with PERIOD FOR and
AS OF semantics. What is newer is bringing this
into the agent memory layer directly, where the two time
axes govern what the agent can retrieve and reason about at
any given point.
The implementation in operon is deliberately simple. It is an in-memory append-only store with no database dependency. The point is not to compete with temporal databases. The point is to give agent workflows the right retrieval semantics without requiring external infrastructure.
3. The Data Model
A BiTemporalFact is a frozen dataclass with
two time intervals:
BiTemporalFact(
fact_id="a1b2c3d4",
subject="client:42",
predicate="risk_tier",
value="medium",
valid_from=day1, # when the fact became true in the world
valid_to=None, # None = still true (open-ended)
recorded_from=day3, # when the system ingested it
recorded_to=None, # None = currently active record
source="crm",
confidence=1.0,
tags=(),
supersedes=None, # points to the fact this corrects
)
The frozen dataclass is a deliberate departure from the mutable dataclasses used elsewhere in operon’s memory layer. The justification is simple: in an append-only store, corrections produce new facts rather than mutating existing ones. Immutability is not a constraint here — it is the semantics.
| Type | Purpose |
|---|---|
BiTemporalFact | Immutable fact with dual time intervals |
BiTemporalQuery | Filter spec for point-in-time queries |
BiTemporalMemory | Mutable store with append-only write semantics |
FactSnapshot | Query result container |
CorrectionResult | Result of correct_fact() |
4. Write Semantics: Record, Correct, Never Mutate
The store exposes two write operations.
record_fact() appends a new fact.
correct_fact() closes the old record by
setting its recorded_to and appends a new
fact with supersedes pointing back to the
original.
from operon_ai import BiTemporalMemory
mem = BiTemporalMemory()
# Day 1: risk tier becomes "medium" in the world
# Day 3: system ingests it
fact = mem.record_fact("client:42", "risk_tier", "medium",
valid_from=day1, recorded_from=day3, source="crm")
# Day 5: manual review corrects to "high", retroactive to day 1
correction = mem.correct_fact(fact.fact_id, "high",
valid_from=day1, recorded_from=day5,
source="manual_review")
The old fact is never deleted or overwritten. Its
recorded_to is set to the correction time,
closing it on the record-time axis. The new fact carries a
fresh fact_id and an open
recorded_to. Both coexist in the store. This
is what makes past belief states reconstructible.
Why Append-Only
If you mutate facts in place, you lose the ability to answer “what did the system believe at decision time?” That question is the foundation of compliance auditing. Append-only semantics make it structurally impossible to lose that information.
5. Three Retrieval Questions
The dual time axes give rise to three distinct retrieval modes, each answering a different question:
What is true at time T?
# Active records valid at day 2
mem.retrieve_valid_at(at=day2, subject="client:42") # → "high"
This filters on the valid-time axis and only returns
currently active records (those with open
recorded_to). It reflects the corrected
world-state.
What had the system recorded by time T?
# What was known by day 2?
mem.retrieve_known_at(at=day2, subject="client:42") # → [] (nothing yet)
This filters on the record-time axis only. It answers “what information had arrived by this point?” without judging whether it was valid. Since the fact was not ingested until day 3, the result is empty.
What did the system believe at time T?
# Belief state: valid at day 2, known by day 4
mem.retrieve_belief_state(at_valid=day2, at_record=day4) # → "medium"
# Same question, but known by day 6 (after correction)
mem.retrieve_belief_state(at_valid=day2, at_record=day6) # → "high"
This is the intersection: facts that were both valid at
at_valid and recorded by at_record.
It reconstructs exactly what the agent would have believed
at any past moment. The first query returns
“medium” because the correction had not arrived
yet. The second returns “high” because it had.
6. History, Diff, and Timeline
Three more methods support auditing and debugging:
history(subject)— all facts for a subject, including closed records, sorted by record time. Shows the full correction chain.diff_between(t1, t2, axis)— facts that appear att2but nott1on the specified axis. Useful for “what changed between these two points?”timeline_for(subject)— all facts sorted by valid time. Shows the world-time narrative including superseded records.
# Full correction history
mem.history("client:42")
# What changed on the record-time axis between day 4 and day 6?
mem.diff_between(day4, day6, axis="record")
# World-time narrative
mem.timeline_for("client:42")
7. Relationship to Existing Memory Systems
Operon already has two memory subsystems, and bi-temporal memory is not a replacement for either. They solve different problems:
| System | Time Model | Mutation | Use Case |
|---|---|---|---|
HistoneStore |
Single timestamp | Mutable marks | Epigenetic metadata on retrieval |
EpisodicMemory |
Single timestamp | Mutable (decay, promote) | Tiered memory with relevance scoring |
BiTemporalMemory |
Dual timestamps | Append-only | Auditable fact tracking with corrections |
HistoneStore annotates retrievals with
epigenetic marks — access frequency, recency, context.
EpisodicMemory manages tiered decay and
promotion between working, short-term, and long-term memory.
Neither tracks corrections or supports dual-axis retrieval.
BiTemporalMemory fills a different niche:
domains where the audit trail is the point.
8. Article Updates
v0.19 also updates the research article in four places:
- Related work (Section 2): new subsection on temporal databases and bi-temporal data models, citing Snodgrass and SQL:2011.
- Categorical mapping (Section 3): new paragraph formalizing the dual-interval state space as temporal coalgebra.
- Discussion (Section 6): new subsection on temporal epistemics — what did the agent know, and when did it know it?
- Implementation (Section 8): new subsection documenting the
BiTemporalMemoryimplementation.
The article is built with tectonic. The temporal coalgebra formalization is the most interesting addition: it describes how the dual time intervals form a product of interval domains, and how corrections trace paths through that product space.
9. Validation
| Suite | Tests | Status |
|---|---|---|
| Bi-temporal memory unit tests | 41 | All pass |
| Example 69 smoke path (valid vs record divergence) | 1 | Passes |
| Example 70 smoke path (compliance audit) | 1 | Passes |
| Full regression suite at release | 928+ | All pass |
operon_ai/memory/bitemporal.py,
examples/69_bitemporal_memory.py,
examples/70_bitemporal_compliance_audit.py
10. What Comes Next
Bi-temporal memory is Phase 1 of a longer roadmap. The next phases build on this foundation: causal event logging that tracks why decisions were made, not just what facts were available; persistent storage backends so the append-only log survives process restarts; and eventually temporal reasoning primitives that let agents query their own epistemic history as part of planning.
The deeper motivation is that agents that cannot explain their past decisions are agents you cannot trust with consequential ones. Bi-temporal memory is the first step toward that kind of accountability — not as a governance layer bolted on after the fact, but as a structural property of the memory system itself.
Code and release: github.com/coredipper/operon, operon-ai on PyPI, bi-temporal memory docs