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