1use serde::{Deserialize, Serialize};
9use std::fmt;
10
11use super::{
12 AgentRuntimeState, Session, AGENT_RUNTIME_STATE_VERSION, MODEL_CONTEXT_SCHEMA_VERSION,
13};
14
15pub const CURRENT_SESSION_VERSION: u32 = 1;
16
17#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
18#[serde(rename_all = "snake_case")]
19pub enum SessionCompatibilityStatus {
20 Compatible,
21 LegacyCompatible,
22 Conflict,
23}
24
25#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
26pub struct SessionCompatibilityConflict {
27 pub code: String,
28 pub message: String,
29}
30
31#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
32pub struct SessionCompatibilityReport {
33 pub session_version: Option<u32>,
34 pub current_session_version: u32,
35 pub status: SessionCompatibilityStatus,
36 pub history_readable: bool,
37 pub execution_allowed: bool,
38 pub conflicts: Vec<SessionCompatibilityConflict>,
39}
40
41#[derive(Debug, Clone, PartialEq, Eq)]
42pub struct SessionCompatibilityError {
43 pub compatibility: SessionCompatibilityReport,
44}
45
46impl fmt::Display for SessionCompatibilityError {
47 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
48 f.write_str("session_execution_incompatible: ")?;
49 for (index, conflict) in self.compatibility.conflicts.iter().enumerate() {
50 if index != 0 {
51 f.write_str("; ")?;
52 }
53 write!(f, "{}: {}", conflict.code, conflict.message)?;
54 }
55 f.write_str(" Read the existing history with a compatible runtime, or start a new session with selected context and an explicit workspace. No state was migrated.")
56 }
57}
58
59impl std::error::Error for SessionCompatibilityError {}
60
61fn supported(version: Option<u32>) -> bool {
62 version.is_none_or(|version| version == CURRENT_SESSION_VERSION)
63}
64
65impl Session {
66 pub fn observe_main_session_version(&mut self, main_version: Option<u32>) {
70 self.observed_runtime_session_version = Some(self.session_version);
71 self.session_version = main_version;
72 }
73
74 pub fn execution_compatibility(&self) -> SessionCompatibilityReport {
75 let mut conflicts = Vec::new();
76 if let Some(version) = self
77 .session_version
78 .filter(|_| !supported(self.session_version))
79 {
80 conflicts.push(SessionCompatibilityConflict {
81 code: "unsupported_session_version".into(),
82 message: format!("Session behavior version {version} is not supported; this runtime supports version {CURRENT_SESSION_VERSION} and compatible legacy records."),
83 });
84 }
85 if let Some(runtime_version) = self.observed_runtime_session_version {
86 if runtime_version != self.session_version
87 && !(supported(runtime_version) && supported(self.session_version))
88 {
89 conflicts.push(SessionCompatibilityConflict {
90 code: "session_version_mismatch".into(),
91 message: format!("Canonical Session behavior {:?} and retained runtime behavior {:?} conflict.", self.session_version, runtime_version),
92 });
93 }
94 }
95 if let Some(AgentRuntimeState { version, .. }) = &self.agent_runtime_state {
96 if *version != AGENT_RUNTIME_STATE_VERSION {
97 conflicts.push(SessionCompatibilityConflict {
98 code: "unsupported_runtime_state_version".into(),
99 message: format!("Retained execution-state schema {version} is not supported; expected {AGENT_RUNTIME_STATE_VERSION}."),
100 });
101 }
102 }
103 if let Some(context) = &self.model_context_state {
104 if context.schema_version != MODEL_CONTEXT_SCHEMA_VERSION {
105 conflicts.push(SessionCompatibilityConflict {
106 code: "unsupported_model_context_version".into(),
107 message: format!("Retained model-context schema {} is not supported; expected {MODEL_CONTEXT_SCHEMA_VERSION}.", context.schema_version),
108 });
109 }
110 }
111 let execution_allowed = conflicts.is_empty();
112 SessionCompatibilityReport {
113 session_version: self.session_version,
114 current_session_version: CURRENT_SESSION_VERSION,
115 status: if !execution_allowed {
116 SessionCompatibilityStatus::Conflict
117 } else if self.session_version.is_none() {
118 SessionCompatibilityStatus::LegacyCompatible
119 } else {
120 SessionCompatibilityStatus::Compatible
121 },
122 history_readable: true,
123 execution_allowed,
124 conflicts,
125 }
126 }
127
128 pub fn check_execution_compatibility(&self) -> Result<(), SessionCompatibilityError> {
129 let compatibility = self.execution_compatibility();
130 if compatibility.execution_allowed {
131 Ok(())
132 } else {
133 Err(SessionCompatibilityError { compatibility })
134 }
135 }
136}
137
138#[cfg(test)]
139mod tests {
140 use super::*;
141 use crate::{Message, SessionAuthorityIdentity};
142
143 #[test]
144 fn missing_stamp_is_assessed_and_compatible_without_mutation() {
145 let mut session = Session::new("legacy", "model");
146 session.session_version = None;
147 session.add_message(Message::user("keep history"));
148 let before = serde_json::to_value(&session).unwrap();
149 let report = session.execution_compatibility();
150 assert_eq!(report.status, SessionCompatibilityStatus::LegacyCompatible);
151 assert!(report.execution_allowed && report.history_readable);
152 session.check_execution_compatibility().unwrap();
153 assert_eq!(serde_json::to_value(&session).unwrap(), before);
154 }
155
156 #[test]
157 fn current_and_legacy_contracts_preserve_mode_and_identity_boundaries() {
158 for version in [None, Some(CURRENT_SESSION_VERSION)] {
159 for ultra in [false, true] {
160 for supervisor in [false, true] {
161 let mut session = Session::new(
162 if supervisor {
163 crate::DEFAULT_SUPERVISOR_SESSION_ID
164 } else {
165 "root"
166 },
167 "model",
168 );
169 session.session_version = version;
170 session.set_root_orchestration_only(ultra).unwrap();
171 if supervisor {
172 session.authority_identity = SessionAuthorityIdentity::Supervisor {
173 incarnation_id: uuid::Uuid::new_v4(),
174 };
175 }
176 let before = serde_json::to_value(&session).unwrap();
177 assert!(session.execution_compatibility().execution_allowed);
178 session.check_execution_compatibility().unwrap();
179 assert_eq!(serde_json::to_value(&session).unwrap(), before);
180 assert_eq!(
181 session.allows_model_tool_execution("Bash"),
182 !ultra && !supervisor
183 );
184 let mut child = Session::new_child_of("child", &session, "model", "child");
185 child.session_version = version;
186 assert!(child.execution_compatibility().execution_allowed);
187 assert!(child.allows_model_tool_execution("Bash"));
188 }
189 }
190 }
191 }
192
193 #[test]
194 fn unsupported_versions_are_diagnostics_not_deserialization_failures() {
195 for version in [0, 2, u32::MAX] {
196 let mut session = Session::new("future", "model");
197 session.session_version = Some(version);
198 let before = serde_json::to_value(&session).unwrap();
199 let reloaded: Session = serde_json::from_value(before.clone()).unwrap();
200 let report = reloaded.execution_compatibility();
201 assert!(report.history_readable);
202 assert!(!report.execution_allowed);
203 assert_eq!(report.conflicts[0].code, "unsupported_session_version");
204 assert!(reloaded
205 .check_execution_compatibility()
206 .unwrap_err()
207 .to_string()
208 .contains("session_execution_incompatible"));
209 assert_eq!(serde_json::to_value(reloaded).unwrap(), before);
210 }
211 }
212
213 #[test]
214 fn runtime_overlay_cannot_hide_an_unsupported_main_or_side_stamp() {
215 for (main, runtime) in [(Some(99), None), (Some(1), Some(99)), (None, Some(99))] {
216 let mut side = Session::new("pair", "model");
217 side.session_version = runtime;
218 side.observe_main_session_version(main);
219 assert_eq!(side.session_version, main);
220 assert!(!side.execution_compatibility().execution_allowed);
221 }
222 for (main, runtime) in [(None, Some(1)), (Some(1), None), (None, None)] {
223 let mut side = Session::new("pair", "model");
224 side.session_version = runtime;
225 side.observe_main_session_version(main);
226 assert_eq!(side.session_version, main);
227 assert!(side.execution_compatibility().execution_allowed);
228 }
229 }
230
231 #[test]
232 fn unsupported_retained_model_context_is_refused_without_reset() {
233 let mut session = Session::new("future-context", "model");
234 session.session_version = None;
235 let context = super::super::ModelContextState {
236 schema_version: 99,
237 state_revision: 7,
238 prefix_epoch: 3,
239 ..Default::default()
240 };
241 session.model_context_state = Some(context);
242 let before = serde_json::to_value(&session).unwrap();
243 let report = session.execution_compatibility();
244 assert!(!report.execution_allowed);
245 assert_eq!(
246 report.conflicts[0].code,
247 "unsupported_model_context_version"
248 );
249 assert_eq!(serde_json::to_value(session).unwrap(), before);
250 }
251
252 #[test]
253 fn unsupported_retained_runtime_state_is_refused_without_reset() {
254 let mut session = Session::new("future-runtime", "model");
255 session.session_version = None;
256 let mut state = AgentRuntimeState::new("retained-execution");
257 state.version = 99;
258 session.agent_runtime_state = Some(state);
259 let before = serde_json::to_value(&session).unwrap();
260 let report = session.execution_compatibility();
261 assert!(!report.execution_allowed);
262 assert_eq!(
263 report.conflicts[0].code,
264 "unsupported_runtime_state_version"
265 );
266 assert_eq!(serde_json::to_value(session).unwrap(), before);
267 }
268}