What invariants must survive when a session is rebuilt from an event-sourced log with gaps?
**Date of Record:** 2026 October 27 **Scope:** Applies when an agentic session reconstructs its state by replaying an event-sourced log, and the log has gaps (missing events due to storage failure, retention policy, or partial replication), so the replayed state is incomplete but the gaps may not be obvious. Does not apply to strictly complete event logs or snapshot-based reconstruction that sidesteps replay.
Core Distinction
The separation between **event replay success** (the log was replayed without parse errors) and **event replay completeness** (the log contained all events needed to reconstruct the correct state). Relying on success creates the **Silent Gap Reconstruction**: replay succeeds, the reconstructed state looks syntactically valid, but the gaps mean the state is missing critical events, and downstream actions are based on incorrect state.
What Was Observed
In event-sourced agent architectures, failing to detect log gaps produces three primary failure modes:
- **Missing Business Events:** A payment event is missing from the log, the reconstructed state shows an unpaid order, and the agent re-executes the payment, double-charging the customer.
- **Broken Causality:** A "Shipment Dispatched" event exists but the "Order Placed" event that it depends on is missing. Replay either fails on the dependency or silently treats the shipment as the order's creation.
- **Schema Version Skew:** The log contains events from an old schema version, but the gap was in a version transition event. Replay silently coerces the old events with default values, losing the transition semantics.
Recorded Elements
- **`event_offset_continuity_receipt`:** A cryptographic proof of the log's offset continuity (each offset from `min` to `max` is accounted for, either as present or as a declared gap), signed by the log source.
- **`gap_declaration_signature`:** For each gap, a signed declaration of the gap's scope and reason (e.g., "offsets 500-510 lost due to retention"), allowing the replay to distinguish intentional gaps from corruption.
- **`causal_completeness_receipt`:** A cryptographic proof that replaying the log yielded all events whose causal dependencies were satisfied, avoiding partial-state assumptions.
- **`post_replay_state_attestation`:** The cryptographic hash of the replayed state, compared against a known-good state at the log's highest offset, allowing detection of divergence.
Condition of Validity
* **Applies when:** The session reconstructs state by replaying event logs, and the logs can have gaps (intentional or not). * **Does not apply when:** The log is strictly complete (no gaps possible), or the session reconstructs from atomic snapshots rather than event replay.
Decision Boundary
- **Is the log being replayed?** If no, standard state reconstruction applies. If yes, proceed.
- **Does the `event_offset_continuity_receipt` show gaps?** If yes, the replay must distinguish intentional gaps (declared) from corruption (undeclared).
- **Are all gaps covered by `gap_declaration_signature`?** If yes, the reconstruction can proceed with `PARTIAL_STATE_ACKNOWLEDGED`. If no, the reconstruction is compromised. Halt.
- **Conversely, when can a session accept a gap without declaration?** Only when the gap is on events the session has no dependency on (e.g., unrelated parallel branch), and the `post_replay_state_attestation` proves the reconstructed state matches the expected state at the log's current head.
Operational Effect
- **Silent Corruption Prevention:** Distinguishes declared gaps from undeclared corruption, preventing reconstruction from silent state errors.
- **Causal Correctness:** Verifies that all causal dependencies were satisfied, avoiding replay into phantom states.
- **Divergence Detection:** Compares post-replay state against expected state, catching subtle divergence caused by missing events.
from dataclasses import dataclass
from enum import Enum, auto
import hashlib
import json
class GapVerdict(Enum):
COMPLETE_LOG = auto()
DECLARED_GAPS_ONLY = auto()
UNDECLARED_GAPS_DETECTED = auto()
STATE_DIVERGENCE = auto()
@dataclass(frozen=True)
class LogGap:
start_offset: int
end_offset: int
declared: bool
declaration_signature: str | None
@dataclass(frozen=True)
class EventLogState:
min_offset: int
max_offset: int
gaps: tuple[LogGap, ...]
post_replay_state_hash: str
expected_state_hash: str
@dataclass(frozen=True)
class LogPolicy:
require_declaration_for_gaps: bool
@dataclass(frozen=True)
class GapManifest:
verdict: GapVerdict
directive: str
manifest_digest: str
class EventLogGapGuard:
@staticmethod
def _hash(data: str) -> str:
return hashlib.sha256(data.encode("utf-8")).hexdigest()
@classmethod
def evaluate_replay(
cls, state: EventLogState, policy: LogPolicy
) -> GapManifest:
# Gate 1: No gaps
if not state.gaps:
digest = cls._hash(json.dumps({
"min": str(state.min_offset),
"max": str(state.max_offset),
"gaps": "NONE"
}, sort_keys=True))
return GapManifest(
verdict=GapVerdict.COMPLETE_LOG,
directive="PROCEED: Log is complete, no gaps.",
manifest_digest=digest
)
# Gate 2: Undeclared gaps
for gap in state.gaps:
if not gap.declared and policy.require_declaration_for_gaps:
return GapManifest(
verdict=GapVerdict.UNDECLARED_GAPS_DETECTED,
directive=f"HALT: Undeclared gap at offsets {gap.start_offset}-{gap.end_offset}.",
manifest_digest=cls._hash(f"UNDECLARED:{gap.start_offset}-{gap.end_offset}")
)
# Gate 3: State divergence
if state.post_replay_state_hash != state.expected_state_hash:
return GapManifest(
verdict=GapVerdict.STATE_DIVERGENCE,
directive=f"HALT: Replayed state {state.post_replay_state_hash} != expected {state.expected_state_hash}.",
manifest_digest=cls._hash(f"DIVERGED:{state.post_replay_state_hash}")
)
# Gate 4: Declared gaps only
digest = cls._hash(json.dumps({
"gaps": tuple((g.start_offset, g.end_offset) for g in state.gaps),
"state": state.post_replay_state_hash
}, sort_keys=True))
return GapManifest(
verdict=GapVerdict.DECLARED_GAPS_ONLY,
directive=f"PROCEED: {len(state.gaps)} declared gaps, state verified.",
manifest_digest=digest
)