1use 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
39const CHECKPOINT_BARRIER_TIMEOUT: Duration = Duration::from_secs(30);
41const CHECKPOINT_BARRIER_TIMEOUT_AFTER_RESTART: Duration = Duration::from_secs(300);
44
45pub 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 pub event_frontier_digest: String,
115}
116
117pub(super) enum ControllerRelayLease {
125 Managed {
126 handle: ManagedSessionHandle,
127 lease: Option<ManagedSessionLease>,
128 },
129 Standalone(StandaloneSession),
130}
131
132impl ControllerRelayLease {
133 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 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 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 async fn cancel_abandoned_barrier(&mut self) -> Result<()> {
203 let Self::Managed { handle, lease } = self else {
204 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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
227pub(super) enum LatchExclusivity {
228 ReleaseAfterLatch,
233 HoldThroughClose,
236}
237
238#[derive(Debug, Clone, Copy, PartialEq, Eq)]
240pub(super) enum CheckpointExportPolicy {
241 Always,
243 ReuseUnchangedArchive,
247}
248
249#[derive(Debug, Clone, Copy, PartialEq, Eq)]
251pub(super) enum CheckpointCompletion {
252 HeldBarrier,
256 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
269impl LatchedCheckpoint {
276 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 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 async fn abandon(mut self, session_id: &str) {
304 if self.completion == CheckpointCompletion::ReleasedAfterCapture {
305 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 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 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 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 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 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 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 if exclusivity == LatchExclusivity::ReleaseAfterLatch {
828 relay.end_latch();
829 }
830
831 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 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 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 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 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 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 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 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 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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1212enum BarrierBusyPolicy {
1213 DeferWhileRunning,
1219 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
1254fn 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#[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#[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
1352pub 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
1367fn 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
1385fn 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
1446async 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
1526fn 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
1614fn 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
1645fn 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
1658fn 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
1774fn 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
1901pub(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 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 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 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 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 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 #[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 #[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 #[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 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 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 #[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 #[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 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 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 #[cfg(unix)]
2726 #[derive(Clone, Copy, PartialEq, Eq)]
2727 enum ReleaseSupport {
2728 Supported,
2729 Rejected,
2732 }
2733 #[test]
2740 fn latch_relay_child_serves_stdio() {
2741 let Some(root) = std::env::var_os(LATCH_RELAY_ROOT) else {
2742 return;
2743 };
2744 println!();
2748 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 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 #[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 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 #[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 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 #[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 #[cfg(unix)]
2938 #[tokio::test]
2939 async fn ending_the_checkpoint_latch_returns_the_connection_to_its_actor() {
2940 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 let _writer = hel::hel_database::install_isolated_test_writer();
2966
2967 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 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 let latched = relay.sync_snapshot().await.unwrap();
2993 validate_checkpoint_barrier_snapshot(&latched, &barrier_command_id, &cursor).unwrap();
2994
2995 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 #[cfg(unix)]
3035 #[tokio::test]
3036 async fn releasing_a_checkpoint_after_capture_defers_only_the_recovery_floor() {
3037 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 let _writer = hel::hel_database::install_isolated_test_writer();
3063
3064 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 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 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 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 #[cfg(unix)]
3142 #[tokio::test]
3143 async fn a_worker_that_rejects_the_release_keeps_its_barrier_through_the_transfer() {
3144 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 let _writer = hel::hel_database::install_isolated_test_writer();
3170
3171 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 assert_eq!(relay_starts(&start_log), 1);
3203
3204 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 #[cfg(unix)]
3226 #[tokio::test]
3227 async fn abandoning_a_latched_checkpoint_drops_the_connection_that_opened_its_barrier() {
3228 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 let _writer = hel::hel_database::install_isolated_test_writer();
3254
3255 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 wait_until_the_actor_serves_again(&handle).await;
3290 assert_eq!(relay_starts(&start_log), 2);
3291 }
3292 #[cfg(unix)]
3297 #[tokio::test]
3298 async fn a_close_latch_reuses_an_unchanged_archive_and_exports_after_new_content() {
3299 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 let _writer = hel::hel_database::install_isolated_test_writer();
3325
3326 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 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 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 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 #[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 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 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 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}