Skip to main content

Module session

Module session 

Source
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.