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