feat(kernel): loop-engineer/event@1 EventStore + SQLite/WAL + deterministic reducer (#50) - #62
Conversation
…tore, deterministic reducer (#50) - schemas/event.schema.json: event@1 envelope — run_id/sequence/event_id/ type/actor/ts/payload/artifact_hashes; 4 event types (contract_opened, iteration_appended, receipt_appended, terminal_written) with required payload fields incl. receipt iteration_id - loop/events.py: EventStore Protocol + SQLiteEventStore (WAL, synchronous=FULL, busy_timeout=5000, append-only via no-update/no-delete triggers, expected_sequence CAS, duplicate event_id rejection); validate_event honors --mode basic|strict|release with type-checked structural fallback; read(since_sequence=None) = full stream, int N (incl. 0) = strictly after N — the N=0 resume pattern round-trips - loop/reducer.py: deterministic fold — FSM transition legality, G1 all-required completion, terminal immutability enforced at replay via the same loop.fsm/loop.completion functions the writers use - tests: 32 targeted (replay determinism, 4-of-4 Succeeded G1 tamper cases, CAS success+conflict, PRAGMA asserts, N=0 resume regression) - reference/repo-os-contract.md §16: event-sourcing layer documented Suites: extras 618 passed/15 skipped; pyyaml-only 588/45. Kernel writers byte-unchanged; jsonschema/pyyaml stay optional extras. Claude-Session: https://claude.ai/code/session_01EJ8zA8Cbi4o2amawpj8bZW
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: c1e7bf85f0
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| if expected_sequence is not None and expected_sequence != next_sequence: | ||
| conn.execute("ROLLBACK") | ||
| raise SequenceConflictError(f"expected next sequence {next_sequence} for run_id {run_id!r}, caller supplied {expected_sequence}") |
There was a problem hiding this comment.
Preserve duplicate-event retries under CAS
When a CAS append succeeds but the caller times out and retries the same event_id with the same expected_sequence, this check runs before the UNIQUE(event_id) insert and raises SequenceConflictError because the next sequence has already advanced. That makes the advertised duplicate-event retry path unavailable for the normal CAS retry scenario, so clients cannot distinguish a successfully persisted retry from a real sequence race; check for an existing event_id before treating the stale expected sequence as a conflict, or return the existing event idempotently.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Pull request overview
Introduces the Phase 1 event-sourcing foundation for loop-engineer by adding an event@1 envelope schema, a SQLite/WAL-backed append-only EventStore, and a deterministic reducer that can replay an event stream into a stable projection (with targeted tests validating determinism, CAS semantics, and tamper resistance).
Changes:
- Add
loop.events.SQLiteEventStore(WAL + append-only triggers + expected-sequence CAS) andvalidate_eventwith structural fallback. - Add
loop.reducer.reduce_events()deterministic fold enforcing replay invariants (FSM legality, G1 completion semantics, terminal immutability). - Add schema + contract documentation + targeted pytest coverage for store behavior and replay determinism/resume.
Reviewed changes
Copilot reviewed 7 out of 7 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| scripts/test_reducer.py | Adds deterministic replay/resume tests and tamper/immutability negative cases. |
| scripts/test_eventstore.py | Adds validation, WAL/append-only, CAS, duplicate rejection, and concurrency tests for the SQLite store. |
| schemas/event.schema.json | Defines the loop-engineer/event@1 JSON schema envelope and artifact hash shape. |
| reference/repo-os-contract.md | Documents the new event-sourcing layer boundary and enforcement split (store shape vs reducer semantics). |
| loop/reducer.py | Implements the deterministic reducer and replay-time domain invariant enforcement. |
| loop/events.py | Implements EventStore protocol, SQLiteEventStore, and event validation (jsonschema + structural fallback). |
| loop/init.py | Exposes eventstore/reducer APIs from the package top-level. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| except sqlite3.IntegrityError as exc: | ||
| conn.execute("ROLLBACK") | ||
| raise DuplicateEventError(record["event_id"]) from exc |
| from typing import Any, Mapping, Protocol, Sequence, runtime_checkable | ||
|
|
||
| from .contract import ContractIssue, _resolve_requested_mode, _schemas_dir | ||
| from .emit import _ITERATION_OUTCOMES, _RECEIPT_OUTCOMES, _RECEIPT_ROLES |
| entry = dict(payload, event_id=event["event_id"], causation_id=event.get("causation_id"), correlation_id=event.get("correlation_id"), ts=event["ts"]) | ||
| new_state["runlog_entries"] = new_state["runlog_entries"] + [entry] | ||
| elif event_type == "receipt_appended": | ||
| entry = dict(payload, event_id=event["event_id"], causation_id=event.get("causation_id"), correlation_id=event.get("correlation_id"), ts=event["ts"]) | ||
| new_state["receipts"] = new_state["receipts"] + [entry] |
Closes #50 — Phase 1 foundation: append-only event log + deterministic replay.
What
event@1envelope (run_id, monotonic per-run sequence, event_id, type, actor, ts, payload, artifact_hashes); 4 event types with required payload fields, incl.iteration_idonreceipt_appended.EventStoreProtocol +SQLiteEventStore: WAL, synchronous=FULL, busy_timeout=5000, append-only (no-update/no-delete triggers),expected_sequenceCAS, duplicateevent_idrejection.validate_eventhonors--mode basic|strict|releasewith a type-checked structural fallback (S3 parity lesson). Read-cursor contract:since_sequence=None→ full stream; int N (including 0) → strictly after N — the N=0 resume/fold pattern round-trips byte-identically.loop.fsm/loop.completionfunctions the writers use (two-layer enforcement, never re-implemented).Verification
cx_s4_eventstore_a1(repair_requested) +cx_s4_eventstore_a2(accepted); codex sessions019f5cf6…,019f5d0a….Kernel writers byte-unchanged; pure-stdlib runtime preserved (jsonschema/pyyaml stay optional extras).
https://claude.ai/code/session_01EJ8zA8Cbi4o2amawpj8bZW