diff --git a/packages/paperclip-runner/runner/crates/runner-core/src/acpx_provider_backend.rs b/packages/paperclip-runner/runner/crates/runner-core/src/acpx_provider_backend.rs index 382a14887ba..be646761028 100644 --- a/packages/paperclip-runner/runner/crates/runner-core/src/acpx_provider_backend.rs +++ b/packages/paperclip-runner/runner/crates/runner-core/src/acpx_provider_backend.rs @@ -1788,6 +1788,17 @@ impl AcpxCommandExecutor { "ACPX provider lifetime cleanup is not yet proven", )); } + // No path (session.open, a same-run run.attach) reopens a session whose + // run failed closed at its turn-end provider restart. + if self.session.is_none() + && self.state.as_ref().is_some_and(|state| { + state.turn_end_restart_failed_run.as_deref() == Some(self.context.run_id.as_str()) + }) + { + return Err(DurableRunnerError::invalid( + "ACPX session failed closed after this run's turn-end provider restart failed", + )); + } if self.session.is_none() { let recovering = self .state diff --git a/packages/paperclip-runner/runner/crates/runner-core/tests/support/acpx_turn_end_restart.rs b/packages/paperclip-runner/runner/crates/runner-core/tests/support/acpx_turn_end_restart.rs index f493d766da9..55a7a11f534 100644 --- a/packages/paperclip-runner/runner/crates/runner-core/tests/support/acpx_turn_end_restart.rs +++ b/packages/paperclip-runner/runner/crates/runner-core/tests/support/acpx_turn_end_restart.rs @@ -441,11 +441,30 @@ fn a_runner_restart_in_the_same_run_keeps_the_failed_turn_end_restart_closed() { .to_string(); assert_eq!(failed, "ACPX session is not open"); assert_eq!(harness.fixture.process_starts().len(), starts); + // Neither session.open nor a same-run attach reopens it. + let reopened = harness + .executor + .execute(&command(91, "session.open", json!({}))) + .unwrap_err() + .to_string(); + assert!(reopened.contains("failed closed"), "{reopened}"); + let same_run_attach = json!({"provider": harness.descriptor.clone()}); + let attach_error = harness + .executor + .execute(&command(92, "run.attach", same_run_attach)) + .unwrap_err() + .to_string(); + assert!(attach_error.contains("failed closed"), "{attach_error}"); + assert_eq!(harness.fixture.process_starts().len(), starts); + assert_eq!( + harness.fixture.state()["turnEndRestartFailedRun"], + harness.config.run_id + ); assert_eq!(live_providers(&harness), Vec::::new()); assert_eq!(harness.fixture.state()["lifecycle"], "suspended"); // The next run still resumes the conversation and clears the restriction. harness.executor.shutdown().unwrap(); - harness.sequence = 90; + harness.sequence = 92; harness.config.run_id = "run-2".to_owned(); harness.executor = NativeProviderCommandExecutor::with_runner_config(&harness.fixture.0, &harness.config);