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 branch: BranchDisposition,
169 ) -> Result<()> {
170 self.tear_down_stopped_session(session_id, LifecycleKind::DestroyStopped, branch)
171 .await
172 }
173
174 pub(crate) async fn archive_aged_sessions(
180 self: &Arc<Self>,
181 older_than_days: u32,
182 ) -> Result<usize> {
183 self.wiki()
184 .sync_now(true)
185 .await
186 .context("sync SessionWiki before archiving stopped sessions")?;
187 let candidates = blocking(move || {
188 let controller = Controller::load()?;
189 Ok(crate::sessionwiki::sessions_ready_to_archive(
190 &controller.state.sessions,
191 &controller.state.subagents,
192 chrono::Utc::now(),
193 older_than_days,
194 ))
195 })
196 .await
197 .context("select the stopped sessions old enough to archive")?;
198 if candidates.is_empty() {
199 return Ok(0);
200 }
201 let indexed = blocking({
202 let candidates = candidates.clone();
203 move || crate::sessionwiki::indexed_with_messages(&candidates)
204 })
205 .await
206 .context("check the SessionWiki index before archiving")?;
207 let mut archived = 0;
208 for session_id in candidates {
209 if !indexed.contains(&session_id) {
210 tracing::warn!(
211 %session_id,
212 "SessionWiki holds no conversation for this stopped session; keeping it"
213 );
214 continue;
215 }
216 match self.archive_stopped_session(session_id.clone()).await {
217 Ok(()) => {
218 archived += 1;
219 tracing::info!(
220 %session_id,
221 older_than_days,
222 "archived a stopped session: SessionWiki keeps the conversation, and the repository keeps the branch unless another branch already contains it"
223 );
224 }
225 Err(error) => tracing::warn!(
226 %session_id,
227 error = %format!("{error:#}"),
228 "could not archive a stopped session"
229 ),
230 }
231 }
232 if archived > 0 {
233 self.wiki().request_sync(false);
235 }
236 Ok(archived)
237 }
238
239 async fn archive_stopped_session(self: &Arc<Self>, session_id: String) -> Result<()> {
245 self.tear_down_stopped_session(
246 session_id,
247 LifecycleKind::ArchiveStopped,
248 BranchDisposition::DeleteIfMerged,
249 )
250 .await
251 }
252
253 async fn tear_down_stopped_session(
254 self: &Arc<Self>,
255 session_id: String,
256 kind: LifecycleKind,
257 branch: BranchDisposition,
258 ) -> Result<()> {
259 let children = blocking({
260 let session_id = session_id.clone();
261 move || {
262 Ok(crate::database::list_subagents(&session_id)?
263 .into_iter()
264 .map(|child| child.child_session_id)
265 .collect::<Vec<_>>())
266 }
267 })
268 .await?;
269 for child_id in children {
270 Box::pin(self.force_destroy_session(child_id.clone(), BranchDisposition::Keep))
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(
360 self: &Arc<Self>,
361 session_id: String,
362 branch: BranchDisposition,
363 ) -> Result<()> {
364 let children = blocking({
365 let session_id = session_id.clone();
366 move || {
367 Ok(crate::database::list_subagents(&session_id)?
368 .into_iter()
369 .map(|child| child.child_session_id)
370 .collect::<Vec<_>>())
371 }
372 })
373 .await?;
374 for child_id in children {
375 Box::pin(self.force_destroy_session(child_id.clone(), BranchDisposition::Keep))
377 .await
378 .with_context(|| format!("destroy sub-agent {child_id} before its parent"))?;
379 }
380 self.preempt_active_lifecycle(&session_id).await?;
381 let exists = blocking({
382 let session_id = session_id.clone();
383 move || Ok(Controller::load()?.state.sessions.contains_key(&session_id))
384 })
385 .await?;
386 if !exists {
387 return Ok(());
388 }
389 self.run_lifecycle(
390 session_id,
391 LifecycleKind::ForceDestroy,
392 move |state, session_id, cancelled| async move {
393 let _recovery_reservation = tokio::task::spawn_blocking({
394 let observer = state.recovery_observer.clone();
395 let session_id = session_id.clone();
396 let cancelled = cancelled.clone();
397 move || reserve_recovery_or_cancel(&observer, &session_id, &cancelled)
398 })
399 .await
400 .context("reserve recovery for daemon force-destroy task")??;
401 blocking({
402 let session_id = session_id.clone();
403 move || {
404 let mut controller = Controller::load()?;
405 let executor = DaemonStageReportingExecutor::new(
406 CancellableProcessExecutor::new(cancelled),
407 state,
408 session_id.clone(),
409 );
410 controller.force_destroy_session(&session_id, &executor, branch)?;
411 crate::controller::move_session::release_move_queue_hold(&session_id);
412 Ok(DaemonLifecycleResult::Done)
413 }
414 })
415 .await
416 },
417 )
418 .await?;
419 Ok(())
420 }
421
422 pub async fn force_delete_workspace(self: &Arc<Self>, workspace_id: String) -> Result<()> {
431 ensure!(
432 !self.workspace_has_active_resume(&workspace_id),
433 "workspace has a session resume in progress"
434 );
435 let sessions = blocking({
436 let workspace_id = workspace_id.clone();
437 move || {
438 let controller = Controller::load()?;
439 Ok(active_sessions_for_force_destruction(
440 &controller,
441 &workspace_id,
442 ))
443 }
444 })
445 .await?;
446 for (index, session_id) in sessions.iter().enumerate() {
447 if let Err(error) = self
450 .force_destroy_session(session_id.clone(), BranchDisposition::Keep)
451 .await
452 {
453 let remaining = sessions.len() - index - 1;
454 bail!(
455 "force-destroying session {session_id} failed: {error:#}; \
456 {remaining} session(s) in the workspace remain"
457 );
458 }
459 }
460 blocking({
461 let workspace_id = workspace_id.clone();
462 move || crate::database::force_delete_workspace(&workspace_id)
463 })
464 .await?;
465 refresh_runtime_workspaces(self).await?;
466 Ok(())
467 }
468}