pub struct Store { /* private fields */ }Expand description
The on-disk store: SQLite DB + blob store.
Implementations§
Source§impl Store
impl Store
Sourcepub fn open(db_path: &Path, blobs_dir: &Path) -> Result<Self>
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).
Sourcepub fn blobs(&self) -> &BlobStore
pub fn blobs(&self) -> &BlobStore
Borrow the blob store (e.g. to write file snapshots before referencing them from an event).
Sourcepub fn integrity_check(&self) -> Result<String>
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).
Sourcepub fn vacuum(&self) -> Result<()>
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.
Sourcepub fn prune_orphan_blobs(&self) -> Result<PruneStats>
pub fn prune_orphan_blobs(&self) -> Result<PruneStats>
Remove orphaned blob files and stale blobs rows (Area 3 / hh gc).
Two passes:
- For every blob file on disk, if no live
blobsrow references it (no row, or refcount ≤ 0), remove the file (and the stale row, if any). These are files leaked by a crash betweenBlobStore::putand the referencing event’s commit, or aStore::delete_sessionthat decremented the refcount but crashed before removing the file. - For every
blobsrow 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.
Sourcepub fn store_stats(&self, largest: u32) -> Result<StoreStats>
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).
Sourcepub fn create_session(&self, new: &NewSession) -> Result<CreatedSession>
pub fn create_session(&self, new: &NewSession) -> Result<CreatedSession>
Create a new session row (FR-1.2).
Sourcepub fn finalize_session(
&self,
id: &str,
ended_at: i64,
exit_code: Option<i32>,
status: SessionStatus,
) -> Result<()>
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).
Sourcepub fn set_session_adapter_meta(
&self,
id: &str,
model: Option<&str>,
usage_json: Option<&Value>,
status: AdapterStatus,
) -> Result<()>
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.
Sourcepub fn list_sessions(&self, limit: u32) -> Result<Vec<SessionRow>>
pub fn list_sessions(&self, limit: u32) -> Result<Vec<SessionRow>>
List sessions newest-first (FR-5.1).
Sourcepub fn resolve_session(&self, id_or_last: &str) -> Result<String>
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.
Sourcepub fn session_started_at(&self, id: &str) -> Result<i64>
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.
Sourcepub fn session_short_id(&self, id: &str) -> Result<String>
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.
Sourcepub fn search(
&self,
query: &str,
filters: &SearchFilters,
) -> Result<Vec<SearchResult>>
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.
Sourcepub fn delete_session(&self, id: &str) -> Result<usize>
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.
Sourcepub fn scan_session(
&self,
id: &str,
detectors: &Detectors,
) -> Result<Vec<ScanFinding>>
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).
Sourcepub fn redact_session(
&self,
id: &str,
detectors: &Detectors,
) -> Result<RedactOutcome>
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.
Sourcepub fn import(&self, bundle: &Bundle) -> Result<CreatedSession>
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.
Sourcepub fn session_stats(&self, id: &str) -> Result<(i64, i64)>
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).
Sourcepub fn session_step_count(&self, id: &str) -> Result<i64>
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.
Sourcepub fn list_events(&self, session_id: &str) -> Result<Vec<EventRow>>
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).
Sourcepub fn get_session(&self, id: &str) -> Result<SessionRow>
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.
Sourcepub fn list_event_index(&self, session_id: &str) -> Result<Vec<EventIndexRow>>
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).
Sourcepub fn get_event_detail(&self, id: i64) -> Result<EventDetail>
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.
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.
Sourcepub fn for_each_event_detail(
&self,
session_id: &str,
emit: impl FnMut(EventDetail) -> Result<()>,
) -> Result<()>
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.
Sourcepub fn for_each_event_raw(
&self,
session_id: &str,
emit: impl FnMut(RawEventRow) -> Result<()>,
) -> Result<()>
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.
Sourcepub fn assign_steps(&self, session_id: &str) -> Result<()>
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.
Sourcepub fn mark_stale_interrupted(&self) -> Result<usize>
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.
Sourcepub fn event_writer(&self) -> Result<EventWriter>
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).
Sourcepub fn event_writer_with_redactor(
&self,
redactor: Option<Arc<Detectors>>,
) -> Result<EventWriter>
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.