1use super::*;
2
3pub async fn run_server(
4 args: ServerArgs,
5 termination: tokio_util::sync::CancellationToken,
6 worker: SessionManagerChannels,
7 daemon_runtime: Arc<RuntimeState>,
8 mut workspace_updates: tokio::sync::watch::Receiver<Vec<WorkspaceRecord>>,
9) -> Result<()> {
10 let resolved = resolve_server_args(args, termination.clone()).await?;
11 let bind = resolved.bind;
12 let mut controller = Controller::load()?;
13 let profile_catalog = profile_catalog::ProfileCatalog::new(termination.child_token());
17 profile_catalog.sync(&controller.config);
18 let mut daemon_revisions = daemon_runtime.revisions();
19 daemon_revisions.borrow_and_update();
20 let mut phone_workspaces = workspace_updates.borrow_and_update().clone();
21 let mut quotas = std::collections::BTreeMap::new();
22 let subagent_quota_reports = Arc::new(std::sync::Mutex::new(quotas.clone()));
23 let (quota_profiles_tx, mut quota_updates_rx) = spawn_quota_refresher();
24 let mut quota_batch = QuotaRefreshBatch::default();
25 let mut published_quota_profiles = std::collections::BTreeMap::new();
26 republish_quota_profiles(
27 &controller,
28 &mut published_quota_profiles,
29 &mut quota_batch,
30 "a_profiles_tx,
31 );
32 let mut revision = daemon_runtime.allocate_revision();
33 let mut conversations = std::collections::BTreeMap::new();
34 let mut queued_prompts = projected_queued_prompts(&controller)?;
35 let mut active_user_shells = std::collections::BTreeMap::new();
36 let mut pending_elicitations = std::collections::BTreeMap::new();
37 let mut prompt_images = std::collections::BTreeSet::new();
38 let mut operational = std::collections::BTreeMap::new();
39 let mut materialized_activity = load_materialized_activity(&controller).await?;
40 let mut project_sources = PhoneProjectSources::default();
41 let (records, lifecycles) = daemon_runtime.session_projection();
42 controller.state.sessions = records;
43 let mut operations = lifecycles
44 .iter()
45 .map(|view| (view.session_id.clone(), viewer_operation(view)))
46 .collect::<std::collections::BTreeMap<_, _>>();
47 let mut move_recoveries = ViewerMoveRecoveries::new();
48 let (move_recovery_tx, mut move_recovery_rx) =
49 tokio::sync::mpsc::unbounded_channel::<Result<ViewerMoveRecoveries, String>>();
50 let mut move_recovery_load_in_flight = false;
51 let mut launch_failures = Vec::new();
52 let mut capacity_state: std::collections::BTreeMap<String, PhoneCapacity> =
56 std::collections::BTreeMap::new();
57 let (capacity_targets_tx, capacity_triggers_tx, mut capacity_updates_rx) =
58 crate::pollers::spawn_dashboard_capacity_poller();
59 let (snapshot_tx, snapshot_rx) = tokio::sync::watch::channel(viewer_snapshot(
60 &controller,
61 &phone_workspaces,
62 "as,
63 &PhoneSessionViews {
64 conversations: &conversations,
65 queued_prompts: &queued_prompts,
66 active_user_shells: &active_user_shells,
67 pending_elicitations: &pending_elicitations,
68 prompt_images: &prompt_images,
69 operational: &operational,
70 materialized_activity: &materialized_activity,
71 project_sources: &project_sources,
72 operations: &operations,
73 move_recoveries: &move_recoveries,
74 capacity: &viewer_capacity(&capacity_state),
75 launch_failures: &launch_failures,
76 reviews: &review_views(&daemon_runtime),
77 },
78 revision,
79 ));
80 let (conversation_tx, conversation_rx) = tokio::sync::watch::channel(conversations.clone());
81 let (action_tx, mut action_rx) = tokio::sync::mpsc::channel(32);
82 let (bundle_tx, mut bundle_rx) = tokio::sync::mpsc::channel(16);
83 let (receipt_tx, mut receipt_rx) = tokio::sync::mpsc::channel(32);
84 let (preflight_tx, mut preflight_rx) = tokio::sync::mpsc::channel(32);
85 let (move_preparation_tx, mut move_preparation_rx) = tokio::sync::mpsc::channel(32);
86 let (client_state_tx, mut client_state_rx) = tokio::sync::mpsc::channel(64);
87 let (dictation_tx, mut dictation_rx) =
88 tokio::sync::mpsc::channel::<crate::dictation::DictationRequest>(8);
89 let (background_task_stop_tx, mut background_task_stop_rx) =
90 tokio::sync::mpsc::channel::<BackgroundTaskStopRequest>(32);
91 let SessionManagerChannels {
92 targets: worker_targets_tx,
93 control: worker_commands_tx,
94 updates: mut worker_updates_rx,
95 shutdown: worker_shutdown,
96 } = worker;
97 worker_targets_tx.send_replace(dashboard_worker_targets(&controller));
98 publish_capacity_targets(&controller, &capacity_targets_tx, &mut capacity_state);
99 let mut credential_sync = CredentialSyncCoordinator::spawn();
100 let credential_sync_handle = credential_sync.handle();
101 credential_sync_handle.set_targets(credential_sync_targets(&controller));
102 let mut credential_sync_signals = CredentialSyncSignalTracker::default();
103 let mut credential_sync_notices = CredentialSyncNotices::default();
104 let options_session_ttl = crate::server::default_session_ttl();
106 let activity_snapshots = snapshot_rx.clone();
107 let mut options = ServerOptions::new(
108 bind,
109 snapshot_rx,
110 conversation_rx,
111 crate::server::ServerRequests {
112 action_tx,
113 bundle_tx,
114 receipt_tx,
115 preflight_tx,
116 move_preparation_tx,
117 client_state_tx,
118 dictation_tx,
119 },
120 )?;
121 options.set_background_task_stop_tx(background_task_stop_tx);
122 options.shutdown = termination.clone();
123 let cookie_key_path = crate::server::cookie_key_path();
127 options.set_cookie_key(crate::server::load_or_create_cookie_key(&cookie_key_path)?)?;
128 options.set_api_token(crate::server::load_or_create_api_token(
131 &crate::server::api_token_path(),
132 )?);
133 let api_runtime = daemon_runtime.clone();
134 let api_backend = Arc::new(
135 api::ApiBackend::new(
136 worker_commands_tx.client(),
137 Arc::new(move |session_id: &str| api_runtime.session_state(session_id)),
138 daemon_runtime.clone(),
139 )
140 .with_quota_reports(subagent_quota_reports.clone())
141 .with_profile_catalog(profile_catalog.clone()),
142 );
143 options.set_subagent_backend(api_backend.clone());
144 let renewal_cancellation = termination.child_token();
145 let mut renewal_task = None;
146 if let Some((cert, key)) = resolved.tls_files {
147 let rustls = axum_server::tls_rustls::RustlsConfig::from_pem_file(cert, key)
148 .await
149 .context("load web viewer TLS certificate")?;
150 options.set_tls_config(rustls.clone());
151 if let Some(tailscale) = resolved.tailscale {
152 renewal_task = Some(spawn_tailscale_cert_renewer(
153 tailscale,
154 rustls,
155 renewal_cancellation.clone(),
156 ));
157 }
158 } else if bind.ip().is_loopback() {
159 options.secure_cookie = false;
160 } else {
161 anyhow::bail!("non-loopback web viewer requires TLS");
162 }
163 let fallback_reason = resolved.fallback_reason;
164 let qr_login_url = if fallback_reason.is_none() && resolved.viewer_url.starts_with("https://") {
165 let encoded = url::form_urlencoded::byte_serialize(options.login_token().as_bytes())
166 .collect::<String>();
167 Some(format!(
168 "{}/auth/login?token={encoded}",
169 resolved.viewer_url.trim_end_matches('/')
170 ))
171 } else {
172 None
173 };
174 let ready = crate::server::WebViewerAccess::Ready {
175 viewer_url: resolved.viewer_url,
176 viewer_code: options.viewer_code().to_owned(),
177 qr_login_url,
178 fallback_reason,
179 };
180
181 let serve = crate::web_viewer::serve(options, ready, &daemon_runtime.web_viewer, |access| {
182 daemon_runtime.publish_web_access(access);
183 });
184 let conversation_projection_shutdown = termination.child_token();
185 let control = async {
186 let mut credential_tick = tokio::time::interval(Duration::from_millis(250));
187 let mut prune_tick = tokio::time::interval(prune_tick_interval());
191 daemon_runtime.wiki().request_sync(true);
196 let client_state_retention = options_session_ttl;
197 let (action_done_tx, mut action_done_rx) = tokio::sync::mpsc::unbounded_channel::<(
198 u64,
199 Option<String>,
200 std::result::Result<(), String>,
201 )>();
202 let (action_started_tx, mut action_started_rx) =
203 tokio::sync::mpsc::unbounded_channel::<PhoneActionStarted>();
204 let (receipt_done_tx, mut receipt_done_rx) =
205 tokio::sync::mpsc::unbounded_channel::<ReadReceiptPersisted>();
206 let (controller_reload_tx, mut controller_reload_rx) =
207 tokio::sync::mpsc::unbounded_channel::<ControllerReloaded>();
208 let (bundle_done_tx, mut bundle_done_rx) =
209 tokio::sync::mpsc::unbounded_channel::<BundleCreated>();
210 let (move_prepared_tx, mut move_prepared_rx) =
211 tokio::sync::mpsc::unbounded_channel::<MovePrepared>();
212 let mut dictation_jobs = tokio::task::JoinSet::new();
213 let mut archive_jobs = tokio::task::JoinSet::new();
214 let mut bundle_jobs = tokio::task::JoinSet::new();
215 let mut preflight_jobs = tokio::task::JoinSet::new();
216 let mut move_preparation_jobs = tokio::task::JoinSet::new();
217 let mut move_recovery_jobs = tokio::task::JoinSet::new();
218 let mut background_task_stop_jobs = tokio::task::JoinSet::new();
219 let mut background_task_stop_open = true;
220 let mut controller_reload_in_flight = false;
221 let mut controller_reload_requested = false;
222 let mut controller_reload_invalidated = false;
223 let mut pending_action_errors = std::collections::BTreeMap::<String, String>::new();
224 let mut active_actions = std::collections::BTreeSet::new();
225 let mut closing_actions = std::collections::BTreeMap::<String, u64>::new();
226 let mut next_action_id = 0_u64;
227 let mut action_cancellations = std::collections::BTreeMap::<u64, PhoneActionControl>::new();
228 let mut action_sessions = std::collections::BTreeMap::<u64, String>::new();
229 let mut action_replies = PendingActionReplies::default();
230 let mut launch_workspaces = std::collections::BTreeMap::new();
231 let mut subagent_jobs = tokio::task::JoinSet::new();
232 let mut subagent_completion_jobs = tokio::task::JoinSet::new();
233 let mut active_subagent_requests = std::collections::BTreeSet::new();
234 let (conversation_projection_tx, mut conversation_projection_rx) =
235 tokio::sync::mpsc::channel(CONVERSATION_PROJECTION_CHANNEL_CAPACITY);
236 let mut conversation_projections = ConversationProjectionDispatcher::new(
237 conversation_projection_tx,
238 conversation_projection_shutdown.clone(),
239 );
240 let mut quota_updates_open = true;
241 let mut failure: Option<anyhow::Error> = None;
245 request_move_recovery_reload(
246 &move_recovery_tx,
247 &mut move_recovery_load_in_flight,
248 &mut move_recovery_jobs,
249 );
250 macro_rules! publish_snapshot {
251 ($revision:expr) => {
252 let (records, lifecycles) = daemon_runtime.session_projection();
253 controller.state.sessions = records;
254 for (session_id, error) in &pending_action_errors {
255 if let Some(session) = controller.state.sessions.get_mut(session_id)
256 && session.last_error.is_none()
257 {
258 session.last_error = Some(error.clone());
259 }
260 }
261 operations = lifecycles.iter()
262 .map(|view| (view.session_id.clone(), viewer_operation(view)))
263 .collect();
264 if let Err(error) = snapshot_tx.send(viewer_snapshot(
265 &controller,
266 &phone_workspaces,
267 "as,
268 &PhoneSessionViews {
269 conversations: &conversations,
270 queued_prompts: &queued_prompts,
271 active_user_shells: &active_user_shells,
272 pending_elicitations: &pending_elicitations,
273 prompt_images: &prompt_images,
274 operational: &operational,
275 materialized_activity: &materialized_activity,
276 project_sources: &project_sources,
277 operations: &operations,
278 move_recoveries: &move_recoveries,
279 capacity: &viewer_capacity(&capacity_state),
280 launch_failures: &launch_failures,
281 reviews: &review_views(&daemon_runtime),
282 },
283 $revision,
284 )) {
285 tracing::debug!(revision = $revision, %error, "phone snapshot delivery failed; no viewer is subscribed");
286 }
287 };
288 }
289 loop {
290 project_sources.synchronize(&controller);
291 tokio::select! {
292 _ = termination.cancelled() => break,
293 request = background_task_stop_rx.recv(), if background_task_stop_open => {
294 let Some(request) = request else {
295 background_task_stop_open = false;
296 tracing::warn!("background-task stop request feed closed while the phone server was running");
297 continue;
298 };
299 let session_control = worker_commands_tx.clone();
300 background_task_stop_jobs.spawn(async move {
301 let result = match session_control.session(&request.session_id).await {
302 Ok(session) => session
303 .client()
304 .stop_background_task(request.background_task_id.clone())
305 .await
306 .map_err(|error| {
307 tracing::warn!(
308 session_id = %request.session_id,
309 background_task_id = %request.background_task_id,
310 %error,
311 "provider rejected background-task stop"
312 );
313 BackgroundTaskStopFailure::Provider
314 }),
315 Err(error) => {
316 tracing::warn!(
317 session_id = %request.session_id,
318 %error,
319 "could not resolve live session for background-task stop"
320 );
321 Err(BackgroundTaskStopFailure::SessionUnavailable)
322 }
323 };
324 if request.reply.send(result).is_err() {
325 tracing::debug!(
326 session_id = %request.session_id,
327 background_task_id = %request.background_task_id,
328 "background-task stop result dropped after viewer disconnected"
329 );
330 }
331 });
332 }
333 completed = background_task_stop_jobs.join_next(), if !background_task_stop_jobs.is_empty() => {
334 if let Some(Err(error)) = completed {
335 tracing::error!(%error, "background-task stop task failed unexpectedly");
336 }
337 }
338 move_reloaded = move_recovery_rx.recv() => {
339 let Some(result) = move_reloaded else {
340 failure = feed_stopped(
341 termination.is_cancelled(),
342 "the Move recovery projection stopped while the phone server was running",
343 );
344 break;
345 };
346 move_recovery_load_in_flight = false;
347 match result {
348 Ok(recoveries) => {
349 move_recoveries = recoveries;
350 revision = daemon_runtime.allocate_revision();
351 publish_snapshot!(revision);
352 }
353 Err(error) => tracing::warn!(%error, "could not refresh Move recovery projection"),
354 }
355 }
356 resolved = project_sources.jobs.join_next(), if !project_sources.jobs.is_empty() => {
357 match resolved {
358 Some(Ok(resolved)) => project_sources.complete(resolved),
359 Some(Err(error)) => {
360 failure = Some(anyhow::anyhow!("web project source task failed: {error}"));
361 break;
362 }
363 None => unreachable!("project source jobs were not empty"),
364 }
365 revision = daemon_runtime.allocate_revision();
366 publish_snapshot!(revision);
367 }
368 changed = daemon_revisions.changed() => {
369 if changed.is_err() {
370 failure = feed_stopped(
371 termination.is_cancelled(),
372 "the daemon stopped publishing runtime revisions to the phone server",
373 );
374 break;
375 }
376 daemon_revisions.borrow_and_update();
377 revision = daemon_runtime.allocate_revision();
378 publish_snapshot!(revision);
379 request_controller_reload(
380 &mut controller_reload_in_flight,
381 &mut controller_reload_requested,
382 &controller_reload_tx,
383 );
384 }
385 changed = workspace_updates.changed() => {
386 if changed.is_err() {
387 failure = feed_stopped(
388 termination.is_cancelled(),
389 "the daemon stopped publishing workspaces to the phone server",
390 );
391 break;
392 }
393 phone_workspaces = workspace_updates.borrow_and_update().clone();
394 revision = daemon_runtime.allocate_revision();
395 publish_snapshot!(revision);
396 }
397 update = capacity_updates_rx.recv() => {
398 let Some(update) = update else {
399 failure = feed_stopped(termination.is_cancelled(), "the capacity poller stopped while the phone server was running");
400 break;
401 };
402 if let Some(entry) = capacity_state.get_mut(&update.target_id) {
403 entry.refreshing = false;
404 entry.sampled_at_epoch_seconds = Some(update.sampled_at_epoch_seconds);
405 match update.result {
406 Ok(usage) => {
407 entry.on_demand = usage.is_none();
411 entry.usage = usage;
412 entry.failed = false;
413 }
414 Err(_) => entry.failed = true,
418 }
419 }
420 revision = daemon_runtime.allocate_revision();
421 publish_snapshot!(revision);
422 }
423 update = quota_updates_rx.recv(), if quota_updates_open => {
424 match update {
425 Some(QuotaUpdate::Report(outcome)) => {
426 if outcome.credentials_changed {
427 credential_sync_handle
428 .sync_profile_now(&outcome.report.profile_id, None);
429 }
430 quotas.insert(outcome.report.profile_id.clone(), outcome.report.clone());
431 subagent_quota_reports
432 .lock()
433 .expect("sub-agent quota reports lock poisoned")
434 .insert(outcome.report.profile_id.clone(), outcome.report);
435 revision = daemon_runtime.allocate_revision();
436 publish_snapshot!(revision);
437 }
438 Some(QuotaUpdate::Refreshing { .. } | QuotaUpdate::Finished { .. }) => {}
439 None => {
440 quota_updates_open = false;
441 tracing::warn!("quota refresher stopped while the phone server is running");
442 }
443 }
444 }
445 projected = conversation_projection_rx.recv() => {
446 let Some(projected) = projected else {
447 failure = feed_stopped(
448 termination.is_cancelled(),
449 "the browser transcript projection feed stopped",
450 );
451 break;
452 };
453 let session_id = projected.session_id.clone();
454 let session_active = controller
455 .state
456 .sessions
457 .get(&session_id)
458 .is_some_and(|session| session.state.is_active());
459 if !session_active {
460 conversation_projections.forget(&session_id);
465 }
466 if let Some((session_id, _key, transcript)) =
467 conversation_projections.finish(projected, session_active)
468 {
469 conversations.insert(session_id, transcript);
470 revision = daemon_runtime.allocate_revision();
471 conversation_tx.send_replace(conversations.clone());
472 publish_snapshot!(revision);
473 } else if !session_active && conversations.remove(&session_id).is_some() {
474 revision = daemon_runtime.allocate_revision();
479 conversation_tx.send_replace(conversations.clone());
480 publish_snapshot!(revision);
481 }
482 tokio::task::yield_now().await;
486 }
487 update = worker_updates_rx.recv() => {
488 let Some(update) = update else {
489 failure = feed_stopped(termination.is_cancelled(), "the session manager stopped; the phone server can no longer follow sessions");
490 break;
491 };
492 if let Some(snapshot) = update.view.snapshot.as_ref()
493 && let Some(session) = controller.state.sessions.get(&update.session_id)
494 && let Some(signal) = snapshot.latest_credential_sync_signal.clone()
495 {
496 credential_sync_signals.observe(
497 &update.session_id,
498 &session.last_profile,
499 signal,
500 );
501 }
502 schedule_due_credential_syncs(
503 &mut credential_sync_signals,
504 &credential_sync_handle,
505 Instant::now(),
506 );
507 apply_worker_record_update(&mut controller, &update);
508 if let Some(snapshot) = update.view.snapshot {
509 for request in snapshot.subagent_requests.iter().cloned() {
510 let identity = (update.session_id.clone(), request.request_id.clone());
511 if !active_subagent_requests.insert(identity.clone()) {
512 continue;
513 }
514 let backend = api_backend.clone();
515 let runtime = daemon_runtime.clone();
516 let parent_session_id = update.session_id.clone();
517 subagent_jobs.spawn(async move {
518 let result = backend
519 .execute_subagent_tool(parent_session_id.clone(), request)
520 .await;
521 let outcome = async {
522 let handle = runtime
527 .workspace_session_handle(&parent_session_id)
528 .await?;
529 let mut lease = handle.lease_connection().await?;
530 lease
531 .connection_mut()
532 .complete_subagent_request(result)
533 .await?;
534 lease.release();
535 anyhow::Ok(())
536 }
537 .await;
538 (identity, outcome)
539 });
540 }
541 if let Some(relation) = controller.state.subagents.get_mut(&update.session_id)
542 && matches!(snapshot.materialized.execution, mj_core::state::MaterializedExecutionState::Idle)
543 && let Some(outcome) = snapshot.materialized.last_turn_outcome.as_ref()
544 && relation.noticed_turn != Some(outcome.completed_ordinal)
545 {
546 let turn = outcome.completed_ordinal;
547 let child_id = relation.child_session_id.clone();
548 let parent_id = relation.parent_session_id.clone();
549 let task_name = relation.task_name.clone();
550 let outcome_name = format!("{:?}", outcome.outcome).to_lowercase();
551 relation.noticed_turn = Some(turn);
552 let backend = api_backend.clone();
553 subagent_completion_jobs.spawn(async move {
554 let result = async {
555 backend
556 .record_subagent_completion_notice(
557 parent_id,
558 &child_id,
559 &task_name,
560 turn,
561 &outcome_name,
562 )
563 .await?;
564 tokio::task::spawn_blocking({
565 let child_id = child_id.clone();
566 move || crate::database::mark_subagent_turn_noticed(&child_id, turn)
567 })
568 .await??;
569 anyhow::Ok(())
570 }
571 .await;
572 (child_id, turn, result)
573 });
574 }
575 if snapshot.operational.native_session_is_ready()
576 && operational.get(&update.session_id).is_none_or(|old: &mj_core::relay::RelayOperationalState| old.config_options != snapshot.operational.config_options)
577 && let Some(session) = controller.state.sessions.get(&update.session_id)
578 && matches!(session.target, Some(mj_core::state::TargetLocator::LocalBare { .. } | mj_core::state::TargetLocator::SshBare { .. } | mj_core::state::TargetLocator::AwsEc2 { .. }))
579 && let Some(build) = snapshot.worker_build.clone()
580 {
581 let profile = session.last_profile.clone();
582 let state = snapshot.operational.clone();
583 tokio::spawn(async move {
584 if let Err(error) = crate::controller::profile_config::observe(profile, build, state).await {
585 tracing::warn!(%error, "could not cache observed profile choices");
586 }
587 });
588 }
589 let materialized = snapshot.materialized;
590 let operational_state = snapshot.operational;
591 materialized_activity.insert(
592 update.session_id.clone(),
593 materialized.last_activity_at_ms,
594 );
595 let queued = queued_prompt_projection(&materialized);
596 let pending = materialized.pending_elicitations.clone();
597 let active_shells = operational_state.active_user_shells.clone();
598 let prompt_images_supported =
599 agent_accepts_prompt_images(&operational_state);
600 active_user_shells.insert(
601 update.session_id.clone(),
602 active_shells,
603 );
604 conversation_projections.enqueue(materialized);
605 queued_prompts.insert(
606 update.session_id.clone(),
607 queued,
608 );
609 pending_elicitations.insert(
610 update.session_id.clone(),
611 pending,
612 );
613 if prompt_images_supported {
614 prompt_images.insert(update.session_id.clone());
615 } else {
616 prompt_images.remove(&update.session_id);
617 }
618 operational.insert(
619 update.session_id.clone(),
620 operational_state,
621 );
622 revision = daemon_runtime.allocate_revision();
623 conversation_tx.send_replace(conversations.clone());
624 publish_snapshot!(revision);
625 }
626 tokio::task::yield_now().await;
631 }
632 completed = subagent_jobs.join_next(), if !subagent_jobs.is_empty() => {
633 match completed {
634 Some(Ok((identity, Ok(())))) => {
635 active_subagent_requests.remove(&identity);
636 }
637 Some(Ok((identity, Err(error)))) => {
638 active_subagent_requests.remove(&identity);
639 tracing::warn!(
640 parent_session_id = %identity.0,
641 request_id = %identity.1,
642 error = %format!("{error:#}"),
643 "sub-agent tool request failed"
644 );
645 }
646 Some(Err(error)) => tracing::warn!(%error, "sub-agent tool task panicked"),
647 None => {}
648 }
649 }
650 completed = subagent_completion_jobs.join_next(), if !subagent_completion_jobs.is_empty() => {
651 match completed {
652 Some(Ok((_, _, Ok(())))) => {}
653 Some(Ok((child_id, turn, Err(error)))) => {
654 if let Some(relation) = controller.state.subagents.get_mut(&child_id)
655 && relation.noticed_turn == Some(turn)
656 {
657 relation.noticed_turn = None;
658 }
659 tracing::warn!(%child_id, turn, error = %format!("{error:#}"), "could not record the sub-agent completion notice");
660 }
661 Some(Err(error)) => tracing::warn!(%error, "sub-agent completion task panicked"),
662 None => {}
663 }
664 }
665 _ = prune_tick.tick() => {
666 let archive_after_days = controller.config.sessionwiki.archive_after_days;
677 match archive_after_days {
678 Some(days) if archive_jobs.is_empty() => {
679 let runtime = daemon_runtime.clone();
680 archive_jobs.spawn(async move {
681 runtime.archive_aged_sessions(days).await
682 });
683 }
684 Some(_) => tracing::debug!(
685 "the previous SessionWiki archive pass is still running; skipping this tick"
686 ),
687 None => daemon_runtime.wiki().request_sync(true),
688 }
689 tokio::spawn(async move {
693 let pruned = tokio::task::spawn_blocking(move || {
694 crate::database::prune_phone_client_state(client_state_retention)
695 })
696 .await;
697 match pruned {
698 Ok(Ok(0)) => {}
699 Ok(Ok(rows)) => tracing::debug!(rows, "pruned expired phone viewer state"),
700 Ok(Err(error)) => tracing::warn!(%error, "could not prune phone viewer state"),
701 Err(error) => tracing::warn!(%error, "phone viewer state pruning task failed"),
702 }
703 });
704 }
705 _ = credential_tick.tick() => {
706 schedule_due_credential_syncs(
707 &mut credential_sync_signals,
708 &credential_sync_handle,
709 Instant::now(),
710 );
711 while let Some(result) = credential_sync.try_result() {
712 crate::pollers::log_credential_sync_actions(&result);
713 let harness = controller
714 .config
715 .profiles
716 .get(&result.profile_id)
717 .map(|profile| profile.kind);
718 if let Some(notice) = credential_sync_notices.notice(&result, harness) {
719 eprintln!("Mjolnir: {notice}");
720 }
721 }
722 }
723 request = dictation_rx.recv() => {
724 let Some(request) = request else {
725 failure = feed_stopped(termination.is_cancelled(), "the phone HTTP server stopped delivering dictation requests");
726 break;
727 };
728 let paths = controller.state.sessions.get(&request.session_id).map(|session| {
729 crate::dictation::auth_paths(&controller.config, &session.last_profile)
730 });
731 dictation_jobs.spawn(crate::dictation::execute(
732 request, paths, termination.clone(),
733 ));
734 }
735 job = dictation_jobs.join_next(), if !dictation_jobs.is_empty() => {
736 if let Some(Err(error)) = job {
737 tracing::warn!(%error, "web dictation task failed");
738 }
739 }
740 job = archive_jobs.join_next(), if !archive_jobs.is_empty() => {
741 match job {
742 Some(Ok(Ok(0))) | None => {}
743 Some(Ok(Ok(archived))) => tracing::debug!(
744 archived, "the SessionWiki archive pass finished"
745 ),
746 Some(Ok(Err(error))) => tracing::warn!(
747 error = %format!("{error:#}"),
748 "the SessionWiki archive pass failed"
749 ),
750 Some(Err(error)) => tracing::warn!(
751 %error, "the SessionWiki archive task panicked"
752 ),
753 }
754 }
755 stored = client_state_rx.recv() => {
756 let Some(stored) = stored else {
757 failure = feed_stopped(termination.is_cancelled(), "the phone HTTP server stopped delivering viewer state requests");
758 break;
759 };
760 let workspace_of = |session_id: &str| {
764 controller
765 .state
766 .sessions
767 .get(session_id)
768 .map(|session| session.workspace_id.clone())
769 };
770 let bundle_of = |session_id: &str| {
771 controller
772 .state
773 .sessions
774 .get(session_id)
775 .map(|session| session.bundle_id.clone())
776 };
777 match stored {
778 crate::server::ClientStateRequest::Read { client_id, session_id, reply } => {
779 let workspace = workspace_of(&session_id);
780 tokio::spawn(async move {
781 let answer = tokio::task::spawn_blocking(move || {
782 let workspace = workspace.context("unknown session")?;
783 let state = crate::database::client_session_state(
784 &client_id, &workspace, &session_id,
785 )?;
786 anyhow::Ok(crate::server::ViewerClientState {
787 draft: state.draft,
788 through_event_ordinal: state.through_event_ordinal,
789 })
790 })
791 .await;
792 reply.send(flatten_stored(answer)).ok();
793 });
794 }
795 crate::server::ClientStateRequest::SaveDraft { client_id, session_id, draft, reply } => {
796 let workspace = workspace_of(&session_id);
797 tokio::spawn(async move {
798 let answer = tokio::task::spawn_blocking(move || {
799 let workspace = workspace.context("unknown session")?;
800 crate::database::persist_client_draft(
801 &client_id, &workspace, &session_id, &draft,
802 )
803 })
804 .await;
805 reply.send(flatten_stored(answer)).ok();
806 });
807 }
808 crate::server::ClientStateRequest::MarkWorkspaceRead { client_id, workspace_id, reply } => {
809 let sessions = controller
810 .state
811 .sessions
812 .values()
813 .filter(|session| session.workspace_id == workspace_id)
814 .map(|session| (session.id.clone(), session.viewed_through_event_ordinal))
815 .collect::<Vec<_>>();
816 tokio::spawn(async move {
817 let answer = tokio::task::spawn_blocking(move || {
818 for (session_id, through) in sessions {
819 crate::database::persist_read_receipt(
823 &client_id, &workspace_id, &session_id, through,
824 )
825 .ok();
826 }
827 anyhow::Ok(())
828 })
829 .await;
830 reply.send(flatten_stored(answer)).ok();
831 });
832 }
833 crate::server::ClientStateRequest::History { session_id, query, scope, reply } => {
834 let bundle = bundle_of(&session_id);
835 tokio::spawn(async move {
836 let answer = tokio::task::spawn_blocking(move || {
837 let bundle = bundle.context("unknown session")?;
838 let scope = match scope.as_str() {
839 "session" => crate::database::HistoryScope::Session,
840 "all" => crate::database::HistoryScope::All,
841 _ => crate::database::HistoryScope::Project,
842 };
843 let found = crate::database::search_prompts_bounded(
844 &session_id,
845 &bundle,
846 scope,
847 &query,
848 crate::server::MAX_HISTORY_MATCHES,
849 )?;
850 anyhow::Ok(crate::server::ViewerPromptHistory {
851 entries: found
852 .entries
853 .into_iter()
854 .map(|entry| entry.text)
855 .collect(),
856 truncated: found.truncated,
857 })
858 })
859 .await;
860 reply.send(flatten_stored(answer)).ok();
861 });
862 }
863 }
864 }
865 bundle = bundle_rx.recv(), if bundle_jobs.len() < MAX_CONCURRENT_BUNDLE_CREATIONS => {
866 let Some(crate::server::BundleRequest { source, reply }) = bundle else {
867 failure = feed_stopped(termination.is_cancelled(), "the phone HTTP server stopped delivering bundle requests");
868 break;
869 };
870 let done = bundle_done_tx.clone();
875 let daemon_runtime = daemon_runtime.clone();
876 bundle_jobs.spawn(async move {
877 let result = daemon_runtime
878 .create_quick_bundle(source)
879 .await;
880 if let Err(error) = done.send(BundleCreated { result, reply }) {
881 tracing::debug!(%error, "bundle creation finished after the server stopped");
882 }
883 });
884 }
885 bundle_done = bundle_done_rx.recv() => {
886 let Some(BundleCreated { result, reply }) = bundle_done else {
887 failure = feed_stopped(termination.is_cancelled(), "the bundle creation pipeline stopped while the phone server was running");
888 break;
889 };
890 match result {
891 Ok(created) => {
892 let bundle_id = created.bundle_id;
893 let Some(bundle) = created.config.bundles.get(&bundle_id) else {
899 tracing::error!(%bundle_id, "bundle creation returned a config without its bundle");
900 if reply.send(Err(crate::server::BundleFailure::Controller)).is_err() {
901 tracing::debug!("bundle creation failure reply dropped after client disconnect");
902 }
903 continue;
904 };
905 controller
906 .config
907 .bundles
908 .insert(bundle_id.clone(), bundle.clone());
909 controller_reload_invalidated |= controller_reload_in_flight;
913 revision = daemon_runtime.allocate_revision();
914 publish_snapshot!(revision);
915 request_daemon_controller_reload(
916 daemon_runtime.clone(),
917 "new bundle publication",
918 );
919 if reply.send(Ok(bundle_id)).is_err() {
920 tracing::debug!("bundle creation reply dropped after client disconnect");
921 }
922 }
923 Err(error) => {
924 let failure = match error {
925 crate::controller::QuickBundleFailure::InvalidSource(
926 detail,
927 ) => {
928 tracing::debug!(error = %detail, "phone bundle source was invalid");
929 crate::server::BundleFailure::InvalidSource
930 }
931 crate::controller::QuickBundleFailure::Persistence(
932 detail,
933 ) => {
934 tracing::warn!(error = %detail, "phone bundle creation failed");
935 crate::server::BundleFailure::Controller
936 }
937 };
938 if reply.send(Err(failure)).is_err() {
939 tracing::debug!("bundle creation failure reply dropped after client disconnect");
940 }
941 }
942 }
943 }
944 bundle_job = bundle_jobs.join_next(), if !bundle_jobs.is_empty() => {
945 if let Some(Err(error)) = bundle_job {
946 tracing::warn!(%error, "bundle creation task panicked");
947 }
948 }
949 preflight = preflight_rx.recv(), if preflight_jobs.len() < MAX_CONCURRENT_PREFLIGHTS => {
950 let Some(preflight) = preflight else {
951 failure = feed_stopped(termination.is_cancelled(), "the phone HTTP server stopped delivering preflight requests");
952 break;
953 };
954 let crate::server::NewPreflightRequest {
958 bundle_id,
959 target_id,
960 project_directory,
961 mut reply,
962 remote_repairs,
963 } = match preflight {
964 crate::server::PreflightRequest::New(request) => request,
965 crate::server::PreflightRequest::Resume(request) => {
966 spawn_resume_preflight(
967 &mut preflight_jobs,
968 &controller,
969 request,
970 &termination,
971 );
972 continue;
973 }
974 };
975 let config = controller.config.clone();
980 let project_validation = project_directory.is_some();
981 let task_termination = termination.clone();
982 preflight_jobs.spawn(async move {
983 let cancelled = Arc::new(AtomicBool::new(false));
984 let cancellation_guard = ProcessCancellationGuard(cancelled.clone());
985 let mut blocking = tokio::task::spawn_blocking(move || {
986 run_new_preflight_with_cancellation(
987 config,
988 bundle_id,
989 target_id,
990 project_directory,
991 cancelled,
992 remote_repairs,
993 )
994 });
995 let answer = tokio::select! {
996 biased;
997 _ = task_termination.cancelled() => None,
998 _ = reply.closed() => None,
999 answer = &mut blocking => Some(answer),
1000 };
1001 let Some(answer) = answer else {
1002 drop(cancellation_guard);
1003 match blocking.await {
1004 Err(error) => tracing::warn!(%error, "cancelled phone preflight task failed"),
1005 Ok(Err(error)) => tracing::debug!(%error, "phone preflight cancelled"),
1006 Ok(Ok(_)) => {}
1007 }
1008 return;
1009 };
1010 let answer = match answer {
1011 Ok(Ok(answer)) => Ok(answer),
1012 Ok(Err(error)) => {
1013 tracing::debug!(
1014 error = %error,
1015 project_validation,
1016 "phone preflight check failed"
1017 );
1018 Err(if project_validation {
1019 PreflightFailure::Validation
1020 } else {
1021 PreflightFailure::InvalidRepository(format!("{error:#}"))
1022 })
1023 }
1024 Err(error) => {
1025 tracing::warn!(%error, "phone preflight task failed");
1026 Err(PreflightFailure::Controller(format!(
1027 "preflight task failed: {error}"
1028 )))
1029 }
1030 };
1031 if reply.send(answer).is_err() {
1032 tracing::debug!("phone preflight reply dropped after client disconnect");
1033 }
1034 });
1035 }
1036 preflight_job = preflight_jobs.join_next(), if !preflight_jobs.is_empty() => {
1037 if let Some(Err(error)) = preflight_job {
1038 tracing::warn!(%error, "phone preflight task panicked");
1039 }
1040 }
1041 preparation = move_preparation_rx.recv() => {
1042 let Some(MovePreparationRequest { selection, reply }) = preparation else {
1043 failure = feed_stopped(termination.is_cancelled(), "the phone HTTP server stopped delivering move preparation requests");
1044 break;
1045 };
1046 let done = move_prepared_tx.clone();
1051 let daemon_runtime = daemon_runtime.clone();
1052 move_preparation_jobs.spawn(async move {
1053 let result = daemon_runtime
1054 .prepare_move_session(selection)
1055 .await
1056 .map_err(|error| format!("{error:#}"));
1057 if let Err(error) = done.send(MovePrepared { result, reply }) {
1058 tracing::debug!(%error, "move preparation finished after the server stopped");
1059 }
1060 });
1061 }
1062 prepared = move_prepared_rx.recv() => {
1063 let Some(MovePrepared { result, reply }) = prepared else {
1064 failure = feed_stopped(termination.is_cancelled(), "the move preparation pipeline stopped while the phone server was running");
1065 break;
1066 };
1067 if reply.send(result).is_err() {
1068 tracing::debug!("move preparation reply dropped after client disconnect");
1069 }
1070 }
1071 move_preparation_job = move_preparation_jobs.join_next(), if !move_preparation_jobs.is_empty() => {
1072 if let Some(Err(error)) = move_preparation_job {
1073 tracing::warn!(%error, "move preparation task failed");
1074 }
1075 }
1076 move_recovery_job = move_recovery_jobs.join_next(), if !move_recovery_jobs.is_empty() => {
1077 if let Some(Err(error)) = move_recovery_job {
1078 move_recovery_load_in_flight = false;
1079 tracing::warn!(%error, "Move recovery projection task failed");
1080 }
1081 }
1082 receipt = receipt_rx.recv() => {
1083 let Some(ReadReceiptRequest { client_id, session_id, through, reply }) = receipt else {
1084 failure = feed_stopped(termination.is_cancelled(), "the phone HTTP server stopped delivering read receipts");
1085 break;
1086 };
1087 match controller.state.sessions.get(&session_id) {
1088 None => {
1089 if reply.send(Err("unknown session".into())).is_err() {
1090 tracing::debug!(%session_id, "unknown-session read receipt reply dropped after client disconnect");
1091 }
1092 }
1093 Some(session) => {
1094 let workspace_id = session.workspace_id.clone();
1095 let done = receipt_done_tx.clone();
1096 let persisted_session_id = session_id.clone();
1097 tokio::spawn(async move {
1098 let joined = tokio::task::spawn_blocking(move || {
1099 crate::database::persist_read_receipt(
1100 &client_id,
1101 &workspace_id,
1102 &persisted_session_id,
1103 through,
1104 )
1105 })
1106 .await;
1107 let result = match joined {
1108 Ok(result) => result.map_err(|error| format!("{error:#}")),
1109 Err(error) => Err(format!("phone read receipt task failed: {error}")),
1110 };
1111 if let Err(error) = done.send(ReadReceiptPersisted { session_id, result, reply }) {
1112 tracing::debug!(%error, "phone read receipt finished after the server stopped");
1113 }
1114 });
1115 }
1116 }
1117 }
1118 persisted = receipt_done_rx.recv() => {
1119 let Some(ReadReceiptPersisted { session_id, result, reply }) = persisted else { continue };
1120 match result {
1121 Ok(receipt) => {
1122 let _ = receipt;
1123 if reply.send(Ok(())).is_err() {
1124 tracing::debug!(%session_id, "phone read receipt reply dropped after client disconnect");
1125 }
1126 }
1127 Err(error) => {
1128 tracing::warn!(%session_id, "could not persist a phone read receipt: {error}");
1129 if reply.send(Err(error)).is_err() {
1130 tracing::debug!(%session_id, "failed phone read receipt reply dropped after client disconnect");
1131 }
1132 }
1133 }
1134 }
1135 action = action_rx.recv() => {
1136 let Some(request) = action else {
1137 failure = feed_stopped(termination.is_cancelled(), "the phone HTTP server stopped delivering actions");
1138 break;
1139 };
1140 match &request.action {
1144 ControllerAction::RefreshCapacity { target_id } => {
1145 let known = capacity_state.contains_key(target_id);
1146 let accepted = known && match capacity_triggers_tx.try_send(()) {
1149 Ok(()) | Err(tokio::sync::mpsc::error::TrySendError::Full(())) => true,
1150 Err(tokio::sync::mpsc::error::TrySendError::Closed(())) => {
1151 tracing::warn!("phone capacity refresh rejected: poller stopped");
1152 false
1153 }
1154 };
1155 if known {
1156 if let Some(entry) = capacity_state.get_mut(target_id) {
1157 entry.refreshing = accepted;
1158 if !accepted {
1159 entry.failed = true;
1160 }
1161 }
1162 revision = daemon_runtime.allocate_revision();
1163 publish_snapshot!(revision);
1164 }
1165 let outcome = if accepted {
1166 ActionOutcome::accepted()
1167 } else {
1168 ActionOutcome::Failed
1169 };
1170 if request.reply.send(outcome).is_err() {
1171 tracing::debug!(%target_id, "phone capacity refresh reply dropped after client disconnect");
1172 }
1173 tokio::task::yield_now().await;
1174 continue;
1175 }
1176 ControllerAction::RefreshQuota { profile_id } => {
1177 let known = controller.config.enabled_profile(profile_id).is_some();
1178 if known {
1179 quota_batch.generation = quota_batch.generation.saturating_add(1);
1183 quota_batch.profiles = quota_refresh_profiles(&controller);
1184 quota_profiles_tx.send_replace(quota_batch.clone());
1185 }
1186 let outcome = if known {
1187 ActionOutcome::accepted()
1188 } else {
1189 ActionOutcome::Failed
1190 };
1191 if request.reply.send(outcome).is_err() {
1192 tracing::debug!(%profile_id, "phone quota refresh reply dropped after client disconnect");
1193 }
1194 tokio::task::yield_now().await;
1195 continue;
1196 }
1197 _ => {}
1198 }
1199 if let ControllerAction::Cancel { session_id } = &request.action {
1200 let outcome = if request_phone_action_cancellation(
1201 session_id,
1202 &action_sessions,
1203 &action_cancellations,
1204 ) {
1205 daemon_runtime.cancel_lifecycle_if_active(session_id);
1206 ActionOutcome::accepted()
1207 } else {
1208 ActionOutcome::NotCancellable
1209 };
1210 if request.reply.send(outcome).is_err() {
1211 tracing::debug!(%session_id, "phone cancellation reply dropped after client disconnect");
1212 }
1213 tokio::task::yield_now().await;
1214 continue;
1215 }
1216 if let ControllerAction::Close { session_id } = &request.action {
1217 if closing_actions.contains_key(session_id) {
1218 if request.reply.send(ActionOutcome::accepted()).is_err() { tracing::debug!(%session_id, "repeated close reply dropped"); }
1219 continue;
1220 }
1221 request_phone_action_cancellation(session_id, &action_sessions, &action_cancellations);
1222 daemon_runtime.request_close(session_id);
1223 }
1224 if let ControllerAction::ForceClose { session_id } = &request.action {
1228 request_phone_action_cancellation(session_id, &action_sessions, &action_cancellations);
1229 daemon_runtime.request_close(session_id);
1230 }
1231 let session_id = match admit_phone_action(
1232 &request.action,
1233 action_cancellations.len(),
1234 &mut active_actions,
1235 ) {
1236 Ok(session_id) => session_id,
1237 Err(refusal) => {
1238 if request.reply.send(refusal).is_err() {
1239 tracing::debug!("phone action refusal reply dropped after client disconnect");
1240 }
1241 tokio::task::yield_now().await;
1242 continue;
1243 }
1244 };
1245 let ControllerRequest { action, reply } = request;
1246 let done = action_done_tx.clone();
1247 let session_control = worker_commands_tx.clone();
1248 let daemon_runtime = daemon_runtime.clone();
1249 let started = action_started_tx.clone();
1250 next_action_id = next_action_id.wrapping_add(1).max(1);
1251 let action_id = next_action_id;
1252 if let ControllerAction::Close { session_id } | ControllerAction::ForceClose { session_id } = &action { closing_actions.insert(session_id.clone(), action_id); }
1253 if let ControllerAction::New { workspace_id, .. } = &action {
1254 let workspace_id = if workspace_id.is_empty() && phone_workspaces.len() == 1 {
1255 phone_workspaces[0].id.clone()
1256 } else {
1257 workspace_id.clone()
1258 };
1259 launch_workspaces.insert(action_id, workspace_id);
1260 }
1261 let control = PhoneActionControl::for_action(&action);
1262 action_cancellations.insert(action_id, control.clone());
1263 if let Some(session_id) = &session_id {
1264 action_sessions.insert(action_id, session_id.clone());
1265 }
1266 action_replies.accept(action_id, &action, reply);
1267 let runtime = tokio::runtime::Handle::current();
1268 tokio::spawn(async move {
1269 let joined = tokio::task::spawn_blocking(move || {
1270 let result = (|| -> Result<()> {
1271 if control.cancelled.load(Ordering::Acquire) {
1272 bail!("phone action cancelled");
1273 }
1274 let mut operation_controller = Controller::load()?;
1275 let executor =
1276 CancellableProcessExecutor::new(control.cancelled.clone());
1277 runtime.block_on(apply_phone_action(
1278 &mut operation_controller,
1279 PhoneActionServices {
1280 sessions: &session_control,
1281 daemon_runtime: &daemon_runtime,
1282 },
1283 action,
1284 &executor,
1285 action_id,
1286 &started,
1287 &control,
1288 ))
1289 })();
1290 result.map_err(|error| format!("{error:#}"))
1291 })
1292 .await;
1293 let result = match joined {
1294 Ok(result) => result,
1295 Err(error) => Err(format!("phone action task failed: {error}")),
1296 };
1297 if let Err(error) = done.send((action_id, session_id, result)) {
1298 tracing::debug!(action_id, %error, "phone action finished after the server stopped");
1299 }
1300 });
1301 }
1302 started = action_started_rx.recv() => {
1303 let Some(started) = started else {
1304 tokio::task::yield_now().await;
1305 continue;
1306 };
1307 let started_session_id = started.session.id.clone();
1308 let publication = if !action_cancellations.contains_key(&started.action_id) {
1309 Err("phone action completed before its provisional session was published".into())
1310 } else {
1311 track_started_phone_session(
1312 &mut controller.state,
1313 &mut active_actions,
1314 &mut action_sessions,
1315 started.action_id,
1316 started.session,
1317 )
1318 };
1319 if publication.is_ok() {
1320 revision = daemon_runtime.allocate_revision();
1321 publish_snapshot!(revision);
1322 request_daemon_controller_reload(
1323 daemon_runtime.clone(),
1324 "new session publication",
1325 );
1326 };
1327 if publication.is_err()
1328 && let Some(control) = action_cancellations.get(&started.action_id)
1329 {
1330 control.request_cancel();
1331 }
1332 action_replies.resolve(
1335 started.action_id,
1336 if publication.is_ok() {
1337 ActionOutcome::Accepted {
1338 session_id: Some(started_session_id),
1339 }
1340 } else {
1341 ActionOutcome::Failed
1342 },
1343 );
1344 if started.published.send(publication).is_err() {
1345 tracing::debug!(action_id = started.action_id, "phone new-session publication reply dropped after client disconnect");
1346 }
1347 }
1348 completed = action_done_rx.recv() => {
1349 let Some((action_id, session_id, result)) = completed else {
1350 failure = feed_stopped(termination.is_cancelled(), "the phone action pipeline stopped reporting completions");
1351 break;
1352 };
1353 action_cancellations.remove(&action_id);
1354 let session_id = action_sessions.remove(&action_id).or(session_id);
1355 if closing_actions.values().any(|closing_id| *closing_id == action_id) && let Some(id) = &session_id { daemon_runtime.clear_close_request(id); }
1356 closing_actions.retain(|_, closing_id| *closing_id != action_id);
1357 if let Some(session_id) = &session_id && !action_sessions.values().any(|active| active == session_id) {
1358 active_actions.remove(session_id);
1359 }
1360 action_replies.resolve(action_id, ActionOutcome::Failed);
1364 if let Some(workspace_id) = launch_workspaces.remove(&action_id)
1365 && result.is_err()
1366 && !session_id.as_ref().is_some_and(|id| closing_actions.contains_key(id))
1367 {
1368 record_launch_failure(
1369 &mut launch_failures,
1370 action_id,
1371 workspace_id,
1372 session_id.clone(),
1373 result.as_ref().err().cloned(),
1374 );
1375 revision = daemon_runtime.allocate_revision();
1376 publish_snapshot!(revision);
1377 }
1378 if let Err(error) = &result {
1379 tracing::warn!(
1380 action_id,
1381 session_id = session_id.as_deref(),
1382 %error,
1383 "phone action failed"
1384 );
1385 }
1386 record_action_result(
1387 &mut pending_action_errors,
1388 session_id.as_deref(),
1389 &result,
1390 );
1391 request_controller_reload(
1392 &mut controller_reload_in_flight,
1393 &mut controller_reload_requested,
1394 &controller_reload_tx,
1395 );
1396 request_move_recovery_reload(
1397 &move_recovery_tx,
1398 &mut move_recovery_load_in_flight,
1399 &mut move_recovery_jobs,
1400 );
1401 request_daemon_controller_reload(
1402 daemon_runtime.clone(),
1403 "phone action completion",
1404 );
1405 }
1406 reloaded = controller_reload_rx.recv() => {
1407 let Some(ControllerReloaded { result }) = reloaded else {
1408 failure = feed_stopped(
1409 termination.is_cancelled(),
1410 "the controller reload pipeline stopped while the phone server was running",
1411 );
1412 break;
1413 };
1414 controller_reload_in_flight = false;
1415 if std::mem::take(&mut controller_reload_invalidated) {
1416 if let Err(error) = &result {
1417 tracing::warn!(%error, "superseded controller reload failed");
1418 }
1419 controller_reload_requested = false;
1420 request_controller_reload(
1421 &mut controller_reload_in_flight,
1422 &mut controller_reload_requested,
1423 &controller_reload_tx,
1424 );
1425 continue;
1426 }
1427 match result {
1428 Ok(mut reloaded) => {
1429 for (session_id, error) in &pending_action_errors {
1430 if let Some(session) = reloaded.state.sessions.get_mut(session_id)
1431 && session.last_error.is_none()
1432 {
1433 session.last_error = Some(error.clone());
1434 }
1435 }
1436 controller = reloaded;
1437 quotas.retain(|id, _| controller.config.enabled_profile(id).is_some());
1438 subagent_quota_reports
1439 .lock()
1440 .expect("sub-agent quota reports lock poisoned")
1441 .retain(|id, _| controller.config.enabled_profile(id).is_some());
1442 worker_targets_tx.send_replace(dashboard_worker_targets(&controller));
1443 publish_capacity_targets(
1444 &controller,
1445 &capacity_targets_tx,
1446 &mut capacity_state,
1447 );
1448 credential_sync_handle.set_targets(credential_sync_targets(&controller));
1449 republish_quota_profiles(
1450 &controller,
1451 &mut published_quota_profiles,
1452 &mut quota_batch,
1453 "a_profiles_tx,
1454 );
1455 profile_catalog.sync(&controller.config);
1463 queued_prompts.retain(|session_id, _| {
1464 controller.state.sessions.contains_key(session_id)
1465 });
1466 pending_elicitations.retain(|session_id, _| {
1467 controller.state.sessions.contains_key(session_id)
1468 });
1469 prompt_images.retain(|session_id| {
1470 controller.state.sessions.contains_key(session_id)
1471 });
1472 operational.retain(|session_id, _| {
1473 controller.state.sessions.contains_key(session_id)
1474 });
1475 materialized_activity.retain(|session_id, _| {
1476 controller.state.sessions.contains_key(session_id)
1477 });
1478 request_move_recovery_reload(
1479 &move_recovery_tx,
1480 &mut move_recovery_load_in_flight,
1481 &mut move_recovery_jobs,
1482 );
1483 conversations.retain(|id, _| {
1484 controller.state.sessions.get(id).is_some_and(|session| session.state.is_active())
1485 });
1486 for session_id in conversation_projections.session_ids() {
1487 if !controller
1488 .state
1489 .sessions
1490 .get(&session_id)
1491 .is_some_and(|session| session.state.is_active())
1492 {
1493 conversation_projections.forget(&session_id);
1494 }
1495 }
1496 revision = daemon_runtime.allocate_revision();
1497 conversation_tx.send_replace(conversations.clone());
1498 publish_snapshot!(revision);
1499 }
1500 Err(error) => {
1501 tracing::warn!(%error, "completed phone operation could not reload controller state");
1502 }
1503 }
1504 if controller_reload_requested {
1505 controller_reload_requested = false;
1506 controller_reload_in_flight = true;
1507 spawn_controller_reload(controller_reload_tx.clone());
1508 }
1509 }
1510 }
1511 }
1512 dictation_jobs.shutdown().await;
1514 bundle_jobs.shutdown().await;
1517 preflight_jobs.shutdown().await;
1521 move_preparation_jobs.shutdown().await;
1524 move_recovery_jobs.shutdown().await;
1525 crate::controller::profile_config::cancel_all();
1527 for control in action_cancellations.values() {
1528 control.request_cancel();
1529 }
1530 match failure {
1531 Some(failure) => Err(failure),
1532 None => Ok::<(), anyhow::Error>(()),
1533 }
1534 };
1535 let result = tokio::select! {
1536 result = api_activity::record_activity_stream(activity_snapshots) => result.context("native API activity recorder stopped"),
1537 result = serve => result,
1538 result = control => result,
1539 };
1540 conversation_projection_shutdown.cancel();
1541 renewal_cancellation.cancel();
1542 if let Some(task) = renewal_task
1543 && let Err(error) = task.await
1544 {
1545 tracing::warn!(%error, "Tailscale certificate renewal task failed");
1546 }
1547 worker_shutdown
1548 .shutdown()
1549 .await
1550 .context("shut down phone server session manager")?;
1551 result?;
1552 Ok(())
1553}