mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-11-10 12:37:55 +01:00
fix(runner): also re-bind a same-run open ACPX session after a deploy
A same-build restart restores an open session between turns, which is also the state a lost-turn restore leaves behind, so a later upgrade restart can find it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
ded6d0774e
commit
070365404e
2 files changed
+51
-10
No files matched your search
@@ -113,13 +113,14 @@ conversation, only when all of these are true:
|
|||||||
- The session is in one of these two states:
|
- The session is in one of these two states:
|
||||||
- **Suspended.** It has no active turn, pending runtime request, undelivered
|
- **Suspended.** It has no active turn, pending runtime request, undelivered
|
||||||
provider event, or unconfirmed provider exit.
|
provider event, or unconfirmed provider exit.
|
||||||
- **An interrupted turn of the same heartbeat run.** The deploy restarted the
|
- **An interrupted turn or open session of the same heartbeat run.** The
|
||||||
runner mid-turn and the same run resumes. The turn has no accepted semantic
|
deploy restarted the runner and the same run resumes. There is no accepted
|
||||||
result, no pending runtime request, no unconfirmed provider exit, and no
|
semantic result, no pending runtime request, no unconfirmed provider exit,
|
||||||
active goal. These are the preconditions of the same-build lost-turn
|
and no active goal. Same-build recovery then runs unchanged. It reopens an
|
||||||
recovery, which then runs unchanged: it restores the conversation once the
|
open session. For an active turn it uses lost-turn recovery: the
|
||||||
original provider lifetime loses its fence quorum, and otherwise closes the
|
conversation is restored once the original provider lifetime loses its
|
||||||
lost turn. An active turn saved by a different run stays strict.
|
fence quorum, and otherwise the lost turn is closed. An active turn or open
|
||||||
|
session saved by a different run stays strict.
|
||||||
- The startup has no candidate admission.
|
- The startup has no candidate admission.
|
||||||
- The saved descriptor names this startup's sidecar command and arguments.
|
- The saved descriptor names this startup's sidecar command and arguments.
|
||||||
|
|
||||||
|
|||||||
@@ -1056,7 +1056,8 @@ impl AcpxCommandExecutor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// A deploy that restarts the runner mid-turn resumes the SAME heartbeat
|
// A deploy that restarts the runner mid-turn resumes the SAME heartbeat
|
||||||
// run with a saved active turn. A same-build restart recovers that turn
|
// run with a saved active turn or open session. A same-build restart
|
||||||
|
// reopens an open session and recovers an active turn
|
||||||
// through `restore_conversation_after_lost_turn`; an upgraded build must
|
// through `restore_conversation_after_lost_turn`; an upgraded build must
|
||||||
// take the same path instead of refusing the digest. Re-bind only when
|
// take the same path instead of refusing the digest. Re-bind only when
|
||||||
// that recovery's own preconditions hold, so its provider-lifetime fence
|
// that recovery's own preconditions hold, so its provider-lifetime fence
|
||||||
@@ -1064,8 +1065,13 @@ impl AcpxCommandExecutor {
|
|||||||
// closed. A different run's active turn stays strict.
|
// closed. A different run's active turn stays strict.
|
||||||
fn rebinds_interrupted_turn(&self, state: &AcpxDurableState) -> bool {
|
fn rebinds_interrupted_turn(&self, state: &AcpxDurableState) -> bool {
|
||||||
state.descriptor.run_id == self.context.run_id
|
state.descriptor.run_id == self.context.run_id
|
||||||
&& matches!(state.lifecycle.as_str(), "turn_starting" | "turn_active")
|
&& match state.lifecycle.as_str() {
|
||||||
&& state.active_turn_id.is_some()
|
"turn_starting" | "turn_active" => state.active_turn_id.is_some(),
|
||||||
|
// A same-build restart restores an open session between turns
|
||||||
|
// (also the state a lost-turn restore leaves behind).
|
||||||
|
"session_open" => state.active_turn_id.is_none(),
|
||||||
|
_ => false,
|
||||||
|
}
|
||||||
&& state.semantic_result.is_none()
|
&& state.semantic_result.is_none()
|
||||||
&& (state.goal_projection.is_null() || goal_is_dormant(&state.goal_projection))
|
&& (state.goal_projection.is_null() || goal_is_dormant(&state.goal_projection))
|
||||||
}
|
}
|
||||||
@@ -6218,6 +6224,40 @@ mod tests {
|
|||||||
fs::remove_dir_all(directory).unwrap();
|
fs::remove_dir_all(directory).unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(unix)]
|
||||||
|
#[test]
|
||||||
|
fn open_session_of_the_same_run_rebinds_to_the_upgraded_runner_launch_profile() {
|
||||||
|
let (directory, config, state_path) =
|
||||||
|
interrupted_turn_after_runner_upgrade("upgrade-open-session");
|
||||||
|
let mut state: AcpxDurableState =
|
||||||
|
serde_json::from_slice(&fs::read(&state_path).unwrap()).unwrap();
|
||||||
|
// A lost-turn restore leaves the session open with undelivered events.
|
||||||
|
state.lifecycle = "session_open".into();
|
||||||
|
state.active_turn_id = None;
|
||||||
|
state
|
||||||
|
.push(NormalizedProviderEvent {
|
||||||
|
event_type: "session.resumed".into(),
|
||||||
|
priority: EventPriority::P0,
|
||||||
|
payload: json!({}),
|
||||||
|
})
|
||||||
|
.unwrap();
|
||||||
|
fs::write(&state_path, serde_json::to_vec(&state).unwrap()).unwrap();
|
||||||
|
|
||||||
|
let mut recovered = AcpxCommandExecutor::with_runner_config(&directory, &config);
|
||||||
|
// The fixture's provider cannot start; the restore must get that far
|
||||||
|
// instead of refusing the saved state.
|
||||||
|
let error = recovered.restore().unwrap_err();
|
||||||
|
assert!(
|
||||||
|
error.to_string().contains("failed to start ACPX provider"),
|
||||||
|
"{error}"
|
||||||
|
);
|
||||||
|
assert_ne!(
|
||||||
|
error.classification(),
|
||||||
|
Some(crate::durable::PROVIDER_STATE_INCOMPATIBLE)
|
||||||
|
);
|
||||||
|
fs::remove_dir_all(directory).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(unix)]
|
#[cfg(unix)]
|
||||||
#[test]
|
#[test]
|
||||||
fn interrupted_turn_after_upgrade_stays_strict_when_lost_turn_recovery_is_unsafe() {
|
fn interrupted_turn_after_upgrade_stays_strict_when_lost_turn_recovery_is_unsafe() {
|
||||||
|
|||||||
Reference in new issue
Block a user