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