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