What bitemporal state invariants must survive retroactive corrections to past events?
**Date of Record:** 2026 October 27 **Scope:** Applies when an agentic session maintains state with both valid-time (when the fact was true in the real world) and transaction-time (when the fact was recorded in the system), and a retroactive correction alters the valid-time interpretation of past events. Does not apply to strictly current-state systems where past interpretations are never revisited.
Core Distinction
The separation between **valid-time** (when the fact was true in the real world) and **transaction-time** (when the system recorded it). Relying on a single timestamp creates the **Temporal Conflation Trap**: when a retroactive correction changes the valid-time of a fact, the system cannot distinguish "what was true at time T" from "what the system believed at time T," so agents reasoning about the past either see the corrected truth (losing the audit trail) or the original belief (missing the correction).
What Was Observed
In temporal agent architectures, failing to preserve bitemporal distinction produces three primary failure modes:
- **Retroactive Rewrite:** A "transaction settled" event is retroactively corrected to have occurred a week earlier. The system rewrites the past, and any agent that already made decisions based on the original timeline cannot reconstruct its rationale.
- **Audit Collapse:** A regulatory audit asks "what did the agent believe on date X?" The system shows corrected data rather than the agent's original view, so the audit cannot reproduce the original decision.
- **Causal Disconnect:** An agent orders events by timestamp, but retroactive corrections reorder them, breaking the causal chains the agent relies on.
Recorded Elements
- **`valid_time_range`:** The interval during which the fact was true in the real world, independent of when it was recorded.
- **`transaction_time_range`:** The interval during which the system held this version of the fact, capturing the system's evolving belief.
- **`correction_lineage_receipt`:** For each correction, the cryptographic proof of what was corrected, why, and by whom, allowing audit of correction chains.
- **`query_time_mode_selector`:** The explicit declaration in each query of which time dimension (valid, transaction, or both) to use, preventing implicit conflation.
Condition of Validity
* **Applies when:** The session records facts that can be retroactively corrected, and downstream reasoning depends on either the real-world truth or the system's historical belief. * **Does not apply when:** The system never corrects past facts (append-only, no retroactive corrections), or the distinction between valid-time and transaction-time is irrelevant to the use case.
Decision Boundary
- **Does the fact being recorded have both valid-time and transaction-time semantics?** If no, standard timestamping applies. If yes, proceed.
- **Is the query explicit about which time dimension it uses?** If no, the query must be rejected or defaulted to a documented mode.
- **Has a retroactive correction been applied to this fact?** If yes, the correction must be preserved via `correction_lineage_receipt`, not overwritten.
- **Conversely, when can the system conflate valid-time and transaction-time?** Only when the fact is strictly append-only with no corrections, or when downstream reasoning is provably insensitive to the distinction.
Operational Effect
- **Temporal Auditability:** Enables audits to reconstruct both real-world truth and historical system belief.
- **Correction Discipline:** Corrections are recorded, not overwritten, preserving the lineage.
- **Query Clarity:** Forces explicit time-dimension selection, preventing silent conflation.
from dataclasses import dataclass
from enum import Enum, auto
import hashlib
import json
class TimeMode(Enum):
VALID_TIME = auto()
TRANSACTION_TIME = auto()
BOTH = auto()
class BitemporalVerdict(Enum):
SINGLE_TIMESTAMP = auto()
BITEMPORAL_QUERY_SAFE = auto()
IMPLICIT_CONFLATION = auto()
CORRECTION_OVERWRITE_RISK = auto()
@dataclass(frozen=True)
class BitemporalFact:
fact_id: str
valid_start: float
valid_end: float
transaction_start: float
transaction_end: float
has_correction_lineage: bool
@dataclass(frozen=True)
class BitemporalQuery:
fact_id: str
time_mode: TimeMode | None
@dataclass(frozen=True)
class BitemporalPolicy:
require_explicit_mode: bool
require_correction_lineage: bool
@dataclass(frozen=True)
class BitemporalManifest:
verdict: BitemporalVerdict
directive: str
manifest_digest: str
class BitemporalGuard:
@staticmethod
def _hash(data: str) -> str:
return hashlib.sha256(data.encode("utf-8")).hexdigest()
@classmethod
def evaluate_query(
cls, fact: BitemporalFact, query: BitemporalQuery, policy: BitemporalPolicy
) -> BitemporalManifest:
# Gate 1: Explicit mode required
if policy.require_explicit_mode and query.time_mode is None:
return BitemporalManifest(
verdict=BitemporalVerdict.IMPLICIT_CONFLATION,
directive=f"HALT: Query for {query.fact_id} lacks explicit time mode.",
manifest_digest=cls._hash(f"NO_MODE:{query.fact_id}")
)
# Gate 2: Correction lineage
if policy.require_correction_lineage and not fact.has_correction_lineage:
if fact.valid_start != fact.transaction_start:
return BitemporalManifest(
verdict=BitemporalVerdict.CORRECTION_OVERWRITE_RISK,
directive=f"HALT: Fact {fact.fact_id} has valid/transaction time divergence but no correction lineage.",
manifest_digest=cls._hash(f"NO_LINEAGE:{fact.fact_id}")
)
# Gate 3: Single timestamp fallback
if fact.valid_start == fact.transaction_start and fact.valid_end == fact.transaction_end:
digest = cls._hash(json.dumps({
"fact": fact.fact_id,
"valid": f"{fact.valid_start:.0f}-{fact.valid_end:.0f}",
"txn": f"{fact.transaction_start:.0f}-{fact.transaction_end:.0f}"
}, sort_keys=True))
return BitemporalManifest(
verdict=BitemporalVerdict.SINGLE_TIMESTAMP,
directive=f"PROCEED: Fact {fact.fact_id} has aligned timestamps.",
manifest_digest=digest
)
# Gate 4: Bitemporal safe
digest = cls._hash(json.dumps({
"fact": fact.fact_id,
"valid": f"{fact.valid_start:.0f}-{fact.valid_end:.0f}",
"txn": f"{fact.transaction_start:.0f}-{fact.transaction_end:.0f}",
"mode": query.time_mode.name if query.time_mode else "NONE",
"lineage": str(fact.has_correction_lineage)
}, sort_keys=True))
return BitemporalManifest(
verdict=BitemporalVerdict.BITEMPORAL_QUERY_SAFE,
directive=f"PROCEED: Bitemporal fact {fact.fact_id} with mode {query.time_mode.name if query.time_mode else 'NONE'}.",
manifest_digest=digest
)