Skip to main content

mj_controller/hel_controller/
resume.rs

1//! Resuming a stopped session onto a profile and target.
2
3use std::path::{Path, PathBuf};
4use std::sync::Arc;
5use std::time::{Duration, Instant};
6
7use agent_client_protocol::schema::v1::ContentBlock;
8use anyhow::{Context, Result, bail, ensure};
9use rayon::prelude::*;
10use serde::{Deserialize, Serialize};
11use tokio_util::sync::CancellationToken;
12
13use crate::hel_session_manager::new_command_id;
14use hel::hel_archive::{
15    CanonicalQueuedCommandKind, CanonicalSessionSnapshot, CheckpointRepositoryBundle, SystemGit,
16    checkpoint_bundle_prerequisites, read_checkpoint_repository_bundles, verify_archive_streaming,
17};
18use hel::hel_checkpoint::{CheckpointRestoreSpec, restore_command};
19use hel::hel_config::{HarnessKind, HelConfig, ProjectRepository, mount_history_host};
20use hel::hel_projection::materialized_session_from_canonical;
21use hel::hel_state::{MaterializedSession, SessionRecord, SessionResourceAllocation, SessionState};
22use hel::hel_targets::{
23    self, AdditionalMount, CancellableProcessExecutor, CommandExecutor, CommandOutput, CommandSpec,
24    ProcessExecutor, ProvisionStage, ProvisionStageGuard,
25};
26use hel::hel_worker::RelayCommand;
27
28use super::backend::{backend_locator, controller_github_token, validate_resource_allocation};
29use super::checkpoint::upload_checkpoint_spec;
30use super::provisioning::{
31    ProvisioningFailureDisposition, StagedExecutor, execute_concurrent_lanes,
32    install_attached_resources,
33};
34use super::readiness::{connect_started_worker, wait_for_native_session_in_stage};
35use super::worker_binary::{bridge_readiness_stage, start_worker, worker_probe_diagnosis};
36use super::worktree::{
37    PrimaryCheckoutRequirement, ResumeConversion, ResumePlan, apply_raw_to_workspace,
38    apply_workspace_to_raw, cleanup_managed_worktree, create_managed_worktree,
39    managed_worktree_checkout_exists, plan_raw_to_workspace,
40    preserve_retained_managed_worktree_branch, raw_checkout_divergence_notice,
41    raw_checkout_position, restore_managed_worktree, resume_compatibility, retire_managed_worktree,
42};
43use super::{
44    Controller, SessionResumeOptions, execute_checked, now, selected_host_container_size,
45    target_profile_home,
46};
47
48#[derive(Debug, Clone, PartialEq, Eq)]
49pub struct ResumeRepositorySourceMismatch {
50    pub session_id: String,
51    pub bundle_id: String,
52    pub repository_id: String,
53    pub missing_commit: String,
54    pub archived_origin: String,
55    pub configured_origin: String,
56}
57
58#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
59#[serde(deny_unknown_fields)]
60pub struct ResumeRepositorySourceReceipt {
61    session_id: String,
62    bundle_id: String,
63    checkpoint_sha256: String,
64    repositories: Vec<ProjectRepository>,
65}
66
67#[derive(Debug, Clone, PartialEq, Eq)]
68pub enum ResumeRepositorySourcePreflight {
69    Ready(ResumeRepositorySourceReceipt),
70    RepositoryMoved(ResumeRepositorySourceMismatch),
71}
72
73struct ResumeRepositoryBundles {
74    checkpoint_sha256: String,
75    repositories: Vec<CheckpointRepositoryBundle>,
76}
77
78/// A small timing scope for the expensive resume phases. Target commands
79/// already trace their own durations; this covers controller-side work and
80/// lets an operator see where a slow resume spent its wall-clock budget.
81struct ResumePhaseTimer<'a> {
82    session_id: &'a str,
83    phase: &'static str,
84    started: Instant,
85}
86
87impl<'a> ResumePhaseTimer<'a> {
88    fn new(session_id: &'a str, phase: &'static str) -> Self {
89        Self {
90            session_id,
91            phase,
92            started: Instant::now(),
93        }
94    }
95}
96
97impl Drop for ResumePhaseTimer<'_> {
98    fn drop(&mut self) {
99        tracing::debug!(
100            session_id = self.session_id,
101            phase = self.phase,
102            elapsed_ms = self.started.elapsed().as_millis(),
103            "resume phase completed"
104        );
105    }
106}
107
108impl Controller {
109    /// Muse has one workspace root; native restore relocates its metadata.
110    pub(super) fn validate_muse_resume_destination(
111        &self,
112        source: &SessionRecord,
113        destination_harness: HarnessKind,
114        _target_id: &str,
115    ) -> Result<()> {
116        if destination_harness != HarnessKind::Muse {
117            return Ok(());
118        }
119        ensure!(
120            source.project_directory.is_some()
121                || self
122                    .config
123                    .bundles
124                    .get(&source.bundle_id)
125                    .is_none_or(|bundle| bundle.repositories.len() == 1),
126            "Muse Code ACP supports one workspace root; use a single-repository bundle"
127        );
128        Ok(())
129    }
130    /// Prove that each configured repository source still supplies the commit
131    /// boundary its checkpoint bundle expects, before provisioning anything.
132    pub fn preflight_resume_repository_sources(
133        &self,
134        session_id: &str,
135        target_id: &str,
136        executor: &(impl CommandExecutor + Sync),
137    ) -> Result<ResumeRepositorySourcePreflight> {
138        let session = self
139            .state
140            .sessions
141            .get(session_id)
142            .with_context(|| format!("unknown session {session_id}"))?;
143        let checkpoint = session
144            .checkpoint
145            .as_ref()
146            .context("session has no checkpoint")?;
147        let plan = resume_compatibility(session, &self.config, target_id)
148            .map_err(|reason| anyhow::anyhow!(reason))?;
149        if session.project_directory.is_some() {
150            debug_assert!(matches!(
151                plan,
152                ResumePlan::InPlace | ResumePlan::RawToWorkspace
153            ));
154            // A raw session resumes from its live checkout. Its synthetic
155            // bundle is only a grouping identity and may no longer be in the
156            // config; neither an in-place resume nor a raw-to-workspace
157            // conversion restores repository contents from that bundle.
158            return Ok(ResumeRepositorySourcePreflight::Ready(
159                ResumeRepositorySourceReceipt {
160                    session_id: session_id.to_owned(),
161                    bundle_id: session.bundle_id.clone(),
162                    checkpoint_sha256: checkpoint.sha256.clone(),
163                    repositories: Vec::new(),
164                },
165            ));
166        }
167        let repositories = read_checkpoint_repository_bundles(&checkpoint.archive_path)?;
168        self.preflight_verified_repository_sources(
169            session_id,
170            ResumeRepositoryBundles {
171                checkpoint_sha256: checkpoint.sha256.clone(),
172                repositories,
173            },
174            None,
175            plan != ResumePlan::WorkspaceToRaw,
176            executor,
177        )
178    }
179
180    fn preflight_verified_repository_sources(
181        &self,
182        session_id: &str,
183        verified: ResumeRepositoryBundles,
184        skip_repository_id: Option<&str>,
185        use_archived_network_sources: bool,
186        executor: &(impl CommandExecutor + Sync),
187    ) -> Result<ResumeRepositorySourcePreflight> {
188        let session = self
189            .state
190            .sessions
191            .get(session_id)
192            .with_context(|| format!("unknown session {session_id}"))?;
193        ensure!(
194            verified.repositories.iter().all(|repository| {
195                !repository.metadata.origin.starts_with("mj-local:")
196                    && !repository.metadata.origin.starts_with("ext::")
197            }),
198            "resuming legacy host-bridge sessions is not supported; start a new network-backed session"
199        );
200        // The immutable archive supplies network provenance. Local source
201        // paths and later host configuration changes are irrelevant.
202        if use_archived_network_sources
203            && verified
204                .repositories
205                .iter()
206                .all(|repository| repository.metadata.remote_workspace)
207        {
208            return Ok(ResumeRepositorySourcePreflight::Ready(
209                ResumeRepositorySourceReceipt {
210                    session_id: session_id.to_owned(),
211                    bundle_id: session.bundle_id.clone(),
212                    checkpoint_sha256: verified.checkpoint_sha256,
213                    repositories: Vec::new(),
214                },
215            ));
216        }
217        if verified.repositories.is_empty() {
218            return Ok(ResumeRepositorySourcePreflight::Ready(
219                ResumeRepositorySourceReceipt {
220                    session_id: session_id.to_owned(),
221                    bundle_id: session.bundle_id.clone(),
222                    checkpoint_sha256: verified.checkpoint_sha256,
223                    repositories: Vec::new(),
224                },
225            ));
226        }
227        let bundle = self
228            .config
229            .bundles
230            .get(&session.bundle_id)
231            .with_context(|| format!("session bundle {:?} is missing", session.bundle_id))?;
232        let configured = verified
233            .repositories
234            .iter()
235            .map(|archived| {
236                bundle
237                    .repositories
238                    .iter()
239                    .find(|repository| repository.id == archived.metadata.id)
240                    .cloned()
241                    .with_context(|| {
242                        format!(
243                            "session bundle {:?} no longer contains repository {:?}",
244                            session.bundle_id, archived.metadata.id
245                        )
246                    })
247            })
248            .collect::<Result<Vec<_>>>()?;
249        let github_token = configured
250            .iter()
251            .any(|repository| repository.github.is_some())
252            .then(controller_github_token)
253            .flatten();
254        let outcomes = verified
255            .repositories
256            .par_iter()
257            .zip(configured.par_iter())
258            .map(|(archived, configured)| {
259                if skip_repository_id == Some(configured.id.as_str()) {
260                    return Ok(None);
261                }
262                checkpoint_source_missing_commit(
263                    configured,
264                    archived,
265                    executor,
266                    github_token.as_deref(),
267                )
268                .map(|missing_commit| {
269                    missing_commit.map(|missing_commit| ResumeRepositorySourceMismatch {
270                        session_id: session_id.to_owned(),
271                        bundle_id: session.bundle_id.clone(),
272                        repository_id: configured.id.clone(),
273                        missing_commit,
274                        archived_origin: archived.metadata.origin.clone(),
275                        configured_origin: configured.source_label(),
276                    })
277                })
278            })
279            .collect::<Vec<Result<Option<ResumeRepositorySourceMismatch>>>>();
280        for outcome in outcomes {
281            if let Some(mismatch) = outcome? {
282                return Ok(ResumeRepositorySourcePreflight::RepositoryMoved(mismatch));
283            }
284        }
285        Ok(ResumeRepositorySourcePreflight::Ready(
286            ResumeRepositorySourceReceipt {
287                session_id: session_id.to_owned(),
288                bundle_id: session.bundle_id.clone(),
289                checkpoint_sha256: verified.checkpoint_sha256,
290                repositories: configured,
291            },
292        ))
293    }
294
295    fn repository_source_receipt_is_current(
296        &self,
297        session_id: &str,
298        receipt: &ResumeRepositorySourceReceipt,
299    ) -> bool {
300        let Some(session) = self.state.sessions.get(session_id) else {
301            return false;
302        };
303        if receipt.session_id != session_id
304            || receipt.bundle_id != session.bundle_id
305            || session
306                .checkpoint
307                .as_ref()
308                .map(|checkpoint| &checkpoint.sha256)
309                != Some(&receipt.checkpoint_sha256)
310        {
311            return false;
312        }
313        if receipt.repositories.is_empty() {
314            return true;
315        }
316        let Some(bundle) = self.config.bundles.get(&session.bundle_id) else {
317            return false;
318        };
319        receipt.repositories.iter().all(|expected| {
320            bundle
321                .repositories
322                .iter()
323                .any(|configured| configured == expected)
324        })
325    }
326
327    /// Validate a replacement first, then atomically save it and check the
328    /// remaining sources so multi-repository bundles can report the next moved
329    /// repository without ever provisioning a partial target.
330    pub fn replace_resume_repository_origin(
331        &mut self,
332        session_id: &str,
333        repository_id: &str,
334        replacement: &str,
335        executor: &(impl CommandExecutor + Sync),
336    ) -> Result<ResumeRepositorySourcePreflight> {
337        let session = self
338            .state
339            .sessions
340            .get(session_id)
341            .with_context(|| format!("unknown session {session_id}"))?;
342        let bundle_id = session.bundle_id.clone();
343        let checkpoint = session
344            .checkpoint
345            .as_ref()
346            .context("session has no checkpoint")?;
347        let replacement = replacement_repository_source(repository_id, replacement)?;
348        let repositories = read_checkpoint_repository_bundles(&checkpoint.archive_path)?;
349        let verified = ResumeRepositoryBundles {
350            checkpoint_sha256: checkpoint.sha256.clone(),
351            repositories,
352        };
353        let archived = verified
354            .repositories
355            .iter()
356            .find(|repository| repository.metadata.id == repository_id)
357            .with_context(|| format!("checkpoint does not contain repository {repository_id:?}"))?;
358        if let Some(missing_commit) = checkpoint_source_missing_commit(
359            &replacement,
360            archived,
361            executor,
362            controller_github_token().as_deref(),
363        )? {
364            return Ok(ResumeRepositorySourcePreflight::RepositoryMoved(
365                ResumeRepositorySourceMismatch {
366                    session_id: session_id.to_owned(),
367                    bundle_id,
368                    repository_id: repository_id.to_owned(),
369                    missing_commit,
370                    archived_origin: archived.metadata.origin.clone(),
371                    configured_origin: replacement.source_label(),
372                },
373            ));
374        }
375        let (config, ()) = HelConfig::update(|config| {
376            let bundle = config
377                .bundles
378                .get_mut(&bundle_id)
379                .with_context(|| format!("session bundle {bundle_id:?} is missing"))?;
380            let repository = bundle
381                .repositories
382                .iter_mut()
383                .find(|repository| repository.id == repository_id)
384                .with_context(|| {
385                    format!(
386                        "session bundle {:?} no longer contains repository {repository_id:?}",
387                        bundle_id
388                    )
389                })?;
390            repository.github = replacement.github.clone();
391            repository.local = replacement.local.clone();
392            Ok(())
393        })?;
394        self.config = config;
395        self.preflight_verified_repository_sources(
396            session_id,
397            verified,
398            Some(repository_id),
399            false,
400            executor,
401        )
402    }
403}
404
405fn replacement_repository_source(id: &str, replacement: &str) -> Result<ProjectRepository> {
406    let replacement = replacement.trim();
407    ensure!(!replacement.is_empty(), "enter the repository's new origin");
408    let expanded = hel::hel_path_input::expand_local(Path::new(replacement))?;
409    let path = expanded.as_path();
410    let (github, local) = if path.is_absolute() {
411        ensure!(
412            path.is_dir(),
413            "local repository {replacement:?} is not a directory"
414        );
415        (None, Some(hel::hel_local_git::canonical_repository(path)?))
416    } else {
417        let github = crate::hel_setup::github_repository_from_origin(replacement)
418            .context("origin must be a GitHub repository or an absolute local repository path")?;
419        (
420            Some(format!("{}/{}", github.owner, github.repository)),
421            None,
422        )
423    };
424    Ok(ProjectRepository {
425        id: id.to_owned(),
426        github,
427        local,
428        destination: PathBuf::from(id),
429        git_ref: None,
430    })
431}
432
433fn checkpoint_source_missing_commit(
434    configured: &ProjectRepository,
435    archived: &CheckpointRepositoryBundle,
436    executor: &impl CommandExecutor,
437    github_token: Option<&str>,
438) -> Result<Option<String>> {
439    let staging = tempfile::tempdir().context("create repository source preflight")?;
440    let repository = staging.path().join("repository.git");
441    checked_preflight_git(
442        executor,
443        CommandSpec::new(
444            "git",
445            [
446                "init".to_owned(),
447                "--bare".to_owned(),
448                "--quiet".to_owned(),
449                repository.to_string_lossy().into_owned(),
450            ],
451        )
452        .purpose("initialize repository source preflight"),
453    )?;
454    let missing = checkpoint_bundle_prerequisites(archived)?;
455    if missing.is_empty() {
456        let bundle = staging.path().join("checkpoint.bundle");
457        std::fs::write(&bundle, &archived.committed_bundle)
458            .context("write self-contained checkpoint bundle for source preflight")?;
459        checked_preflight_git(
460            executor,
461            checkpoint_bundle_import_command(&repository, &bundle),
462        )?;
463        return Ok(None);
464    }
465    // The restore clone will obtain the reachable ancestry. This probe only
466    // needs to establish that the source still serves each boundary object, so
467    // stop at that object instead of downloading and walking its whole graph.
468    for commit in missing {
469        let output = fetch_source_commit(executor, &repository, configured, &commit, github_token)?;
470        if output.status != 0 {
471            let stderr = String::from_utf8_lossy(&output.stderr);
472            if source_does_not_have_commit(&stderr) {
473                return Ok(Some(commit));
474            }
475            bail!(
476                "could not check configured source {:?}: {}",
477                configured.source_label(),
478                stderr.trim()
479            );
480        }
481    }
482    // Do not re-index the bundle against this deliberately shallow probe: the
483    // shallow marker would make Git report artificial connectivity failures.
484    // The real restore applies it to the full source clone.
485    Ok(None)
486}
487
488fn checkpoint_bundle_import_command(repository: &Path, bundle: &Path) -> CommandSpec {
489    let mut command = CommandSpec::new(
490        "git",
491        [
492            "-C".to_owned(),
493            repository.to_string_lossy().into_owned(),
494            "fetch".to_owned(),
495            "--no-tags".to_owned(),
496            bundle.to_string_lossy().into_owned(),
497            "HEAD".to_owned(),
498        ],
499    )
500    .purpose("validate self-contained checkpoint bundle");
501    command
502        .env
503        .insert("GIT_NO_LAZY_FETCH".to_owned(), "1".to_owned());
504    command
505        .env
506        .insert("GIT_TERMINAL_PROMPT".to_owned(), "0".to_owned());
507    command
508}
509
510fn fetch_source_commit(
511    executor: &impl CommandExecutor,
512    repository: &Path,
513    configured: &ProjectRepository,
514    commit: &str,
515    github_token: Option<&str>,
516) -> Result<CommandOutput> {
517    let mut arguments = Vec::new();
518    let mut token_auth = false;
519    let mut ssh_transport = false;
520    let source = if let Some(local) = &configured.local {
521        local.to_string_lossy().into_owned()
522    } else {
523        let source = configured
524            .github
525            .as_deref()
526            .context("repository source is missing")?;
527        let github = crate::hel_setup::github_repository_from_origin(source)
528            .context("configured repository is not a GitHub source")?;
529        if github_token.is_some() {
530            token_auth = true;
531            arguments.extend([
532                "-c".to_owned(),
533                "credential.helper=".to_owned(),
534                "-c".to_owned(),
535                "credential.helper=!f() { if [ \"$1\" = get ]; then echo username=x-access-token; echo \"password=$GH_TOKEN\"; fi; }; f".to_owned(),
536            ]);
537            format!(
538                "https://github.com/{}/{}.git",
539                github.owner, github.repository
540            )
541        } else {
542            ssh_transport = true;
543            format!("git@github.com:{}/{}.git", github.owner, github.repository)
544        }
545    };
546    arguments.extend([
547        "-C".to_owned(),
548        repository.to_string_lossy().into_owned(),
549        "fetch".to_owned(),
550        "--no-tags".to_owned(),
551        "--depth=1".to_owned(),
552        "--filter=blob:none".to_owned(),
553        source,
554        commit.to_owned(),
555    ]);
556    let mut command = CommandSpec::new("git", arguments).purpose("check checkpoint base commit");
557    command
558        .env
559        .insert("GIT_NO_LAZY_FETCH".to_owned(), "1".to_owned());
560    command
561        .env
562        .insert("GIT_TERMINAL_PROMPT".to_owned(), "0".to_owned());
563    if token_auth {
564        let token = github_token.expect("token authentication requires a GitHub token");
565        command.env.insert("GH_TOKEN".to_owned(), token.to_owned());
566    }
567    if ssh_transport {
568        command.env.insert(
569            "GIT_SSH_COMMAND".to_owned(),
570            "ssh -o BatchMode=yes -o StrictHostKeyChecking=accept-new -o ConnectTimeout=15"
571                .to_owned(),
572        );
573    }
574    executor.execute(&command)
575}
576
577fn source_does_not_have_commit(stderr: &str) -> bool {
578    let stderr = stderr.to_ascii_lowercase();
579    [
580        "not our ref",
581        "couldn't find remote ref",
582        "not a valid object name",
583        "no such ref was fetched",
584    ]
585    .iter()
586    .any(|needle| stderr.contains(needle))
587}
588
589fn checked_preflight_git(
590    executor: &impl CommandExecutor,
591    command: CommandSpec,
592) -> Result<CommandOutput> {
593    let output = executor.execute(&command)?;
594    ensure!(
595        output.status == 0,
596        "{}: {}",
597        command.purpose,
598        String::from_utf8_lossy(&output.stderr).trim()
599    );
600    Ok(output)
601}
602
603impl Controller {
604    /// Resume a stopped logical session on any configured profile and
605    /// target. Cross-harness resume restores Git and canonical history, starts
606    /// a fresh native session, and supplies the prior transcript as its first
607    /// context turn.
608    pub async fn resume_session_with_options(
609        &mut self,
610        session_id: &str,
611        profile_id: &str,
612        target_id: &str,
613        additional_mounts: Option<Vec<AdditionalMount>>,
614        resource_allocation: Option<SessionResourceAllocation>,
615    ) -> Result<MaterializedSession> {
616        self.resume_session_with_options_and_queue_disposition(
617            session_id,
618            profile_id,
619            target_id,
620            additional_mounts,
621            resource_allocation,
622            false,
623        )
624        .await
625    }
626
627    pub async fn resume_session_with_options_and_queue_disposition(
628        &mut self,
629        session_id: &str,
630        profile_id: &str,
631        target_id: &str,
632        additional_mounts: Option<Vec<AdditionalMount>>,
633        resource_allocation: Option<SessionResourceAllocation>,
634        discard_queue: bool,
635    ) -> Result<MaterializedSession> {
636        self.resume_session_controlled(
637            session_id,
638            profile_id,
639            target_id,
640            SessionResumeOptions {
641                additional_mounts,
642                resource_allocation,
643                discard_queue,
644            },
645            &ProcessExecutor,
646        )
647        .await
648    }
649
650    pub async fn resume_session_controlled(
651        &mut self,
652        session_id: &str,
653        profile_id: &str,
654        target_id: &str,
655        options: SessionResumeOptions,
656        executor: &(impl CommandExecutor + Sync),
657    ) -> Result<MaterializedSession> {
658        self.resume_session_controlled_with_repository_preflight(
659            session_id, profile_id, target_id, options, None, executor,
660        )
661        .await
662    }
663
664    pub async fn resume_session_controlled_with_repository_preflight(
665        &mut self,
666        session_id: &str,
667        profile_id: &str,
668        target_id: &str,
669        options: SessionResumeOptions,
670        repository_preflight: Option<ResumeRepositorySourceReceipt>,
671        executor: &(impl CommandExecutor + Sync),
672    ) -> Result<MaterializedSession> {
673        let SessionResumeOptions {
674            additional_mounts,
675            resource_allocation,
676            discard_queue,
677        } = options;
678        let previous = self
679            .state
680            .sessions
681            .get(session_id)
682            .with_context(|| format!("unknown session {session_id}"))?
683            .clone();
684        if !matches!(
685            previous.state,
686            SessionState::Stopped | SessionState::Lost | SessionState::Error
687        ) {
688            bail!("session {session_id} is not stopped, lost, or retryable");
689        }
690        let checkpoint = previous
691            .checkpoint
692            .as_ref()
693            .context("session has no checkpoint")?;
694        // A receipt for an isolated destination says nothing about a host
695        // checkout. Explicit moves to raw execution must check that source.
696        let moving_to_raw = previous.project_directory.is_none()
697            && self
698                .config
699                .targets
700                .get(target_id)
701                .is_some_and(hel::hel_config::is_bare_project_target);
702        if moving_to_raw
703            || !repository_preflight.as_ref().is_some_and(|receipt| {
704                self.repository_source_receipt_is_current(session_id, receipt)
705            })
706        {
707            let _phase = ResumePhaseTimer::new(session_id, "preflight repository sources");
708            if let ResumeRepositorySourcePreflight::RepositoryMoved(mismatch) =
709                self.preflight_resume_repository_sources(session_id, target_id, executor)?
710            {
711                bail!(
712                    "checkpoint base commit {} is missing from configured source {:?} for repository {:?}; the repository may have moved (archived origin: {:?})",
713                    mismatch.missing_commit,
714                    mismatch.configured_origin,
715                    mismatch.repository_id,
716                    mismatch.archived_origin,
717                );
718            }
719        }
720        // Canonicalize before verification and keep this exact absolute path
721        // for the restore. A LocalBare worker shares the controller's
722        // filesystem, so it can consume the verified archive directly instead
723        // of copying a second large file into its worker root.
724        let archive_path = {
725            let _phase = ResumePhaseTimer::new(session_id, "verify checkpoint archive");
726            checkpoint.archive_path.canonicalize().with_context(|| {
727                format!(
728                    "resolve checkpoint archive {}",
729                    checkpoint.archive_path.display()
730                )
731            })?
732        };
733        ensure!(
734            archive_path.is_absolute() && archive_path.is_file(),
735            "checkpoint archive path is not an absolute regular file: {}",
736            archive_path.display()
737        );
738        // Take the snapshot out of the verified metadata and share it behind an
739        // `Arc`: on a long session it is tens of megabytes, and resume reads it
740        // from three places that used to hold private copies.
741        let hel::hel_archive::VerifiedArchiveMetadata {
742            manifest: archive_manifest,
743            canonical_session,
744            archive_sha256,
745        } = {
746            let _phase = ResumePhaseTimer::new(session_id, "verify checkpoint archive contents");
747            verify_archive_streaming(&archive_path)?
748        };
749        if archive_sha256 != checkpoint.sha256 || archive_manifest.session.id != session_id {
750            bail!("persisted checkpoint verification failed");
751        }
752        ensure!(
753            archive_manifest.repositories.iter().all(|repository| {
754                !repository.metadata.origin.starts_with("mj-local:")
755                    && !repository.metadata.origin.starts_with("ext::")
756            }),
757            "resuming legacy host-bridge sessions is not supported; start a new network-backed session"
758        );
759        let canonical_session = Arc::new(canonical_session);
760        let profile = self
761            .config
762            .profiles
763            .get(profile_id)
764            .with_context(|| format!("unknown profile {profile_id:?}"))?
765            .clone();
766        ensure!(profile.enabled, "profile {profile_id:?} is disabled");
767        let target_template = self
768            .config
769            .targets
770            .get(target_id)
771            .with_context(|| format!("unknown target template {target_id:?}"))?
772            .clone();
773        if !hel::hel_config::is_bare_project_target(&target_template) {
774            super::network_git::bundle_from_manifest(&archive_manifest)?;
775        }
776        // Decide the representation before the record changes, so an
777        // incompatible target fails here instead of during provisioning.
778        self.validate_muse_resume_destination(&previous, profile.kind, target_id)?;
779        ensure!(
780            profile.kind != HarnessKind::Muse || previous.additional_mounts.is_empty(),
781            "Muse Code ACP supports one workspace root; attached directories are unsupported"
782        );
783        let plan = resume_compatibility(&previous, &self.config, target_id)
784            .map_err(|reason| anyhow::anyhow!("{reason}"))?;
785        if plan == ResumePlan::InPlace
786            && previous.managed_worktree.is_none()
787            && let Some(project_directory) = &previous.project_directory
788        {
789            self.validate_project_directory(target_id, project_directory, executor)
790                .context("raw project is unavailable for resume")?;
791        }
792        let conversion = match plan {
793            ResumePlan::InPlace => None,
794            ResumePlan::RawToWorkspace => Some(ResumeConversion::RawToWorkspace(
795                plan_raw_to_workspace(&previous, &self.config, executor)
796                    .context("prepare the raw checkout for its new target")?,
797            )),
798            ResumePlan::WorkspaceToRaw => Some(ResumeConversion::WorkspaceToRaw(
799                self.plan_workspace_to_raw(&previous, target_id, executor)
800                    .context("prepare a checkout for this session")?,
801            )),
802        };
803        let resource_allocation =
804            resource_allocation.or_else(|| previous.resource_allocation.clone());
805        let additional_mounts =
806            additional_mounts.unwrap_or_else(|| previous.additional_mounts.clone());
807        validate_resource_allocation(&target_template, resource_allocation.as_ref())?;
808        let selected_container_size =
809            selected_host_container_size(&target_template, resource_allocation.as_ref());
810        if !additional_mounts.is_empty() && mount_history_host(&target_template).is_none() {
811            bail!("attached resources are unsupported for this target");
812        }
813        hel_targets::validate_additional_mounts(&additional_mounts)?;
814        let history_host = mount_history_host(&target_template);
815        let history_mounts = additional_mounts.clone();
816        if previous.state == SessionState::Error
817            && let Some(locator) = &previous.target
818        {
819            let backend = backend_locator(locator, &previous, &self.config)?;
820            hel_targets::close_plan(&backend, session_id)?
821                .execute(executor)
822                .context("clean up target from failed resume")?;
823        }
824        let mut resume_notices = Vec::new();
825        if let Some(conversion) = conversion
826            .as_ref()
827            .and_then(ResumeConversion::raw_to_workspace)
828            && let Some(project_directory) = &previous.project_directory
829        {
830            resume_notices.push(match &conversion.retire {
831                Some(worktree) => format!(
832                    "This session moved out of {} and into the {target_id} target. Its branch {} stays in {}.",
833                    project_directory.display(),
834                    worktree.branch,
835                    worktree.source_repository.display()
836                ),
837                None => format!(
838                    "This session moved out of {} and into the {target_id} target.",
839                    project_directory.display()
840                ),
841            });
842        }
843        if let Some(conversion) = conversion
844            .as_ref()
845            .and_then(ResumeConversion::workspace_to_raw)
846        {
847            resume_notices.push(format!(
848                "This session moved out of its {} target and into {}. Its branch {} is now {}.",
849                previous.target_template_id,
850                conversion.worktree.worktree_root.display(),
851                archive_manifest
852                    .repositories
853                    .first()
854                    .and_then(|repository| repository.metadata.branch.as_deref())
855                    .unwrap_or("a detached head"),
856                conversion.worktree.branch,
857            ));
858        }
859        let managed_checkout_present = previous
860            .managed_worktree
861            .as_ref()
862            .map(|worktree| managed_worktree_checkout_exists(executor, worktree))
863            .transpose()?
864            .unwrap_or(true);
865        // A checkout Mjolnir did not retire remains the truth for a raw session.
866        // A retired checkout is recreated from the branch and archive below.
867        if managed_checkout_present && let Some(project_directory) = &previous.project_directory {
868            match raw_checkout_position(&previous, &self.config, project_directory, executor) {
869                Ok(live) => resume_notices.extend(raw_checkout_divergence_notice(
870                    project_directory,
871                    archive_manifest
872                        .repositories
873                        .first()
874                        .map(|repository| &repository.metadata),
875                    &live,
876                )),
877                // Informational only: a resume must not fail because Mjolnir could
878                // not read where the checkout stands.
879                Err(error) => tracing::warn!(
880                    session_id,
881                    error = format!("{error:#}"),
882                    "could not read the raw checkout position for a resume notice"
883                ),
884            }
885        }
886        // Resolving the worker binary is local and costs microseconds, while
887        // the compaction below costs minutes and paid model requests. A resume
888        // that could never install a worker fails here rather than after all
889        // that work has been thrown away.
890        super::worker_binary::preflight_worker_binary(&target_template)?;
891        let same_harness = profile.kind == archive_manifest.session.harness_kind;
892        let context_bytes = profile
893            .context_window_bytes
894            .unwrap_or(crate::hel_compaction::DEFAULT_CONTEXT_BYTES);
895        // Cross-harness compaction is started alongside destination
896        // provisioning below. Clone only the configuration it reads so the
897        // controller can continue owning and mutating its session record.
898        let utility_config = (!same_harness).then(|| self.config.clone());
899        let discard_queued_prompts = discard_queue || !same_harness;
900        // When this controller archived the session, its durable projection is
901        // already the archive's content. Reading one row decides that; a read
902        // failure or any mismatch rebuilds as before.
903        let stored_frontier = hel::hel_database::materialized_event_frontier(session_id)
904            .unwrap_or_else(|error| {
905                tracing::warn!(
906                    session_id,
907                    error = format!("{error:#}"),
908                    "could not read the stored projection frontier; rebuilding it from the archive"
909                );
910                None
911            });
912        let rebuild_projection = projection_rebuild_required(
913            stored_frontier
914                .as_ref()
915                .map(|(ordinal, digest)| (*ordinal, digest.as_str())),
916            canonical_session.event_frontier,
917            &canonical_session.event_frontier_digest,
918        );
919        // Rebuilding the projection is a pure function of the archive and costs
920        // seconds on a long session. Start it now so it runs while the target is
921        // being provisioned; its result is awaited where it was consumed
922        // before, and the writes it feeds have not moved.
923        //
924        // A resume that fails before the result is needed drops the handle.
925        // `spawn_blocking` work cannot be cancelled, so the computation still
926        // finishes on the blocking pool and its result is discarded; it owns
927        // nothing but its own inputs, so nothing leaks beyond that CPU.
928        let projection_build = rebuild_projection.then(|| {
929            let canonical = Arc::clone(&canonical_session);
930            let session_id = session_id.to_owned();
931            tokio::task::spawn_blocking(move || {
932                materialized_session_from_canonical(session_id, &canonical)
933            })
934        });
935        let github_token = controller_github_token();
936
937        // The configuration gains the bundle before the record points at it, so
938        // no persisted session ever names a bundle that is not there.
939        if let Some(conversion) = conversion
940            .as_ref()
941            .and_then(ResumeConversion::raw_to_workspace)
942            && let Some(bundle) = &conversion.new_bundle
943        {
944            let (config, ()) = HelConfig::update(|config| {
945                if let Some(existing) = config.bundles.get(&conversion.bundle_id) {
946                    ensure!(
947                        existing == bundle,
948                        "bundle {:?} was configured concurrently with a different definition; retry the resume",
949                        conversion.bundle_id
950                    );
951                } else {
952                    config
953                        .bundles
954                        .insert(conversion.bundle_id.clone(), bundle.clone());
955                }
956                Ok(())
957            })
958            .context("save the bundle for a converted raw session")?;
959            self.config = config;
960        }
961
962        let record = self.state.sessions.get_mut(session_id).unwrap();
963        record.harness_kind = profile.kind;
964        record.last_profile = profile_id.to_string();
965        record.target_template_id = target_id.to_string();
966        record.resource_allocation = resource_allocation;
967        record.additional_mounts = additional_mounts;
968        record.target = None;
969        record.native_session_id =
970            same_harness.then(|| archive_manifest.session.native_session_id.clone());
971        record.state = SessionState::Provisioning;
972        record.updated_at = now();
973        record.last_error = None;
974        match &conversion {
975            Some(ResumeConversion::RawToWorkspace(conversion)) => {
976                apply_raw_to_workspace(record, conversion);
977            }
978            Some(ResumeConversion::WorkspaceToRaw(conversion)) => {
979                apply_workspace_to_raw(record, conversion);
980            }
981            None => {}
982        }
983        let resumed_project_directory = record.project_directory.clone();
984        if let Some(host) = history_host {
985            self.state.remember_mount_sources(host, &history_mounts);
986            hel::hel_database::remember_mount_sources(host, &history_mounts)?;
987        }
988        // The session's prompt history is filed under its bundle, so a
989        // conversion moves the history with it before the record is persisted.
990        if let Some(conversion) = conversion
991            .as_ref()
992            .and_then(ResumeConversion::raw_to_workspace)
993        {
994            hel::hel_database::rebind_session_bundle(session_id, &conversion.bundle_id)?;
995        }
996        // Resume rewrites the record it resumes, including the attached
997        // directories and the harness session id, so it writes the whole row.
998        if let Some((host, size)) = selected_container_size.as_ref() {
999            hel::hel_database::save_session_with_container_size(
1000                &self.state.sessions[session_id],
1001                host,
1002                *size,
1003            )?;
1004        } else {
1005            hel::hel_database::save_session(&self.state.sessions[session_id])?;
1006        }
1007        if let Some((host, size)) = selected_container_size {
1008            self.state.remember_container_size(&host, size);
1009        }
1010
1011        let mut recreated_managed_worktree = false;
1012        let result = async {
1013            if let Some(worktree) = previous.managed_worktree.as_ref() {
1014                recreated_managed_worktree = restore_managed_worktree(executor, worktree)?;
1015                if recreated_managed_worktree && plan == ResumePlan::RawToWorkspace {
1016                    hel::hel_checkpoint::restore_single_repository_onto_branch(
1017                        &archive_path,
1018                        &worktree.worktree_root,
1019                        &worktree.branch,
1020                        &SystemGit,
1021                    )
1022                    .context("restore the retired checkout before moving it into a target")?;
1023                }
1024            }
1025            // The record already names the worktree, so a failure here rolls
1026            // back through the same path that cleans up a new session's.
1027            if let Some(conversion) = conversion
1028                .as_ref()
1029                .and_then(ResumeConversion::workspace_to_raw)
1030            {
1031                if conversion.reuse_existing_branch {
1032                    let recovery_ref =
1033                        preserve_retained_managed_worktree_branch(executor, &conversion.worktree)?;
1034                    restore_managed_worktree(executor, &conversion.worktree)?;
1035                    resume_notices.push(format!(
1036                        "Before restoring this session's retained branch, Mjolnir preserved its tip at {recovery_ref}."
1037                    ));
1038                } else {
1039                    create_managed_worktree(
1040                        executor,
1041                        &conversion.worktree,
1042                        None,
1043                        PrimaryCheckoutRequirement::Any,
1044                    )?;
1045                }
1046                hel::hel_checkpoint::restore_single_repository_onto_branch(
1047                    &archive_path,
1048                    &conversion.worktree.worktree_root,
1049                    &conversion.worktree.branch,
1050                    &SystemGit,
1051                )
1052                .context("restore this session's checkout")?;
1053            }
1054            let utility_handoff = {
1055                let _provisioning = ResumePhaseTimer::new(session_id, "provision destination");
1056                if let Some(config) = utility_config.as_ref() {
1057                    Some(
1058                        provision_with_cross_harness_handoff(
1059                            self,
1060                            session_id,
1061                            executor,
1062                            github_token.as_deref(),
1063                            config,
1064                            &canonical_session,
1065                            context_bytes,
1066                        )
1067                        .context("prepare the cross-harness destination")?,
1068                    )
1069                } else {
1070                    self.provision_session_with_failure_disposition(
1071                        session_id,
1072                        executor,
1073                        github_token.as_deref(),
1074                        ProvisioningFailureDisposition::Preserve,
1075                    )
1076                    .await?;
1077                    None
1078                }
1079            };
1080            let (backend, worker_root) = self.worker_placement(session_id)?;
1081            let harness_home = target_profile_home(&backend, session_id, &profile);
1082            let workspace_root = if let Some(project_directory) = &resumed_project_directory {
1083                project_directory
1084                    .parent()
1085                    .context("bare project directory has no parent")?
1086                    .to_string_lossy()
1087                    .into_owned()
1088            } else {
1089                match &backend {
1090                    hel_targets::TargetLocator::LocalPodman { .. }
1091                    | hel_targets::TargetLocator::LocalDocker { .. }
1092                    | hel_targets::TargetLocator::AppleContainer { .. }
1093                    | hel_targets::TargetLocator::SshPodman { .. }
1094                    | hel_targets::TargetLocator::SshDocker { .. } => "/workspace".to_string(),
1095                    hel_targets::TargetLocator::AwsEc2 { workspace, .. }
1096                    | hel_targets::TargetLocator::SshBare { workspace, .. } => workspace.clone(),
1097                    hel_targets::TargetLocator::LocalBare { worker_root } => worker_root.clone(),
1098                }
1099            };
1100            let target_path = |path: &str| match &backend {
1101                hel_targets::TargetLocator::AwsEc2 { .. }
1102                | hel_targets::TargetLocator::SshBare { .. }
1103                    if !path.starts_with('/') =>
1104                {
1105                    PathBuf::from(format!("~/{path}"))
1106                }
1107                _ => PathBuf::from(path),
1108            };
1109            let remote_archive = format!("{worker_root}/restore.hel.zip");
1110            let remote_spec = format!("{worker_root}/restore-spec.json");
1111            let restore = CheckpointRestoreSpec {
1112                archive_path: restore_archive_path(
1113                    &backend,
1114                    &archive_path,
1115                    &target_path(&remote_archive),
1116                ),
1117                workspace_root: target_path(&workspace_root),
1118                relay_root: target_path(&worker_root),
1119                harness_home: target_path(&harness_home),
1120                // A converted session's repository arrives as a seed from its
1121                // own checkout. An in-place managed checkout recreated from
1122                // its retained branch still needs the archive's dirty state.
1123                restore_repositories: (resumed_project_directory.is_none() && conversion.is_none())
1124                    || (recreated_managed_worktree && plan == ResumePlan::InPlace),
1125                restore_native: same_harness,
1126                // A conversion puts the checkout somewhere the archive could
1127                // not have named, so the restored harness session is pointed at
1128                // the real working directory instead of the archived one.
1129                primary_repository_root: conversion
1130                    .is_some()
1131                    .then(|| resumed_project_directory.clone())
1132                    .flatten()
1133                    .map(|directory| target_path(&directory.to_string_lossy())),
1134                discard_queued_prompts,
1135            };
1136            // A bare target keeps the closed session's worker root on the host.
1137            // Stop anything still writing there and clear the leftover relay
1138            // state, or the restore's seed loses to a stale snapshot whose
1139            // frontier no journal can support. This runs before the worker
1140            // binary is installed: a surviving daemon still holds the old one
1141            // open, and the install would land on a running executable.
1142            {
1143                let syncing = &StagedExecutor::new(executor, ProvisionStage::Syncing);
1144                if let Some(command) = hel_targets::clear_relay_state_plan(&backend, session_id)? {
1145                    execute_checked(syncing, command)?;
1146                }
1147                // Both lanes below write into the worker root, so it exists first.
1148                execute_checked(
1149                    syncing,
1150                    hel_targets::command_on_locator(
1151                        &backend,
1152                        session_id,
1153                        vec!["mkdir".into(), "-p".into(), worker_root.clone()],
1154                        "create the session worker root",
1155                    )?,
1156                )?;
1157            }
1158            let staging = tempfile::tempdir().context("create restore staging")?;
1159            let local_spec = staging.path().join("restore-spec.json");
1160            std::fs::write(&local_spec, serde_json::to_vec_pretty(&restore)?)?;
1161            // Two independent lanes into the target. The checkpoint transfer
1162            // needs nothing from the worker install, and the worker install
1163            // is independent of archive upload, so both run concurrently.
1164            let controller = &*self;
1165            let backend_ref = &backend;
1166            let worker_root_ref = worker_root.as_str();
1167            let local_spec_ref = local_spec.as_path();
1168            execute_concurrent_lanes(
1169                || {
1170                    let syncing = &StagedExecutor::new(executor, ProvisionStage::Syncing);
1171                    controller.prepare_worker_files(
1172                        session_id,
1173                        backend_ref,
1174                        worker_root_ref,
1175                        syncing,
1176                    )?;
1177                    super::provisioning::install_inherited_git_settings(
1178                        syncing,
1179                        backend_ref,
1180                        session_id,
1181                    )?;
1182                    Ok(())
1183                },
1184                || {
1185                    let restoring = &StagedExecutor::new(executor, ProvisionStage::Restoring);
1186                    if should_upload_restore_archive(&backend) {
1187                        upload_checkpoint_spec(
1188                            restoring,
1189                            backend_ref,
1190                            session_id,
1191                            &archive_path,
1192                            &remote_archive,
1193                        )?;
1194                    }
1195                    upload_checkpoint_spec(
1196                        restoring,
1197                        backend_ref,
1198                        session_id,
1199                        local_spec_ref,
1200                        &remote_spec,
1201                    )
1202                },
1203            )?;
1204            {
1205                let restoring = &StagedExecutor::new(executor, ProvisionStage::Restoring);
1206                execute_checked(
1207                    restoring,
1208                    restore_command(&backend, session_id, &remote_spec)?,
1209                )?;
1210            }
1211            {
1212                let syncing = &StagedExecutor::new(executor, ProvisionStage::Syncing);
1213                install_attached_resources(
1214                    &self.state,
1215                    session_id,
1216                    &backend,
1217                    &worker_root,
1218                    syncing,
1219                )?;
1220
1221            }
1222            match projection_build {
1223                Some(build) => {
1224                    let mut restored_projection = build
1225                        .await
1226                        .context("rebuild the restored projection")?
1227                        .context("rebuild the restored projection")?;
1228                    if discard_queued_prompts {
1229                        restored_projection.queued_prompts.clear();
1230                    }
1231                    hel::hel_database::save_materialized_session(&restored_projection)?;
1232                }
1233                // The stored projection already is the archived one. Only the
1234                // queue can still need changing.
1235                None if discard_queued_prompts => {
1236                    hel::hel_database::replace_materialized_queued_prompts(session_id, &[])?;
1237                }
1238                None => {}
1239            }
1240            let readiness_stage = bridge_readiness_stage(&profile);
1241            let spec = self.reconnect_command(session_id)?;
1242            let readiness = async {
1243                let mut relay = {
1244                    let _starting = ProvisionStageGuard::new(executor, ProvisionStage::Starting);
1245                    start_worker(executor, &backend, &worker_root)?;
1246                    connect_started_worker(&spec, session_id, executor, &backend, &worker_root)
1247                        .await?
1248                };
1249                let native_session_id =
1250                    wait_for_native_session_in_stage(&mut relay, executor, readiness_stage).await?;
1251                Ok::<_, anyhow::Error>((relay, native_session_id))
1252            }
1253            .await;
1254            let (mut relay, native_session_id) = readiness
1255                .map_err(|error| worker_probe_diagnosis(executor, &backend, &worker_root, error))?;
1256            if same_harness {
1257                if native_session_id != archive_manifest.session.native_session_id {
1258                    bail!(
1259                        "ACP loaded native session {native_session_id}, expected {}",
1260                        archive_manifest.session.native_session_id
1261                    );
1262                }
1263            } else {
1264                relay
1265                    .install_prompt_context(
1266                        utility_handoff
1267                            .clone()
1268                            .context("cross-harness resume has no utility-model handoff")?,
1269                    )
1270                    .await?;
1271                if !discard_queue {
1272                    for prompt in &canonical_session.queued_prompts {
1273                        // A queued configuration change is replayed as itself;
1274                        // rebuilding it as a prompt would send `/model x` to
1275                        // the agent as text.
1276                        let command = match &prompt.kind {
1277                            CanonicalQueuedCommandKind::Prompt => RelayCommand::Prompt {
1278                                prompt: prompt
1279                                    .content
1280                                    .iter()
1281                                    .cloned()
1282                                    .map(serde_json::from_value)
1283                                    .collect::<serde_json::Result<Vec<ContentBlock>>>()?,
1284                            },
1285                            CanonicalQueuedCommandKind::SetConfig { key, value } => {
1286                                RelayCommand::SetConfig {
1287                                    key: key.clone(),
1288                                    value: value.clone(),
1289                                }
1290                            }
1291                        };
1292                        relay.submit(prompt.command_id.clone(), command).await?;
1293                    }
1294                }
1295            }
1296            // Last, and only once the resume has otherwise succeeded: a failure
1297            // before this point rolls the record back to a session whose
1298            // worktree still has to be there.
1299            if let Some(worktree) = conversion
1300                .as_ref()
1301                .and_then(ResumeConversion::raw_to_workspace)
1302                .and_then(|plan| plan.retire.as_ref())
1303                && let Err(error) = retire_managed_worktree(executor, worktree)
1304            {
1305                tracing::warn!(
1306                    session_id,
1307                    worktree = %worktree.worktree_root.display(),
1308                    error = format!("{error:#}"),
1309                    "could not retire the old managed worktree after resume"
1310                );
1311                resume_notices.push(worktree_cleanup_notice(&worktree.worktree_root, &error));
1312            }
1313            for notice in &resume_notices {
1314                let submitted = async {
1315                    let command_id = new_command_id("resume-notice")?;
1316                    relay
1317                        .submit(
1318                            command_id,
1319                            RelayCommand::RecordNotice {
1320                                text: notice.clone(),
1321                            },
1322                        )
1323                        .await
1324                }
1325                .await;
1326                // The conversation line is a courtesy. A relay that refuses it
1327                // has not damaged the resume, so report and carry on.
1328                if let Err(error) = submitted {
1329                    tracing::warn!(
1330                        session_id,
1331                        error = format!("{error:#}"),
1332                        "could not record a resume notice in the conversation"
1333                    );
1334                }
1335            }
1336            self.mark_worker_connected(session_id, Some(native_session_id))?;
1337            Ok::<_, anyhow::Error>(relay.sync().await?.materialized)
1338        }
1339        .await;
1340        match result {
1341            Ok(materialized) => Ok(materialized),
1342            Err(error) => {
1343                // Put back whatever this resume could have written to the
1344                // durable projection. Both branches restore archived content,
1345                // so they are correct whether or not the write had happened
1346                // when the resume failed.
1347                if rebuild_projection {
1348                    match materialized_session_from_canonical(session_id, &canonical_session) {
1349                        Ok(previous_projection) => {
1350                            if let Err(restore_error) =
1351                                hel::hel_database::save_materialized_session(&previous_projection)
1352                            {
1353                                tracing::error!(
1354                                    session_id,
1355                                    error = format!("{restore_error:#}"),
1356                                    "could not restore the durable projection after resume failed"
1357                                );
1358                            }
1359                        }
1360                        Err(restore_error) => {
1361                            tracing::error!(
1362                                session_id,
1363                                error = format!("{restore_error:#}"),
1364                                "could not rebuild the durable projection after resume failed"
1365                            );
1366                        }
1367                    }
1368                } else if discard_queued_prompts
1369                    && let Err(restore_error) =
1370                        hel::hel_database::replace_materialized_queued_prompts(
1371                            session_id,
1372                            &hel::hel_projection::materialized_queued_prompts_from_canonical(
1373                                &canonical_session.queued_prompts,
1374                            ),
1375                        )
1376                {
1377                    tracing::error!(
1378                        session_id,
1379                        error = format!("{restore_error:#}"),
1380                        "could not restore queued prompts after resume failed"
1381                    );
1382                }
1383                Err(self.rollback_failed_resume(
1384                    session_id,
1385                    &previous,
1386                    recreated_managed_worktree,
1387                    error,
1388                    executor,
1389                )?)
1390            }
1391        }
1392    }
1393
1394    pub(super) fn rollback_failed_resume(
1395        &mut self,
1396        session_id: &str,
1397        previous: &SessionRecord,
1398        recreated_managed_worktree: bool,
1399        error: anyhow::Error,
1400        _executor: &impl CommandExecutor,
1401    ) -> Result<anyhow::Error> {
1402        let current = self
1403            .state
1404            .sessions
1405            .get(session_id)
1406            .with_context(|| format!("unknown session {session_id}"))?
1407            .clone();
1408        let cleanup = match current.target.as_ref() {
1409            Some(locator) => (|| -> Result<()> {
1410                let backend = backend_locator(locator, &current, &self.config)?;
1411                hel_targets::close_plan(&backend, session_id)?
1412                    // Use a fresh executor: cancellation applies to the
1413                    // requested operation, not to its compensating cleanup.
1414                    .execute(&CancellableProcessExecutor::with_timeout(
1415                        Duration::from_secs(15),
1416                    ))
1417                    .map(|_| ())
1418            })(),
1419            None => Ok(()),
1420        };
1421        // A failed target teardown may leave its harness writing. Keep its
1422        // checkout intact until a later retry proves the process is stopped.
1423        let worktree_cleanup = if cleanup.is_err() {
1424            Ok(())
1425        } else {
1426            match (
1427                current.managed_worktree.as_ref(),
1428                previous.managed_worktree.as_ref(),
1429            ) {
1430                (_, Some(previous)) if recreated_managed_worktree => retire_managed_worktree(
1431                    &CancellableProcessExecutor::with_timeout(Duration::from_secs(15)),
1432                    previous,
1433                ),
1434                (Some(current), Some(previous)) if current == previous => Ok(()),
1435                (Some(worktree), _) => cleanup_managed_worktree(
1436                    &CancellableProcessExecutor::with_timeout(Duration::from_secs(15)),
1437                    worktree,
1438                ),
1439                (None, _) => Ok(()),
1440            }
1441        };
1442        let cleanup_error = [cleanup, worktree_cleanup]
1443            .into_iter()
1444            .filter_map(Result::err)
1445            .map(|cleanup_error| format!("{cleanup_error:#}"))
1446            .collect::<Vec<_>>()
1447            .join("; ");
1448        if !cleanup_error.is_empty() {
1449            tracing::warn!(
1450                session_id,
1451                error = %cleanup_error,
1452                "resume rollback cleanup reported failures"
1453            );
1454        }
1455        let original = format!("{error:#}");
1456        let record = self.state.sessions.get_mut(session_id).unwrap();
1457        let failure = apply_failed_resume_rollback(
1458            record,
1459            previous,
1460            &original,
1461            (!cleanup_error.is_empty()).then_some(cleanup_error),
1462        );
1463        // A conversion filed the session's prompt history under its new bundle.
1464        // The record went back, so the history goes back with it.
1465        if record.bundle_id != current.bundle_id {
1466            let bundle_id = record.bundle_id.clone();
1467            hel::hel_database::rebind_session_bundle(session_id, &bundle_id)?;
1468        }
1469        // The rollback restores the record the resume replaced, attached
1470        // directories included, so it writes the whole row back.
1471        hel::hel_database::save_session(&self.state.sessions[session_id])?;
1472        Ok(failure)
1473    }
1474}
1475
1476fn worktree_cleanup_notice(worktree_root: &Path, error: &anyhow::Error) -> String {
1477    format!(
1478        "Mjolnir could not remove the worktree at {}: {error:#}. Remove it with `git worktree remove --force {}`.",
1479        worktree_root.display(),
1480        worktree_root.display()
1481    )
1482}
1483
1484pub(super) fn apply_failed_resume_rollback(
1485    current: &mut SessionRecord,
1486    previous: &SessionRecord,
1487    original_error: &str,
1488    cleanup_error: Option<String>,
1489) -> anyhow::Error {
1490    match cleanup_error {
1491        None => {
1492            *current = previous.clone();
1493            current.state = SessionState::Stopped;
1494            current.target = None;
1495            current.updated_at = now();
1496            current.last_error = Some(format!("resume failed: {original_error}"));
1497            anyhow::anyhow!(original_error.to_owned())
1498        }
1499        Some(cleanup_error) => {
1500            let failure = format!(
1501                "{original_error}; cleanup of the partial resume target failed: {cleanup_error}"
1502            );
1503            // Keep the exact partial target and checkout ownership until
1504            // cleanup succeeds; the harness may still be writing there.
1505            // A container conversion has no new managed host checkout, so
1506            // retain the original host checkout that it has not retired yet.
1507            if current.managed_worktree.is_none() {
1508                current
1509                    .project_directory
1510                    .clone_from(&previous.project_directory);
1511                current
1512                    .managed_worktree
1513                    .clone_from(&previous.managed_worktree);
1514                current.bundle_id.clone_from(&previous.bundle_id);
1515            }
1516            current.state = SessionState::Error;
1517            current.updated_at = now();
1518            current.last_error = Some(format!("resume failed: {failure}"));
1519            anyhow::anyhow!(failure)
1520        }
1521    }
1522}
1523
1524/// Whether a resume has to rebuild the durable projection from its archive.
1525///
1526/// The projection is a deterministic fold of the relay event chain, so a stored
1527/// projection standing at the archive's frontier *and* carrying the archive's
1528/// frontier digest already holds the archived content: same chain, same
1529/// ordinal, same result. Anything else - no stored row, a different ordinal, a
1530/// different digest, or a frontier that could not be read - rebuilds.
1531fn projection_rebuild_required(
1532    stored: Option<(u64, &str)>,
1533    archive_frontier: u64,
1534    archive_frontier_digest: &str,
1535) -> bool {
1536    stored != Some((archive_frontier, archive_frontier_digest))
1537}
1538
1539fn restore_archive_path(
1540    backend: &hel_targets::TargetLocator,
1541    verified_archive: &Path,
1542    remote_archive: &Path,
1543) -> PathBuf {
1544    if matches!(backend, hel_targets::TargetLocator::LocalBare { .. }) {
1545        verified_archive.to_path_buf()
1546    } else {
1547        remote_archive.to_path_buf()
1548    }
1549}
1550
1551fn should_upload_restore_archive(backend: &hel_targets::TargetLocator) -> bool {
1552    !matches!(backend, hel_targets::TargetLocator::LocalBare { .. })
1553}
1554
1555/// Provisioning currently performs its target plan synchronously inside an
1556/// async function. Run the network-bound cross-harness handoff on a joined
1557/// side runtime so it can make progress during that plan without borrowing
1558/// the mutable controller or leaving work behind on failure.
1559struct CrossHarnessProvisionExecutor<'a, E: CommandExecutor + ?Sized> {
1560    inner: &'a E,
1561    cancellation: CancellationToken,
1562}
1563
1564impl<E: CommandExecutor + ?Sized> CommandExecutor for CrossHarnessProvisionExecutor<'_, E> {
1565    fn execute(&self, command: &CommandSpec) -> Result<CommandOutput> {
1566        if self.cancellation.is_cancelled() {
1567            bail!("operation cancelled while provisioning destination");
1568        }
1569        self.inner.execute(command)
1570    }
1571
1572    fn cancellation_requested(&self) -> bool {
1573        self.cancellation.is_cancelled() || self.inner.cancellation_requested()
1574    }
1575
1576    fn stage_started(&self, stage: ProvisionStage) {
1577        self.inner.stage_started(stage);
1578    }
1579
1580    fn stage_finished(&self, stage: ProvisionStage) {
1581        self.inner.stage_finished(stage);
1582    }
1583
1584    fn notify_notice(&self, notice: &str) {
1585        self.inner.notify_notice(notice);
1586    }
1587
1588    fn execute_with_stdin(
1589        &self,
1590        command: &CommandSpec,
1591        input: &mut (dyn std::io::Read + Send),
1592    ) -> Result<CommandOutput> {
1593        if self.cancellation.is_cancelled() {
1594            bail!("operation cancelled while provisioning destination");
1595        }
1596        self.inner.execute_with_stdin(command, input)
1597    }
1598}
1599
1600fn provision_with_cross_harness_handoff(
1601    controller: &mut Controller,
1602    session_id: &str,
1603    executor: &(impl CommandExecutor + Sync),
1604    github_token: Option<&str>,
1605    config: &HelConfig,
1606    snapshot: &CanonicalSessionSnapshot,
1607    context_bytes: usize,
1608) -> Result<String> {
1609    let (_provision, handoff) = execute_joined_cross_harness_work(
1610        "cross-harness provisioning",
1611        move |cancellation| {
1612            let provision_executor = CrossHarnessProvisionExecutor {
1613                inner: executor,
1614                cancellation,
1615            };
1616            futures::executor::block_on(controller.provision_session_with_failure_disposition(
1617                session_id,
1618                &provision_executor,
1619                github_token,
1620                ProvisioningFailureDisposition::Preserve,
1621            ))
1622        },
1623        "cross-harness handoff",
1624        move |cancellation| {
1625            let runtime = tokio::runtime::Builder::new_current_thread()
1626                .enable_all()
1627                .build()
1628                .context("create cross-harness handoff runtime")?;
1629            runtime.block_on(utility_handoff_while_cancellable(
1630                session_id,
1631                config,
1632                snapshot,
1633                context_bytes,
1634                executor,
1635                cancellation,
1636            ))
1637        },
1638    )?;
1639    ensure!(
1640        !executor.cancellation_requested(),
1641        "operation cancelled while provisioning destination"
1642    );
1643    Ok(handoff)
1644}
1645
1646/// Run the two independent cross-harness lanes together, cancelling and
1647/// joining the peer as soon as either lane fails. The lane order is stable so
1648/// diagnostics do not depend on which worker happened to finish first.
1649fn execute_joined_cross_harness_work<A: Send, B: Send>(
1650    first_name: &'static str,
1651    first: impl FnOnce(CancellationToken) -> Result<A> + Send,
1652    second_name: &'static str,
1653    second: impl FnOnce(CancellationToken) -> Result<B> + Send,
1654) -> Result<(A, B)> {
1655    let cancellation = CancellationToken::new();
1656    std::thread::scope(|scope| {
1657        let first_cancel = cancellation.clone();
1658        let mut first_handle = Some(scope.spawn(move || first(first_cancel)));
1659        let second_cancel = cancellation.clone();
1660        let mut second_handle = Some(scope.spawn(move || second(second_cancel)));
1661        let mut first_result = None;
1662        let mut second_result = None;
1663
1664        while first_result.is_none() || second_result.is_none() {
1665            if first_result.is_none()
1666                && first_handle
1667                    .as_ref()
1668                    .is_some_and(|handle| handle.is_finished())
1669            {
1670                let handle = first_handle.take().expect("first lane handle present");
1671                first_result = Some(match handle.join() {
1672                    Ok(result) => result,
1673                    Err(panic) => {
1674                        cancellation.cancel();
1675                        Err(anyhow::anyhow!(
1676                            "{first_name} thread panicked: {}",
1677                            hel_targets::command_thread_panic_message(panic.as_ref())
1678                        ))
1679                    }
1680                });
1681                if first_result.as_ref().is_some_and(Result::is_err) {
1682                    cancellation.cancel();
1683                }
1684            }
1685            if second_result.is_none()
1686                && second_handle
1687                    .as_ref()
1688                    .is_some_and(|handle| handle.is_finished())
1689            {
1690                let handle = second_handle.take().expect("second lane handle present");
1691                second_result = Some(match handle.join() {
1692                    Ok(result) => result,
1693                    Err(panic) => {
1694                        cancellation.cancel();
1695                        Err(anyhow::anyhow!(
1696                            "{second_name} thread panicked: {}",
1697                            hel_targets::command_thread_panic_message(panic.as_ref())
1698                        ))
1699                    }
1700                });
1701                if second_result.as_ref().is_some_and(Result::is_err) {
1702                    cancellation.cancel();
1703                }
1704            }
1705            if first_result.is_none() || second_result.is_none() {
1706                std::thread::sleep(Duration::from_millis(10));
1707            }
1708        }
1709
1710        match (
1711            first_result.expect("first lane result received after joined handle"),
1712            second_result.expect("second lane result received after joined handle"),
1713        ) {
1714            (Err(first), Err(second)) => {
1715                Err(first.context(format!("{second_name} lane also failed: {second:#}")))
1716            }
1717            (Err(error), Ok(_)) => Err(error),
1718            (Ok(_), Err(error)) => Err(error),
1719            (Ok(first), Ok(second)) => Ok((first, second)),
1720        }
1721    })
1722}
1723
1724/// Discover a utility model and compact the cross-harness handoff while still
1725/// watching for cancellation. Discovery and compaction can both make several
1726/// network requests, so a cancelled resume must not wait them out.
1727async fn utility_handoff_while_cancellable(
1728    session_id: &str,
1729    config: &HelConfig,
1730    snapshot: &CanonicalSessionSnapshot,
1731    context_bytes: usize,
1732    executor: &impl CommandExecutor,
1733    cancellation: CancellationToken,
1734) -> Result<String> {
1735    let _phase = ResumePhaseTimer::new(session_id, "cross-harness handoff");
1736    if executor.cancellation_requested() {
1737        bail!("operation cancelled while compacting the cross-harness handoff");
1738    }
1739    let _compacting = ProvisionStageGuard::new(executor, ProvisionStage::Compacting);
1740    let cancel = cancellation.child_token();
1741    let operation = async {
1742        let candidates = crate::hel_utility_llm::UtilityLlmRuntime::shared()
1743            .resolve(config, &cancel)
1744            .await?;
1745        let backend =
1746            crate::hel_utility_llm::UtilityCompactionBackend::new(candidates, cancel.clone());
1747        // Pages are sized by what the summarizer can read; the handoff is
1748        // sized by what the target harness accepts. They are unrelated
1749        // numbers, and using the target's for both is what made one incident
1750        // shard a transcript into 33 pages.
1751        let budget = crate::hel_compaction::CompactionBudget {
1752            page_bytes: backend.page_bytes(),
1753            handoff_bytes: context_bytes,
1754        };
1755        crate::hel_compaction::compact_snapshot(snapshot, budget, &backend).await
1756    };
1757    tokio::pin!(operation);
1758    loop {
1759        tokio::select! {
1760            context = &mut operation => return context,
1761            _ = cancellation.cancelled() => {
1762                cancel.cancel();
1763                bail!("operation cancelled while compacting the cross-harness handoff");
1764            }
1765            _ = tokio::time::sleep(super::readiness::CANCELLATION_POLL_INTERVAL) => {
1766                if executor.cancellation_requested() {
1767                    cancel.cancel();
1768                    bail!("operation cancelled while compacting the cross-harness handoff");
1769                }
1770            }
1771        }
1772    }
1773}
1774
1775#[cfg(test)]
1776mod tests {
1777    use std::cell::RefCell;
1778    use std::collections::BTreeMap;
1779    use std::path::{Path, PathBuf};
1780    use std::process::Command;
1781    use std::sync::{Barrier, Mutex};
1782
1783    use anyhow::Result;
1784
1785    use crate::hel_controller::test_support::{
1786        checkpoint_test_session, committed_repository, managed_worktree_session,
1787        resume_compatibility_config, write_checkpoint_gate_archive,
1788    };
1789    use crate::hel_controller::{Controller, SessionResumeOptions};
1790    use hel::hel_archive::{GitCommandRunner, verify_archive_streaming};
1791    use hel::hel_config::{
1792        ContainerTemplate as ConfigContainer, HarnessProfile, HelConfig, ProjectBundle,
1793        ProjectRepository, TargetTemplate,
1794    };
1795    use hel::hel_projection::materialized_session_from_canonical;
1796    use hel::hel_state::{HelState, SessionRecord, SessionState, TargetLocator};
1797    use hel::hel_targets::{CommandExecutor, CommandOutput, CommandSpec, ProcessExecutor};
1798
1799    use super::*;
1800
1801    const RESUME_ROLLBACK_TEST_CHILD: &str = "MJ_RESUME_ROLLBACK_TEST_CHILD";
1802    const RETIRED_WORKTREE_RESUME_TEST_CHILD: &str = "MJ_RETIRED_WORKTREE_RESUME_TEST_CHILD";
1803    const WORKER_PREFLIGHT_TEST_CHILD: &str = "MJ_WORKER_PREFLIGHT_TEST_CHILD";
1804
1805    #[test]
1806    fn muse_resume_allows_workspace_relocation_before_provisioning() {
1807        let mut config = resume_compatibility_config();
1808        config
1809            .targets
1810            .insert("other-container".into(), config.targets["podman"].clone());
1811        let controller = Controller {
1812            config,
1813            state: HelState::default(),
1814        };
1815        let mut session = checkpoint_test_session("0123456789abcdef0123456789abcdef");
1816        session.harness_kind = HarnessKind::Muse;
1817        assert!(
1818            controller
1819                .validate_muse_resume_destination(&session, HarnessKind::Muse, "podman")
1820                .is_ok()
1821        );
1822        assert!(
1823            controller
1824                .validate_muse_resume_destination(&session, HarnessKind::Muse, "other-container")
1825                .is_ok()
1826        );
1827        controller
1828            .validate_muse_resume_destination(&session, HarnessKind::Muse, "ssh-bare")
1829            .unwrap();
1830        assert!(
1831            controller
1832                .validate_muse_resume_destination(&session, HarnessKind::Codex, "ssh-bare")
1833                .is_ok()
1834        );
1835        assert_eq!(session.state, SessionState::Running);
1836    }
1837
1838    /// Compaction costs minutes and paid model requests; resolving the worker
1839    /// binary is local and costs microseconds. A cross-harness resume that
1840    /// cannot produce a worker must say so before it compacts anything.
1841    #[test]
1842    fn a_resume_preflights_the_worker_binary_before_compacting() {
1843        // MJ_WORKER_BINARY, MJ_DATA_DIR, and MJ_CONFIG_DIR are process-global,
1844        // so run the half that sets them in an exact child test.
1845        if std::env::var_os(WORKER_PREFLIGHT_TEST_CHILD).is_none() {
1846            let directory = tempfile::tempdir().unwrap();
1847            let test_name = format!(
1848                "{}::a_resume_preflights_the_worker_binary_before_compacting",
1849                module_path!()
1850                    .strip_prefix("mj_controller::")
1851                    .unwrap_or(module_path!())
1852            );
1853            let output = Command::new(std::env::current_exe().unwrap())
1854                .args(["--exact", &test_name, "--nocapture"])
1855                .env(WORKER_PREFLIGHT_TEST_CHILD, "1")
1856                .env("MJ_DATA_DIR", directory.path().join("data"))
1857                .env("MJ_CONFIG_DIR", directory.path().join("config"))
1858                // Names a worker binary that is not there, which is how a
1859                // machine without an installed worker fails the same lookup.
1860                .env("MJ_WORKER_BINARY", directory.path().join("absent-worker"))
1861                .output()
1862                .unwrap();
1863            assert!(
1864                output.status.success(),
1865                "isolated worker preflight test failed\nstdout:\n{}\nstderr:\n{}",
1866                String::from_utf8_lossy(&output.stdout),
1867                String::from_utf8_lossy(&output.stderr)
1868            );
1869            return;
1870        }
1871        // Alone in this child process, so it installs the one writer.
1872        let _writer = hel::hel_database::install_isolated_test_writer();
1873
1874        let data_directory = PathBuf::from(std::env::var_os("MJ_DATA_DIR").unwrap());
1875        let archive_directory = data_directory.join("archives");
1876        std::fs::create_dir_all(&archive_directory).unwrap();
1877        let session_id = "0123456789abcdef0123456789abcdef";
1878        let checkpoint = write_checkpoint_gate_archive(&archive_directory, session_id, 7);
1879        let repository = committed_repository();
1880        let mut session = managed_worktree_session(repository.path(), session_id);
1881        session.checkpoint = Some(checkpoint);
1882
1883        let profile_home = data_directory.join("profile");
1884        std::fs::create_dir_all(&profile_home).unwrap();
1885        let mut config = resume_compatibility_config();
1886        // The archive was written by Codex, so resuming onto Claude is a
1887        // cross-harness resume and would compact the transcript.
1888        config.profiles.insert(
1889            "claude".into(),
1890            HarnessProfile {
1891                enabled: true,
1892                kind: hel::hel_config::HarnessKind::Claude,
1893                home: profile_home,
1894                environment: BTreeMap::new(),
1895                context_window_bytes: None,
1896            },
1897        );
1898        let mut controller = Controller {
1899            config,
1900            state: HelState {
1901                sessions: BTreeMap::from([(session_id.into(), session)]),
1902                ..HelState::default()
1903            },
1904        };
1905        hel::hel_database::save_state(&controller.state).unwrap();
1906
1907        let error = tokio::runtime::Builder::new_current_thread()
1908            .enable_all()
1909            .build()
1910            .unwrap()
1911            .block_on(controller.resume_session_controlled(
1912                session_id,
1913                "claude",
1914                "local-bare",
1915                SessionResumeOptions {
1916                    additional_mounts: None,
1917                    resource_allocation: None,
1918                    discard_queue: false,
1919                },
1920                &ProcessExecutor,
1921            ))
1922            .unwrap_err();
1923
1924        let detail = format!("{error:#}");
1925        assert!(
1926            detail.contains("preflight the worker binary before resuming"),
1927            "{detail}"
1928        );
1929        assert!(detail.contains("absent-worker"), "{detail}");
1930        assert!(
1931            !detail.contains("compact the cross-harness handoff transcript"),
1932            "compaction must not run for a resume that cannot install a worker: {detail}"
1933        );
1934        assert_eq!(
1935            controller.state.sessions[session_id].state,
1936            SessionState::Stopped
1937        );
1938    }
1939
1940    #[test]
1941    fn network_resume_ignores_host_history_but_an_explicit_raw_move_checks_it() {
1942        let directory = tempfile::tempdir().unwrap();
1943        let repository = committed_repository();
1944        let session_id = "0123456789abcdef0123456789abcdef";
1945        let mut session = checkpoint_test_session(session_id);
1946        session.state = SessionState::Stopped;
1947        session.checkpoint = Some(
1948            super::super::test_support::write_network_checkpoint_archive(
1949                directory.path(),
1950                session_id,
1951                0,
1952            ),
1953        );
1954        let mut config = resume_compatibility_config();
1955        config.bundles.insert(
1956            "project".into(),
1957            super::super::test_support::local_bundle(repository.path()),
1958        );
1959        let controller = Controller {
1960            config,
1961            state: HelState {
1962                sessions: BTreeMap::from([(session_id.into(), session)]),
1963                ..HelState::default()
1964            },
1965        };
1966        assert!(matches!(
1967            controller
1968                .preflight_resume_repository_sources(session_id, "podman", &ProcessExecutor,)
1969                .unwrap(),
1970            ResumeRepositorySourcePreflight::Ready(_)
1971        ));
1972        let result = controller
1973            .preflight_resume_repository_sources(session_id, "local-bare", &ProcessExecutor)
1974            .unwrap();
1975        let ResumeRepositorySourcePreflight::RepositoryMoved(mismatch) = result else {
1976            panic!("moving into a host checkout must detect its missing archive base");
1977        };
1978        assert_eq!(mismatch.missing_commit, "a".repeat(40));
1979        assert!(!repository.path().join(".mj/worktrees").exists());
1980    }
1981
1982    #[test]
1983    fn raw_in_place_preflight_does_not_require_its_synthetic_bundle() {
1984        struct UnusedExecutor;
1985
1986        impl CommandExecutor for UnusedExecutor {
1987            fn execute(&self, command: &CommandSpec) -> Result<CommandOutput> {
1988                panic!("raw in-place preflight ran {}", command.purpose);
1989            }
1990        }
1991
1992        let directory = tempfile::tempdir().unwrap();
1993        let session_id = "0123456789abcdef0123456789abcdef";
1994        let mut session = checkpoint_test_session(session_id);
1995        session.checkpoint = Some(write_checkpoint_gate_archive(
1996            directory.path(),
1997            session_id,
1998            3,
1999        ));
2000        session.bundle_id = "remote-project-a66373eef659f856".into();
2001        session.target_template_id = "localhost".into();
2002        session.project_directory = Some("/mnt/optane/bifrost-fird".into());
2003        let controller = Controller {
2004            config: HelConfig {
2005                targets: BTreeMap::from([("localhost".into(), TargetTemplate::LocalBare)]),
2006                // The raw checkout is still usable even though its synthetic
2007                // grouping bundle has disappeared from the config.
2008                bundles: BTreeMap::new(),
2009                ..HelConfig::default()
2010            },
2011            state: HelState {
2012                sessions: BTreeMap::from([(session_id.into(), session)]),
2013                ..HelState::default()
2014            },
2015        };
2016
2017        let preflight = controller
2018            .preflight_resume_repository_sources(session_id, "localhost", &UnusedExecutor)
2019            .unwrap();
2020        let ResumeRepositorySourcePreflight::Ready(receipt) = preflight else {
2021            panic!("raw in-place resume unexpectedly needs a repository replacement");
2022        };
2023        assert!(controller.repository_source_receipt_is_current(session_id, &receipt));
2024    }
2025
2026    #[test]
2027    fn repository_preflight_distinguishes_the_original_source_from_a_reused_name() {
2028        fn git(repository: &Path, arguments: &[&str]) {
2029            let output = SystemGit
2030                .run(
2031                    repository,
2032                    &hel::hel_archive::GitCommand {
2033                        arguments: arguments.iter().map(std::ffi::OsString::from).collect(),
2034                        stdin: Vec::new(),
2035                        env: Vec::new(),
2036                    },
2037                )
2038                .unwrap();
2039            assert_eq!(
2040                output.status,
2041                0,
2042                "git {arguments:?}: {}",
2043                String::from_utf8_lossy(&output.stderr)
2044            );
2045        }
2046
2047        let directory = tempfile::tempdir().unwrap();
2048        let origin = directory.path().join("original");
2049        std::fs::create_dir(&origin).unwrap();
2050        git(&origin, &["init", "-q", "-b", "main"]);
2051        git(&origin, &["config", "user.name", "Hel Test"]);
2052        git(&origin, &["config", "user.email", "hel@example.test"]);
2053        git(&origin, &["commit", "--allow-empty", "-qm", "base"]);
2054        let source = directory.path().join("source");
2055        git(
2056            directory.path(),
2057            &["clone", "-q", origin.to_str().unwrap(), "source"],
2058        );
2059        git(&source, &["config", "user.name", "Hel Test"]);
2060        git(&source, &["config", "user.email", "hel@example.test"]);
2061        git(&source, &["commit", "--allow-empty", "-qm", "session"]);
2062        let snapshot = hel::hel_archive::collect_git_snapshot(
2063            &SystemGit,
2064            &source,
2065            &hel::hel_archive::GitCollectionSpec {
2066                id: "project".into(),
2067                relative_destination: "project".into(),
2068                history: hel::hel_archive::GitHistoryMode::SessionDelta,
2069                origin_override: None,
2070            },
2071        )
2072        .unwrap();
2073        let configured = ProjectRepository {
2074            id: "project".into(),
2075            github: None,
2076            local: Some(origin.clone()),
2077            destination: "project".into(),
2078            git_ref: None,
2079        };
2080        assert_eq!(
2081            checkpoint_source_missing_commit(
2082                &configured,
2083                &CheckpointRepositoryBundle {
2084                    metadata: snapshot.metadata.clone(),
2085                    committed_bundle: snapshot.committed_bundle.clone(),
2086                },
2087                &ProcessExecutor,
2088                None,
2089            )
2090            .unwrap(),
2091            None
2092        );
2093
2094        let replacement = directory.path().join("replacement");
2095        std::fs::create_dir(&replacement).unwrap();
2096        git(&replacement, &["init", "-q", "-b", "main"]);
2097        git(&replacement, &["config", "user.name", "Hel Test"]);
2098        git(&replacement, &["config", "user.email", "hel@example.test"]);
2099        git(
2100            &replacement,
2101            &["commit", "--allow-empty", "-qm", "different history"],
2102        );
2103        let configured = ProjectRepository {
2104            local: Some(replacement),
2105            ..configured
2106        };
2107        assert!(
2108            checkpoint_source_missing_commit(
2109                &configured,
2110                &CheckpointRepositoryBundle {
2111                    metadata: snapshot.metadata,
2112                    committed_bundle: snapshot.committed_bundle,
2113                },
2114                &ProcessExecutor,
2115                None,
2116            )
2117            .unwrap()
2118            .is_some()
2119        );
2120    }
2121
2122    #[test]
2123    fn repository_preflight_checks_independent_sources_concurrently_and_receipts_are_scoped() {
2124        struct ConcurrentSourceExecutor {
2125            source_checks: Barrier,
2126        }
2127
2128        impl CommandExecutor for ConcurrentSourceExecutor {
2129            fn execute(&self, command: &CommandSpec) -> Result<CommandOutput> {
2130                if command.purpose == "check checkpoint base commit" {
2131                    self.source_checks.wait();
2132                }
2133                Ok(CommandOutput {
2134                    status: 0,
2135                    stdout: Vec::new(),
2136                    stderr: Vec::new(),
2137                })
2138            }
2139        }
2140
2141        let directory = tempfile::tempdir().unwrap();
2142        let session_id = "0123456789abcdef0123456789abcdef";
2143        let checkpoint = write_checkpoint_gate_archive(directory.path(), session_id, 3);
2144        let repositories = ["one", "two"]
2145            .map(|id| ProjectRepository {
2146                id: id.into(),
2147                github: None,
2148                local: Some(PathBuf::from(format!("/origin/{id}"))),
2149                destination: id.into(),
2150                git_ref: None,
2151            })
2152            .to_vec();
2153        let mut session = checkpoint_test_session(session_id);
2154        session.checkpoint = Some(checkpoint.clone());
2155        let mut controller = Controller {
2156            config: HelConfig {
2157                bundles: BTreeMap::from([(
2158                    session.bundle_id.clone(),
2159                    ProjectBundle {
2160                        primary_repo: "one".into(),
2161                        repositories: repositories.clone(),
2162                    },
2163                )]),
2164                ..HelConfig::default()
2165            },
2166            state: HelState {
2167                sessions: BTreeMap::from([(session_id.into(), session)]),
2168                ..HelState::default()
2169            },
2170        };
2171        let verified = ResumeRepositoryBundles {
2172            checkpoint_sha256: checkpoint.sha256,
2173            repositories: repositories
2174                .iter()
2175                .map(|repository| CheckpointRepositoryBundle {
2176                    metadata: hel::hel_archive::RepositoryMetadata {
2177                        push_urls: Vec::new(),
2178                        remote_workspace: false,
2179                        id: repository.id.clone(),
2180                        relative_destination: repository.destination.clone(),
2181                        origin: repository.source_label(),
2182                        base_commit: String::new(),
2183                        head_commit: if repository.id == "one" {
2184                            "a".repeat(40)
2185                        } else {
2186                            "b".repeat(40)
2187                        },
2188                        branch: Some("main".into()),
2189                    },
2190                    committed_bundle: Vec::new(),
2191                })
2192                .collect(),
2193        };
2194        let executor = ConcurrentSourceExecutor {
2195            source_checks: Barrier::new(2),
2196        };
2197        let pool = rayon::ThreadPoolBuilder::new()
2198            .num_threads(2)
2199            .build()
2200            .unwrap();
2201        let preflight = pool
2202            .install(|| {
2203                controller.preflight_verified_repository_sources(
2204                    session_id, verified, None, false, &executor,
2205                )
2206            })
2207            .unwrap();
2208        let ResumeRepositorySourcePreflight::Ready(receipt) = preflight else {
2209            panic!("expected repository source receipt");
2210        };
2211        assert!(controller.repository_source_receipt_is_current(session_id, &receipt));
2212
2213        controller
2214            .config
2215            .bundles
2216            .values_mut()
2217            .next()
2218            .unwrap()
2219            .repositories[0]
2220            .local = Some(PathBuf::from("/different-origin"));
2221        assert!(!controller.repository_source_receipt_is_current(session_id, &receipt));
2222    }
2223
2224    #[test]
2225    fn repository_preflight_checks_declared_boundary_without_importing_delta_bundle() {
2226        struct RecordingExecutor {
2227            commands: Mutex<Vec<CommandSpec>>,
2228        }
2229
2230        impl CommandExecutor for RecordingExecutor {
2231            fn execute(&self, command: &CommandSpec) -> Result<CommandOutput> {
2232                self.commands.lock().unwrap().push(command.clone());
2233                Ok(CommandOutput {
2234                    status: 0,
2235                    stdout: Vec::new(),
2236                    stderr: Vec::new(),
2237                })
2238            }
2239        }
2240
2241        let prerequisite = "a".repeat(40);
2242        let head = "b".repeat(40);
2243        let archived = CheckpointRepositoryBundle {
2244            metadata: hel::hel_archive::RepositoryMetadata {
2245                push_urls: Vec::new(),
2246                remote_workspace: false,
2247                id: "project".into(),
2248                relative_destination: "project".into(),
2249                origin: "https://github.com/archived/should-not-be-contacted.git".into(),
2250                base_commit: prerequisite.clone(),
2251                head_commit: head.clone(),
2252                branch: Some("main".into()),
2253            },
2254            committed_bundle: format!(
2255                "# v2 git bundle\n-{prerequisite} base\n{head} HEAD\n\nPACKnot-read"
2256            )
2257            .into_bytes(),
2258        };
2259        let configured = ProjectRepository {
2260            id: "project".into(),
2261            github: Some("configured/project".into()),
2262            local: None,
2263            destination: "project".into(),
2264            git_ref: None,
2265        };
2266        let executor = RecordingExecutor {
2267            commands: Mutex::new(Vec::new()),
2268        };
2269
2270        assert_eq!(
2271            checkpoint_source_missing_commit(
2272                &configured,
2273                &archived,
2274                &executor,
2275                Some("secret-token")
2276            )
2277            .unwrap(),
2278            None
2279        );
2280
2281        let commands = executor.commands.into_inner().unwrap();
2282        assert_eq!(commands.len(), 2, "commands: {commands:?}");
2283        assert_eq!(
2284            commands
2285                .iter()
2286                .map(|command| command.purpose.as_str())
2287                .collect::<Vec<_>>(),
2288            [
2289                "initialize repository source preflight",
2290                "check checkpoint base commit"
2291            ]
2292        );
2293        let source_check = &commands[1];
2294        assert!(
2295            source_check
2296                .args
2297                .iter()
2298                .any(|argument| argument == "credential.helper=")
2299        );
2300        assert_eq!(
2301            source_check
2302                .env
2303                .get("GIT_NO_LAZY_FETCH")
2304                .map(String::as_str),
2305            Some("1")
2306        );
2307        assert_eq!(
2308            source_check
2309                .env
2310                .get("GIT_TERMINAL_PROMPT")
2311                .map(String::as_str),
2312            Some("0")
2313        );
2314        assert_eq!(
2315            source_check.args.last().map(String::as_str),
2316            Some(prerequisite.as_str())
2317        );
2318        assert!(
2319            !source_check
2320                .args
2321                .iter()
2322                .any(|argument| argument.contains("archived"))
2323        );
2324    }
2325
2326    #[test]
2327    fn self_contained_bundle_validation_cannot_lazy_fetch_or_prompt() {
2328        let command = checkpoint_bundle_import_command(
2329            Path::new("/tmp/repository.git"),
2330            Path::new("/tmp/checkpoint.bundle"),
2331        );
2332        assert_eq!(
2333            command.env.get("GIT_NO_LAZY_FETCH").map(String::as_str),
2334            Some("1")
2335        );
2336        assert_eq!(
2337            command.env.get("GIT_TERMINAL_PROMPT").map(String::as_str),
2338            Some("0")
2339        );
2340    }
2341
2342    #[test]
2343    fn lost_bundle_sessions_reach_resume_compatibility_before_the_record_changes() {
2344        struct UnusedExecutor;
2345
2346        impl CommandExecutor for UnusedExecutor {
2347            fn execute(&self, command: &CommandSpec) -> Result<CommandOutput> {
2348                panic!("resume ran {} before rejecting the target", command.program);
2349            }
2350        }
2351
2352        let directory = tempfile::tempdir().unwrap();
2353        let session_id = "0123456789abcdef0123456789abcdef";
2354        let checkpoint = write_checkpoint_gate_archive(directory.path(), session_id, 3);
2355        let mut session = checkpoint_test_session(session_id);
2356        session.state = SessionState::Lost;
2357        session.checkpoint = Some(checkpoint);
2358        let previous = session.clone();
2359        let profile_home = directory.path().join("profile");
2360        std::fs::create_dir_all(&profile_home).unwrap();
2361        let mut config = HelConfig::default();
2362        config.profiles.insert(
2363            "codex".into(),
2364            HarnessProfile {
2365                enabled: true,
2366                kind: hel::hel_config::HarnessKind::Codex,
2367                home: profile_home,
2368                environment: BTreeMap::new(),
2369                context_window_bytes: None,
2370            },
2371        );
2372        config
2373            .targets
2374            .insert("localhost".into(), TargetTemplate::LocalBare);
2375        let mut controller = Controller {
2376            config,
2377            state: HelState {
2378                sessions: BTreeMap::from([(session_id.into(), session)]),
2379                ..HelState::default()
2380            },
2381        };
2382
2383        let error = tokio::runtime::Builder::new_current_thread()
2384            .enable_all()
2385            .build()
2386            .unwrap()
2387            .block_on(controller.resume_session_controlled(
2388                session_id,
2389                "codex",
2390                "localhost",
2391                SessionResumeOptions {
2392                    additional_mounts: None,
2393                    resource_allocation: None,
2394                    discard_queue: false,
2395                },
2396                &UnusedExecutor,
2397            ))
2398            .unwrap_err();
2399
2400        let detail = format!("{error:#}");
2401        assert!(detail.contains("created from a project bundle"), "{detail}");
2402        assert!(
2403            detail.contains("resume it on a container, SSH, or EC2 target"),
2404            "{detail}"
2405        );
2406        assert_eq!(controller.state.sessions[session_id], previous);
2407    }
2408    /// Records what it ran and blocks every command on a barrier sized to
2409    /// both lanes, so a run only finishes if the second lane started before
2410    /// the first one's command returned.
2411    struct BarrierExecutor {
2412        seen: Mutex<Vec<String>>,
2413        barrier: Barrier,
2414    }
2415
2416    impl CommandExecutor for BarrierExecutor {
2417        fn execute(&self, command: &CommandSpec) -> Result<CommandOutput> {
2418            self.seen.lock().unwrap().push(command.purpose.clone());
2419            self.barrier.wait();
2420            Ok(CommandOutput {
2421                status: 0,
2422                stdout: Vec::new(),
2423                stderr: Vec::new(),
2424            })
2425        }
2426    }
2427
2428    fn lane_command(purpose: &str) -> CommandSpec {
2429        CommandSpec::new("hel", ["worker"]).purpose(purpose)
2430    }
2431
2432    /// Launch progress must not claim "Start" while the target is still
2433    /// receiving the worker binary, the checkpoint archive and the restore.
2434    /// Everything before the daemon launch reports as Sync; the launch itself
2435    /// names its own stage, so a Sync-labelled executor cannot relabel it.
2436    #[test]
2437    fn start_begins_at_the_worker_launch_not_at_the_transfers_before_it() {
2438        struct RecordingExecutor {
2439            commands: RefCell<Vec<CommandSpec>>,
2440        }
2441        impl CommandExecutor for RecordingExecutor {
2442            fn execute(&self, command: &CommandSpec) -> Result<CommandOutput> {
2443                self.commands.borrow_mut().push(command.clone());
2444                Ok(CommandOutput {
2445                    status: 0,
2446                    stdout: Vec::new(),
2447                    stderr: Vec::new(),
2448                })
2449            }
2450        }
2451
2452        let session_id = "0123456789abcdef0123456789abcdef";
2453        let worker_root = format!("/var/lib/hel/workers/{session_id}");
2454        let executor = RecordingExecutor {
2455            commands: RefCell::new(Vec::new()),
2456        };
2457        let syncing = StagedExecutor::new(&executor, ProvisionStage::Syncing);
2458        let backend = hel_targets::TargetLocator::LocalPodman {
2459            container_id: "abcdef0123456789".into(),
2460            workspace_storage: Default::default(),
2461        };
2462
2463        upload_checkpoint_spec(
2464            &syncing,
2465            &backend,
2466            session_id,
2467            Path::new("/archives/session.hel.zip"),
2468            &format!("{worker_root}/restore.hel.zip"),
2469        )
2470        .unwrap();
2471        execute_checked(
2472            &syncing,
2473            restore_command(
2474                &backend,
2475                session_id,
2476                &format!("{worker_root}/restore-spec.json"),
2477            )
2478            .unwrap(),
2479        )
2480        .unwrap();
2481        // Deliberately run the launch through the Sync-labelled executor: it
2482        // must still report Start.
2483        start_worker(&syncing, &backend, &worker_root).unwrap();
2484
2485        let stages = executor
2486            .commands
2487            .borrow()
2488            .iter()
2489            .map(|command| (command.purpose.clone(), command.stage))
2490            .collect::<Vec<_>>();
2491        assert_eq!(
2492            stages,
2493            vec![
2494                (
2495                    "upload checkpoint specification".to_owned(),
2496                    Some(ProvisionStage::Syncing)
2497                ),
2498                (
2499                    "restore target checkpoint".to_owned(),
2500                    Some(ProvisionStage::Syncing)
2501                ),
2502                (
2503                    "start detached Mjolnir worker".to_owned(),
2504                    Some(ProvisionStage::Starting)
2505                ),
2506            ]
2507        );
2508    }
2509    #[test]
2510    fn independent_target_lanes_run_at_the_same_time() {
2511        let executor = BarrierExecutor {
2512            seen: Mutex::new(Vec::new()),
2513            barrier: Barrier::new(2),
2514        };
2515
2516        execute_concurrent_lanes(
2517            || execute_checked(&executor, lane_command("install the worker")).map(|_| ()),
2518            || execute_checked(&executor, lane_command("upload the checkpoint")).map(|_| ()),
2519        )
2520        .unwrap();
2521
2522        let mut seen = executor.seen.into_inner().unwrap();
2523        seen.sort();
2524        assert_eq!(seen, ["install the worker", "upload the checkpoint"]);
2525    }
2526    #[test]
2527    fn a_lane_failure_is_reported_in_lane_order_and_never_abandons_the_other_lane() {
2528        let reached = Mutex::new(Vec::new());
2529
2530        // The first lane fails slowly and the second immediately, so a
2531        // completion-order report could only pick the second.
2532        let error = execute_concurrent_lanes(
2533            || -> Result<()> {
2534                std::thread::sleep(Duration::from_millis(50));
2535                bail!("worker install failed")
2536            },
2537            || -> Result<()> {
2538                reached.lock().unwrap().push("second");
2539                bail!("checkpoint upload failed")
2540            },
2541        )
2542        .unwrap_err();
2543
2544        assert_eq!(error.to_string(), "worker install failed");
2545        assert_eq!(
2546            *reached.lock().unwrap(),
2547            ["second"],
2548            "a failing first lane must not cut the second one short"
2549        );
2550
2551        let error = execute_concurrent_lanes(
2552            || Ok(()),
2553            || -> Result<()> { bail!("checkpoint upload failed") },
2554        )
2555        .unwrap_err();
2556        assert_eq!(error.to_string(), "checkpoint upload failed");
2557    }
2558
2559    #[test]
2560    fn cross_harness_lanes_prove_overlap_with_handshake_channels() {
2561        let (provision_started_tx, provision_started_rx) = std::sync::mpsc::sync_channel::<()>(1);
2562        let (handoff_started_tx, handoff_started_rx) = std::sync::mpsc::sync_channel::<()>(1);
2563        let (provision_seen_handoff_tx, provision_seen_handoff_rx) =
2564            std::sync::mpsc::sync_channel::<()>(1);
2565        let (handoff_seen_provision_tx, handoff_seen_provision_rx) =
2566            std::sync::mpsc::sync_channel::<()>(1);
2567
2568        execute_joined_cross_harness_work(
2569            "provision",
2570            move |_cancellation| -> Result<()> {
2571                provision_started_tx
2572                    .send(())
2573                    .map_err(|error| anyhow::anyhow!("signal provisioning start: {error}"))?;
2574                handoff_started_rx
2575                    .recv_timeout(Duration::from_secs(2))
2576                    .map_err(|error| anyhow::anyhow!("wait for handoff start: {error}"))?;
2577                provision_seen_handoff_tx
2578                    .send(())
2579                    .map_err(|error| anyhow::anyhow!("signal provisioning overlap: {error}"))?;
2580                Ok(())
2581            },
2582            "handoff",
2583            move |_cancellation| -> Result<()> {
2584                handoff_started_tx
2585                    .send(())
2586                    .map_err(|error| anyhow::anyhow!("signal handoff start: {error}"))?;
2587                provision_started_rx
2588                    .recv_timeout(Duration::from_secs(2))
2589                    .map_err(|error| anyhow::anyhow!("wait for provisioning start: {error}"))?;
2590                handoff_seen_provision_tx
2591                    .send(())
2592                    .map_err(|error| anyhow::anyhow!("signal handoff overlap: {error}"))?;
2593                Ok(())
2594            },
2595        )
2596        .unwrap();
2597
2598        assert!(provision_seen_handoff_rx.recv().is_ok());
2599        assert!(handoff_seen_provision_rx.recv().is_ok());
2600    }
2601
2602    #[test]
2603    fn cross_harness_lane_failure_cancels_and_joins_the_peer() {
2604        let (handoff_started_tx, handoff_started_rx) = std::sync::mpsc::sync_channel::<()>(1);
2605        let (handoff_joined_tx, handoff_joined_rx) = std::sync::mpsc::sync_channel::<()>(1);
2606
2607        let error = execute_joined_cross_harness_work(
2608            "provision",
2609            move |_cancellation| -> Result<()> {
2610                handoff_started_rx
2611                    .recv_timeout(Duration::from_secs(2))
2612                    .map_err(|error| anyhow::anyhow!("wait for handoff start: {error}"))?;
2613                bail!("provisioning failed after handoff started");
2614            },
2615            "handoff",
2616            move |cancellation| -> Result<()> {
2617                handoff_started_tx
2618                    .send(())
2619                    .map_err(|error| anyhow::anyhow!("signal handoff start: {error}"))?;
2620                let runtime = tokio::runtime::Builder::new_current_thread()
2621                    .enable_all()
2622                    .build()
2623                    .map_err(|error| {
2624                        anyhow::anyhow!("create cancellation test runtime: {error}")
2625                    })?;
2626                runtime
2627                    .block_on(async {
2628                        tokio::time::timeout(Duration::from_secs(2), cancellation.cancelled()).await
2629                    })
2630                    .map_err(|error| anyhow::anyhow!("peer was not cancelled: {error}"))?;
2631                handoff_joined_tx
2632                    .send(())
2633                    .map_err(|error| anyhow::anyhow!("signal handoff join: {error}"))?;
2634                Ok(())
2635            },
2636        )
2637        .unwrap_err();
2638
2639        assert_eq!(
2640            error.to_string(),
2641            "provisioning failed after handoff started"
2642        );
2643        assert!(handoff_joined_rx.recv().is_ok());
2644    }
2645
2646    #[test]
2647    fn a_projection_standing_at_the_archived_frontier_is_reused() {
2648        let digest = "a".repeat(64);
2649        let other = "b".repeat(64);
2650
2651        assert!(!projection_rebuild_required(
2652            Some((82_000, &digest)),
2653            82_000,
2654            &digest
2655        ));
2656
2657        for stored in [
2658            // Same ordinal, different event chain.
2659            Some((82_000, other.as_str())),
2660            // Behind the archive, and ahead of it.
2661            Some((81_999, digest.as_str())),
2662            Some((82_001, digest.as_str())),
2663            // No projection stored, or none that could be read.
2664            None,
2665        ] {
2666            assert!(
2667                projection_rebuild_required(stored, 82_000, &digest),
2668                "{stored:?} must not be mistaken for the archived projection"
2669            );
2670        }
2671    }
2672
2673    #[test]
2674    fn local_bare_restore_reuses_verified_absolute_archive_without_upload() {
2675        let archive = Path::new("/var/lib/hel/archives/session.hel.zip");
2676        let remote = Path::new("/var/lib/hel/workers/session/restore.hel.zip");
2677        let local = hel_targets::TargetLocator::LocalBare {
2678            worker_root: "/var/lib/hel/workers/session".into(),
2679        };
2680        let container = hel_targets::TargetLocator::LocalPodman {
2681            container_id: "container".into(),
2682            workspace_storage: Default::default(),
2683        };
2684
2685        assert_eq!(restore_archive_path(&local, archive, remote), archive);
2686        assert!(!should_upload_restore_archive(&local));
2687        assert_eq!(restore_archive_path(&container, archive, remote), remote);
2688        assert!(should_upload_restore_archive(&container));
2689    }
2690
2691    #[test]
2692    fn cross_harness_provision_cancellation_stops_the_next_command() {
2693        struct RecordingExecutor {
2694            commands: Mutex<Vec<String>>,
2695        }
2696
2697        impl CommandExecutor for RecordingExecutor {
2698            fn execute(&self, command: &CommandSpec) -> Result<CommandOutput> {
2699                self.commands.lock().unwrap().push(command.purpose.clone());
2700                Ok(CommandOutput {
2701                    status: 0,
2702                    stdout: Vec::new(),
2703                    stderr: Vec::new(),
2704                })
2705            }
2706        }
2707
2708        let inner = RecordingExecutor {
2709            commands: Mutex::new(Vec::new()),
2710        };
2711        let cancellation = CancellationToken::new();
2712        let provision = CrossHarnessProvisionExecutor {
2713            inner: &inner,
2714            cancellation: cancellation.clone(),
2715        };
2716        let command = CommandSpec::new("hel", ["worker"]).purpose("provision target");
2717        provision.execute(&command).unwrap();
2718        cancellation.cancel();
2719
2720        let error = provision.execute(&command).unwrap_err();
2721        assert!(error.to_string().contains("cancelled while provisioning"));
2722        assert_eq!(
2723            inner.commands.lock().unwrap().as_slice(),
2724            ["provision target"]
2725        );
2726    }
2727
2728    #[test]
2729    fn failed_resume_rolls_back_only_after_target_cleanup() {
2730        let previous = SessionRecord {
2731            workspace_id: hel::hel_workspace::DEFAULT_WORKSPACE_ID.to_owned(),
2732            archived: false,
2733            container_cpus: None,
2734            container_memory: None,
2735            id: "0123456789abcdef0123456789abcdef".into(),
2736            title: "imported session".into(),
2737            harness_kind: hel::hel_config::HarnessKind::Codex,
2738            last_profile: "codex-old".into(),
2739            bundle_id: "project".into(),
2740            project_directory: None,
2741            managed_worktree: None,
2742            target_template_id: "podman-old".into(),
2743            resource_allocation: None,
2744            additional_mounts: Vec::new(),
2745            state: SessionState::Stopped,
2746            target: None,
2747            native_session_id: Some("native-session".into()),
2748            acp_session_title: None,
2749            session_title_override: None,
2750            created_at: "2026-08-12T00:00:00Z".into(),
2751            updated_at: "2026-08-12T00:00:00Z".into(),
2752            viewed_through_event_ordinal: 0,
2753            draft_input: String::new(),
2754            last_error: None,
2755            last_checkpoint_error: None,
2756            checkpoint: None,
2757        };
2758        let partial_target = TargetLocator::LocalPodman {
2759            container_id: "partial-container".into(),
2760            workspace_storage: Default::default(),
2761        };
2762        let mut cleaned = previous.clone();
2763        cleaned.state = SessionState::Error;
2764        cleaned.last_profile = "codex-new".into();
2765        cleaned.target = Some(partial_target.clone());
2766
2767        let failure =
2768            apply_failed_resume_rollback(&mut cleaned, &previous, "worker upload failed", None);
2769
2770        assert_eq!(cleaned.state, SessionState::Stopped);
2771        assert_eq!(cleaned.last_profile, "codex-old");
2772        assert_eq!(cleaned.target, None);
2773        assert_eq!(failure.to_string(), "worker upload failed");
2774        assert_eq!(
2775            cleaned.last_error.as_deref(),
2776            Some("resume failed: worker upload failed")
2777        );
2778
2779        let mut cleanup_failed = previous.clone();
2780        cleanup_failed.state = SessionState::Error;
2781        cleanup_failed.last_profile = "codex-new".into();
2782        cleanup_failed.target = Some(partial_target.clone());
2783        let partial_checkout = crate::hel_controller::test_support::managed_raw_session(
2784            hel::hel_state::ManagedWorktreeTarget::Local,
2785        );
2786        cleanup_failed.project_directory = partial_checkout.project_directory.clone();
2787        cleanup_failed.managed_worktree = partial_checkout.managed_worktree.clone();
2788
2789        let failure = apply_failed_resume_rollback(
2790            &mut cleanup_failed,
2791            &previous,
2792            "worker upload failed",
2793            Some("podman rm failed".into()),
2794        );
2795
2796        assert_eq!(cleanup_failed.state, SessionState::Error);
2797        assert_eq!(cleanup_failed.last_profile, "codex-new");
2798        assert_eq!(cleanup_failed.target, Some(partial_target));
2799        assert_eq!(
2800            cleanup_failed.project_directory,
2801            partial_checkout.project_directory
2802        );
2803        assert_eq!(
2804            cleanup_failed.managed_worktree,
2805            partial_checkout.managed_worktree
2806        );
2807        assert!(failure.to_string().contains("cleanup"));
2808    }
2809    #[test]
2810    fn failed_worktree_cleanup_notice_names_mjolnir_and_the_recovery_command() {
2811        let notice = worktree_cleanup_notice(
2812            Path::new("/workspace/project"),
2813            &anyhow::anyhow!("permission denied"),
2814        );
2815
2816        assert!(
2817            notice.starts_with(
2818                "Mjolnir could not remove the worktree at /workspace/project: permission denied."
2819            ),
2820            "{notice}"
2821        );
2822        assert!(
2823            notice.contains("`git worktree remove --force /workspace/project`"),
2824            "{notice}"
2825        );
2826        assert!(!notice.contains("Hel"), "{notice}");
2827    }
2828    #[test]
2829    fn failed_resume_provisioning_preserves_checkpoint_and_projection_lineage() {
2830        // MJ_DATA_DIR is process-global, so run the database-backed half in an
2831        // exact child test instead of racing unrelated tests in this process.
2832        if std::env::var_os(RESUME_ROLLBACK_TEST_CHILD).is_none() {
2833            let directory = tempfile::tempdir().unwrap();
2834            let test_name = format!(
2835                "{}::failed_resume_provisioning_preserves_checkpoint_and_projection_lineage",
2836                module_path!()
2837                    .strip_prefix("mj_controller::")
2838                    .unwrap_or(module_path!())
2839            );
2840            let output = Command::new(std::env::current_exe().unwrap())
2841                .args(["--exact", &test_name, "--nocapture"])
2842                .env(RESUME_ROLLBACK_TEST_CHILD, "1")
2843                // A remote target needs a portable worker; any existing file
2844                // satisfies the preflight so the test reaches provisioning.
2845                .env("MJ_WORKER_BINARY", std::env::current_exe().unwrap())
2846                .env("MJ_DATA_DIR", directory.path())
2847                .env("GH_TOKEN", "test-token")
2848                .output()
2849                .unwrap();
2850            assert!(
2851                output.status.success(),
2852                "isolated resume rollback test failed\nstdout:\n{}\nstderr:\n{}",
2853                String::from_utf8_lossy(&output.stdout),
2854                String::from_utf8_lossy(&output.stderr)
2855            );
2856            return;
2857        }
2858        // Alone in this child process, so it installs the one writer.
2859        let _writer = hel::hel_database::install_isolated_test_writer();
2860
2861        /// Provisioning runs after the resumed record is persisted, so the
2862        /// durable mounts read here are the ones resume just committed.
2863        #[derive(Default)]
2864        struct FailingPreflightExecutor {
2865            mounts_during_provisioning: Mutex<Option<Vec<AdditionalMount>>>,
2866        }
2867
2868        impl CommandExecutor for FailingPreflightExecutor {
2869            fn execute(&self, command: &CommandSpec) -> Result<CommandOutput> {
2870                // Provisioning probes the mount source's filesystem before it
2871                // builds the run arguments; a local disk keeps the overlay.
2872                if command.program == "stat" {
2873                    return Ok(CommandOutput {
2874                        status: 0,
2875                        stdout: b"ext4\n".to_vec(),
2876                        stderr: Vec::new(),
2877                    });
2878                }
2879                assert_eq!(command.program, "podman");
2880                let mut observed = self.mounts_during_provisioning.lock().unwrap();
2881                if observed.is_none() {
2882                    let durable = hel::hel_database::load_state().unwrap();
2883                    *observed = Some(
2884                        durable.sessions["0123456789abcdef0123456789abcdef"]
2885                            .additional_mounts
2886                            .clone(),
2887                    );
2888                }
2889                Ok(CommandOutput {
2890                    status: 1,
2891                    stdout: Vec::new(),
2892                    stderr: b"podman is temporarily unavailable".to_vec(),
2893                })
2894            }
2895        }
2896
2897        let data_directory = PathBuf::from(std::env::var_os("MJ_DATA_DIR").unwrap());
2898        let archive_directory = data_directory.join("archives");
2899        std::fs::create_dir_all(&archive_directory).unwrap();
2900        let session_id = "0123456789abcdef0123456789abcdef";
2901        let checkpoint = super::super::test_support::write_network_checkpoint_archive(
2902            &archive_directory,
2903            session_id,
2904            7,
2905        );
2906        let archive = verify_archive_streaming(&checkpoint.archive_path).unwrap();
2907        let expected_projection =
2908            materialized_session_from_canonical(session_id, &archive.canonical_session).unwrap();
2909
2910        let mut session = checkpoint_test_session(session_id);
2911        session.state = SessionState::Stopped;
2912        session.checkpoint = Some(checkpoint.clone());
2913        session.additional_mounts = vec![AdditionalMount {
2914            source: PathBuf::from("/host/old"),
2915            destination: PathBuf::from("/mnt/old"),
2916            read_only: false,
2917        }];
2918        let previous = session.clone();
2919        let resumed_mounts = vec![AdditionalMount {
2920            source: PathBuf::from("/host/new"),
2921            destination: PathBuf::from("/mnt/new"),
2922            read_only: false,
2923        }];
2924        let profile_home = data_directory.join("profile");
2925        std::fs::create_dir_all(&profile_home).unwrap();
2926        let mut config = HelConfig::default();
2927        config.profiles.insert(
2928            "codex".into(),
2929            HarnessProfile {
2930                enabled: true,
2931                kind: hel::hel_config::HarnessKind::Codex,
2932                home: profile_home,
2933                environment: BTreeMap::new(),
2934                context_window_bytes: None,
2935            },
2936        );
2937        config.bundles.insert(
2938            "project".into(),
2939            ProjectBundle {
2940                primary_repo: "project".into(),
2941                repositories: vec![ProjectRepository {
2942                    id: "project".into(),
2943                    github: None,
2944                    local: Some(data_directory.join("host-clone-that-no-longer-exists")),
2945                    destination: "project".into(),
2946                    git_ref: None,
2947                }],
2948            },
2949        );
2950        config.targets.insert(
2951            "podman".into(),
2952            TargetTemplate::LocalPodman {
2953                container: ConfigContainer {
2954                    image: "example.invalid/hel-test:latest".into(),
2955                    pull_policy: Default::default(),
2956                    platform: None,
2957                    cpus: None,
2958                    memory: None,
2959                    environment: BTreeMap::new(),
2960                    workspace_storage: Default::default(),
2961                },
2962            },
2963        );
2964        let mut controller = Controller {
2965            config,
2966            state: HelState {
2967                sessions: BTreeMap::from([(session_id.into(), session)]),
2968                ..HelState::default()
2969            },
2970        };
2971        hel::hel_database::save_state(&controller.state).unwrap();
2972        hel::hel_database::save_materialized_session(&expected_projection).unwrap();
2973
2974        let runtime = tokio::runtime::Builder::new_current_thread()
2975            .enable_all()
2976            .build()
2977            .unwrap();
2978        let executor = FailingPreflightExecutor::default();
2979        let error = runtime
2980            .block_on(controller.resume_session_controlled(
2981                session_id,
2982                "codex",
2983                "podman",
2984                SessionResumeOptions {
2985                    additional_mounts: Some(resumed_mounts.clone()),
2986                    resource_allocation: None,
2987                    discard_queue: false,
2988                },
2989                &executor,
2990            ))
2991            .unwrap_err();
2992        let detail = format!("{error:#}");
2993        assert!(
2994            detail.contains("podman is temporarily unavailable"),
2995            "{detail}"
2996        );
2997        assert!(!detail.contains("returned to stopped"), "{detail}");
2998        assert!(!detail.contains("unknown session"), "{detail}");
2999        assert_eq!(
3000            executor.mounts_during_provisioning.into_inner().unwrap(),
3001            Some(resumed_mounts)
3002        );
3003
3004        let retained = controller.state.sessions.get(session_id).unwrap();
3005        assert_eq!(retained.state, SessionState::Stopped);
3006        assert_eq!(retained.checkpoint, previous.checkpoint);
3007        assert_eq!(retained.managed_worktree, previous.managed_worktree);
3008        assert!(checkpoint.archive_path.is_file());
3009
3010        let durable = hel::hel_database::load_state().unwrap();
3011        let durable_session = durable.sessions.get(session_id).unwrap();
3012        assert_eq!(durable_session.state, SessionState::Stopped);
3013        assert_eq!(durable_session.checkpoint, previous.checkpoint);
3014        assert_eq!(
3015            durable_session.additional_mounts,
3016            previous.additional_mounts
3017        );
3018        assert_eq!(
3019            hel::hel_database::load_materialized_session(session_id).unwrap(),
3020            Some(expected_projection)
3021        );
3022    }
3023    #[test]
3024    fn failed_resume_retires_a_checkout_it_recreated() {
3025        if std::env::var_os(RETIRED_WORKTREE_RESUME_TEST_CHILD).is_none() {
3026            let directory = tempfile::tempdir().unwrap();
3027            let test_name = format!(
3028                "{}::failed_resume_retires_a_checkout_it_recreated",
3029                module_path!()
3030                    .strip_prefix("mj_controller::")
3031                    .unwrap_or(module_path!())
3032            );
3033            let output = Command::new(std::env::current_exe().unwrap())
3034                .args(["--exact", &test_name, "--nocapture"])
3035                .env(RETIRED_WORKTREE_RESUME_TEST_CHILD, "1")
3036                // A remote target needs a portable worker; any existing file
3037                // satisfies the preflight so the test reaches provisioning.
3038                .env("MJ_WORKER_BINARY", std::env::current_exe().unwrap())
3039                .env("MJ_DATA_DIR", directory.path().join("data"))
3040                .env("MJ_CONFIG_DIR", directory.path().join("config"))
3041                .output()
3042                .unwrap();
3043            assert!(
3044                output.status.success(),
3045                "isolated retired-worktree resume test failed\nstdout:\n{}\nstderr:\n{}",
3046                String::from_utf8_lossy(&output.stdout),
3047                String::from_utf8_lossy(&output.stderr)
3048            );
3049            return;
3050        }
3051        // Alone in this child process, so it installs the one writer.
3052        let _writer = hel::hel_database::install_isolated_test_writer();
3053
3054        struct FailAfterWorktreeRestore;
3055
3056        impl CommandExecutor for FailAfterWorktreeRestore {
3057            fn execute(&self, command: &CommandSpec) -> Result<CommandOutput> {
3058                if matches!(command.program.as_str(), "git" | "mkdir") {
3059                    return ProcessExecutor.execute(command);
3060                }
3061                Ok(CommandOutput {
3062                    status: 1,
3063                    stdout: Vec::new(),
3064                    stderr: b"stop after recreating the checkout".to_vec(),
3065                })
3066            }
3067        }
3068
3069        let data_directory = PathBuf::from(std::env::var_os("MJ_DATA_DIR").unwrap());
3070        let archive_directory = data_directory.join("archives");
3071        std::fs::create_dir_all(&archive_directory).unwrap();
3072        let session_id = "0123456789abcdef0123456789abcdef";
3073        let checkpoint = write_checkpoint_gate_archive(&archive_directory, session_id, 7);
3074        let repository = committed_repository();
3075        let mut session = managed_worktree_session(repository.path(), session_id);
3076        session.checkpoint = Some(checkpoint);
3077        let worktree = session.managed_worktree.clone().unwrap();
3078        retire_managed_worktree(&ProcessExecutor, &worktree).unwrap();
3079        assert!(!worktree.worktree_root.exists());
3080
3081        let profile_home = data_directory.join("profile");
3082        std::fs::create_dir_all(&profile_home).unwrap();
3083        let mut config = resume_compatibility_config();
3084        config.profiles.insert(
3085            "codex".into(),
3086            HarnessProfile {
3087                enabled: true,
3088                kind: hel::hel_config::HarnessKind::Codex,
3089                home: profile_home,
3090                environment: BTreeMap::new(),
3091                context_window_bytes: None,
3092            },
3093        );
3094        let mut controller = Controller {
3095            config,
3096            state: HelState {
3097                sessions: BTreeMap::from([(session_id.into(), session)]),
3098                ..HelState::default()
3099            },
3100        };
3101        hel::hel_database::save_state(&controller.state).unwrap();
3102
3103        let error = tokio::runtime::Builder::new_current_thread()
3104            .enable_all()
3105            .build()
3106            .unwrap()
3107            .block_on(controller.resume_session_controlled(
3108                session_id,
3109                "codex",
3110                "local-bare",
3111                SessionResumeOptions {
3112                    additional_mounts: None,
3113                    resource_allocation: None,
3114                    discard_queue: false,
3115                },
3116                &FailAfterWorktreeRestore,
3117            ))
3118            .unwrap_err();
3119
3120        assert!(
3121            format!("{error:#}").contains("stop after recreating the checkout"),
3122            "{error:#}"
3123        );
3124        assert!(!worktree.worktree_root.exists());
3125        assert_eq!(
3126            controller.state.sessions[session_id].state,
3127            SessionState::Stopped
3128        );
3129        let branch = Command::new("git")
3130            .arg("-C")
3131            .arg(repository.path())
3132            .args([
3133                "show-ref",
3134                "--verify",
3135                &format!("refs/heads/{}", worktree.branch),
3136            ])
3137            .status()
3138            .unwrap();
3139        assert!(branch.success(), "resume rollback must retain the branch");
3140    }
3141    const RAW_CONVERSION_TEST_CHILD: &str = "MJ_RAW_CONVERSION_TEST_CHILD";
3142    #[test]
3143    fn unsupported_raw_conversion_leaves_config_and_worktree_unchanged() {
3144        // MJ_DATA_DIR and MJ_CONFIG_DIR are process-global, so run the half
3145        // that writes them in an exact child test.
3146        if std::env::var_os(RAW_CONVERSION_TEST_CHILD).is_none() {
3147            let directory = tempfile::tempdir().unwrap();
3148            let test_name = format!(
3149                "{}::unsupported_raw_conversion_leaves_config_and_worktree_unchanged",
3150                module_path!()
3151                    .strip_prefix("mj_controller::")
3152                    .unwrap_or(module_path!())
3153            );
3154            let output = Command::new(std::env::current_exe().unwrap())
3155                .args(["--exact", &test_name, "--nocapture"])
3156                .env(RAW_CONVERSION_TEST_CHILD, "1")
3157                // A remote target needs a portable worker; any existing file
3158                // satisfies the preflight so the test reaches provisioning.
3159                .env("MJ_WORKER_BINARY", std::env::current_exe().unwrap())
3160                .env("MJ_DATA_DIR", directory.path().join("data"))
3161                .env("MJ_CONFIG_DIR", directory.path().join("config"))
3162                .env("GH_TOKEN", "test-token")
3163                .output()
3164                .unwrap();
3165            assert!(
3166                output.status.success(),
3167                "isolated raw conversion test failed\nstdout:\n{}\nstderr:\n{}",
3168                String::from_utf8_lossy(&output.stdout),
3169                String::from_utf8_lossy(&output.stderr)
3170            );
3171            return;
3172        }
3173        // Alone in this child process, so it installs the one writer.
3174        let _writer = hel::hel_database::install_isolated_test_writer();
3175
3176        /// Real Git, no container runtime. Provisioning fails at preflight,
3177        /// after the conversion has already reshaped the record.
3178        struct GitWithoutPodmanExecutor;
3179
3180        impl CommandExecutor for GitWithoutPodmanExecutor {
3181            fn execute(&self, command: &CommandSpec) -> Result<CommandOutput> {
3182                if command.program == "git" {
3183                    return ProcessExecutor.execute(command);
3184                }
3185                Ok(CommandOutput {
3186                    status: 1,
3187                    stdout: Vec::new(),
3188                    stderr: b"podman is temporarily unavailable".to_vec(),
3189                })
3190            }
3191        }
3192
3193        let data_directory = PathBuf::from(std::env::var_os("MJ_DATA_DIR").unwrap());
3194        let archive_directory = data_directory.join("archives");
3195        std::fs::create_dir_all(&archive_directory).unwrap();
3196        std::fs::create_dir_all(hel::hel_config::config_dir()).unwrap();
3197        let session_id = "0123456789abcdef0123456789abcdef";
3198        let checkpoint = write_checkpoint_gate_archive(&archive_directory, session_id, 7);
3199
3200        let repository = committed_repository();
3201        let mut session = managed_worktree_session(repository.path(), session_id);
3202        session.checkpoint = Some(checkpoint);
3203        let worktree = session.managed_worktree.clone().unwrap();
3204        let previous = session.clone();
3205
3206        let profile_home = data_directory.join("profile");
3207        std::fs::create_dir_all(&profile_home).unwrap();
3208        let mut config = resume_compatibility_config();
3209        config.profiles.insert(
3210            "codex".into(),
3211            HarnessProfile {
3212                enabled: true,
3213                kind: hel::hel_config::HarnessKind::Codex,
3214                home: profile_home,
3215                environment: BTreeMap::new(),
3216                context_window_bytes: None,
3217            },
3218        );
3219        // Production controllers read this configuration from disk; bundle
3220        // updates now deliberately reload it under the transaction lock.
3221        config.save().unwrap();
3222        let original_config = config.clone();
3223        let mut controller = Controller {
3224            config,
3225            state: HelState {
3226                sessions: BTreeMap::from([(session_id.into(), session)]),
3227                ..HelState::default()
3228            },
3229        };
3230        hel::hel_database::save_state(&controller.state).unwrap();
3231
3232        let error = tokio::runtime::Builder::new_current_thread()
3233            .enable_all()
3234            .build()
3235            .unwrap()
3236            .block_on(controller.resume_session_controlled(
3237                session_id,
3238                "codex",
3239                "podman",
3240                SessionResumeOptions {
3241                    additional_mounts: None,
3242                    resource_allocation: None,
3243                    discard_queue: false,
3244                },
3245                &GitWithoutPodmanExecutor,
3246            ))
3247            .unwrap_err();
3248        assert!(
3249            format!("{error:#}").contains("network repository provenance"),
3250            "{error:#}"
3251        );
3252        assert!(!format!("{error:#}").contains("returned to stopped"));
3253
3254        assert_eq!(controller.config, original_config);
3255        assert_eq!(hel::hel_config::HelConfig::load().unwrap(), original_config);
3256
3257        let retained = controller.state.sessions.get(session_id).unwrap();
3258        assert_eq!(retained.state, SessionState::Stopped);
3259        assert_eq!(retained.project_directory, previous.project_directory);
3260        assert_eq!(retained.managed_worktree, previous.managed_worktree);
3261        assert_eq!(retained.bundle_id, previous.bundle_id);
3262        assert!(worktree.worktree_root.is_dir(), "the checkout stays put");
3263    }
3264}