Skip to main content

Store

Struct Store 

Source
pub struct Store { /* private fields */ }
Expand description

The on-disk store: SQLite DB + blob store.

Implementations§

Source§

impl Store

Source

pub fn open(db_path: &Path, blobs_dir: &Path) -> Result<Self>

Open or create the store at db_path, applying migrations idempotently (DR-1). The data and blob directories are created with 0700 permissions (NFR-4).

Source

pub fn blobs(&self) -> &BlobStore

Borrow the blob store (e.g. to write file snapshots before referencing them from an event).

Source

pub fn integrity_check(&self) -> Result<String>

Run PRAGMA integrity_check and return its result. A healthy database yields "ok"; a corrupt one yields the first reported fault line. Used by hh doctor as a non-mutating health probe (integrity_check is read-only).

Source

pub fn vacuum(&self) -> Result<()>

Reclaim free pages and shrink hh.db on disk (Area 3 / hh gc). VACUUM rebuilds the database file, so it must run with no other connection open and outside a transaction — hh gc is the only caller and owns the store’s sole connection, with the single-writer task not running. A failure here (e.g. another process holds the file) is surfaced; it never corrupts the store.

Source

pub fn prune_orphan_blobs(&self) -> Result<PruneStats>

Remove orphaned blob files and stale blobs rows (Area 3 / hh gc). Two passes:

  1. For every blob file on disk, if no live blobs row references it (no row, or refcount ≤ 0), remove the file (and the stale row, if any). These are files leaked by a crash between BlobStore::put and the referencing event’s commit, or a Store::delete_session that decremented the refcount but crashed before removing the file.
  2. For every blobs row with a positive refcount, if its file is missing on disk (deleted out of band), remove the row so a future reference re-creates the blob instead of pointing at a missing file.

See PruneStats for what is reported. Never panics on a stray file in the blobs directory; unparseable names are skipped.

Source

pub fn store_stats(&self, largest: u32) -> Result<StoreStats>

Whole-store inventory: counts, per-file disk footprint (the hh.db file plus its -wal/-shm sidecars and the compressed blob directory), and the largest sessions by event count (Area 3 / hh stats).

Source

pub fn create_session(&self, new: &NewSession) -> Result<CreatedSession>

Create a new session row (FR-1.2).

Source

pub fn finalize_session( &self, id: &str, ended_at: i64, exit_code: Option<i32>, status: SessionStatus, ) -> Result<()>

Finalize a session with end metadata (FR-1.6) and checkpoint WAL (NFR-3 fsync-on-finalize).

Source

pub fn set_session_adapter_meta( &self, id: &str, model: Option<&str>, usage_json: Option<&Value>, status: AdapterStatus, ) -> Result<()>

Update a session’s adapter-reported metadata at finalize (FR-1.5): model name, token-usage JSON, and final adapter status. model and usage_json use COALESCE so passing None (the adapter saw no assistant records) does not clobber a value an earlier update set; adapter_status is always overwritten with the outcome’s status.

Source

pub fn list_sessions(&self, limit: u32) -> Result<Vec<SessionRow>>

List sessions newest-first (FR-5.1).

Source

pub fn resolve_session(&self, id_or_last: &str) -> Result<String>

Resolve a session id per FR-3.1: last → most recently started; a short-id prefix → the unique session whose short id starts with it (ambiguity is an error listing candidates); a full id → itself.

Source

pub fn session_started_at(&self, id: &str) -> Result<i64>

Look up a session’s started_at (unix-ms). Used by the MCP proxy in attached mode to express event timestamps relative to the parent session’s clock, so MCP events interleave correctly on the parent’s timeline (FR-2). Errors if the session id does not exist.

Source

pub fn session_short_id(&self, id: &str) -> Result<String>

Look up a session’s short id (6 hex). Used by the MCP proxy to print the parent session’s short id in its epilogue when attaching (FR-2). Errors if the session id does not exist.

Source

pub fn search( &self, query: &str, filters: &SearchFilters, ) -> Result<Vec<SearchResult>>

Search events across all sessions using FTS5 full-text search. Returns matching events with highlighted snippets, ordered by session start time descending then event timestamp ascending.

The query parameter uses FTS5 query syntax: words, phrases in quotes, prefix with *, AND/OR/NOT operators. An empty query returns no results.

Source

pub fn delete_session(&self, id: &str) -> Result<usize>

Delete a session and garbage-collect blobs no longer referenced by any event (FR-6.1). Returns the number of blob files removed.

Refcount semantics: the writer bumps a blob’s refcount once per referencing event (see insert_event’s ON CONFLICT DO UPDATE SET refcount = refcount + 1), so deleting a session must decrement by the number of this session’s events that reference each blob — not by 1. Otherwise a session that references the same content blob from two events (e.g. two files with identical content) would leave the blob leaked at refcount 1 after deletion. Grouping by blob_hash with COUNT(*) and decrementing by that count keeps the books balanced and lets a blob shared across sessions survive the deletion of one.

Source

pub fn scan_session( &self, id: &str, detectors: &Detectors, ) -> Result<Vec<ScanFinding>>

Scan a session for secrets without mutating anything (hh scan, docs/redaction-design.md). Runs detectors over the session’s recorded command line, every event’s summary and inline body, and every referenced blob’s content (JSON-aware for structured payloads). Findings carry the secret kind, location, and hash8 — never the secret itself. Each distinct blob is scanned once, attributed to its first referencing event (and to the file path when that event is a file_change).

Source

pub fn redact_session( &self, id: &str, detectors: &Detectors, ) -> Result<RedactOutcome>

Redact a session in place (hh redact, docs/redaction-design.md): rewrite every event summary/body, rewrite affected blobs under new hashes (repointing this session’s references and moving refcounts), securely delete originals that reach refcount zero, append a redaction audit event, and purge plaintext remnants from the WAL and freelist (checkpoint + VACUUM). Irreversible by design.

Refuses a session still in recording status — a live writer could re-insert plaintext mid-rewrite.

Source

pub fn import(&self, bundle: &Bundle) -> Result<CreatedSession>

Import a validated crate::bundle::Bundle (hh import file.hh) as a brand-new local session: a fresh UUIDv7 id, imported_from set to the bundle’s original session id, every referenced blob written content-addressed into this store, and every event/file_change re-inserted with fresh row ids.

correlates is resolved from the bundle’s seq-based scheme in a second pass after every event has a new id — a single forward pass cannot always resolve it inline, since a tool_result can legitimately correlate to a tool_call that sorts after it by ts_ms (a concurrent source; see timeline.rs’s “out of order result” test). step/ts_ms are reused verbatim from the bundle — they were already valid ordinals for this exact event sequence, so there is no need to re-run Self::assign_steps.

Blob writes happen before the transaction (content-addressed and idempotent — safe even if the import is retried), then every event insert runs in one transaction on the store’s own connection, the same pattern Self::redact_session/Self::delete_session use: this is a one-shot batch operation, not concurrent live recording, so the writer-thread/mpsc machinery (Self::event_writer) is unnecessary.

Source

pub fn session_stats(&self, id: &str) -> Result<(i64, i64)>

Return (event_count, files_changed) for a session (FR-1.6 epilogue). event_count is the total event rows; files_changed is the count of distinct paths in file_changes. Reads from the store’s own connection (concurrent with the writer under WAL).

Source

pub fn session_step_count(&self, id: &str) -> Result<i64>

Count the distinct stored step ordinals for a session (FR-3.4). A correlated tool_result shares its tool_call’s step, so this is COUNT(DISTINCT step) over non-null steps — not a count of semantic events. Step ordinals are stored at finalize (ADR-0002; see Self::assign_steps) and self-healed on Store::open.

Source

pub fn list_events(&self, session_id: &str) -> Result<Vec<EventRow>>

Read all events for a session as EventRows, ordered by (ts_ms, id) (the order the FR-3.4 step pass assigns in). Used by Self::assign_steps and by replay/inspect (FR-3/FR-4, future).

Source

pub fn get_session(&self, id: &str) -> Result<SessionRow>

Fetch one session’s full row by its (already-resolved) full id (FR-3.2 header; FR-4). Call Self::resolve_session first to turn a short id / last into the full id this expects.

Source

pub fn list_event_index(&self, session_id: &str) -> Result<Vec<EventIndexRow>>

Read the full per-event index for a session (FR-3.5 eager index load): every event (including terminal_output) with its one-line summary, but not the (potentially large) body_json/blob payload — that is fetched lazily per selected row via Self::get_event_detail. Ordered by (ts_ms, id).

Source

pub fn get_event_detail(&self, id: i64) -> Result<EventDetail>

Fetch one event’s full detail by row id (FR-3.5 lazy body load): resolves a blob-overflowed body_json transparently (fetches and decompresses the blob, returning its parsed content in place of the {"overflow": true, ...} envelope) and attaches the file_changes row for FileChange events. Errors if the id does not exist.

Source

pub fn get_correlated_event( &self, event_id: i64, correlates: Option<i64>, ) -> Result<Option<EventDetail>>

Fetch the event correlated with event_id (FR-3.2 MCP/tool call+result pairing): if correlates is Some, that is the request/call this event points at; otherwise look for an event that points at event_id (its response/result). Returns None if there is no correlated event either way.

Source

pub fn for_each_event_detail( &self, session_id: &str, emit: impl FnMut(EventDetail) -> Result<()>, ) -> Result<()>

Stream every event detail of session_id, in (ts_ms, id) order, calling emit once per event. This is the constant-memory path for hh inspect --json (FR-4): a single SQL cursor with a LEFT JOIN to file_changes walks the session row by row, and any blob-overflow body is resolved inline (the same overflow-envelope path as Self::get_event_detail), so the whole session is never collected into RAM — unlike Self::list_event_index followed by per-row Self::get_event_detail, which is the right path for the interactive timeline but not for streaming a 100k-event session to NDJSON (Area 2).

Each EventDetail is built, emitted, and dropped before the next row is fetched, so peak memory is O(1) regardless of session size. If emit returns Err, iteration stops immediately and the error propagates; a row-decode error likewise short-circuits.

Source

pub fn for_each_event_raw( &self, session_id: &str, emit: impl FnMut(RawEventRow) -> Result<()>, ) -> Result<()>

Stream every event of session_id exactly as stored — no blob- overflow resolution, plus the referenced blob’s size (for_each_event_detail’s resolution silently loses the association between an event and a binary/non-JSON blob it references — see crate::event::RawEventRow). Used by hh-core::bundle (hh export --bundle), which must carry every referenced blob byte-for-byte, not just the ones that happen to resolve as JSON.

Source

pub fn assign_steps(&self, session_id: &str) -> Result<()>

Run the FR-3.4 step-assignment pass for a session and write the ordinals back to events.step in one transaction on the main connection (ADR-0002). Call after the writer is flushed + joined so there is no within-process writer contention. Idempotent.

Source

pub fn mark_stale_interrupted(&self) -> Result<usize>

Mark any sessions still in recording status as interrupted (FR-1.7 crash-safety). Called on Store::open so a crashed hh run is readable on the next invocation.

§SRS deviation (flagged)

FR-1.7 says “detected via missing end timestamp + no live PID”. The SRS schema (§4.1 sessions) has no PID column, so we cannot check liveness; we mark all recording sessions as interrupted on open. This is correct for the common case (one hh at a time) but would mis-mark a genuinely-live recording if two hh runs share a data dir — which the SRS does not support in v0.1 anyway. See the decisions summary.

Source

pub fn event_writer(&self) -> Result<EventWriter>

Spawn the single-writer task and return a handle for appending events. The writer opens its own Connection (never shared with the store’s).

Source

pub fn event_writer_with_redactor( &self, redactor: Option<Arc<Detectors>>, ) -> Result<EventWriter>

Like Self::event_writer, but with an optional record-time redactor: every event’s summary and body_json are redacted on the writer thread before the INSERT (docs/redaction-design.md enforcement point 1 — nothing hits disk unredacted). Blob content is covered by the other chokepoint, crate::blob::BlobStore::put on a crate::blob::BlobStore::with_redactor store.

Auto Trait Implementations§

§

impl !Freeze for Store

§

impl !RefUnwindSafe for Store

§

impl !Sync for Store

§

impl !UnwindSafe for Store

§

impl Send for Store

§

impl Unpin for Store

§

impl UnsafeUnpin for Store

Blanket Implementations§

Source§

impl<T> Any for T
where T: 'static + ?Sized,

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
Source§

impl<T> Borrow<T> for T
where T: ?Sized,

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

Source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
Source§

impl<ST, DT> CastableFrom<ST, Initialized, Initialized> for DT
where ST: ?Sized, DT: ?Sized,

Source§

impl<ST, DT> CastableFrom<ST, Uninit, Uninit> for DT
where ST: ?Sized, DT: ?Sized,

Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

Source§

impl<T, U> Into<U> for T
where U: From<T>,

Source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

Source§

impl<T> Read<Exclusive, BecauseExclusive> for T
where T: ?Sized,

Source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

Source§

type Error = Infallible

The type returned in the event of a conversion error.
Source§

fn try_from(value: U) -> Result<T, <T as TryFrom<U>>::Error>

Performs the conversion.
Source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

Source§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
Source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.