1use super::*;
2
3pub(in crate::runtime) struct RuntimePersistenceBindings {
4 runtime_store: Option<Arc<dyn crate::store::RuntimePersistence>>,
5 attachment_manifest_store: Option<Arc<dyn crate::store::RuntimePersistence>>,
6}
7
8impl RuntimePersistenceBindings {
9 pub(in crate::runtime) fn new(
10 runtime_store: Option<Arc<dyn crate::store::RuntimePersistence>>,
11 ) -> Self {
12 Self {
13 attachment_manifest_store: runtime_store.clone(),
14 runtime_store,
15 }
16 }
17
18 pub(in crate::runtime) fn with_attachment_manifest_store(
19 mut self,
20 store: Arc<dyn crate::store::RuntimePersistence>,
21 ) -> Self {
22 self.attachment_manifest_store = Some(store);
23 self
24 }
25}
26
27impl LashRuntime {
28 pub fn set_runtime_lease_owner(&mut self, owner: crate::LeaseOwnerIdentity) {
35 self.runtime_lease_owner = owner;
36 self.last_committed_lease_continuity = None;
37 }
38
39 pub fn unregister_plugin_session(&self) -> Result<(), crate::PluginError> {
40 if let Some(session) = self.session.as_ref() {
41 session
42 .plugins()
43 .host()
44 .unregister_session(&self.state.session_id)?;
45 }
46 Ok(())
47 }
48
49 pub(super) async fn from_host_state(
50 policy: SessionPolicy,
51 host: RuntimeHost,
52 services: RuntimeServices,
53 mut state: RuntimeSessionState,
54 ) -> Result<Self, SessionError> {
55 if state.session_id.is_empty() {
56 state.session_id = uuid::Uuid::new_v4().to_string();
57 }
58 let state_policy_was_unconfigured = state.policy.recorded_provider_id().is_empty()
64 && state.policy.model.id.trim().is_empty();
65 if state_policy_was_unconfigured {
66 state.policy = policy.clone();
67 }
68 state.ensure_agent_frame_initialized();
69 let state_policy = state.policy.clone();
70 if let Some(frame) = state.current_agent_frame_mut()
71 && frame.assignment.policy.recorded_provider_id().is_empty()
72 && frame.assignment.policy.model.id.trim().is_empty()
73 {
74 frame.assignment.policy = state_policy;
75 }
76 state.policy = state.effective_policy().clone();
77 state.protocol_turn_options = state.effective_protocol_turn_options().clone();
78 normalize_session_graph(&mut state);
79 let policy = state.effective_policy().clone();
80 if policy.model.id.trim().is_empty() {
81 return Err(SessionError::Protocol(
82 "session policy missing model spec; hosts must supply explicit model metadata"
83 .to_string(),
84 ));
85 }
86 let mut host = host;
87 if let Some(store) = services.attachment_manifest_store.clone() {
94 let manifest: Arc<dyn crate::AttachmentManifest> =
95 Arc::new(crate::attachments::PersistenceManifestAdapter(store));
96 let previous_attachment_store = Arc::clone(&host.core.durability.attachment_store);
100 let backend = Arc::clone(previous_attachment_store.backend());
101 let scoped = Arc::new(crate::SessionAttachmentStore::new_with_clock(
102 backend,
103 manifest,
104 state.session_id.clone(),
105 Arc::clone(&host.core.clock),
106 ));
107 host.core.durability.attachment_store = scoped;
108 }
109 let services = services
110 .with_attachment_store(Arc::clone(&host.core.durability.attachment_store))
111 .with_process_env_store(Arc::clone(&host.core.durability.process_env_store))
112 .with_clock(Arc::clone(&host.core.clock));
113 let mut session = Session::new(services.clone(), &state.session_id).await?;
114 if let Some(tool_state) = state.tool_state_snapshot.clone() {
115 let report = session
125 .plugins()
126 .tool_registry()
127 .restore_state(tool_state)
128 .map_err(|err| SessionError::Protocol(err.to_string()))?;
129 if !report.orphaned.is_empty() {
130 tracing::warn!(
131 session_id = %state.session_id,
132 orphaned = ?report.orphaned,
133 "session restored with orphaned tools: no registered source \
134 resolves them; they remain non-members until their source returns"
135 );
136 }
137 }
138 session.refresh_tool_catalog().await?;
139 if let Some(snapshot) = state.plugin_snapshot.clone() {
140 session
141 .plugins()
142 .restore(&snapshot)
143 .map_err(|err| SessionError::Protocol(err.to_string()))?;
144 }
145 let protocol_session = Arc::clone(session.plugins().protocol_session());
146 let session_id = state.session_id.clone();
147 protocol_session
148 .restore_session(
149 crate::plugin::ProtocolSessionContext::new(&mut session, &session_id),
150 &state,
151 )
152 .await?;
153 state.discard_runtime_snapshots();
154 session
155 .plugins()
156 .emit_runtime_event(crate::PluginLifecycleEvent::SessionRestored(
157 crate::SessionReadView::from_persisted_state(&state),
158 ))
159 .await;
160 let protocol_turn_options = state.protocol_turn_options.clone();
161 let runtime_scope_id = uuid::Uuid::new_v4().to_string();
162 let runtime_lease_owner = crate::LeaseOwnerIdentity::opaque(
163 runtime_scope_id.clone(),
164 uuid::Uuid::new_v4().to_string(),
165 );
166 Ok(Self {
167 session: Some(session),
168 policy,
169 host,
170 services,
171 state,
172 runtime_scope_id: Arc::<str>::from(runtime_scope_id),
173 runtime_lease_owner,
174 managed_sessions: Arc::new(Mutex::new(HashMap::new())),
175 managed_turns: Arc::new(Mutex::new(HashMap::new())),
176 protocol_turn_options,
177 shared_token_ledger: Arc::new(std::sync::Mutex::new(Vec::new())),
178 process_sync_needed: Arc::new(AtomicBool::new(false)),
179 turn_phase_probe: None,
180 last_committed_lease_continuity: None,
181 graph_loaded_from_store: false,
182 residency: Residency::default(),
183 })
184 }
185
186 pub async fn from_embedded_state(
188 policy: SessionPolicy,
189 host: EmbeddedRuntimeHost,
190 services: RuntimeServices,
191 state: RuntimeSessionState,
192 ) -> Result<Self, SessionError> {
193 Self::from_host_state(policy, host.into(), services, state).await
194 }
195
196 pub async fn from_background_state(
198 policy: SessionPolicy,
199 host: ProcessRuntimeHost,
200 services: RuntimeServices,
201 state: RuntimeSessionState,
202 ) -> Result<Self, SessionError> {
203 Self::from_host_state(policy, host.into(), services, state).await
204 }
205
206 pub async fn from_persistent_embedded_state(
208 policy: SessionPolicy,
209 host: EmbeddedRuntimeHost,
210 services: PersistentRuntimeServices,
211 state: RuntimeSessionState,
212 ) -> Result<Self, SessionError> {
213 Self::from_host_state(policy, host.into(), services.into_runtime_services(), state).await
214 }
215
216 pub async fn from_persistent_background_state(
218 policy: SessionPolicy,
219 host: ProcessRuntimeHost,
220 services: PersistentRuntimeServices,
221 state: RuntimeSessionState,
222 ) -> Result<Self, SessionError> {
223 Self::from_host_state(policy, host.into(), services.into_runtime_services(), state).await
224 }
225
226 pub(in crate::runtime) async fn assemble_runtime(
237 policy: SessionPolicy,
238 embedded_host: EmbeddedRuntimeHost,
239 plugin_session: Arc<crate::PluginSession>,
240 persistence: RuntimePersistenceBindings,
241 process_registry: Option<Arc<dyn ProcessRegistry>>,
242 mut state: RuntimeSessionState,
243 residency: Residency,
244 ) -> Result<Self, SessionError> {
245 let RuntimePersistenceBindings {
246 runtime_store: store,
247 attachment_manifest_store,
248 } = persistence;
249 if matches!(residency, Residency::ActivePathOnly) && store.is_none() {
252 return Err(SessionError::Protocol(
253 "Residency::ActivePathOnly requires a persistent store — \
254 without one, trimmed orphans are irrecoverable"
255 .to_string(),
256 ));
257 }
258 normalize_session_graph(&mut state);
261 apply_residency_on_load(&mut state, residency);
262 let mut runtime = match (store, process_registry) {
263 (Some(store), Some(registry)) => {
264 let host = ProcessRuntimeHost::new(embedded_host, registry);
265 let mut services = PersistentRuntimeServices::new(plugin_session, store);
266 if let Some(manifest_store) = attachment_manifest_store {
267 services = services.with_attachment_manifest_store(manifest_store);
268 }
269 Self::from_persistent_background_state(policy, host, services, state).await?
270 }
271 (Some(store), None) => {
272 let mut services = PersistentRuntimeServices::new(plugin_session, store);
273 if let Some(manifest_store) = attachment_manifest_store {
274 services = services.with_attachment_manifest_store(manifest_store);
275 }
276 Self::from_persistent_embedded_state(policy, embedded_host, services, state).await?
277 }
278 (None, Some(registry)) => {
279 let host = ProcessRuntimeHost::new(embedded_host, registry);
280 let services = RuntimeServices::new(plugin_session);
281 Self::from_background_state(policy, host, services, state).await?
282 }
283 (None, None) => {
284 let services = RuntimeServices::new(plugin_session);
285 Self::from_embedded_state(policy, embedded_host, services, state).await?
286 }
287 };
288 runtime.residency = residency;
289 Ok(runtime)
290 }
291
292 pub async fn from_environment(
308 env: &RuntimeEnvironment,
309 policy: SessionPolicy,
310 state: RuntimeSessionState,
311 store: Option<Arc<dyn crate::store::RuntimePersistence>>,
312 ) -> Result<Self, SessionError> {
313 let plugin_host = env.plugin_host.as_ref().ok_or_else(|| {
314 SessionError::Protocol(
315 "RuntimeEnvironment.plugin_host is required for from_environment".to_string(),
316 )
317 })?;
318 let plugin_session = plugin_host
319 .build_session(state.session_id.as_str(), state.plugin_snapshot.as_ref())
320 .map_err(|err| SessionError::Protocol(err.to_string()))?;
321 let mut embedded = EmbeddedRuntimeHost::new(env.core.clone());
322 if let Some(factory) = env.session_store_factory.as_ref() {
323 embedded = embedded.with_session_store_factory(Arc::clone(factory));
324 }
325 if let Some(store) = env.trigger_store.as_ref() {
326 embedded = embedded.with_trigger_store(Arc::clone(store));
327 }
328 let mut runtime = Self::assemble_runtime(
329 policy,
330 embedded,
331 plugin_session,
332 RuntimePersistenceBindings::new(store),
333 env.process_registry.as_ref().cloned(),
334 state,
335 env.residency,
336 )
337 .await?;
338 runtime.host.process_work_driver = env.process_work_driver.clone();
341 runtime.host.queued_work_driver = env.queued_work_driver.clone();
342 Ok(runtime)
343 }
344
345 pub async fn park(mut self) -> Result<ParkedSession, SessionError> {
351 let store = self.services.store.clone().ok_or_else(|| {
352 SessionError::Protocol(
353 "park() requires a persistent runtime (store is not set)".to_string(),
354 )
355 })?;
356 let session_id = self.state.session_id.clone();
357 let policy = self.policy.clone();
358 if self.state.head_revision.is_none() || self.state.graph_replace_required {
366 let commit = crate::store::RuntimeCommit::persisted_state(&self.state, &[]);
367 let result = commit_runtime_state_with_fresh_session_execution_lease(
368 Arc::clone(&store),
369 commit,
370 &self.runtime_lease_owner,
371 self.host.core.control.lease_timings,
372 Arc::clone(&self.host.core.clock),
373 )
374 .await
375 .map_err(|err| {
376 SessionError::Protocol(format!("failed to persist runtime state: {err}"))
377 })?;
378 self.state.apply_persisted_commit_result(result);
379 }
380 Ok(ParkedSession {
385 session_id,
386 store,
387 policy,
388 })
389 }
390
391 pub async fn resume(
396 parked: ParkedSession,
397 env: &RuntimeEnvironment,
398 ) -> Result<Self, SessionError> {
399 let loaded = match env.residency {
405 Residency::KeepAll => {
406 crate::store::load_persisted_session_state(parked.store.as_ref()).await
407 }
408 Residency::ActivePathOnly => {
409 crate::store::load_persisted_session_state_active_path(parked.store.as_ref(), None)
410 .await
411 }
412 }
413 .map_err(|err| SessionError::Protocol(format!("failed to load runtime state: {err}")))?;
414 let state = loaded.unwrap_or_else(|| RuntimeSessionState {
415 session_id: parked.session_id.clone(),
416 policy: parked.policy.clone(),
417 ..RuntimeSessionState::default()
418 });
419 Self::from_environment(env, parked.policy, state, Some(parked.store)).await
420 }
421
422 pub async fn get_historic_node(
427 &self,
428 node_id: &str,
429 ) -> Result<Option<crate::SessionNodeRecord>, SessionError> {
430 if let Some(node) = self.state.session_graph.find_node(node_id) {
431 return Ok(Some(node.clone()));
432 }
433 let store = self.services.store.clone().ok_or_else(|| {
434 SessionError::Protocol("get_historic_node() requires a persistent runtime".to_string())
435 })?;
436 store
437 .load_node(node_id)
438 .await
439 .map_err(|err| SessionError::Protocol(format!("failed to load historic node: {err}")))
440 }
441
442 pub async fn orphaned_node_ids(&self) -> Result<Vec<String>, SessionError> {
458 let store = self.services.store.clone().ok_or_else(|| {
459 SessionError::Protocol("orphaned_node_ids() requires a persistent runtime".to_string())
460 })?;
461 let Some(read) = store
462 .load_session(crate::store::SessionReadScope::FullGraph)
463 .await
464 .map_err(|err| SessionError::Protocol(format!("failed to load full graph: {err}")))?
465 else {
466 return Ok(Vec::new());
467 };
468 let active: std::collections::HashSet<&str> = read
469 .graph
470 .active_path_nodes()
471 .iter()
472 .map(|node| node.node_id.as_str())
473 .collect();
474 Ok(read
475 .graph
476 .nodes
477 .iter()
478 .filter(|node| !active.contains(node.node_id.as_str()))
479 .map(|node| node.node_id.clone())
480 .collect())
481 }
482}