Skip to main content

Module store

Module store 

Source
Expand description

Persistence: the open-episode store, the closed-record store, and closing exactly once.

§The incident this module is built around

Episode s-20260909152316-024f has four immutable records in .ai/repo/sessions/: one at 21:41:35 with 3 changed files and 94 commits, and three the next morning at 10:39:02, 10:39:18 and 10:39:31 with 120, 121 and 122 changed files and 296 commits. A provider’s end event can fire more than once — thirteen and fifteen seconds apart, in this case — and mj_publish_record is deliberately designed never to collide, so each close wrote a new file rather than failing. The record is immutable by contract; the store had four of them.

lib/session.sh was hardened after the incident: before publishing, it greps the store for a record already carrying the episode’s id and, finding one, keeps it. That removes the repeat and not the race. Between the grep and the publish sit a git status, a ledger walk over every line of the episode, and the composition of the record — hundreds of milliseconds on a loaded machine. Two processes that grep inside that window both see nothing and both publish. A check without an atomic claim is a time-of-check-to- time-of-use window with the expensive part inside it.

§The guarantee

SessionStore::close takes an atomic claim before the expensive part: state/sessions-closing/<episode id>.claim, created with O_EXCL. The filesystem decides, once, which caller created it; every other caller — another thread, another process, another provider window on the same machine — observes the claim and returns a diagnosable outcome instead of writing. The claim is the whole mechanism: it is not an in-process mutex, so the test below that spawns real child processes exercises the same primitive as the one that spawns threads.

Three outcomes, and only one of them writes:

  • CloseOutcome::Closed — this caller took the claim and published the record.
  • CloseOutcome::AlreadyClosed — a record for this episode already stands. Not an error: a provider’s second end event is that provider’s normal behaviour. The existing record’s path is returned, because every reader downstream must name the record that exists rather than the file that was not written.
  • CloseOutcome::AlreadyClosing — another caller holds the claim right now. The caller is told which episode and that it may retry; nothing is written.

And one diagnostic that is not an outcome of this close at all: CloseReport::duplicates names every historical record of the episode beyond the first. This repository already contains four records for one episode, and a closer that silently kept the first would leave a reader to discover the other three. Damage that already exists is reported, never added to.

use majordomus_cli::session::{CloseOutcome, Episode, EpisodeId, Outcome, SessionStore};
use std::path::Path;

let dir = tempfile::tempdir().unwrap();
let store = SessionStore::writable(dir.path());
let id = EpisodeId::parse("s-20260909152316-024f").expect("the id from the incident");
let episode = Episode::opening(id.clone(), Path::new("/r"))
    .opened_at("2026-09-09T15:23:16Z", "master", "9f3a2a0", 1_000);
let body = || format!("---\nschema: session/v1\nsession_id: {}\n---\n", id);

let first = store.close(&episode, Outcome::Closed, body).unwrap();
let second = store.close(&episode, Outcome::Closed, body).unwrap();
assert_eq!(first.outcome, CloseOutcome::Closed);
assert_eq!(second.outcome, CloseOutcome::AlreadyClosed);
assert_eq!(store.records_of(&id).len(), 1, "the incident produced four");

Structs§

CloseReport
What a close said: the episode, the outcome, the canonical record, any historical duplicates of it, and a line a person can act on.
SessionStore
The stores of one checkout.

Enums§

CloseOutcome
What a close did: wrote the record, found one already standing, found another caller inside the window, or was refused by the machine.
StoreError
What can go wrong closing an episode, as distinct from an outcome that is not Closed. An outcome is a fact about the episode; an error is a fact about the disk, or about a caller that reached the write path without saying it meant to.

Constants§

CLAIM_DIR
Where a close takes its claim, relative to the repository root. A directory of its own rather than a lock beside the record, because the record store is the shared half of the layer and a claim is a fact about this machine (ADR 0014).
OPEN_DIR
Where the open episodes live, relative to the repository root.
RECORD_DIR
Where the immutable records live, relative to the repository root. Tracked: a closed episode is a shared object of the layer.