Prevent crosscheck races with pending AI work
This commit is contained in:
@@ -1329,8 +1329,20 @@ impl BlocklistAIActionModel {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// The phase is fully drained — sort results back into original tool-call order before
|
// A serial phase may have more queued actions. Start those before notifying the controller;
|
||||||
// notifying the controller that it may send the follow-up request.
|
// otherwise the controller can drain this phase's result while the next action is still
|
||||||
|
// pending, and that later tool result will lose its follow-up trigger.
|
||||||
|
if self
|
||||||
|
.pending_actions
|
||||||
|
.get(&conversation_id)
|
||||||
|
.is_some_and(|actions| !actions.is_empty())
|
||||||
|
{
|
||||||
|
self.try_to_execute_available_actions(conversation_id, ctx);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The entire action batch is fully drained — sort results back into original tool-call
|
||||||
|
// order before notifying the controller that it may send one complete follow-up request.
|
||||||
self.sort_finished_results(conversation_id);
|
self.sort_finished_results(conversation_id);
|
||||||
ctx.emit(BlocklistAIActionEvent::FinishedAction {
|
ctx.emit(BlocklistAIActionEvent::FinishedAction {
|
||||||
action_id,
|
action_id,
|
||||||
@@ -1374,8 +1386,6 @@ impl BlocklistAIActionModel {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
self.try_to_execute_available_actions(conversation_id, ctx);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2522,13 +2522,42 @@ impl BlocklistAIController {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Don't trigger crosscheck for child conversations
|
// Crosscheck is a terminal-turn activity. Do not start it until every source of work for
|
||||||
|
// this conversation has drained, or its feedback can race a tool result or another stream.
|
||||||
|
if self
|
||||||
|
.in_flight_response_streams
|
||||||
|
.has_active_stream_for_conversation(conversation_id, ctx)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let action_model = self.action_model.as_ref(ctx);
|
||||||
|
if action_model.has_unfinished_actions_for_conversation(conversation_id)
|
||||||
|
|| action_model
|
||||||
|
.get_finished_action_results(conversation_id)
|
||||||
|
.is_some_and(|results| !results.is_empty())
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if self
|
||||||
|
.crosscheck_reviewer
|
||||||
|
.as_ref(ctx)
|
||||||
|
.is_reviewing(conversation_id)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Don't trigger crosscheck for child conversations or a conversation that is no longer
|
||||||
|
// running under this controller.
|
||||||
let Some(conversation) =
|
let Some(conversation) =
|
||||||
BlocklistAIHistoryModel::as_ref(ctx).conversation(&conversation_id)
|
BlocklistAIHistoryModel::as_ref(ctx).conversation(&conversation_id)
|
||||||
else {
|
else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
if conversation.parent_conversation_id().is_some() {
|
if conversation.parent_conversation_id().is_some()
|
||||||
|
|| !conversation.status().is_in_progress()
|
||||||
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user