magi_code/sessions/
manager.rs1use super::metadata::{SessionMetadataRecord, SessionMetadataSummary};
2use super::read::validate_session_id;
3use super::store::{prepare_session_root, primary_path};
4use crate::persistence::CrossProcessFileLock;
5use std::{
6 collections::HashMap,
7 fmt,
8 path::{Path, PathBuf},
9 sync::{Arc, Mutex, OnceLock, Weak},
10};
11use uuid::Uuid;
12
13#[derive(Debug, Clone, PartialEq, Eq)]
14pub(crate) struct SessionInternalDiagnostic {
15 pub(crate) session_id: Option<String>,
16 pub(crate) message: String,
17}
18
19#[derive(Debug, Clone, PartialEq, Eq)]
20pub(crate) struct SessionListReport {
21 pub(crate) summaries: Vec<SessionMetadataSummary>,
22 pub(crate) diagnostics: Vec<SessionInternalDiagnostic>,
23}
24
25#[derive(Debug, Clone)]
26pub struct SessionManager {
27 pub(in crate::sessions) root: PathBuf,
28}
29
30impl SessionManager {
31 #[must_use]
32 pub fn new(root: PathBuf) -> Self {
33 Self { root }
34 }
35
36 pub(crate) fn create(&self) -> anyhow::Result<Session> {
37 prepare_session_root(&self.root)?;
38 let id = Uuid::new_v4().to_string();
39 let path = self.path_for_valid_id(&id)?;
40 Ok(Session::new(id, path))
42 }
43
44 pub fn open(&self, id: impl Into<String>) -> anyhow::Result<Session> {
45 let id = validate_session_id(id.into())?;
46 Ok(Session::new(id.clone(), self.path_for_valid_id(&id)?))
47 }
48
49 pub(crate) fn open_existing(&self, id: impl Into<String>) -> anyhow::Result<Session> {
50 let session = self.open(id)?;
51 super::store::open_existing_primary(&self.root, &session.id)?
52 .ok_or_else(|| anyhow::anyhow!("session JSONL is missing"))?;
53 Ok(session)
54 }
55
56 #[cfg(test)]
57 pub(crate) fn list(&self) -> anyhow::Result<Vec<Session>> {
58 Ok(self
59 .list_metadata_summaries()?
60 .into_iter()
61 .map(|summary| summary.session)
62 .collect())
63 }
64
65 pub(crate) fn most_recent(&self) -> anyhow::Result<Option<Session>> {
66 let report = self.list_metadata_report()?;
67 if !report.diagnostics.is_empty() {
68 anyhow::bail!("session discovery found unreadable or unsafe history");
69 }
70 Ok(report
71 .summaries
72 .into_iter()
73 .last()
74 .map(|summary| summary.session))
75 }
76
77 pub(crate) fn path_for_valid_id(&self, id: &str) -> anyhow::Result<PathBuf> {
78 primary_path(&self.root, id)
79 }
80}
81
82pub struct Session {
83 pub(in crate::sessions) id: String,
84 pub(in crate::sessions) path: PathBuf,
85 pub(in crate::sessions) metadata_cache: Arc<Mutex<Option<SessionMetadataRecord>>>,
86 pub(crate) maintenance: crate::progressive_session_maintenance::ObserverSlot,
87 replay_state: Arc<SessionReplayState>,
88 active_lease: Arc<Mutex<Option<Arc<ActiveLeaseSlot>>>>,
89 standalone_writer: Option<Arc<CrossProcessFileLock>>,
91}
92
93struct SessionReplayState {
94 generation: std::sync::atomic::AtomicU64,
95}
96
97type ActiveLeaseSlot = Mutex<Option<Arc<CrossProcessFileLock>>>;
98
99static ACTIVE_SESSION_LEASES: OnceLock<Mutex<HashMap<PathBuf, Weak<ActiveLeaseSlot>>>> =
100 OnceLock::new();
101static SESSION_REPLAY_GENERATIONS: OnceLock<Mutex<HashMap<PathBuf, Weak<SessionReplayState>>>> =
102 OnceLock::new();
103
104impl fmt::Debug for Session {
105 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
106 formatter
107 .debug_struct("Session")
108 .field("id", &self.id)
109 .field("path", &self.path)
110 .finish()
111 }
112}
113
114impl Clone for Session {
115 fn clone(&self) -> Self {
116 Self {
117 id: self.id.clone(),
118 path: self.path.clone(),
119 metadata_cache: Arc::clone(&self.metadata_cache),
120 maintenance: Arc::clone(&self.maintenance),
121 replay_state: Arc::clone(&self.replay_state),
122 active_lease: Arc::clone(&self.active_lease),
123 standalone_writer: self.standalone_writer.clone(),
124 }
125 }
126}
127
128impl PartialEq for Session {
129 fn eq(&self, other: &Self) -> bool {
130 self.id == other.id && self.path == other.path
131 }
132}
133
134impl Eq for Session {}
135
136#[derive(Debug)]
137pub(crate) struct SessionWriterBusy;
138impl std::fmt::Display for SessionWriterBusy {
139 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
140 f.write_str("session is busy: another process owns the writer")
141 }
142}
143impl std::error::Error for SessionWriterBusy {}
144impl Session {
145 pub(in crate::sessions) fn new(id: String, path: PathBuf) -> Self {
146 let replay_state = replay_state_for_path(&path);
147 Self {
148 id,
149 path,
150 metadata_cache: Arc::new(Mutex::new(None)),
151 maintenance: Arc::new(Mutex::new(None)),
152 replay_state,
153 active_lease: Arc::new(Mutex::new(None)),
154 standalone_writer: None,
155 }
156 }
157
158 pub(crate) fn admit_standalone_writer(mut self) -> anyhow::Result<Self> {
159 if self.standalone_writer.is_none() {
160 let writer = self
161 .try_frontend_writer()?
162 .ok_or_else(|| anyhow::Error::new(SessionWriterBusy))?;
163 self.standalone_writer = Some(Arc::new(writer));
164 }
165 self.activate()
166 }
167
168 pub(crate) fn activate(self) -> anyhow::Result<Self> {
169 self.ensure_active_lease()?;
170 Ok(self)
171 }
172
173 pub(in crate::sessions) fn ensure_active_lease(&self) -> anyhow::Result<()> {
174 let slot = {
175 let mut local_slot = self
176 .active_lease
177 .lock()
178 .map_err(|_| anyhow::anyhow!("active session lease slot was poisoned"))?;
179 if let Some(slot) = local_slot.as_ref() {
180 Arc::clone(slot)
181 } else {
182 let slot = active_lease_slot(&self.path)?;
183 *local_slot = Some(Arc::clone(&slot));
184 slot
185 }
186 };
187 let mut lease = slot
188 .lock()
189 .map_err(|_| anyhow::anyhow!("active session lease was poisoned"))?;
190 if lease.is_none() {
191 let target = active_lease_target(&self.path);
192 *lease = Some(Arc::new(
193 CrossProcessFileLock::try_acquire(&target)?
194 .ok_or_else(|| anyhow::Error::new(SessionWriterBusy))?,
195 ));
196 }
197 Ok(())
198 }
199
200 pub(in crate::sessions) fn metadata_cache(&self) -> &Mutex<Option<SessionMetadataRecord>> {
201 &self.metadata_cache
202 }
203}
204
205fn replay_state_for_path(path: &Path) -> Arc<SessionReplayState> {
206 let key = normalize_active_lease_path(path);
207 let registry = SESSION_REPLAY_GENERATIONS.get_or_init(|| Mutex::new(HashMap::new()));
208 let Ok(mut states) = registry.lock() else {
209 return Arc::new(SessionReplayState {
211 generation: std::sync::atomic::AtomicU64::new(0),
212 });
213 };
214 if let Some(state) = states.get(&key).and_then(Weak::upgrade) {
215 return state;
216 }
217 states.retain(|_, state| state.strong_count() > 0);
218 let state = Arc::new(SessionReplayState {
219 generation: std::sync::atomic::AtomicU64::new(0),
220 });
221 states.insert(key, Arc::downgrade(&state));
222 state
223}
224
225fn active_lease_slot(path: &Path) -> anyhow::Result<Arc<ActiveLeaseSlot>> {
226 let key = normalize_active_lease_path(path);
227 let registry = ACTIVE_SESSION_LEASES.get_or_init(|| Mutex::new(HashMap::new()));
228 let mut slots = registry
229 .lock()
230 .map_err(|_| anyhow::anyhow!("active session lease registry was poisoned"))?;
231 if let Some(slot) = slots.get(&key).and_then(Weak::upgrade) {
232 return Ok(slot);
233 }
234 slots.retain(|_, slot| slot.strong_count() > 0);
235 let slot = Arc::new(Mutex::new(None));
236 slots.insert(key, Arc::downgrade(&slot));
237 Ok(slot)
238}
239
240fn normalize_active_lease_path(path: &Path) -> PathBuf {
241 if let Ok(canonical) = path.canonicalize() {
242 return canonical;
243 }
244 if let (Some(parent), Some(file_name)) = (path.parent(), path.file_name())
245 && let Ok(parent) = parent.canonicalize()
246 {
247 return parent.join(file_name);
248 }
249 path.to_path_buf()
250}
251
252pub(in crate::sessions) fn active_lease_target(path: &Path) -> PathBuf {
253 path.with_extension("active")
254}
255
256impl Session {
257 pub fn id(&self) -> &str {
258 &self.id
259 }
260
261 pub(crate) fn path(&self) -> &Path {
262 &self.path
263 }
264
265 pub(crate) fn replay_generation(&self) -> u64 {
266 self.replay_state
267 .generation
268 .load(std::sync::atomic::Ordering::Acquire)
269 }
270
271 pub(in crate::sessions) fn mark_replay_changed(&self) {
272 self.replay_state
273 .generation
274 .fetch_add(1, std::sync::atomic::Ordering::AcqRel);
275 }
276}