You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
[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
Part of #553 (group 2 — depends on burn #554 backend seam, burn #555 characterization lock, and relayhistory group 1 + #178SessionStore 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
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.
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.
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).
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").
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.
Part of #553 (group 2 — depends on burn #554 backend seam, burn #555 characterization lock, and relayhistory group 1 + #178
SessionStorefacade + #179 change feed being published on crates.io as the singleai-histcrate; parallel with the other burn group-2 issues).Goal
A second
SessionSourceBackendimplementation that produces exactly the rows the builtin readers produce, sourced from relayhistory's store throughSessionStoreinstead of the harness logs. It ships off by default so it can be validated on real machines withburn state paritybefore the cutover.Scope
ai-hist = "<version>"(default features — nodelivery, noopencode-backup) incrates/relayburn-sdk/Cargo.tomlbehind[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 userusqlite 0.32 bundled; add acargo tree -dCI check so a future divergence (two SQLite copies) fails loudly.crates/relayburn-sdk/src/ingest/backend/relayhistory.rsimplementing 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_revisionfromstore.sync's last report /changes_since(...).head()(a changed store ⇒ changed fingerprint; no file stats).pass()→store.sync(SyncOptions{ force, .. })thenstore.changes_since(Watermark::CONSUMER, ChangeQuery{ consumer: "burn", kinds, batch }); group changes by session; for each touched session callstore.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 apass()restricted to that session;ClaudeSession{cwd,id}→SessionRef::Id+hydrate.watch_roots()→Source::capabilities().watch_roots(or delegate the whole loop tostore.watch()— decide in [G2] Watch mode,--hook claude, pending stamps and gap warnings over the relayhistory backend #558).ingest/backend/relayhistory/adapter.rs—SessionEvidence → impl DerivedRecords+TurnRecords:TurnRecordper assistantMessage:message_id,ts,model,usagefromNormalizedUsage(input,output,reasoning,cache_read,cache_write_5m/1h— fall back to total cache write when the split is absent),tool_callsfromToolCalls in that message (targetvia burn's per-tool pick,args_hashviareader/hash.rs::args_hashoverargs,edit_pre_hash/post_hashfromold_string/new_string/content,is_error),files_touchedfromFileEdits,subagentfrom relationships + sidechain flag,stop_reasonviaStopReason::from_wire,activity/retries/has_editsviaclassify_activityfed with prompt text + assistant text + control kinds (relayhistory Add oversized tool output bloat detector (#168) #180),fidelityfromSource::capabilities()+ per-message usage presence (reader/fidelity.rsrules),project/project_keyfromSession.project_key.ContentRecords fromMessage.blockshonouringContentStoreMode.CompactionEventfromcompaction_boundarymarkers (tokens_before_compact).SessionRelationshipRecordfromRelationships (delegated → subagent,fork,continuation/resume → continuation, plus onerootrow per session as today).ToolResultEventRecordfromToolResults (all fields map 1:1 after relayhistory Makeburn comparetake models as a required positional (#159) #171;usage_attributionfor OpenCode computed here via the existing even-split code, moved out of the reader).UserTurnRecordfromUserTurnblocks withapprox_tokens = ceil(bytes/4);user_uuidsynthesized for Codex/OpenCode exactly as before.RequestIdLookupfromMessage.request_id.SourceKindmapping 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).ingest/backend/relayhistory/tests.rs): for every corpus case in [G1] Characterization lock: ledger golden snapshots for the full fixture corpus and aburn state parityledger differ #555, populate a relayhistory store from the same fixture files (viaai-histagainst a temp HOME), runingest_allwith the relayhistory backend into a fresh ledger, and diff against the committed ledger snapshots with thestate parityfunction. Every difference must be either fixed or listed indocs/relayhistory-backend-parity.mdwith a reason (e.g. "relayhistory does not synthesize X; burn's value was Y").docs/relayhistory-backend-parity.md):burn --ledger-path /tmp/b-rh ingestwithRELAYBURN_SOURCE=relayhistoryvs. the builtin ledger, thenburn state parity --against, expected classes of acceptable diffs.Acceptance
cargo test -p relayburn-sdk --features relayhistory-sourceruns the parity suite; zero unexplained differences across the corpus; incremental-equivalence (N passes) holds for the relayhistory backend too.cargo build --workspace) does not compileai-hist.burn summary --jsontotals (turns, tokens by model, cost) are identical between backends on the corpus.turn_id_fingerprint,turn_content_fingerprint) are identical for every corpus turn ([G1] Characterization lock: ledger golden snapshots for the full fixture corpus and aburn state parityledger 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).