Skip to content

[G2] RelayHistoryBackend: depend on the ai-hist crate, map SessionEvidence onto burn's record types behind the backend seam (feature-flagged, off by default) #557

Description

@willwashburn

Part of #553 (group 2 — depends on burn #554 backend seam, burn #555 characterization lock, and relayhistory group 1 + #178 SessionStore facade + #179 change feed being published on crates.io as the single ai-hist crate; parallel with the other burn group-2 issues).

Goal

A second SessionSourceBackend implementation that produces exactly the rows the builtin readers produce, sourced from relayhistory's store through SessionStore instead of the harness logs. It ships off by default so it can be validated on real machines with burn state parity before the cutover.

Scope

  1. Dependency: ai-hist = "<version>" (default features — no delivery, no opencode-backup) in crates/relayburn-sdk/Cargo.toml behind [features] relayhistory-source = ["dep:ai-hist"]. The crate version is the contract (Cargo semver; there is no contract constant) — pin with a caret range inside one minor and bump deliberately. Both workspaces use rusqlite 0.32 bundled; add a cargo tree -d CI check so a future divergence (two SQLite copies) fails loudly.
  2. Backend crates/relayburn-sdk/src/ingest/backend/relayhistory.rs implementing the seam from [G1] Introduce a SessionSourceBackend seam in ingest so the built-in readers can be swapped for relayhistory #554, using only the seven facade methods:
    • fingerprint() → head_revision from store.sync's last report / changes_since(...).head() (a changed store ⇒ changed fingerprint; no file stats).
    • pass() → store.sync(SyncOptions{ force, .. }) then store.changes_since(Watermark::CONSUMER, ChangeQuery{ consumer: "burn", kinds, batch }); group changes by session; for each touched session call store.session(&SessionRef::Id{..}, SessionQuery{ include_text: <from ContentStoreMode>, kinds: None }) and run the adapter; call .commit() on the change iterator only after the ledger transaction commits (cursor-then-fingerprint ordering preserved: ingest.rs:386-404).
    • hydrate_one(ClaudeTranscript{path}) → store.hydrate(&SessionRef::Path{ source: Claude, path }, ..) then a pass() restricted to that session; ClaudeSession{cwd,id} → SessionRef::Id + hydrate.
    • watch_roots() → Source::capabilities().watch_roots (or delegate the whole loop to store.watch() — decide in [G2] Watch mode, --hook claude, pending stamps and gap warnings over the relayhistory backend #558).
  3. Adapter ingest/backend/relayhistory/adapter.rs — SessionEvidence → impl DerivedRecords + TurnRecords:
    • TurnRecord per assistant Message: message_id, ts, model, usage from NormalizedUsage (input, output, reasoning, cache_read, cache_write_5m/1h — fall back to total cache write when the split is absent), tool_calls from ToolCalls in that message (target via burn's per-tool pick, args_hash via reader/hash.rs::args_hash over args, edit_pre_hash/post_hash from old_string/new_string/content, is_error), files_touched from FileEdits, subagent from relationships + sidechain flag, stop_reason via StopReason::from_wire, activity/retries/has_edits via classify_activity fed with prompt text + assistant text + control kinds (relayhistory Add oversized tool output bloat detector (#168) #180), fidelity from Source::capabilities() + per-message usage presence (reader/fidelity.rs rules), project/project_key from Session.project_key.
    • ContentRecords from Message.blocks honouring ContentStoreMode.
    • CompactionEvent from compaction_boundary markers (tokens_before_compact).
    • SessionRelationshipRecord from Relationships (delegated → subagent, fork, continuation/resume → continuation, plus one root row per session as today).
    • ToolResultEventRecord from ToolResults (all fields map 1:1 after relayhistory Make burn compare take models as a required positional (#159) #171; usage_attribution for OpenCode computed here via the existing even-split code, moved out of the reader).
    • UserTurnRecord from UserTurn blocks with approx_tokens = ceil(bytes/4); user_uuid synthesized for Codex/OpenCode exactly as before.
    • RequestIdLookup from Message.request_id.
    • Per-source SourceKind mapping incl. new sources (cursor, grok → extend the enum, see [G3] Harness expansion through relayhistory: Cursor and Grok in burn (SourceKind, pricing, TOOL_ALIASES, overhead mapping, fidelity policy) and collector-backlog transfer #560).
  4. Fixture parity test (ingest/backend/relayhistory/tests.rs): for every corpus case in [G1] Characterization lock: ledger golden snapshots for the full fixture corpus and a burn state parity ledger differ #555, populate a relayhistory store from the same fixture files (via ai-hist against a temp HOME), run ingest_all with the relayhistory backend into a fresh ledger, and diff against the committed ledger snapshots with the state parity function. Every difference must be either fixed or listed in docs/relayhistory-backend-parity.md with a reason (e.g. "relayhistory does not synthesize X; burn's value was Y").
  5. Real-machine validation runbook (docs/relayhistory-backend-parity.md): burn --ledger-path /tmp/b-rh ingest with RELAYBURN_SOURCE=relayhistory vs. the builtin ledger, then burn state parity --against, expected classes of acceptable diffs.

Acceptance

  • cargo test -p relayburn-sdk --features relayhistory-source runs the parity suite; zero unexplained differences across the corpus; incremental-equivalence (N passes) holds for the relayhistory backend too.
  • Default build (cargo build --workspace) does not compile ai-hist.
  • burn summary --json totals (turns, tokens by model, cost) are identical between backends on the corpus.
  • Ledger fingerprints (turn_id_fingerprint, turn_content_fingerprint) are identical for every corpus turn ([G1] Characterization lock: ledger golden snapshots for the full fixture corpus and a burn state parity ledger differ #555 identity vectors).
  • grep -rn "rusqlite" crates/relayburn-sdk/src/ingest/backend/relayhistory/ returns nothing (burn never touches relayhistory's connection).

Out of scope

Making it the default; deleting readers; watch/hook wiring (#558); Node packaging (#559).

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions