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