Skip to main content

mj_controller/hel_controller/
checkpoint.rs

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