Fix child agent startup routing and terminal failure status

This commit is contained in:
2026-09-18 16:30:34 -05:00
parent a29ee94d84
commit 2790407ad5
4 changed files with 134 additions and 7 deletions
+4 -2
View File
@@ -3445,7 +3445,7 @@ impl AIConversation {
}; };
let current_comment_state = self.code_review.as_ref().cloned(); let current_comment_state = self.code_review.as_ref().cloned();
task.add_messages( let add_result = task.add_messages(
messages, messages,
exchange_id, exchange_id,
TaskMessageContext { TaskMessageContext {
@@ -3459,9 +3459,11 @@ impl AIConversation {
// to mimic the normal conversation flow. (If this is not a shared session, the // to mimic the normal conversation flow. (If this is not a shared session, the
// exchange inputs will already be populated). // exchange inputs will already be populated).
self.is_viewing_shared_session, self.is_viewing_shared_session,
)?; );
// Keep the task available for failure reporting even if message projection fails.
self.task_store.insert(task); self.task_store.insert(task);
add_result?;
if !added_exchanges if !added_exchanges
.iter() .iter()
.any(|new_exchange_info| new_exchange_info.exchange_id == exchange_id) .any(|new_exchange_info| new_exchange_info.exchange_id == exchange_id)
+51 -4
View File
@@ -3206,6 +3206,27 @@ impl BlocklistAIController {
); );
} }
/// Starts a newly created child on its root task. Shell initialization may still be
/// finishing when the bootstrap event arrives; it is not a command to monitor.
pub(crate) fn send_initial_child_agent_query(
&mut self,
query: String,
conversation_id: AIConversationId,
ctx: &mut ModelContext<Self>,
) {
self.send_user_query_in_conversation_internal(
query,
conversation_id,
None,
RunningCommandDetection::Skip,
HashMap::new(),
EntrypointType::AgentInitiated,
false,
None,
ctx,
);
}
/// Sends one non-preemptive final assessment to the root task after a CLI monitor completes. /// Sends one non-preemptive final assessment to the root task after a CLI monitor completes.
/// ///
/// This deliberately bypasses `send_query`: command completion must not cancel /// This deliberately bypasses `send_query`: command completion must not cancel
@@ -8339,8 +8360,11 @@ impl BlocklistAIController {
}); });
} }
}, },
// Failed outcomes are finalized by the projected InternalError event. ProviderRunOutcome::Failed(failure) => {
ProviderRunOutcome::Failed(_) => {} // Projection itself can fail before an exchange can display InternalError.
// The durable outcome must still release parents waiting for this child.
self.finalize_failed_provider_conversation(conversation_id, failure.message, ctx);
}
ProviderRunOutcome::Cancelled { .. } => { ProviderRunOutcome::Cancelled { .. } => {
let cancellation_reason = let cancellation_reason =
self.active_provider_runs[&conversation_id].cancellation_reason; self.active_provider_runs[&conversation_id].cancellation_reason;
@@ -8373,6 +8397,28 @@ impl BlocklistAIController {
self.cleanup_active_provider_run(conversation_id, &stream_id, &response_stream, ctx); self.cleanup_active_provider_run(conversation_id, &stream_id, &response_stream, ctx);
} }
fn finalize_failed_provider_conversation(
&self,
conversation_id: AIConversationId,
message: String,
ctx: &mut ModelContext<Self>,
) {
BlocklistAIHistoryModel::handle(ctx).update(ctx, |history_model, ctx| {
if history_model
.conversation_status(&conversation_id)
.is_some_and(|status| status != &ConversationStatus::Error)
{
history_model.update_conversation_status_with_error(
self.terminal_surface_id,
conversation_id,
ConversationStatus::Error,
Some(RenderableAIError::other(message, false)),
ctx,
);
}
});
}
fn fail_provider_startup( fn fail_provider_startup(
&mut self, &mut self,
conversation_id: AIConversationId, conversation_id: AIConversationId,
@@ -8391,7 +8437,7 @@ impl BlocklistAIController {
BlocklistAIHistoryModel::handle(ctx).update(ctx, |history_model, ctx| { BlocklistAIHistoryModel::handle(ctx).update(ctx, |history_model, ctx| {
history_model.mark_response_stream_completed_with_error( history_model.mark_response_stream_completed_with_error(
RenderableAIError::Other { RenderableAIError::Other {
error_message: message, error_message: message.clone(),
will_attempt_resume: false, will_attempt_resume: false,
waiting_for_network: false, waiting_for_network: false,
is_user_error: false, is_user_error: false,
@@ -8403,6 +8449,7 @@ impl BlocklistAIController {
ctx, ctx,
); );
}); });
self.finalize_failed_provider_conversation(conversation_id, message, ctx);
self.cleanup_active_provider_run(conversation_id, &stream_id, &response_stream, ctx); self.cleanup_active_provider_run(conversation_id, &stream_id, &response_stream, ctx);
} }
@@ -10361,7 +10408,7 @@ fn get_running_command_for_conversation(
conversation_id: AIConversationId, conversation_id: AIConversationId,
) -> Option<RunningCommand> { ) -> Option<RunningCommand> {
let active_block = terminal_model.block_list().active_block(); let active_block = terminal_model.block_list().active_block();
if !active_block.is_active_and_long_running() { if !active_block.bootstrap_stage().is_done() || !active_block.is_active_and_long_running() {
return None; return None;
} }
if active_block.is_agent_monitoring() if active_block.is_agent_monitoring()
+78
View File
@@ -35,6 +35,8 @@ use crate::ai::remote_logging::RemoteLogLevel;
use crate::ai::runtime::ProviderRunProjection; use crate::ai::runtime::ProviderRunProjection;
use crate::persistence::model::{AcpConversationData, AgentBackend}; use crate::persistence::model::{AcpConversationData, AgentBackend};
use crate::terminal::model::block::{BlockId, BlockState}; use crate::terminal::model::block::{BlockId, BlockState};
use crate::terminal::model::bootstrap::BootstrapStage;
use crate::terminal::model::test_utils::TestBlockBuilder;
use crate::test_util::settings::initialize_history_persistence_for_tests; use crate::test_util::settings::initialize_history_persistence_for_tests;
use crate::test_util::terminal::{add_window_with_terminal, initialize_app_for_terminal_view}; use crate::test_util::terminal::{add_window_with_terminal, initialize_app_for_terminal_view};
@@ -3812,3 +3814,79 @@ fn progressive_summary_keeps_retained_tool_results_paired() {
let budget = super::estimated_history_tokens(&messages[2..]); let budget = super::estimated_history_tokens(&messages[2..]);
assert_eq!(super::progressive_summary_split_point(&messages, budget), 1); assert_eq!(super::progressive_summary_split_point(&messages, budget), 1);
} }
#[test]
fn shell_bootstrap_is_never_a_command_monitor_target() {
App::test((), |mut app| async move {
initialize_app_for_terminal_view(&mut app);
let terminal = add_window_with_terminal(&mut app, None);
terminal.update(&mut app, |terminal, _ctx| {
let mut model = terminal.model.lock();
let conversation_id = AIConversationId::new();
for stage in [
BootstrapStage::WarpInput,
BootstrapStage::ScriptExecution,
BootstrapStage::Bootstrapped,
BootstrapStage::PostBootstrapPrecmd,
] {
*model.block_list_mut().active_block_mut() =
TestBlockBuilder::new().with_bootstrap_stage(stage).build();
model.simulate_long_running_block("sleep 100", "shell startup output");
assert!(model
.block_list()
.active_block()
.is_active_and_long_running());
assert_eq!(
super::get_running_command_for_conversation(&model, conversation_id).is_some(),
stage == BootstrapStage::PostBootstrapPrecmd,
"bootstrap stage {stage:?}",
);
}
});
});
}
#[test]
fn failed_child_without_an_exchange_reaches_terminal_error() {
App::test((), |mut app| async move {
initialize_app_for_terminal_view(&mut app);
let terminal = add_window_with_terminal(&mut app, None);
terminal.update(&mut app, |terminal, ctx| {
let terminal_id = terminal.id();
let children = BlocklistAIHistoryModel::handle(ctx).update(ctx, |history, ctx| {
let parent = history.start_new_conversation(terminal_id, false, false, false, ctx);
["stakeholder", "architect", "developer"].map(|name| {
history.start_new_child_conversation(
terminal_id,
name.to_owned(),
parent,
None,
ctx,
)
})
});
terminal.ai_controller().update(ctx, |controller, ctx| {
for child in [children[0], children[2]] {
controller.finalize_failed_provider_conversation(
child,
"projection failed: ExchangeNotFound".to_owned(),
ctx,
);
}
});
let history = BlocklistAIHistoryModel::as_ref(ctx);
for child in [children[0], children[2]] {
let conversation = history.conversation(&child).unwrap();
assert_eq!(conversation.status(), &ConversationStatus::Error);
assert!(conversation
.status_error_message()
.unwrap()
.contains("ExchangeNotFound"));
}
assert_eq!(
history.conversation(&children[1]).unwrap().status(),
&ConversationStatus::InProgress
);
});
});
}
+1 -1
View File
@@ -16279,7 +16279,7 @@ impl TerminalView {
return; return;
} }
self.ai_controller.update(ctx, |controller, ctx| { self.ai_controller.update(ctx, |controller, ctx| {
controller.send_agent_query_in_conversation(prompt, conversation_id, ctx); controller.send_initial_child_agent_query(prompt, conversation_id, ctx);
}); });
} }
pub fn has_pending_command_or_awaiting_completion(&self, ctx: &AppContext) -> bool { pub fn has_pending_command_or_awaiting_completion(&self, ctx: &AppContext) -> bool {