mj_controller/daemon/
subagent_park.rs1use super::*;
7
8const PARK_TIMEOUT: Duration = Duration::from_secs(120);
10
11const UNPARK_TIMEOUT: Duration = Duration::from_secs(600);
13
14impl RuntimeState {
15 const WAIT_PROMPT_RETRY_DELAY: std::time::Duration = std::time::Duration::from_secs(1);
16
17 pub(crate) fn install_wait_prompt_backend(
18 &self,
19 backend: &Arc<crate::server_runtime::api::ApiBackend>,
20 ) {
21 assert!(
22 self.wait_prompt_backend
23 .set(Arc::downgrade(backend))
24 .is_ok(),
25 "delegation wait-prompt backend is installed once"
26 );
27 }
28
29 pub(crate) async fn ensure_parent_wait_prompt(&self, parent_session_id: &str) -> Result<()> {
30 if let Some(backend) = self
31 .wait_prompt_backend
32 .get()
33 .and_then(std::sync::Weak::upgrade)
34 {
35 match backend.ensure_parent_wait_prompt(parent_session_id).await {
36 Ok(()) => {
37 self.wait_prompt_retries
38 .lock()
39 .unwrap_or_else(PoisonError::into_inner)
40 .remove(parent_session_id);
41 }
42 Err(error) => {
43 self.schedule_wait_prompt_retry(parent_session_id);
44 return Err(error);
45 }
46 }
47 }
48 Ok(())
49 }
50
51 pub(crate) fn schedule_wait_prompt_retry(&self, parent_session_id: &str) {
52 self.wait_prompt_retries
53 .lock()
54 .unwrap_or_else(PoisonError::into_inner)
55 .insert(
56 parent_session_id.to_owned(),
57 std::time::Instant::now() + Self::WAIT_PROMPT_RETRY_DELAY,
58 );
59 }
60
61 pub(crate) fn take_due_wait_prompt_retries(
62 &self,
63 now: std::time::Instant,
64 limit: usize,
65 ) -> Vec<String> {
66 let mut retries = self
67 .wait_prompt_retries
68 .lock()
69 .unwrap_or_else(PoisonError::into_inner);
70 let due = retries
71 .iter()
72 .filter(|(_, due)| **due <= now)
73 .take(limit)
74 .map(|(parent, _)| parent.clone())
75 .collect::<Vec<_>>();
76 for parent in &due {
77 retries.remove(parent);
78 }
79 due
80 }
81
82 pub async fn park_subagent(
86 self: &Arc<Self>,
87 child_session_id: String,
88 ) -> Result<crate::controller::ParkOutcome> {
89 self.stop_idle_subagent(child_session_id, None).await
90 }
91
92 pub(super) async fn fail_subagent_start(self: &Arc<Self>, child_session_id: &str, cause: &str) {
99 let parent_session_id = self
100 .owner()
101 .controller()
102 .state
103 .subagents
104 .get(child_session_id)
105 .map(|relation| relation.parent_session_id.clone());
106 if !self
107 .owner()
108 .controller()
109 .state
110 .subagents
111 .contains_key(child_session_id)
112 {
113 return;
114 }
115 match self
116 .stop_idle_subagent(child_session_id.to_owned(), Some(cause.to_owned()))
117 .await
118 {
119 Ok(crate::controller::ParkOutcome::Parked) => {
120 tracing::info!(
121 session_id = child_session_id,
122 cause,
123 "sub-agent recorded as failed: its first prompt was refused"
124 );
125 }
126 Ok(outcome) => tracing::warn!(
127 session_id = child_session_id,
128 ?outcome,
129 "a sub-agent whose first prompt was refused was left running"
130 ),
131 Err(error) => tracing::warn!(
132 session_id = child_session_id,
133 error = format!("{error:#}"),
134 "could not record a sub-agent whose first prompt was refused as failed"
135 ),
136 }
137 if let Some(parent_session_id) = parent_session_id
138 && let Err(error) = self.ensure_parent_wait_prompt(&parent_session_id).await
139 {
140 tracing::warn!(
141 child_session_id,
142 parent_session_id,
143 error = format!("{error:#}"),
144 "could not reconcile the parent's sub-agent wait prompt after startup failure"
145 );
146 }
147 }
148
149 async fn stop_idle_subagent(
152 self: &Arc<Self>,
153 child_session_id: String,
154 failure: Option<String>,
155 ) -> Result<crate::controller::ParkOutcome> {
156 let result = self
157 .run_lifecycle(
158 child_session_id,
159 LifecycleKind::Park,
160 move |state, session_id, _cancelled| async move {
161 let never = AtomicBool::new(false);
165 let _recovery_reservation = blocking({
166 let observer = state.recovery_observer.clone();
167 let session_id = session_id.clone();
168 move || reserve_recovery_or_cancel(&observer, &session_id, &never)
169 })
170 .await?;
171 if state.close_is_requested(&session_id) {
172 return Ok(DaemonLifecycleResult::Park(
173 crate::controller::ParkOutcome::NotRunning,
174 ));
175 }
176 let controller = blocking(Controller::load).await?;
177 let executor = CancellableProcessExecutor::with_timeout(PARK_TIMEOUT);
178 let outcome = match &failure {
179 None => {
180 controller
181 .park_subagent_worker(
182 &session_id,
183 &executor,
184 &state.session_manager,
185 )
186 .await?
187 }
188 Some(cause) => {
189 controller
190 .fail_subagent_start_worker(
191 &session_id,
192 cause,
193 &executor,
194 &state.session_manager,
195 )
196 .await?
197 }
198 };
199 tracing::info!(%session_id, ?outcome, "sub-agent park finished");
200 Ok(DaemonLifecycleResult::Park(outcome))
201 },
202 )
203 .await?;
204 self.publish_revision();
205 match result {
206 DaemonLifecycleResult::Park(outcome) => Ok(outcome),
207 _ => unreachable!("a park returns its worker's reservation outcome"),
208 }
209 }
210
211 pub async fn unpark_subagent(self: &Arc<Self>, child_session_id: String) -> Result<()> {
214 self.wait_for_subagent_park(&child_session_id).await;
218 let parked = self
219 .session_state(&child_session_id)
220 .is_some_and(|state| state == SessionState::Parked);
221 if parked {
222 self.run_lifecycle(
223 child_session_id.clone(),
224 LifecycleKind::Unpark,
225 |state, session_id, cancelled| async move {
228 let executor = DaemonStageReportingExecutor::new(
229 CancellableProcessExecutor::new(cancelled.clone())
230 .with_deadline(UNPARK_TIMEOUT),
231 state.clone(),
232 session_id.clone(),
233 );
234 executor.begin_resumable_work()?;
235 let _recovery_reservation = tokio::task::spawn_blocking({
238 let observer = state.recovery_observer.clone();
239 let session_id = session_id.clone();
240 let cancelled = cancelled.clone();
241 move || reserve_recovery_or_cancel(&observer, &session_id, &cancelled)
242 })
243 .await
244 .context("reserve recovery for sub-agent wake")??;
245 let controller = blocking(Controller::load).await?;
246 controller
247 .unpark_subagent_worker(&session_id, &executor)
248 .await?;
249 Ok(DaemonLifecycleResult::Done)
250 },
251 )
252 .await?;
253 self.publish_revision();
254 }
255 Ok(())
256 }
257
258 pub fn subagent_park_running(&self, child_session_id: &str) -> bool {
260 self.owner()
261 .lifecycle
262 .get(child_session_id)
263 .is_some_and(|active| active.kind == LifecycleKind::Park && active.is_running())
264 }
265
266 async fn wait_for_subagent_park(self: &Arc<Self>, child_session_id: &str) {
268 let pending = self
269 .owner()
270 .lifecycle
271 .get(child_session_id)
272 .filter(|active| active.kind == LifecycleKind::Park)
273 .map(|active| active.result.clone());
274 if let Some(pending) = pending {
275 if let Err(error) = Self::wait_lifecycle_result(pending.clone()).await {
276 tracing::debug!(
277 session_id = %child_session_id,
278 error = format!("{error:#}"),
279 "the sub-agent park a restart waited for failed"
280 );
281 }
282 self.remove_completed_lifecycle(&pending);
283 }
284 }
285}