How the Q00/ouroboros EventStore Reconstructs Lineage Across Restarts: Event Sourcing Deep Dive
The Q00/ouroboros EventStore reconstructs lineage across restarts by replaying persisted "lineage" aggregate events from SQLite and folding them through a deterministic LineageProjector to rebuild the immutable OntologyLineage state.
When the Ouroboros process stops, the evolutionary history of an ontology—its lineage—is lost from memory. To ensure continuity, the Q00/ouroboros repository implements an event-sourced persistence model where the EventStore serves as the single source of truth. This article explains exactly how the EventStore reconstructs lineage across restarts by replaying events from src/ouroboros/persistence/schema.py and projecting them through LineageProjector.
The Event Sourcing Foundation for Lineage Persistence
Unified Event Storage in SQLite
All domain events, including lineage mutations, share a single append-only table defined in src/ouroboros/persistence/schema.py:
# src/ouroboros/persistence/schema.py
events_table = Table(
"events",
metadata,
Column("id", String(36), primary_key=True),
Column("aggregate_type", String(100), nullable=False),
Column("aggregate_id", String(36), nullable=False),
Column("event_type", String(200), nullable=False),
Column("payload", JSON, nullable=False),
Column("timestamp", DateTime(timezone=True), nullable=False, …),
)
Lineage-specific events are distinguished by aggregate_type = "lineage" and aggregate_id = <lineage-id>, with event_type values such as lineage.created, lineage.generation.started, lineage.rewound, and terminal states like lineage.converged.
The store writes them with EventStore.append and append_batch (see lines 71‑92 and 100‑112 of event_store.py).
Appending Lineage Events
Before reconstruction can occur, events must be persisted using the BaseEvent class from src/ouroboros/events/base.py:
event = BaseEvent(
id=str(uuid4()),
aggregate_type="lineage",
aggregate_id="lin_123abc",
type="lineage.created",
data={"goal": "Create a personal finance ontology"},
timestamp=datetime.now(UTC),
)
await store.append(event)
Replaying Lineage Events on Startup
The replay_lineage Method
When Ouroboros restarts, the EventStore reconstructs state by replaying only the events belonging to a specific lineage aggregate. The entry point is replay_lineage in src/ouroboros/persistence/event_store.py:
# src/ouroboros/persistence/event_store.py
async def replay_lineage(self, lineage_id: str) -> list[BaseEvent]:
"""Replay all events for a lineage aggregate."""
return await self.replay("lineage", lineage_id) # ← lines 414‑428
This delegates to the generic replay method, which queries the SQLite events table filtering on aggregate_type and aggregate_id.
Deterministic Event Ordering
To guarantee that reconstruction yields the exact same state across every restart, the replay method orders results by timestamp, id (lines 66‑73 of event_store.py):
-- Conceptual SQL generated by the replay method
SELECT * FROM events
WHERE aggregate_type = ? AND aggregate_id = ?
ORDER BY timestamp, id
This deterministic ordering ensures that the LineageProjector processes events in the precise sequence they were originally emitted, preserving causal relationships between generations, rewinds, and terminal states.
Projecting Events into Domain State
The LineageProjector.fold Logic
Raw events must be transformed into the domain model. The LineageProjector in src/ouroboros/evolution/projector.py implements a left-fold (reduce) operation that iterates through the event list and builds an OntologyLineage instance:
# src/ouroboros/evolution/projector.py
class LineageProjector:
def project(self, events: list[BaseEvent]) -> OntologyLineage | None:
if not events:
return None # lines 43‑44
lineage: OntologyLineage | None = None
generations: dict[int, GenerationRecord] = {}
rewind_history: list[RewindRecord] = []
for event in events: # lines 50‑66
if event.type == "lineage.created":
lineage = OntologyLineage(
lineage_id=event.aggregate_id,
goal=event.data.get("goal", ""),
created_at=event.timestamp,
)
elif event.type == "lineage.generation.started":
# build a pending GenerationRecord
pass
elif event.type == "lineage.generation.completed":
# build a completed GenerationRecord
pass
elif event.type == "lineage.generation.failed":
# mark failure
pass
elif event.type == "lineage.rewound":
# record rewind & truncate generations
pass
elif event.type in {"lineage.converged",
"lineage.exhausted",
"lineage.stagnated"}:
lineage = lineage.with_status(...)
# after the loop build the final immutable model
sorted_records = tuple(generations[k] for k in sorted(generations))
return lineage.model_copy(
update={"generations": sorted_records,
"rewind_history": tuple(rewind_history)},
)
The projector maintains three accumulators: the root lineage object, a dictionary of generations, and a list of rewind_history. It pattern-matches on event.type to apply the appropriate state transition, ensuring that rewinds properly truncate generation history and terminal events update the lineage status.
Immutability and the OntologyLineage Model
The domain model itself is defined in src/ouroboros/core/lineage.py as an immutable data structure. The projector never mutates the stored events; instead, it creates a fresh OntologyLineage instance that represents the cumulative effect of the entire event stream. This aligns with Event Sourcing principles where the event log is the single source of truth and domain objects are ephemeral projections.
Complete Startup Flow Example
The following snippet demonstrates the exact sequence used by Ouroboros to reconstruct lineage after a restart:
from ouroboros.persistence.event_store import EventStore
from ouroboros.evolution.projector import LineageProjector
async def load_lineage(lineage_id: str) -> OntologyLineage | None:
store = EventStore() # default ~/.ouroboros/ouroboros.db
await store.initialize()
events = await store.replay_lineage(lineage_id) # ← fetch persisted events
projector = LineageProjector()
lineage = projector.project(events) # ← reconstruct state
await store.close()
return lineage
If the process crashes or is stopped, executing load_lineage on the next run will re‑play the exact same event sequence, guaranteeing that the lineage is restored faithfully without data loss.
Summary
- Event Store as Source of Truth: The
eventstable insrc/ouroboros/persistence/schema.pystores all lineage mutations as immutable records withaggregate_type="lineage". - Deterministic Replay: The
EventStore.replay_lineagemethod insrc/ouroboros/persistence/event_store.pyqueries events ordered bytimestamp, idto ensure consistent reconstruction order. - Stateless Projection: The
LineageProjector.projectmethod insrc/ouroboros/evolution/projector.pyfolds the event stream into a freshOntologyLineageinstance without database side effects. - Crash Safety: Because reconstruction happens entirely from the persisted event log, stopping or crashing the process never corrupts lineage state; the next startup simply replays the log to reach the same deterministic state.
Frequently Asked Questions
What happens if the SQLite database is corrupted or deleted?
If the SQLite database file (default ~/.ouroboros/ouroboros.db) is lost or corrupted, the event log is destroyed and lineage reconstruction becomes impossible. Ouroboros treats the event store as the sole source of truth, so proper backup strategies for the SQLite file are essential for production deployments.
How does the EventStore handle concurrent writes to the same lineage?
The EventStore.append and append_batch methods in src/ouroboros/persistence/event_store.py utilize SQLite's transaction isolation. Because SQLite handles file-level locking and the event store appends are atomic operations, concurrent processes writing to the same lineage aggregate will be serialized by the database engine, preventing event loss or ordering corruption.
Can the lineage reconstruction process be optimized for large event logs?
Currently, the replay_lineage method loads all events for a given lineage into memory before projection. For lineages with thousands of generations, you could implement snapshotting by persisting periodic checkpoints of the OntologyLineage state and modifying the projector to load only events after the latest snapshot. However, the current implementation in src/ouroboros/evolution/projector.py performs a full fold from the beginning of the event stream.
What is the difference between lineage.rewound and lineage.generation.failed events?
A lineage.rewound event represents an intentional rollback to a previous generation, triggering the LineageProjector to truncate the generations dictionary and record a RewindRecord. In contrast, a lineage.generation.failed event marks a specific generation attempt as unsuccessful without altering the lineage history; the failed generation remains in the record as a terminal state for that specific attempt, allowing the system to retry or abandon that path without discarding prior successful work.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →