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