Skip to main content

Episode

Struct Episode 

Source
pub struct Episode {
    pub id: EpisodeId,
    pub state: EpisodeState,
    pub repository: Option<RepositoryId>,
    pub checkout: CheckoutId,
    pub provider: String,
    pub provider_session: Option<ProviderSessionId>,
    pub worker: Option<WorkerId>,
    pub task: Option<TaskId>,
    pub started_at: String,
    pub branch: String,
    pub start_head: String,
    pub last_sign_of_life: Option<i64>,
}
Expand description

One execution episode: a provider’s conversation boundary, with the identities that say whose and where it is, and at most a relation to a task.

use majordomus_cli::session::{Episode, EpisodeId, EpisodeState, ProviderSessionId, TaskId};
use std::path::Path;

let id = EpisodeId::parse("s-20260910205542-e2a6").unwrap();
let e = Episode::opening(id, Path::new("/r"))
    .with_provider("claude-code", ProviderSessionId::new("01Bv2gJsf"));

assert_eq!(e.state, EpisodeState::Opening);
assert!(e.task.is_none(), "an episode without a task is ordinary");

// a task is a relation the episode carries, never a thing that gates it
let e = e.with_task(TaskId::parse("t-20260905034523-a9f1").unwrap());
assert!(e.task.is_some());
assert!(e.state.may_move_to(EpisodeState::Open), "whatever the task says");

Fields§

§id: EpisodeId

The episode’s own identity, and the only session identity there is.

§state: EpisodeState

Where it is in its life.

§repository: Option<RepositoryId>

The repository, as this executable computes it. None where git cannot be asked.

§checkout: CheckoutId

The checkout it was opened in. Two worktrees of one repository differ here, and that difference is the first tier of every record resolution in the tool.

§provider: String

The provider that opened it, when a provider did. Only something running inside a provider’s own hook can name it, which is what makes it a fact rather than a claim.

§provider_session: Option<ProviderSessionId>

The provider’s own name for its conversation: an external correlation id, used to find this episode again and never to identify it. None for an episode opened by hand, which is the one episode no provider session owns.

§worker: Option<WorkerId>

The worker, when one was supplied. Never inferred.

§task: Option<TaskId>

The task this episode relates to, when there is one. The whole of the relation.

§started_at: String

When it opened, RFC 3339 in UTC. Empty for an episode that is still Opening.

§branch: String

The branch it opened on.

§start_head: String

The commit it opened at.

§last_sign_of_life: Option<i64>

The newest sign of life: the later of started_at and the newest ledger line this episode stamped, as Unix seconds. None when neither can be read as a timestamp, which is the one case Episode::stranded_after refuses to judge.

Implementations§

Source§

impl Episode

Source

pub fn opening(id: EpisodeId, root: &Path) -> Self

A newly minted episode, before anything durable has been written. It exists as a state so that a crash between minting an id and writing the open record is something the model can name rather than an episode that never happened.

use majordomus_cli::session::{Episode, EpisodeId, EpisodeState};
use std::path::Path;

let id = EpisodeId::parse("s-20260910205542-e2a6").expect("an episode id");
let e = Episode::opening(id, Path::new("/r"));
assert_eq!(e.state, EpisodeState::Opening);
assert!(e.started_at.is_empty(), "nothing durable has been written yet");
Source

pub fn with_provider( self, provider: impl Into<String>, session: ProviderSessionId, ) -> Self

Name the provider and its own session identity — the external correlation id a hook passes back to find this episode again, and never the episode’s own name.

use majordomus_cli::session::{Episode, EpisodeId, ProviderSessionId};
use std::path::Path;

let id = EpisodeId::parse("s-20260910205542-e2a6").expect("an episode id");
let e = Episode::opening(id, Path::new("/r"))
    .with_provider("claude-code", ProviderSessionId::new("01Bv2gJsf"));
assert_eq!(e.provider, "claude-code");
assert_ne!(e.provider_session.as_ref().map(|p| p.as_str()), Some(e.id.as_str()));
Source

pub fn with_task(self, task: TaskId) -> Self

Relate the episode to a task. The whole of the relation: an episode carries a task’s identity and nothing else of it, so nothing here can gate a transition on one.

use majordomus_cli::session::{Episode, EpisodeId, EpisodeState, TaskId};
use std::path::Path;

let id = EpisodeId::parse("s-20260910205542-e2a6").expect("an episode id");
let e = Episode::opening(id, Path::new("/r"))
    .with_task(TaskId::parse("t-20260905034523-a9f1").expect("a task id"));
assert!(e.task.is_some());
assert!(e.state.may_move_to(EpisodeState::Open), "whatever the task says");
Source

pub fn with_worker(self, worker: Option<WorkerId>) -> Self

Record the worker, when one was supplied. Absent stays absent: an inferred worker is indistinguishable from a recorded one the moment it is written down.

use majordomus_cli::session::{Episode, EpisodeId, WorkerId};
use std::path::Path;

let id = EpisodeId::parse("s-20260910205542-e2a6").expect("an episode id");
let e = Episode::opening(id, Path::new("/r")).with_worker(WorkerId::supplied(""));
assert!(e.worker.is_none());
Source

pub fn opened_at( self, started_at: impl Into<String>, branch: impl Into<String>, head: impl Into<String>, at: i64, ) -> Self

Record where and when it opened, and move it to EpisodeState::Open.

use majordomus_cli::session::{Episode, EpisodeId, EpisodeState};
use std::path::Path;

let e = Episode::opening(EpisodeId::parse("s-20260910205542-e2a6").unwrap(), Path::new("/r"))
    .opened_at("2026-09-10T20:55:42Z", "master", "9f3a2a0", 1_789_073_742);
assert_eq!(e.state, EpisodeState::Open);
assert_eq!(e.last_sign_of_life, Some(1_789_073_742));
Source

pub fn stranded_after(&self, now: i64, threshold_seconds: i64) -> Option<bool>

Is this episode’s last sign of life older than threshold_seconds at now?

The one predicate the state machine needs from the time dimension, and deliberately the only one. The vocabulary of age — fresh | aging | stale | unknown | invalid and Thresholds::judge — belongs to continuity (ADR 0052) and is not restated here; this domain consumes seconds and answers a boolean.

None means unjudgeable, not fine. An episode whose evidence cannot be read as a timestamp is skipped and counted by a recovery sweep, never swept: on 2026-09-10 a first draft of the shell sweep would have deleted a staging directory belonging to a scripts/derive that was running, because “the run that made it did not finish” is a claim and not a measurement until something reads the clock.

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

let id = EpisodeId::parse("s-20260910205542-e2a6").unwrap();
let live = Episode::opening(id.clone(), Path::new("/r"))
    .opened_at("2026-09-10T20:55:42Z", "master", "9f3a2a0", 1_000);
assert_eq!(live.stranded_after(1_600, 3_600), Some(false), "ten minutes is not stranded");
assert_eq!(live.stranded_after(5_000, 3_600), Some(true), "an hour later it is");

let unjudgeable = Episode::opening(id, Path::new("/r"));
assert_eq!(unjudgeable.stranded_after(5_000, 3_600), None, "no evidence is not `fine`");
Source

pub fn may_move_to(&self, target: EpisodeState) -> bool

The transition this episode may take to reach target, or None when the machine forbids the move. The store asks this before it writes; a caller that wants to know without writing asks the same question and gets the same answer.

use majordomus_cli::session::{Episode, EpisodeId, EpisodeState};
use std::path::Path;

let mut e = Episode::opening(EpisodeId::parse("s-20260910205542-e2a6").unwrap(), Path::new("/r"))
    .opened_at("2026-09-10T20:55:42Z", "master", "9f3a2a0", 1_000);
assert!(e.may_move_to(EpisodeState::Closed));
e.state = EpisodeState::Closed;
assert!(!e.may_move_to(EpisodeState::Closed), "closing twice is not a move");

Trait Implementations§

Source§

impl Clone for Episode

Source§

fn clone(&self) -> Episode

Returns a duplicate of the value. Read more
1.0.0 (const: unstable) · Source§

fn clone_from(&mut self, source: &Self)

Performs copy-assignment from source. Read more
Source§

impl Debug for Episode

Source§

fn fmt(&self, f: &mut Formatter<'_>) -> Result

Formats the value using the given formatter. Read more
Source§

impl<'de> Deserialize<'de> for Episode

Source§

fn deserialize<__D>(__deserializer: __D) -> Result<Self, __D::Error>
where __D: Deserializer<'de>,

Deserialize this value from the given Serde deserializer. Read more
Source§

impl Eq for Episode

Source§

impl JsonSchema for Episode

Source§

fn schema_name() -> Cow<'static, str>

The name of the generated JSON Schema. Read more
Source§

fn schema_id() -> Cow<'static, str>

Returns a string that uniquely identifies the schema produced by this type. Read more
Source§

fn json_schema(generator: &mut SchemaGenerator) -> Schema

Generates a JSON Schema for this type. Read more
Source§

fn inline_schema() -> bool

Whether JSON Schemas generated for this type should be included directly in parent schemas, rather than being re-used where possible using the $ref keyword. Read more
Source§

impl Ordered for Episode

Source§

fn order_key(&self) -> OrderKey<'_>

The four parts this item is ordered by. Borrowed from the item: building a key allocates nothing, so sorting a large collection costs the comparisons and no more.
Source§

impl PartialEq for Episode

Source§

fn eq(&self, other: &Episode) -> bool

Tests for self and other values to be equal, and is used by ==.
1.0.0 (const: unstable) · Source§

fn ne(&self, other: &Rhs) -> bool

Tests for !=. The default implementation is almost always sufficient, and should not be overridden without very good reason.
Source§

impl Serialize for Episode

Source§

fn serialize<__S>(&self, __serializer: __S) -> Result<__S::Ok, __S::Error>
where __S: Serializer,

Serialize this value into the given Serde serializer. Read more
Source§

impl StructuralPartialEq for Episode

Auto Trait Implementations§

Blanket Implementations§

Source§

impl<T> Any for T
where T: 'static + ?Sized,

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
Source§

impl<T> Borrow<T> for T
where T: ?Sized,

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

Source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
§

impl<ST, DT> CastableFrom<ST, Initialized, Initialized> for DT
where ST: ?Sized, DT: ?Sized,

§

impl<ST, DT> CastableFrom<ST, Uninit, Uninit> for DT
where ST: ?Sized, DT: ?Sized,

Source§

impl<T> CloneToUninit for T
where T: Clone,

Source§

unsafe fn clone_to_uninit(&self, dest: *mut u8)

🔬This is a nightly-only experimental API. (clone_to_uninit)
Performs copy-assignment from self to dest. Read more
Source§

impl<T> DeserializeOwned for T
where T: for<'de> Deserialize<'de>,

Source§

impl<T> DynClone for T
where T: Clone,

Source§

fn __clone_box(&self, _: Private) -> *mut ()

§

impl<Q, K> Equivalent<K> for Q
where Q: Eq + ?Sized, K: Borrow<Q> + ?Sized,

§

fn equivalent(&self, key: &K) -> bool

Compare self to key and return true if they are equal.
Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

§

impl<T> Instrument for T

§

fn instrument(self, span: Span) -> Instrumented<Self>

Instruments this type with the provided [Span], returning an Instrumented wrapper. Read more
§

fn in_current_span(self) -> Instrumented<Self>

Instruments this type with the current Span, returning an Instrumented wrapper. Read more
Source§

impl<T, U> Into<U> for T
where U: From<T>,

Source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

§

impl<T> Read<Exclusive, BecauseExclusive> for T
where T: ?Sized,

Source§

impl<T> Same for T

Source§

type Output = T

Should always be Self
Source§

impl<T> ToOwned for T
where T: Clone,

Source§

type Owned = T

The resulting type after obtaining ownership.
Source§

fn to_owned(&self) -> T

Creates owned data from borrowed data, usually by cloning. Read more
Source§

fn clone_into(&self, target: &mut T)

Uses borrowed data to replace owned data, usually by cloning. Read more
Source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

Source§

type Error = Infallible

The type returned in the event of a conversion error.
Source§

fn try_from(value: U) -> Result<T, <T as TryFrom<U>>::Error>

Performs the conversion.
Source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

Source§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
Source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.
§

impl<T> WithSubscriber for T

§

fn with_subscriber<S>(self, subscriber: S) -> WithDispatch<Self>
where S: Into<Dispatch>,

Attaches the provided Subscriber to this type, returning a [WithDispatch] wrapper. Read more
§

fn with_current_subscriber(self) -> WithDispatch<Self>

Attaches the current default Subscriber to this type, returning a [WithDispatch] wrapper. Read more