All 3 cloud models converged on real concurrency bugs in the SQLite stores:
- AppendVersion (HIGH): the seq key was `SELECT MAX(seq)+1` then INSERT in two
un-transacted statements with a NON-unique index, AND the Scan error was
swallowed (seq stayed 0 on failure). Concurrent appends could both land the
same seq, silently breaking newest-first ordering. Now: one transaction, the
Scan error is propagated, the (skill_id, seq) index is UNIQUE (the loser of a
race fails loudly), and an empty SkillID is rejected.
- MarkScheduledRun / MarkAgentScheduledRun (all 3): replaced the Get→mutate→Save
read-modify-write (lost-update window) with a single atomic UPDATE using
json_set, so a concurrent Mark/edit can't clobber it. json_set keeps the JSON
blob's NextRunAt/LastScheduledRunAt consistent with the indexed column;
RFC3339Nano matches Go's time encoding so the blob still round-trips (tested).
- Open: actually applies PRAGMA busy_timeout=5000 (the doc advertised it but it
was never set) — a contended writer waits instead of erroring SQLITE_BUSY.
- budgetStore.Add: rejects NaN/Inf secondsUsed (would irrecoverably poison the
column).
Triaged-but-kept: plaintext webhook secret (documented design, high-entropy URL
key, pre-existing); SQL()/free-form `where` helpers (no untrusted input reaches
them — defense-in-depth notes only).
Core go.sum still free of host/DB deps; contrib/store green (incl. a json_set
blob-round-trip test).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Establish the nested persistence module — the architectural reason the core
stays lean: a SEPARATE go.mod carrying modernc.org/sqlite (pure Go, no cgo), so
the SQLite driver NEVER enters the executus core go.sum. A static-binary host
(gadfly) importing only the core stays static; a host wanting turnkey
persistence imports contrib/store.
- sqlite.go: store.Open(dsn) -> *DB (one SQLite file), accessor-per-seam.
- budget_store.go: db.Budget() satisfies budget.BudgetStorage; Add() does the
7-day window rollover atomically inside a transaction (concurrent Adds can't
race the read-modify-write — the in-memory store's one weak spot).
- Conformance test: budget.NewDBBudget over the SQLite store passes the SAME
rolling-window contract as the in-memory store.
- CI: a new step builds + tests contrib/store on its own AND asserts it carries
the sqlite driver the core forbids (proof the split works). Verified: core
go.sum has 0 sqlite refs; contrib/store go.sum has it.
persona/skill/audit SQLite stores follow next (same JSON-blob + indexed-columns
pattern, sidestepping the three-layer field-loss footgun).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>