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