1use super::*;
2
3impl RuntimeState {
4 pub async fn resume_session(self: &Arc<Self>, request: ResumeSessionRequest) -> Result<()> {
12 let session_id = request.session_id.clone();
13 self.wait_for_deferred_cleanup(&session_id).await?;
14 let already_running = blocking({
17 let session_id = session_id.clone();
18 move || {
19 let controller = Controller::load()?;
20 Ok(controller
21 .state
22 .sessions
23 .get(&session_id)
24 .is_some_and(|session| session.state == SessionState::Running))
25 }
26 })
27 .await?;
28 if already_running {
29 return Ok(());
30 }
31 let profile_id = request.profile_id.clone();
32 let target_template_id = request.target_template_id.clone();
33 let workspace_id = request.workspace_id.clone();
34 let rebind_workspace_id = workspace_id.clone();
35 let operation_session_id = session_id.clone();
36 let result = self.start_or_join_lifecycle_for_workspace(
37 session_id,
38 LifecycleKind::Resume,
39 Some(workspace_id),
40 move |state, session_id, cancelled| async move {
41 let _recovery_reservation = tokio::task::spawn_blocking({
42 let observer = state.recovery_observer.clone();
43 let session_id = session_id.clone();
44 let cancelled = cancelled.clone();
45 move || reserve_recovery_or_cancel(&observer, &session_id, &cancelled)
46 })
47 .await
48 .context("reserve recovery for daemon resume task")??;
49 blocking({
50 let session_id = session_id.clone();
51 move || {
52 crate::database::reassign_resumable_session_workspace(
53 &session_id,
54 &rebind_workspace_id,
55 )
56 }
57 })
58 .await?;
59 let restore_request = request.clone();
60 let mut controller = tokio::task::spawn_blocking(move || {
61 session_move::load_controller_for_resume(&restore_request)
62 })
63 .await
64 .context("load controller for daemon resume task")??;
65 let executor = DaemonStageReportingExecutor::new(
66 CancellableProcessExecutor::new(cancelled),
67 state.clone(),
68 session_id.clone(),
69 );
70 let materialized = controller
71 .resume_session_controlled_with_repository_preflight(
72 &session_id,
73 &request.profile_id,
74 &request.target_template_id,
75 SessionResumeOptions {
76 additional_mounts: request.additional_mounts,
77 resource_allocation: request.resource_allocation,
78 discard_queue: request.discard_queue,
79 },
80 request.repository_preflight,
81 &executor,
82 )
83 .await?;
84 let _ = materialized;
88 Ok(DaemonLifecycleResult::Done)
89 },
90 )?;
91 self.set_lifecycle_resume_destination(
92 &operation_session_id,
93 profile_id,
94 target_template_id,
95 );
96 let channel = result.clone();
97 let result = Self::wait_lifecycle_result(result).await;
98 self.remove_completed_lifecycle(&channel);
99 match result? {
100 DaemonLifecycleResult::Done => {}
101 DaemonLifecycleResult::Move(_) => unreachable!("resume cannot return a move outcome"),
102 DaemonLifecycleResult::DeferredCleanup => {
103 unreachable!("session resume cannot schedule target cleanup")
104 }
105 }
106 blocking(move || {
107 if let Some(mut operation) =
108 crate::database::load_move_operation(&operation_session_id)?
109 && !operation.queue_admission_started
110 {
111 operation.phase = mj_core::state::MovePhase::Cancelled;
112 operation.queue_admission_finished = true;
113 operation.updated_at = chrono::Utc::now().to_rfc3339();
114 operation.error = Some("Recovered through an explicit Resume operation".into());
115 crate::database::save_move_operation(&operation)?;
116 }
117 Ok(())
118 })
119 .await?;
120 Ok(())
121 }
122
123 pub(super) async fn force_stop_session(self: &Arc<Self>, session_id: String) -> Result<()> {
124 let children = blocking({
125 let session_id = session_id.clone();
126 move || {
127 let controller = Controller::load()?;
128 Ok(active_child_session_ids(&controller.state, &session_id))
129 }
130 })
131 .await?;
132 for child_id in children {
133 Box::pin(self.force_stop_session(child_id.clone()))
134 .await
135 .with_context(|| format!("force-stop sub-agent {child_id} before its parent"))?;
136 }
137 let operation_session_id = session_id.clone();
138 let result = self
139 .run_lifecycle(
140 operation_session_id,
141 LifecycleKind::ForceStop,
142 |state, session_id, cancelled| async move {
143 blocking(move || {
144 let mut controller = Controller::load()?;
145 let executor = DaemonStageReportingExecutor::new(
146 CancellableProcessExecutor::new(cancelled),
147 state,
148 session_id.clone(),
149 );
150 let deferred = controller.force_stop(&session_id, &executor)?;
151 Ok(if deferred {
152 DaemonLifecycleResult::DeferredCleanup
153 } else {
154 DaemonLifecycleResult::Done
155 })
156 })
157 .await
158 },
159 )
160 .await?;
161 let _ = result; Ok(())
163 }
164
165 pub(super) async fn destroy_stopped_session(
166 self: &Arc<Self>,
167 session_id: String,
168 ) -> Result<()> {
169 self.tear_down_stopped_session(
170 session_id,
171 LifecycleKind::DestroyStopped,
172 BranchDisposition::Delete,
173 )
174 .await
175 }
176
177 pub(crate) async fn archive_aged_sessions(
183 self: &Arc<Self>,
184 older_than_days: u32,
185 ) -> Result<usize> {
186 self.wiki()
187 .sync_now(true)
188 .await
189 .context("sync SessionWiki before archiving stopped sessions")?;
190 let candidates = blocking(move || {
191 let controller = Controller::load()?;
192 Ok(crate::sessionwiki::sessions_ready_to_archive(
193 &controller.state.sessions,
194 &controller.state.subagents,
195 chrono::Utc::now(),
196 older_than_days,
197 ))
198 })
199 .await
200 .context("select the stopped sessions old enough to archive")?;
201 if candidates.is_empty() {
202 return Ok(0);
203 }
204 let indexed = blocking({
205 let candidates = candidates.clone();
206 move || crate::sessionwiki::indexed_with_messages(&candidates)
207 })
208 .await
209 .context("check the SessionWiki index before archiving")?;
210 let mut archived = 0;
211 for session_id in candidates {
212 if !indexed.contains(&session_id) {
213 tracing::warn!(
214 %session_id,
215 "SessionWiki holds no conversation for this stopped session; keeping it"
216 );
217 continue;
218 }
219 match self.archive_stopped_session(session_id.clone()).await {
220 Ok(()) => {
221 archived += 1;
222 tracing::info!(
223 %session_id,
224 older_than_days,
225 "archived a stopped session: SessionWiki keeps the conversation and the repository keeps the branch"
226 );
227 }
228 Err(error) => tracing::warn!(
229 %session_id,
230 error = %format!("{error:#}"),
231 "could not archive a stopped session"
232 ),
233 }
234 }
235 if archived > 0 {
236 self.wiki().request_sync(false);
238 }
239 Ok(archived)
240 }
241
242 async fn archive_stopped_session(self: &Arc<Self>, session_id: String) -> Result<()> {
247 self.tear_down_stopped_session(
248 session_id,
249 LifecycleKind::ArchiveStopped,
250 BranchDisposition::Keep,
251 )
252 .await
253 }
254
255 async fn tear_down_stopped_session(
256 self: &Arc<Self>,
257 session_id: String,
258 kind: LifecycleKind,
259 branch: BranchDisposition,
260 ) -> Result<()> {
261 let children = blocking({
262 let session_id = session_id.clone();
263 move || {
264 Ok(crate::database::list_subagents(&session_id)?
265 .into_iter()
266 .map(|child| child.child_session_id)
267 .collect::<Vec<_>>())
268 }
269 })
270 .await?;
271 for child_id in children {
272 Box::pin(self.force_destroy_session(child_id.clone()))
273 .await
274 .with_context(|| format!("destroy sub-agent {child_id} before its parent"))?;
275 }
276 self.wait_for_deferred_cleanup(&session_id).await?;
277 let exists = blocking({
278 let session_id = session_id.clone();
279 move || Ok(Controller::load()?.state.sessions.contains_key(&session_id))
280 })
281 .await?;
282 if !exists {
283 return Ok(());
284 }
285 self.run_lifecycle(
286 session_id,
287 kind,
288 move |state, session_id, cancelled| async move {
289 blocking(move || {
290 let mut controller = Controller::load()?;
291 let executor = DaemonStageReportingExecutor::new(
292 CancellableProcessExecutor::new(cancelled),
293 state,
294 session_id.clone(),
295 );
296 controller.destroy_session_controlled_with(&session_id, &executor, branch)?;
297 Ok(DaemonLifecycleResult::Done)
298 })
299 .await
300 },
301 )
302 .await?;
303 Ok(())
304 }
305
306 pub(super) async fn preempt_active_lifecycle(self: &Arc<Self>, session_id: &str) -> Result<()> {
317 let mut result = {
318 let lifecycle = self
319 .lifecycle
320 .lock()
321 .unwrap_or_else(PoisonError::into_inner);
322 let Some(active) = lifecycle.get(session_id) else {
323 return Ok(());
324 };
325 if !active.result.borrow().is_none() {
326 return Ok(());
327 }
328 active.request_cancel();
329 active.result.clone()
330 };
331 let finished = tokio::time::timeout(FORCE_DESTROY_PREEMPT_TIMEOUT, async {
332 loop {
333 if result.borrow().is_some() {
334 return Ok(());
335 }
336 if result.changed().await.is_err() {
337 return Err(());
338 }
339 }
340 })
341 .await;
342 match finished {
343 Ok(Ok(())) => Ok(()),
346 Ok(Err(())) => bail!(
347 "daemon lifecycle operation stopped without a result for session {session_id}"
348 ),
349 Err(_) => bail!(
350 "session {session_id} still has an operation that did not stop after cancellation; try again"
351 ),
352 }
353 }
354
355 pub async fn force_destroy_session(self: &Arc<Self>, session_id: String) -> Result<()> {
359 let children = blocking({
360 let session_id = session_id.clone();
361 move || {
362 Ok(crate::database::list_subagents(&session_id)?
363 .into_iter()
364 .map(|child| child.child_session_id)
365 .collect::<Vec<_>>())
366 }
367 })
368 .await?;
369 for child_id in children {
370 Box::pin(self.force_destroy_session(child_id.clone()))
371 .await
372 .with_context(|| format!("destroy sub-agent {child_id} before its parent"))?;
373 }
374 self.preempt_active_lifecycle(&session_id).await?;
375 let exists = blocking({
376 let session_id = session_id.clone();
377 move || Ok(Controller::load()?.state.sessions.contains_key(&session_id))
378 })
379 .await?;
380 if !exists {
381 return Ok(());
382 }
383 self.run_lifecycle(
384 session_id,
385 LifecycleKind::ForceDestroy,
386 |state, session_id, cancelled| async move {
387 let _recovery_reservation = tokio::task::spawn_blocking({
388 let observer = state.recovery_observer.clone();
389 let session_id = session_id.clone();
390 let cancelled = cancelled.clone();
391 move || reserve_recovery_or_cancel(&observer, &session_id, &cancelled)
392 })
393 .await
394 .context("reserve recovery for daemon force-destroy task")??;
395 blocking({
396 let session_id = session_id.clone();
397 move || {
398 let mut controller = Controller::load()?;
399 let executor = DaemonStageReportingExecutor::new(
400 CancellableProcessExecutor::new(cancelled),
401 state,
402 session_id.clone(),
403 );
404 controller.force_destroy_session(&session_id, &executor)?;
405 crate::controller::move_session::release_move_queue_hold(&session_id);
406 Ok(DaemonLifecycleResult::Done)
407 }
408 })
409 .await
410 },
411 )
412 .await?;
413 Ok(())
414 }
415
416 pub async fn force_delete_workspace(self: &Arc<Self>, workspace_id: String) -> Result<()> {
425 ensure!(
426 !self.workspace_has_active_resume(&workspace_id),
427 "workspace has a session resume in progress"
428 );
429 let sessions = blocking({
430 let workspace_id = workspace_id.clone();
431 move || {
432 let controller = Controller::load()?;
433 Ok(active_sessions_for_force_destruction(
434 &controller,
435 &workspace_id,
436 ))
437 }
438 })
439 .await?;
440 for (index, session_id) in sessions.iter().enumerate() {
441 if let Err(error) = self.force_destroy_session(session_id.clone()).await {
442 let remaining = sessions.len() - index - 1;
443 bail!(
444 "force-destroying session {session_id} failed: {error:#}; \
445 {remaining} session(s) in the workspace remain"
446 );
447 }
448 }
449 blocking({
450 let workspace_id = workspace_id.clone();
451 move || crate::database::force_delete_workspace(&workspace_id)
452 })
453 .await?;
454 refresh_runtime_workspaces(self).await?;
455 Ok(())
456 }
457}