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