Skip to content

HTTP API: session_domain

The typed session domain: the lifecycle state machine an episode moves through, declared once in the executable and projected here rather than redrawn per surface, and the identities this checkout records — the repository, the checkout, the episode and the provider's own session — each with the canonical value and each store's own spelling of it, because the stores spell six things as three words and nothing said so.

Rendered from docs/generated/openapi.json, which the executable generates from its capability registry and CI refuses stale. The raw document is at /openapi.json; a running server serves the same document at its own /openapi.json and Swagger UI at /swagger.

session_domain

The typed session domain: the lifecycle state machine an episode moves through, declared once in the executable and projected here rather than redrawn per surface, and the identities this checkout records — the repository, the checkout, the episode and the provider's own session — each with the canonical value and each store's own spelling of it, because the stores spell six things as three words and nothing said so.

GET /api/v1/session/identity session_domain.identity

What identifies this checkout, and how each store spells it

The repository, the checkout, the open episode and the provider session, each with the canonical value this executable computes and each store's own spelling beside it. The stores record the repository sometimes as an absolute .git path and sometimes as a remote URL, the worktree as a path and as a digest, and the session as both an episode id and a provider UUID; this is the surface that says so, so that a reader never compares two spellings of different things and concludes they disagree.

  • query
  • behaviorally_verified
  • benchmark required
  • cache process (max_entries=2, ttl_seconds=2)
  • MCP tool majordomus_session_identity
  • MCP resource {"name": "session-identity", "uri": "majordomus://session/identity"}

No parameters.

Try it

The port is whatever the server logged; 8741 is the one it asks for first. The values are the operation's first example.

curl -s "http://127.0.0.1:8741/api/v1/session/identity"

Tested by apps/majordomus-cli/tests/http_serve.rs, which replays the route with these values against a server on a real socket.

Responses

statusbodymeaning
200 IdentityReport What identifies this checkout, and how each store spells it: the result.
400 ErrorBody invalid_input: The input does not fit the schema: a parameter of the wrong type, an unknown one, or a value the capability rejects.
404 ErrorBody not_found: The input names something the repository does not hold.
500 ErrorBody internal: The capability failed for a reason of its own; the message names it.
default ErrorBody Any other failure the transport reports: 405 method_not_allowed for another method on this path, 400 invalid_input for a body that is not a JSON object.
GET /api/v1/session/machine session_domain.machine

The episode lifecycle, as data

Every state an episode can be in, whether it is terminal and what it may move to; every transition — open, resume, checkpoint, detach, close, recover — with the states it runs between and whether it moves the state at all. Derived from the types, so a diagram that disagrees with the code cannot exist. What the machine deliberately does not depend on is stated rather than left to be inferred: task state, which is ADR 0052.

  • query
  • behaviorally_verified
  • benchmark required
  • cache process (max_entries=1, ttl_seconds=600)
  • MCP tool majordomus_session_machine
  • MCP resource {"name": "session-machine", "uri": "majordomus://session/machine"}

No parameters.

Try it

The port is whatever the server logged; 8741 is the one it asks for first. The values are the operation's first example.

curl -s "http://127.0.0.1:8741/api/v1/session/machine"

Tested by apps/majordomus-cli/tests/http_serve.rs, which replays the route with these values against a server on a real socket.

Responses

statusbodymeaning
200 MachineReport The episode lifecycle, as data: the result.
400 ErrorBody invalid_input: The input does not fit the schema: a parameter of the wrong type, an unknown one, or a value the capability rejects.
404 ErrorBody not_found: The input names something the repository does not hold.
500 ErrorBody internal: The capability failed for a reason of its own; the message names it.
default ErrorBody Any other failure the transport reports: 405 method_not_allowed for another method on this path, 400 invalid_input for a body that is not a JSON object.

Schemas

The components only this tag's operations use. Each row is the type's own field, its doc comment the description; a type another page owns links there.

EpisodeState

"opening" | "open" | "detached" | "closed"
Where an episode is: minted, open, detached from the connection that held it, or closed into the immutable record.

use majordomus_cli::session::EpisodeState;

assert!(EpisodeState::Open.may_move_to(EpisodeState::Closed));
assert!(!EpisodeState::Closed.may_move_to(EpisodeState::Open), "a record is immutable");
assert!(EpisodeState::Open.may_move_to(EpisodeState::Open), "a resume is not a move");
assert!(EpisodeState::Closed.is_final());
  • openingAn identity has been minted and nothing durable has been written yet. It exists so that a crash between minting an id and writing the open record is a state the model can name rather than an episode that never happened.
  • openThe open record stands. The worker is here, or was recently.
  • detachedThe connection that held the episode went away, and the episode did not. A dropped socket is the most ordinary thing that happens to a session; an episode that closed itself on every disconnect would lose the work of every worker whose network hiccuped. It is still resumable, and it is still recoverable if nobody resumes it.
  • closedThe immutable record has been published. Terminal, and terminal is the point: the four records of s-20260909152316-024f all exist because nothing in the previous representation could say this.

Used by StateView TransitionView

IdentityFacet

object
One identity of this checkout, as this executable computes it and as each store spells it. What session.identity answers, and the direct projection of the audit's table.

use majordomus_cli::session::{identities, IdentityFacet};

let dir = tempfile::tempdir().unwrap();
let facets: Vec<IdentityFacet> = identities(dir.path());
let repository = facets.iter().find(|f| f.subject == "repository").expect("a facet");
assert!(repository.spellings.len() >= 2, "one word, two writers, two values");
propertytyperequireddescription
subject string yes What is being identified: repository, checkout, episode, provider_session.
canonical string no The canonical value this executable computes, when it can compute one.
spellings array of RecordedSpelling → RecordedSpelling no How each store spells the same subject, in the order crate::order gives.
note string yes Why the subject needs its own type, in one line, for a reader meeting it in a projection rather than in this module.

Used by IdentityReport

IdentityReport

object
The identities of this checkout: one facet per subject, with a standing note about what a reader must not do with them.

use majordomus_cli::capability::builtin::IdentityReport;
use majordomus_cli::session::identities;

let dir = tempfile::tempdir().unwrap();
let report = IdentityReport {
    checkout: dir.path().display().to_string(),
    facets: identities(dir.path()),
    findings: Vec::new(),
};
let subjects: Vec<&str> = report.facets.iter().map(|f| f.subject.as_str()).collect();
assert_eq!(subjects, ["repository", "checkout", "episode", "provider_session"]);
propertytyperequireddescription
checkout string yes The checkout this answer is about. An absolute path, because this capability is served to the worker in front of this checkout and never published — the same exposure rule continuity.state follows and for the same reason (ADR 0014).
facets array of IdentityFacet → IdentityFacet yes One entry per subject, in the order a reader meets them: repository, checkout, episode, provider session.
findings array of string yes What a reader should know before comparing any two of these. Empty is not the good case here — the conflation is a property of the stores, not of this checkout — so the standing note is always present.

Used by session_domain.identity

MachineReport

object
The lifecycle machine, with the decision it implements, so that a reader meeting it in a projection can go and read why it is shaped this way.

use majordomus_cli::capability::builtin::MachineReport;
use majordomus_cli::session::Machine;

// the report is the machine plus one citation; the machine itself is derived from the
// types and carries no second list
let report = MachineReport { machine: Machine::describe(), decision: "adr-0047".into() };
assert!(report.machine.transitions.iter().any(|t| !t.moves_state), "a checkpoint is an event");
assert!(report.decision.contains("0047"));
propertytyperequireddescription
states array of StateView → StateView yes Every state, with what it means and whether it is terminal.
transitions array of TransitionView → TransitionView yes Every transition, with the states it runs between.
independent_of array of string yes What the machine deliberately does not depend on, stated so that a reader does not have to infer an absence. One entry today: task state.
decision string yes The decision this machine is the implementation of, so a reader meeting it in a projection can go and read why it is shaped this way.

Used by session_domain.machine

RecordedSpelling

object
One store's spelling of one subject. Which half of the layer wrote it, what wrote it, and the value it would have here.

use majordomus_cli::session::{RecordedSpelling, Spelling};

let local = RecordedSpelling {
    spelling: Spelling::Local,
    writer: "mj_git_repo_id (lib/common.sh)".into(),
    value: "/src/repo/.git".into(),
};
// knowing which half a string came from is what stops a path being compared with a URL
assert_eq!(local.spelling.as_str(), "local");
assert!(local.value.starts_with('/'));
propertytyperequireddescription
spelling Spelling → Spelling yes Which half of the layer wrote it.
writer string yes The writer, named so a reader can go and look at it.
value string no The value, as this checkout would have it written. Empty when nothing has written one here yet, which is a fresh clone and not a fault.

Used by IdentityFacet

Spelling

"local" | "shared" | "canonical"
Which store a recorded identity was read from, kept beside the value.

The point is not to choose between mj_git_repo_id and mj_repository_id, or between worktree: and worktree_id:. It is to stop pretending they are one value. A reader that knows a string is the local spelling can compare it with other local spellings and can refuse to compare it with a shared one, which is precisely the comparison that produced "these two records are about different repositories" for records of the same repository written four minutes apart.

use majordomus_cli::session::Spelling;
// the two halves are different answers to one question, and are never compared
assert_ne!(Spelling::Local, Spelling::Shared);
assert_eq!(Spelling::Canonical.as_str(), "canonical");
  • localThe local half's spelling: a path, in .ai/local/state/. It names this disk and is never published.
  • sharedThe shared half's spelling: a remote URL or a digest, in .ai/repo/sessions/. It names the repository and travels with a clone (ADR 0014).
  • canonicalThis executable's own canonical digest, computed rather than read.

Used by RecordedSpelling

StateView

object
One state of the machine: its word, whether an episode ever leaves it, and where it may go from there.

use majordomus_cli::session::{EpisodeState, Machine, StateView};

let m = Machine::describe();
let closed: &StateView =
    m.states.iter().find(|s| s.state == EpisodeState::Closed).expect("a state");
assert!(closed.terminal && closed.may_move_to.is_empty());
propertytyperequireddescription
state EpisodeState → EpisodeState yes The state's word.
terminal boolean yes Whether an episode ever leaves it.
may_move_to array of EpisodeState → EpisodeState yes The states an episode in this one may move to, in canonical order.

Used by MachineReport

Transition

"open" | "resume" | "checkpoint" | "detach" | "close" | "recover"
What moved an episode. The vocabulary the ADR 0052 audit needs: open, resume, checkpoint, detach, close, recover.

One of them does not change the state, and it is a transition anyway. A checkpoint leaves an open episode open — but it is one of the two events whose absence is what the audit was looking for, and an event that is modelled as "nothing happened" cannot be counted. The six days of silence were six days in which session.started and session.closed kept arriving and the two events that produce the records a worker resumes from did not. A vocabulary in which those two are not events is a vocabulary in which that outage is invisible.

use majordomus_cli::session::{EpisodeState, Transition};

// a checkpoint is a transition that does not move the state, and says so
let c = Transition::Checkpoint;
assert_eq!(c.from_to(), (EpisodeState::Open, EpisodeState::Open));
assert!(!c.moves_state());

// a resume is canonically the return to a detached episode; a provider's start event
// arriving at one that is already open is the same transition seen from `Open`, which
// the machine allows without a move
assert_eq!(Transition::Resume.from_to(), (EpisodeState::Detached, EpisodeState::Open));
assert!(EpisodeState::Open.may_move_to(EpisodeState::Open));

// and every transition's own move is one the machine allows
for t in Transition::ALL {
    let (from, to) = t.from_to();
    assert!(from.may_move_to(to), "{} is not a legal move", t.as_str());
}
  • openThe episode begins: an id is minted and the open record is written.
  • resumeThe worker came back to a detached episode. A provider's start event also fires again on a resume and on a compaction against an episode that is still open; that is the same transition observed from Open, and the honest answer to "your episode is already open" is to keep it rather than to open a second one.
  • checkpointA progress note was recorded against the episode. An artefact of the episode and not of a task (ADR 0052): a checkpoint outside a task records task: none and is a normal record, not a refusal.
  • detachThe connection went away without an end event.
  • closeThe worker ended the episode and the immutable record was published.
  • recoverNobody ended it and its last sign of life is older than the stranded threshold, so maintenance closed it. The record says so: an episode closed this way is interrupted, because a reader resuming from it needs to know it may be incomplete.

Used by TransitionView

TransitionView

object
One transition of the machine: its word, the states it runs between, and whether it moves the state at all.

use majordomus_cli::session::{Machine, Transition, TransitionView};

let m = Machine::describe();
let close: &TransitionView = m
    .transitions
    .iter()
    .find(|t| t.transition == Transition::Close)
    .expect("a transition");
assert!(close.moves_state);
propertytyperequireddescription
transition Transition → Transition yes The transition's word.
from EpisodeState → EpisodeState yes Where it starts.
to EpisodeState → EpisodeState yes Where it ends.
moves_state boolean yes False for resume and checkpoint, which are events inside a state.

Used by MachineReport

Searches the titles and descriptions of every page this site publishes.