Skip to main content

mj_controller/hel_controller/
checkpoint.rs

1//! Checkpoint export, latching, verification, and archive bookkeeping.
2
3use std::collections::BTreeSet;
4use std::ffi::OsStr;
5use std::path::{Path, PathBuf};
6use std::time::{Duration, Instant};
7
8use anyhow::{Context, Result, bail, ensure};
9
10use crate::hel_session_manager::{
11    ManagedSessionHandle, ManagedSessionLease, SessionManagerControl, StandaloneSession,
12    new_command_id, worker_connect_needs_restart,
13};
14use crate::hel_worker_client::RelayRejected;
15use hel::hel_archive::{
16    BundleManifest, CanonicalSessionSnapshot, SessionManifest, TargetManifest,
17    verify_archive_streaming,
18};
19use hel::hel_checkpoint::{
20    CHECKPOINT_EXPORT_PROTOCOL_VERSION, CHECKPOINT_STAGING_PROTOCOL_VERSION, CapturedCheckpoint,
21    CheckpointCaptureSpec, CheckpointExportSpec, CheckpointPackSpec, CheckpointRepositoryCapture,
22    CheckpointRepositorySpec, CheckpointTransfer, canonical_session_contains_prompt,
23    capture_stdin_command, checkpoint_sha256, export_command, export_stdin_command,
24    pack_stdin_command,
25};
26use hel::hel_config::sessions_dir;
27use hel::hel_projection::canonical_session_from_materialized;
28use hel::hel_state::{
29    CheckpointMetadata, HelState, ManagedSessionSnapshot, SessionRecord, SessionState,
30};
31use hel::hel_targets::{
32    self, CommandExecutor, CommandOutput, CommandSpec, ProcessExecutor, ProvisionStage,
33    ProvisionStageGuard,
34};
35use hel::hel_worker::{RelayCommand, RelayCursor, RelayExecutionState};
36
37use super::backend::backend_locator;
38use super::readiness::wait_for_native_session_in_stage;
39use super::worker_restart::{InstalledWorkerRestart, RESTART_FOR_CHECKPOINT};
40use super::{
41    Controller, execute_checked, now, persist_session_record_transition_or_restore,
42    scp_command_spec, ssh_command_spec, target_kind, target_profile_home,
43};
44
45/// How long an idle relay may fail to admit a barrier before its worker is
46/// treated as wedged. Busy recovery checkpoints defer immediately. A close
47/// sends a non-steering turn cancellation and gives the worker this same
48/// bounded interval to settle before recovery restarts it.
49const CHECKPOINT_BARRIER_TIMEOUT: Duration = Duration::from_secs(30);
50/// A close gets a fresh cancellation grace period once the worker accepts the
51/// request. This keeps an expensive status sync from consuming the whole
52/// cancellation budget before the worker has had a chance to settle.
53const CHECKPOINT_CANCEL_TIMEOUT: Duration = Duration::from_secs(30);
54/// After a wedged ACP forces a worker restart, wait as long as native-session
55/// startup: session/load of a long kimi transcript can outlast 30s.
56const CHECKPOINT_BARRIER_TIMEOUT_AFTER_RESTART: Duration = Duration::from_secs(300);
57
58/// Remove checkpoint archives installed by a process that exited before its
59/// database transaction committed. Call this only while holding the
60/// machine-wide controller-store guard and before starting background work.
61pub fn reconcile_managed_checkpoint_archives() -> Result<usize> {
62    let mut state = HelState::load()?;
63    // Include operation-owned recovery copies even after a ready destination
64    // installs a newer ordinary checkpoint.
65    for operation in hel::hel_database::load_move_operations()? {
66        if operation.retains_checkpoint()
67            && let Some(checkpoint) = operation.checkpoint
68            && let Some(mut session) = state.sessions.get(&operation.selection.session_id).cloned()
69        {
70            session.checkpoint = Some(checkpoint);
71            state
72                .sessions
73                .insert(format!("move:{}", operation.operation_id), session);
74        }
75    }
76    reconcile_managed_checkpoint_archives_in(&sessions_dir(), &state)
77}
78
79fn reconcile_managed_checkpoint_archives_in(directory: &Path, state: &HelState) -> Result<usize> {
80    if !directory.exists() {
81        return Ok(0);
82    }
83    let referenced_names = state
84        .sessions
85        .values()
86        .filter_map(|session| session.checkpoint.as_ref())
87        .filter_map(|checkpoint| checkpoint.archive_path.file_name())
88        .map(ToOwned::to_owned)
89        .collect::<BTreeSet<_>>();
90    let mut removed = 0;
91    for entry in std::fs::read_dir(directory)
92        .with_context(|| format!("scan checkpoint directory {}", directory.display()))?
93    {
94        let entry = entry?;
95        let file_type = entry.file_type()?;
96        if !file_type.is_file()
97            || !is_managed_checkpoint_archive_name(&entry.file_name())
98            || referenced_names.contains(&entry.file_name())
99        {
100            continue;
101        }
102        std::fs::remove_file(entry.path()).with_context(|| {
103            format!(
104                "remove unreferenced managed checkpoint {}",
105                entry.path().display()
106            )
107        })?;
108        removed += 1;
109    }
110    Ok(removed)
111}
112
113fn is_managed_checkpoint_archive_name(name: &OsStr) -> bool {
114    let Some(stem) = name.to_str().and_then(|name| name.strip_suffix(".hel.zip")) else {
115        return false;
116    };
117    let Some((frontier_prefix, nonce)) = stem.rsplit_once("-archive-") else {
118        return false;
119    };
120    if nonce.len() != 32
121        || !nonce
122            .bytes()
123            .all(|byte| byte.is_ascii_hexdigit() && !byte.is_ascii_uppercase())
124    {
125        return false;
126    }
127    let Some((session_id, frontier)) = frontier_prefix.rsplit_once('-') else {
128        return false;
129    };
130    !session_id.is_empty()
131        && frontier.parse::<u64>().is_ok()
132        && hel::hel_config::validate_id("session", session_id).is_ok()
133}
134
135#[derive(Debug, Clone)]
136pub struct CheckpointArtifact {
137    pub metadata: CheckpointMetadata,
138    pub native_session_id: String,
139    /// Digest paired with `metadata.event_frontier` at the relay barrier.
140    pub event_frontier_digest: String,
141}
142
143/// The relay connection one lifecycle operation talks to.
144///
145/// A managed operation borrows the session actor's own connection instead of
146/// opening a competing one. Exclusivity is only needed while a checkpoint
147/// latches its projection at the barrier's ready cursor; `end_latch` hands the
148/// connection back so the dashboard keeps syncing and submitting while the
149/// archive exports and transfers.
150pub(super) enum ControllerRelayLease {
151    Managed {
152        handle: ManagedSessionHandle,
153        lease: Option<ManagedSessionLease>,
154    },
155    Standalone(StandaloneSession),
156}
157
158impl ControllerRelayLease {
159    /// The exclusively held connection. Only a latch phase, or an operation
160    /// that deliberately holds its lease to the end, may use this.
161    pub(super) fn connection_mut(&mut self) -> &mut StandaloneSession {
162        match self {
163            Self::Managed { lease, .. } => lease
164                .as_mut()
165                .expect("checkpoint latch has already returned its connection")
166                .connection_mut(),
167            Self::Standalone(connection) => connection,
168        }
169    }
170
171    async fn submit(&mut self, command_id: String, command: RelayCommand) -> Result<u64> {
172        match self {
173            Self::Managed {
174                lease: Some(lease), ..
175            } => lease.connection_mut().submit(command_id, command).await,
176            Self::Managed { handle, .. } => handle.submit(command_id, command).await,
177            Self::Standalone(connection) => connection.submit(command_id, command).await,
178        }
179    }
180
181    async fn sync_snapshot(&mut self) -> Result<ManagedSessionSnapshot> {
182        match self {
183            Self::Managed {
184                lease: Some(lease), ..
185            } => lease.connection_mut().sync().await,
186            Self::Managed { handle, .. } => {
187                handle.sync_now().await?;
188                handle
189                    .view()
190                    .snapshot
191                    .context("managed session has no snapshot")
192            }
193            Self::Standalone(connection) => connection.sync().await,
194        }
195    }
196
197    /// Swap the proxy after the worker process behind it was restarted.
198    fn replace_connection(&mut self, connection: StandaloneSession) {
199        match self {
200            Self::Managed {
201                lease: Some(lease), ..
202            } => lease.replace_connection(connection),
203            Self::Standalone(existing) => *existing = connection,
204            Self::Managed { lease: None, .. } => {
205                *self = Self::Standalone(connection);
206            }
207        }
208    }
209
210    /// Return the connection to its session actor now that the projection is
211    /// latched. Releasing keeps the connection alive, so the relay barrier it
212    /// opened stays open. Idempotent.
213    fn end_latch(&mut self) {
214        if let Self::Managed { lease, .. } = self
215            && let Some(lease) = lease.take()
216        {
217            lease.release();
218        }
219    }
220
221    /// Abandon a checkpoint barrier this controller can no longer complete.
222    ///
223    /// A relay barrier belongs to the connection that opened it and only a
224    /// disconnect cancels it (`cancel_checkpoint_barrier_on_disconnect`).
225    /// Completing it instead would advance the relay's recovery floor past
226    /// history that no verified checkpoint covers, so reclaim the connection
227    /// and drop it: the worker cancels the barrier and resumes dispatch.
228    async fn cancel_abandoned_barrier(&mut self) -> Result<()> {
229        let Self::Managed { handle, lease } = self else {
230            // A standalone connection is dropped with this value, which the
231            // worker sees as the same disconnect.
232            return Ok(());
233        };
234        match lease.take() {
235            Some(lease) => drop(lease),
236            None => drop(handle.lease_connection().await?),
237        }
238        Ok(())
239    }
240
241    pub(super) fn release(self) {
242        if let Self::Managed {
243            lease: Some(lease), ..
244        } = self
245        {
246            lease.release();
247        }
248    }
249}
250
251/// Whether a checkpoint keeps its exclusive connection after latching.
252#[derive(Debug, Clone, Copy, PartialEq, Eq)]
253pub(super) enum LatchExclusivity {
254    /// Ordinary and recovery checkpoints only need exclusivity to latch the
255    /// projection at the barrier's ready cursor. Everything after that runs
256    /// through the session actor, so prompts keep flowing while the archive
257    /// exports and transfers.
258    ReleaseAfterLatch,
259    /// Close seals the relay at the exact latched cursor, so nothing else may
260    /// reach the relay between the barrier and its Close command.
261    HoldThroughClose,
262}
263
264/// Whether a latched checkpoint must export a fresh archive.
265#[derive(Debug, Clone, Copy, PartialEq, Eq)]
266pub(super) enum CheckpointExportPolicy {
267    /// Always export, transfer, and install a new archive.
268    Always,
269    /// Keep the installed archive when the latched projection holds the same
270    /// session content. Relay bookkeeping (the checkpoint commands themselves)
271    /// always moves the event frontier, so only content can decide this.
272    ReuseUnchangedArchive,
273}
274
275/// How a latched checkpoint ends the barrier it opened.
276#[derive(Debug, Clone, Copy, PartialEq, Eq)]
277pub(super) enum CheckpointCompletion {
278    /// The barrier is still open. Completing it resumes ACP dispatch and
279    /// advances the relay's recovery floor in one durable step; abandoning it
280    /// cancels the barrier and leaves the floor alone.
281    HeldBarrier,
282    /// The worker already resumed dispatch when target capture finished. All that
283    /// is left for a durably installed archive is the recovery floor move.
284    ReleasedAfterCapture,
285}
286
287pub(super) struct LatchedCheckpoint {
288    pub(super) artifact: CheckpointArtifact,
289    pub(super) relay: ControllerRelayLease,
290    pub(super) barrier_command_id: String,
291    pub(super) cursor: RelayCursor,
292    pub(super) completion: CheckpointCompletion,
293}
294
295/// A latched checkpoint owns an open relay barrier, and that barrier freezes
296/// ACP dispatch until something ends it. Every path out of one must therefore
297/// either [`LatchedCheckpoint::complete`] it or [`LatchedCheckpoint::abandon`]
298/// it; both consume the value so a new exit cannot quietly skip the choice.
299/// Close is the exception: it holds its lease to the end, so dropping that
300/// lease is what ends its barrier.
301impl LatchedCheckpoint {
302    /// Let the relay release the history that this installed archive covers.
303    async fn complete(mut self) -> Result<()> {
304        let (prefix, command) = match self.completion {
305            CheckpointCompletion::HeldBarrier => (
306                "checkpoint-complete",
307                RelayCommand::CompleteCheckpoint {
308                    barrier_command_id: self.barrier_command_id.clone(),
309                },
310            ),
311            // The worker that accepted the early release also understands the
312            // floor move; they were added together.
313            CheckpointCompletion::ReleasedAfterCapture => (
314                "checkpoint-floor",
315                RelayCommand::AdvanceRecoveryFloor {
316                    through: self.cursor.clone(),
317                },
318            ),
319        };
320        let command_id = new_command_id(prefix)?;
321        self.relay.submit(command_id, command).await.map(|_| ())
322    }
323
324    /// Cancel the barrier of a checkpoint the caller could not install.
325    ///
326    /// The latch is already back with the session actor, whose connection can
327    /// stay healthy for the rest of the session, so nothing else would ever
328    /// end this barrier.
329    async fn abandon(mut self, session_id: &str) {
330        if self.completion == CheckpointCompletion::ReleasedAfterCapture {
331            // Dispatch resumed when target capture finished, so there is no barrier
332            // left to cancel, and the recovery floor must stay behind an
333            // archive that was never installed. Doing nothing is the exit.
334            return;
335        }
336        if let Err(error) = self.relay.cancel_abandoned_barrier().await {
337            tracing::warn!(
338                session_id,
339                "abandoned checkpoint could not cancel its relay barrier: {error:#}"
340            );
341        }
342    }
343}
344
345impl Controller {
346    pub(super) fn persist_checkpoint_transition_or_restore(
347        &mut self,
348        session_id: &str,
349        previous: &SessionRecord,
350        context: &'static str,
351    ) -> Result<()> {
352        persist_session_record_transition_or_restore(
353            &mut self.state,
354            session_id,
355            previous,
356            context,
357            &hel::hel_database::save_checkpointed_session,
358        )
359    }
360
361    pub(super) fn persist_failed_checkpoint_state_or_restore(
362        &mut self,
363        session_id: &str,
364        previous: &SessionRecord,
365        primary: anyhow::Error,
366    ) -> anyhow::Error {
367        match self.persist_session_state(session_id) {
368            Ok(()) => primary,
369            Err(error) => self.restore_prior_session_after_persistence_failure(
370                session_id,
371                previous,
372                primary.context(format!(
373                    "failed to persist the checkpoint rollback state: {error:#}"
374                )),
375            ),
376        }
377    }
378
379    /// Materialize and locally verify a complete session checkpoint while the
380    /// target remains live. A failed export or transfer leaves the previous
381    /// archive and target untouched.
382    pub async fn checkpoint_session(&mut self, session_id: &str) -> Result<CheckpointMetadata> {
383        self.checkpoint_session_controlled(session_id, &ProcessExecutor)
384            .await
385    }
386
387    pub async fn checkpoint_session_controlled(
388        &mut self,
389        session_id: &str,
390        executor: &(impl CommandExecutor + Sync),
391    ) -> Result<CheckpointMetadata> {
392        self.checkpoint_session_controlled_with_manager(session_id, executor, None)
393            .await
394    }
395
396    async fn checkpoint_session_controlled_with_manager(
397        &mut self,
398        session_id: &str,
399        executor: &(impl CommandExecutor + Sync),
400        manager: Option<&SessionManagerControl>,
401    ) -> Result<CheckpointMetadata> {
402        let previous = self
403            .state
404            .sessions
405            .get(session_id)
406            .with_context(|| format!("unknown session {session_id}"))?
407            .clone();
408        ensure!(
409            !matches!(
410                previous.state,
411                SessionState::Closing | SessionState::Destroying
412            ),
413            "session {session_id} is already closing; resume that close instead of starting an ordinary checkpoint"
414        );
415        let record = self.state.sessions.get_mut(session_id).unwrap();
416        record.state = SessionState::Checkpointing;
417        record.updated_at = now();
418        record.last_checkpoint_error = None;
419        self.persist_session_transition_or_restore(
420            session_id,
421            &previous,
422            "persist checkpointing state before creating a checkpoint",
423        )?;
424
425        match self
426            .checkpoint_session_latched(
427                session_id,
428                executor,
429                manager,
430                LatchExclusivity::ReleaseAfterLatch,
431                CheckpointExportPolicy::Always,
432            )
433            .await
434        {
435            Ok(latched) => {
436                let artifact = latched.artifact.clone();
437                if let Err(error) = hel::hel_test_hooks::reach_test_hook(
438                    "checkpoint_archive_before_database_publication",
439                ) {
440                    latched.abandon(session_id).await;
441                    return Err(remove_uninstalled_checkpoint(
442                        &artifact.metadata.archive_path,
443                        error,
444                    ));
445                }
446                {
447                    let record = self.state.sessions.get_mut(session_id).unwrap();
448                    record.state = SessionState::Running;
449                    record.native_session_id = Some(artifact.native_session_id.clone());
450                    record.checkpoint = Some(artifact.metadata.clone());
451                    record.updated_at = now();
452                    record.last_error = None;
453                    record.last_checkpoint_error = None;
454                }
455                let persist_started = Instant::now();
456                if let Err(error) = self.persist_checkpoint_transition_or_restore(
457                    session_id,
458                    &previous,
459                    "persist verified checkpoint before releasing relay history",
460                ) {
461                    latched.abandon(session_id).await;
462                    return Err(error);
463                }
464                tracing::info!(
465                    session_id,
466                    persist_ms = persist_started.elapsed().as_millis() as u64,
467                    "checkpoint metadata persisted"
468                );
469                prune_replaced_checkpoint(previous.checkpoint.as_ref(), &artifact.metadata);
470                release_projection_behind_checkpoint(session_id, &artifact.metadata);
471                if let Err(error) = latched.complete().await {
472                    // Only journal retention is at stake. A barrier that is
473                    // still open cannot dangle: the actor retries a failed
474                    // submission over a fresh connection, and the worker
475                    // cancels barriers whose submitting connection dropped.
476                    // The next checkpoint moves the recovery floor again.
477                    tracing::warn!(
478                        session_id,
479                        "verified checkpoint was saved, but the relay could not be told to release the history it covers: {error:#}"
480                    );
481                }
482                Ok(artifact.metadata)
483            }
484            Err(error) => {
485                // A deferred checkpoint says the agent was working, not that
486                // anything failed. Recording it would leave a warning on the
487                // session row until the next successful copy, so the caller is
488                // told and the row is left alone.
489                let deferred = checkpoint_was_deferred(&error);
490                if let Some(record) = self.state.sessions.get_mut(session_id) {
491                    record.state = if previous.state == SessionState::Checkpointing {
492                        SessionState::Running
493                    } else {
494                        previous.state
495                    };
496                    record.updated_at = now();
497                    if !deferred {
498                        record.last_checkpoint_error = Some(format!("{error:#}"));
499                    }
500                }
501                Err(self.persist_failed_checkpoint_state_or_restore(session_id, &previous, error))
502            }
503        }
504    }
505
506    /// Create, checksum, and durably install a recovery archive before
507    /// allowing the relay to garbage-collect through its event frontier.
508    pub async fn create_recovery_checkpoint_managed_controlled(
509        &self,
510        session_id: &str,
511        manager: &SessionManagerControl,
512        executor: &(impl CommandExecutor + Sync),
513    ) -> Result<CheckpointArtifact> {
514        self.create_recovery_checkpoint_with_manager(session_id, Some(manager), executor)
515            .await
516    }
517
518    async fn create_recovery_checkpoint_with_manager(
519        &self,
520        session_id: &str,
521        manager: Option<&SessionManagerControl>,
522        executor: &(impl CommandExecutor + Sync),
523    ) -> Result<CheckpointArtifact> {
524        let previous_checkpoint = self
525            .state
526            .sessions
527            .get(session_id)
528            .with_context(|| format!("unknown session {session_id}"))?
529            .checkpoint
530            .clone();
531        let latched = self
532            .checkpoint_session_latched_with_recovery_stage(
533                session_id,
534                executor,
535                manager,
536                LatchExclusivity::ReleaseAfterLatch,
537                CheckpointExportPolicy::Always,
538                true,
539            )
540            .await?;
541        let artifact = latched.artifact.clone();
542        let verification = {
543            let _verifying = ProvisionStageGuard::new(executor, ProvisionStage::Verifying);
544            verify_checkpoint_artifact(session_id, &artifact)
545        };
546        if let Err(error) = verification {
547            latched.abandon(session_id).await;
548            return Err(remove_uninstalled_checkpoint(
549                &artifact.metadata.archive_path,
550                error.context("final recovery checkpoint verification"),
551            ));
552        }
553        if let Err(error) =
554            hel::hel_test_hooks::reach_test_hook("checkpoint_archive_before_database_publication")
555        {
556            latched.abandon(session_id).await;
557            return Err(remove_uninstalled_checkpoint(
558                &artifact.metadata.archive_path,
559                error,
560            ));
561        }
562        let persist_started = Instant::now();
563        if let Err(error) = hel::hel_database::record_recovery_success(
564            session_id,
565            &artifact.native_session_id,
566            &artifact.metadata,
567        ) {
568            latched.abandon(session_id).await;
569            return Err(error
570                .context("persist verified recovery checkpoint before releasing relay history"));
571        }
572        tracing::info!(
573            session_id,
574            persist_ms = persist_started.elapsed().as_millis() as u64,
575            "recovery checkpoint metadata persisted"
576        );
577        if let Err(error) = latched.complete().await {
578            // Only journal retention is at stake. A barrier that is still open
579            // cannot dangle: the actor retries a failed submission over a fresh
580            // connection, and the worker cancels barriers whose submitting
581            // connection dropped. The next checkpoint moves the floor again.
582            tracing::warn!(
583                session_id,
584                "recovery checkpoint was saved, but the relay could not be told to release the history it covers: {error:#}"
585            );
586        }
587        prune_replaced_checkpoint(previous_checkpoint.as_ref(), &artifact.metadata);
588        release_projection_behind_checkpoint(session_id, &artifact.metadata);
589        Ok(artifact)
590    }
591
592    pub(super) async fn checkpoint_session_latched(
593        &self,
594        session_id: &str,
595        executor: &(impl CommandExecutor + Sync),
596        manager: Option<&SessionManagerControl>,
597        exclusivity: LatchExclusivity,
598        export_policy: CheckpointExportPolicy,
599    ) -> Result<LatchedCheckpoint> {
600        self.checkpoint_session_latched_with_recovery_stage(
601            session_id,
602            executor,
603            manager,
604            exclusivity,
605            export_policy,
606            exclusivity == LatchExclusivity::HoldThroughClose,
607        )
608        .await
609    }
610
611    async fn checkpoint_session_latched_with_recovery_stage(
612        &self,
613        session_id: &str,
614        executor: &(impl CommandExecutor + Sync),
615        manager: Option<&SessionManagerControl>,
616        exclusivity: LatchExclusivity,
617        export_policy: CheckpointExportPolicy,
618        recovery_copy: bool,
619    ) -> Result<LatchedCheckpoint> {
620        if let Some(operation) = hel::hel_database::load_move_operation(session_id)?
621            && operation.queue_admission_started
622            && !operation.queue_admission_finished
623        {
624            // Advancing the recovery floor can prune terminal command IDs.
625            // Keep them until a retained Move queue has been fully admitted.
626            bail!(
627                "move queue admission is incomplete; retry Move before checkpointing this destination"
628            );
629        }
630        let session = self
631            .state
632            .sessions
633            .get(session_id)
634            .with_context(|| format!("unknown session {session_id}"))?
635            .clone();
636        let locator = session
637            .target
638            .as_ref()
639            .context("session has no live target")?;
640        let backend = backend_locator(locator, &session, &self.config)?;
641        let profile = self
642            .config
643            .profiles
644            .get(&session.last_profile)
645            .context("session profile is missing")?;
646        let bundle = session
647            .project_directory
648            .is_none()
649            .then(|| self.config.bundles.get(&session.bundle_id))
650            .flatten();
651        let reconnect = hel_targets::reconnect_plan(&backend, session_id)?
652            .commands
653            .into_iter()
654            .next()
655            .context("reconnect plan is empty")?;
656        let worker_root = hel_targets::worker_root(&backend, session_id)?;
657        let harness_home = target_profile_home(&backend, session_id, profile);
658        let (workspace_root, primary_repository, repositories) =
659            if let Some(project_directory) = &session.project_directory {
660                let parent = project_directory
661                    .parent()
662                    .context("bare project directory has no parent")?;
663                let destination = project_directory
664                    .file_name()
665                    .context("bare project directory cannot be the filesystem root")?;
666                (
667                    parent.to_string_lossy().into_owned(),
668                    "project".to_owned(),
669                    vec![CheckpointRepositorySpec {
670                        id: "project".into(),
671                        relative_destination: PathBuf::from(destination),
672                        // Managed worktrees are retired on Stop, so their
673                        // dirty/untracked state must travel in the archive.
674                        // Their branch and objects remain in the owning Git
675                        // repository; no remote origin is required. Unmanaged
676                        // raw checkouts remain in place.
677                        capture: if session.managed_worktree.is_some() {
678                            CheckpointRepositoryCapture::DeltaFrom {
679                                base_commit: super::worktree::raw_checkout_position(
680                                    &session,
681                                    &self.config,
682                                    project_directory,
683                                    executor,
684                                )?
685                                .head_commit,
686                            }
687                        } else {
688                            CheckpointRepositoryCapture::MetadataOnly
689                        },
690                        origin_override: None,
691                    }],
692                )
693            } else {
694                let bundle = bundle.context("session bundle is missing")?;
695                let workspace_root = match &backend {
696                    hel_targets::TargetLocator::LocalPodman { .. }
697                    | hel_targets::TargetLocator::LocalDocker { .. }
698                    | hel_targets::TargetLocator::AppleContainer { .. }
699                    | hel_targets::TargetLocator::SshPodman { .. }
700                    | hel_targets::TargetLocator::SshDocker { .. } => "/workspace".to_string(),
701                    hel_targets::TargetLocator::AwsEc2 { workspace, .. }
702                    | hel_targets::TargetLocator::SshBare { workspace, .. } => workspace.clone(),
703                    hel_targets::TargetLocator::LocalBare { worker_root } => worker_root.clone(),
704                };
705                let repositories = bundle
706                    .repositories
707                    .iter()
708                    .map(|repository| CheckpointRepositorySpec {
709                        id: repository.id.clone(),
710                        relative_destination: repository.destination.clone(),
711                        capture: CheckpointRepositoryCapture::SessionDelta,
712                        origin_override: repository
713                            .is_local()
714                            .then(|| format!("mj-local:{}", repository.id)),
715                    })
716                    .collect();
717                (workspace_root, bundle.primary_repo.clone(), repositories)
718            };
719        let target_path = |path: &str| match &backend {
720            hel_targets::TargetLocator::AwsEc2 { .. }
721            | hel_targets::TargetLocator::SshBare { .. }
722                if !path.starts_with('/') =>
723            {
724                PathBuf::from(format!("~/{path}"))
725            }
726            _ => PathBuf::from(path),
727        };
728        let remote_spec = format!("{worker_root}/checkpoint-spec.json");
729        let remote_archive = format!("{worker_root}/checkpoint.hel.zip");
730        let remote_stage = format!(
731            "{worker_root}/checkpoint-stage-{}",
732            new_command_id("capture")?
733        );
734        let checkpointed_at = now();
735        let target_manifest = TargetManifest {
736            template_id: session.target_template_id.clone(),
737            target_kind: target_kind(&backend).into(),
738            details: Default::default(),
739        };
740        let bundle_manifest = BundleManifest {
741            id: session.bundle_id.clone(),
742            primary_repository,
743        };
744        let session_manifest = |native_session_id: &str| SessionManifest {
745            id: session.id.clone(),
746            title: session.title.clone(),
747            harness_kind: session.harness_kind,
748            profile_id: session.last_profile.clone(),
749            native_session_id: native_session_id.to_owned(),
750            created_at: session.created_at.clone(),
751            checkpointed_at: checkpointed_at.clone(),
752            hel_version: env!("CARGO_PKG_VERSION").into(),
753            relay_version: env!("CARGO_PKG_VERSION").into(),
754            adapter_version: "acp-v1".into(),
755        };
756        let releases_after_capture = exclusivity == LatchExclusivity::ReleaseAfterLatch;
757        if releases_after_capture
758            && let Some(native_session_id) = session.native_session_id.as_deref()
759        {
760            let prestage = CheckpointCaptureSpec {
761                protocol_version: CHECKPOINT_STAGING_PROTOCOL_VERSION,
762                session: session_manifest(native_session_id),
763                target: target_manifest.clone(),
764                bundle: bundle_manifest.clone(),
765                relay_root: target_path(&worker_root),
766                harness_home: target_path(&harness_home),
767                workspace_root: target_path(&workspace_root),
768                repositories: repositories.clone(),
769                allow_empty_native: false,
770                stage_path: target_path(&remote_stage),
771                refresh_existing: false,
772            };
773            let prestage_started = Instant::now();
774            let prestaged = {
775                let _recovery_copy = recovery_copy
776                    .then(|| ProvisionStageGuard::new(executor, ProvisionStage::RecoveryCopy));
777                run_checkpoint_staging_command(
778                    executor,
779                    &backend,
780                    session_id,
781                    &prestage,
782                    capture_stdin_command,
783                    "prestage target checkpoint",
784                )
785            };
786            match prestaged {
787                Ok(output) => match serde_json::from_slice::<CapturedCheckpoint>(&output.stdout) {
788                    Ok(captured) => tracing::info!(
789                        session_id,
790                        prestage_ms = prestage_started.elapsed().as_millis() as u64,
791                        native_bytes = captured.native_bytes,
792                        repository_bytes = captured.repository_bytes,
793                        reused_native = captured.reused_native,
794                        "checkpoint target state prestaged while ACP dispatch remained active"
795                    ),
796                    Err(error) => tracing::warn!(
797                        session_id,
798                        error = format!("{error:#}"),
799                        "checkpoint prestage returned an invalid result; barrier capture will replace it"
800                    ),
801                },
802                Err(error) => {
803                    if executor.cancellation_requested() {
804                        return Err(error.context("checkpoint prestage was cancelled"));
805                    }
806                    tracing::warn!(
807                        session_id,
808                        error = format!("{error:#}"),
809                        "checkpoint prestage failed; barrier capture will collect a fresh generation"
810                    );
811                }
812            }
813        }
814        let (mut relay, mut restarted_worker) = self
815            .open_checkpoint_relay(
816                session_id,
817                executor,
818                manager,
819                &backend,
820                &worker_root,
821                &reconnect,
822            )
823            .await?;
824        let (barrier, barrier_command_id) = loop {
825            // Restored native identity is not current-process readiness.
826            // Startup gets its own cancellable budget; its timeout must not
827            // enter the wedged-checkpoint worker-restart path below.
828            wait_for_native_session_in_stage(
829                relay.connection_mut(),
830                executor,
831                hel_targets::ProvisionStage::Starting,
832            )
833            .await?;
834            if exclusivity == LatchExclusivity::ReleaseAfterLatch
835                && relay.connection_mut().sync().await?.operational.execution
836                    == RelayExecutionState::Running
837            {
838                // A routine recovery copy must not open a barrier just to
839                // abandon it as soon as it observes the active turn.
840                relay.release();
841                return Err(CheckpointDeferred::harness_busy().into());
842            }
843            let barrier_command_id = new_command_id("checkpoint")?;
844            let timeout = if restarted_worker {
845                CHECKPOINT_BARRIER_TIMEOUT_AFTER_RESTART
846            } else {
847                CHECKPOINT_BARRIER_TIMEOUT
848            };
849            let result = {
850                let connection = relay.connection_mut();
851                connection
852                    .submit(
853                        barrier_command_id.clone(),
854                        RelayCommand::BeginCheckpoint {
855                            reason: Some("controller archive checkpoint".into()),
856                        },
857                    )
858                    .await?;
859                wait_for_checkpoint_barrier(
860                    connection,
861                    session_id,
862                    &barrier_command_id,
863                    timeout,
864                    BarrierBusyPolicy::of(exclusivity),
865                )
866                .await
867            };
868            match result {
869                Ok(barrier) => break (barrier, barrier_command_id),
870                Err(error)
871                    if !restarted_worker && checkpoint_barrier_needs_worker_restart(&error) =>
872                {
873                    tracing::warn!(
874                        session_id,
875                        "checkpoint requires a worker restart; restarting and retrying: {error:#}"
876                    );
877                    let connection = self
878                        .restart_worker_for_checkpoint(
879                            session_id,
880                            executor,
881                            &backend,
882                            &worker_root,
883                            &reconnect,
884                        )
885                        .await?;
886                    relay.replace_connection(connection);
887                    restarted_worker = true;
888                }
889                Err(error) => return Err(error),
890            }
891        };
892        let barrier_ready_at = Instant::now();
893        // Project memory is checkpoint state, not relay connection state.
894        // Reconcile it once while the checkpoint barrier keeps the harness
895        // idle. Ordinary attach and polling deliberately never touch it.
896        relay
897            .connection_mut()
898            .sync_project_memory()
899            .await
900            .context("synchronize project memory for checkpoint")?;
901        let cursor = barrier
902            .operational
903            .checkpoint_ready
904            .clone()
905            .context("relay reported a checkpoint barrier without its ready cursor")?;
906        let materialized = barrier.materialized;
907        let expected_ordinal = materialized.applied_event_ordinal;
908        let expected_digest = materialized.applied_event_digest.clone();
909        ensure!(
910            expected_ordinal == barrier.operational.latest_ordinal,
911            "checkpoint projection frontier {expected_ordinal} does not match relay frontier {}",
912            barrier.operational.latest_ordinal
913        );
914        ensure!(
915            expected_digest == barrier.operational.latest_digest,
916            "checkpoint projection digest does not match the relay frontier digest"
917        );
918        ensure_exact_checkpoint_cut(&cursor, expected_ordinal, &expected_digest)?;
919        let canonical_session = canonical_session_from_materialized(&materialized)?;
920        let native_session_id = barrier
921            .operational
922            .native_session_id
923            .or_else(|| session.native_session_id.clone())
924            .context("harness did not report its native session ID")?;
925
926        // The latch holds: this projection sits exactly at the barrier's ready
927        // cursor. Exporting and transferring the archive needs the barrier, not
928        // the connection, so hand it back and let the dashboard keep syncing
929        // and submitting while the slow phase runs.
930        if exclusivity == LatchExclusivity::ReleaseAfterLatch {
931            relay.end_latch();
932        }
933
934        // Reuse before exporting: verifying an installed archive costs far less
935        // than exporting and transferring an identical one. A reused archive's
936        // frontier trails the cursor its caller seals by the checkpoint's own
937        // bookkeeping events, and only by those; resume rolls the controller's
938        // projection back to the archived record.
939        if export_policy == CheckpointExportPolicy::ReuseUnchangedArchive
940            // Host worktree edits do not advance the relay frontier. Always
941            // recapture before retiring one, including archives written by
942            // older workers that only recorded its Git metadata.
943            && session.managed_worktree.is_none()
944            && let Some(artifact) = reusable_installed_checkpoint(
945                session_id,
946                session.checkpoint.as_ref(),
947                &native_session_id,
948                cursor.ordinal,
949                &canonical_session,
950            )
951        {
952            return Ok(LatchedCheckpoint {
953                artifact,
954                relay,
955                barrier_command_id,
956                cursor,
957                completion: CheckpointCompletion::HeldBarrier,
958            });
959        }
960
961        // Close must keep ACP dispatch frozen until it seals the relay, so only
962        // an ordinary checkpoint may hand dispatch back at the end of its
963        // export. `completion` also records whether an error path still has a
964        // barrier to cancel.
965        let mut completion = CheckpointCompletion::HeldBarrier;
966
967        let exported: Result<CheckpointArtifact> = async {
968            let spec = CheckpointExportSpec {
969                protocol_version: CHECKPOINT_EXPORT_PROTOCOL_VERSION,
970                session: session_manifest(&native_session_id),
971                target: target_manifest,
972                bundle: bundle_manifest,
973                relay_root: target_path(&worker_root),
974                harness_home: target_path(&harness_home),
975                workspace_root: target_path(&workspace_root),
976                repositories,
977                canonical_session,
978                output_path: target_path(&remote_archive),
979            };
980            // Only the single-shot export path measures itself here; the
981            // capture/pack path already logs its own phases above.
982            let mut export_ms: Option<u64> = None;
983            let exported = if releases_after_capture {
984                let capture_spec = CheckpointCaptureSpec {
985                    protocol_version: CHECKPOINT_STAGING_PROTOCOL_VERSION,
986                    session: spec.session.clone(),
987                    target: spec.target.clone(),
988                    bundle: spec.bundle.clone(),
989                    relay_root: spec.relay_root.clone(),
990                    harness_home: spec.harness_home.clone(),
991                    workspace_root: spec.workspace_root.clone(),
992                    repositories: spec.repositories.clone(),
993                    allow_empty_native: !canonical_session_contains_prompt(&spec.canonical_session),
994                    stage_path: target_path(&remote_stage),
995                    refresh_existing: true,
996                };
997                let capture_started = Instant::now();
998                let captured = {
999                    let _recovery_copy = recovery_copy.then(|| {
1000                        ProvisionStageGuard::new(executor, ProvisionStage::RecoveryCopy)
1001                    });
1002                    run_checkpoint_staging_command(
1003                        executor,
1004                        &backend,
1005                        session_id,
1006                        &capture_spec,
1007                        capture_stdin_command,
1008                        "capture target checkpoint",
1009                    )?
1010                };
1011                let captured: CapturedCheckpoint = serde_json::from_slice(&captured.stdout)
1012                    .context("decode captured checkpoint result")?;
1013                tracing::info!(
1014                    session_id,
1015                    capture_ms = capture_started.elapsed().as_millis() as u64,
1016                    barrier_held_ms = barrier_ready_at.elapsed().as_millis() as u64,
1017                    native_bytes = captured.native_bytes,
1018                    repository_bytes = captured.repository_bytes,
1019                    reused_native = captured.reused_native,
1020                    "checkpoint target state captured; releasing ACP dispatch"
1021                );
1022                completion = release_checkpoint_after_capture(
1023                    &mut relay,
1024                    session_id,
1025                    &barrier_command_id,
1026                    &cursor,
1027                )
1028                .await?;
1029                let pack_spec = CheckpointPackSpec {
1030                    protocol_version: CHECKPOINT_STAGING_PROTOCOL_VERSION,
1031                    relay_root: spec.relay_root.clone(),
1032                    stage_path: target_path(&remote_stage),
1033                    canonical_session: spec.canonical_session.clone(),
1034                    output_path: spec.output_path.clone(),
1035                };
1036                let pack_started = Instant::now();
1037                let output = {
1038                    let _recovery_copy = recovery_copy.then(|| {
1039                        ProvisionStageGuard::new(executor, ProvisionStage::RecoveryCopy)
1040                    });
1041                    run_checkpoint_staging_command(
1042                        executor,
1043                        &backend,
1044                        session_id,
1045                        &pack_spec,
1046                        pack_stdin_command,
1047                        "pack target checkpoint",
1048                    )?
1049                };
1050                tracing::info!(
1051                    session_id,
1052                    pack_ms = pack_started.elapsed().as_millis() as u64,
1053                    "checkpoint archive packaged after ACP dispatch resumed"
1054                );
1055                output
1056            } else {
1057                let export_started = Instant::now();
1058                let output = {
1059                    let _recovery_copy = recovery_copy.then(|| {
1060                        ProvisionStageGuard::new(executor, ProvisionStage::RecoveryCopy)
1061                    });
1062                    export_target_checkpoint(
1063                        executor,
1064                        &backend,
1065                        session_id,
1066                        &spec,
1067                        &remote_spec,
1068                    )?
1069                };
1070                export_ms = Some(export_started.elapsed().as_millis() as u64);
1071                output
1072            };
1073            let target_checkpoint: hel::hel_checkpoint::TargetCheckpoint =
1074                serde_json::from_slice(&exported.stdout)
1075                    .context("decode target checkpoint result")?;
1076            if let Some(export_ms) = export_ms {
1077                // A worker that predates the timings field reports nothing, so
1078                // the phase numbers read as zero; `timings_reported` says which.
1079                let timings = target_checkpoint.timings.unwrap_or_default();
1080                tracing::info!(
1081                    session_id,
1082                    export_ms,
1083                    timings_reported = target_checkpoint.timings.is_some(),
1084                    native_ms = timings.native_ms,
1085                    repositories_ms = timings.repositories_ms,
1086                    archive_ms = timings.archive_ms,
1087                    worker_total_ms = timings.total_ms,
1088                    "checkpoint archive exported on the target"
1089                );
1090            }
1091            if target_checkpoint.event_frontier != expected_ordinal {
1092                bail!(
1093                    "target checkpoint event frontier changed: expected {expected_ordinal}, found {}",
1094                    target_checkpoint.event_frontier
1095                );
1096            }
1097            if target_checkpoint.event_frontier_digest != expected_digest {
1098                bail!("target checkpoint event frontier digest changed");
1099            }
1100
1101            // Checkpoint archives are immutable once controller metadata points
1102            // at them. A repeated checkpoint may have the same event frontier,
1103            // so a frontier-only name could overwrite the last known-good
1104            // archive before the metadata swap commits.
1105            let archive_id = new_command_id("archive")?;
1106            let destination = sessions_dir().join(format!(
1107                "{session_id}-{}-{archive_id}.hel.zip",
1108                target_checkpoint.event_frontier
1109            ));
1110            let transfer = CheckpointTransfer {
1111                locator: &backend,
1112                session_id,
1113                remote_archive: &remote_archive,
1114                destination: &destination,
1115                expected_sha256: &target_checkpoint.sha256,
1116                expected_event_frontier: target_checkpoint.event_frontier,
1117                expected_event_frontier_digest: &target_checkpoint.event_frontier_digest,
1118            };
1119            let metadata = {
1120                let _verifying = ProvisionStageGuard::new(executor, ProvisionStage::Verifying);
1121                let transfer_started = Instant::now();
1122                let verified = transfer.execute(executor)?;
1123                tracing::info!(
1124                    session_id,
1125                    transfer_and_checksum_ms = transfer_started.elapsed().as_millis() as u64,
1126                    "checkpoint archive transferred and checksum-verified"
1127                );
1128                let installed_archive = verified.archive_path().to_path_buf();
1129                let validate_transferred = || -> Result<()> {
1130                    ensure!(
1131                        verified.sha256() == target_checkpoint.sha256,
1132                        "target and controller checkpoint checksums differ"
1133                    );
1134                    ensure!(
1135                        verified.event_frontier_digest() == expected_digest,
1136                        "verified checkpoint event frontier digest changed"
1137                    );
1138                    Ok(())
1139                };
1140                if let Err(error) = validate_transferred() {
1141                    return Err(remove_uninstalled_checkpoint(&installed_archive, error));
1142                }
1143                // A checkpoint that still holds its barrier proves workspace
1144                // consistency here instead. One that already released proved it
1145                // before releasing; the sha256 chain covers the transfer itself.
1146                if completion == CheckpointCompletion::HeldBarrier {
1147                    let revalidated = relay.sync_snapshot().await.and_then(|snapshot| {
1148                        validate_checkpoint_barrier_snapshot(
1149                            &snapshot,
1150                            &barrier_command_id,
1151                            &cursor,
1152                        )
1153                    });
1154                    if let Err(error) = revalidated {
1155                        return Err(remove_uninstalled_checkpoint(
1156                            &installed_archive,
1157                            error.context(
1158                                "checkpoint barrier changed while transferring its archive",
1159                            ),
1160                        ));
1161                    }
1162                }
1163                if let Err(error) = transfer
1164                    .cleanup_plan(&verified)
1165                    .and_then(|plan| plan.execute(executor).map(|_| ()))
1166                {
1167                    return Err(remove_uninstalled_checkpoint(
1168                        &installed_archive,
1169                        error.context("clean target checkpoint staging"),
1170                    ));
1171                }
1172                CheckpointMetadata {
1173                    archive_path: verified.archive_path().to_path_buf(),
1174                    sha256: verified.sha256().to_string(),
1175                    created_at: checkpointed_at.clone(),
1176                    event_frontier: verified.event_frontier(),
1177                }
1178            };
1179            Ok(CheckpointArtifact {
1180                metadata,
1181                native_session_id,
1182                event_frontier_digest: expected_digest,
1183            })
1184        }
1185        .await;
1186
1187        let artifact = match exported {
1188            Ok(artifact) => artifact,
1189            Err(error) => {
1190                // The barrier freezes ACP dispatch until it ends. Nothing will
1191                // complete it now, and the connection that opened it is back
1192                // with the session actor, so cancel it instead of leaving the
1193                // harness frozen until that connection happens to drop. A
1194                // barrier released after the export is already gone.
1195                if completion == CheckpointCompletion::HeldBarrier
1196                    && let Err(cancel_error) = relay.cancel_abandoned_barrier().await
1197                {
1198                    tracing::warn!(
1199                        session_id,
1200                        "failed checkpoint could not cancel its relay barrier: {cancel_error:#}"
1201                    );
1202                }
1203                return Err(error);
1204            }
1205        };
1206        Ok(LatchedCheckpoint {
1207            artifact,
1208            relay,
1209            barrier_command_id,
1210            cursor,
1211            completion,
1212        })
1213    }
1214
1215    /// Reach the session worker for a checkpoint, restarting it when the proxy
1216    /// cannot complete hello. A previous Stop can leave the daemon dead; failing
1217    /// that first connect without a bounce never gets to the barrier retry.
1218    async fn open_checkpoint_relay(
1219        &self,
1220        session_id: &str,
1221        executor: &(impl CommandExecutor + Sync),
1222        manager: Option<&SessionManagerControl>,
1223        backend: &hel_targets::TargetLocator,
1224        worker_root: &str,
1225        reconnect: &hel_targets::CommandSpec,
1226    ) -> Result<(ControllerRelayLease, bool)> {
1227        let project_memory = match self.project_memory_sync_target(session_id) {
1228            Ok(target) => Some(target),
1229            Err(error) => {
1230                tracing::warn!(
1231                    session_id,
1232                    error = format!("{error:#}"),
1233                    "project memory will not be synchronized during checkpoint reconnect"
1234                );
1235                None
1236            }
1237        };
1238        match connect_checkpoint_relay(session_id, manager, reconnect, project_memory.clone()).await
1239        {
1240            Ok(relay) => Ok((relay, false)),
1241            Err(error) if worker_connect_needs_restart(&error) => {
1242                tracing::warn!(
1243                    session_id,
1244                    "checkpoint could not reach the worker; restarting it: {error:#}"
1245                );
1246                let mut connection = self
1247                    .restart_worker_for_checkpoint(
1248                        session_id,
1249                        executor,
1250                        backend,
1251                        worker_root,
1252                        reconnect,
1253                    )
1254                    .await?;
1255                connection.set_project_memory_target(project_memory);
1256                let relay =
1257                    adopt_restarted_checkpoint_relay(session_id, manager, connection).await?;
1258                Ok((relay, true))
1259            }
1260            Err(error) => Err(error).context("connect to the session worker for checkpoint"),
1261        }
1262    }
1263
1264    /// Kill a worker whose ACP turn will not finish, install the current
1265    /// binary, and reconnect. Restart recovery interrupts the in-flight prompt
1266    /// so a later BeginCheckpoint can be admitted.
1267    async fn restart_worker_for_checkpoint(
1268        &self,
1269        session_id: &str,
1270        executor: &(impl CommandExecutor + Sync),
1271        backend: &hel_targets::TargetLocator,
1272        worker_root: &str,
1273        reconnect: &hel_targets::CommandSpec,
1274    ) -> Result<StandaloneSession> {
1275        self.restart_worker_with_installed_binary(
1276            session_id,
1277            executor,
1278            InstalledWorkerRestart {
1279                backend,
1280                worker_root,
1281                reconnect,
1282                launch: None,
1283                messages: &RESTART_FOR_CHECKPOINT,
1284            },
1285        )
1286        .await
1287    }
1288}
1289
1290async fn connect_checkpoint_relay(
1291    session_id: &str,
1292    manager: Option<&SessionManagerControl>,
1293    reconnect: &hel_targets::CommandSpec,
1294    project_memory: Option<crate::hel_session_manager::ProjectMemorySyncTarget>,
1295) -> Result<ControllerRelayLease> {
1296    if let Some(manager) = manager {
1297        let handle = manager
1298            .wait_for_session(session_id, Duration::from_secs(5))
1299            .await?;
1300        let mut lease = handle.lease_connection().await?;
1301        lease
1302            .connection_mut()
1303            .set_project_memory_target(project_memory);
1304        Ok(ControllerRelayLease::Managed {
1305            handle,
1306            lease: Some(lease),
1307        })
1308    } else {
1309        let target = crate::hel_session_manager::RelaySessionTarget {
1310            session_id: session_id.to_owned(),
1311            spec: reconnect.clone(),
1312            worker_recovery: None,
1313            project_memory,
1314        };
1315        Ok(ControllerRelayLease::Standalone(
1316            StandaloneSession::connect(&target).await?,
1317        ))
1318    }
1319}
1320
1321async fn adopt_restarted_checkpoint_relay(
1322    session_id: &str,
1323    manager: Option<&SessionManagerControl>,
1324    connection: StandaloneSession,
1325) -> Result<ControllerRelayLease> {
1326    let Some(manager) = manager else {
1327        return Ok(ControllerRelayLease::Standalone(connection));
1328    };
1329    let handle = manager
1330        .wait_for_session(session_id, Duration::from_secs(5))
1331        .await?;
1332    match handle.lease_connection().await {
1333        Ok(mut lease) => {
1334            lease.replace_connection(connection);
1335            Ok(ControllerRelayLease::Managed {
1336                handle,
1337                lease: Some(lease),
1338            })
1339        }
1340        Err(error) => {
1341            tracing::warn!(
1342                session_id,
1343                "session actor could not lease after worker restart; using the restarted proxy: {error:#}"
1344            );
1345            Ok(ControllerRelayLease::Standalone(connection))
1346        }
1347    }
1348}
1349
1350/// What waiting for a barrier does while the session is working.
1351#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1352enum BarrierBusyPolicy {
1353    /// Give up as soon as the session is seen working. A checkpoint that can
1354    /// run again later has nothing to gain from holding a barrier behind a
1355    /// prompt or a turn the harness started on its own: the wait would only
1356    /// end at the deadline, and the deadline means "wedged", which restarts
1357    /// the worker and kills the work in flight.
1358    DeferWhileRunning,
1359    /// Request non-steering cancellation and wait for the turn to settle.
1360    /// Close may interrupt work, but only an unresponsive or incompatible
1361    /// worker needs restart recovery.
1362    InterruptWhileRunning,
1363}
1364
1365impl BarrierBusyPolicy {
1366    fn of(exclusivity: LatchExclusivity) -> Self {
1367        match exclusivity {
1368            LatchExclusivity::ReleaseAfterLatch => Self::DeferWhileRunning,
1369            LatchExclusivity::HoldThroughClose => Self::InterruptWhileRunning,
1370        }
1371    }
1372}
1373
1374async fn wait_for_checkpoint_barrier(
1375    relay: &mut StandaloneSession,
1376    session_id: &str,
1377    command_id: &str,
1378    timeout: Duration,
1379    busy: BarrierBusyPolicy,
1380) -> Result<ManagedSessionSnapshot> {
1381    let deadline = tokio::time::Instant::now() + timeout;
1382    let mut cancel_submitted = false;
1383    let mut cancel_deadline = None;
1384    let mut cancel_started_at: Option<Instant> = None;
1385    loop {
1386        let snapshot = relay.sync().await?;
1387        if checkpoint_barrier_is_ready(&snapshot, command_id) {
1388            if let Some(started_at) = cancel_started_at {
1389                tracing::info!(
1390                    session_id,
1391                    barrier_command_id = command_id,
1392                    cancellation_ms = started_at.elapsed().as_millis() as u64,
1393                    "active turn cancellation settled before checkpoint barrier"
1394                );
1395            }
1396            return Ok(snapshot);
1397        }
1398        if busy == BarrierBusyPolicy::InterruptWhileRunning
1399            && snapshot.operational.execution == RelayExecutionState::Running
1400            && !cancel_submitted
1401        {
1402            let cancel_turn = RelayCommand::CancelTurn;
1403            if relay.protocol_version() < cancel_turn.minimum_protocol() {
1404                return Err(CheckpointBarrierUnreachable::cancel_turn_unavailable(
1405                    command_id,
1406                    relay.protocol_version(),
1407                )
1408                .into());
1409            }
1410            let cancel_command_id = new_command_id("checkpoint-cancel-turn")?;
1411            match relay.submit(cancel_command_id, cancel_turn).await {
1412                Ok(_) => {
1413                    cancel_submitted = true;
1414                    cancel_started_at = Some(Instant::now());
1415                    cancel_deadline = Some(tokio::time::Instant::now() + CHECKPOINT_CANCEL_TIMEOUT);
1416                    tracing::info!(
1417                        session_id,
1418                        barrier_command_id = command_id,
1419                        "requested active turn cancellation before checkpoint barrier"
1420                    );
1421                }
1422                Err(error) if checkpoint_cancel_turn_needs_worker_restart(&error) => {
1423                    return Err(error.context(
1424                        CheckpointBarrierUnreachable::cancel_turn_unavailable(
1425                            command_id,
1426                            relay.protocol_version(),
1427                        ),
1428                    ));
1429                }
1430                Err(error) if worker_connect_needs_restart(&error) => {
1431                    return Err(error.context(
1432                        CheckpointBarrierUnreachable::cancel_turn_unreachable(command_id),
1433                    ));
1434                }
1435                Err(error) => {
1436                    // The turn can finish between the status sync and this
1437                    // submit. If the barrier won that race, continue from its
1438                    // durable ready state; otherwise preserve the rejection.
1439                    if let Ok(snapshot) = relay.sync().await
1440                        && checkpoint_barrier_is_ready(&snapshot, command_id)
1441                    {
1442                        tracing::info!(
1443                            session_id,
1444                            barrier_command_id = command_id,
1445                            "active turn settled while submitting checkpoint cancellation"
1446                        );
1447                        return Ok(snapshot);
1448                    }
1449                    return Err(error.context("cancel active ACP turn before checkpoint barrier"));
1450                }
1451            }
1452            continue;
1453        }
1454        let out_of_time = tokio::time::Instant::now() >= cancel_deadline.unwrap_or(deadline);
1455        if let Some(error) = checkpoint_barrier_wait_ended(
1456            &snapshot,
1457            command_id,
1458            busy,
1459            out_of_time,
1460            cancel_submitted,
1461        ) {
1462            return Err(error);
1463        }
1464        tokio::time::sleep(std::time::Duration::from_millis(100)).await;
1465    }
1466}
1467
1468/// Why one sync of a barrier that is not ready yet ends the wait, or `None` to
1469/// keep waiting.
1470///
1471/// The deadline means "wedged": it restarts the worker only after a close has
1472/// already requested cancellation and the turn still has not settled. A
1473/// checkpoint that can try again later defers as soon as it sees work.
1474fn checkpoint_barrier_wait_ended(
1475    snapshot: &ManagedSessionSnapshot,
1476    command_id: &str,
1477    busy: BarrierBusyPolicy,
1478    out_of_time: bool,
1479    cancel_submitted: bool,
1480) -> Option<anyhow::Error> {
1481    if snapshot.operational.execution == RelayExecutionState::Closed {
1482        return Some(CheckpointBarrierUnreachable::runtime_stopped().into());
1483    }
1484    if snapshot.operational.execution == RelayExecutionState::Running {
1485        return Some(match busy {
1486            BarrierBusyPolicy::DeferWhileRunning => CheckpointDeferred::harness_busy().into(),
1487            BarrierBusyPolicy::InterruptWhileRunning if out_of_time && cancel_submitted => {
1488                CheckpointBarrierUnreachable::cancel_timed_out(command_id).into()
1489            }
1490            BarrierBusyPolicy::InterruptWhileRunning => return None,
1491        });
1492    }
1493    out_of_time.then(|| CheckpointBarrierUnreachable::not_admitted(command_id).into())
1494}
1495
1496/// The ACP runtime never admitted a checkpoint barrier: it stopped first, or it
1497/// never reached the barrier before the deadline.
1498///
1499/// [`wait_for_checkpoint_barrier`] is the only producer, and the retry decision
1500/// downcasts for this marker rather than reading the message, so rewording a
1501/// diagnostic cannot silently disable the restart-and-retry path.
1502#[derive(Debug)]
1503struct CheckpointBarrierUnreachable(String);
1504
1505impl CheckpointBarrierUnreachable {
1506    fn runtime_stopped() -> Self {
1507        Self("ACP runtime stopped before reaching the checkpoint barrier".to_owned())
1508    }
1509
1510    fn not_admitted(command_id: &str) -> Self {
1511        Self(format!(
1512            "ACP relay did not reach checkpoint barrier {command_id}"
1513        ))
1514    }
1515
1516    fn cancel_timed_out(command_id: &str) -> Self {
1517        Self(format!(
1518            "active ACP turn did not settle after cancellation before checkpoint barrier {command_id}"
1519        ))
1520    }
1521
1522    fn cancel_turn_unavailable(command_id: &str, protocol_version: u32) -> Self {
1523        Self(format!(
1524            "worker protocol {protocol_version} cannot cancel the active ACP turn before checkpoint barrier {command_id} (requires protocol {})",
1525            RelayCommand::CancelTurn.minimum_protocol(),
1526        ))
1527    }
1528
1529    fn cancel_turn_unreachable(command_id: &str) -> Self {
1530        Self(format!(
1531            "worker transport became unavailable while cancelling the active ACP turn before checkpoint barrier {command_id}"
1532        ))
1533    }
1534}
1535
1536impl std::fmt::Display for CheckpointBarrierUnreachable {
1537    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1538        formatter.write_str(&self.0)
1539    }
1540}
1541
1542impl std::error::Error for CheckpointBarrierUnreachable {}
1543
1544fn checkpoint_barrier_needs_worker_restart(error: &anyhow::Error) -> bool {
1545    error
1546        .downcast_ref::<CheckpointBarrierUnreachable>()
1547        .is_some()
1548}
1549
1550/// A worker that reports an incompatible protocol for `CancelTurn` needs to be
1551/// replaced before the close can retry the checkpoint with cancellation
1552/// available. The negotiated protocol is checked before submission; this
1553/// handles a race with a worker-side protocol rejection as well.
1554fn checkpoint_cancel_turn_needs_worker_restart(error: &anyhow::Error) -> bool {
1555    error.chain().any(|cause| {
1556        let Some(rejected) = cause.downcast_ref::<RelayRejected>() else {
1557            return false;
1558        };
1559        rejected.0.code == hel::hel_worker::RelayErrorCode::IncompatibleProtocol
1560    })
1561}
1562
1563/// The session was working, so this checkpoint did not run. Nothing is wrong
1564/// with the session, the target, or the last archive.
1565///
1566/// A busy session is the normal state of a session someone is using, including
1567/// one working through a turn the harness started on its own after a
1568/// background command. Treating that as a checkpoint failure would restart the
1569/// worker, record a failure against the session, and back the next attempt off
1570/// for hours. Callers that can try again later defer instead; the same work is
1571/// copied at the next idle observation.
1572#[derive(Debug)]
1573pub struct CheckpointDeferred(String);
1574
1575impl CheckpointDeferred {
1576    pub(crate) fn harness_busy() -> Self {
1577        Self("the agent is working; try again when it is idle".to_owned())
1578    }
1579
1580    fn frontier_moved() -> Self {
1581        Self(
1582            "the session moved past the checkpoint-ready cursor before the barrier latched, so this checkpoint was deferred"
1583                .to_owned(),
1584        )
1585    }
1586
1587    fn harness_turn_during_capture() -> Self {
1588        Self(
1589            "the agent started a turn of its own while target state was captured, so this checkpoint was deferred"
1590                .to_owned(),
1591        )
1592    }
1593}
1594
1595impl std::fmt::Display for CheckpointDeferred {
1596    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1597        formatter.write_str(&self.0)
1598    }
1599}
1600
1601impl std::error::Error for CheckpointDeferred {}
1602
1603/// Whether a failed checkpoint only means the session was busy.
1604///
1605/// The marker is carried by the error, not by its text, and callers wrap
1606/// checkpoint errors in context, so the whole chain is searched.
1607pub fn checkpoint_was_deferred(error: &anyhow::Error) -> bool {
1608    error
1609        .chain()
1610        .any(|cause| cause.downcast_ref::<CheckpointDeferred>().is_some())
1611}
1612
1613fn checkpoint_barrier_is_ready(snapshot: &ManagedSessionSnapshot, command_id: &str) -> bool {
1614    snapshot.operational.checkpoint_barrier.as_deref() == Some(command_id)
1615        && snapshot.operational.checkpoint_ready.is_some()
1616}
1617
1618/// The latched projection must sit exactly at the barrier's ready cursor.
1619///
1620/// The barrier was admitted, but the relay can record more events before the
1621/// controller latches - the harness spoke again in the gap. The archive would
1622/// not be an exact cut of the session, so the attempt is dropped and the next
1623/// idle observation copies the settled session instead. This is not a fault in
1624/// the session, the target, or the last archive.
1625fn ensure_exact_checkpoint_cut(
1626    cursor: &RelayCursor,
1627    expected_ordinal: u64,
1628    expected_digest: &str,
1629) -> Result<()> {
1630    if cursor.ordinal != expected_ordinal || cursor.digest != expected_digest {
1631        bail!(CheckpointDeferred::frontier_moved());
1632    }
1633    Ok(())
1634}
1635
1636/// Prove the barrier that latched an archive is still the same barrier, still
1637/// held at the same ready cursor.
1638///
1639/// The relay frontier may have moved past that cursor: an active ordinary
1640/// barrier still accepts and journals submissions, it only freezes ACP
1641/// dispatch. Nothing the harness could write reaches the workspace while
1642/// dispatch is frozen, so an advanced frontier does not invalidate the archive.
1643/// Requiring frontier equality here would fail every checkpoint that overlapped
1644/// a prompt.
1645///
1646/// A turn the harness starts on its own is the exception. The barrier freezes
1647/// Mjolnir's dispatch, not the harness, so a harness turn that opened after the
1648/// cursor was captured means the agent may have been writing to the workspace
1649/// while it was staged. That archive is abandoned rather than installed.
1650fn validate_checkpoint_barrier_snapshot(
1651    snapshot: &ManagedSessionSnapshot,
1652    command_id: &str,
1653    expected: &RelayCursor,
1654) -> Result<()> {
1655    ensure!(
1656        snapshot.operational.checkpoint_barrier.as_deref() == Some(command_id),
1657        "checkpoint barrier {command_id} is no longer active"
1658    );
1659    ensure!(
1660        snapshot.operational.checkpoint_ready.as_ref() == Some(expected),
1661        "checkpoint barrier {command_id} has a different ready cursor"
1662    );
1663    if snapshot
1664        .operational
1665        .last_harness_turn_started_ordinal
1666        .is_some_and(|ordinal| ordinal > expected.ordinal)
1667    {
1668        bail!(CheckpointDeferred::harness_turn_during_capture());
1669    }
1670    Ok(())
1671}
1672
1673fn remove_uninstalled_checkpoint(path: &Path, error: anyhow::Error) -> anyhow::Error {
1674    match std::fs::remove_file(path) {
1675        Ok(()) => error,
1676        Err(remove_error) if remove_error.kind() == std::io::ErrorKind::NotFound => error,
1677        Err(remove_error) => error.context(format!(
1678            "also failed to remove uninstalled checkpoint {}: {remove_error}",
1679            path.display()
1680        )),
1681    }
1682}
1683
1684pub(super) async fn wait_for_relay_closed(relay: &mut StandaloneSession) -> Result<()> {
1685    let deadline = tokio::time::Instant::now() + std::time::Duration::from_secs(30);
1686    loop {
1687        if relay.sync().await?.operational.execution == RelayExecutionState::Closed {
1688            return Ok(());
1689        }
1690        if tokio::time::Instant::now() >= deadline {
1691            bail!("ACP runtime did not close within 30 seconds");
1692        }
1693        tokio::time::sleep(std::time::Duration::from_millis(100)).await;
1694    }
1695}
1696
1697/// Hand ACP dispatch back as soon as target-owned state is sealed.
1698///
1699/// Proving the barrier first moves the workspace-consistency proof ahead of the
1700/// release: the same barrier still holding the same ready cursor means nothing
1701/// the harness could write reached the workspace while the stage was captured.
1702/// The recovery floor stays put, because nothing yet proves the archive reached
1703/// the controller's disk.
1704///
1705/// A worker that does not understand the release keeps its barrier, and the
1706/// caller falls back to ending it only after the archive is installed. That is
1707/// slower, not wrong, so it is not a checkpoint failure.
1708async fn release_checkpoint_after_capture(
1709    relay: &mut ControllerRelayLease,
1710    session_id: &str,
1711    barrier_command_id: &str,
1712    cursor: &RelayCursor,
1713) -> Result<CheckpointCompletion> {
1714    relay
1715        .sync_snapshot()
1716        .await
1717        .and_then(|snapshot| {
1718            validate_checkpoint_barrier_snapshot(&snapshot, barrier_command_id, cursor)
1719        })
1720        .context("checkpoint barrier changed while capturing target state")?;
1721    match relay
1722        .submit(
1723            new_command_id("checkpoint-release")?,
1724            RelayCommand::ReleaseCheckpoint {
1725                barrier_command_id: barrier_command_id.to_owned(),
1726            },
1727        )
1728        .await
1729    {
1730        Ok(_) => Ok(CheckpointCompletion::ReleasedAfterCapture),
1731        Err(error) => {
1732            tracing::debug!(
1733                session_id,
1734                "relay kept the checkpoint barrier through the transfer: {error:#}"
1735            );
1736            Ok(CheckpointCompletion::HeldBarrier)
1737        }
1738    }
1739}
1740
1741fn run_checkpoint_staging_command<T: serde::Serialize>(
1742    executor: &impl CommandExecutor,
1743    locator: &hel_targets::TargetLocator,
1744    session_id: &str,
1745    spec: &T,
1746    command: fn(&hel_targets::TargetLocator, &str) -> Result<CommandSpec>,
1747    operation: &str,
1748) -> Result<CommandOutput> {
1749    let body = serde_json::to_vec(spec).with_context(|| format!("serialize {operation} spec"))?;
1750    let mut replaced_worker = false;
1751    loop {
1752        let command = command(locator, session_id)?;
1753        let output = executor.execute_with_stdin(&command, &mut body.as_slice())?;
1754        if output.status == 0 {
1755            return Ok(output);
1756        }
1757        let failure = String::from_utf8_lossy(&output.stderr).into_owned();
1758        if staging_protocol_unsupported(&failure)
1759            && replace_stale_export_worker(
1760                executor,
1761                locator,
1762                session_id,
1763                None,
1764                &failure,
1765                &mut replaced_worker,
1766            )?
1767        {
1768            continue;
1769        }
1770        bail!(
1771            "{operation} failed with status {}: {failure}",
1772            output.status
1773        );
1774    }
1775}
1776
1777/// Run the target's checkpoint export with the spec streamed over stdin.
1778///
1779/// Streaming removes a whole `podman cp`/`scp` round trip from the window in
1780/// which the relay barrier keeps ACP dispatch frozen.
1781fn export_target_checkpoint(
1782    executor: &impl CommandExecutor,
1783    locator: &hel_targets::TargetLocator,
1784    session_id: &str,
1785    spec: &CheckpointExportSpec,
1786    remote_spec: &str,
1787) -> Result<CommandOutput> {
1788    export_target_checkpoint_with_worker(executor, locator, session_id, spec, remote_spec, None)
1789}
1790
1791fn export_target_checkpoint_with_worker(
1792    executor: &impl CommandExecutor,
1793    locator: &hel_targets::TargetLocator,
1794    session_id: &str,
1795    spec: &CheckpointExportSpec,
1796    remote_spec: &str,
1797    worker_binary: Option<&Path>,
1798) -> Result<CommandOutput> {
1799    let body = serde_json::to_vec(spec).context("serialize checkpoint export spec")?;
1800    let mut replaced_worker = false;
1801    loop {
1802        let streamed = export_stdin_command(locator, session_id)?;
1803        let output = executor.execute_with_stdin(&streamed, &mut body.as_slice())?;
1804        if output.status == 0 {
1805            return Ok(output);
1806        }
1807        let failure = String::from_utf8_lossy(&output.stderr).into_owned();
1808        if export_spec_stdin_unsupported(&failure) {
1809            tracing::debug!(
1810                session_id,
1811                "target worker predates streamed checkpoint specs; uploading the spec file instead"
1812            );
1813            let output = export_uploaded_spec(executor, locator, session_id, spec, remote_spec)?;
1814            if output.status == 0 {
1815                return Ok(output);
1816            }
1817            let failure = String::from_utf8_lossy(&output.stderr).into_owned();
1818            if replace_stale_export_worker(
1819                executor,
1820                locator,
1821                session_id,
1822                worker_binary,
1823                &failure,
1824                &mut replaced_worker,
1825            )? {
1826                continue;
1827            }
1828            bail!(
1829                "export target checkpoint failed with status {}: {failure}",
1830                output.status
1831            );
1832        }
1833        if replace_stale_export_worker(
1834            executor,
1835            locator,
1836            session_id,
1837            worker_binary,
1838            &failure,
1839            &mut replaced_worker,
1840        )? {
1841            continue;
1842        }
1843        bail!(
1844            "{} failed with status {}: {failure}",
1845            streamed.purpose,
1846            output.status
1847        );
1848    }
1849}
1850
1851fn export_uploaded_spec(
1852    executor: &impl CommandExecutor,
1853    locator: &hel_targets::TargetLocator,
1854    session_id: &str,
1855    spec: &CheckpointExportSpec,
1856    remote_spec: &str,
1857) -> Result<CommandOutput> {
1858    let staging = tempfile::tempdir().context("create checkpoint staging")?;
1859    let local_spec = staging.path().join("checkpoint-spec.json");
1860    spec.write(&local_spec)?;
1861    upload_checkpoint_spec(executor, locator, session_id, &local_spec, remote_spec)?;
1862    executor.execute(&export_command(locator, session_id, remote_spec)?)
1863}
1864
1865/// When the installed worker cannot execute this export protocol, replace its
1866/// `mj` with the controller's current binary and tell the caller to retry. The
1867/// live daemon keeps the previous inode; only the next `export-checkpoint`
1868/// process changes.
1869fn replace_stale_export_worker(
1870    executor: &impl CommandExecutor,
1871    locator: &hel_targets::TargetLocator,
1872    session_id: &str,
1873    worker_binary: Option<&Path>,
1874    failure: &str,
1875    replaced_worker: &mut bool,
1876) -> Result<bool> {
1877    if *replaced_worker || !staging_protocol_unsupported(failure) {
1878        return Ok(false);
1879    }
1880    tracing::debug!(
1881        session_id,
1882        "target worker does not support this checkpoint export protocol; replacing the installed Mjolnir binary and retrying"
1883    );
1884    let owned_binary;
1885    let binary = if let Some(path) = worker_binary {
1886        path
1887    } else {
1888        owned_binary = super::worker_binary::worker_binary_for(locator, executor)?;
1889        owned_binary.as_path()
1890    };
1891    super::worker_binary::replace_installed_worker_binary(executor, locator, session_id, binary)?;
1892    *replaced_worker = true;
1893    Ok(true)
1894}
1895
1896/// Whether an export failure says the target's worker cannot read its spec from
1897/// standard input.
1898///
1899/// A worker built before `--spec -` treats the dash as a file name, so it fails
1900/// while reading that file rather than while running the checkpoint. One built
1901/// before the flag existed at all fails in argument parsing. Every other
1902/// failure is a real checkpoint error and must surface.
1903fn export_spec_stdin_unsupported(failure: &str) -> bool {
1904    failure.contains("read checkpoint export spec -")
1905        || failure.contains("unexpected argument")
1906        || failure.contains("invalid value")
1907}
1908
1909/// Whether an export failure says the target's worker cannot deserialize this
1910/// spec. `CheckpointExportSpec` and its nested canonical snapshot use
1911/// `deny_unknown_fields`, so a controller that gained a field such as
1912/// `terminal_refs` cannot pause a session whose installed `mj` predates it.
1913fn export_spec_schema_unsupported(failure: &str) -> bool {
1914    failure.contains("parse checkpoint")
1915        && (failure.contains("unknown field") || failure.contains("unknown variant"))
1916}
1917
1918fn export_protocol_unsupported(failure: &str) -> bool {
1919    export_spec_schema_unsupported(failure)
1920        || failure.contains("unsupported checkpoint export protocol version")
1921}
1922
1923fn staging_protocol_unsupported(failure: &str) -> bool {
1924    export_protocol_unsupported(failure)
1925        || failure.contains("unrecognized subcommand")
1926        || failure.contains("unexpected argument")
1927}
1928
1929pub(super) fn upload_checkpoint_spec(
1930    executor: &impl CommandExecutor,
1931    locator: &hel_targets::TargetLocator,
1932    session_id: &str,
1933    local: &Path,
1934    remote: &str,
1935) -> Result<()> {
1936    match locator {
1937        hel_targets::TargetLocator::LocalBare { .. } => {
1938            std::fs::copy(local, remote)
1939                .with_context(|| format!("copy checkpoint specification to {remote}"))?;
1940            Ok(())
1941        }
1942        hel_targets::TargetLocator::LocalPodman { container_id, .. } => execute_checked(
1943            executor,
1944            CommandSpec::new(
1945                "podman",
1946                [
1947                    "cp".into(),
1948                    local.to_string_lossy().into_owned(),
1949                    format!("{container_id}:{remote}"),
1950                ],
1951            )
1952            .purpose("upload checkpoint specification"),
1953        )
1954        .map(|_| ()),
1955        hel_targets::TargetLocator::LocalDocker { container_id } => execute_checked(
1956            executor,
1957            CommandSpec::new(
1958                "docker",
1959                [
1960                    "cp".into(),
1961                    local.to_string_lossy().into_owned(),
1962                    format!("{container_id}:{remote}"),
1963                ],
1964            )
1965            .purpose("upload checkpoint specification"),
1966        )
1967        .map(|_| ()),
1968        hel_targets::TargetLocator::AppleContainer { container_id } => execute_checked(
1969            executor,
1970            CommandSpec::new(
1971                "container",
1972                [
1973                    "cp".into(),
1974                    local.to_string_lossy().into_owned(),
1975                    format!("{container_id}:{remote}"),
1976                ],
1977            )
1978            .purpose("upload checkpoint specification"),
1979        )
1980        .map(|_| ()),
1981        hel_targets::TargetLocator::AwsEc2 { ssh, .. }
1982        | hel_targets::TargetLocator::SshBare { ssh, .. } => execute_checked(
1983            executor,
1984            scp_command_spec(ssh, local, remote, false).purpose("upload checkpoint specification"),
1985        )
1986        .map(|_| ()),
1987        hel_targets::TargetLocator::SshPodman {
1988            ssh, container_id, ..
1989        }
1990        | hel_targets::TargetLocator::SshDocker { ssh, container_id } => {
1991            let engine = match locator {
1992                hel_targets::TargetLocator::SshPodman { .. } => "podman",
1993                hel_targets::TargetLocator::SshDocker { .. } => "docker",
1994                _ => unreachable!("matched remote container target"),
1995            };
1996            let staging = format!(".local/share/hel/uploads/{session_id}-checkpoint.json");
1997            execute_checked(
1998                executor,
1999                ssh_command_spec(ssh, ["mkdir", "-p", ".local/share/hel/uploads"])
2000                    .purpose("create remote checkpoint staging"),
2001            )?;
2002            execute_checked(
2003                executor,
2004                scp_command_spec(ssh, local, &staging, false)
2005                    .purpose("upload remote container checkpoint specification"),
2006            )?;
2007            execute_checked(
2008                executor,
2009                ssh_command_spec(
2010                    ssh,
2011                    [engine, "cp", &staging, &format!("{container_id}:{remote}")],
2012                )
2013                .purpose("install remote container checkpoint specification"),
2014            )?;
2015            execute_checked(
2016                executor,
2017                ssh_command_spec(ssh, ["rm", "-f", "--", &staging])
2018                    .purpose("remove remote checkpoint staging"),
2019            )?;
2020            Ok(())
2021        }
2022    }?;
2023    Ok(())
2024}
2025
2026/// The artifact a latched checkpoint may keep instead of exporting a new one,
2027/// or `None` when a full export has to run.
2028///
2029/// Every relay command is journalled, checkpoint plumbing included, so the
2030/// event frontier always moves between two checkpoints. Session content is
2031/// what decides whether the installed archive still represents the session.
2032/// Every reason to decline is reported; none of them fails the checkpoint.
2033fn reusable_installed_checkpoint(
2034    session_id: &str,
2035    installed: Option<&CheckpointMetadata>,
2036    native_session_id: &str,
2037    latched_ordinal: u64,
2038    latched_session: &CanonicalSessionSnapshot,
2039) -> Option<CheckpointArtifact> {
2040    let installed = installed?;
2041    if installed.event_frontier > latched_ordinal {
2042        tracing::warn!(
2043            session_id,
2044            installed_frontier = installed.event_frontier,
2045            latched_ordinal,
2046            "installed checkpoint is ahead of the latched cursor; exporting a fresh archive"
2047        );
2048        return None;
2049    }
2050    let verified = match verify_archive_streaming(&installed.archive_path) {
2051        Ok(verified) => verified,
2052        Err(error) => {
2053            tracing::warn!(
2054                session_id,
2055                path = %installed.archive_path.display(),
2056                "installed checkpoint could not be verified for reuse: {error:#}"
2057            );
2058            return None;
2059        }
2060    };
2061    if verified.archive_sha256 != installed.sha256
2062        || verified.manifest.session.id != session_id
2063        || verified.canonical_session.event_frontier != installed.event_frontier
2064    {
2065        tracing::warn!(
2066            session_id,
2067            path = %installed.archive_path.display(),
2068            "installed checkpoint no longer matches its controller metadata; exporting a fresh archive"
2069        );
2070        return None;
2071    }
2072    if !verified.canonical_session.content_matches(latched_session) {
2073        tracing::info!(
2074            session_id,
2075            archive_frontier = verified.canonical_session.event_frontier,
2076            latched_ordinal,
2077            "session content changed since the installed checkpoint; exporting a fresh archive"
2078        );
2079        return None;
2080    }
2081    tracing::info!(
2082        session_id,
2083        archive_frontier = verified.canonical_session.event_frontier,
2084        latched_ordinal,
2085        "reusing the installed checkpoint archive; only relay bookkeeping moved"
2086    );
2087    Some(CheckpointArtifact {
2088        metadata: installed.clone(),
2089        native_session_id: native_session_id.to_owned(),
2090        event_frontier_digest: verified.canonical_session.event_frontier_digest,
2091    })
2092}
2093
2094pub(super) fn verify_installed_checkpoint_gate(
2095    session_id: &str,
2096    checkpoint: &CheckpointMetadata,
2097) -> Result<()> {
2098    let sha256 = checkpoint_sha256(&checkpoint.archive_path).with_context(|| {
2099        format!(
2100            "hash installed checkpoint {} before target cleanup",
2101            checkpoint.archive_path.display()
2102        )
2103    })?;
2104    ensure!(
2105        sha256 == checkpoint.sha256,
2106        "refusing target cleanup for session {session_id}: installed checkpoint SHA changed"
2107    );
2108    Ok(())
2109}
2110
2111fn verify_checkpoint_artifact(session_id: &str, artifact: &CheckpointArtifact) -> Result<()> {
2112    let sha256 = checkpoint_sha256(&artifact.metadata.archive_path).with_context(|| {
2113        format!(
2114            "hash completed checkpoint {}",
2115            artifact.metadata.archive_path.display()
2116        )
2117    })?;
2118    ensure!(
2119        sha256 == artifact.metadata.sha256,
2120        "completed checkpoint SHA changed before persistence for session {session_id}"
2121    );
2122    Ok(())
2123}
2124
2125/// Release the projection history the new checkpoint covers.
2126///
2127/// The checkpoint archive holds the complete transcript up to its frontier, so
2128/// the tool output stored below that frontier is a second copy of something
2129/// already durable. Reclaiming it is housekeeping: a checkpoint that is
2130/// verified and persisted stays good whether or not this succeeds, so a
2131/// failure is logged rather than returned.
2132pub(super) fn release_projection_behind_checkpoint(session_id: &str, current: &CheckpointMetadata) {
2133    match hel::hel_database::compact_materialized_transcript_through(
2134        session_id,
2135        current.event_frontier,
2136    ) {
2137        Ok(retention) if retention.items == 0 => {}
2138        Ok(retention) => tracing::info!(
2139            session_id,
2140            items = retention.items,
2141            bytes = retention.bytes,
2142            remaining = retention.remaining,
2143            event_frontier = current.event_frontier,
2144            "released projection history the checkpoint covers"
2145        ),
2146        Err(error) => tracing::warn!(
2147            session_id,
2148            "checkpoint was saved, but the projection history it covers could not be released: {error:#}"
2149        ),
2150    }
2151}
2152
2153pub(super) fn prune_replaced_checkpoint(
2154    previous: Option<&CheckpointMetadata>,
2155    current: &CheckpointMetadata,
2156) {
2157    let Some(previous) = previous.filter(|old| old.archive_path != current.archive_path) else {
2158        return;
2159    };
2160    match hel::hel_database::move_checkpoint_is_retained(&previous.archive_path) {
2161        Ok(true) => return,
2162        Ok(false) => {}
2163        Err(error) => {
2164            tracing::warn!(%error, "could not check move retention; keeping superseded checkpoint");
2165            return;
2166        }
2167    }
2168    if let Err(error) = std::fs::remove_file(&previous.archive_path)
2169        && error.kind() != std::io::ErrorKind::NotFound
2170    {
2171        tracing::warn!(
2172            path = %previous.archive_path.display(),
2173            "could not remove superseded recovery copy: {error}"
2174        );
2175    }
2176}
2177
2178#[cfg(test)]
2179mod tests {
2180    use std::cell::{Cell, RefCell};
2181    use std::collections::BTreeMap;
2182    use std::fs::OpenOptions;
2183    use std::path::{Path, PathBuf};
2184    #[cfg(unix)]
2185    use std::process::Command;
2186    #[cfg(unix)]
2187    use std::time::Duration;
2188
2189    #[cfg(unix)]
2190    use agent_client_protocol::schema::v1::{ContentBlock, TextContent};
2191    use anyhow::Result;
2192
2193    #[cfg(unix)]
2194    use crate::hel_controller::now;
2195    use crate::hel_controller::restore_session_after_persistence_failure;
2196    use crate::hel_controller::test_support::{
2197        checkpoint_test_session, write_checkpoint_gate_archive,
2198    };
2199    #[cfg(unix)]
2200    use crate::hel_session_manager::{ManagedSessionHandle, new_command_id};
2201    use crate::hel_worker_client::RelayTransportDead;
2202    use hel::hel_archive::{
2203        BundleManifest, CanonicalTranscriptBody, CanonicalTranscriptItem, TargetManifest,
2204    };
2205    use hel::hel_checkpoint::CheckpointExportSpec;
2206    #[cfg(unix)]
2207    use hel::hel_config::{
2208        HarnessProfile, HelConfig, ProjectBundle, ProjectRepository, TargetTemplate,
2209    };
2210    use hel::hel_projection::canonical_session_from_materialized;
2211    #[cfg(unix)]
2212    use hel::hel_state::TargetLocator;
2213    use hel::hel_state::{
2214        CheckpointMetadata, HelState, ManagedSessionSnapshot, MaterializedSession, SessionState,
2215    };
2216    #[cfg(unix)]
2217    use hel::hel_targets::ProvisionStage;
2218    use hel::hel_targets::{self, CommandExecutor, CommandOutput, CommandSpec};
2219    #[cfg(unix)]
2220    use hel::hel_worker::RelayCommandOutcome;
2221    use hel::hel_worker::{RelayCommand, RelayCursor, RelayExecutionState};
2222
2223    use super::*;
2224
2225    #[test]
2226    fn startup_reconciliation_only_removes_unreferenced_controller_checkpoints() {
2227        let directory = tempfile::tempdir().unwrap();
2228        let session_id = "1123456789abcdef0123456789abcdef";
2229        let referenced_name =
2230            format!("{session_id}-7-archive-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.hel.zip");
2231        let orphan_name =
2232            format!("{session_id}-8-archive-bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb.hel.zip");
2233        let imported_name = format!("{session_id}.hel.zip");
2234        for name in [
2235            &referenced_name,
2236            &orphan_name,
2237            &imported_name,
2238            "notes.hel.zip",
2239        ] {
2240            std::fs::write(directory.path().join(name), b"test").unwrap();
2241        }
2242        let mut state = HelState::default();
2243        let mut session = checkpoint_test_session(session_id);
2244        session.checkpoint = Some(CheckpointMetadata {
2245            archive_path: directory.path().join(&referenced_name),
2246            sha256: "c".repeat(64),
2247            created_at: "2026-08-12T00:00:00Z".into(),
2248            event_frontier: 7,
2249        });
2250        state.sessions.insert(session_id.into(), session);
2251
2252        assert_eq!(
2253            reconcile_managed_checkpoint_archives_in(directory.path(), &state).unwrap(),
2254            1
2255        );
2256        assert!(directory.path().join(referenced_name).exists());
2257        assert!(!directory.path().join(orphan_name).exists());
2258        assert!(directory.path().join(imported_name).exists());
2259        assert!(directory.path().join("notes.hel.zip").exists());
2260    }
2261    #[test]
2262    fn recovery_artifact_final_verification_checks_the_archive_digest() {
2263        let directory = tempfile::tempdir().unwrap();
2264        let session_id = "1123456789abcdef0123456789abcdef";
2265        let metadata = write_checkpoint_gate_archive(directory.path(), session_id, 7);
2266        let mut artifact = CheckpointArtifact {
2267            metadata,
2268            native_session_id: "native-session".into(),
2269            event_frontier_digest: "a".repeat(64),
2270        };
2271
2272        verify_checkpoint_artifact(session_id, &artifact).unwrap();
2273        artifact.metadata.sha256 = "b".repeat(64);
2274        assert!(
2275            verify_checkpoint_artifact(session_id, &artifact)
2276                .unwrap_err()
2277                .to_string()
2278                .contains("checkpoint SHA changed")
2279        );
2280    }
2281    /// A snapshot of a session whose checkpoint barrier is open but not yet
2282    /// ready, projected exactly at `cursor`.
2283    fn checkpoint_barrier_snapshot(cursor: &RelayCursor) -> ManagedSessionSnapshot {
2284        let mut materialized = MaterializedSession::empty("session-1");
2285        materialized.applied_event_ordinal = cursor.ordinal;
2286        materialized.applied_event_digest = cursor.digest.clone();
2287        ManagedSessionSnapshot {
2288            window: hel::hel_state::ProjectionWindow::of(&materialized),
2289            materialized,
2290            latest_credential_sync_signal: None,
2291            worker_build: None,
2292            operational: hel::hel_worker::RelayOperationalState {
2293                acp_ready: None,
2294                store_id: None,
2295                idle_since_ms: None,
2296                session_id: "session-1".into(),
2297                execution: RelayExecutionState::Idle,
2298                latest_ordinal: cursor.ordinal,
2299                latest_digest: cursor.digest.clone(),
2300                acknowledged_through: cursor.ordinal,
2301                acknowledged_digest: cursor.digest.clone(),
2302                recovery_floor_ordinal: 0,
2303                recovery_floor_digest: hel::hel_worker::RELAY_EVENT_GENESIS_DIGEST.into(),
2304                native_session_id: Some("native-session".into()),
2305                agent_capabilities: None,
2306                agent_info: None,
2307                config_options: Vec::new(),
2308                modes: None,
2309                available_commands: Vec::new(),
2310                config: BTreeMap::new(),
2311                active_prompt: None,
2312                queued_prompts: Vec::new(),
2313                active_user_shells: Vec::new(),
2314                active_agent_terminals: Vec::new(),
2315                checkpoint_barrier: Some("checkpoint-1".into()),
2316                checkpoint_ready: None,
2317                last_acp_activity_at_ms: None,
2318                current_step_started_at_ms: None,
2319                foreground_tool_started_at_ms: None,
2320                harness_turn: None,
2321                last_harness_turn_started_ordinal: None,
2322                background_commands: Vec::new(),
2323            },
2324        }
2325    }
2326    #[test]
2327    fn checkpoint_barrier_is_not_reached_until_its_ready_cursor_is_projected() {
2328        let cursor = RelayCursor {
2329            ordinal: 7,
2330            digest: "a".repeat(64),
2331        };
2332        let mut snapshot = checkpoint_barrier_snapshot(&cursor);
2333
2334        assert!(!checkpoint_barrier_is_ready(&snapshot, "checkpoint-1"));
2335        snapshot.operational.checkpoint_ready = Some(cursor.clone());
2336        assert!(checkpoint_barrier_is_ready(&snapshot, "checkpoint-1"));
2337        validate_checkpoint_barrier_snapshot(&snapshot, "checkpoint-1", &cursor).unwrap();
2338    }
2339    #[test]
2340    fn checkpoint_revalidation_accepts_a_frontier_that_moved_past_the_ready_cursor() {
2341        let cursor = RelayCursor {
2342            ordinal: 7,
2343            digest: "a".repeat(64),
2344        };
2345        let mut snapshot = checkpoint_barrier_snapshot(&cursor);
2346        snapshot.operational.checkpoint_ready = Some(cursor.clone());
2347
2348        // An open ordinary barrier keeps accepting and journalling commands; it
2349        // only freezes dispatch. The archive still matches the sealed
2350        // workspace, so a frontier past the ready cursor stays valid.
2351        snapshot.operational.latest_ordinal = cursor.ordinal + 2;
2352        snapshot.operational.latest_digest = "b".repeat(64);
2353        snapshot.materialized.applied_event_ordinal = cursor.ordinal + 2;
2354        snapshot.materialized.applied_event_digest = "b".repeat(64);
2355        validate_checkpoint_barrier_snapshot(&snapshot, "checkpoint-1", &cursor).unwrap();
2356
2357        // Losing the barrier, or reaching a different cut, still invalidates it.
2358        snapshot.operational.checkpoint_ready = Some(RelayCursor {
2359            ordinal: cursor.ordinal + 1,
2360            digest: "c".repeat(64),
2361        });
2362        assert!(validate_checkpoint_barrier_snapshot(&snapshot, "checkpoint-1", &cursor).is_err());
2363        snapshot.operational.checkpoint_ready = Some(cursor.clone());
2364        snapshot.operational.checkpoint_barrier = None;
2365        assert!(validate_checkpoint_barrier_snapshot(&snapshot, "checkpoint-1", &cursor).is_err());
2366    }
2367    /// Target-side answer of a successful export.
2368    fn exported_checkpoint_json() -> Vec<u8> {
2369        serde_json::to_vec(&hel::hel_checkpoint::TargetCheckpoint {
2370            path: PathBuf::from("/var/lib/hel/workers/session/checkpoint.hel.zip"),
2371            sha256: "c".repeat(64),
2372            event_frontier: 7,
2373            event_frontier_digest: "d".repeat(64),
2374            timings: None,
2375        })
2376        .unwrap()
2377    }
2378    fn export_spec_fixture() -> CheckpointExportSpec {
2379        CheckpointExportSpec {
2380            protocol_version: CHECKPOINT_EXPORT_PROTOCOL_VERSION,
2381            session: hel::hel_archive::SessionManifest {
2382                id: LATCH_RELAY_SESSION.into(),
2383                title: "streamed spec".into(),
2384                harness_kind: hel::hel_config::HarnessKind::Codex,
2385                profile_id: "codex".into(),
2386                native_session_id: "native-session".into(),
2387                created_at: "2026-08-12T00:00:00Z".into(),
2388                checkpointed_at: "2026-08-16T00:00:00Z".into(),
2389                hel_version: "test".into(),
2390                relay_version: "test".into(),
2391                adapter_version: "acp-v1".into(),
2392            },
2393            target: TargetManifest {
2394                template_id: "podman".into(),
2395                target_kind: "local-podman".into(),
2396                details: BTreeMap::new(),
2397            },
2398            bundle: BundleManifest {
2399                id: "project".into(),
2400                primary_repository: "app".into(),
2401            },
2402            relay_root: PathBuf::from("/var/lib/hel/workers/session"),
2403            harness_home: PathBuf::from("/var/lib/hel/profiles/codex"),
2404            workspace_root: PathBuf::from("/workspace"),
2405            repositories: Vec::new(),
2406            canonical_session: canonical_session_from_materialized(&MaterializedSession::empty(
2407                LATCH_RELAY_SESSION.to_owned(),
2408            ))
2409            .unwrap(),
2410            output_path: PathBuf::from("/var/lib/hel/workers/session/checkpoint.hel.zip"),
2411        }
2412    }
2413    /// Answers the streamed export with a scripted status, and every other
2414    /// command as a success.
2415    struct ExportExecutor {
2416        streamed_status: i32,
2417        streamed_stderr: String,
2418        retry_stdin_after_failure: bool,
2419        stdin_calls: Cell<usize>,
2420        purposes: RefCell<Vec<String>>,
2421        streamed_spec: RefCell<Vec<u8>>,
2422    }
2423    impl ExportExecutor {
2424        fn new(streamed_status: i32, streamed_stderr: &str) -> Self {
2425            Self {
2426                streamed_status,
2427                streamed_stderr: streamed_stderr.to_owned(),
2428                retry_stdin_after_failure: false,
2429                stdin_calls: Cell::new(0),
2430                purposes: RefCell::new(Vec::new()),
2431                streamed_spec: RefCell::new(Vec::new()),
2432            }
2433        }
2434
2435        fn retry_stdin_after_failure(mut self) -> Self {
2436            self.retry_stdin_after_failure = true;
2437            self
2438        }
2439    }
2440    impl CommandExecutor for ExportExecutor {
2441        fn execute(&self, command: &CommandSpec) -> Result<CommandOutput> {
2442            self.purposes.borrow_mut().push(command.purpose.clone());
2443            Ok(CommandOutput {
2444                status: 0,
2445                stdout: exported_checkpoint_json(),
2446                stderr: Vec::new(),
2447            })
2448        }
2449
2450        fn execute_with_stdin(
2451            &self,
2452            command: &CommandSpec,
2453            input: &mut (dyn std::io::Read + Send),
2454        ) -> Result<CommandOutput> {
2455            self.purposes.borrow_mut().push(command.purpose.clone());
2456            let mut spec = Vec::new();
2457            input.read_to_end(&mut spec)?;
2458            *self.streamed_spec.borrow_mut() = spec;
2459            let attempt = self.stdin_calls.get();
2460            self.stdin_calls.set(attempt + 1);
2461            let failed =
2462                self.streamed_status != 0 && (attempt == 0 || !self.retry_stdin_after_failure);
2463            Ok(CommandOutput {
2464                status: if failed { self.streamed_status } else { 0 },
2465                stdout: if failed {
2466                    Vec::new()
2467                } else {
2468                    exported_checkpoint_json()
2469                },
2470                stderr: if failed {
2471                    self.streamed_stderr.clone().into_bytes()
2472                } else {
2473                    Vec::new()
2474                },
2475            })
2476        }
2477    }
2478    #[test]
2479    fn docker_checkpoint_fallback_upload_uses_docker_cp() {
2480        struct RecordingExecutor {
2481            commands: RefCell<Vec<CommandSpec>>,
2482        }
2483        impl CommandExecutor for RecordingExecutor {
2484            fn execute(&self, command: &CommandSpec) -> Result<CommandOutput> {
2485                self.commands.borrow_mut().push(command.clone());
2486                Ok(CommandOutput {
2487                    status: 0,
2488                    stdout: Vec::new(),
2489                    stderr: Vec::new(),
2490                })
2491            }
2492        }
2493
2494        let executor = RecordingExecutor {
2495            commands: RefCell::new(Vec::new()),
2496        };
2497        let locator = hel_targets::TargetLocator::LocalDocker {
2498            container_id: "hel-session-12345678".to_owned(),
2499        };
2500        upload_checkpoint_spec(
2501            &executor,
2502            &locator,
2503            LATCH_RELAY_SESSION,
2504            Path::new("checkpoint-spec.json"),
2505            "/var/lib/hel/workers/session/checkpoint-spec.json",
2506        )
2507        .unwrap();
2508
2509        let commands = executor.commands.borrow();
2510        assert_eq!(commands.len(), 1);
2511        assert_eq!(commands[0].program, "docker");
2512        assert_eq!(
2513            commands[0].args,
2514            [
2515                "cp",
2516                "checkpoint-spec.json",
2517                "hel-session-12345678:/var/lib/hel/workers/session/checkpoint-spec.json"
2518            ]
2519        );
2520        assert_eq!(commands[0].purpose, "upload checkpoint specification");
2521    }
2522    #[test]
2523    fn checkpoint_export_streams_its_spec_instead_of_uploading_it() {
2524        let locator = hel_targets::TargetLocator::LocalPodman {
2525            container_id: hel_targets::resource_name(LATCH_RELAY_SESSION).unwrap(),
2526            workspace_storage: Default::default(),
2527        };
2528        let spec = export_spec_fixture();
2529        let executor = ExportExecutor::new(0, "");
2530
2531        let output = export_target_checkpoint(
2532            &executor,
2533            &locator,
2534            LATCH_RELAY_SESSION,
2535            &spec,
2536            "/var/lib/hel/workers/session/checkpoint-spec.json",
2537        )
2538        .unwrap();
2539
2540        assert_eq!(output.stdout, exported_checkpoint_json());
2541        assert_eq!(
2542            serde_json::from_slice::<CheckpointExportSpec>(&executor.streamed_spec.borrow())
2543                .unwrap(),
2544            spec
2545        );
2546        assert_eq!(
2547            executor.purposes.into_inner(),
2548            vec!["export target checkpoint".to_owned()]
2549        );
2550    }
2551    /// A worker copied into the target before `--spec -` existed reads the dash
2552    /// as a file name. The checkpoint has to keep working on it.
2553    #[test]
2554    fn an_export_that_cannot_read_stdin_falls_back_to_uploading_the_spec() {
2555        let locator = hel_targets::TargetLocator::LocalPodman {
2556            container_id: hel_targets::resource_name(LATCH_RELAY_SESSION).unwrap(),
2557            workspace_storage: Default::default(),
2558        };
2559        let executor = ExportExecutor::new(
2560            1,
2561            "Error: read checkpoint export spec -\n\nCaused by:\n    \
2562                 No such file or directory (os error 2)\n",
2563        );
2564
2565        let output = export_target_checkpoint(
2566            &executor,
2567            &locator,
2568            LATCH_RELAY_SESSION,
2569            &export_spec_fixture(),
2570            "/var/lib/hel/workers/session/checkpoint-spec.json",
2571        )
2572        .unwrap();
2573
2574        assert_eq!(output.stdout, exported_checkpoint_json());
2575        assert_eq!(
2576            executor.purposes.into_inner(),
2577            vec![
2578                "export target checkpoint".to_owned(),
2579                "upload checkpoint specification".to_owned(),
2580                "export target checkpoint".to_owned(),
2581            ]
2582        );
2583    }
2584    #[test]
2585    fn a_failing_export_is_not_retried_as_an_old_worker() {
2586        let locator = hel_targets::TargetLocator::LocalPodman {
2587            container_id: hel_targets::resource_name(LATCH_RELAY_SESSION).unwrap(),
2588            workspace_storage: Default::default(),
2589        };
2590        let executor = ExportExecutor::new(1, "Error: repository 'app' is missing\n");
2591
2592        let error = export_target_checkpoint(
2593            &executor,
2594            &locator,
2595            LATCH_RELAY_SESSION,
2596            &export_spec_fixture(),
2597            "/var/lib/hel/workers/session/checkpoint-spec.json",
2598        )
2599        .unwrap_err();
2600
2601        assert!(
2602            format!("{error:#}").contains("repository 'app' is missing"),
2603            "{error:#}"
2604        );
2605        assert_eq!(
2606            executor.purposes.into_inner(),
2607            vec!["export target checkpoint".to_owned()]
2608        );
2609    }
2610    /// The explicit export protocol field makes every older worker reject the
2611    /// current spec before it can apply obsolete path or collection behavior.
2612    #[test]
2613    fn a_legacy_export_worker_is_replaced_before_it_runs_obsolete_behavior() {
2614        let locator = hel_targets::TargetLocator::LocalPodman {
2615            container_id: hel_targets::resource_name(LATCH_RELAY_SESSION).unwrap(),
2616            workspace_storage: Default::default(),
2617        };
2618        let spec = export_spec_fixture();
2619        let executor = ExportExecutor::new(
2620            1,
2621            "Error: parse checkpoint export spec from standard input\n\nCaused by:\n    \
2622                 unknown field `protocol_version`, expected `session` at line 1 column 20\n",
2623        )
2624        .retry_stdin_after_failure();
2625        let worker_binary = Path::new("/hel-test-worker");
2626
2627        let output = export_target_checkpoint_with_worker(
2628            &executor,
2629            &locator,
2630            LATCH_RELAY_SESSION,
2631            &spec,
2632            "/var/lib/hel/workers/session/checkpoint-spec.json",
2633            Some(worker_binary),
2634        )
2635        .unwrap();
2636
2637        assert_eq!(output.stdout, exported_checkpoint_json());
2638        assert_eq!(
2639            serde_json::from_slice::<CheckpointExportSpec>(&executor.streamed_spec.borrow())
2640                .unwrap(),
2641            spec
2642        );
2643        assert_eq!(
2644            executor.purposes.into_inner(),
2645            vec![
2646                "export target checkpoint".to_owned(),
2647                "stage replacement Mjolnir worker".to_owned(),
2648                "replace installed Mjolnir worker".to_owned(),
2649                "make replaced Mjolnir worker executable".to_owned(),
2650                "export target checkpoint".to_owned(),
2651            ]
2652        );
2653    }
2654    #[test]
2655    fn a_schema_mismatch_after_uploading_the_spec_still_replaces_the_worker_binary() {
2656        let locator = hel_targets::TargetLocator::LocalPodman {
2657            container_id: hel_targets::resource_name(LATCH_RELAY_SESSION).unwrap(),
2658            workspace_storage: Default::default(),
2659        };
2660        struct FileThenRefreshExecutor {
2661            purposes: RefCell<Vec<String>>,
2662            file_export_calls: Cell<usize>,
2663        }
2664        impl CommandExecutor for FileThenRefreshExecutor {
2665            fn execute(&self, command: &CommandSpec) -> Result<CommandOutput> {
2666                self.purposes.borrow_mut().push(command.purpose.clone());
2667                if command.purpose == "export target checkpoint" {
2668                    let attempt = self.file_export_calls.get();
2669                    self.file_export_calls.set(attempt + 1);
2670                    if attempt == 0 {
2671                        return Ok(CommandOutput {
2672                            status: 1,
2673                            stdout: Vec::new(),
2674                            stderr: b"Error: parse checkpoint export spec /spec.json\n\nCaused by:\n    unknown variant `terminal_output`, expected one of `user`, `agent`, `thought`, `tool`, `plan`, `system`\n".to_vec(),
2675                        });
2676                    }
2677                }
2678                Ok(CommandOutput {
2679                    status: 0,
2680                    stdout: exported_checkpoint_json(),
2681                    stderr: Vec::new(),
2682                })
2683            }
2684
2685            fn execute_with_stdin(
2686                &self,
2687                command: &CommandSpec,
2688                input: &mut (dyn std::io::Read + Send),
2689            ) -> Result<CommandOutput> {
2690                self.purposes.borrow_mut().push(command.purpose.clone());
2691                let mut discarded = Vec::new();
2692                input.read_to_end(&mut discarded)?;
2693                let stdin_calls = self
2694                    .purposes
2695                    .borrow()
2696                    .iter()
2697                    .filter(|purpose| *purpose == "export target checkpoint")
2698                    .count();
2699                if stdin_calls == 1 {
2700                    return Ok(CommandOutput {
2701                        status: 1,
2702                        stdout: Vec::new(),
2703                        stderr: b"Error: read checkpoint export spec -\n\nCaused by:\n    No such file or directory (os error 2)\n".to_vec(),
2704                    });
2705                }
2706                Ok(CommandOutput {
2707                    status: 0,
2708                    stdout: exported_checkpoint_json(),
2709                    stderr: Vec::new(),
2710                })
2711            }
2712        }
2713
2714        let executor = FileThenRefreshExecutor {
2715            purposes: RefCell::new(Vec::new()),
2716            file_export_calls: Cell::new(0),
2717        };
2718        let output = export_target_checkpoint_with_worker(
2719            &executor,
2720            &locator,
2721            LATCH_RELAY_SESSION,
2722            &export_spec_fixture(),
2723            "/var/lib/hel/workers/session/checkpoint-spec.json",
2724            Some(Path::new("/hel-test-worker")),
2725        )
2726        .unwrap();
2727
2728        assert_eq!(output.stdout, exported_checkpoint_json());
2729        assert_eq!(
2730            executor.purposes.into_inner(),
2731            vec![
2732                "export target checkpoint".to_owned(),
2733                "upload checkpoint specification".to_owned(),
2734                "export target checkpoint".to_owned(),
2735                "stage replacement Mjolnir worker".to_owned(),
2736                "replace installed Mjolnir worker".to_owned(),
2737                "make replaced Mjolnir worker executable".to_owned(),
2738                "export target checkpoint".to_owned(),
2739            ]
2740        );
2741    }
2742    /// A session that is working is busy, not wedged. A copy that can run
2743    /// again later leaves at once instead of waiting out the deadline, which
2744    /// would restart the worker and kill the turn in flight.
2745    #[test]
2746    fn a_working_session_defers_but_close_waits_for_cancellation_before_recovery() {
2747        let cursor = RelayCursor {
2748            ordinal: 7,
2749            digest: "a".repeat(64),
2750        };
2751        let mut snapshot = checkpoint_barrier_snapshot(&cursor);
2752        snapshot.operational.execution = RelayExecutionState::Running;
2753
2754        let deferred = checkpoint_barrier_wait_ended(
2755            &snapshot,
2756            "checkpoint-1",
2757            BarrierBusyPolicy::DeferWhileRunning,
2758            false,
2759            false,
2760        )
2761        .expect("a working session ends the wait at once");
2762        assert!(checkpoint_was_deferred(&deferred), "{deferred:#}");
2763        assert!(
2764            !checkpoint_barrier_needs_worker_restart(&deferred),
2765            "a deferred copy must never restart the worker: {deferred:#}"
2766        );
2767        assert_eq!(
2768            BarrierBusyPolicy::of(LatchExclusivity::HoldThroughClose),
2769            BarrierBusyPolicy::InterruptWhileRunning
2770        );
2771
2772        // Stop requests a non-steering cancellation and waits for the real
2773        // turn boundary instead of selecting restart recovery immediately.
2774        assert!(
2775            checkpoint_barrier_wait_ended(
2776                &snapshot,
2777                "checkpoint-1",
2778                BarrierBusyPolicy::InterruptWhileRunning,
2779                false,
2780                false,
2781            )
2782            .is_none()
2783        );
2784        let interrupted = checkpoint_barrier_wait_ended(
2785            &snapshot,
2786            "checkpoint-1",
2787            BarrierBusyPolicy::InterruptWhileRunning,
2788            true,
2789            true,
2790        )
2791        .expect("an unresponsive cancellation ends the wait at the deadline");
2792        assert!(
2793            checkpoint_barrier_needs_worker_restart(&interrupted),
2794            "{interrupted:#}"
2795        );
2796        assert!(!checkpoint_was_deferred(&interrupted), "{interrupted:#}");
2797
2798        // An idle session that never admits the barrier is the real wedge,
2799        // whatever the policy.
2800        snapshot.operational.execution = RelayExecutionState::Idle;
2801        let wedged = checkpoint_barrier_wait_ended(
2802            &snapshot,
2803            "checkpoint-1",
2804            BarrierBusyPolicy::DeferWhileRunning,
2805            true,
2806            false,
2807        )
2808        .expect("the deadline ends the wait");
2809        assert!(
2810            checkpoint_barrier_needs_worker_restart(&wedged),
2811            "{wedged:#}"
2812        );
2813        assert!(!checkpoint_was_deferred(&wedged), "{wedged:#}");
2814    }
2815
2816    /// The relay moved on before the controller latched, so the archive would
2817    /// not be an exact cut. That is a deferral, not a failed checkpoint.
2818    #[test]
2819    fn a_frontier_that_moved_before_the_latch_defers_the_checkpoint() {
2820        let cursor = RelayCursor {
2821            ordinal: 220,
2822            digest: "a".repeat(64),
2823        };
2824        ensure_exact_checkpoint_cut(&cursor, cursor.ordinal, &cursor.digest)
2825            .expect("a projection latched at the ready cursor is an exact cut");
2826
2827        for (ordinal, digest) in [(223, "a".repeat(64)), (220, "b".repeat(64))] {
2828            let error = ensure_exact_checkpoint_cut(&cursor, ordinal, &digest)
2829                .expect_err("a projection past the ready cursor is not an exact cut");
2830            assert!(checkpoint_was_deferred(&error), "{error:#}");
2831            assert!(
2832                !checkpoint_barrier_needs_worker_restart(&error),
2833                "{error:#}"
2834            );
2835        }
2836    }
2837
2838    /// The barrier freezes Mjolnir's dispatch, not the harness. A turn the harness
2839    /// started on its own after the cursor was captured may have written to
2840    /// the workspace while it was staged, so that archive is abandoned.
2841    #[test]
2842    fn a_harness_turn_started_during_capture_abandons_the_archive() {
2843        let cursor = RelayCursor {
2844            ordinal: 220,
2845            digest: "a".repeat(64),
2846        };
2847        let mut snapshot = checkpoint_barrier_snapshot(&cursor);
2848        snapshot.operational.checkpoint_ready = Some(cursor.clone());
2849
2850        snapshot.operational.last_harness_turn_started_ordinal = Some(cursor.ordinal);
2851        validate_checkpoint_barrier_snapshot(&snapshot, "checkpoint-1", &cursor)
2852            .expect("a turn that started at or before the cursor is covered by the archive");
2853
2854        snapshot.operational.last_harness_turn_started_ordinal = Some(cursor.ordinal + 1);
2855        let error = validate_checkpoint_barrier_snapshot(&snapshot, "checkpoint-1", &cursor)
2856            .expect_err("a turn that started after the cursor invalidates the capture");
2857        assert!(checkpoint_was_deferred(&error), "{error:#}");
2858    }
2859
2860    #[test]
2861    fn a_stuck_checkpoint_barrier_is_retried_by_restarting_the_worker() {
2862        // Both ways the wait can end without a barrier, each wrapped the way
2863        // the checkpoint path wraps them, and each still asking for the retry.
2864        for failure in [
2865            CheckpointBarrierUnreachable::not_admitted(
2866                "checkpoint-976f6746887c5ccd93b9d8bbe120ef06",
2867            ),
2868            CheckpointBarrierUnreachable::runtime_stopped(),
2869        ] {
2870            let error = anyhow::Error::new(failure).context("latch a session checkpoint");
2871            assert!(checkpoint_barrier_needs_worker_restart(&error), "{error:#}");
2872        }
2873        assert!(!checkpoint_barrier_needs_worker_restart(&anyhow::anyhow!(
2874            "export target checkpoint failed with status 1"
2875        )));
2876        // The decision reads the type, not the text, so the old wording alone
2877        // no longer restarts a worker and rewording one cannot stop it either.
2878        assert!(!checkpoint_barrier_needs_worker_restart(&anyhow::anyhow!(
2879            "ACP relay did not reach checkpoint barrier checkpoint-1"
2880        )));
2881    }
2882
2883    #[test]
2884    fn an_incompatible_cancel_turn_requests_worker_recovery() {
2885        let error = anyhow::Error::new(RelayRejected(hel::hel_worker::RelayProtocolError {
2886            code: hel::hel_worker::RelayErrorCode::IncompatibleProtocol,
2887            message: "request uses protocol 6".into(),
2888            retryable: false,
2889            detail: None,
2890        }))
2891        .context("cancel active ACP turn before checkpoint barrier");
2892        assert!(
2893            checkpoint_cancel_turn_needs_worker_restart(&error),
2894            "{error:#}"
2895        );
2896        assert!(checkpoint_barrier_needs_worker_restart(&error.context(
2897            CheckpointBarrierUnreachable::cancel_turn_unavailable("checkpoint-1", 6,)
2898        )));
2899    }
2900    #[test]
2901    fn a_dead_worker_hello_failure_is_retried_by_restarting_the_worker() {
2902        let dead = anyhow::Error::new(RelayTransportDead::new("the proxy is gone"))
2903            .context("connect to the session worker for checkpoint");
2904        assert!(worker_connect_needs_restart(&dead), "{dead:#}");
2905        assert!(!worker_connect_needs_restart(&anyhow::anyhow!(
2906            "unknown session"
2907        )));
2908    }
2909    #[cfg(unix)]
2910    #[tokio::test]
2911    async fn checkpoint_restart_stop_failure_names_mjolnir() {
2912        struct FailingStop;
2913
2914        impl CommandExecutor for FailingStop {
2915            fn execute(&self, _command: &CommandSpec) -> Result<CommandOutput> {
2916                Ok(CommandOutput {
2917                    status: 1,
2918                    stdout: Vec::new(),
2919                    stderr: b"permission denied".to_vec(),
2920                })
2921            }
2922        }
2923
2924        let session_id = "0123456789abcdef0123456789abcdef";
2925        let worker_root = format!("/tmp/mjolnir-checkpoint-test/{session_id}");
2926        let backend = hel_targets::TargetLocator::LocalBare {
2927            worker_root: worker_root.clone(),
2928        };
2929        let controller = Controller {
2930            config: HelConfig::default(),
2931            state: HelState::default(),
2932        };
2933        let reconnect = CommandSpec::new("unused", std::iter::empty::<&str>());
2934
2935        let result = controller
2936            .restart_worker_for_checkpoint(
2937                session_id,
2938                &FailingStop,
2939                &backend,
2940                &worker_root,
2941                &reconnect,
2942            )
2943            .await;
2944        let error = match result {
2945            Ok(_) => panic!("a failed worker stop unexpectedly restarted the checkpoint worker"),
2946            Err(error) => error,
2947        };
2948        let detail = format!("{error:#}");
2949        assert!(
2950            detail.starts_with("stop wedged Mjolnir worker before retrying checkpoint"),
2951            "{detail}"
2952        );
2953        assert!(detail.contains("permission denied"), "{detail}");
2954    }
2955    #[test]
2956    fn export_spec_schema_mismatch_is_detected_from_the_parse_error() {
2957        assert!(export_spec_schema_unsupported(
2958            "Error: parse checkpoint export spec from standard input\n\nCaused by:\n    \
2959                 unknown field `terminal_refs`, expected `call` at line 1 column 7276552\n"
2960        ));
2961        assert!(export_spec_schema_unsupported(
2962            "Error: parse checkpoint export spec /spec.json\n\nCaused by:\n    \
2963                 unknown variant `terminal_output`, expected one of `user`, `agent`\n"
2964        ));
2965        assert!(!export_spec_schema_unsupported(
2966            "Error: repository 'app' is missing\n"
2967        ));
2968        assert!(!export_spec_schema_unsupported(
2969            "Error: parse checkpoint export spec from standard input\n\nCaused by:\n    \
2970                 missing field `relay_root`\n"
2971        ));
2972        assert!(export_protocol_unsupported(
2973            "Error: unsupported checkpoint export protocol version 2; worker supports 1\n"
2974        ));
2975    }
2976    const LATCH_RELAY_ROOT: &str = "MJ_TEST_LATCH_RELAY_ROOT";
2977    const LATCH_RELAY_STARTS: &str = "MJ_TEST_LATCH_RELAY_STARTS";
2978    const LATCH_RELAY_REJECT_RELEASE: &str = "MJ_TEST_LATCH_REJECT_RELEASE";
2979    #[cfg(unix)]
2980    const LATCH_RELAY_RUNNING: &str = "MJ_TEST_LATCH_RELAY_RUNNING";
2981    #[cfg(unix)]
2982    const LATCH_TEST_CHILD: &str = "MJ_TEST_LATCH_CHILD";
2983    #[cfg(unix)]
2984    const ABANDON_TEST_CHILD: &str = "MJ_TEST_ABANDON_LATCH_CHILD";
2985    #[cfg(unix)]
2986    const RELEASE_TEST_CHILD: &str = "MJ_TEST_RELEASE_LATCH_CHILD";
2987    #[cfg(unix)]
2988    const LEGACY_RELEASE_TEST_CHILD: &str = "MJ_TEST_LEGACY_RELEASE_LATCH_CHILD";
2989    #[cfg(unix)]
2990    const REUSE_TEST_CHILD: &str = "MJ_TEST_REUSE_LATCH_CHILD";
2991    const LATCH_RELAY_STARTUP_DELAY_MS: &str = "MJ_TEST_LATCH_STARTUP_DELAY_MS";
2992    const LATCH_RELAY_SESSION: &str = "018f9dd2-a3b4-7c8d-9000-0123456789ab";
2993    /// Whether the scripted relay understands the early checkpoint release.
2994    #[cfg(unix)]
2995    #[derive(Clone, Copy, PartialEq, Eq)]
2996    enum ReleaseSupport {
2997        Supported,
2998        /// Answer a release exactly as a worker that predates the command does:
2999        /// its `RelayCommand` cannot deserialize the variant at all.
3000        Rejected,
3001    }
3002    /// Relay server half of the checkpoint latch test.
3003    ///
3004    /// A durable relay only reports a checkpoint barrier ready once a dispatch
3005    /// driver claims it, so this also runs the one step the worker runtime
3006    /// performs for a barrier. It does nothing unless a parent test points it
3007    /// at a relay journal root.
3008    #[test]
3009    fn latch_relay_child_serves_stdio() {
3010        let Some(root) = std::env::var_os(LATCH_RELAY_ROOT) else {
3011            return;
3012        };
3013        // With `--nocapture` libtest writes `test <name> ... ` without a
3014        // trailing newline before the body runs. End that line first so it
3015        // cannot glue itself onto the first protocol frame.
3016        println!();
3017        // Record this start so a parent test can tell a reconnect from a reused
3018        // connection.
3019        if let Some(starts) = std::env::var_os(LATCH_RELAY_STARTS) {
3020            use std::io::Write;
3021            let mut log = OpenOptions::new()
3022                .create(true)
3023                .append(true)
3024                .open(starts)
3025                .expect("open the relay start log");
3026            writeln!(log, "{}", std::process::id()).expect("record this relay start");
3027        }
3028        let mut relay =
3029            hel::hel_worker::DurableRelay::open(Path::new(&root), LATCH_RELAY_SESSION, "1.0.0")
3030                .expect("open the test relay journal");
3031        if relay.operational_state().native_session_id.is_none() {
3032            relay
3033                .record_observation(hel::hel_worker::RelayObservation::SessionOpened {
3034                    native_session_id: "native-session".into(),
3035                    resumed: true,
3036                })
3037                .unwrap();
3038        }
3039        let ready_at = Instant::now()
3040            + Duration::from_millis(
3041                std::env::var(LATCH_RELAY_STARTUP_DELAY_MS)
3042                    .ok()
3043                    .map(|value| value.parse::<u64>().unwrap())
3044                    .unwrap_or(0),
3045            );
3046        let reject_release = std::env::var_os(LATCH_RELAY_REJECT_RELEASE).is_some();
3047        #[cfg(unix)]
3048        let running = std::env::var_os(LATCH_RELAY_RUNNING).is_some();
3049        #[cfg(unix)]
3050        if running && relay.operational_state().active_prompt.is_none() {
3051            let response = relay.handle(hel::hel_worker::RelayRequestEnvelope {
3052                request_id: "seed-running-request".into(),
3053                protocol_version: hel::hel_worker::RELAY_PROTOCOL_VERSION,
3054                request: hel::hel_worker::RelayRequest::Submit {
3055                    command_id: "seed-running-prompt".into(),
3056                    command: RelayCommand::Prompt {
3057                        prompt: vec![ContentBlock::Text(TextContent::new("running"))],
3058                    },
3059                },
3060            });
3061            assert!(matches!(
3062                response.body,
3063                hel::hel_worker::RelayResponseBody::Ok {
3064                    payload: hel::hel_worker::RelayResponsePayload::Accepted { .. }
3065                }
3066            ));
3067            let claimed = relay
3068                .claim_pending_commands(true)
3069                .expect("seed the running prompt");
3070            assert_eq!(claimed.len(), 1);
3071            assert_eq!(claimed[0].command_id, "seed-running-prompt");
3072        }
3073        let mut reader = std::io::stdin().lock();
3074        let mut writer = std::io::stdout().lock();
3075        let mut configured = false;
3076        while let Some(request) =
3077            hel::hel_worker::read_relay_frame(&mut reader).expect("read a relay request")
3078        {
3079            if !configured && Instant::now() >= ready_at {
3080                relay
3081                    .record_observation(hel::hel_worker::RelayObservation::SessionConfigured {
3082                        config_options: Vec::new(),
3083                    })
3084                    .unwrap();
3085                configured = true;
3086            }
3087            if matches!(
3088                &request.request,
3089                hel::hel_worker::RelayRequest::Submit {
3090                    command: RelayCommand::BeginCheckpoint { .. },
3091                    ..
3092                }
3093            ) {
3094                assert!(
3095                    relay.operational_state().native_session_is_ready(),
3096                    "checkpoint submitted before current ACP startup finished"
3097                );
3098            }
3099            let response = if reject_release && requests_checkpoint_release(&request) {
3100                unparseable_request_response(&request)
3101            } else {
3102                relay.handle(request)
3103            };
3104            hel::hel_worker::write_relay_frame(&mut writer, &response)
3105                .expect("answer a relay request");
3106            for claimed in relay
3107                .claim_pending_commands(true)
3108                .expect("claim relay commands")
3109            {
3110                match claimed.command {
3111                    RelayCommand::BeginCheckpoint { .. } => {
3112                        relay
3113                            .record_checkpoint_ready(&claimed.command_id)
3114                            .expect("report the checkpoint barrier ready");
3115                    }
3116                    #[cfg(unix)]
3117                    RelayCommand::CancelTurn => {
3118                        let prompt_id = relay
3119                            .operational_state()
3120                            .active_prompt
3121                            .as_ref()
3122                            .map(|prompt| prompt.command_id.clone())
3123                            .expect("a prompt to cancel");
3124                        relay
3125                            .record_command_completed(
3126                                &claimed.command_id,
3127                                RelayCommandOutcome::Cancelled,
3128                            )
3129                            .expect("complete the cancellation");
3130                        relay
3131                            .record_command_completed(
3132                                &prompt_id,
3133                                RelayCommandOutcome::Prompt {
3134                                    stop_reason: "cancelled".into(),
3135                                },
3136                            )
3137                            .expect("complete the cancelled prompt");
3138                    }
3139                    _ => {}
3140                }
3141            }
3142        }
3143    }
3144    fn requests_checkpoint_release(request: &hel::hel_worker::RelayRequestEnvelope) -> bool {
3145        matches!(
3146            &request.request,
3147            hel::hel_worker::RelayRequest::Submit {
3148                command: RelayCommand::ReleaseCheckpoint { .. },
3149                ..
3150            }
3151        )
3152    }
3153    /// The answer a worker gives for a frame its own protocol cannot decode.
3154    /// An older `RelayCommand` has no `release_checkpoint` variant, and the
3155    /// enum denies unknown ones, so the request never reaches its relay.
3156    fn unparseable_request_response(
3157        request: &hel::hel_worker::RelayRequestEnvelope,
3158    ) -> hel::hel_worker::RelayResponseEnvelope {
3159        hel::hel_worker::RelayResponseEnvelope {
3160            request_id: request.request_id.clone(),
3161            protocol_version: request.protocol_version,
3162            body: hel::hel_worker::RelayResponseBody::Error {
3163                error: hel::hel_worker::RelayProtocolError {
3164                    code: hel::hel_worker::RelayErrorCode::InvalidRequest,
3165                    message: "unknown variant `release_checkpoint`".into(),
3166                    retryable: false,
3167                    detail: None,
3168                },
3169            },
3170        }
3171    }
3172    /// A relay target served by this test binary over stdio. Each start of the
3173    /// server appends to `starts`, if given.
3174    #[cfg(unix)]
3175    fn latch_relay_target(
3176        relay_root: &Path,
3177        starts: Option<&Path>,
3178        release: ReleaseSupport,
3179        running: bool,
3180    ) -> crate::hel_session_manager::RelaySessionTarget {
3181        // `RelayClient` parses every stdout line as JSON, so libtest's own
3182        // progress lines are dropped before they reach the protocol reader.
3183        let script = format!(
3184            "\"$0\" --exact {}::latch_relay_child_serves_stdio --nocapture | \
3185                 grep --line-buffered '^{{'",
3186            module_path!()
3187                .strip_prefix("mj_controller::")
3188                .unwrap_or(module_path!())
3189        );
3190        let mut spec = CommandSpec::new(
3191            "sh",
3192            [
3193                "-c".to_owned(),
3194                script,
3195                std::env::current_exe()
3196                    .unwrap()
3197                    .to_string_lossy()
3198                    .into_owned(),
3199            ],
3200        )
3201        .purpose("test latch relay");
3202        spec.env.insert(
3203            LATCH_RELAY_ROOT.to_owned(),
3204            relay_root.to_string_lossy().into_owned(),
3205        );
3206        if let Some(starts) = starts {
3207            spec.env.insert(
3208                LATCH_RELAY_STARTS.to_owned(),
3209                starts.to_string_lossy().into_owned(),
3210            );
3211        }
3212        if release == ReleaseSupport::Rejected {
3213            spec.env
3214                .insert(LATCH_RELAY_REJECT_RELEASE.to_owned(), "1".to_owned());
3215        }
3216        if running {
3217            spec.env
3218                .insert(LATCH_RELAY_RUNNING.to_owned(), "1".to_owned());
3219        }
3220        crate::hel_session_manager::RelaySessionTarget {
3221            session_id: LATCH_RELAY_SESSION.to_owned(),
3222            spec,
3223            worker_recovery: None,
3224            project_memory: None,
3225        }
3226    }
3227    /// Start a session manager against a live relay and latch a checkpoint on
3228    /// it, exactly as [`Controller::checkpoint_session_latched`] does.
3229    #[cfg(unix)]
3230    async fn latch_a_live_checkpoint(
3231        relay_root: &Path,
3232        starts: Option<&Path>,
3233        release: ReleaseSupport,
3234        running: bool,
3235    ) -> (
3236        crate::hel_session_manager::SessionManagerChannels,
3237        ManagedSessionHandle,
3238        ControllerRelayLease,
3239        String,
3240        RelayCursor,
3241    ) {
3242        // The projection refuses events for sessions the controller does not
3243        // know, so register the one the relay journals for.
3244        hel::hel_database::save_session(&checkpoint_test_session(LATCH_RELAY_SESSION)).unwrap();
3245        let channels = crate::hel_session_manager::spawn_session_manager().unwrap();
3246        channels
3247            .targets
3248            .send(vec![latch_relay_target(
3249                relay_root, starts, release, running,
3250            )])
3251            .unwrap();
3252        let handle = channels
3253            .control
3254            .wait_for_session(LATCH_RELAY_SESSION, Duration::from_secs(10))
3255            .await
3256            .unwrap();
3257
3258        let lease = handle.lease_connection().await.unwrap();
3259        let mut relay = ControllerRelayLease::Managed {
3260            handle: handle.clone(),
3261            lease: Some(lease),
3262        };
3263        let barrier_command_id = new_command_id("checkpoint").unwrap();
3264        let connection = relay.connection_mut();
3265        connection
3266            .submit(
3267                barrier_command_id.clone(),
3268                RelayCommand::BeginCheckpoint { reason: None },
3269            )
3270            .await
3271            .unwrap();
3272        let barrier = wait_for_checkpoint_barrier(
3273            connection,
3274            LATCH_RELAY_SESSION,
3275            &barrier_command_id,
3276            CHECKPOINT_BARRIER_TIMEOUT,
3277            BarrierBusyPolicy::InterruptWhileRunning,
3278        )
3279        .await
3280        .unwrap();
3281        assert_eq!(
3282            barrier.materialized.applied_event_ordinal,
3283            barrier.operational.latest_ordinal
3284        );
3285        let cursor = barrier.operational.checkpoint_ready.clone().unwrap();
3286        (channels, handle, relay, barrier_command_id, cursor)
3287    }
3288
3289    /// A close checkpoint cancels an active prompt and waits for the prompt's
3290    /// terminal event before admitting its barrier. The scripted worker clears
3291    /// the prompt only when it receives `CancelTurn`, so a single-start log
3292    /// proves that a responsive cancellation did not take restart recovery.
3293    #[cfg(unix)]
3294    #[tokio::test]
3295    async fn a_close_checkpoint_cancels_a_running_turn_without_restarting_the_worker() {
3296        // MJ_DATA_DIR is process-global, so keep the database-backed relay
3297        // actor isolated from unrelated tests.
3298        if std::env::var_os(LATCH_TEST_CHILD).is_none() {
3299            let directory = tempfile::tempdir().unwrap();
3300            let test_name = format!(
3301                "{}::a_close_checkpoint_cancels_a_running_turn_without_restarting_the_worker",
3302                module_path!()
3303                    .strip_prefix("mj_controller::")
3304                    .unwrap_or(module_path!())
3305            );
3306            let output = Command::new(std::env::current_exe().unwrap())
3307                .args(["--exact", &test_name, "--nocapture"])
3308                .env(LATCH_TEST_CHILD, "1")
3309                .env("MJ_DATA_DIR", directory.path())
3310                .output()
3311                .unwrap();
3312            assert!(
3313                output.status.success(),
3314                "isolated cancellation checkpoint test failed\nstdout:\n{}\nstderr:\n{}",
3315                String::from_utf8_lossy(&output.stdout),
3316                String::from_utf8_lossy(&output.stderr)
3317            );
3318            return;
3319        }
3320        let _writer = hel::hel_database::install_isolated_test_writer();
3321        let relay_root = tempfile::tempdir().unwrap();
3322        let start_log_directory = tempfile::tempdir().unwrap();
3323        let start_log = start_log_directory.path().join("relay-starts");
3324        let (_channels, _handle, mut relay, _barrier_command_id, _cursor) =
3325            latch_a_live_checkpoint(
3326                relay_root.path(),
3327                Some(&start_log),
3328                ReleaseSupport::Supported,
3329                true,
3330            )
3331            .await;
3332        let snapshot = relay.sync_snapshot().await.unwrap();
3333        assert_eq!(
3334            snapshot.operational.execution,
3335            RelayExecutionState::Idle,
3336            "the close wait returned before the cancelled turn became idle"
3337        );
3338        assert!(
3339            snapshot.operational.active_prompt.is_none(),
3340            "the close wait returned before the cancelled prompt settled"
3341        );
3342        assert_eq!(
3343            relay_starts(&start_log),
3344            1,
3345            "responsive cancellation restarted worker"
3346        );
3347    }
3348    /// The session actor absorbs a returned connection on its own task, so the
3349    /// first command after a latch ends may still be refused.
3350    #[cfg(unix)]
3351    async fn wait_until_the_actor_serves_again(handle: &ManagedSessionHandle) {
3352        for attempt in 0.. {
3353            if handle.sync_now().await.is_ok() {
3354                return;
3355            }
3356            assert!(attempt < 200, "the actor never took its connection back");
3357            tokio::time::sleep(std::time::Duration::from_millis(10)).await;
3358        }
3359    }
3360    /// Ending the latch is the whole point of the split checkpoint: the actor
3361    /// serves the dashboard again while the archive is still being exported,
3362    /// and the events it accepts do not invalidate the latched archive.
3363    #[cfg(unix)]
3364    #[tokio::test]
3365    async fn ending_the_checkpoint_latch_returns_the_connection_to_its_actor() {
3366        // MJ_DATA_DIR is process-global, so run the database-backed half in an
3367        // exact child test instead of racing unrelated tests in this process.
3368        if std::env::var_os(LATCH_TEST_CHILD).is_none() {
3369            let directory = tempfile::tempdir().unwrap();
3370            let test_name = format!(
3371                "{}::ending_the_checkpoint_latch_returns_the_connection_to_its_actor",
3372                module_path!()
3373                    .strip_prefix("mj_controller::")
3374                    .unwrap_or(module_path!())
3375            );
3376            let output = Command::new(std::env::current_exe().unwrap())
3377                .args(["--exact", &test_name, "--nocapture"])
3378                .env(LATCH_TEST_CHILD, "1")
3379                .env("MJ_DATA_DIR", directory.path())
3380                .output()
3381                .unwrap();
3382            assert!(
3383                output.status.success(),
3384                "isolated checkpoint latch test failed\nstdout:\n{}\nstderr:\n{}",
3385                String::from_utf8_lossy(&output.stdout),
3386                String::from_utf8_lossy(&output.stderr)
3387            );
3388            return;
3389        }
3390        // Alone in this child process, so it installs the one writer.
3391        let _writer = hel::hel_database::install_isolated_test_writer();
3392
3393        // A connection that never comes back would hang the suite instead of
3394        // failing it, so turn a stall into a hard error.
3395        std::thread::spawn(|| {
3396            std::thread::sleep(std::time::Duration::from_secs(120));
3397            eprintln!("the checkpoint latch never returned its connection");
3398            std::process::exit(101);
3399        });
3400
3401        let relay_root = tempfile::tempdir().unwrap();
3402        let (_channels, handle, mut relay, barrier_command_id, cursor) =
3403            latch_a_live_checkpoint(relay_root.path(), None, ReleaseSupport::Supported, false)
3404                .await;
3405
3406        // Latch phase: the projection must be read at the exact ready cursor,
3407        // so the actor cannot reach the relay at all.
3408        assert!(
3409            handle.sync_now().await.is_err(),
3410            "a latched projection must not be advanced by its own actor"
3411        );
3412
3413        relay.end_latch();
3414        wait_until_the_actor_serves_again(&handle).await;
3415
3416        // Slow phase, before anything else reaches the relay: the controller
3417        // reads its barrier back through the actor, which must report what the
3418        // latch already applied.
3419        let latched = relay.sync_snapshot().await.unwrap();
3420        validate_checkpoint_barrier_snapshot(&latched, &barrier_command_id, &cursor).unwrap();
3421
3422        // A prompt accepted while the archive transfers moves the frontier past
3423        // the ready cursor. The barrier still seals the same workspace.
3424        let prompt_ordinal = relay
3425            .submit(
3426                new_command_id("prompt").unwrap(),
3427                RelayCommand::Prompt {
3428                    prompt: vec![ContentBlock::Text(TextContent::new("hello"))],
3429                },
3430            )
3431            .await
3432            .unwrap();
3433        assert!(prompt_ordinal > cursor.ordinal);
3434        let snapshot = relay.sync_snapshot().await.unwrap();
3435        assert!(snapshot.operational.latest_ordinal > cursor.ordinal);
3436        validate_checkpoint_barrier_snapshot(&snapshot, &barrier_command_id, &cursor).unwrap();
3437
3438        latched_checkpoint(
3439            relay,
3440            barrier_command_id,
3441            cursor,
3442            CheckpointCompletion::HeldBarrier,
3443        )
3444        .complete()
3445        .await
3446        .unwrap();
3447        handle.sync_now().await.unwrap();
3448        assert_eq!(
3449            handle
3450                .view()
3451                .snapshot
3452                .expect("the actor published the completed barrier")
3453                .operational
3454                .checkpoint_barrier,
3455            None
3456        );
3457    }
3458    /// The archive is complete once the export returns, so the harness stops
3459    /// waiting there: the barrier ends, ACP dispatch resumes, and only the
3460    /// recovery floor waits for the installed archive.
3461    #[cfg(unix)]
3462    #[tokio::test]
3463    async fn releasing_a_checkpoint_after_capture_defers_only_the_recovery_floor() {
3464        // MJ_DATA_DIR is process-global, so run the database-backed half in an
3465        // exact child test instead of racing unrelated tests in this process.
3466        if std::env::var_os(RELEASE_TEST_CHILD).is_none() {
3467            let directory = tempfile::tempdir().unwrap();
3468            let test_name = format!(
3469                "{}::releasing_a_checkpoint_after_capture_defers_only_the_recovery_floor",
3470                module_path!()
3471                    .strip_prefix("mj_controller::")
3472                    .unwrap_or(module_path!())
3473            );
3474            let output = Command::new(std::env::current_exe().unwrap())
3475                .args(["--exact", &test_name, "--nocapture"])
3476                .env(RELEASE_TEST_CHILD, "1")
3477                .env("MJ_DATA_DIR", directory.path())
3478                .output()
3479                .unwrap();
3480            assert!(
3481                output.status.success(),
3482                "isolated checkpoint release test failed\nstdout:\n{}\nstderr:\n{}",
3483                String::from_utf8_lossy(&output.stdout),
3484                String::from_utf8_lossy(&output.stderr)
3485            );
3486            return;
3487        }
3488        // Alone in this child process, so it installs the one writer.
3489        let _writer = hel::hel_database::install_isolated_test_writer();
3490
3491        // A barrier that never releases would hang the suite instead of failing
3492        // it, so turn a stall into a hard error.
3493        std::thread::spawn(|| {
3494            std::thread::sleep(std::time::Duration::from_secs(120));
3495            eprintln!("the captured checkpoint never released its barrier");
3496            std::process::exit(101);
3497        });
3498
3499        let relay_root = tempfile::tempdir().unwrap();
3500        let (_channels, handle, mut relay, barrier_command_id, cursor) =
3501            latch_a_live_checkpoint(relay_root.path(), None, ReleaseSupport::Supported, false)
3502                .await;
3503        relay.end_latch();
3504        wait_until_the_actor_serves_again(&handle).await;
3505
3506        // Target state capture has just finished. Releasing proves the barrier first and
3507        // then hands ACP dispatch back.
3508        let completion = release_checkpoint_after_capture(
3509            &mut relay,
3510            LATCH_RELAY_SESSION,
3511            &barrier_command_id,
3512            &cursor,
3513        )
3514        .await
3515        .unwrap();
3516        assert_eq!(completion, CheckpointCompletion::ReleasedAfterCapture);
3517        let released = relay.sync_snapshot().await.unwrap();
3518        assert_eq!(released.operational.checkpoint_barrier, None);
3519        assert_eq!(released.operational.checkpoint_ready, None);
3520        assert_eq!(
3521            released.operational.recovery_floor_ordinal, 0,
3522            "an exported archive that is not installed may not release journal history"
3523        );
3524
3525        // The transfer is still running, and the harness is already working
3526        // again: a prompt submitted now reaches ACP dispatch.
3527        relay
3528            .submit(
3529                new_command_id("prompt").unwrap(),
3530                RelayCommand::Prompt {
3531                    prompt: vec![ContentBlock::Text(TextContent::new("during transfer"))],
3532                },
3533            )
3534            .await
3535            .unwrap();
3536        let mut dispatched = None;
3537        for attempt in 0.. {
3538            let snapshot = relay.sync_snapshot().await.unwrap();
3539            if let Some(active) = snapshot.operational.active_prompt {
3540                dispatched = Some(active);
3541                break;
3542            }
3543            assert!(attempt < 200, "a released barrier still froze ACP dispatch");
3544            tokio::time::sleep(std::time::Duration::from_millis(10)).await;
3545        }
3546        assert!(dispatched.is_some());
3547
3548        // The archive is installed, so the relay may finally forget the history
3549        // it covers.
3550        latched_checkpoint(
3551            relay,
3552            barrier_command_id,
3553            cursor.clone(),
3554            CheckpointCompletion::ReleasedAfterCapture,
3555        )
3556        .complete()
3557        .await
3558        .unwrap();
3559        handle.sync_now().await.unwrap();
3560        let installed = handle
3561            .view()
3562            .snapshot
3563            .expect("the actor published the advanced recovery floor");
3564        assert_eq!(installed.operational.recovery_floor_ordinal, cursor.ordinal);
3565        assert_eq!(installed.operational.recovery_floor_digest, cursor.digest);
3566    }
3567    /// A target still running a worker that predates the early release keeps
3568    /// its barrier through the transfer and ends it the way it always did.
3569    #[cfg(unix)]
3570    #[tokio::test]
3571    async fn a_worker_that_rejects_the_release_keeps_its_barrier_through_the_transfer() {
3572        // MJ_DATA_DIR is process-global, so run the database-backed half in an
3573        // exact child test instead of racing unrelated tests in this process.
3574        if std::env::var_os(LEGACY_RELEASE_TEST_CHILD).is_none() {
3575            let directory = tempfile::tempdir().unwrap();
3576            let test_name = format!(
3577                "{}::a_worker_that_rejects_the_release_keeps_its_barrier_through_the_transfer",
3578                module_path!()
3579                    .strip_prefix("mj_controller::")
3580                    .unwrap_or(module_path!())
3581            );
3582            let output = Command::new(std::env::current_exe().unwrap())
3583                .args(["--exact", &test_name, "--nocapture"])
3584                .env(LEGACY_RELEASE_TEST_CHILD, "1")
3585                .env("MJ_DATA_DIR", directory.path())
3586                .output()
3587                .unwrap();
3588            assert!(
3589                output.status.success(),
3590                "isolated legacy checkpoint release test failed\nstdout:\n{}\nstderr:\n{}",
3591                String::from_utf8_lossy(&output.stdout),
3592                String::from_utf8_lossy(&output.stderr)
3593            );
3594            return;
3595        }
3596        // Alone in this child process, so it installs the one writer.
3597        let _writer = hel::hel_database::install_isolated_test_writer();
3598
3599        // A rejected release that lost its barrier would hang the suite instead
3600        // of failing it, so turn a stall into a hard error.
3601        std::thread::spawn(|| {
3602            std::thread::sleep(std::time::Duration::from_secs(120));
3603            eprintln!("the rejected release never finished its checkpoint");
3604            std::process::exit(101);
3605        });
3606
3607        let relay_root = tempfile::tempdir().unwrap();
3608        let start_log = tempfile::tempdir().unwrap();
3609        let start_log = start_log.path().join("relay-starts");
3610        let (_channels, handle, mut relay, barrier_command_id, cursor) = latch_a_live_checkpoint(
3611            relay_root.path(),
3612            Some(&start_log),
3613            ReleaseSupport::Rejected,
3614            false,
3615        )
3616        .await;
3617        relay.end_latch();
3618        wait_until_the_actor_serves_again(&handle).await;
3619
3620        let completion = release_checkpoint_after_capture(
3621            &mut relay,
3622            LATCH_RELAY_SESSION,
3623            &barrier_command_id,
3624            &cursor,
3625        )
3626        .await
3627        .unwrap();
3628        assert_eq!(completion, CheckpointCompletion::HeldBarrier);
3629        // A refused command is a completed round trip, so the connection that
3630        // owns the barrier must survive it.
3631        assert_eq!(relay_starts(&start_log), 1);
3632
3633        // Today's ordering carries on: the barrier holds through the transfer,
3634        // the post-transfer revalidation still has something to prove, and the
3635        // completion both resumes dispatch and advances the recovery floor.
3636        let transferring = relay.sync_snapshot().await.unwrap();
3637        validate_checkpoint_barrier_snapshot(&transferring, &barrier_command_id, &cursor).unwrap();
3638        latched_checkpoint(relay, barrier_command_id, cursor.clone(), completion)
3639            .complete()
3640            .await
3641            .unwrap();
3642        handle.sync_now().await.unwrap();
3643        let completed = handle
3644            .view()
3645            .snapshot
3646            .expect("the actor published the completed barrier");
3647        assert_eq!(completed.operational.checkpoint_barrier, None);
3648        assert_eq!(completed.operational.recovery_floor_ordinal, cursor.ordinal);
3649    }
3650    /// A caller that cannot install a latched archive has to cancel its
3651    /// barrier. The latch is already back with the session actor, so the only
3652    /// thing that ends the barrier is dropping the connection that opened it:
3653    /// the worker cancels barriers whose connection disappears.
3654    #[cfg(unix)]
3655    #[tokio::test]
3656    async fn abandoning_a_latched_checkpoint_drops_the_connection_that_opened_its_barrier() {
3657        // MJ_DATA_DIR is process-global, so run the database-backed half in an
3658        // exact child test instead of racing unrelated tests in this process.
3659        if std::env::var_os(ABANDON_TEST_CHILD).is_none() {
3660            let directory = tempfile::tempdir().unwrap();
3661            let test_name = format!(
3662                "{}::abandoning_a_latched_checkpoint_drops_the_connection_that_opened_its_barrier",
3663                module_path!()
3664                    .strip_prefix("mj_controller::")
3665                    .unwrap_or(module_path!())
3666            );
3667            let output = Command::new(std::env::current_exe().unwrap())
3668                .args(["--exact", &test_name, "--nocapture"])
3669                .env(ABANDON_TEST_CHILD, "1")
3670                .env("MJ_DATA_DIR", directory.path())
3671                .output()
3672                .unwrap();
3673            assert!(
3674                output.status.success(),
3675                "isolated abandoned checkpoint test failed\nstdout:\n{}\nstderr:\n{}",
3676                String::from_utf8_lossy(&output.stdout),
3677                String::from_utf8_lossy(&output.stderr)
3678            );
3679            return;
3680        }
3681        // Alone in this child process, so it installs the one writer.
3682        let _writer = hel::hel_database::install_isolated_test_writer();
3683
3684        // An abandoned barrier that never releases its connection would hang
3685        // the suite instead of failing it, so turn a stall into a hard error.
3686        std::thread::spawn(|| {
3687            std::thread::sleep(std::time::Duration::from_secs(120));
3688            eprintln!("an abandoned checkpoint never released its relay connection");
3689            std::process::exit(101);
3690        });
3691
3692        let relay_root = tempfile::tempdir().unwrap();
3693        let start_log = tempfile::tempdir().unwrap();
3694        let start_log = start_log.path().join("relay-starts");
3695        let (_channels, handle, mut relay, barrier_command_id, cursor) = latch_a_live_checkpoint(
3696            relay_root.path(),
3697            Some(&start_log),
3698            ReleaseSupport::Supported,
3699            false,
3700        )
3701        .await;
3702        relay.end_latch();
3703        wait_until_the_actor_serves_again(&handle).await;
3704        assert_eq!(relay_starts(&start_log), 1);
3705
3706        latched_checkpoint(
3707            relay,
3708            barrier_command_id,
3709            cursor,
3710            CheckpointCompletion::HeldBarrier,
3711        )
3712        .abandon(LATCH_RELAY_SESSION)
3713        .await;
3714
3715        // The actor serves again, which proves the reclaimed lease was not
3716        // leaked, and it is talking to a new relay process, which proves the
3717        // connection that opened the barrier was dropped rather than handed
3718        // back alive.
3719        wait_until_the_actor_serves_again(&handle).await;
3720        assert_eq!(relay_starts(&start_log), 2);
3721    }
3722    /// The close policy, end to end against a live relay: a latch that finds
3723    /// its own content already archived issues no export or transfer command
3724    /// and keeps the installed archive, while the next latch after real
3725    /// session content goes back through the full export.
3726    #[cfg(unix)]
3727    #[tokio::test]
3728    async fn a_close_latch_reuses_an_unchanged_archive_and_exports_after_new_content() {
3729        // MJ_DATA_DIR is process-global, so run the database-backed half in an
3730        // exact child test instead of racing unrelated tests in this process.
3731        if std::env::var_os(REUSE_TEST_CHILD).is_none() {
3732            let directory = tempfile::tempdir().unwrap();
3733            let test_name = format!(
3734                "{}::a_close_latch_reuses_an_unchanged_archive_and_exports_after_new_content",
3735                module_path!()
3736                    .strip_prefix("mj_controller::")
3737                    .unwrap_or(module_path!())
3738            );
3739            let output = Command::new(std::env::current_exe().unwrap())
3740                .args(["--exact", &test_name, "--nocapture"])
3741                .env(REUSE_TEST_CHILD, "1")
3742                // Longer than the normal checkpoint barrier deadline. The
3743                // controller must wait for startup rather than restart it.
3744                .env(LATCH_RELAY_STARTUP_DELAY_MS, "31000")
3745                .env("MJ_DATA_DIR", directory.path())
3746                .output()
3747                .unwrap();
3748            assert!(
3749                output.status.success(),
3750                "isolated checkpoint reuse test failed\nstdout:\n{}\nstderr:\n{}",
3751                String::from_utf8_lossy(&output.stdout),
3752                String::from_utf8_lossy(&output.stderr)
3753            );
3754            return;
3755        }
3756        // Alone in this child process, so it installs the one writer.
3757        let _writer = hel::hel_database::install_isolated_test_writer();
3758
3759        // A latch that never returns would hang the suite instead of failing
3760        // it, so turn a stall into a hard error.
3761        std::thread::spawn(|| {
3762            std::thread::sleep(std::time::Duration::from_secs(120));
3763            eprintln!("the reuse checkpoint never finished its latch");
3764            std::process::exit(101);
3765        });
3766
3767        #[derive(Default)]
3768        struct RecordingExecutor {
3769            purposes: std::sync::Mutex<Vec<String>>,
3770            active_stages: std::sync::Mutex<Vec<ProvisionStage>>,
3771            stage_events: std::sync::Mutex<Vec<(ProvisionStage, bool)>>,
3772            observed_stages: std::sync::Mutex<Vec<(String, Vec<ProvisionStage>)>>,
3773        }
3774
3775        impl RecordingExecutor {
3776            fn refused(&self, command: &CommandSpec) -> Result<CommandOutput> {
3777                self.purposes.lock().unwrap().push(command.purpose.clone());
3778                self.observed_stages.lock().unwrap().push((
3779                    command.purpose.clone(),
3780                    self.active_stages.lock().unwrap().clone(),
3781                ));
3782                Ok(CommandOutput {
3783                    status: 1,
3784                    stdout: Vec::new(),
3785                    stderr: b"no target is provisioned for this test".to_vec(),
3786                })
3787            }
3788
3789            fn purposes(&self) -> Vec<String> {
3790                self.purposes.lock().unwrap().clone()
3791            }
3792
3793            fn observed_stages(&self) -> Vec<(String, Vec<ProvisionStage>)> {
3794                self.observed_stages.lock().unwrap().clone()
3795            }
3796
3797            fn stage_events(&self) -> Vec<(ProvisionStage, bool)> {
3798                self.stage_events.lock().unwrap().clone()
3799            }
3800        }
3801
3802        impl CommandExecutor for RecordingExecutor {
3803            fn execute(&self, command: &CommandSpec) -> Result<CommandOutput> {
3804                self.refused(command)
3805            }
3806
3807            fn execute_with_stdin(
3808                &self,
3809                command: &CommandSpec,
3810                _input: &mut (dyn std::io::Read + Send),
3811            ) -> Result<CommandOutput> {
3812                self.refused(command)
3813            }
3814
3815            fn stage_started(&self, stage: ProvisionStage) {
3816                self.active_stages.lock().unwrap().push(stage);
3817                self.stage_events.lock().unwrap().push((stage, true));
3818            }
3819
3820            fn stage_finished(&self, stage: ProvisionStage) {
3821                let mut active = self.active_stages.lock().unwrap();
3822                let position = active
3823                    .iter()
3824                    .position(|active_stage| *active_stage == stage)
3825                    .expect("stage finished without a matching start");
3826                active.remove(position);
3827                self.stage_events.lock().unwrap().push((stage, false));
3828            }
3829        }
3830
3831        let data_directory = PathBuf::from(std::env::var_os("MJ_DATA_DIR").unwrap());
3832        let relay_root = data_directory.join("relay");
3833        let profile_home = data_directory.join("profile");
3834        let archive_directory = data_directory.join("archives");
3835        for directory in [&relay_root, &profile_home, &archive_directory] {
3836            std::fs::create_dir_all(directory).unwrap();
3837        }
3838        // The archive covers the fake runtime's SessionOpened and
3839        // SessionConfigured events, before any checkpoint bookkeeping.
3840        let checkpoint = write_checkpoint_gate_archive(&archive_directory, LATCH_RELAY_SESSION, 2);
3841
3842        let mut session = checkpoint_test_session(LATCH_RELAY_SESSION);
3843        session.target_template_id = "local".into();
3844        session.target = Some(TargetLocator::LocalBare {
3845            worker_root: data_directory.join("workers").join(LATCH_RELAY_SESSION),
3846        });
3847        session.checkpoint = Some(checkpoint.clone());
3848        hel::hel_database::save_session(&session).unwrap();
3849
3850        let mut config = HelConfig::default();
3851        config.profiles.insert(
3852            "codex".into(),
3853            HarnessProfile {
3854                kind: hel::hel_config::HarnessKind::Codex,
3855                home: profile_home,
3856                environment: BTreeMap::new(),
3857                context_window_bytes: None,
3858            },
3859        );
3860        config
3861            .targets
3862            .insert("local".into(), TargetTemplate::LocalBare);
3863        config.bundles.insert(
3864            "project".into(),
3865            ProjectBundle {
3866                primary_repo: "project".into(),
3867                repositories: vec![ProjectRepository {
3868                    id: "project".into(),
3869                    github: Some("example/project".into()),
3870                    local: None,
3871                    destination: "project".into(),
3872                    git_ref: None,
3873                }],
3874            },
3875        );
3876        let controller = Controller {
3877            config,
3878            state: HelState {
3879                sessions: BTreeMap::from([(LATCH_RELAY_SESSION.into(), session)]),
3880                ..HelState::default()
3881            },
3882        };
3883
3884        let channels = crate::hel_session_manager::spawn_session_manager().unwrap();
3885        channels
3886            .targets
3887            .send(vec![latch_relay_target(
3888                &relay_root,
3889                None,
3890                ReleaseSupport::Supported,
3891                false,
3892            )])
3893            .unwrap();
3894        let handle = channels
3895            .control
3896            .wait_for_session(LATCH_RELAY_SESSION, Duration::from_secs(10))
3897            .await
3898            .unwrap();
3899
3900        let executor = RecordingExecutor::default();
3901        let latched = controller
3902            .checkpoint_session_latched(
3903                LATCH_RELAY_SESSION,
3904                &executor,
3905                Some(&channels.control),
3906                LatchExclusivity::HoldThroughClose,
3907                CheckpointExportPolicy::ReuseUnchangedArchive,
3908            )
3909            .await
3910            .unwrap();
3911
3912        assert!(
3913            executor.purposes().is_empty(),
3914            "an unchanged session exported an archive anyway: {:?}",
3915            executor.purposes()
3916        );
3917        assert_eq!(latched.artifact.metadata, checkpoint);
3918        assert!(checkpoint.archive_path.exists());
3919
3920        // The cursor close seals is ahead of the reused archive by this
3921        // checkpoint's own bookkeeping.
3922        assert!(latched.cursor.ordinal > checkpoint.event_frontier);
3923        let cursor = latched.cursor.clone();
3924        latched.complete().await.unwrap();
3925        wait_until_the_actor_serves_again(&handle).await;
3926
3927        // An ordinary recovery copy during a turn must defer before it
3928        // journals BeginCheckpoint, so no disconnect-cancellation message is
3929        // produced for a routine busy observation.
3930        handle
3931            .submit(
3932                new_command_id("busy-prompt").unwrap(),
3933                RelayCommand::Prompt {
3934                    prompt: vec![ContentBlock::Text(TextContent::new("keep working"))],
3935                },
3936            )
3937            .await
3938            .unwrap();
3939        let mut connection = handle.lease_connection().await.unwrap();
3940        let before = connection.connection_mut().sync().await.unwrap();
3941        assert_eq!(before.operational.execution, RelayExecutionState::Running);
3942        connection.release();
3943        let deferred = controller
3944            .checkpoint_session_latched(
3945                LATCH_RELAY_SESSION,
3946                &executor,
3947                Some(&channels.control),
3948                LatchExclusivity::ReleaseAfterLatch,
3949                CheckpointExportPolicy::ReuseUnchangedArchive,
3950            )
3951            .await;
3952        assert!(
3953            matches!(deferred, Err(ref error) if error.downcast_ref::<CheckpointDeferred>().is_some())
3954        );
3955        wait_until_the_actor_serves_again(&handle).await;
3956        let mut connection = handle.lease_connection().await.unwrap();
3957        let after = connection.connection_mut().sync().await.unwrap();
3958        assert_eq!(after.operational.execution, RelayExecutionState::Running);
3959        assert!(after.operational.checkpoint_barrier.is_none());
3960        let journal =
3961            std::fs::read_to_string(relay_root.join("relay-journal/active.jsonl")).unwrap();
3962        for line in journal.lines() {
3963            let event: hel::hel_worker::RelayEvent = serde_json::from_str(line).unwrap();
3964            if event.ordinal > before.operational.latest_ordinal {
3965                assert!(
3966                    !matches!(
3967                        event.observation,
3968                        hel::hel_worker::RelayObservation::CommandQueued {
3969                            command: RelayCommand::BeginCheckpoint { .. },
3970                            ..
3971                        } | hel::hel_worker::RelayObservation::CommandInterrupted {
3972                            command: hel::hel_worker::RelayCommandKind::BeginCheckpoint,
3973                            ..
3974                        }
3975                    ),
3976                    "busy deferral journaled checkpoint activity: {event:?}"
3977                );
3978            }
3979        }
3980        connection.release();
3981        handle
3982            .submit(
3983                new_command_id("finish-busy-prompt").unwrap(),
3984                RelayCommand::CancelTurn,
3985            )
3986            .await
3987            .unwrap();
3988        handle.sync_now().await.unwrap();
3989
3990        // Real session content, and the same policy has to export again.
3991        handle
3992            .submit(
3993                new_command_id("resume-notice").unwrap(),
3994                RelayCommand::RecordNotice {
3995                    text: "the session changed".into(),
3996                },
3997            )
3998            .await
3999            .unwrap();
4000        for attempt in 0.. {
4001            handle.sync_now().await.unwrap();
4002            let materialized = handle.view().snapshot.map(|snapshot| snapshot.materialized);
4003            if materialized.is_some_and(|materialized| {
4004                materialized.applied_event_ordinal > cursor.ordinal
4005                    && !materialized.transcript.is_empty()
4006            }) {
4007                break;
4008            }
4009            assert!(attempt < 200, "the notice never reached the projection");
4010            tokio::time::sleep(std::time::Duration::from_millis(10)).await;
4011        }
4012
4013        let changed = controller
4014            .checkpoint_session_latched(
4015                LATCH_RELAY_SESSION,
4016                &executor,
4017                Some(&channels.control),
4018                LatchExclusivity::HoldThroughClose,
4019                CheckpointExportPolicy::ReuseUnchangedArchive,
4020            )
4021            .await;
4022        let Err(error) = changed else {
4023            panic!("a changed session reused its installed archive");
4024        };
4025
4026        assert!(
4027            executor
4028                .purposes()
4029                .contains(&"export target checkpoint".to_owned()),
4030            "a changed session skipped its export: {:?}",
4031            executor.purposes()
4032        );
4033        assert!(
4034            format!("{error:#}").contains("no target is provisioned for this test"),
4035            "{error:#}"
4036        );
4037        assert!(
4038            executor.observed_stages().iter().any(|(purpose, stages)| {
4039                purpose == "export target checkpoint"
4040                    && stages.contains(&ProvisionStage::RecoveryCopy)
4041            }),
4042            "close checkpoint export did not run inside RecoveryCopy: {:?}",
4043            executor.observed_stages()
4044        );
4045        assert_eq!(
4046            executor
4047                .stage_events()
4048                .into_iter()
4049                .filter(|(stage, _)| *stage == ProvisionStage::RecoveryCopy)
4050                .collect::<Vec<_>>(),
4051            vec![
4052                (ProvisionStage::RecoveryCopy, true),
4053                (ProvisionStage::RecoveryCopy, false)
4054            ]
4055        );
4056        assert!(executor.active_stages.lock().unwrap().is_empty());
4057        assert!(checkpoint.archive_path.exists());
4058    }
4059    #[cfg(unix)]
4060    fn relay_starts(path: &Path) -> usize {
4061        std::fs::read_to_string(path)
4062            .unwrap_or_default()
4063            .lines()
4064            .count()
4065    }
4066    /// A latched checkpoint carrying a placeholder artifact. These tests
4067    /// exercise its relay barrier, not the archive it names.
4068    #[cfg(unix)]
4069    fn latched_checkpoint(
4070        relay: ControllerRelayLease,
4071        barrier_command_id: String,
4072        cursor: RelayCursor,
4073        completion: CheckpointCompletion,
4074    ) -> LatchedCheckpoint {
4075        LatchedCheckpoint {
4076            artifact: CheckpointArtifact {
4077                metadata: CheckpointMetadata {
4078                    archive_path: PathBuf::from("checkpoint.hel.zip"),
4079                    sha256: "a".repeat(64),
4080                    created_at: now(),
4081                    event_frontier: cursor.ordinal,
4082                },
4083                native_session_id: "native-session".into(),
4084                event_frontier_digest: cursor.digest.clone(),
4085            },
4086            relay,
4087            barrier_command_id,
4088            cursor,
4089            completion,
4090        }
4091    }
4092    #[test]
4093    fn checkpoint_persistence_rollback_restores_memory_and_reports_both_failures() {
4094        let session_id = "0123456789abcdef0123456789abcdef";
4095        let previous = checkpoint_test_session(session_id);
4096        let mut changed = previous.clone();
4097        changed.state = SessionState::Closing;
4098        changed.last_checkpoint_error = Some("partially installed checkpoint".into());
4099        let mut state = HelState::default();
4100        state.sessions.insert(session_id.into(), changed);
4101
4102        let error = restore_session_after_persistence_failure(
4103            &mut state,
4104            session_id,
4105            &previous,
4106            anyhow::anyhow!("verified checkpoint persistence failed"),
4107            |record| {
4108                assert_eq!(record, &previous);
4109                Err(anyhow::anyhow!("rollback database write failed"))
4110            },
4111        );
4112
4113        assert_eq!(state.sessions.get(session_id), Some(&previous));
4114        let detail = format!("{error:#}");
4115        assert!(detail.contains("verified checkpoint persistence failed"));
4116        assert!(detail.contains("rollback database write failed"));
4117    }
4118    #[test]
4119    fn installed_checkpoint_gate_reopens_and_checks_sha() {
4120        let directory = tempfile::tempdir().unwrap();
4121        let session_id = "0123456789abcdef0123456789abcdef";
4122        let checkpoint = write_checkpoint_gate_archive(directory.path(), session_id, 7);
4123        verify_installed_checkpoint_gate(session_id, &checkpoint).unwrap();
4124
4125        let mut wrong_sha = checkpoint.clone();
4126        wrong_sha.sha256 = "b".repeat(64);
4127        assert!(
4128            verify_installed_checkpoint_gate(session_id, &wrong_sha)
4129                .unwrap_err()
4130                .to_string()
4131                .contains("SHA changed")
4132        );
4133        std::fs::write(
4134            &checkpoint.archive_path,
4135            b"changed after first verification",
4136        )
4137        .unwrap();
4138        assert!(
4139            format!(
4140                "{:#}",
4141                verify_installed_checkpoint_gate(session_id, &checkpoint).unwrap_err()
4142            )
4143            .contains("installed checkpoint SHA changed")
4144        );
4145    }
4146    #[test]
4147    fn an_installed_archive_is_reused_when_only_relay_bookkeeping_moved() {
4148        let directory = tempfile::tempdir().unwrap();
4149        let session_id = "0123456789abcdef0123456789abcdef";
4150        let checkpoint = write_checkpoint_gate_archive(directory.path(), session_id, 7);
4151        let archived = verify_archive_streaming(&checkpoint.archive_path)
4152            .unwrap()
4153            .canonical_session;
4154
4155        // What a checkpoint taken seconds later latches on an idle session:
4156        // the frontier and the activity watermark moved, the content did not.
4157        let mut latched = archived.clone();
4158        latched.event_frontier += 6;
4159        latched.event_frontier_digest = "b".repeat(64);
4160        latched.session.last_activity_at_ms = Some(9_999);
4161
4162        let artifact = reusable_installed_checkpoint(
4163            session_id,
4164            Some(&checkpoint),
4165            "native-session",
4166            latched.event_frontier,
4167            &latched,
4168        )
4169        .expect("an unchanged session reuses its installed archive");
4170
4171        assert_eq!(artifact.metadata, checkpoint);
4172        assert_eq!(artifact.native_session_id, "native-session");
4173        assert_eq!(
4174            artifact.event_frontier_digest,
4175            archived.event_frontier_digest
4176        );
4177        // The reused archive is still the gate close destroys through.
4178        verify_checkpoint_artifact(session_id, &artifact).unwrap();
4179        verify_installed_checkpoint_gate(session_id, &artifact.metadata).unwrap();
4180    }
4181    #[test]
4182    fn archive_reuse_falls_back_to_a_full_export_for_anything_but_bookkeeping() {
4183        let directory = tempfile::tempdir().unwrap();
4184        let session_id = "0123456789abcdef0123456789abcdef";
4185        let checkpoint = write_checkpoint_gate_archive(directory.path(), session_id, 7);
4186        let archived = verify_archive_streaming(&checkpoint.archive_path)
4187            .unwrap()
4188            .canonical_session;
4189        let mut latched = archived.clone();
4190        latched.event_frontier += 6;
4191        let reuse = |installed: Option<&CheckpointMetadata>,
4192                     ordinal: u64,
4193                     session: &CanonicalSessionSnapshot| {
4194            reusable_installed_checkpoint(session_id, installed, "native-session", ordinal, session)
4195        };
4196
4197        assert!(reuse(None, latched.event_frontier, &latched).is_none());
4198
4199        let mut with_new_content = latched.clone();
4200        with_new_content.transcript.push(CanonicalTranscriptItem {
4201            stable_id: "system:notice:notice-1".into(),
4202            position: latched.event_frontier,
4203            latest_content_event_ordinal: None,
4204            created_at_ms: 2_000,
4205            last_changed_at_ms: 2_000,
4206            body: CanonicalTranscriptBody::System {
4207                text: "resumed".into(),
4208            },
4209        });
4210        assert!(reuse(Some(&checkpoint), latched.event_frontier, &with_new_content).is_none());
4211
4212        // An archive the latch has not reached yet cannot describe the session.
4213        assert!(reuse(Some(&checkpoint), checkpoint.event_frontier - 1, &latched).is_none());
4214
4215        let mut wrong_sha = checkpoint.clone();
4216        wrong_sha.sha256 = "b".repeat(64);
4217        assert!(reuse(Some(&wrong_sha), latched.event_frontier, &latched).is_none());
4218
4219        let another_session =
4220            write_checkpoint_gate_archive(directory.path(), "1123456789abcdef0123456789abcdef", 7);
4221        assert!(reuse(Some(&another_session), latched.event_frontier, &latched).is_none());
4222
4223        std::fs::write(&checkpoint.archive_path, b"not an archive any more").unwrap();
4224        assert!(reuse(Some(&checkpoint), latched.event_frontier, &latched).is_none());
4225    }
4226}