Skip to main content

telltale_vm/session/
store.rs

1impl<'de> Deserialize<'de> for SessionState {
2    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
3    where
4        D: serde::Deserializer<'de>,
5    {
6        let raw = SessionStateSerde::deserialize(deserializer)?;
7        let mut session = Self {
8            sid: raw.sid,
9            roles: raw.roles,
10            role_ids: BTreeMap::new(),
11            local_types: raw.local_types,
12            buffers: raw.buffers,
13            edge_lookup: BTreeMap::new(),
14            handler_ids: BTreeMap::new(),
15            handlers_by_id: Vec::new(),
16            edge_handler_lookup: BTreeMap::new(),
17            default_handler_id: None,
18            label_ids: BTreeMap::new(),
19            labels_by_id: Vec::new(),
20            branch_lookup: BTreeMap::new(),
21            auth_leaves: raw.auth_leaves,
22            auth_trees: raw.auth_trees,
23            auth_roots: raw.auth_roots,
24            edge_handlers: raw.edge_handlers,
25            default_handler: raw.default_handler,
26            edge_traces: raw.edge_traces,
27            status: raw.status,
28            epoch: raw.epoch,
29            ownership: raw.ownership,
30        };
31        session.rebuild_derived_indexes();
32        Ok(session)
33    }
34}
35
36/// Store of all sessions managed by the VM.
37///
38/// Provides type lookup/update methods that match the Lean
39/// `SessionStore.lookupType` / `SessionStore.updateType` pattern.
40#[derive(Debug, Default, Serialize, Deserialize)]
41pub struct SessionStore {
42    sessions: BTreeMap<SessionId, SessionState>,
43    #[serde(default)]
44    archived_closed: Vec<ClosedSessionSummary>,
45    next_id: SessionId,
46}
47
48impl SessionStore {
49    fn session_mut_or_error(
50        &mut self,
51        sid: SessionId,
52    ) -> Result<&mut SessionState, OwnershipError> {
53        self.sessions
54            .get_mut(&sid)
55            .ok_or(OwnershipError::SessionNotFound { session_id: sid })
56    }
57
58    fn terminal_error(session: &SessionState) -> Option<OwnershipError> {
59        session
60            .ownership
61            .terminal_reason
62            .clone()
63            .map(|reason| OwnershipError::Terminal {
64                session_id: session.sid,
65                reason,
66            })
67    }
68
69    fn ensure_mutable_ownership(session: &SessionState) -> Result<(), OwnershipError> {
70        if let Some(err) = Self::terminal_error(session) {
71            return Err(err);
72        }
73        Ok(())
74    }
75
76    fn validate_current_owner(
77        session: &SessionState,
78        capability: &OwnershipCapability,
79    ) -> Result<(), OwnershipError> {
80        Self::ensure_mutable_ownership(session)?;
81        let Some(current) = session.ownership.current.as_ref() else {
82            return Err(OwnershipError::Unclaimed {
83                session_id: session.sid,
84            });
85        };
86        if current.owner_id != capability.owner_id || current.generation != capability.generation {
87            return Err(OwnershipError::StaleCapability {
88                session_id: session.sid,
89                owner_id: capability.owner_id.clone(),
90                expected_generation: capability.generation,
91                actual_generation: current.generation,
92            });
93        }
94        Ok(())
95    }
96
97    fn require_session_scope(
98        session: &SessionState,
99        capability: &OwnershipCapability,
100    ) -> Result<(), OwnershipError> {
101        Self::validate_current_owner(session, capability)?;
102        let Some(current) = session.ownership.current.as_ref() else {
103            return Err(OwnershipError::Unclaimed {
104                session_id: session.sid,
105            });
106        };
107        if !current.scope.allows_session_mutation() {
108            return Err(OwnershipError::ScopeViolation {
109                session_id: session.sid,
110                owner_id: current.owner_id.clone(),
111                required: OwnershipScope::Session,
112                actual: current.scope.clone(),
113            });
114        }
115        Ok(())
116    }
117
118    fn next_witness_id(session: &mut SessionState) -> AuthorityWitnessId {
119        let witness_id = session.ownership.next_witness_id;
120        session.ownership.next_witness_id = session.ownership.next_witness_id.saturating_add(1);
121        witness_id
122    }
123
124    fn push_authority_audit(
125        session: &mut SessionState,
126        artifact: AuthorityArtifact,
127        event: AuthorityAuditEvent,
128        reason: Option<String>,
129    ) {
130        session.ownership.audit_log.push(AuthorityAuditRecord {
131            tick: None,
132            artifact,
133            event,
134            reason,
135        });
136    }
137
138    /// Create an empty session store.
139    #[must_use]
140    pub fn new() -> Self {
141        Self::default()
142    }
143
144    /// Open a new session with an externally supplied session id.
145    ///
146    /// Callers should source ids from `SessionStore::next_session_id()`.
147    #[allow(clippy::needless_pass_by_value)]
148    pub fn open_with_sid(
149        &mut self,
150        sid: SessionId,
151        roles: Vec<String>,
152        buffer_config: &BufferConfig,
153        initial_types: &BTreeMap<String, LocalTypeR>,
154    ) -> SessionId {
155        let plan = SessionOpenPlan::new(&roles, initial_types);
156        self.open_with_sid_from_plan(sid, &plan, buffer_config)
157    }
158
159    /// Open a new session from a reusable precomputed open plan.
160    pub fn open_with_sid_from_plan(
161        &mut self,
162        sid: SessionId,
163        plan: &SessionOpenPlan,
164        buffer_config: &BufferConfig,
165    ) -> SessionId {
166        let state = SessionState::from_open_plan(sid, plan, buffer_config);
167        self.sessions.insert(sid, state);
168        self.next_id = self.next_id.max(sid.saturating_add(1));
169        sid
170    }
171
172    /// Open a new session with the given roles, buffer config, and initial local types.
173    ///
174    /// Returns the session ID. Endpoints are constructed as `Endpoint { sid, role }`.
175    #[allow(clippy::needless_pass_by_value)]
176    pub fn open(
177        &mut self,
178        roles: Vec<String>,
179        buffer_config: &BufferConfig,
180        initial_types: &BTreeMap<String, LocalTypeR>,
181    ) -> SessionId {
182        let sid = self.next_id;
183        self.open_with_sid(sid, roles, buffer_config, initial_types)
184    }
185
186    /// Next session identifier that will be allocated by `open`.
187    #[must_use]
188    pub fn next_session_id(&self) -> SessionId {
189        self.next_id
190    }
191
192    // ---- Type state methods (match Lean SessionStore.lookupType / updateType) ----
193
194    /// Lookup the current local type for an endpoint.
195    ///
196    /// Matches Lean `SessionStore.lookupType`.
197    #[must_use]
198    pub fn lookup_type(&self, ep: &Endpoint) -> Option<&LocalTypeR> {
199        self.sessions
200            .get(&ep.sid)?
201            .local_types
202            .get(ep)
203            .map(|e| &e.current)
204    }
205
206    /// Update the local type for an endpoint (type advancement on commit).
207    ///
208    /// Matches Lean `SessionStore.updateType`.
209    pub fn update_type(&mut self, ep: &Endpoint, new_type: LocalTypeR) {
210        if let Some(session) = self.sessions.get_mut(&ep.sid) {
211            if let Some(entry) = session.local_types.get_mut(ep) {
212                entry.current = new_type;
213            }
214            session.refresh_endpoint_branch_lookup(ep);
215        }
216    }
217
218    /// Update the original type (when entering a new Mu scope).
219    pub fn update_original(&mut self, ep: &Endpoint, new_original: LocalTypeR) {
220        if let Some(session) = self.sessions.get_mut(&ep.sid) {
221            if let Some(entry) = session.local_types.get_mut(ep) {
222                entry.original = new_original;
223            }
224        }
225    }
226
227    /// Get the original type for recursive unfolding.
228    #[must_use]
229    pub fn original_type(&self, ep: &Endpoint) -> Option<&LocalTypeR> {
230        self.sessions
231            .get(&ep.sid)?
232            .local_types
233            .get(ep)
234            .map(|e| &e.original)
235    }
236
237    /// Remove type entry (on Halt/End — session endpoint completed).
238    pub fn remove_type(&mut self, ep: &Endpoint) {
239        if let Some(session) = self.sessions.get_mut(&ep.sid) {
240            session.local_types.remove(ep);
241            session.branch_lookup.remove(ep);
242        }
243    }
244
245    // ---- Session access methods ----
246
247    /// Get a reference to a session.
248    #[must_use]
249    pub fn get(&self, sid: SessionId) -> Option<&SessionState> {
250        self.sessions.get(&sid)
251    }
252
253    /// Get a mutable reference to a session.
254    pub fn get_mut(&mut self, sid: SessionId) -> Option<&mut SessionState> {
255        self.sessions.get_mut(&sid)
256    }
257
258    /// Get the current live ownership capability for a session, if any.
259    #[must_use]
260    pub fn current_ownership(&self, sid: SessionId) -> Option<&OwnershipCapability> {
261        self.sessions.get(&sid)?.ownership.current.as_ref()
262    }
263
264    /// Validate that a capability still matches the live owner state.
265    ///
266    /// # Errors
267    ///
268    /// Returns an `OwnershipError` if the session is missing, terminal, or stale.
269    pub fn validate_ownership_capability(
270        &self,
271        capability: &OwnershipCapability,
272    ) -> Result<(), OwnershipError> {
273        let session = self
274            .sessions
275            .get(&capability.session_id)
276            .ok_or(OwnershipError::SessionNotFound {
277                session_id: capability.session_id,
278            })?;
279        Self::validate_current_owner(session, capability)
280    }
281
282    /// Read deterministic authority audit records for one session.
283    #[must_use]
284    pub fn authority_audit_log(&self, sid: SessionId) -> Option<&[AuthorityAuditRecord]> {
285        Some(self.sessions.get(&sid)?.ownership.audit_log.as_slice())
286    }
287
288    /// Claim ownership for an unclaimed session.
289    ///
290    /// # Errors
291    ///
292    /// Returns an `OwnershipError` if the session is missing, terminal, or already claimed.
293    pub fn claim_ownership(
294        &mut self,
295        sid: SessionId,
296        owner_id: impl Into<FragmentOwnerId>,
297        scope: OwnershipScope,
298    ) -> Result<OwnershipCapability, OwnershipError> {
299        let session = self.session_mut_or_error(sid)?;
300        Self::ensure_mutable_ownership(session)?;
301        if let Some(current) = session.ownership.current.as_ref() {
302            return Err(OwnershipError::AlreadyClaimed {
303                session_id: sid,
304                current_owner_id: current.owner_id.clone(),
305            });
306        }
307        if let Some(pending) = session.ownership.pending_transfer.as_ref() {
308            return Err(OwnershipError::TransferPending {
309                session_id: sid,
310                claim_id: pending.receipt.claim_id,
311            });
312        }
313        let capability = OwnershipCapability {
314            session_id: sid,
315            owner_id: owner_id.into(),
316            generation: 0,
317            scope,
318        };
319        session.ownership.current = Some(capability.clone());
320        Ok(capability)
321    }
322
323    /// Release the current owner capability for a session.
324    ///
325    /// # Errors
326    ///
327    /// Returns an `OwnershipError` if the capability is stale or a transfer is still pending.
328    pub fn release_ownership(
329        &mut self,
330        capability: &OwnershipCapability,
331    ) -> Result<(), OwnershipError> {
332        let session = self.session_mut_or_error(capability.session_id)?;
333        Self::validate_current_owner(session, capability)?;
334        if let Some(pending) = session.ownership.pending_transfer.as_ref() {
335            return Err(OwnershipError::TransferPending {
336                session_id: capability.session_id,
337                claim_id: pending.receipt.claim_id,
338            });
339        }
340        session.ownership.current = None;
341        Ok(())
342    }
343
344    /// Begin an explicit ownership transfer and return a typed receipt.
345    ///
346    /// # Errors
347    ///
348    /// Returns an `OwnershipError` if the capability is stale or another transfer is pending.
349    pub fn begin_ownership_transfer(
350        &mut self,
351        capability: &OwnershipCapability,
352        new_owner_id: impl Into<FragmentOwnerId>,
353        new_scope: OwnershipScope,
354    ) -> Result<OwnershipReceipt, OwnershipError> {
355        let session = self.session_mut_or_error(capability.session_id)?;
356        Self::validate_current_owner(session, capability)?;
357        if let Some(pending) = session.ownership.pending_transfer.as_ref() {
358            return Err(OwnershipError::TransferPending {
359                session_id: capability.session_id,
360                claim_id: pending.receipt.claim_id,
361            });
362        }
363        let claim_id = session.ownership.next_claim_id;
364        session.ownership.next_claim_id = session.ownership.next_claim_id.saturating_add(1);
365        let receipt = OwnershipReceipt {
366            session_id: capability.session_id,
367            claim_id,
368            from_owner_id: capability.owner_id.clone(),
369            from_generation: capability.generation,
370            to_owner_id: new_owner_id.into(),
371            to_generation: capability.generation.saturating_add(1),
372            scope: new_scope,
373        };
374        session.ownership.pending_transfer = Some(PendingOwnershipTransfer {
375            receipt: receipt.clone(),
376        });
377        Ok(receipt)
378    }
379
380    /// Commit a previously staged ownership transfer.
381    ///
382    /// # Errors
383    ///
384    /// Returns an `OwnershipError` if the receipt is stale or mismatched.
385    pub fn commit_ownership_transfer(
386        &mut self,
387        receipt: &OwnershipReceipt,
388    ) -> Result<OwnershipCapability, OwnershipError> {
389        let session = self.session_mut_or_error(receipt.session_id)?;
390        Self::ensure_mutable_ownership(session)?;
391        let Some(current) = session.ownership.current.as_ref() else {
392            return Err(OwnershipError::Unclaimed {
393                session_id: receipt.session_id,
394            });
395        };
396        let Some(pending) = session.ownership.pending_transfer.as_ref() else {
397            return Err(OwnershipError::TransferNotPending {
398                session_id: receipt.session_id,
399            });
400        };
401        if pending.receipt != *receipt {
402            return Err(OwnershipError::ReceiptMismatch {
403                session_id: receipt.session_id,
404                claim_id: receipt.claim_id,
405            });
406        }
407        if current.owner_id != receipt.from_owner_id || current.generation != receipt.from_generation
408        {
409            return Err(OwnershipError::StaleCapability {
410                session_id: receipt.session_id,
411                owner_id: receipt.from_owner_id.clone(),
412                expected_generation: receipt.from_generation,
413                actual_generation: current.generation,
414            });
415        }
416        let capability = OwnershipCapability {
417            session_id: receipt.session_id,
418            owner_id: receipt.to_owner_id.clone(),
419            generation: receipt.to_generation,
420            scope: receipt.scope.clone(),
421        };
422        session.ownership.current = Some(capability.clone());
423        session.ownership.pending_transfer = None;
424        Ok(capability)
425    }
426
427    /// Roll back only the staged transfer identified by this receipt.
428    ///
429    /// # Errors
430    ///
431    /// Returns an `OwnershipError` if the receipt does not match the current staged transfer.
432    pub fn rollback_ownership_transfer(
433        &mut self,
434        receipt: &OwnershipReceipt,
435    ) -> Result<(), OwnershipError> {
436        let session = self.session_mut_or_error(receipt.session_id)?;
437        Self::ensure_mutable_ownership(session)?;
438        let Some(pending) = session.ownership.pending_transfer.as_ref() else {
439            return Err(OwnershipError::TransferNotPending {
440                session_id: receipt.session_id,
441            });
442        };
443        if pending.receipt != *receipt {
444            return Err(OwnershipError::ReceiptMismatch {
445                session_id: receipt.session_id,
446                claim_id: receipt.claim_id,
447            });
448        }
449        session.ownership.pending_transfer = None;
450        Ok(())
451    }
452
453    /// Attenuate or otherwise change scope for the same owner.
454    ///
455    /// # Errors
456    ///
457    /// Returns an `OwnershipError` if the capability is stale or a transfer is pending.
458    pub fn attenuate_ownership_scope(
459        &mut self,
460        capability: &OwnershipCapability,
461        new_scope: OwnershipScope,
462    ) -> Result<OwnershipCapability, OwnershipError> {
463        let session = self.session_mut_or_error(capability.session_id)?;
464        Self::validate_current_owner(session, capability)?;
465        if let Some(pending) = session.ownership.pending_transfer.as_ref() {
466            return Err(OwnershipError::TransferPending {
467                session_id: capability.session_id,
468                claim_id: pending.receipt.claim_id,
469            });
470        }
471        let next = OwnershipCapability {
472            session_id: capability.session_id,
473            owner_id: capability.owner_id.clone(),
474            generation: capability.generation.saturating_add(1),
475            scope: new_scope,
476        };
477        session.ownership.current = Some(next.clone());
478        Ok(next)
479    }
480
481    /// Apply session-local host mutation through the ownership gate.
482    ///
483    /// # Errors
484    ///
485    /// Returns an `OwnershipError` if the capability is stale or lacks full-session scope.
486    pub fn apply_owned_session_mutation(
487        &mut self,
488        capability: &OwnershipCapability,
489        mutation: SessionHostMutation,
490    ) -> Result<(), OwnershipError> {
491        let session = self.session_mut_or_error(capability.session_id)?;
492        Self::require_session_scope(session, capability)?;
493        match mutation {
494            SessionHostMutation::SetDefaultHandler { handler } => {
495                let handler_id = session.intern_handler_binding(&handler);
496                session.default_handler = handler;
497                session.default_handler_id = Some(handler_id);
498            }
499            SessionHostMutation::UpdateEdgeHandler { edge, handler } => {
500                let handler_id = session.intern_handler_binding(&handler);
501                if let Some(edge_key) = session.edge_key_for_roles(&edge.sender, &edge.receiver) {
502                    session.edge_handler_lookup.insert(edge_key, handler_id);
503                }
504                session.edge_handlers.insert(edge, handler);
505            }
506            SessionHostMutation::UpdateTrace { edge, trace } => {
507                session.edge_traces.insert(edge, trace);
508            }
509        }
510        Ok(())
511    }
512
513    /// Issue a single-use readiness witness under the current owner capability.
514    ///
515    /// # Errors
516    ///
517    /// Returns an `OwnershipError` if the capability is stale or lacks full-session scope.
518    pub fn issue_readiness_witness(
519        &mut self,
520        capability: &OwnershipCapability,
521        predicate_ref: impl Into<String>,
522    ) -> Result<ReadinessWitness, OwnershipError> {
523        let session = self.session_mut_or_error(capability.session_id)?;
524        Self::require_session_scope(session, capability)?;
525        let witness = ReadinessWitness {
526            witness_id: Self::next_witness_id(session),
527            session_id: capability.session_id,
528            owner_id: capability.owner_id.clone(),
529            generation: capability.generation,
530            scope: capability.scope.clone(),
531            predicate_ref: predicate_ref.into(),
532        };
533        session
534            .ownership
535            .issued_readiness
536            .insert(witness.witness_id, witness.clone());
537        Self::push_authority_audit(
538            session,
539            AuthorityArtifact::Readiness(witness.clone()),
540            AuthorityAuditEvent::Issued,
541            None,
542        );
543        Ok(witness)
544    }
545
546    /// Consume a readiness witness exactly once under the same live owner capability.
547    ///
548    /// # Errors
549    ///
550    /// Returns an `OwnershipError` if the witness is stale, forged, mismatched, or already used.
551    pub fn consume_readiness_witness(
552        &mut self,
553        capability: &OwnershipCapability,
554        witness: &ReadinessWitness,
555    ) -> Result<(), OwnershipError> {
556        let session = self.session_mut_or_error(capability.session_id)?;
557        Self::require_session_scope(session, capability)?;
558        if session.ownership.consumed_witnesses.contains(&witness.witness_id) {
559            Self::push_authority_audit(
560                session,
561                AuthorityArtifact::Readiness(witness.clone()),
562                AuthorityAuditEvent::Rejected,
563                Some("witness already consumed".to_string()),
564            );
565            return Err(OwnershipError::WitnessConsumed {
566                session_id: witness.session_id,
567                witness_id: witness.witness_id,
568            });
569        }
570        let Some(issued) = session.ownership.issued_readiness.get(&witness.witness_id) else {
571            Self::push_authority_audit(
572                session,
573                AuthorityArtifact::Readiness(witness.clone()),
574                AuthorityAuditEvent::Rejected,
575                Some("witness was never issued".to_string()),
576            );
577            return Err(OwnershipError::InvalidWitness {
578                session_id: capability.session_id,
579                witness_id: witness.witness_id,
580                reason: "witness was never issued".to_string(),
581            });
582        };
583        if issued != witness {
584            Self::push_authority_audit(
585                session,
586                AuthorityArtifact::Readiness(witness.clone()),
587                AuthorityAuditEvent::Rejected,
588                Some("witness payload mismatch".to_string()),
589            );
590            return Err(OwnershipError::InvalidWitness {
591                session_id: capability.session_id,
592                witness_id: witness.witness_id,
593                reason: "witness payload mismatch".to_string(),
594            });
595        }
596        if witness.session_id != capability.session_id
597            || witness.owner_id != capability.owner_id
598            || witness.generation != capability.generation
599            || witness.scope != capability.scope
600        {
601            Self::push_authority_audit(
602                session,
603                AuthorityArtifact::Readiness(witness.clone()),
604                AuthorityAuditEvent::Rejected,
605                Some("live ownership no longer matches witness".to_string()),
606            );
607            return Err(OwnershipError::InvalidWitness {
608                session_id: capability.session_id,
609                witness_id: witness.witness_id,
610                reason: "live ownership no longer matches witness".to_string(),
611            });
612        }
613        session.ownership.issued_readiness.remove(&witness.witness_id);
614        session
615            .ownership
616            .consumed_witnesses
617            .insert(witness.witness_id);
618        Self::push_authority_audit(
619            session,
620            AuthorityArtifact::Readiness(witness.clone()),
621            AuthorityAuditEvent::Consumed,
622            None,
623        );
624        Ok(())
625    }
626
627    /// Mark the current owner as dead and fault the session.
628    ///
629    /// # Errors
630    ///
631    /// Returns an `OwnershipError` if the session is missing or owner mismatch occurs.
632    pub fn mark_owner_died(
633        &mut self,
634        sid: SessionId,
635        owner_id: &str,
636    ) -> Result<CancellationWitness, OwnershipError> {
637        let session = self.session_mut_or_error(sid)?;
638        Self::ensure_mutable_ownership(session)?;
639        let Some(current) = session.ownership.current.as_ref() else {
640            return Err(OwnershipError::Unclaimed { session_id: sid });
641        };
642        if current.owner_id != owner_id {
643            return Err(OwnershipError::StaleCapability {
644                session_id: sid,
645                owner_id: owner_id.to_string(),
646                expected_generation: current.generation,
647                actual_generation: current.generation,
648            });
649        }
650        let generation = current.generation;
651        let reason = OwnershipTerminalReason::OwnerDied {
652            owner_id: owner_id.to_string(),
653        };
654        session.status = SessionStatus::Faulted {
655            reason: format!("ownership owner `{owner_id}` died"),
656        };
657        let witness = CancellationWitness {
658            witness_id: Self::next_witness_id(session),
659            session_id: sid,
660            owner_id: owner_id.to_string(),
661            generation,
662            reason: reason.clone(),
663        };
664        session.ownership.current = None;
665        session.ownership.pending_transfer = None;
666        session.ownership.terminal_reason = Some(reason);
667        Self::push_authority_audit(
668            session,
669            AuthorityArtifact::Cancellation(witness.clone()),
670            AuthorityAuditEvent::Issued,
671            None,
672        );
673        Ok(witness)
674    }
675
676    /// Cancel a session because a staged transfer was abandoned.
677    ///
678    /// # Errors
679    ///
680    /// Returns an `OwnershipError` if the receipt does not match the staged transfer.
681    pub fn cancel_abandoned_transfer(
682        &mut self,
683        receipt: &OwnershipReceipt,
684    ) -> Result<CancellationWitness, OwnershipError> {
685        let session = self.session_mut_or_error(receipt.session_id)?;
686        Self::ensure_mutable_ownership(session)?;
687        let Some(pending) = session.ownership.pending_transfer.as_ref() else {
688            return Err(OwnershipError::TransferNotPending {
689                session_id: receipt.session_id,
690            });
691        };
692        if pending.receipt != *receipt {
693            return Err(OwnershipError::ReceiptMismatch {
694                session_id: receipt.session_id,
695                claim_id: receipt.claim_id,
696            });
697        }
698        let reason = OwnershipTerminalReason::TransferAbandoned {
699            owner_id: receipt.from_owner_id.clone(),
700            claim_id: receipt.claim_id,
701        };
702        session.status = SessionStatus::Cancelled;
703        let witness = CancellationWitness {
704            witness_id: Self::next_witness_id(session),
705            session_id: receipt.session_id,
706            owner_id: receipt.from_owner_id.clone(),
707            generation: receipt.from_generation,
708            reason: reason.clone(),
709        };
710        session.ownership.current = None;
711        session.ownership.pending_transfer = None;
712        session.ownership.terminal_reason = Some(reason);
713        Self::push_authority_audit(
714            session,
715            AuthorityArtifact::Cancellation(witness.clone()),
716            AuthorityAuditEvent::Issued,
717            None,
718        );
719        Ok(witness)
720    }
721
722    /// Fault a session because a staged transfer could not commit.
723    ///
724    /// # Errors
725    ///
726    /// Returns an `OwnershipError` if the receipt does not match the staged transfer.
727    pub fn fault_failed_transfer_commit(
728        &mut self,
729        receipt: &OwnershipReceipt,
730        reason: impl Into<String>,
731    ) -> Result<(), OwnershipError> {
732        let session = self.session_mut_or_error(receipt.session_id)?;
733        Self::ensure_mutable_ownership(session)?;
734        let Some(pending) = session.ownership.pending_transfer.as_ref() else {
735            return Err(OwnershipError::TransferNotPending {
736                session_id: receipt.session_id,
737            });
738        };
739        if pending.receipt != *receipt {
740            return Err(OwnershipError::ReceiptMismatch {
741                session_id: receipt.session_id,
742                claim_id: receipt.claim_id,
743            });
744        }
745        let reason = reason.into();
746        let terminal = OwnershipTerminalReason::TransferCommitFailed {
747            owner_id: receipt.from_owner_id.clone(),
748            claim_id: receipt.claim_id,
749            reason: reason.clone(),
750        };
751        session.status = SessionStatus::Faulted {
752            reason: format!("ownership transfer commit failed: {reason}"),
753        };
754        session.ownership.current = None;
755        session.ownership.pending_transfer = None;
756        session.ownership.terminal_reason = Some(terminal);
757        Ok(())
758    }
759
760    /// Iterate over all sessions.
761    pub fn iter(&self) -> impl Iterator<Item = &SessionState> {
762        self.sessions.values()
763    }
764
765    /// Close a session.
766    ///
767    /// # Errors
768    ///
769    /// Returns an error if the session is not found.
770    pub fn close(&mut self, sid: SessionId) -> Result<(), String> {
771        let session = self
772            .sessions
773            .get_mut(&sid)
774            .ok_or_else(|| format!("session {sid} not found"))?;
775
776        session.status = SessionStatus::Closed;
777        session.buffers.clear();
778        session.edge_traces.clear();
779        session.epoch = session.epoch.saturating_add(1);
780        Ok(())
781    }
782
783    /// Closed/cancelled/faulted session identifiers still resident in the store.
784    #[must_use]
785    pub fn closed_session_ids(&self) -> Vec<SessionId> {
786        self.sessions
787            .iter()
788            .filter_map(|(sid, session)| {
789                matches!(
790                    session.status,
791                    SessionStatus::Closed
792                        | SessionStatus::Cancelled
793                        | SessionStatus::Faulted { .. }
794                )
795                .then_some(*sid)
796            })
797            .collect()
798    }
799
800    /// Reap specific session ids from live storage and archive compact summaries.
801    ///
802    /// # Panics
803    ///
804    /// Panics if a session disappears between the initial residency/status check
805    /// and the subsequent removal from the store.
806    pub fn reap_sessions(&mut self, session_ids: &[SessionId]) -> Vec<ClosedSessionSummary> {
807        let mut reaped = Vec::new();
808        for sid in session_ids {
809            let Some(session) = self.sessions.get(sid) else {
810                continue;
811            };
812            if !matches!(
813                session.status,
814                SessionStatus::Closed | SessionStatus::Cancelled | SessionStatus::Faulted { .. }
815            ) {
816                continue;
817            }
818
819            let session = self
820                .sessions
821                .remove(sid)
822                .expect("session existence checked before removal");
823            let summary = ClosedSessionSummary::from_session(&session);
824            self.archived_closed.push(summary.clone());
825            reaped.push(summary);
826        }
827        reaped
828    }
829
830    /// Reap all closed/cancelled/faulted sessions from live storage.
831    pub fn reap_closed(&mut self) -> Vec<ClosedSessionSummary> {
832        let sids = self.closed_session_ids();
833        self.reap_sessions(&sids)
834    }
835
836    /// Number of active sessions.
837    #[must_use]
838    pub fn active_count(&self) -> usize {
839        self.sessions
840            .values()
841            .filter(|s| s.status == SessionStatus::Active)
842            .count()
843    }
844
845    /// Number of sessions still resident in the store.
846    #[must_use]
847    pub fn live_count(&self) -> usize {
848        self.sessions.len()
849    }
850
851    /// All session IDs.
852    #[must_use]
853    pub fn session_ids(&self) -> Vec<SessionId> {
854        self.sessions.keys().copied().collect()
855    }
856
857    /// Archived closed-session summaries retained after reaping.
858    #[must_use]
859    pub fn archived_closed(&self) -> &[ClosedSessionSummary] {
860        &self.archived_closed
861    }
862
863    /// Approximate retained state for the session store.
864    #[must_use]
865    pub fn memory_usage(&self) -> SessionStoreMemoryUsage {
866        let mut usage = SessionStoreMemoryUsage {
867            live_sessions: self.sessions.len(),
868            archived_closed_sessions: self.archived_closed.len(),
869            ..SessionStoreMemoryUsage::default()
870        };
871        usage.retained_bytes.archived_closed = self
872            .archived_closed
873            .iter()
874            .map(ClosedSessionSummary::retained_bytes_estimate)
875            .sum();
876
877        for session in self.sessions.values() {
878            if matches!(
879                session.status,
880                SessionStatus::Closed | SessionStatus::Cancelled | SessionStatus::Faulted { .. }
881            ) {
882                usage.live_closed_sessions += 1;
883            }
884            usage.live_local_type_entries += session.local_types.len();
885            usage.live_buffer_count += session.buffers.len();
886            usage.live_buffered_messages += session
887                .buffers
888                .values()
889                .map(BoundedBuffer::len)
890                .sum::<usize>();
891            usage.live_edge_handler_count += session.edge_handlers.len();
892            usage.live_auth_leaf_count += session.auth_leaves.values().map(Vec::len).sum::<usize>();
893            usage.live_auth_tree_count += session.auth_trees.len();
894            usage.live_auth_root_count += session.auth_roots.len();
895            usage.retained_bytes.live_sessions += session.retained_session_core_bytes();
896            usage.retained_bytes.local_types += session.retained_local_type_bytes();
897            usage.retained_bytes.buffers += session.retained_buffer_bytes();
898            usage.retained_bytes.traces += session.retained_trace_bytes();
899            usage.retained_bytes.auth += session.retained_auth_bytes();
900            usage.retained_bytes.handlers += session.retained_handler_bytes();
901        }
902        usage.retained_bytes.total = usage
903            .retained_bytes
904            .live_sessions
905            .saturating_add(usage.retained_bytes.archived_closed)
906            .saturating_add(usage.retained_bytes.local_types)
907            .saturating_add(usage.retained_bytes.buffers)
908            .saturating_add(usage.retained_bytes.traces)
909            .saturating_add(usage.retained_bytes.auth)
910            .saturating_add(usage.retained_bytes.handlers);
911
912        usage
913    }
914
915    /// Lookup edge-bound handler id.
916    #[must_use]
917    pub fn lookup_handler(&self, edge: &Edge) -> Option<&HandlerId> {
918        self.sessions
919            .get(&edge.sid)?
920            .lookup_handler_for_roles(&edge.sender, &edge.receiver)
921    }
922
923    /// Lookup a default handler id for a session.
924    #[must_use]
925    pub fn default_handler_for_session(&self, sid: SessionId) -> Option<&HandlerId> {
926        self.sessions.get(&sid)?.default_handler_binding()
927    }
928
929    /// Set the default handler id for a session.
930    pub(crate) fn set_default_handler_for_session(&mut self, sid: SessionId, handler: HandlerId) {
931        if let Some(session) = self.sessions.get_mut(&sid) {
932            let handler_id = session.intern_handler_binding(&handler);
933            session.default_handler = handler;
934            session.default_handler_id = Some(handler_id);
935        }
936    }
937
938    /// Update edge-bound handler id.
939    pub(crate) fn update_handler(&mut self, edge: &Edge, handler: HandlerId) {
940        if let Some(session) = self.sessions.get_mut(&edge.sid) {
941            let handler_id = session.intern_handler_binding(&handler);
942            if let Some(edge_key) = session.edge_key_for_roles(&edge.sender, &edge.receiver) {
943                session.edge_handler_lookup.insert(edge_key, handler_id);
944            }
945            session.edge_handlers.insert(edge.clone(), handler);
946        }
947    }
948
949    /// Lookup coherence trace for an edge.
950    #[must_use]
951    pub fn lookup_trace(&self, edge: &Edge) -> Option<&[ValType]> {
952        self.sessions
953            .get(&edge.sid)?
954            .edge_traces
955            .get(edge)
956            .map(Vec::as_slice)
957    }
958
959    /// Update coherence trace for an edge.
960    #[cfg_attr(not(test), allow(dead_code))]
961    pub(crate) fn update_trace(&mut self, edge: &Edge, trace: Vec<ValType>) {
962        if let Some(session) = self.sessions.get_mut(&edge.sid) {
963            session.edge_traces.insert(edge.clone(), trace);
964        }
965    }
966}
967
968// ---- Type unfolding utilities ----
969
970/// Unfold top-level `Mu` to its body.
971///
972/// Recursively strips `Mu` constructors to reach the first action.
973#[must_use]
974// RECURSION_SAFE: each step unwraps one Mu node from a finite local type tree.
975pub fn unfold_mu(lt: &LocalTypeR) -> LocalTypeR {
976    match lt {
977        LocalTypeR::Mu { body, .. } => unfold_mu(body),
978        other => other.clone(),
979    }
980}
981
982/// Resolve a continuation that may be a `Var` (recursive reference).
983///
984/// If `cont` is `Var`, unfolds back to the original type's mu body.
985/// If `cont` is `Mu`, unfolds it. Otherwise returns as-is.
986#[must_use]
987pub fn unfold_if_var(cont: &LocalTypeR, original: &LocalTypeR) -> LocalTypeR {
988    match cont {
989        LocalTypeR::Var(_) => unfold_mu(original),
990        LocalTypeR::Mu { .. } => unfold_mu(cont),
991        other => other.clone(),
992    }
993}
994
995/// Like `unfold_if_var`, but also returns the new Mu scope (original) if one was entered.
996///
997/// When the continuation is a `Mu`, the Mu itself becomes the new original
998/// for subsequent `Var` resolution. Returns `(resolved_type, Some(mu))` when
999/// entering a new Mu scope, `(resolved_type, None)` otherwise.
1000#[must_use]
1001pub fn unfold_if_var_with_scope(
1002    cont: &LocalTypeR,
1003    original: &LocalTypeR,
1004) -> (LocalTypeR, Option<LocalTypeR>) {
1005    match cont {
1006        LocalTypeR::Var(_) => (unfold_mu(original), None),
1007        LocalTypeR::Mu { .. } => (unfold_mu(cont), Some(cont.clone())),
1008        other => (other.clone(), None),
1009    }
1010}