1use super::*;
2
3impl LashRuntime {
4 pub fn set_runtime_lease_owner(&mut self, owner: crate::LeaseOwnerIdentity) {
11 self.runtime_lease_owner = owner;
12 self.last_committed_lease_continuity = None;
13 }
14
15 pub fn unregister_plugin_session(&self) -> Result<(), crate::PluginError> {
16 if let Some(session) = self.session.as_ref() {
17 session
18 .plugins()
19 .host()
20 .unregister_session(&self.state.session_id)?;
21 }
22 Ok(())
23 }
24
25 pub(super) async fn from_host_state(
26 policy: SessionPolicy,
27 host: RuntimeHost,
28 services: RuntimeServices,
29 mut state: RuntimeSessionState,
30 ) -> Result<Self, SessionError> {
31 if state.session_id.is_empty() {
32 state.session_id = uuid::Uuid::new_v4().to_string();
33 }
34 let state_policy_was_unconfigured = state.policy.recorded_provider_id().is_empty()
40 && state.policy.model.id.trim().is_empty();
41 if state_policy_was_unconfigured {
42 state.policy = policy.clone();
43 }
44 state.ensure_agent_frame_initialized();
45 let state_policy = state.policy.clone();
46 if let Some(frame) = state.current_agent_frame_mut()
47 && frame.assignment.policy.recorded_provider_id().is_empty()
48 && frame.assignment.policy.model.id.trim().is_empty()
49 {
50 frame.assignment.policy = state_policy;
51 }
52 state.policy = state.effective_policy().clone();
53 state.protocol_turn_options = state.effective_protocol_turn_options().clone();
54 normalize_session_graph(&mut state);
55 let policy = state.effective_policy().clone();
56 if policy.model.id.trim().is_empty() {
57 return Err(SessionError::Protocol(
58 "session policy missing model spec; hosts must supply explicit model metadata"
59 .to_string(),
60 ));
61 }
62 let mut host = host;
63 if let Some(store) = services.store.clone() {
70 let manifest: Arc<dyn crate::AttachmentManifest> =
71 Arc::new(crate::attachments::PersistenceManifestAdapter(store));
72 let previous_attachment_store = Arc::clone(&host.core.durability.attachment_store);
79 let inherited_pending_attachment_ids =
80 if previous_attachment_store.session_id() == state.session_id {
81 previous_attachment_store.pending_manifest_commit_ids()
82 } else {
83 Vec::new()
84 };
85 let backend = Arc::clone(previous_attachment_store.backend());
86 let scoped = Arc::new(crate::SessionAttachmentStore::new_with_pending(
87 backend,
88 manifest,
89 state.session_id.clone(),
90 inherited_pending_attachment_ids,
91 ));
92 host.core.durability.attachment_store = scoped;
93 }
94 let services = services
95 .with_attachment_store(Arc::clone(&host.core.durability.attachment_store))
96 .with_process_env_store(Arc::clone(&host.core.durability.process_env_store))
97 .with_clock(Arc::clone(&host.core.clock));
98 let mut session = Session::new(services.clone(), &state.session_id).await?;
99 if let Some(tool_state) = state.tool_state_snapshot.clone() {
100 let report = session
110 .plugins()
111 .tool_registry()
112 .restore_state(tool_state)
113 .map_err(|err| SessionError::Protocol(err.to_string()))?;
114 if !report.orphaned.is_empty() {
115 tracing::warn!(
116 session_id = %state.session_id,
117 orphaned = ?report.orphaned,
118 "session restored with orphaned tools: no registered source \
119 resolves them; they remain non-members until their source returns"
120 );
121 }
122 }
123 session.refresh_tool_catalog().await?;
124 if let Some(snapshot) = state.plugin_snapshot.clone() {
125 session
126 .plugins()
127 .restore(&snapshot)
128 .map_err(|err| SessionError::Protocol(err.to_string()))?;
129 }
130 let protocol_session = Arc::clone(session.plugins().protocol_session());
131 let session_id = state.session_id.clone();
132 protocol_session
133 .restore_session(
134 crate::plugin::ProtocolSessionContext::new(&mut session, &session_id),
135 &state,
136 )
137 .await?;
138 state.discard_runtime_snapshots();
139 session
140 .plugins()
141 .emit_runtime_event(crate::PluginLifecycleEvent::SessionRestored(
142 crate::SessionReadView::from_persisted_state(&state),
143 ))
144 .await;
145 let protocol_turn_options = state.protocol_turn_options.clone();
146 let runtime_scope_id = uuid::Uuid::new_v4().to_string();
147 let runtime_lease_owner = crate::LeaseOwnerIdentity::opaque(
148 runtime_scope_id.clone(),
149 uuid::Uuid::new_v4().to_string(),
150 );
151 Ok(Self {
152 session: Some(session),
153 policy,
154 host,
155 services,
156 state,
157 runtime_scope_id: Arc::<str>::from(runtime_scope_id),
158 runtime_lease_owner,
159 managed_sessions: Arc::new(Mutex::new(HashMap::new())),
160 managed_turns: Arc::new(Mutex::new(HashMap::new())),
161 protocol_turn_options,
162 shared_token_ledger: Arc::new(std::sync::Mutex::new(Vec::new())),
163 process_sync_needed: Arc::new(AtomicBool::new(false)),
164 turn_phase_probe: None,
165 last_committed_lease_continuity: None,
166 graph_loaded_from_store: false,
167 residency: Residency::default(),
168 })
169 }
170
171 pub async fn from_embedded_state(
173 policy: SessionPolicy,
174 host: EmbeddedRuntimeHost,
175 services: RuntimeServices,
176 state: RuntimeSessionState,
177 ) -> Result<Self, SessionError> {
178 Self::from_host_state(policy, host.into(), services, state).await
179 }
180
181 pub async fn from_background_state(
183 policy: SessionPolicy,
184 host: ProcessRuntimeHost,
185 services: RuntimeServices,
186 state: RuntimeSessionState,
187 ) -> Result<Self, SessionError> {
188 Self::from_host_state(policy, host.into(), services, state).await
189 }
190
191 pub async fn from_persistent_embedded_state(
193 policy: SessionPolicy,
194 host: EmbeddedRuntimeHost,
195 services: PersistentRuntimeServices,
196 state: RuntimeSessionState,
197 ) -> Result<Self, SessionError> {
198 Self::from_host_state(policy, host.into(), services.into_runtime_services(), state).await
199 }
200
201 pub async fn from_persistent_background_state(
203 policy: SessionPolicy,
204 host: ProcessRuntimeHost,
205 services: PersistentRuntimeServices,
206 state: RuntimeSessionState,
207 ) -> Result<Self, SessionError> {
208 Self::from_host_state(policy, host.into(), services.into_runtime_services(), state).await
209 }
210
211 pub(crate) async fn assemble_runtime(
222 policy: SessionPolicy,
223 embedded_host: EmbeddedRuntimeHost,
224 plugin_session: Arc<crate::PluginSession>,
225 store: Option<Arc<dyn crate::store::RuntimePersistence>>,
226 process_registry: Option<Arc<dyn ProcessRegistry>>,
227 mut state: RuntimeSessionState,
228 residency: Residency,
229 ) -> Result<Self, SessionError> {
230 if matches!(residency, Residency::ActivePathOnly) && store.is_none() {
233 return Err(SessionError::Protocol(
234 "Residency::ActivePathOnly requires a persistent store — \
235 without one, trimmed orphans are irrecoverable"
236 .to_string(),
237 ));
238 }
239 normalize_session_graph(&mut state);
242 apply_residency_on_load(&mut state, residency);
243 let mut runtime = match (store, process_registry) {
244 (Some(store), Some(registry)) => {
245 let host = ProcessRuntimeHost::new(embedded_host, registry);
246 let services = PersistentRuntimeServices::new(plugin_session, store);
247 Self::from_persistent_background_state(policy, host, services, state).await?
248 }
249 (Some(store), None) => {
250 let services = PersistentRuntimeServices::new(plugin_session, store);
251 Self::from_persistent_embedded_state(policy, embedded_host, services, state).await?
252 }
253 (None, Some(registry)) => {
254 let host = ProcessRuntimeHost::new(embedded_host, registry);
255 let services = RuntimeServices::new(plugin_session);
256 Self::from_background_state(policy, host, services, state).await?
257 }
258 (None, None) => {
259 let services = RuntimeServices::new(plugin_session);
260 Self::from_embedded_state(policy, embedded_host, services, state).await?
261 }
262 };
263 runtime.residency = residency;
264 Ok(runtime)
265 }
266
267 pub async fn from_environment(
283 env: &RuntimeEnvironment,
284 policy: SessionPolicy,
285 state: RuntimeSessionState,
286 store: Option<Arc<dyn crate::store::RuntimePersistence>>,
287 ) -> Result<Self, SessionError> {
288 let plugin_host = env.plugin_host.as_ref().ok_or_else(|| {
289 SessionError::Protocol(
290 "RuntimeEnvironment.plugin_host is required for from_environment".to_string(),
291 )
292 })?;
293 let plugin_session = plugin_host
294 .build_session(state.session_id.as_str(), state.plugin_snapshot.as_ref())
295 .map_err(|err| SessionError::Protocol(err.to_string()))?;
296 let mut embedded = EmbeddedRuntimeHost::new(env.core.clone());
297 if let Some(factory) = env.session_store_factory.as_ref() {
298 embedded = embedded.with_session_store_factory(Arc::clone(factory));
299 }
300 if let Some(store) = env.trigger_store.as_ref() {
301 embedded = embedded.with_trigger_store(Arc::clone(store));
302 }
303 let mut runtime = Self::assemble_runtime(
304 policy,
305 embedded,
306 plugin_session,
307 store,
308 env.process_registry.as_ref().cloned(),
309 state,
310 env.residency,
311 )
312 .await?;
313 runtime.host.process_work_driver = env.process_work_driver.clone();
316 runtime.host.queued_work_driver = env.queued_work_driver.clone();
317 Ok(runtime)
318 }
319
320 pub async fn park(mut self) -> Result<ParkedSession, SessionError> {
326 let store = self.services.store.clone().ok_or_else(|| {
327 SessionError::Protocol(
328 "park() requires a persistent runtime (store is not set)".to_string(),
329 )
330 })?;
331 let session_id = self.state.session_id.clone();
332 let policy = self.policy.clone();
333 if self.state.head_revision.is_none() || self.state.graph_replace_required {
341 let commit = crate::store::RuntimeCommit::persisted_state(&self.state, &[]);
342 let result = commit_runtime_state_with_fresh_session_execution_lease(
343 Arc::clone(&store),
344 commit,
345 &self.runtime_lease_owner,
346 self.host.core.control.lease_timings,
347 Arc::clone(&self.host.core.clock),
348 )
349 .await
350 .map_err(|err| {
351 SessionError::Protocol(format!("failed to persist runtime state: {err}"))
352 })?;
353 self.state.apply_persisted_commit_result(result);
354 }
355 Ok(ParkedSession {
360 session_id,
361 store,
362 policy,
363 })
364 }
365
366 pub async fn resume(
371 parked: ParkedSession,
372 env: &RuntimeEnvironment,
373 ) -> Result<Self, SessionError> {
374 let loaded = match env.residency {
380 Residency::KeepAll => {
381 crate::store::load_persisted_session_state(parked.store.as_ref()).await
382 }
383 Residency::ActivePathOnly => {
384 crate::store::load_persisted_session_state_active_path(parked.store.as_ref(), None)
385 .await
386 }
387 }
388 .map_err(|err| SessionError::Protocol(format!("failed to load runtime state: {err}")))?;
389 let state = loaded.unwrap_or_else(|| RuntimeSessionState {
390 session_id: parked.session_id.clone(),
391 policy: parked.policy.clone(),
392 ..RuntimeSessionState::default()
393 });
394 Self::from_environment(env, parked.policy, state, Some(parked.store)).await
395 }
396
397 pub async fn get_historic_node(
402 &self,
403 node_id: &str,
404 ) -> Result<Option<crate::SessionNodeRecord>, SessionError> {
405 if let Some(node) = self.state.session_graph.find_node(node_id) {
406 return Ok(Some(node.clone()));
407 }
408 let store = self.services.store.clone().ok_or_else(|| {
409 SessionError::Protocol("get_historic_node() requires a persistent runtime".to_string())
410 })?;
411 store
412 .load_node(node_id)
413 .await
414 .map_err(|err| SessionError::Protocol(format!("failed to load historic node: {err}")))
415 }
416
417 pub async fn orphaned_node_ids(&self) -> Result<Vec<String>, SessionError> {
433 let store = self.services.store.clone().ok_or_else(|| {
434 SessionError::Protocol("orphaned_node_ids() requires a persistent runtime".to_string())
435 })?;
436 let Some(read) = store
437 .load_session(crate::store::SessionReadScope::FullGraph)
438 .await
439 .map_err(|err| SessionError::Protocol(format!("failed to load full graph: {err}")))?
440 else {
441 return Ok(Vec::new());
442 };
443 let active: std::collections::HashSet<&str> = read
444 .graph
445 .active_path_nodes()
446 .iter()
447 .map(|node| node.node_id.as_str())
448 .collect();
449 Ok(read
450 .graph
451 .nodes
452 .iter()
453 .filter(|node| !active.contains(node.node_id.as_str()))
454 .map(|node| node.node_id.clone())
455 .collect())
456 }
457}