Skip to main content

telltale_vm/session/
overview.rs

1// Session lifecycle and store.
2//
3// Matches the Lean `SessionState`, `SessionStore` from `lean/Runtime/VM/Model/State.lean`.
4// Local type state lives here — the session store is the single source
5// of truth for per-endpoint type advancement.
6
7/// Archival summary for a closed session that has been reaped from live state.
8#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
9pub struct ClosedSessionSummary {
10    /// Session identifier.
11    pub sid: SessionId,
12    /// Terminal session status at reap time.
13    pub status: SessionStatus,
14    /// Number of participant roles.
15    pub role_count: usize,
16    /// Number of retained endpoint type entries at reap time.
17    pub local_type_entries: usize,
18    /// Number of directed edges tracked by the session.
19    pub edge_count: usize,
20    /// Number of edge-bound handlers.
21    pub edge_handler_count: usize,
22    /// Number of accumulated auth leaves across all edges.
23    pub auth_leaf_count: usize,
24    /// Number of auth trees retained by the session.
25    pub auth_tree_count: usize,
26    /// Number of auth roots retained by the session.
27    pub auth_root_count: usize,
28    /// Final epoch value.
29    pub epoch: usize,
30}
31
32impl ClosedSessionSummary {
33    fn from_session(session: &SessionState) -> Self {
34        Self {
35            sid: session.sid,
36            status: session.status.clone(),
37            role_count: session.roles.len(),
38            local_type_entries: session.local_types.len(),
39            edge_count: session.buffers.len(),
40            edge_handler_count: session.edge_handlers.len(),
41            auth_leaf_count: session.auth_leaves.values().map(Vec::len).sum(),
42            auth_tree_count: session.auth_trees.len(),
43            auth_root_count: session.auth_roots.len(),
44            epoch: session.epoch,
45        }
46    }
47
48    fn retained_bytes_estimate(&self) -> usize {
49        std::mem::size_of::<Self>().saturating_add(serialized_bytes(self))
50    }
51}
52
53/// Reusable session-open layout derived from a fixed role topology and local types.
54#[derive(Debug, Clone)]
55pub struct SessionOpenPlan {
56    pub(crate) roles: Vec<String>,
57    pub(crate) role_ids: BTreeMap<String, u16>,
58    pub(crate) initial_types: Vec<(String, LocalTypeR, LocalTypeR)>,
59    pub(crate) edge_blueprint: Vec<((u16, u16), String, String)>,
60    pub(crate) active_branch_roles: Vec<String>,
61}
62
63impl SessionOpenPlan {
64    fn collect_protocol_edges(
65        role: &str,
66        local_type: &LocalTypeR,
67        role_ids: &BTreeMap<String, u16>,
68        edges: &mut BTreeSet<(u16, u16)>,
69    ) {
70        match local_type {
71            LocalTypeR::End | LocalTypeR::Var(_) => {}
72            LocalTypeR::Mu { body, .. } => {
73                Self::collect_protocol_edges(role, body, role_ids, edges);
74            }
75            LocalTypeR::Send { partner, branches } => {
76                if let (Some(from_id), Some(to_id)) = (role_ids.get(role), role_ids.get(partner)) {
77                    if from_id != to_id {
78                        edges.insert((*from_id, *to_id));
79                    }
80                }
81                for (_, _, continuation) in branches {
82                    Self::collect_protocol_edges(role, continuation, role_ids, edges);
83                }
84            }
85            LocalTypeR::Recv { partner, branches } => {
86                if let (Some(from_id), Some(to_id)) = (role_ids.get(partner), role_ids.get(role)) {
87                    if from_id != to_id {
88                        edges.insert((*from_id, *to_id));
89                    }
90                }
91                for (_, _, continuation) in branches {
92                    Self::collect_protocol_edges(role, continuation, role_ids, edges);
93                }
94            }
95        }
96    }
97
98    /// Build a reusable open plan from a role list and initial local types.
99    ///
100    /// # Panics
101    ///
102    /// Panics if an internally assigned role id does not map back into the
103    /// canonical `roles` slice while constructing the edge blueprint.
104    #[must_use]
105    pub fn new(roles: &[String], initial_types: &BTreeMap<String, LocalTypeR>) -> Self {
106        let role_ids = SessionState::build_role_ids(roles);
107        let mut planned_types = Vec::with_capacity(roles.len());
108        let mut active_branch_roles = Vec::new();
109        for role in roles {
110            if let Some(original) = initial_types.get(role) {
111                let current = unfold_mu(original);
112                if SessionState::branch_shape(&current).is_some() {
113                    active_branch_roles.push(role.clone());
114                }
115                planned_types.push((role.clone(), current, original.clone()));
116            }
117        }
118
119        let mut protocol_edges = BTreeSet::new();
120        for role in roles {
121            if let Some(original) = initial_types.get(role) {
122                Self::collect_protocol_edges(role, original, &role_ids, &mut protocol_edges);
123            }
124        }
125        let mut edge_blueprint = Vec::with_capacity(protocol_edges.len());
126        for (from_id, to_id) in protocol_edges {
127            let from = roles
128                .get(usize::from(from_id))
129                .expect("sender role id must index the session-open role set")
130                .clone();
131            let to = roles
132                .get(usize::from(to_id))
133                .expect("receiver role id must index the session-open role set")
134                .clone();
135            edge_blueprint.push(((from_id, to_id), from, to));
136        }
137
138        Self {
139            roles: roles.to_vec(),
140            role_ids,
141            initial_types: planned_types,
142            edge_blueprint,
143            active_branch_roles,
144        }
145    }
146
147    /// Canonical role ordering for this plan.
148    #[must_use]
149    pub fn roles(&self) -> &[String] {
150        &self.roles
151    }
152
153    /// Directed protocol edges needed by this session.
154    #[must_use]
155    pub fn edge_blueprint(&self) -> &[((u16, u16), String, String)] {
156        &self.edge_blueprint
157    }
158}
159
160/// Approximate retained state for the session store.
161#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
162pub struct SessionStoreMemoryUsage {
163    /// Number of live sessions still resident in the store.
164    pub live_sessions: usize,
165    /// Number of closed/cancelled/faulted sessions still resident in the store.
166    pub live_closed_sessions: usize,
167    /// Number of archived closed-session summaries retained after reaping.
168    pub archived_closed_sessions: usize,
169    /// Number of live endpoint type entries.
170    pub live_local_type_entries: usize,
171    /// Number of live directed buffers.
172    pub live_buffer_count: usize,
173    /// Number of live buffered messages.
174    pub live_buffered_messages: usize,
175    /// Number of live edge-bound handlers.
176    pub live_edge_handler_count: usize,
177    /// Number of live auth leaves across sessions.
178    pub live_auth_leaf_count: usize,
179    /// Number of live auth trees.
180    pub live_auth_tree_count: usize,
181    /// Number of live auth roots.
182    pub live_auth_root_count: usize,
183    /// Estimated retained bytes by session-store subsystem.
184    pub retained_bytes: SessionStoreRetainedBytes,
185}
186
187/// Estimated retained bytes for session-store subsystems.
188#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
189pub struct SessionStoreRetainedBytes {
190    /// Live session metadata excluding dedicated subsystems below.
191    pub live_sessions: usize,
192    /// Archived closed-session summaries.
193    pub archived_closed: usize,
194    /// Local type storage and endpoint bindings.
195    pub local_types: usize,
196    /// Buffer storage and buffered payloads.
197    pub buffers: usize,
198    /// Edge-trace storage.
199    pub traces: usize,
200    /// Auth leaves, trees, and roots.
201    pub auth: usize,
202    /// Handler bindings and defaults.
203    pub handlers: usize,
204    /// Aggregate retained bytes across all session-store subsystems.
205    pub total: usize,
206}
207
208/// Session identifier. Each session gets a unique ID within the VM.
209pub type SessionId = usize;
210
211/// Handler identifier for edge-bound runtime dispatch.
212pub type HandlerId = String;
213type HandlerNumericId = u16;
214type LabelNumericId = u16;
215type EdgeKey = (u16, u16);
216type LocalBranches<'a> = &'a [(Label, Option<ValType>, LocalTypeR)];
217type HandlerIndexBuild = (
218    BTreeMap<HandlerId, HandlerNumericId>,
219    Vec<HandlerId>,
220    BTreeMap<EdgeKey, HandlerNumericId>,
221    Option<HandlerNumericId>,
222);
223
224#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
225pub(crate) enum BranchDirection {
226    Send,
227    Recv,
228}
229
230#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
231pub(crate) struct CachedBranch {
232    pub(crate) direction: BranchDirection,
233    pub(crate) partner: String,
234    pub(crate) expected_type: Option<ValType>,
235    pub(crate) continuation: LocalTypeR,
236}
237
238/// Built-in fallback handler id used when no edge-specific binding exists.
239pub const DEFAULT_HANDLER_ID: &str = "default_handler";
240
241fn default_handler_id() -> HandlerId {
242    DEFAULT_HANDLER_ID.to_string()
243}
244
245fn serialized_bytes<T: Serialize>(value: &T) -> usize {
246    bincode::serialized_size(value)
247        .ok()
248        .and_then(|bytes| usize::try_from(bytes).ok())
249        .unwrap_or(0)
250}
251
252/// Edge between two roles in a session (directed: sender → receiver).
253#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
254pub struct Edge {
255    /// Session scope for this edge.
256    pub sid: SessionId,
257    /// Sender role name.
258    pub sender: String,
259    /// Receiver role name.
260    pub receiver: String,
261}
262
263impl Edge {
264    /// Construct a sid-qualified edge.
265    #[must_use]
266    pub fn new(sid: SessionId, sender: impl Into<String>, receiver: impl Into<String>) -> Self {
267        Self {
268            sid,
269            sender: sender.into(),
270            receiver: receiver.into(),
271        }
272    }
273}
274
275#[derive(Debug, Deserialize)]
276struct EdgeJson {
277    sid: Option<SessionId>,
278    sender: String,
279    receiver: String,
280}
281
282/// Decode an edge from JSON.
283///
284/// # Errors
285///
286/// Returns an error when fields are missing.
287pub fn decode_edge_json(
288    value: &JsonValue,
289    session_hint: Option<SessionId>,
290) -> Result<Edge, String> {
291    let raw: EdgeJson =
292        serde_json::from_value(value.clone()).map_err(|e| format!("invalid edge json: {e}"))?;
293
294    let sid = raw
295        .sid
296        .or(session_hint)
297        .ok_or_else(|| "missing sid in edge json".to_string())?;
298    Ok(Edge::new(sid, raw.sender, raw.receiver))
299}
300
301/// Session status.
302#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
303pub enum SessionStatus {
304    /// Session is active and processing messages.
305    Active,
306    /// Session is draining buffered messages before close.
307    Draining,
308    /// Session is closed normally.
309    Closed,
310    /// Session was cancelled.
311    Cancelled,
312    /// Session faulted.
313    Faulted {
314        /// Reason for the fault.
315        reason: String,
316    },
317}
318
319/// Per-endpoint type tracking: current state + original for unfolding.
320#[derive(Debug, Clone, Serialize, Deserialize)]
321pub struct TypeEntry {
322    /// Current local type (advances with each completed instruction).
323    pub current: LocalTypeR,
324    /// Original local type (for unfolding recursive variables).
325    pub original: LocalTypeR,
326}