1use super::*;
2
3impl Controller {
4 pub(in crate::controller) fn worker_placement(
8 &self,
9 session_id: &str,
10 ) -> Result<(targets::TargetLocator, String)> {
11 let session = self
12 .state
13 .sessions
14 .get(session_id)
15 .with_context(|| format!("unknown session {session_id}"))?;
16 let locator = session
17 .target
18 .as_ref()
19 .context("session target is missing")?;
20 let backend = backend_locator(locator, session, &self.config)?;
21 let worker_root = targets::worker_root(&backend, session_id)?;
22 Ok((backend, worker_root))
23 }
24
25 pub(in crate::controller) fn prepare_worker_files(
26 &self,
27 session_id: &str,
28 backend: &targets::TargetLocator,
29 worker_root: &str,
30 executor: &impl CommandExecutor,
31 ) -> Result<()> {
32 let session = self
33 .state
34 .sessions
35 .get(session_id)
36 .with_context(|| format!("unknown session {session_id}"))?;
37 let profile = self
38 .config
39 .profiles
40 .get(&session.last_profile)
41 .context("session profile is missing")?;
42 let (mut launch, project_memory, target_profile_home) =
43 self.session_launch_config(session_id, backend)?;
44
45 if session.native_session_id.is_some()
46 && profile.kind == mj_core::config::HarnessKind::Codex
47 {
48 launch.goal_resume_request = Some(mj_core::state::new_session_id()?);
49 }
50 let staging = tempfile::tempdir().context("create worker staging directory")?;
51 let launch_path = staging.path().join("launch.json");
52 launch.write(&launch_path)?;
53 let ownership_path = staging.path().join("ownership.json");
54 WorkerOwnership {
55 version: WorkerOwnership::VERSION,
56 workspace_id: session.workspace_id.clone(),
57 session_id: session_id.to_string(),
58 profile_id: session.last_profile.clone(),
59 bundle_id: session.bundle_id.clone(),
60 target_template_id: session.target_template_id.clone(),
61 instance_id: Some(mj_core::config::instance_identity()),
62 }
63 .write(&ownership_path)?;
64 let profile_stage = staging.path().join("profile");
65 if crate::controller::session_owns_profile_home(backend, session_id, profile) {
71 let started = Instant::now();
72 let result = stage_profile(profile, &profile_stage);
73 tracing::debug!(
74 session_id,
75 elapsed_ms = started.elapsed().as_millis(),
76 "profile staging completed"
77 );
78 result?;
79 stage_codex_catalog(
80 &session.last_profile,
81 profile,
82 &profile_stage,
83 &fetch_catalog_over_https,
84 &SharedCatalogCache,
85 )?;
86 append_hel_target_environment(profile.kind, &profile_stage, backend)?;
87 apply_staged_execution_setting(profile.kind, launch.execution_policy, &profile_stage)?;
88 if launch.subagent_tools && profile.kind == mj_core::config::HarnessKind::Claude {
89 configure_claude_subagent_mcp(&profile_stage, worker_root)?;
90 }
91 stage_memory_replica(
92 &project_memory,
93 Path::new(&target_profile_home),
94 &profile_stage,
95 )?;
96 if project_memory.mcp_delivery == ProjectMemoryMcpDelivery::HarnessProfile {
97 configure_kimi_project_memory_mcp(&profile_stage, worker_root, &project_memory)?;
98 }
99 } else {
100 seed_local_memory_replica(&project_memory)?;
101 }
102 let worker_binary = worker_binary_for(backend, executor)?;
103
104 install_worker_files(
105 executor,
106 backend,
107 session_id,
108 worker_root,
109 &target_profile_home,
110 &worker_binary,
111 &launch_path,
112 &ownership_path,
113 &profile_stage,
114 )?;
115 if session.build_cache.is_some()
118 && let Err(error) = self.install_build_cache_shim(session, backend, executor)
119 {
120 tracing::warn!(
121 session_id,
122 "installing the mbx build cache failed: {error:#}"
123 );
124 }
125 prepare_installed_managed_harness(executor, backend, worker_root, &launch)
126 }
127
128 fn install_build_cache_shim(
133 &self,
134 session: &mj_core::state::SessionRecord,
135 backend: &targets::TargetLocator,
136 executor: &impl CommandExecutor,
137 ) -> Result<()> {
138 let worker_root = targets::worker_root(backend, &session.id)?;
139 let binary =
143 crate::controller::mbx::binary_for(backend, executor).inspect_err(|error| {
144 executor.notify_notice(&format!(
145 "The Rust build cache is unavailable: {error:#}; this session builds without it."
146 ));
147 })?;
148 let configuration = self
149 .config
150 .targets
151 .get(&session.target_template_id)
152 .map(|template| {
153 crate::controller::backend::backend_target(
154 template,
155 session.resource_allocation.as_ref(),
156 crate::controller::backend::ContainerOverrides::for_session(session),
157 )
158 })
159 .transpose()?
160 .and_then(|target| {
161 crate::controller::mbx::host_configuration(
162 &target,
163 &self.config.build_cache,
164 executor,
165 )
166 });
167 install_mbx_files(
168 executor,
169 backend,
170 &session.id,
171 &worker_root,
172 &binary,
173 configuration.as_deref(),
174 )
175 }
176
177 pub fn diagnose_worker(&self, session_id: &str) -> Option<String> {
181 self.diagnose_worker_controlled(session_id, &crate::targets::ProcessExecutor)
182 }
183
184 pub fn diagnose_worker_controlled(
185 &self,
186 session_id: &str,
187 executor: &impl CommandExecutor,
188 ) -> Option<String> {
189 let session = self.state.sessions.get(session_id)?;
190 let locator = session.target.as_ref()?;
191 let backend = match backend_locator(locator, session, &self.config) {
192 Ok(backend) => backend,
193 Err(error) => {
194 tracing::debug!(
195 session_id,
196 error = format!("{error:#}"),
197 "could not construct a worker diagnostic probe"
198 );
199 return None;
200 }
201 };
202 let worker_root = match targets::worker_root(&backend, session_id) {
203 Ok(root) => root,
204 Err(error) => {
205 tracing::debug!(
206 session_id,
207 error = format!("{error:#}"),
208 "could not derive the worker diagnostic root"
209 );
210 return None;
211 }
212 };
213 let binary_failure = worker_binary_probe_failure(executor, &backend, &worker_root);
214 let last_words = worker_last_words(executor, &backend, &worker_root);
215 match (binary_failure, last_words) {
216 (Some(binary_failure), Some(last_words)) => {
217 Some(format!("{binary_failure}; {last_words}"))
218 }
219 (Some(binary_failure), None) => Some(binary_failure),
220 (None, last_words) => last_words,
221 }
222 }
223
224 pub fn worker_recovery_plan(&self, session_id: &str) -> Result<WorkerRecoveryPlan> {
228 let (backend, worker_root) = self.worker_placement(session_id)?;
229 let launch = self.current_worker_launch_config(session_id, &backend)?;
230 let workspace = worker_workspace_for_recovery(&backend, &launch.cwd);
231 Ok(WorkerRecoveryPlan {
232 source_target: self.state.sessions[session_id]
233 .target
234 .clone()
235 .context("session target is missing")?,
236 target: targets::target_recovery_plan(&backend, session_id)?,
237 workspace,
238 liveness_probe: worker_liveness_command(&backend, &worker_root),
239 binary_refresh: worker_binary_refresh_plan(&backend, session_id)?,
240 launch_refresh: Some(worker_launch_refresh_plan(&backend, session_id, &launch)?),
241 restart: CommandPlan {
242 description: format!("restart Mjolnir worker for session {session_id}"),
243 commands: vec![
244 stop_worker_command(&backend, &worker_root),
245 start_worker_command(&backend, &worker_root),
246 ],
247 },
248 })
249 }
250
251 fn session_launch_config(
256 &self,
257 session_id: &str,
258 backend: &targets::TargetLocator,
259 ) -> Result<(WorkerLaunchConfig, ProjectMemoryLaunchConfig, String)> {
260 let session = self
261 .state
262 .sessions
263 .get(session_id)
264 .with_context(|| format!("unknown session {session_id}"))?;
265 session.validate_configuration(&self.config)?;
266 let profile = self
267 .config
268 .profiles
269 .get(&session.last_profile)
270 .context("session profile is missing")?;
271 let bundle = session
272 .project_directory
273 .is_none()
274 .then(|| self.config.bundles.get(&session.bundle_id))
275 .flatten();
276 let target = self
277 .config
278 .targets
279 .get(&session.target_template_id)
280 .context("session target template is missing")?;
281 let subagent = crate::database::load_subagent(session_id)?;
282 let (workspace_session_id, workspace_container) = match subagent.as_ref() {
285 Some(child) => {
286 let parent = self
287 .state
288 .sessions
289 .get(&child.parent_session_id)
290 .context("sub-agent parent session is missing")?;
291 (parent.id.clone(), parent.container_workspace.clone())
292 }
293 None => (session_id.to_owned(), session.container_workspace.clone()),
294 };
295 let (mut launch, project_memory, target_profile_home) = worker_launch_config(
296 session,
297 profile,
298 bundle,
299 backend,
300 &workspace_session_id,
301 workspace_container.as_deref(),
302 target,
303 )?;
304 launch.subagent_tools =
305 subagent_tools_enabled(session, self.config.subagents.enabled, subagent.is_some());
306 if launch.subagent_tools
312 && profile.kind == mj_core::config::HarnessKind::Claude
313 && !crate::controller::session_owns_profile_home(backend, session_id, profile)
314 {
315 tracing::info!(
316 session_id,
317 "Mjolnir sub-agents need a harness home of their own; this Claude session runs \
318 out of the user's own home and keeps Claude's Agent and Task tools instead"
319 );
320 launch.subagent_tools = false;
321 }
322 launch.review_capture =
328 self.config.review.reviewer_profile().is_some() && subagent.is_none();
329 if let Some(subagent) = &subagent {
330 let parent = self
331 .state
332 .sessions
333 .get(&subagent.parent_session_id)
334 .context("sub-agent parent session is missing")?;
335 let parent_profile = self
336 .config
337 .profiles
338 .get(&parent.last_profile)
339 .context("sub-agent parent profile is missing")?;
340 let parent_target = self
341 .config
342 .targets
343 .get(&parent.target_template_id)
344 .context("sub-agent parent target template is missing")?;
345 let parent_locator = parent
346 .target
347 .as_ref()
348 .context("sub-agent parent has no live target")?;
349 let parent_backend = backend_locator(parent_locator, parent, &self.config)?;
350 let parent_bundle = parent
351 .project_directory
352 .is_none()
353 .then(|| self.config.bundles.get(&parent.bundle_id))
354 .flatten();
355 let (parent_launch, _, _) = worker_launch_config(
356 parent,
357 parent_profile,
358 parent_bundle,
359 &parent_backend,
360 &parent.id,
361 parent.container_workspace.as_deref(),
362 parent_target,
363 )?;
364 launch.cwd = if subagent.working_directory.as_os_str().is_empty() {
365 parent_launch.cwd
366 } else {
367 parent_launch.cwd.join(&subagent.working_directory)
368 };
369 launch.additional_directories = parent_launch.additional_directories;
370 }
371 Ok((launch, project_memory, target_profile_home))
372 }
373
374 pub(in crate::controller) fn current_worker_launch_config(
375 &self,
376 session_id: &str,
377 backend: &targets::TargetLocator,
378 ) -> Result<WorkerLaunchConfig> {
379 let session = self
380 .state
381 .sessions
382 .get(session_id)
383 .with_context(|| format!("unknown session {session_id}"))?;
384 let (mut launch, _, _) = self.session_launch_config(session_id, backend)?;
385 if crate::database::load_move_operation(session_id)?.is_some_and(|operation| {
386 operation.source_checkpoint_only
387 && operation.destination_target.is_none()
388 && matches!(
389 operation.phase,
390 mj_core::state::MovePhase::Preparing
391 | mj_core::state::MovePhase::ClosingSource
392 | mj_core::state::MovePhase::Failed
393 | mj_core::state::MovePhase::Cancelled
394 )
395 && session.last_profile == operation.source_profile_id
396 && session.target == operation.source_target
397 && matches!(
398 session.state,
399 mj_core::state::SessionState::Running
400 | mj_core::state::SessionState::Disconnected
401 | mj_core::state::SessionState::Closing
402 )
403 }) {
404 launch.run_mode = mj_core::worker_launch::WorkerRunMode::CheckpointOnly;
405 }
406 Ok(launch)
407 }
408
409 pub fn project_memory_sync_target(&self, session_id: &str) -> Result<ProjectMemorySyncTarget> {
410 let session = self
411 .state
412 .sessions
413 .get(session_id)
414 .with_context(|| format!("unknown session {session_id}"))?;
415 session.validate_configuration(&self.config)?;
416 let locator = session
417 .target
418 .as_ref()
419 .context("session target is missing")?;
420 let backend = backend_locator(locator, session, &self.config)?;
421 let profile = self
422 .config
423 .profiles
424 .get(&session.last_profile)
425 .context("session profile is missing")?;
426 let bundle = session
427 .project_directory
428 .is_none()
429 .then(|| self.config.bundles.get(&session.bundle_id))
430 .flatten();
431 let workspace = if let Some(project_directory) = &session.project_directory {
432 (project_directory.to_string_lossy().into_owned(), Vec::new())
433 } else {
434 workspace_paths(
435 &backend,
436 bundle.context("session bundle is missing")?,
437 session_id,
438 session.container_workspace.as_deref(),
439 )?
440 };
441 let target_home = target_profile_home(&backend, session_id, profile);
442 let launch = project_memory_launch(session, bundle, &workspace, &target_home)?;
443 Ok(ProjectMemorySyncTarget {
444 canonical_root: canonical_memory_root(&launch.project_key),
445 })
446 }
447}
448
449pub(super) fn subagent_tools_enabled(
455 session: &mj_core::state::SessionRecord,
456 global_enabled: bool,
457 is_child: bool,
458) -> bool {
459 session.mjolnir_subagents.unwrap_or(global_enabled)
460 && !is_child
461 && matches!(
462 session.harness_kind,
463 mj_core::config::HarnessKind::Claude | mj_core::config::HarnessKind::Codex
464 )
465}
466
467pub(super) fn worker_workspace_for_recovery(
468 backend: &targets::TargetLocator,
469 directory: &Path,
470) -> Option<WorkerWorkspace> {
471 let target = match backend {
472 targets::TargetLocator::LocalBare { .. } => mj_core::state::ManagedWorktreeTarget::Local,
473 targets::TargetLocator::SshBare { ssh, .. } => mj_core::state::ManagedWorktreeTarget::Ssh {
474 destination: ssh.destination.clone(),
475 ssh_args: ssh.ssh_args.clone(),
476 },
477 targets::TargetLocator::LocalPodman { .. }
478 | targets::TargetLocator::LocalDocker { .. }
479 | targets::TargetLocator::AppleContainer { .. }
480 | targets::TargetLocator::AwsEc2 { .. }
481 | targets::TargetLocator::SshPodman { .. }
482 | targets::TargetLocator::SshDocker { .. } => return None,
483 };
484 Some(WorkerWorkspace {
485 target,
486 directory: directory.to_path_buf(),
487 })
488}
489
490pub(super) fn worker_launch_config(
491 session: &mj_core::state::SessionRecord,
492 profile: &mj_core::config::HarnessProfile,
493 bundle: Option<&ProjectBundle>,
494 backend: &targets::TargetLocator,
495 workspace_session_id: &str,
496 workspace_container: Option<&Path>,
497 target: &mj_core::config::TargetTemplate,
498) -> Result<(WorkerLaunchConfig, ProjectMemoryLaunchConfig, String)> {
499 let session_id = session.id.as_str();
500 let execution_policy = profile
501 .kind
502 .effective_execution_policy(target.execution_policy());
503 let target_profile_home = target_profile_home(backend, session_id, profile);
504 let workspace = if let Some(project_directory) = &session.project_directory {
505 (project_directory.to_string_lossy().into_owned(), Vec::new())
506 } else {
507 workspace_paths(
508 backend,
509 bundle.context("session bundle is missing")?,
510 workspace_session_id,
511 workspace_container,
512 )?
513 };
514 let mut additional_directories = workspace.1.iter().map(PathBuf::from).collect::<Vec<_>>();
515 additional_directories.extend(
516 session
517 .additional_mounts
518 .iter()
519 .map(|resource| resource.destination.clone()),
520 );
521 if profile.kind == mj_core::config::HarnessKind::Muse && !additional_directories.is_empty() {
522 bail!(
523 "{} ACP does not support multiple workspace roots; use a single-repository bundle",
524 profile.kind.display_name()
525 );
526 }
527 let (bridge_command, bridge_args) = bridge_launch(profile.kind, execution_policy);
528 use mj_core::config::TargetTemplate;
529 let target_environment = match target {
530 TargetTemplate::LocalPodman { container }
531 | TargetTemplate::LocalDocker { container }
532 | TargetTemplate::AppleContainer { container }
533 | TargetTemplate::SshPodman { container, .. }
534 | TargetTemplate::SshDocker { container, .. } => container.environment.clone(),
535 _ => Default::default(),
536 };
537 let mut target_environment = target_environment;
538 for name in [
548 "MJ_TURN_STALL_TIMEOUT_MS",
549 "MJ_TURN_TOOL_STALL_TIMEOUT_MS",
550 "RUST_LOG",
551 ] {
552 if let Ok(value) = std::env::var(name) {
553 target_environment.insert(name.to_owned(), value);
554 }
555 }
556 target_environment.insert(
561 "MJ_INSTANCE".to_owned(),
562 mj_core::config::instance_identity(),
563 );
564 if let Some(build_cache) = &session.build_cache {
567 target_environment.insert(
568 "MBX_CACHE_DIR".into(),
569 build_cache.directory.to_string_lossy().into_owned(),
570 );
571 if let Some(max_size) = &build_cache.max_size {
572 target_environment.insert("MBX_GC_MAX_SIZE".into(), max_size.clone());
573 }
574 target_environment.insert("MBX_SUMMARY".into(), "off".into());
577 target_environment.insert("MBX_SAVINGS".into(), "off".into());
578 }
579 let mut environment = target_environment.clone();
580 environment.extend(profile.environment.clone());
581 profile.kind.configure_home_environment(
582 Path::new(&target_profile_home),
583 backend.harness_host(),
584 &mut environment,
585 );
586 profile
587 .kind
588 .configure_execution_environment(execution_policy, &mut environment)?;
589 let mut project_memory =
590 project_memory_launch(session, bundle, &workspace, &target_profile_home)?;
591 project_memory.mcp_delivery = project_memory_mcp_delivery(profile.kind, backend);
592 if profile.kind == mj_core::config::HarnessKind::Claude {
593 environment.insert(
594 "CLAUDE_CODE_PROJECT_DIR_NAME".into(),
595 project_memory_replica_slug(&project_memory.project_key, session_id),
596 );
597 }
598 apply_claude_setup_token(
599 &mut environment,
600 profile.kind,
601 &mj_core::credentials::claude_oauth_token_path(&session.last_profile),
602 );
603 Ok((
604 WorkerLaunchConfig {
605 goal_resume_request: None,
606 target_environment,
607 seed_image_environment: backend.container_engine().is_some(),
608 run_mode: Default::default(),
609 session_id: session_id.to_string(),
610 subagent_tools: false,
611 review_capture: false,
612 harness: profile.kind,
613 harness_home: PathBuf::from(&target_profile_home),
614 authentication_marker: profile
617 .authentication_marker()
618 .file_name()
619 .map(|name| name.to_string_lossy().into_owned()),
620 bridge_command: PathBuf::from(bridge_command),
621 bridge_args,
622 harness_runtime: harness_runtime_policy(backend),
623 environment,
624 cwd: PathBuf::from(&workspace.0),
625 additional_directories,
626 native_session_id: session.native_session_id.clone(),
627 project_memory: profile
628 .kind
629 .supports_injected_mcp()
630 .then(|| project_memory.clone()),
631 execution_policy,
632 },
633 project_memory,
634 target_profile_home,
635 ))
636}
637
638pub(super) fn harness_runtime_policy(backend: &targets::TargetLocator) -> HarnessRuntimePolicy {
639 match backend {
640 targets::TargetLocator::LocalBare { .. }
641 | targets::TargetLocator::AwsEc2 { .. }
642 | targets::TargetLocator::SshBare { .. } => HarnessRuntimePolicy::Managed,
643 _ => HarnessRuntimePolicy::Ambient,
644 }
645}