Skip to content

Commit 2abeaa0

Browse files
authored
fix: stamp finished_at for CandidateRecoverable terminal runs in set_run_state (#9069)
set_run_state's finished_at condition hand-listed 5 of the 6 terminal run states, omitting CandidateRecoverable. A run that finished with a recoverable candidate goes through set_run_state (via run_state_for_aggregate in failure_recording) but never got its lifecycle.execution.finished_at stamped -- even though it is terminal and the legacy-record migration path in health.rs stamps finished_at for every non-Queued/Running state. The two write paths disagreed: the exact drift the just-merged is_terminal() consolidation (#9068) made visible. Replace the hand-listed subset with state.is_terminal() so the live setter matches the canonical terminal definition and the migration path. This removes the last hand-copied terminal-state list. Regression test asserts a CandidateRecoverable run stamps finished_at and a Running run does not; verified it FAILS on the pre-fix setter (panics with 'a terminal CandidateRecoverable run must stamp finished_at') and passes with the fix. Verified: agents --lib clean; new test passes with the fix, fails without it.
1 parent 4512a77 commit 2abeaa0

2 files changed

Lines changed: 42 additions & 8 deletions

File tree

crates/homeboy-agents/src/agent_task_lifecycle/lifecycle_record_ops.rs

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -27,14 +27,14 @@ pub(crate) fn set_run_state(record: &mut AgentTaskRunRecord, state: AgentTaskRun
2727
if state == AgentTaskRunState::Running && record.lifecycle.execution.started_at.is_none() {
2828
record.lifecycle.execution.started_at = Some(timestamp.clone());
2929
}
30-
if matches!(
31-
state,
32-
AgentTaskRunState::Succeeded
33-
| AgentTaskRunState::PartialRecoverable
34-
| AgentTaskRunState::PartialFailure
35-
| AgentTaskRunState::Failed
36-
| AgentTaskRunState::Cancelled
37-
) {
30+
// A terminal run has finished executing, so stamp `finished_at`. Use the
31+
// canonical terminal set (`is_terminal`) rather than a hand-listed subset:
32+
// the previous inline list omitted `CandidateRecoverable`, so a run that
33+
// finished with a recoverable candidate never got a `finished_at` here —
34+
// while the legacy-record migration path (`health.rs`) stamps it for every
35+
// non-Queued/Running state. This aligns the live setter with that path and
36+
// with the single terminal definition.
37+
if state.is_terminal() {
3838
record.lifecycle.execution.finished_at = Some(timestamp.clone());
3939
}
4040
record.lifecycle.updated_at = Some(timestamp);

crates/homeboy-agents/src/agent_task_lifecycle/tests/status_and_recovery.rs

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1311,3 +1311,37 @@ fn logs_include_normalized_event_envelopes() {
13111311
assert_eq!(log.normalized_events[1].artifact_refs.len(), 1);
13121312
});
13131313
}
1314+
1315+
#[test]
1316+
fn set_run_state_stamps_finished_at_for_candidate_recoverable_terminal_runs() {
1317+
// A run that finished with a recoverable candidate is terminal, so
1318+
// set_run_state must stamp finished_at for it exactly as it does for the
1319+
// other terminal states. Regression guard for the drift where the setter's
1320+
// hand-listed terminal subset omitted CandidateRecoverable, leaving these
1321+
// runs without a finished_at while the legacy-record migration path stamped
1322+
// one.
1323+
with_isolated_home(|_| {
1324+
let record =
1325+
submit_plan(&test_plan(), Some("candidate-recoverable-finished-at")).expect("submit");
1326+
rewrite_record_for_test(&record.run_id, |record| {
1327+
set_run_state(record, AgentTaskRunState::CandidateRecoverable);
1328+
assert_eq!(record.state, AgentTaskRunState::CandidateRecoverable);
1329+
assert!(
1330+
record.lifecycle.execution.finished_at.is_some(),
1331+
"a terminal CandidateRecoverable run must stamp finished_at"
1332+
);
1333+
})
1334+
.expect("rewrite record");
1335+
1336+
// And a non-terminal state must NOT stamp finished_at.
1337+
rewrite_record_for_test(&record.run_id, |record| {
1338+
record.lifecycle.execution.finished_at = None;
1339+
set_run_state(record, AgentTaskRunState::Running);
1340+
assert!(
1341+
record.lifecycle.execution.finished_at.is_none(),
1342+
"a non-terminal Running run must not stamp finished_at"
1343+
);
1344+
})
1345+
.expect("rewrite record running");
1346+
});
1347+
}

0 commit comments

Comments
 (0)