Skip to main content

atelier_sdk/
session.rs

1use std::fmt;
2use std::path::PathBuf;
3use std::str::FromStr;
4
5use rusqlite::types::{FromSql, FromSqlError, FromSqlResult, ToSql, ToSqlOutput, ValueRef};
6
7use crate::config::Actor;
8use crate::error::Error;
9
10/// The task or prompt that drove a session's acts. The journal keeps the
11/// summary and run reference; verbatim capture is policy-decided (ADR-0004).
12#[derive(Debug, Clone, PartialEq, Eq)]
13pub struct Instruction {
14    /// A one-line statement of what the session is for.
15    pub summary: String,
16    /// A reference to the run that carried the instruction: a ticket, a
17    /// conversation, a pipeline id.
18    pub run_ref: Option<String>,
19    /// The instruction's verbatim body, when the caller supplies it.
20    pub verbatim: Option<String>,
21}
22
23/// A session's identity: `s` plus its row in the workspace store.
24#[derive(Debug, Clone, Copy, PartialEq, Eq)]
25pub struct SessionId(pub(crate) i64);
26
27impl fmt::Display for SessionId {
28    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
29        write!(f, "s{}", self.0)
30    }
31}
32
33impl FromStr for SessionId {
34    type Err = Error;
35
36    fn from_str(text: &str) -> Result<Self, Self::Err> {
37        let not_found = || Error::SessionNotFound(text.to_owned());
38        let digits = text.strip_prefix('s').ok_or_else(not_found)?;
39        let row: i64 = digits.parse().map_err(|_| not_found())?;
40        Ok(Self(row))
41    }
42}
43
44/// One actor's bounded run of work in a workspace: its own working copy,
45/// its own change, journal entries grouped under it.
46#[derive(Debug, Clone, PartialEq, Eq)]
47pub struct Session {
48    /// The session's identity.
49    pub id: SessionId,
50    /// Who the session belongs to.
51    pub actor: Actor,
52    /// Where the session stands.
53    pub state: SessionState,
54    /// The stable identity of the session's unit of work on the root —
55    /// source zero; it survives rewrites — amended snapshots and the
56    /// landing rebase.
57    pub change_id: String,
58    /// One change per source the session spans, root first then mounts in
59    /// name order (ADR-0009).
60    pub changes: Vec<SourceChange>,
61    /// The session's editable directory, absolute, mirroring the
62    /// workspace's shape: root files at its top, each mounted source's
63    /// working copy at `<mount>/`. A real directory on disk: sessions
64    /// survive process restarts.
65    pub working_copy: PathBuf,
66    /// The instruction's summary, as journaled at open.
67    pub instruction_summary: String,
68    /// The instruction's run reference, when one was given.
69    pub instruction_run_ref: Option<String>,
70    /// When the session opened, in unix milliseconds.
71    pub opened_at_ms: i64,
72}
73
74/// One source's change under a session: the root's when `source` is `None`.
75#[derive(Debug, Clone, PartialEq, Eq)]
76pub struct SourceChange {
77    /// The mount the change belongs to; `None` for the root.
78    pub source: Option<String>,
79    /// The stable identity of the session's unit of work on this source.
80    pub change_id: String,
81}
82
83/// Where a session stands: open for work, its change landed, or closed
84/// without landing.
85#[derive(Debug, Clone, Copy, PartialEq, Eq)]
86pub enum SessionState {
87    /// Open for work.
88    Open,
89    /// The session's change landed on the shared line.
90    Landed,
91    /// Closed without landing; its work stays in history.
92    Abandoned,
93}
94
95impl SessionState {
96    /// The state's canonical lowercase name, as stored and rendered.
97    #[must_use]
98    pub fn as_str(self) -> &'static str {
99        match self {
100            Self::Open => "open",
101            Self::Landed => "landed",
102            Self::Abandoned => "abandoned",
103        }
104    }
105}
106
107impl fmt::Display for SessionState {
108    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
109        f.write_str(self.as_str())
110    }
111}
112
113impl FromStr for SessionState {
114    type Err = Error;
115
116    fn from_str(text: &str) -> Result<Self, Self::Err> {
117        match text {
118            "open" => Ok(Self::Open),
119            "landed" => Ok(Self::Landed),
120            "abandoned" => Ok(Self::Abandoned),
121            other => Err(Error::Engine(format!("unknown session state: {other}"))),
122        }
123    }
124}
125
126impl ToSql for SessionState {
127    fn to_sql(&self) -> rusqlite::Result<ToSqlOutput<'_>> {
128        Ok(ToSqlOutput::from(self.as_str()))
129    }
130}
131
132impl FromSql for SessionState {
133    fn column_result(value: ValueRef<'_>) -> FromSqlResult<Self> {
134        let text = value.as_str()?;
135        Self::from_str(text).map_err(|error| FromSqlError::Other(error.to_string().into()))
136    }
137}