Skip to content

[G2] SessionStore facade: the single public entry point (seven methods, typed evidence, no raw connections, no contract constant) #178

Closed

Description

@willwashburn

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's ContentStoreMode::{full, hash-only, off} maps onto include_text. For very large sessions session() still returns one struct; bounded paging is internal and SessionQuery.kinds lets a consumer skip what it does not need. No per-kind public page methods.
  • sync, hydrate, watch take the same SyncRunLock / hydration locks the CLI uses (lib.rs:530-583, hydrate.rs:288); a concurrent ai-hist sync yields Error::SyncLocked with the documented wait/timeout, never a silent no-op.
  • One #[non_exhaustive] enum Error with codes mirroring the TS native errors (docs/architecture.md "Native errors") plus SyncLocked, WatermarkAheadOfStore, SourceMismatch.
  • No SOURCING_CONTRACT_VERSION. The crate version is the contract; a checked-in cargo public-api snapshot ([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-internal feature from [G1] Merge ai-hist-core and ai-hist-engine into one published ai-hist crate 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

Acceptance

  • An out-of-workspace crate depending on the published ai-hist can open, sync, iterate the catalog, and read usage, request_id, stop_reason, markers, tool_results[].payload_bytes, and continuity relationships on the corpus fixtures using nothing but SessionStore and the structs above.
  • grep -rn "rusqlite" crates/ai-hist/src/sourcing/ shows no pub signature.
  • The CLI, napi and plugins compile against the facade with the internal feature enabled only where listed in a docs/sourcing-sdk.md appendix.

Out of scope

Change-feed internals (#179); napi/TS exposure (#181).

Activity

  1. changed the title [-][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)[/+] on Sep 19, 2026
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