Treat intentional cancellation as a non-failure terminal
This commit is contained in:
@@ -1075,6 +1075,17 @@ fn project_session_snapshot_for_segment(
|
||||
},
|
||||
));
|
||||
}
|
||||
LogEntry::RunCancelled { ts } => {
|
||||
active_run_turn_count = None;
|
||||
entries.push(legacy_entry(
|
||||
&session_key,
|
||||
segment_id.as_ref(),
|
||||
log_index,
|
||||
0,
|
||||
*ts,
|
||||
SessionSnapshotEntryData::RunCancelled,
|
||||
));
|
||||
}
|
||||
LogEntry::RunErrored {
|
||||
ts,
|
||||
message,
|
||||
@@ -2849,6 +2860,27 @@ mod tests {
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cancelled_run_projects_as_a_non_failure_terminal() {
|
||||
let snapshot = project_session_snapshot(
|
||||
crate::new_session_id(),
|
||||
&[
|
||||
LogEntry::Invoke {
|
||||
ts: 10,
|
||||
trigger: protocol::InvokeKind::UserSend,
|
||||
},
|
||||
LogEntry::RunCancelled { ts: 20 },
|
||||
],
|
||||
);
|
||||
|
||||
assert_eq!(snapshot.entries.len(), 1);
|
||||
assert_eq!(snapshot.entries[0].timestamp, 20);
|
||||
assert!(matches!(
|
||||
snapshot.entries[0].data,
|
||||
SessionSnapshotEntryData::RunCancelled
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn retained_snapshot_read_projects_active_segment_without_rewriting_files() {
|
||||
let root = tempfile::tempdir().unwrap();
|
||||
|
||||
@@ -28,7 +28,7 @@ use crate::logged_item::LoggedItem;
|
||||
/// `run()`/`resume()` (current callers persist a single TurnEnd at
|
||||
/// run completion); the fork-point seq for `at_turn_index` is the
|
||||
/// preceding `Invoke` entry, not the TurnEnd.
|
||||
/// - `RunCompleted` / `RunErrored` — marks end of a `run()` or `resume()` call
|
||||
/// - `RunCompleted` / `RunCancelled` / `RunErrored` — terminal run outcomes
|
||||
/// - `PausedTurnAbandoned` — explicit abandon/cancel of a paused interrupted turn
|
||||
/// - `ConfigChanged` — `RequestConfig` mutation
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
@@ -86,9 +86,10 @@ pub enum LogEntry {
|
||||
/// serde tag already occupies `"kind"`.
|
||||
///
|
||||
/// Replay marks the run interrupted until a terminal `RunCompleted`,
|
||||
/// `RunErrored`, or `PausedTurnAbandoned` entry proves how it ended. This
|
||||
/// makes a process/disk failure between Invoke and its terminal record
|
||||
/// restore conservatively instead of re-running a dangling tool call.
|
||||
/// `RunCancelled`, `RunErrored`, or `PausedTurnAbandoned` entry proves how
|
||||
/// it ended. This makes a process/disk failure between Invoke and its
|
||||
/// terminal record restore conservatively instead of re-running a dangling
|
||||
/// tool call.
|
||||
Invoke { ts: u64, trigger: InvokeKind },
|
||||
|
||||
/// Canonical user submission with its exact model-visible entries. Typed
|
||||
@@ -152,6 +153,10 @@ pub enum LogEntry {
|
||||
active_run_turn_count: usize,
|
||||
},
|
||||
|
||||
/// The logical Run ended because the user intentionally cancelled it.
|
||||
/// This is terminal, but is not an execution failure.
|
||||
RunCancelled { ts: u64 },
|
||||
|
||||
/// `run()` / `resume()` が `EngineError` で終了した。
|
||||
/// `EngineError` は `Serialize` 不可なので `message` のみ lossy 保持する。
|
||||
/// Audit-only metadata: replay は `interrupted` のみ反映する。
|
||||
@@ -389,6 +394,10 @@ pub fn collect_state(entries: &[LogEntry]) -> RestoredState {
|
||||
state.last_run_interrupted = true;
|
||||
state.active_run_turn_count = Some(*active_run_turn_count);
|
||||
}
|
||||
LogEntry::RunCancelled { .. } => {
|
||||
state.last_run_interrupted = false;
|
||||
state.active_run_turn_count = None;
|
||||
}
|
||||
LogEntry::RunErrored { interrupted, .. } => {
|
||||
state.last_run_interrupted = *interrupted;
|
||||
state.active_run_turn_count = None;
|
||||
@@ -889,6 +898,25 @@ mod tests {
|
||||
assert_eq!(state.active_run_turn_count, Some(2));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn replay_cancelled_run_is_terminal_without_becoming_a_failure() {
|
||||
let state = collect_state(&[
|
||||
LogEntry::Invoke {
|
||||
ts: 100,
|
||||
trigger: InvokeKind::UserSend,
|
||||
},
|
||||
LogEntry::TurnEnd {
|
||||
ts: 200,
|
||||
turn_count: 1,
|
||||
},
|
||||
LogEntry::RunCancelled { ts: 300 },
|
||||
]);
|
||||
|
||||
assert!(!state.last_run_interrupted);
|
||||
assert_eq!(state.active_run_turn_count, None);
|
||||
assert_eq!(state.turn_count, 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn legacy_yielded_run_completed_remains_resumable_but_is_not_terminal_success() {
|
||||
let entry: LogEntry = serde_json::from_value(serde_json::json!({
|
||||
|
||||
Reference in New Issue
Block a user