Files
correx/docs/decisions/adr-0005-persistence-choice.md
T

114 lines
6.2 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
name: "Adr 0005 Persistence Choice"
description: "SQLite primary event store, deferred Redis/Elasticsearch"
depth: 2
links: ["../index.md", "../modules/core-events-submodule-spec.md"
---
# ADR 0005: persistence choice SQLite primary event store with deferred Redis and Elasticsearch
**status:** accepted
**date:** 07.05.2026 (May)
**deciders:** correx design team
---
## context
Correx requires an appendonly event store as the systems authoritative source of truth. The store must support:
* strict persession ordering and idempotent writes
* efficient replay via sequencerange scans
* atomic multievent transactions
* zerosetup, embedded operation for localfirst deployment
* a design that allows future migration to distributed backends
We evaluated SQLite, MongoDB, RocksDB, Redis, Kafka, and PostgreSQL against these requirements, with an emphasis on simplicity for v1 while preserving architectural flexibility.
## decision
**SQLite with WAL mode** is the primary event store for correx v1.
All events, snapshots, and materialised projections will be stored in a single local SQLite database. The `:core` modules will define repository interfaces (`EventStore`, `SnapshotStore`); `:infrastructure:persistence:sqlite` will provide implementations.
### rationale for SQLite
* **Embedded, zerosetup** critical for a localfirst tool; no external daemons.
* **WAL mode** allows concurrent reads from multiple processes (e.g., CLI, dashboard) while the orchestrator writes.
* **Singlefile portability** easy backup, archival, and replay.
* **Sufficient write performance** session events are written sequentially and rarely exceed a few thousand per session; SQLites write throughput is more than adequate.
* **Mature Kotlin/Java support** via `sqlite-jdbc` and optional query libraries (Exposed).
* **Familiarity** simpler mental model for contributors; SQL inspection during development is straightforward.
### event storage schema
A single `events` table:
| column | type | description |
|--------|------|-------------|
| `session_id` | text | UUID of the session |
| `sequence` | integer | strict monotonic ordering within session |
| `event_id` | text | globally unique event UUID |
| `timestamp` | text | ISO8601 creation time |
| `type` | text | event category and name |
| `payload` | text | JSON blob (schema versioned) |
| `causation_id` | text | parent event UUID |
| `correlation_id` | text | execution lineage identifier |
| `version` | integer | event schema version |
Index: `(session_id, sequence)` unique. Queries: range scans by session with cursor.
### snapshotting
**Trigger policy:** semantic, not purely countbased.
Snapshots are created after key “snapshotmaterial” events:
* `SessionInitialized`
* `StageCompleted`
* `ArtifactValidated`
* `ApprovalGranted`
* `SessionPaused`
plus a fallback **maxevent count** (default 200) to prevent replay of unbounded gaps. A minimum interval (30 seconds) prevents snapshot storms.
Snapshots are stored in a dedicated table, keyed by `(session_id, sequence)` and contain the serialized current projection state. They are always an optimisation; the event stream remains authoritative. Replay always applies events after the snapshot cursor, including any destructive or compensating events, so no state is silently lost.
### logevent correlation
All external logs (tool stdout/stderr, inference debug) carry the corresponding `event_id` and `correlation_id` in their output. Tool receipts stored as `ToolExecutionCompleted` event payloads include a file path to the raw log. Drift between logs and events is prevented by design: events are written atomically first; logs reference those events.
### future backends (v2+)
We explicitly note two additional data stores that are **not part of v1** but are planned as optional infrastructure modules later:
* **Redis** for inprocess pub/sub streaming of events to realtime dashboards, CLI progress, and live inspection. Redis is not a store of record; events remain in SQLite.
* **Elasticsearch** as a secondary readmodel for fulltext search across artifact summaries, rationales, and tool outputs, and for observability queries (e.g., “show all sessions where approval tier T3 was used”). Events are pushed asynchronously via a projection subscriber; Elasticsearch is not the primary store.
Both are optional, additive, and do not alter the core `EventStore` contract.
## consequences
**positive:**
* zerosetup local experience; the harness “just works” after install
* event store is inspectable with standard SQL tools during development and debugging
* semantic snapshotting ensures fast replay without capturing inconsistent intermediate states
* eventfirst design prevents log/event drift
* clean migration path to PostgreSQL, RocksDB, or distributed backends via the `EventStore` interface
**negative:**
* SQLites singlewriter limitation means concurrent sessions must be serialised at the persistence layer (acceptable for singlenode v1; future versions will require a different backend for distributed workers)
* JSON payloads in SQLite lack native JSONB indexing; complex queries over event payloads are slower than in PostgreSQL or Elasticsearch (mitigated by keeping queries simple: always by session + sequence range)
* snapshot material events must be chosen conservatively; if an important event type is omitted, replay may be slower than ideal (adjustable via config)
## alternatives considered
* **RocksDB (embedded KV):** excellent write throughput and natural appendonly model; rejected because SQLites inspection friendliness and ecosystem maturity better suit a small team iterating rapidly.
* **MongoDB / PostgreSQL / Kafka:** require external daemons, violating localfirst simplicity. They remain strong candidates for v2 distributed deployments.
* **Redis as primary store:** insufficient durability guarantees for sourceoftruth event storage; retained as a pub/sub transport for realtime events.
## status
This decision governs the v1 persistence architecture. Reevaluation is expected when distributed workers, highvolume concurrent sessions, or advanced fulltext search become hard requirements.