Skip to content

Event log unification#188

Merged
pbdeuchler merged 5 commits into
masterfrom
event-log-unification
Jul 15, 2026
Merged

Event log unification#188
pbdeuchler merged 5 commits into
masterfrom
event-log-unification

Conversation

@pbdeuchler

@pbdeuchler pbdeuchler commented Jul 15, 2026

Copy link
Copy Markdown
Owner

Summary

This PR makes the append-only per-session event log the authoritative source for session history and treats persisted SessionState as a checkpoint at a known log position. Runtime hydration, optimistic concurrency, trace export, and telemetry now derive from the same ordered event stream instead of relying on partially independent persistence paths.

The result is a replayable session model with explicit invariants: committed events have gap-free monotonic sequences, checkpoints declare which sequence they cover, lagging checkpoints are hydrated from the log tail, and concurrent writers conflict on the event-log head.

What changed

Canonical event folding and checkpoints

  • Adds halter_protocol::fold with pure apply_event, fold_events, and covered_state_matches operations.
  • Folds canonical events into the checkpoint-covered portions of SessionState, including messages, compacted context, and accumulated usage.
  • Adds state_sequence and head_sequence to StoredSession, plus StoredSession::new for correctly initialized records.
  • Adds SessionStore::replay_after so hydration can replay only events newer than a checkpoint; SQLite pushes the sequence bound into its query.
  • Hydrates a lagging checkpoint by validating and folding its log tail before exposing the session.
  • Rejects invalid replay data, including checkpoint positions ahead of the head, gaps, reordered sequences, duplicate positions, and cross-session events.

Reproducible state transitions

  • Extends ContextCompacted with optional CompactionEventEffects, recording the post-compaction message window and provider-native prefix needed to replay the only history-rewriting operation.
  • Preserves deserialization of legacy compaction events that do not contain effects.
  • Adds SessionResumed, ensuring resume-related state changes also advance the canonical log.
  • Adds saturating usage accumulation shared by live execution and replay so counters cannot overflow or diverge.

Event-head concurrency

  • Changes SessionStore::commit to accept expected_head_sequence: Option<u64> instead of an expected state snapshot.
  • Detects stale writers by comparing the expected and actual event-log heads.
  • Assigns gap-free event sequences atomically in both the in-memory and SQLite backends.
  • Threads the current head through the runtime commit path, avoiding repeated full-state clones and comparisons during a turn.
  • Keeps checkpoint updates and event appends atomic; event-only commits may intentionally leave a checkpoint behind the head for later hydration.

SQLite migration

  • Adds schema migration v2 with sessions.state_sequence.
  • Backfills existing sessions to their current log head because v1 checkpoints reflected all events committed at that time.
  • Retains the single serialized writer and read-only WAL connection pool behavior.

Trace unification

  • Adds HalterSession::export_trace() and halter_runtime::export_session_trace to rebuild traces from persisted events, whether or not live trace recording was configured.
  • Exports root and descendant subagent sessions in the existing trace-file format.
  • Gives live and exported traces one version-2 header schema with a shared generated_at timestamp field.
  • Detects cyclic or duplicate subagent ancestry and fails export rather than recursing indefinitely or emitting ambiguous traces.
  • Keeps a future halter trace CLI command out of this PR; this change provides the runtime API it can call.

Documentation and compatibility

  • Updates the workspace README, crate READMEs, and changelog with the log-authoritative checkpoint model and storage contract.
  • This is a breaking change for custom SessionStore implementations because the commit concurrency argument changes from expected state to expected head sequence.
  • create_session now rejects records with non-zero state/head sequences.
  • Legacy effect-less ContextCompacted events remain readable.

CI follow-up included here

The final commits also repair failures exposed by the current CI environment:

  • Pins playwright-rs exactly to 0.12.0 so fresh lockfile generation cannot silently change the browser API dependency.
  • Supplies Playwright's own declared 1.59.1 compile-time version to every CI job that compiles the browser crate. The upstream 0.12.x build script otherwise omits that required value when its build-time driver URL returns 404; runtime driver discovery and installation behavior is unchanged.
  • Applies four behavior-preserving refactors required by Rust 1.97 Clippy lints in the vendored brush core and builtins crates.

Test coverage

  • Shared store conformance tests run against both built-in backends and cover sequence assignment, stale-head conflicts, event-only commits, checkpoint lag, and the full replay/checkpoint invariant.
  • Property tests cover ordered replay, usage saturation, and re-folding from arbitrary checkpoint boundaries.
  • Runtime tests cover hydration validation, resume events, commit/checkpoint agreement, trace export, shared trace headers, and cyclic ancestry rejection.
  • The vendored crate suites remain green after the formatting-only refactors (49 brush-core and 27 brush-builtins unit tests).

Verification

  • cargo +1.97.0 fmt --all -- --check
  • cargo +1.97.0 clippy --workspace --all-features --all-targets -- -D warnings
  • cargo +1.97.0 clippy --workspace --all-targets -- -D warnings
  • cargo +stable test --workspace --all-features
  • cargo +stable test --workspace
  • PLAYWRIGHT_DRIVER_VERSION=1.59.1 cargo +1.88.0 check --workspace --all-features --all-targets
  • cargo +1.88.0 test --workspace --all-features

pbdeuchler and others added 5 commits July 3, 2026 02:06
Make the per-session event log the source of truth and demote the
persisted SessionState to a checkpoint stamped with the log position it
reflects (log-authoritative with checkpoints):

- halter-protocol: add the pure fold module (apply_event, fold_events,
  covered_state_matches) reproducing messages, compacted_prefix, and
  usage_so_far from committed events; ContextCompacted now carries
  optional state-complete CompactionEventEffects; new SessionResumed
  payload; Usage::saturating_accumulate shared by runtime and fold.
- halter-session: commit() takes expected_head_sequence instead of
  expected_state (conflicts are now an O(1) head check, not a
  structural whole-state compare); StoredSession carries
  state_sequence/head_sequence with StoredSession::new; replay_after
  for log tails; sqlite migration v2 backfills state_sequence to each
  session's log head; conformance suite locks in head semantics and
  the fold(replay()) == checkpoint invariant on every backend.
- halter-runtime: turn loop threads a u64 expected head instead of
  cloning/comparing full SessionState per flush; loaders hydrate
  lagging checkpoints by folding the log tail; compaction events carry
  their effects; resume appends SessionResumed; new export_session_trace
  / HalterSession::export_trace derive the trace-file format from
  replay(), no traces_dir required.

Follow-ups tracked in docs/event-log-unification.md (local): halter
trace CLI subcommand, fold-derived usage as the sole accumulator,
#172 outcome summaries over the export, BestEffort-event pruning.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@pbdeuchler
pbdeuchler merged commit 65fead9 into master Jul 15, 2026
10 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant