Skip to content

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.

  • IDsBTL-<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 (#1junk is INVALID_INPUT, never ordinal 1) and ID dates must be calendar-valid — parseId rejects impossible dates and doctor flags 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 bare writeFileSync.
  • 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 typed CONCURRENT_MODIFICATION on contention; mutate(ref, fn) re-reads under the lock. Locks are never broken by age — recovery is tri-state liveness via beetl unlock (--force only for unknown, never auto-selected); doctor reports 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 via fix.refixOf / links.related.
  • Config integrity (H-10).beetl must be a directory (a stray file named .beetl is 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.ini etc. → named in the error and flagged by doctor (CONFIG_UNSUPPORTED). Legacy stores adopt a config explicitly via beetl adopt [--format]. All keys optional with zod-applied defaults. User identity lives in ~/.beetl/config.json, never committed.
  • UpdatesapplyUpdates sets dotted paths, forbids protected fields (id, schemaVersion, status, events, resolution, attribution), revalidates the full schema, and logs one update event 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 (core lifecycle/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 with INVARIANT_VIOLATION; doctor reports the same violations (INVARIANT) plus cross-session refix cycles (REFIX_CYCLE).