pub struct SessionStore { /* private fields */ }Expand description
The stores of one checkout.
Reading needs no permission. Writing needs SessionStore::writable, which is the
flag ADR 0047 promised: while lib/session.sh owns the lifecycle, nothing that is
merely served can write a session record, and a caller that has not said out loud
that it means to is refused by StoreError::NotWritable.
use majordomus_cli::session::SessionStore;
let dir = tempfile::tempdir().unwrap();
let reading = SessionStore::read_only(dir.path());
assert!(!reading.is_writable());
assert!(SessionStore::writable(dir.path()).is_writable());Implementations§
Source§impl SessionStore
impl SessionStore
Sourcepub fn read_only(root: impl Into<PathBuf>) -> Self
pub fn read_only(root: impl Into<PathBuf>) -> Self
A store that reads. Every projection uses this one, and a caller that reaches the write path through it is refused rather than allowed to become a second writer.
use majordomus_cli::session::SessionStore;
let dir = tempfile::tempdir().unwrap();
assert!(!SessionStore::read_only(dir.path()).is_writable());Sourcepub fn writable(root: impl Into<PathBuf>) -> Self
pub fn writable(root: impl Into<PathBuf>) -> Self
A store that may write. Not reachable from any surface in this change: the cutover
(ADR 0047) is what routes majordomus session close through it.
use majordomus_cli::session::SessionStore;
let dir = tempfile::tempdir().unwrap();
assert!(SessionStore::writable(dir.path()).is_writable());Sourcepub fn is_writable(&self) -> bool
pub fn is_writable(&self) -> bool
May this store write? The question the closer asks first, before it touches the disk at all.
use majordomus_cli::session::SessionStore;
let dir = tempfile::tempdir().unwrap();
assert!(!SessionStore::read_only(dir.path()).is_writable());Sourcepub fn root(&self) -> &Path
pub fn root(&self) -> &Path
The repository root it is over, which every path below is composed from.
use majordomus_cli::session::SessionStore;
let dir = tempfile::tempdir().unwrap();
assert_eq!(SessionStore::read_only(dir.path()).root(), dir.path());Sourcepub fn claim_dir(&self) -> PathBuf
pub fn claim_dir(&self) -> PathBuf
Where the claims live: the local half, because a claim is a fact about this machine and the record store is the shared half of the layer (ADR 0014).
use majordomus_cli::session::SessionStore;
let dir = tempfile::tempdir().unwrap();
assert!(SessionStore::read_only(dir.path()).claim_dir().ends_with("sessions-closing"));Sourcepub fn record_dir(&self) -> PathBuf
pub fn record_dir(&self) -> PathBuf
Where the immutable records live: the tracked half, because a closed episode is a shared object of the layer and travels with a clone.
use majordomus_cli::session::SessionStore;
let dir = tempfile::tempdir().unwrap();
assert!(SessionStore::read_only(dir.path()).record_dir().ends_with(".ai/repo/sessions"));Sourcepub fn records_of(&self, episode: &EpisodeId) -> Vec<String>
pub fn records_of(&self, episode: &EpisodeId) -> Vec<String>
Every record in the store that names episode, repository-relative, in canonical
order — which for these filenames is chronological, because the name begins with a
compact UTC timestamp.
The scan reads front matter rather than grepping the whole file, so a record whose
body quotes another episode’s id is not counted as that episode’s record. The
shell’s grep -rl "^session_id: <id>$" has the same intent and reaches it by
anchoring; this reads the field.
use majordomus_cli::session::{EpisodeId, SessionStore};
let dir = tempfile::tempdir().unwrap();
let records = dir.path().join(".ai/repo/sessions");
std::fs::create_dir_all(&records).unwrap();
std::fs::write(
records.join("20260910T210426Z--a.md"),
"---\nschema: session/v1\nsession_id: s-20260910205542-e2a6\n---\n",
).unwrap();
std::fs::write(
records.join("20260910T211503Z--b.md"),
"---\nschema: session/v1\nsession_id: s-20260910210510-8f20\n---\n\nquotes s-20260910205542-e2a6 in prose\n",
).unwrap();
let store = SessionStore::read_only(dir.path());
let id = EpisodeId::parse("s-20260910205542-e2a6").unwrap();
let found = store.records_of(&id);
assert_eq!(found.len(), 1, "the field, not the prose");
assert!(found[0].ends_with("20260910T210426Z--a.md"));Sourcepub fn close<F>(
&self,
episode: &Episode,
outcome: Outcome,
compose: F,
) -> Result<CloseReport, StoreError>
pub fn close<F>( &self, episode: &Episode, outcome: Outcome, compose: F, ) -> Result<CloseReport, StoreError>
Close episode, exactly once.
compose is called only by the caller that took the claim, and only after it did:
composing a record is the expensive part — a git status, a walk of the episode’s
ledger lines — and doing it before the claim is what leaves the window the shell’s
scan cannot close. It returns the record’s full text.
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-20260910205542-e2a6").unwrap();
let episode = Episode::opening(id.clone(), Path::new("/r"))
.opened_at("2026-09-10T20:55:42Z", "master", "9f3a2a0", 1_000);
let body = || format!("---\nschema: session/v1\nkind: session\nsession_id: {}\n---\n", id);
// the first close writes
let first = store.close(&episode, Outcome::Closed, body).unwrap();
assert_eq!(first.outcome, CloseOutcome::Closed);
assert!(first.record.starts_with(".ai/repo/sessions/"));
// the second, third and fourth do not, and each names the record that stands
for _ in 0..3 {
let again = store.close(&episode, Outcome::Closed, body).unwrap();
assert_eq!(again.outcome, CloseOutcome::AlreadyClosed);
assert_eq!(again.record, first.record);
}
assert_eq!(store.records_of(&id).len(), 1, "one episode, one record");Sourcepub fn claim_path(&self, episode: &EpisodeId) -> PathBuf
pub fn claim_path(&self, episode: &EpisodeId) -> PathBuf
Where the claim of episode is. The episode id is parsed, so it is one inert path
segment and nothing a provider sends can name a file outside the directory.
use majordomus_cli::session::{EpisodeId, SessionStore};
let dir = tempfile::tempdir().unwrap();
let id = EpisodeId::parse("s-20260910205542-e2a6").expect("an episode id");
let claim = SessionStore::read_only(dir.path()).claim_path(&id);
assert!(claim.ends_with("s-20260910205542-e2a6.claim"));
assert!(!claim.exists(), "asking where it would be does not take it");Trait Implementations§
Source§impl Clone for SessionStore
impl Clone for SessionStore
Source§fn clone(&self) -> SessionStore
fn clone(&self) -> SessionStore
1.0.0 (const: unstable) · Source§fn clone_from(&mut self, source: &Self)
fn clone_from(&mut self, source: &Self)
source. Read more