Repository navigation
[G2] SessionStore facade: the single public entry point (seven methods, typed evidence, no raw connections, no contract constant) #178
Description
Part of #160 (group 2 — depends on group 1: #161 ADR, #162 single crate, #164 raw facts, #165 markers, #171 tool-result facts, #172 usage records, #175 project identity; can run in parallel with the other group-2 issues).
Problem
A Rust embedder today has to stitch together ai_hist_engine::{sync_local_at, discover_sessions_with_env, hydrate_session_at} (crates/ai-hist/src/lib.rs:44-62, :84-290) and ai_hist_core::{open_db_readonly, session_events_page, session_tool_calls_page, session_file_edits_page, session_relationships, source_evidence::read_session} with raw rusqlite::Connections and JSON strings (token_json, args_json, structured_patch_json). There is no single typed "give me everything about this session" call and no stability promise on the struct set. burn's consumer contract is the DerivedRecords trait in crates/relayburn-sdk/src/ingest/ingest.rs:1153-1173: per session, turns, content, compaction events, relationships, tool_result_events, user_turns, request_id_lookup.
#162 makes everything except this facade pub(crate). This issue defines the facade. Design rules: one entry type, seven methods, typed fields, no rusqlite in any signature, no contract-version constant (Cargo semver is the contract).
The API
pub struct SessionStore { /* db path, handles, locks */ }
pub enum SessionRef {
Id { source: Source, session_id: String },
Path { source: Source, path: PathBuf }, // hook fast path (#174): hydrate a transcript by locator
}
impl SessionStore {
pub fn open(opts: StoreOptions) -> Result<Self, Error>; // StoreOptions { db_path: Option<PathBuf>, home: Option<PathBuf>, roots: Option<ProviderRoots>, read_only: bool }
pub fn sync(&self, opts: SyncOptions) -> Result<SyncReport, Error>; // full local scan; takes SyncRunLock; SyncOptions { force: bool, sources: Option<Vec<Source>> }; report lists changed SessionRefs
pub fn hydrate(&self, r: &SessionRef, opts: HydrateOptions) -> Result<HydrateReport, Error>; // one session (+related unless include_related=false)
pub fn watch(&self, opts: WatchOptions) -> Result<WatchHandle, Error>; // #174 loop; WatchHandle: Iterator<Item = TickReport { changed: Vec<SessionRef>, .. }> + stop()
pub fn sessions(&self, q: CatalogQuery) -> impl Iterator<Item = Result<CatalogSession, Error>>; // keyset-paged internally
pub fn session(&self, r: &SessionRef, opts: SessionQuery) -> Result<Option<SessionEvidence>, Error>;
pub fn changes_since(&self, wm: Watermark, opts: ChangeQuery) -> impl Iterator<Item = Result<Change, Error>>; // #179; ChangeQuery { kinds, consumer: Option<&str> }
}
impl Source {
pub fn capabilities(self) -> SourceCapabilities; // static: declared evidence coverage (#169), relationship caps, usage accounting mode (#172), watch roots
}
pub struct SessionEvidence {
pub session: CatalogSession, // includes project_key (#175)
pub messages: Vec<Message>, // one per message_id: role, ts_ms, model, provider, request_id, stop_reason, turn_id, usage: Option<NormalizedUsage>, blocks: Vec<Block>
pub tool_calls: Vec<ToolCall>, // tool_use_id, name, target, args: serde_json::Value, is_error, error_signal
pub tool_results: Vec<ToolResult>, // tool_use_id, call_index, event_index, payload_bytes, payload_truncated, payload_hash, result_status, event_source, subagent_session_id
pub file_edits: Vec<FileEdit>, // structured_patch: Option<Value>
pub markers: Vec<Marker>, // #165
pub relationships: Vec<Relationship>, // delegated | fork | resume | continuation (#170)
pub requests: Vec<RequestUsage>, // #172
pub user_turns: Vec<UserTurn>, // #171 blocks
pub coverage: Vec<EvidenceKind>,
}Rules:
- All JSON columns arrive parsed into typed fields; the raw string is reachable via
raw_*()accessors on the struct, never as a public field. - Every struct is
#[non_exhaustive],Serialize,Clone, documented with which source populates which field. SessionQuery { include_text: bool /* default true */, kinds: Option<Vec<EvidenceKind>> }— burn'sContentStoreMode::{full, hash-only, off}maps ontoinclude_text. For very large sessionssession()still returns one struct; bounded paging is internal andSessionQuery.kindslets a consumer skip what it does not need. No per-kind public page methods.sync,hydrate,watchtake the sameSyncRunLock/ hydration locks the CLI uses (lib.rs:530-583,hydrate.rs:288); a concurrentai-hist syncyieldsError::SyncLockedwith the documented wait/timeout, never a silent no-op.- One
#[non_exhaustive] enum Errorwith codes mirroring the TS native errors (docs/architecture.md"Native errors") plusSyncLocked,WatermarkAheadOfStore,SourceMismatch. - No
SOURCING_CONTRACT_VERSION. The crate version is the contract; a checked-incargo public-apisnapshot ([G2] Embedder guide, example consumer crate, and a CI job that builds an out-of-tree Rust consumer against the published crates #182) fails CI on unreviewed surface changes. - The CLI, the napi addon, and both plugin Rust crates are rewritten to use only this facade (plus the
unstable-internalfeature from [G1] Merge ai-hist-core and ai-hist-engine into one publishedai-histcrate with feature gates, a release pipeline and a semver policy #162 where a maintenance verb truly needs more). That is the forcing function: if the plugins can live on the public API, burn can.
Tests
crates/ai-hist/tests/sourcing_api.rs: for every corpus fixture ([G1] Checked-in harness fixture corpus and parser characterization tests (port burn's 40 fixtures) #163),SessionStore::open→sync→sessions()→session()and assert theSessionEvidenceround-trips throughserde_jsonand matches the [G1] Checked-in harness fixture corpus and parser characterization tests (port burn's 40 fixtures) #163 snapshot field-for-field.- Concurrency: an in-flight
syncon one handle makessyncon a second handle returnSyncLockedwithin the documented timeout. cargo public-api -p ai-histsnapshot committed; CI diff.
Acceptance
- An out-of-workspace crate depending on the published
ai-histcan open, sync, iterate the catalog, and readusage,request_id,stop_reason,markers,tool_results[].payload_bytes, and continuity relationships on the corpus fixtures using nothing butSessionStoreand the structs above. grep -rn "rusqlite" crates/ai-hist/src/sourcing/shows nopubsignature.- The CLI, napi and plugins compile against the facade with the internal feature enabled only where listed in a
docs/sourcing-sdk.mdappendix.
Activity
[-][G2] Sourcing facade API: `ai_hist_engine::sourcing` — open store, sync/hydrate/watch, and typed per-session evidence for Rust embedders[/-][+][G2] `SessionStore` facade: the single public entry point (seven methods, typed evidence, no raw connections, no contract constant)[/+]