db.Personas() satisfies persona.Storage over SQLite. Each Agent is stored as a
JSON blob with extracted indexed columns (owner_id, name, webhook_secret,
chatbot_channel_filter, schedule, next_run_at) — so the WHOLE struct round-trips
(no domain<->GORM<->DB field-loss footgun) while the lookups stay indexable.
Test proves the round-trip preserves nested + map fields (SkillPalette,
StateReactEmoji), the owner/name + webhook + chatbot-filter queries, the
scheduled-due query, and MarkAgentScheduledRun clearing the due window.
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>