feat: add SQLite cache store for incremental session persistence

Implement the caching layer that enables fast subsequent runs by
persisting parsed session data in SQLite:

- store/schema.go: DDL for three tables — sessions (primary metrics
  with file_mtime_ns/file_size for change detection), session_models
  (per-model breakdown, FK cascade on delete), and file_tracker
  (path -> mtime+size mapping for cache invalidation). Indexes on
  start_time and project for efficient time-range and filter queries.

- store/cache.go: Cache struct wrapping database/sql with WAL mode
  and synchronous=normal for concurrent read safety and write
  performance. Key operations:

  * Open: creates the cache directory, opens/creates the database,
    and ensures the schema is applied (idempotent via IF NOT EXISTS).

  * GetTrackedFiles: returns the mtime/size map used by the pipeline
    to determine which files need reparsing.

  * SaveSession: transactional upsert of session stats + model
    breakdown + file tracker entry. Uses INSERT OR REPLACE to handle
    both new files and files that changed since last parse.

  * LoadAllSessions: batch-loads all cached sessions with a two-pass
    strategy — first loads session rows, then batch-loads model data
    with an index map for O(1) join, avoiding N+1 queries.

  Uses modernc.org/sqlite (pure-Go, no CGO) for zero-dependency
  cross-platform builds.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
teernisse
2026-02-19 13:01:27 -05:00
parent ad484a2a6f
commit 0e9091f56e
2 changed files with 288 additions and 0 deletions

49
internal/store/schema.go Normal file
View File

@@ -0,0 +1,49 @@
package store
const schemaSQL = `
CREATE TABLE IF NOT EXISTS sessions (
session_id TEXT PRIMARY KEY,
project TEXT NOT NULL,
project_path TEXT,
file_path TEXT NOT NULL,
is_subagent INTEGER NOT NULL DEFAULT 0,
parent_session TEXT,
start_time TEXT,
end_time TEXT,
duration_secs INTEGER,
user_messages INTEGER,
api_calls INTEGER,
input_tokens INTEGER,
output_tokens INTEGER,
cache_creation_5m INTEGER,
cache_creation_1h INTEGER,
cache_read_tokens INTEGER,
estimated_cost REAL,
cache_hit_rate REAL,
file_mtime_ns INTEGER NOT NULL,
file_size INTEGER NOT NULL,
parsed_at TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS session_models (
session_id TEXT NOT NULL REFERENCES sessions(session_id) ON DELETE CASCADE,
model TEXT NOT NULL,
api_calls INTEGER,
input_tokens INTEGER,
output_tokens INTEGER,
cache_creation_5m INTEGER,
cache_creation_1h INTEGER,
cache_read_tokens INTEGER,
estimated_cost REAL,
PRIMARY KEY (session_id, model)
);
CREATE TABLE IF NOT EXISTS file_tracker (
file_path TEXT PRIMARY KEY,
mtime_ns INTEGER NOT NULL,
size_bytes INTEGER NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_sessions_start ON sessions(start_time);
CREATE INDEX IF NOT EXISTS idx_sessions_project ON sessions(project);
`