Session store
Source: packages/core/src/store/ · Spec: §3
JSON session files are the sole source of truth, date-partitioned by creation
date at .beetl/sessions/YYYY/MM/<id>.json (partition derived from the ID’s
yyyymmdd). .beetl/.gitignore excludes .cache/ and *.private.json.
- IDs —
BTL-<yyyymmdd>-<rand4>(base36), collision-checked at mint time. References resolve by canonical ID, unique suffix fragment (a4f2), or friendly ordinal#N(1-based over date-sorted IDs; re-resolvable after merges because the canonical ID is unique). Parsing is strict (L-03): ordinals are full-string (#1junkisINVALID_INPUT, never ordinal 1) and ID dates must be calendar-valid —parseIdrejects impossible dates anddoctorflags them on disk (ID_DATE_INVALID). - Atomic writes — every canonical write (session public/private files,
global mirror + registry, config) goes through
atomicWriteFile(store/atomic.ts): same-directory temp file → write → fsync → rename → directory fsync (best-effort where the platform can’t fsync directories). Readers never observe a truncated file; a crash leaves old or new content, never a mix. No canonical state is written via barewriteFileSync. - Locks + lost-update protection (ADR-0004) — per-session and
global-registry advisory directory locks with ownership tokens, held
across read-modify-write.
save()CAS-checks the on-disk predecessor (event-history prefix + legal lifecycle move) and returns a typedCONCURRENT_MODIFICATIONon contention;mutate(ref, fn)re-reads under the lock. Locks are never broken by age — recovery is tri-state liveness viabeetl unlock(--forceonly forunknown, never auto-selected);doctorreports held locks (LOCK_PRESENT). - Append-only discipline — a session is mutable while open; once terminal
on disk every write is refused (
TERMINAL_READONLY). Follow-ups are new sessions linking back viafix.refixOf/links.related. - Config integrity (H-10) —
.beetlmust be a directory (a stray file named.beetlis not a store), and an initialized store must hold exactly one supported config (.beetl/config.{json,toml}— YAML was dropped in ADR-0011): none →CONFIG_MISSING(a deleted config never silently reverts a private project to committed defaults), several →CONFIG_AMBIGUOUS(no silent precedence),config.inietc. → named in the error and flagged by doctor (CONFIG_UNSUPPORTED). Legacy stores adopt a config explicitly viabeetl adopt [--format]. All keys optional with zod-applied defaults. User identity lives in~/.beetl/config.json, never committed. - Updates —
applyUpdatessets dotted paths, forbids protected fields (id,schemaVersion,status,events,resolution,attribution), revalidates the full schema, and logs oneupdateevent naming the fields. - file-complete — builds the candidate in memory, validates all gates at once, and only then writes; on failure the store is untouched.
- doctor — validates schema, filename/partition placement, duplicate IDs, broken links, and config parseability. Errors exit 2. Terminal-edit detection via content hashes arrives with the global layer (M3).
- Invariant auditor (M-04) —
auditSession(corelifecycle/audit.ts) checks what the schema can’t: status↔resolution consistency, resolved terminal evidence (strict stores), event-log start/ordering, status-event continuity (replay must land on the stored status), and self-refix.save()/mutate()hard-fail withINVARIANT_VIOLATION;doctorreports the same violations (INVARIANT) plus cross-session refix cycles (REFIX_CYCLE).