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