epic-12: after epic audit and init commit

This commit is contained in:
2026-05-16 11:42:00 +04:00
commit c77277af0b
461 changed files with 28958 additions and 0 deletions
+241
View File
@@ -0,0 +1,241 @@
---
name: "Adr 0000 Invariants"
description: "Foundational architectural invariants of the Correx system"
depth: 2
links: ["../index.md", "../architecture/overview.md", "../decisions/adr-0001-event-sourcing.md"]
---
# ADR 0000: architectural invariants of the correx system
**status:** accepted
**date:** 08.05.2026
**deciders:** Kami
---
## context
Correx is an event-sourced orchestration system with deterministic replay requirements, LLM-driven inference, and multi-layered execution (validation, approvals, tools, context synthesis, and transitions).
Without explicit invariants, the system risks:
* implicit coupling between modules
* nondeterministic behavior creeping into core logic
* inconsistent interpretation of event streams across bounded contexts
* broken replay guarantees under evolution of modules
This ADR defines the **hard architectural rules** that all other ADRs and implementation decisions must obey.
These invariants are not suggestions. They are constraints on system evolution.
---
## invariants
### 1. event log is the single source of truth
All system state MUST be derivable from the event stream.
* no hidden mutable state is allowed to influence execution outcomes
* all decisions, approvals, transitions, and tool executions MUST emit events
* projections are disposable and rebuildable at any time
implication:
* caching is allowed only if it is derivable from events
* any system behavior not reproducible from events is a bug
---
### 2. projections are bounded-context owned
Each bounded context owns its own projection(s).
* `sessions` owns session state projection
* `artifacts` owns artifact state projection
* `context` owns context assembly projection
* `validation` owns validation state projection
projections:
* MAY NOT mutate each other
* MAY NOT be shared as a global mutable model
* MAY be reconstructed independently from the event stream
implication:
* there is no “global system state object”
* cross-domain reasoning must go through events or read-only queries
---
### 3. deterministic core vs nondeterministic inputs separation
The system is split into two layers:
**deterministic core:**
* event handling
* state transitions
* approval logic
* validation routing decisions
* scheduling
**nondeterministic layer:**
* LLM inference
* tool output
* user input
* external system responses
rule:
* nondeterministic outputs MAY influence events
* but ONLY deterministic core may commit events
implication:
* LLMs propose; core decides
* tools suggest; core validates and records
---
### 4. no cross-module implementation dependencies inside core (outdated)
Inside `:core`, modules MAY depend only on:
* `:core:contracts` (or equivalent shared model module)
* or strictly lower-level primitives
forbidden:
* `context → validation`
* `validation → approvals`
* `router → kernel internals`
allowed:
* interaction only via:
* events
* interfaces
* explicit orchestration entrypoints
implication:
* core is a DAG, not a mesh
---
### 5. event causality is strictly linear per session
Within a single session:
* every event MUST have exactly one causation parent (except root)
* sequence ordering is total and monotonic
* correlation IDs define logical grouping but do not define ordering
implication:
* no ambiguous causality graphs
* replay is deterministic and linear
---
### 6. policy layer is absolute and non-overridable
Policy evaluation is the highest authority layer.
* approvals cannot override policy denial
* validation cannot bypass policy constraints
* tool execution cannot ignore policy results
policy failure is terminal for the operation path.
implication:
* approval system is not a security boundary; policy is
---
### 7. tool execution is sandbox-first by default
All tools:
* MUST declare execution tier
* MUST be assumed unsafe until validated
* MUST produce structured receipts
side effects:
* are only valid if captured in events
implication:
* “silent execution” is not allowed in system design
---
### 8. compression and summarization are non-authoritative
Any compressed, summarized, or derived representation:
* MUST NOT replace original events or artifacts
* MUST be reproducible from source data
* MUST be explicitly marked as derived
implication:
* no lossy compression in the source-of-truth layer
* only in read models
---
### 9. LLM outputs are untrusted until validated
Any output from inference:
* is considered a proposal
* cannot affect state unless validated and approved
* cannot bypass schema validation
implication:
* prompt injection is structurally contained, not just filtered
---
### 10. replay must be environment-independent
Replaying a session:
* MUST NOT depend on current system state
* MUST NOT depend on external services
* MUST NOT require live LLM calls (unless explicitly in “live replay mode”)
implication:
* full determinism is a first-class requirement, not a debug feature
---
## consequences
**positive:**
* system behavior remains consistent under evolution
* replay and debugging are guaranteed properties, not best-effort
* module boundaries become enforceable in code and architecture
* future distributed execution becomes feasible without redesign
**negative:**
* requires discipline in module design (especially core dependencies)
* increases upfront design overhead for new components
* forces strict separation between “thinking” and “executing” layers
---
## status
This ADR is foundational. All future ADRs are subordinate to these invariants. Any contradiction requires revision of this document before implementation proceeds.
+58
View File
@@ -0,0 +1,58 @@
---
name: "Adr 0001 Event Sourcing"
description: "Decision to use event sourcing as primary persistence model"
depth: 2
links: ["../index.md", "../architecture/event-model.md", "./adr-0000-invariants.md"]
---
# ADR 0001: use event sourcing as the foundational state model
**status:** accepted
**date:** 07.05.2026 (May)
**deciders:** Kami
---
## context
Correx orchestrates workflows driven by nondeterministic language models. To achieve reliability, we need:
* full traceability of every decision
* the ability to replay and debug sessions offline
* deterministic recovery after crashes
* a way to generate synthetic training data from execution history
Traditional mutablestate architectures would make debugging a nightmare: hidden state, race conditions, and the inability to step backward would undermine the core promise of “deterministicenough” execution.
## decision
We adopt **event sourcing** as the fundamental persistence and state management pattern.
* All workflow state is derived from an appendonly log of immutable events.
* Projections (the current state of a session) are rebuilt from events on demand or via snapshots.
* No mutable memory exists outside the event stream.
## consequences
**positive:**
* Complete auditability: every state transition, approval, and validation is recorded.
* Replayability: a session can be rerun from scratch with different models or in “dry” mode.
* Recovery: if the Harness crashes, we can resume by replaying the event log.
* Testability: orchestration logic can be tested with fabricated event streams.
* Synthetic dataset generation: replay can feed other models or evaluations.
**negative:**
* Eventual storage growth; mitigated by snapshots and configurable retention policies.
* Complexity in designing projections and replay logic; mitigated by clear module boundaries (`:core:events`, `:core:projections`).
* Event schema evolution must be handled deliberately; versioning and migration tooling needed.
## alternatives considered
* **Mutable relational state with audit tables**: would still need manual change tracking, harder to guarantee deterministic replay.
* **Op log in a distributed database**: overkill for localfirst operation; introduces ordering complexity.
## status
This decision is foundational and will not be revisited without a complete architectural overhaul.
+57
View File
@@ -0,0 +1,57 @@
---
name: "Adr 0002 Kotlin Choice"
description: "Decision to implement in Kotlin"
depth: 2
links: ["../index.md", "./adr-0000-invariants.md"]
---
# ADR 0002: use kotlin as the primary implementation language
**status:** accepted
**date:** 07.05.2026 (May)
**deciders:** Kami
---
## context
Correx is a stateheavy, eventheavy, concurrencyheavy orchestration runtime. The language choice affects maintainability, type safety, concurrency model, and ecosystem compatibility for a localfirst tool.
## decision
We implement the core, orchestration, and infrastructure layers in **Kotlin** (JVM), using the following primary libraries:
* **coroutines** structured concurrency
* **kotlinx.serialization** event/artifact serialization
* **Exposed** typesafe SQL for persistence
* **Ktor** web/API server
* **Hoplite** / Typesafe Config configuration
Frontend interface uses **SvelteKit**; CLI uses **Clikt**.
## rationale
1. **Sealed hierarchies and data classes** map naturally to event types, artifact schemas, and validation outcomes.
2. **Coroutines** provide safe, cancellable concurrency that aligns with the session lifecycle (cancellation tokens, explicit scopes).
3. **Immutability by default** reduces accidental state mutation critical for event sourcing.
4. **Typesafe DSLs** enable clean definition of transitions, conditions, and tool contracts directly in config or code.
5. The existing team knows Java/Kotlin, reducing rampup.
6. Libraries like `kotlinx.serialization` give deterministic, reflectionfree serialization.
7. Strong typing prevents many categories of bugs that would be hard to catch in a dynamic language when dealing with complex state machines.
## alternatives considered
* **Python**: excellent for glue and rapid prototyping, but dynamic typing and global interpreter lock make deterministic concurrency, state integrity, and longterm maintenance riskier. Python was used in the failed `llm-pipeline` prototype and contributed to hidden mutable state issues.
* **TypeScript (Node.js)**: singlethreaded event loop simplifies some concurrency but lacks the typesafety and structured concurrency needed for complex orchestration state machines. Validation and serialization would rely on thirdparty libraries with varying degrees of strictness.
* **Rust**: performance and safety are excellent, but the borrow checker and ecosystem immaturity for LLM tooling would slow development significantly.
## consequences
* JVM startup overhead; acceptable for a tool that runs as a persistent server or longlived CLI.
* Requires Kotlin knowledge; mitigated by good documentation and a growing community.
* Interop with native libraries (e.g., llama.cpp) will be via JNI or external processes; we accept that complexity and encapsulate it in infrastructure providers.
* We must enforce module dependency rules (with tools like ArchUnit) to prevent core from depending on infrastructure.
## status
This decision is foundational and aligns with the architectural need for a “workflow engine” rather than a “scripting environment”.
@@ -0,0 +1,103 @@
---
name: "Adr 0003 Compression Strategy"
description: "Rulebased compression, no summariser model for v1"
depth: 2
links: ["../index.md", "../architecture/context-layers.md"]
---
# ADR 0003: adopt rulebased compression with structured decision points; defer modelbased summarisation
**status:** accepted
**date:** 07.05.2026 (May)
**deciders:** Kami
---
## context
Correx must feed large volumes of tool output, conversation history, artifacts, and event streams into limited local model context windows. Raw accumulation destroys performance, especially for smaller models. We need a compression strategy that:
* is deterministic and replaysafe
* does not itself require a large or separate model (for v1)
* preserves decisioncritical context, not just surface summaries
* works across multiple data sources (tools, chat, artifacts, events)
## decision
We adopt a **100% rulebased compression system** for v1. No summariser model will be used for compression, even as an option.
The four data sources will be handled as follows:
### 1. tool output RTKstyle pipeline
All shell and tool command output is compressed using a configurable pipeline:
1. **smart filter** strip comments, whitespace, and known boilerplate
2. **group** aggregate similar items (e.g., files by directory, errors by type)
3. **deduplicate** collapse repeated identical lines with occurrence counts (`Error: timeout (×14)`)
4. **truncate** keep first N and last M lines; discard middle unless marked critical
On command failure, the full unfiltered output is saved to disk and only a pointer path is included in the compressed receipt. The model may request the raw log if needed.
Userdefined tools can declare their own compression patterns (regex + fallback) directly in the tool definition, not in a global config. This ties compression ownership to the tool.
### 2. conversation history manual compaction with microcompaction
For Router ↔ User conversations:
* the last N messages are kept verbatim (configurable; default N = 6)
* **microcompaction** at ~6070% context pressure silently clears old tool outputs, keeping conversation intact
* **manual compaction** (like `/compact`) at logical breakpoints summarises older conversation into a brief paragraph, preserving action items and user intent
* users may **pin individual messages** to prevent their removal during compaction
No automatic summarisation of conversation will occur; compaction is only triggered explicitly or when token pressure requires microcompaction.
### 3. artifacts chunking + summary field
Large artifacts (e.g., plans) are broken into chunks; only the current step ± 1 neighbor is kept in context. Remaining chunks are stored on disk with a file pointer left in the artifact.
All artifacts must include a `summary` field as part of their canonical schema. Nonchunkable artifacts use this summary directly in context packs.
### 4. event history → L2 structured decision points
L2 (compressed session memory) is built not by summarising events but by harvesting structured fields from existing validated artifacts and approval events:
* `summary` from the artifact → what was done
* `rationale` from the artifact (optional) → why it was done
* user steering text from approval events → human guidance
* stage outcome (success/failure/retry) → final status
These are assembled into a lightweight `DecisionPoint` record per completed stage. No modelbased summarisation is performed; the process is purely rulebased and deterministic.
Older decision points eventually graduate to L3 project memory when the L2 token cap is exceeded.
### 5. no tiny summariser model
We explicitly postpone any modelbased compression beyond v1. Even v2 will reevaluate only if rulebased compression proves insufficient, and then only for specific narrow tasks.
## consequences
**positive:**
* full determinism and replayability for all compression steps
* zero additional inference cost for compression
* tool output compressed dramatically (≥90% reduction on typical dev commands)
* conversation compaction is usercontrolled, preventing silent loss of important instructions
* decision points capture the *why*, not just the *what*, allowing later stages to reason about past choices
* architecture scales with more sophisticated compression later without changing contracts
**negative:**
* rulebased extraction may miss decision nuances when models omit the `rationale` field (mitigated by prompt design and user steering)
* manual compaction requires user awareness; if never used, conversation history may still grow until microcompaction kicks in
* pertool regex patterns require tool authors to think about summarisation, adding a small authoring burden (mitigated by sensible defaults and fallback truncation)
## alternatives considered
* **dedicated tiny summariser model** would improve compression quality but introduces nondeterminism, increased system complexity, and higher resource usage. Rejected for v1.
* **only truncation** simplest but loses too much critical information. Rejected.
* **full context window without compression** impossible for local models with limited context (≤32k tokens) and longrunning sessions. Rejected.
## status
This decision defines the v1 compression architecture. It may be revisited in a future ADR if empirical data shows rulebased compression is insufficient for critical decisionmaking.
@@ -0,0 +1,145 @@
---
name: "Adr 0004 Approval Tier Design"
description: "Fivetier approval system with grants, timeouts, diff previews"
depth: 2
links: ["../index.md", "../modules/core-approvals-submodule-spec.md"]
---
# ADR 0004: approval tier design with scoped grants, timeouts, and mandatory diff previews
**status:** accepted
**date:** 07.05.2026 (May)
**deciders:** Kami
---
## context
Correx executes potentially destructive operations (shell commands, file mutations, network calls) based on model proposals. To maintain bounded autonomy and auditability, every such operation must pass through an approval gate. The system needs a configurable, replaysafe approval model that is neither overly intrusive in safe contexts nor permissive in risky ones.
Key requirements:
- tierbased risk classification for all tools and operations
- sessionscoped elevation of trust (modes) and finegrained grants
- firstclass support for user steering during approval
- timeout handling when the user is not available
- mandatory diff/preview of proposed changes before irreversible actions
- replay determinism without user interaction
## decision
We adopt a **fivetier approval system** (T0T4) with configurable timeouts, session/stage/projectscoped grants, and mandatory diff previews. The approval engine is implemented in `:core:approvals`, called by `:core:validation`, and coordinates with `:core:orchestration` and `:core:sessions`.
### 1. tier definitions
| tier | meaning | default autoapprove mode | examples |
|------|---------|---------------------------|----------|
| T0 | inference only | auto (always) | generating text, plan, summary |
| T1 | readonly tools | auto (but mode can restrict) | `ls`, `cat`, `git status`, `grep` within allowlisted paths |
| T2 | reversible mutation | prompt | `git commit`, file edits inside versioned project, `pip install` in venv |
| T3 | external/network | prompt or deny (configurable) | `curl`, `git push`, `npm publish`, remote API calls |
| T4 | destructive | deny | `rm -rf`, database drop, force push to main |
Every tool and stage declares its required tier. The highest tier among a chain (e.g., a pipe involving `curl`) determines the approval requirement.
### 2. session modes
Session mode sets the maximum tier that is automatically approved without user interaction.
| mode | autoapprove up to | use case |
|------|-------------------|----------|
| `deny` | nothing (prompt for T0+) | maximum safety, untrusted instructions |
| `prompt` | T1 | normal daily use (default) |
| `auto` | T2 | trusted, focused development session |
| `yolo` | T4 (all) | testing, debugging; heavily logged, explicit optin required |
Modes can be changed midsession via `/mode <name>`, which emits an `ApprovalModeChanged` event. Yolo mode requires a confirmation prompt and enables verbose execution logging (full stdin/stdout/stderr, timing, scrubbed env vars) stored in a persession debug directory.
### 3. session, stage, and projectscoped grants
Beyond the mode, users may issue finegrained, replayable grants that elevate autoapproval for specific actions or time windows.
| scope | duration | example | event emitted |
|-------|----------|---------|---------------|
| **session** | until session end | "autoapprove all T2 for this session" | `ApprovalSessionGrantAdded` |
| **stage** | until current stage completes | "autoapprove T2 for the rest of this stage" | `ApprovalStageGrantAdded` |
| **project** | persists across sessions (stored in project config) | "always autoapprove `git commit` in this repo" | `GrantLoadedFromProject` (on load) |
Grants are additive; they cannot lower the effective tier of a tool (a T3 tool remains T3, but a grant may autoapprove it within scope). The approval engine evaluates grants in order of specificity: session overrides stage, stage overrides project.
### 4. approval lifecycle (endtoend)
1. An artifact or tool invocation is about to proceed.
2. Validation pipelines approval layer queries `:core:approvals` with the current tier, session mode, and active grants.
3. If the tier is **autoapproved** (by mode or grant), an `ApprovalGrantedAuto` event is recorded; execution proceeds immediately.
4. If the tier requires **user prompting**, an `ApprovalRequired` event is emitted. The session enters `AWAITING_APPROVAL` and orchestration suspends.
5. Router presents the approval prompt, including any generated diff/preview.
6. User responds with `approve` (optionally with steering text), `reject`, or an autoapprove grant. The response is recorded as `ApprovalGranted` or `ApprovalRejected`.
7. If approved and steering text is present, it is captured verbatim and injected into the next context pack as a `UserSteering` entry.
8. Orchestration resumes (or transitions to recovery on rejection).
### 5. approval timeouts
Default timeout behavior for all tiers is **pause indefinitely** (`AWAITING_APPROVAL` persists until user response). For localfirst safety, this ensures no action is taken if the user steps away.
Users may override timeout actions per tier in global or project config:
```yaml
approvals:
timeouts:
T0: auto
T1: auto
T2: pause
T3: reject
T4: reject
```
Supported timeout actions: `reject`, `pause`, `auto_deny` (records rejection, does not pause). If a timeout fires, the corresponding decision event is recorded.
### 6. diff/preview mandatory for T2+
Before any T2+ operation is approved, the user must see exactly what will happen.
- **File mutations (filesystem tool):** a unified diff of proposed changes. Generated by writing the proposed content to a temporary file and computing a `git diff` against the original (using the real file or a temporary copy). The diff is included in the approval prompt.
- **Shell commands:** the exact command string, plus if the command is known to modify files (e.g., `rm`, `mv`), a list of affected paths via dryrun inspection.
- **Git operations:** the diff of working tree changes or commit message/patch.
- **Scripts (bash, python):** the full script content is shown as a preview.
Tools that cannot produce a preview are capped at T1 (readonly) and may not mutate state.
### 7. nested/chained commands
- **Simple pipes** (`cat | grep | awk`): a single `ToolInvocationRequested` event; the entire pipeline inherits the highest risk tier of its components (e.g., if any part reaches network, T3).
- **Multicommand scripts:** executed as a single tool invocation; tier determined by tool definition; preview shows the full script content.
- **Interactive sessions** (`python -i`, `bash -i`): treated as T3 minimum; short default timeout (30s); user must be present. Output is captured as a sidechannel log; not treated as an artifact until the session ends and a receipt is produced.
### 8. replay determinism
During replay, all approval decisions are reapplied from the event log. No user interaction is required. If a session is replayed in a mode different from the original (e.g., yolo → prompt), replay still uses the recorded decisions; the mode change only affects future new events.
## consequences
**positive:**
- transparent, auditable, finegrained control over risk
- mandatory diff previews prevent blind execution of destructive commands
- session/stage/project grants balance safety with usability
- steering injection turns human approval into active guidance for models
- replay works without user presence, preserving audit integrity
- yolo mode enables efficient testing with full debug logs
**negative:**
- more complex orchestration (suspension/resumption, timeout enforcement) compared to a simpler approveeverything or denyeverything model
- mandatory diff previews impose a requirement on tool implementations (must support preview interface); tool authors bear a small additional burden
- interactive session handling is intentionally limited in v1
## alternatives considered
- **binary approve/deny without tiers:** too coarse; some operations are inherently safe and should not require user presence; others must never autoapprove.
- **perstage approval only (no toollevel):** insufficient granularity; a single stage may run multiple independent risky commands.
- **no yolo mode:** slows down development and testing; rejected.
## status
This decision defines the v1 approval subsystem. Future extensions may add evaluatormodelbased approvals, batchapprove for multiple tool calls, and richer sandbox isolation. The core tier + grant + preview model is expected to remain stable.
@@ -0,0 +1,114 @@
---
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.
@@ -0,0 +1,76 @@
---
name: "Adr 0006 Event Store Append Only"
description: "Appendonly, ordered, idempotent event store invariants"
depth: 2
links: ["../index.md", "../modules/core-events-submodule-spec.md", "./adr-0005-persistence-choice.md"]
---
# ADR-0006: Event Store Append-Only & Ordering Invariant
**status:** accepted
**date:** 08.05.2026
**deciders:** Kami
---
## context
The system relies on an append-only event log as the single source of truth. Replay correctness, session reconstruction, and deterministic projections all depend on strict ordering and immutability of persisted events.
Without enforced guarantees at the storage layer, higher-level invariants (projection determinism, replay correctness) cannot be trusted.
This ADR formalizes storage-level constraints for all EventStore implementations.
---
## content
The EventStore MUST enforce:
### 1. append-only semantics
* events cannot be updated or deleted
* event_id is immutable and unique per event
### 2. idempotency
* repeated append attempts with same event_id MUST be ignored or rejected safely
* duplicate events MUST NOT be stored
### 3. per-session ordering
* sequence MUST be strictly monotonically increasing per session_id
* ordering MUST be total within a session stream
### 4. causality preservation
* causation_id and correlation_id MUST be preserved as metadata only
* they MUST NOT affect ordering
### 5. deterministic read model
* read(session_id) MUST always return events in sequence order
* read consistency MUST not depend on implementation timing
---
## consequences
**positive:**
* guarantees replay correctness foundation
* enables deterministic projection logic
* simplifies concurrency model (append-only reduces state conflicts)
* allows multiple backend implementations (sqlite, in-memory)
**negative:**
* no event mutation allowed (requires compensating events instead)
* stricter implementation requirements for persistence engines
* potential write amplification in correction scenarios
---
## status
accepted
@@ -0,0 +1,83 @@
---
name: "Adr 0007 Projection Replay"
description: "Projection purity and replay determinism contract"
depth: 2
links: ["../index.md", "../modules/core-events-submodule-spec.md", "./adr-0006-event-store-append-only.md"]
---
# ADR-0007: Projection & Replay Determinism Contract
**status:** accepted
**date:** 08.05.2026
**deciders:** Kami
---
## context
Projections and replay are responsible for reconstructing system state from event streams. Any nondeterminism in projection logic or replay execution breaks system consistency guarantees.
The system requires strict guarantees that state reconstruction is reproducible across time, environment, and implementation.
This ADR formalizes projection and replay correctness requirements.
---
## content
### 1. projection purity
* Projection MUST be a pure function:
`EventStream → State`
* no hidden state or external dependencies are allowed
* projections MUST NOT mutate shared state
### 2. determinism requirement
* identical event streams MUST produce identical state
* ordering of events MUST be respected (sequence order)
### 3. replay invariance
* replay MUST produce identical results regardless of:
* runtime environment
* execution timing
* storage backend implementation
### 4. environment independence
* replay MUST NOT depend on:
* live services
* LLM calls
* external APIs
* replay MUST be fully deterministic offline
### 5. disposable projections
* projection state MUST be rebuildable at any time
* no projection state is authoritative; only event log is
---
## consequences
**positive:**
* enables safe rebuild of entire system state
* guarantees consistency across infrastructure implementations
* simplifies debugging (full replayability)
* enables future snapshotting and caching layers
**negative:**
* prohibits non-deterministic projection logic
* forces strict separation of computation vs external effects
* requires careful handling of ordering assumptions
---
## status
accepted
@@ -0,0 +1,82 @@
---
name: "Adr 0008 Contract Enforcment With Tests"
description: "Contract testing to enforce event and replay invariants"
depth: 2
links: ["../index.md", "./adr-0006-event-store-append-only.md", "./adr-0007-projection-replay.md"]
---
# ADR-0009: Contract Testing Enforcement of Event & Replay Invariants
**status:** accepted
**date:** 08.05.2026
**deciders:** Kami
---
## context
System correctness depends not only on design-time invariants (ADR-0007, ADR-0008) but also on continuous enforcement across implementations.
Multiple EventStore and Replay implementations exist (in-memory, sqlite, future persistence layers). Without shared contract tests, invariants may diverge silently.
This ADR defines the role of contract tests as enforcement layer for system invariants.
---
## content
### 1. contract-first enforcement model
* all EventStore implementations MUST satisfy shared contract tests
* all replay implementations MUST satisfy shared replay contracts
### 2. EventStore contract requirements
Contract tests MUST enforce:
* append-only behavior
* idempotency of event_id
* strict ordering per session
* deterministic read consistency
* correct sequence continuity
### 3. Replay contract requirements
Contract tests MUST enforce:
* deterministic reconstruction of state
* identical results across repeated replay executions
* correctness over full event streams
* correctness over partial event streams (cursor-based replay)
### 4. implementation independence
* contract tests MUST NOT depend on specific implementation details
* tests operate only through interfaces (EventStore, EventReplayer, Projection)
### 5. cross-backend equivalence
* in-memory and persistent stores MUST produce identical replay results for identical inputs
---
## consequences
**positive:**
* prevents divergence between implementations
* guarantees correctness across persistence backends
* enforces architectural invariants at CI level
* makes replay correctness regression-resistant
**negative:**
* increases test suite complexity
* requires careful abstraction of test fixtures
* slower feedback cycles for persistence changes
---
## status
accepted
@@ -0,0 +1,189 @@
---
name: "Adr 0009 Replay Engine Projection Separation"
description: "Generic replay engine separated from domain projections"
depth: 2
links: ["../index.md", "../modules/core-events-submodule-spec.md", "../modules/core-sessions-submodule-spec.md"]
---
# ADR 0009: Generic Replay Engine and Projection Separation
**status:** accepted
**date:** 08.05.2026
**deciders:** Kami
---
## context
Correx uses an append-only event store as the single source of truth.
Session state and future domain states are reconstructed entirely from ordered event streams. Replay determinism and projection purity are core system invariants.
During implementation of Epic 2 (Session Lifecycle FSM), a distinction emerged between:
* generic replay infrastructure
* domain-specific projection semantics
The system already contained reusable replay infrastructure in `:core:events`:
* `Projection<S>`
* `StateBuilder<S>`
* `EventReplayer<S>`
At the same time, session lifecycle logic introduced:
* `SessionProjector`
* `SessionFsm`
* `SessionState`
* `SessionEvent`
A design decision was required to prevent:
* replay logic duplication
* infrastructure/domain coupling
* projection semantics leaking into the event engine
---
## content
The replay architecture is separated into two layers:
### 1. Generic replay engine (`:core:events`)
The `:core:events` module owns:
* replay orchestration
* projection execution contracts
* state folding mechanics
* ordered event replay infrastructure
This layer contains:
* `Projection<S>`
* `StateBuilder<S>`
* `EventReplayer<S>`
* replay implementations
This layer MUST remain domain-agnostic.
It MUST NOT contain:
* session lifecycle logic
* FSM semantics
* domain state models
* business transition rules
---
### 2. Domain projections (`:core:sessions`)
The `:core:sessions` module owns:
* session lifecycle semantics
* FSM transition logic
* session state reconstruction
* event interpretation for session domain
This layer contains:
* `SessionState`
* `SessionStatus`
* `SessionEvent`
* `SessionFsm`
* `SessionProjector`
`SessionProjector` implements:
```kotlin
Projection<SessionState>
```
and is executed by the generic replay engine.
---
### 3. Replay flow
Replay is defined as:
```text
EventStore
EventReplayer<S>
Projection<S>
State
```
For sessions:
```text
EventStore
EventReplayer<SessionState>
SessionProjector
SessionState
```
---
### 4. Projection purity rules
All projections MUST be:
* deterministic
* side-effect free
* replay-safe
* stateless outside reduction input
Projections MUST NOT:
* perform IO
* access system clocks
* mutate external state
* depend on runtime execution order outside event sequence
Projection output MUST depend solely on:
* previous state
* current event
---
### 5. Session replay semantics
Session state is not persisted directly.
`SessionState` is reconstructed entirely from replaying ordered `StoredEvent` streams.
FSM transitions are interpreted from persisted `EventPayload` instances through session-specific mapping logic.
---
## consequences
**positive:**
* replay engine becomes reusable across domains
* deterministic replay guarantees remain centralized
* domain logic remains isolated from persistence mechanics
* multiple independent projections can coexist over same event stream
* session lifecycle logic remains testable in isolation
* future projections can reuse replay infrastructure without duplication
**negative:**
* introduces additional abstraction layers
* domain projections require explicit wiring into replay engine
* event interpretation requires mapper logic between payloads and FSM events
* debugging replay chains may require traversing multiple layers
---
## status
accepted
@@ -0,0 +1,93 @@
---
name: "Adr 0010 L3 Vector Retrieval"
description: "SQLite-vec default, Qdrant optional — semantic retrieval for L3 context injection"
depth: 2
links: ["../index.md", "../architecture/context-layers.md", "../epics/epic-7-resolution.md"]
---
# ADR 0010: L3 context retrieval — SQLite-vec default with optional Qdrant backend
**status:** accepted
**date:** 11.05.2026 (May)
**deciders:** correx design team
---
## context
L3 (durable project memory) holds persistent facts, architecture decisions, and key outputs across sessions. The context engine (Epic 7) assembles token-budgeted packs from L0L2; L3 is injected selectively when relevant.
Injecting all L3 entries unconditionally is not viable — the layer grows unboundedly and most entries will be irrelevant to any given stage. A relevance signal is needed to select which L3 entries enter a pack.
Semantic similarity via vector embeddings is the natural fit: embed each L3 entry at write time, query by the current stage's context at pack-build time, inject the top-K matches.
**Determinism constraint (ADR-0000 #10):** vector search is nondeterministic — the same query may return different results as the index evolves. Allowing live vector search to determine pack contents would break replay. This is resolved by committing the selected L3 entry IDs as an event (`L3EntriesSelectedEvent`) before pack assembly. Replay uses the committed IDs, not a live query.
## decision
Correx will use **SQLite-vec** as the default vector store for L3 retrieval, with **Qdrant** as an optional configurable backend.
`:core:context` defines a `VectorStore` port (interface). `:infrastructure:tools:sqlite-vec` and `:infrastructure:tools:qdrant` provide implementations. The active backend is selected via configuration — no core module is coupled to either.
### SQLite-vec (default)
* embedded, zero-setup — consistent with the local-first posture established in ADR-0005
* co-located with the primary SQLite event store; single-file deployment
* accessed via SQLite JDBC — no additional client library
* sufficient for local sessions where L3 grows to tens of thousands of entries
### Qdrant (optional)
* self-hosted Rust binary, HTTP/gRPC API
* scales to large L3 indices and supports filtered ANN queries
* activated by setting `vectorStore.backend = qdrant` in configuration
* requires a sidecar process; not suitable for zero-setup local deployments
### determinism contract
1. at L3 entry write time: embed content, store vector in the active backend
2. at pack-build time: query top-K by cosine similarity to the current stage context
3. commit `L3EntriesSelectedEvent(sessionId, stageId, selectedEntryIds, queryEmbedding)`
4. pack assembly reads `selectedEntryIds` from the event — not from a live query
5. replay uses the committed event; the vector store is never queried during replay
The vector store is a read model for selection. The event log remains authoritative.
### VectorStore port
```kotlin
interface VectorStore {
fun upsert(id: ContextEntryId, vector: FloatArray, metadata: Map<String, String>)
fun query(vector: FloatArray, topK: Int, filter: Map<String, String> = emptyMap()): List<VectorMatch>
fun delete(id: ContextEntryId)
}
data class VectorMatch(val id: ContextEntryId, val score: Float)
```
## consequences
**positive:**
* zero-setup default — SQLite-vec adds no external process for local deployments
* Qdrant available for production or large-scale use without changing core code
* determinism preserved — vector search never touches replay path
* selection is auditable and replayable via `L3EntriesSelectedEvent`
* port/adapter pattern is consistent with existing infrastructure separation (ADR-0005)
**negative:**
* SQLite-vec has practical limits at very large L3 indices (tens of millions of entries); Qdrant required at that scale
* embedding generation (not vector retrieval) is a nondeterministic step — embedding model version changes may alter which entries are selected for future sessions (existing committed events are unaffected)
* two infrastructure implementations to maintain
## alternatives considered
* **Chroma:** Python-native; adds a Python service dependency inconsistent with a JVM-first project.
* **pgvector:** requires PostgreSQL; heavier than SQLite for local-first deployment.
* **Keyword/tag-based filtering:** deterministic but misses semantic relevance; insufficient for open-ended L3 memory.
* **Inject all L3 entries:** correct but not token-budget-safe as L3 grows.
## status
accepted. Implementation deferred to the epic that introduces L3 write and retrieval infrastructure.
@@ -0,0 +1,60 @@
---
name: "Adr 0011 Transition Engine Contracts"
description: "Defines ownership of StageId and the no-match behavior of the transition resolver"
depth: 2
links: ["../index.md", "./adr-0001-event-sourcing.md", "./adr-0009-replay-engine-projection-separation.md"]
---
# ADR 0011: transition engine contracts — StageId ownership and no-match behavior
**status:** accepted
**date:** 15.05.2026 (May)
**deciders:** Kami
---
## context
Two behaviors in the transition engine were undefined, creating silent failure modes:
1. **No-match behavior**: `DefaultTransitionResolver` returned `Stay` when a stage had no outgoing transitions. This conflated two distinct cases: "conditions not yet met, keep waiting" (`Stay`) and "no transitions are defined for this stage" (structurally undefined). The latter would cause the orchestrator to loop indefinitely with no path forward.
2. **`StageId` origin**: nothing explicitly prevented the transition engine from constructing `StageId` values internally. This would break the invariant that the orchestrator is the sole source of stage identity, making execution traces harder to reason about and replay.
## decision
### 1. No-match is a typed failure
`TransitionDecision` gains a `NoMatch` case. The resolver returns `NoMatch` when the graph contains no outgoing edges for the current stage. This is distinct from:
- `Stay` — outgoing edges exist but no condition evaluated to true; waiting is the correct response.
- `Blocked` — a specific guard explicitly prevented the transition.
- `NoMatch` — the graph has no definition for what to do next; this is a workflow authoring error or an unhandled terminal state.
The orchestrator is responsible for observing `NoMatch` and emitting a `StageFailedEvent` with reason `"no matching transition"`. The resolver is pure: it classifies; it does not emit events.
### 2. StageId is always caller-generated
`StageId` values are created exclusively by the orchestrator (or workflow graph construction at startup). The transition engine — resolver, evaluator, and related types — must never construct a `StageId`. It receives `StageId` values through `EvaluationContext` and `WorkflowGraph` and returns them in `TransitionDecision.Move`. It does not originate them.
This rule preserves:
- **Replay determinism**: stage identity is fixed at graph construction time, not derived during evaluation.
- **Single source of truth**: the orchestrator owns execution flow; the transition engine advises it.
## consequences
**positive:**
- No-match is observable and testable; indefinite loops caused by missing transitions become explicit failures.
- `Stay` retains its precise meaning: "wait for conditions", not "don't know what to do".
- `StageId` origin is unambiguous; code review can statically verify compliance.
**negative:**
- Callers of `resolve()` that previously exhaustively handled `Move | Stay | Blocked` must now handle `NoMatch`. This is intentional: forcing the call site to handle it prevents silent suppression.
## alternatives considered
- **Return `Blocked` with a special reason string**: rejected. Encoding structural failure as a `Blocked` with a magic string degrades type safety and makes exhaustive matching useless.
- **Throw an exception on no-match**: rejected. Exceptions are not part of the resolver's contract. The orchestrator decides what a `NoMatch` means in context (fail, alert, retry on graph reload); that policy does not belong in the resolver.
- **Allow transition engine to generate `StageId`**: rejected. Any engine-generated ID would be invisible to the event log before the `StageFailedEvent`, breaking the invariant that all state transitions are traceable.