Expand description
The session domain: one typed account of what an episode is, what identifies it, how it moves, and how it is closed exactly once.
§Why this module exists
This repository runs two programs over one subsystem. The shell tool (lib/session.sh,
lib/capture.sh, lib/checkpoint.sh, lib/handover.sh) owns the writes, the lifecycle
and the state machine; this executable owns the typed read model, the server, the HTTP
API, MCP and the Cockpit. crate::capability::builtin::continuity opens by declaring
the split — “It is read, never written. The lifecycle is the shell tool’s.”
ADR 0052 found what the split costs. For six days this repository wrote no checkpoint
and no handover while doctor and watch reported health, because the invariant that
would have caught it — lifecycle events are arriving, therefore derived state must
advance — spans both halves and is owned by neither. The reader faithfully reported the
newest record it could find; the writer faithfully refused to write one. ADR 0052 names
the target: one canonical session/continuity domain service in this crate, with
.envrc, the provider hook shims, the CLI, MCP, HTTP and the Cockpit as adapters over
it. This module is that service’s domain.
§What this change is, and what it is not
It is additive. The shell keeps every write it has. Nothing here changes the
behaviour of the running lifecycle: the projections are read-only queries, and the write
path (SessionStore::close) exists, is tested, and is reachable only through
SessionStore::writable — a constructor no surface calls. ADR 0047 records the cutover
and its order. Four workers are editing lib/*.sh as this is written, and a cutover in
the same change would be a merge whose conflicts are semantic rather than textual, in the
one subsystem where a wrong merge is invisible until somebody resumes from a record that
is not theirs.
§The four things it settles
Identity. Six things the stores spell as three words become six types
with no conversion between them. RepositoryId and CheckoutId wrap the digests
this crate already computes for its lease, index and worktree topology, rather than
inventing a seventh notion of “which repository”.
The aggregates. An Episode carries Option<TaskId> and nothing else
of a task. ADR 0052’s defect — a task’s state deciding whether an episode’s artefact is
written — is unrepresentable, because EpisodeState::may_move_to takes no task.
The machine. Four states, six transitions, one terminal state, declared
once and projected as data by session.machine so that no surface draws its own.
Persistence. Closing takes an O_EXCL claim before the expensive part, so
four repeated closes and eight simultaneous ones yield exactly one record. The episode
that has four records in this repository is the reason.
Events. Typed, over the ledger that already exists, against the vocabulary
share/events.yaml already declares. No second store: locked append-only JSONL meets
concurrency, corruption detection and recovery, and ADR 0047 records why a database was
refused rather than merely not chosen.
§Freshness is consumed, not restated
fresh | aging | stale | unknown | invalid, Thresholds::judge and epoch_seconds
arrive with ADR 0052 in continuity.rs. This domain does not define a second vocabulary
for age: it takes Unix seconds and answers the single predicate the machine needs,
Episode::stranded_after.
use majordomus_cli::session::{EpisodeState, Machine, open_episodes};
// the machine is derived from the types and has one terminal state
let machine = Machine::describe();
assert_eq!(machine.states.len(), EpisodeState::ALL.len());
assert_eq!(machine.states.iter().filter(|s| s.terminal).count(), 1);
// and a checkout that has never run the lifecycle reports absence, not a fault
let dir = tempfile::tempdir().unwrap();
assert!(open_episodes(dir.path()).is_empty());Re-exports§
pub use episode::Episode;pub use episode::Outcome;pub use identity::CheckoutId;pub use identity::EpisodeId;pub use identity::IdentityFacet;pub use identity::ProviderSessionId;pub use identity::RecordedSpelling;pub use identity::RepositoryId;pub use identity::Spelling;pub use identity::TaskId;pub use identity::WorkerId;pub use ledger::Corruption;pub use ledger::DeclaredEvent;pub use ledger::Envelope;pub use ledger::Ledger;pub use ledger::LedgerError;pub use ledger::LedgerEvent;pub use ledger::LedgerRead;pub use ledger::Vocabulary;pub use state::EpisodeState;pub use state::Machine;pub use state::StateView;pub use state::Transition;pub use state::TransitionView;pub use store::CloseOutcome;pub use store::CloseReport;pub use store::SessionStore;pub use store::StoreError;
Modules§
- episode
- The aggregate: what an episode is, and what it is not.
- identity
- The identities of the session domain: six things that the stores spell as three words.
- ledger
- The typed event substrate: the existing append-only ledger, read and written with types.
- state
- Where an episode is in its life, and the transitions that move it there.
- store
- Persistence: the open-episode store, the closed-record store, and closing exactly once.
Constants§
- WRITTEN_
EVENTS - Every event name this domain writes.
Functions§
- identities
- How this checkout spells each of the identities the audit found conflated, beside the canonical value this executable computes.
- open_
episodes - The open episodes of one checkout, as the store holds them.
- public_
text - Free text as it may be shown and written down: every URL in it without its credentials.
- public_
url - A URL as it may be shown and written down: without its credentials.