Files
ai-harness/docs/08-storage.md
T
Erik Simon 8b16348b4c M0: scaffold Cargo workspace, core types, event bus, permission engine, storage actor
Seven-crate workspace per docs/01-architecture.md. harness-core gets the
domain types (Session/Message/Part/ToolState), a broadcast EventBus, the
last-match-wins wildcard permission evaluator, and a SQLite storage actor
(dedicated thread + mpsc, JSON-blob rows) with roundtrip tests. All other
crates are compiling stubs. CI runs fmt/clippy -D warnings/test.
2026-07-08 16:28:42 +02:00

2.4 KiB

08 — Storage

harness-core/src/store/. SQLite per project: ~/.local/share/ai-harness/db/<project-slug>.sqlite (slug = sanitized worktree path + hash), WAL mode.

Schema

CREATE TABLE session (id TEXT PRIMARY KEY, parent_id TEXT, created_at INTEGER, updated_at INTEGER,
                      data TEXT NOT NULL);          -- full Session JSON
CREATE TABLE message (id TEXT PRIMARY KEY, session_id TEXT NOT NULL, created_at INTEGER,
                      data TEXT NOT NULL);
CREATE TABLE part    (id TEXT PRIMARY KEY, message_id TEXT NOT NULL, session_id TEXT NOT NULL,
                      idx INTEGER NOT NULL, data TEXT NOT NULL);
CREATE TABLE job     (task_id TEXT PRIMARY KEY, parent_session_id TEXT NOT NULL, data TEXT NOT NULL);
CREATE TABLE meta    (key TEXT PRIMARY KEY, value TEXT);   -- schema_version, cache stamps

CREATE INDEX message_session ON message(session_id);
CREATE INDEX part_message    ON part(message_id);
CREATE INDEX session_parent  ON session(parent_id);

JSON data columns (opencode's approach): schema evolution without a migration per field. IDs are ULIDs, so ORDER BY id is chronological.

Storage actor

One dedicated thread owns the rusqlite::Connection; commands arrive over mpsc::Sender<StoreCmd>, each carrying a oneshot reply channel. store::api exposes typed async facades: upsert_session, upsert_message, upsert_part, messages(session), parts(message), sessions(), jobs(parent), …

Rationale vs sqlx/async drivers: single writer, embedded DB, tiny schema — an actor thread is simpler, avoids async-driver complexity, and serializes writes for free.

Write cadence

  • Messages and tool-state transitions: written immediately.
  • Streaming text deltas: buffered by the processor's DeltaFlusher — flushed to the part row every 50 ms or 2 KB, while AppEvent::PartDelta goes to the UI immediately. UI is realtime; SQLite writes are throttled.
  • Synthetic parts (job board injection, reminders) are not persisted.

Session resume

  • Session list from the DB; opening a session replays its messages/parts into the TUI view.
  • Crash recovery: on load, an unfinished assistant message (no finished, no error) is marked aborted; in-flight tool parts become Error { "interrupted" }.
  • Job records reload from the job table so the board survives restarts (running jobs whose runs died are marked Error on load).