bamboo_domain/session/
conversation_generation.rs1use sha2::{Digest, Sha256};
8
9use super::{ConversationResetState, Session, SessionKind};
10
11#[derive(Debug, Clone, PartialEq, Eq)]
15pub struct ConversationGenerationObservation(Option<ConversationResetState>);
16
17impl ConversationGenerationObservation {
18 pub fn check_canonical(&self, canonical: &Session) -> Result<(), ConversationGenerationError> {
21 canonical.validate_conversation_state()?;
22 if self.0 != canonical.conversation_reset {
23 return Err(ConversationGenerationError::StaleGeneration);
24 }
25 Ok(())
26 }
27}
28
29#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
30pub enum ConversationGenerationError {
31 #[error("Session conversation reset state is invalid or unsupported")]
32 InvalidState,
33 #[error("Session conversation generation belongs to a different lifetime")]
34 LifetimeChanged,
35 #[error("Session conversation generation changed; reload the complete canonical Session")]
36 StaleGeneration,
37}
38
39impl Session {
40 pub fn conversation_generation_observation(&self) -> ConversationGenerationObservation {
42 ConversationGenerationObservation(self.conversation_reset.clone())
43 }
44
45 pub fn conversation_revision(&self) -> u64 {
48 self.conversation_reset
49 .as_ref()
50 .map_or(0, |state| state.conversation_revision)
51 }
52
53 pub fn validate_conversation_state(&self) -> Result<(), ConversationGenerationError> {
54 let Some(state) = self.conversation_reset.as_ref() else {
55 return Ok(());
56 };
57 if self.kind != SessionKind::Root
58 || self.parent_session_id.is_some()
59 || self.parent_created_at.is_some()
60 || self.spawn_depth != 0
61 || (!self.root_session_id.is_empty() && self.root_session_id != self.id)
62 || state.conversation_revision == 0
65 || !state.valid_for(&self.root_mode_birth_token())
66 {
67 return Err(ConversationGenerationError::InvalidState);
68 }
69 Ok(())
70 }
71
72 pub fn check_conversation_generation(
78 &self,
79 canonical: &Session,
80 ) -> Result<(), ConversationGenerationError> {
81 self.validate_conversation_state()?;
82 canonical.validate_conversation_state()?;
83 if self.conversation_reset.is_none() && canonical.conversation_reset.is_none() {
84 return Ok(());
85 }
86 if self.id != canonical.id
87 || self.created_at != canonical.created_at
88 || self.kind != canonical.kind
89 || self.authority_identity != canonical.authority_identity
90 {
91 return Err(ConversationGenerationError::LifetimeChanged);
92 }
93 if self.conversation_reset != canonical.conversation_reset {
94 return Err(ConversationGenerationError::StaleGeneration);
95 }
96 Ok(())
97 }
98
99 pub fn conversation_reset_sha256(&self) -> Result<Option<String>, ConversationGenerationError> {
102 self.validate_conversation_state()?;
103 let Some(state) = self.conversation_reset.as_ref() else {
104 return Ok(None);
105 };
106 let bytes =
107 serde_json::to_vec(state).map_err(|_| ConversationGenerationError::InvalidState)?;
108 let mut digest = Sha256::new();
109 digest.update(b"bamboo.conversation-reset.ledger.v1\0");
110 digest.update(bytes);
111 Ok(Some(hex::encode(digest.finalize())))
112 }
113}
114
115#[cfg(test)]
116#[path = "conversation_generation_tests.rs"]
117mod tests;