Skip to main content

bamboo_domain/session/
conversation_generation.rs

1//! One canonical conversation generation, independent of roles and tool aliases.
2//!
3//! These checks never adopt a newer generation into an older transcript. A
4//! mismatch requires a complete canonical reload or refusal. Only the future
5//! fenced reset transaction may publish a new ledger; normal writers preserve it.
6
7use sha2::{Digest, Sha256};
8
9use super::{ConversationResetState, Session, SessionKind};
10
11/// Host-local data captured from the original caller. This is not serializable,
12/// is not a grant and is never refreshed from a new Actor lease. Identity and
13/// caller authority remain independently checked by their existing boundaries.
14#[derive(Debug, Clone, PartialEq, Eq)]
15pub struct ConversationGenerationObservation(Option<ConversationResetState>);
16
17impl ConversationGenerationObservation {
18    /// Check the whole ledger against a validated canonical Session. No context
19    /// is adopted, even when the observed revision number happens to match.
20    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    /// Capture data only; final use validates canonical state and authority.
41    pub fn conversation_generation_observation(&self) -> ConversationGenerationObservation {
42        ConversationGenerationObservation(self.conversation_reset.clone())
43    }
44
45    /// Observation only, never an authority grant. Invalid ledgers are rejected
46    /// by `validate_conversation_state` and final canonical writer checks.
47    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            // The zero state is represented only by absence. Do not migrate an
63            // old Session or introduce a second serialized spelling by saving.
64            || 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    /// Preserve the exact canonical ledger, including receipts at the same
73    /// revision. A copied revision number alone cannot authorize stale context.
74    /// Original zero/zero Sessions retain their existing lifetime validation at
75    /// their own caller/storage boundary; this method adds no new zero-state
76    /// rebind or identity semantics.
77    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    /// Bounded proof projection of the complete canonical ledger. The inline
100    /// state stays authoritative; a hash alone is not a replacement ledger.
101    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;