fix: clear abandoned run budget before compaction
This commit is contained in:
+73
-19
@@ -785,6 +785,17 @@ struct EmptyTurnRollbackSnapshot {
|
|||||||
flow_runtime_state: Option<flow::FlowRuntimeState>,
|
flow_runtime_state: Option<flow::FlowRuntimeState>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn active_run_checkpoint_entry(
|
||||||
|
active_run_turn_count: Option<usize>,
|
||||||
|
total_turn_count: usize,
|
||||||
|
) -> Option<LogEntry> {
|
||||||
|
active_run_turn_count.map(|active_turn_count| LogEntry::ActiveRunCheckpoint {
|
||||||
|
ts: segment_log::now_millis(),
|
||||||
|
active_turn_count,
|
||||||
|
total_turn_count,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
fn is_ai_materialized_item(item: &Item) -> bool {
|
fn is_ai_materialized_item(item: &Item) -> bool {
|
||||||
match item {
|
match item {
|
||||||
Item::Message { role, .. } => *role == Role::Assistant,
|
Item::Message { role, .. } => *role == Role::Assistant,
|
||||||
@@ -2547,9 +2558,7 @@ impl<C: LlmClient, St: Store> Worker<C, St> {
|
|||||||
// `last_run_interrupted` flag; `Worker::resume` reuses the prior
|
// `last_run_interrupted` flag; `Worker::resume` reuses the prior
|
||||||
// context via a different entry point and never triggers this
|
// context via a different entry point and never triggers this
|
||||||
// path.
|
// path.
|
||||||
if self.engine.as_ref().unwrap().last_run_interrupted() {
|
self.prepare_interrupted_history_for_fresh_run()?;
|
||||||
self.apply_interrupt_prep()?;
|
|
||||||
}
|
|
||||||
|
|
||||||
self.prepare_for_run().await?;
|
self.prepare_for_run().await?;
|
||||||
|
|
||||||
@@ -2660,6 +2669,19 @@ impl<C: LlmClient, St: Store> Worker<C, St> {
|
|||||||
out
|
out
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Close interrupted history before a fresh user/notification run.
|
||||||
|
///
|
||||||
|
/// Clearing the interrupted flag also ends the old logical-run budget and
|
||||||
|
/// must happen before `prepare_for_run`: proactive compaction checkpoints
|
||||||
|
/// only resumable runs, never the run this invocation is abandoning.
|
||||||
|
fn prepare_interrupted_history_for_fresh_run(&mut self) -> Result<(), WorkerError> {
|
||||||
|
if self.engine().last_run_interrupted() {
|
||||||
|
self.apply_interrupt_prep()?;
|
||||||
|
self.engine_mut().set_last_run_interrupted(false);
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
/// Stage the post-interruption cleanup at the front of worker
|
/// Stage the post-interruption cleanup at the front of worker
|
||||||
/// history: close every unanswered `Item::ToolCall` with a synthetic
|
/// history: close every unanswered `Item::ToolCall` with a synthetic
|
||||||
/// `Item::ToolResult` (Anthropic wire-validity), then append a
|
/// `Item::ToolResult` (Anthropic wire-validity), then append a
|
||||||
@@ -2775,16 +2797,7 @@ impl<C: LlmClient, St: Store> Worker<C, St> {
|
|||||||
),
|
),
|
||||||
"run_for_notification expects a non-UserSend InvokeKind; got {kind:?}"
|
"run_for_notification expects a non-UserSend InvokeKind; got {kind:?}"
|
||||||
);
|
);
|
||||||
// This is a fresh Invoke, not an explicit resume of the interrupted
|
self.prepare_interrupted_history_for_fresh_run()?;
|
||||||
// turn. Close any dangling tool calls before an auto-run notification
|
|
||||||
// can enter `Engine::resume` and execute them again after a crash.
|
|
||||||
if self.engine.as_ref().unwrap().last_run_interrupted() {
|
|
||||||
self.apply_interrupt_prep()?;
|
|
||||||
// Notification delivery begins a new logical run over the
|
|
||||||
// prepared history rather than inheriting the abandoned run's
|
|
||||||
// max-turn budget.
|
|
||||||
self.engine_mut().set_last_run_interrupted(false);
|
|
||||||
}
|
|
||||||
self.prepare_for_run().await?;
|
self.prepare_for_run().await?;
|
||||||
|
|
||||||
// IDLE → active marker for the buffered notification / worker-event
|
// IDLE → active marker for the buffered notification / worker-event
|
||||||
@@ -3766,12 +3779,10 @@ impl<C: LlmClient, St: Store> Worker<C, St> {
|
|||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
let mut initial_entries = vec![entry.clone()];
|
let mut initial_entries = vec![entry.clone()];
|
||||||
if let Some(active_turn_count) = w.active_run_turn_count() {
|
if let Some(checkpoint) =
|
||||||
initial_entries.push(LogEntry::ActiveRunCheckpoint {
|
active_run_checkpoint_entry(w.active_run_turn_count(), source_turn_count)
|
||||||
ts: segment_log::now_millis(),
|
{
|
||||||
active_turn_count,
|
initial_entries.push(checkpoint);
|
||||||
total_turn_count: source_turn_count,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
if let Some(flow_state) = self
|
if let Some(flow_state) = self
|
||||||
.flow_runtime_state
|
.flow_runtime_state
|
||||||
@@ -6977,6 +6988,49 @@ mod build_summary_prompt_tests {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn fresh_run_clears_interrupted_budget_before_pre_run_compaction() {
|
||||||
|
let dir = tempfile::tempdir().unwrap();
|
||||||
|
let store = session_store::FsStore::new(dir.path().join("sessions")).unwrap();
|
||||||
|
let mut worker = Worker::new(
|
||||||
|
minimal_manifest(),
|
||||||
|
Engine::new(NoopClient),
|
||||||
|
store,
|
||||||
|
WorkerWorkspaceContext::no_workspace(),
|
||||||
|
WorkerFilesystemAuthority::None,
|
||||||
|
Scope::empty(),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
worker.ensure_segment_head().unwrap();
|
||||||
|
worker.engine_mut().set_last_run_interrupted(true);
|
||||||
|
worker.engine_mut().set_active_run_turn_count(Some(3));
|
||||||
|
|
||||||
|
worker.prepare_interrupted_history_for_fresh_run().unwrap();
|
||||||
|
|
||||||
|
assert!(!worker.engine().last_run_interrupted());
|
||||||
|
assert_eq!(worker.engine().active_run_turn_count(), None);
|
||||||
|
let checkpoint = active_run_checkpoint_entry(
|
||||||
|
worker.engine().active_run_turn_count(),
|
||||||
|
worker.engine().turn_count(),
|
||||||
|
);
|
||||||
|
assert!(checkpoint.is_none());
|
||||||
|
|
||||||
|
let mut replacement_entries = vec![LogEntry::SegmentStart {
|
||||||
|
ts: segment_log::now_millis(),
|
||||||
|
session_id: uuid::Uuid::nil(),
|
||||||
|
system_prompt: None,
|
||||||
|
config: RequestConfig::default(),
|
||||||
|
history: vec![],
|
||||||
|
forked_from: None,
|
||||||
|
compacted_from: None,
|
||||||
|
}];
|
||||||
|
replacement_entries.extend(checkpoint);
|
||||||
|
let restored = session_store::collect_state(&replacement_entries);
|
||||||
|
assert!(!restored.last_run_interrupted);
|
||||||
|
assert_eq!(restored.active_run_turn_count, None);
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn flow_transition_feature_installs_runtime_local_coordinator() {
|
async fn flow_transition_feature_installs_runtime_local_coordinator() {
|
||||||
let dir = tempfile::tempdir().unwrap();
|
let dir = tempfile::tempdir().unwrap();
|
||||||
|
|||||||
Reference in New Issue
Block a user