From 96160177569ad931949fae479d4685c4f011494a Mon Sep 17 00:00:00 2001 From: seemeroland Date: Wed, 15 Jul 2026 12:23:39 -0700 Subject: [PATCH 1/7] Handle StopFailure hook from claude code --- .../agent_management_model.rs | 22 +++++++++++++++++++ app/src/ai/agent_sdk/driver.rs | 4 +++- .../agent_sdk/driver/harness/claude_code.rs | 1 + .../driver/harness_output_monitor.rs | 5 ++++- .../blocklist/local_agent_task_sync_model.rs | 4 ++++ .../terminal/cli_agent_sessions/event/mod.rs | 1 + .../terminal/cli_agent_sessions/event/v1.rs | 1 + app/src/terminal/cli_agent_sessions/mod.rs | 8 +++++++ app/src/terminal/view.rs | 6 ++++- 9 files changed, 49 insertions(+), 3 deletions(-) diff --git a/app/src/ai/agent_management/agent_management_model.rs b/app/src/ai/agent_management/agent_management_model.rs index 4d17d972675..3ff8be28bb2 100644 --- a/app/src/ai/agent_management/agent_management_model.rs +++ b/app/src/ai/agent_management/agent_management_model.rs @@ -177,6 +177,28 @@ impl AgentNotificationsModel { ctx, ); } + CLIAgentSessionStatus::Failed { message } => { + let title = session_context + .display_title() + .unwrap_or_else(|| format!("{} failed", agent.display_name())); + let metadata = TerminalViewMetadata::lookup(*terminal_view_id, ctx); + self.add_notification( + title, + message + .clone() + .unwrap_or_else(|| "The agent encountered an error.".to_owned()), + NotificationCategory::Error, + NotificationSourceAgent::CLI { + agent: *agent, + is_ambient: metadata.is_ambient, + }, + NotificationOrigin::CLISession(*terminal_view_id), + *terminal_view_id, + vec![], + metadata.branch, + ctx, + ); + } CLIAgentSessionStatus::Blocked { message } => { let title = session_context .display_title() diff --git a/app/src/ai/agent_sdk/driver.rs b/app/src/ai/agent_sdk/driver.rs index 472b5f57728..942c51460ce 100644 --- a/app/src/ai/agent_sdk/driver.rs +++ b/app/src/ai/agent_sdk/driver.rs @@ -3435,7 +3435,9 @@ impl AgentDriver { // Drive idle-on-complete timer for the harness exit signal. match status { - CLIAgentSessionStatus::Success | CLIAgentSessionStatus::Blocked { .. } => { + CLIAgentSessionStatus::Success + | CLIAgentSessionStatus::Failed { .. } + | CLIAgentSessionStatus::Blocked { .. } => { if let Some(idle_timeout) = me.idle_on_complete { log::info!( "Ambient agent CLI lifecycle: event=idle_timeout_scheduled task_id={:?} terminal_view_id={terminal_view_id:?} timeout={idle_timeout:?}", diff --git a/app/src/ai/agent_sdk/driver/harness/claude_code.rs b/app/src/ai/agent_sdk/driver/harness/claude_code.rs index 3e421efc722..c7ea08bc7cf 100644 --- a/app/src/ai/agent_sdk/driver/harness/claude_code.rs +++ b/app/src/ai/agent_sdk/driver/harness/claude_code.rs @@ -416,6 +416,7 @@ impl ClaudeHarnessRunner { cli_agent_session_status(&self.terminal_driver, foreground).await, Some(crate::terminal::cli_agent_sessions::CLIAgentSessionStatus::Blocked { .. }) | Some(crate::terminal::cli_agent_sessions::CLIAgentSessionStatus::InProgress) + | Some(crate::terminal::cli_agent_sessions::CLIAgentSessionStatus::Failed { .. }) ) } diff --git a/app/src/ai/agent_sdk/driver/harness_output_monitor.rs b/app/src/ai/agent_sdk/driver/harness_output_monitor.rs index 0d92a150a88..e7f309c86dd 100644 --- a/app/src/ai/agent_sdk/driver/harness_output_monitor.rs +++ b/app/src/ai/agent_sdk/driver/harness_output_monitor.rs @@ -197,7 +197,10 @@ pub(crate) async fn watch_block_for_errors( } pub(crate) fn should_suppress_runtime_failure(status: Option<&CLIAgentSessionStatus>) -> bool { - matches!(status, Some(CLIAgentSessionStatus::Success)) + matches!( + status, + Some(CLIAgentSessionStatus::Success) | Some(CLIAgentSessionStatus::Failed { .. }) + ) } /// Cap excerpt length so we don't blow up status messages or logs with diff --git a/app/src/ai/blocklist/local_agent_task_sync_model.rs b/app/src/ai/blocklist/local_agent_task_sync_model.rs index 5a4a4adefa6..2a65037a4e9 100644 --- a/app/src/ai/blocklist/local_agent_task_sync_model.rs +++ b/app/src/ai/blocklist/local_agent_task_sync_model.rs @@ -548,6 +548,10 @@ fn map_cli_session_status( match status { CLIAgentSessionStatus::InProgress => (AgentTaskState::InProgress, None), CLIAgentSessionStatus::Success => (AgentTaskState::Succeeded, None), + CLIAgentSessionStatus::Failed { message } => ( + AgentTaskState::Failed, + message.as_ref().map(TaskStatusUpdate::message), + ), CLIAgentSessionStatus::Blocked { message } => ( AgentTaskState::Blocked, message.as_ref().map(TaskStatusUpdate::message), diff --git a/app/src/terminal/cli_agent_sessions/event/mod.rs b/app/src/terminal/cli_agent_sessions/event/mod.rs index 18e96dee42b..3efbe89ed79 100644 --- a/app/src/terminal/cli_agent_sessions/event/mod.rs +++ b/app/src/terminal/cli_agent_sessions/event/mod.rs @@ -19,6 +19,7 @@ pub enum CLIAgentEventType { PromptSubmit, ToolComplete, Stop, + StopFailure, PermissionRequest, PermissionReplied, QuestionAsked, diff --git a/app/src/terminal/cli_agent_sessions/event/v1.rs b/app/src/terminal/cli_agent_sessions/event/v1.rs index ae7077b4918..1fcd1b1c182 100644 --- a/app/src/terminal/cli_agent_sessions/event/v1.rs +++ b/app/src/terminal/cli_agent_sessions/event/v1.rs @@ -18,6 +18,7 @@ pub(super) fn parse(body: &str) -> Option { "prompt_submit" => CLIAgentEventType::PromptSubmit, "tool_complete" => CLIAgentEventType::ToolComplete, "stop" => CLIAgentEventType::Stop, + "stop_failure" => CLIAgentEventType::StopFailure, "permission_request" => CLIAgentEventType::PermissionRequest, "permission_replied" => CLIAgentEventType::PermissionReplied, "question_asked" => CLIAgentEventType::QuestionAsked, diff --git a/app/src/terminal/cli_agent_sessions/mod.rs b/app/src/terminal/cli_agent_sessions/mod.rs index b7b0de8da21..b4d500f3a8b 100644 --- a/app/src/terminal/cli_agent_sessions/mod.rs +++ b/app/src/terminal/cli_agent_sessions/mod.rs @@ -17,6 +17,7 @@ use crate::ai::blocklist::InputConfig; pub enum CLIAgentSessionStatus { InProgress, Success, + Failed { message: Option }, Blocked { message: Option }, } @@ -26,6 +27,7 @@ impl CLIAgentSessionStatus { match self { CLIAgentSessionStatus::InProgress => ConversationStatus::InProgress, CLIAgentSessionStatus::Success => ConversationStatus::Success, + CLIAgentSessionStatus::Failed { .. } => ConversationStatus::Error, CLIAgentSessionStatus::Blocked { message } => ConversationStatus::Blocked { blocked_action: message.clone().unwrap_or_default(), }, @@ -206,6 +208,12 @@ impl CLIAgentSession { self.clear_permission_scoped_state(); CLIAgentSessionStatus::Success } + CLIAgentEventType::StopFailure => { + self.clear_permission_scoped_state(); + CLIAgentSessionStatus::Failed { + message: event.payload.response.clone(), + } + } CLIAgentEventType::PermissionRequest => { self.session_context.summary = event.payload.summary.clone(); self.session_context.tool_name = event.payload.tool_name.clone(); diff --git a/app/src/terminal/view.rs b/app/src/terminal/view.rs index b366240311b..66870367983 100644 --- a/app/src/terminal/view.rs +++ b/app/src/terminal/view.rs @@ -13479,7 +13479,9 @@ impl TerminalView { ctx, ); } - CLIAgentSessionStatus::InProgress | CLIAgentSessionStatus::Success => { + CLIAgentSessionStatus::InProgress + | CLIAgentSessionStatus::Success + | CLIAgentSessionStatus::Failed { .. } => { // Auto-open rich input when the agent resumes or completes. if !self.has_active_cli_agent_input_session(ctx) { self.open_cli_agent_rich_input(CLIAgentInputEntrypoint::AutoShow, ctx); @@ -13511,6 +13513,8 @@ impl TerminalView { let trigger = if matches!(status, CLIAgentSessionStatus::Blocked { .. }) { NotificationsTrigger::NeedsAttention + } else if matches!(status, CLIAgentSessionStatus::Failed { .. }) { + NotificationsTrigger::AgentTaskCompleted(false) } else { NotificationsTrigger::AgentTaskCompleted(true) }; From 0341b31693e07680713b9f1d0898961fce017085 Mon Sep 17 00:00:00 2001 From: seemeroland Date: Wed, 15 Jul 2026 12:38:45 -0700 Subject: [PATCH 2/7] Handle error type --- .../ai/agent_management/agent_management_model.rs | 12 ++++++++---- app/src/ai/agent_sdk/driver.rs | 3 ++- app/src/ai/blocklist/local_agent_task_sync_model.rs | 2 +- app/src/terminal/cli_agent_sessions/event/mod.rs | 3 +++ app/src/terminal/cli_agent_sessions/event/v1.rs | 2 ++ app/src/terminal/cli_agent_sessions/mod.rs | 6 +++++- 6 files changed, 21 insertions(+), 7 deletions(-) diff --git a/app/src/ai/agent_management/agent_management_model.rs b/app/src/ai/agent_management/agent_management_model.rs index 3ff8be28bb2..ede907e172e 100644 --- a/app/src/ai/agent_management/agent_management_model.rs +++ b/app/src/ai/agent_management/agent_management_model.rs @@ -177,16 +177,20 @@ impl AgentNotificationsModel { ctx, ); } - CLIAgentSessionStatus::Failed { message } => { + CLIAgentSessionStatus::Failed { error_type, message } => { let title = session_context .display_title() .unwrap_or_else(|| format!("{} failed", agent.display_name())); + let body = match (message.as_deref(), error_type.as_deref()) { + (Some(msg), Some(kind)) => format!("{kind}: {msg}"), + (Some(msg), None) => msg.to_owned(), + (None, Some(kind)) => kind.to_owned(), + (None, None) => "The agent encountered an error.".to_owned(), + }; let metadata = TerminalViewMetadata::lookup(*terminal_view_id, ctx); self.add_notification( title, - message - .clone() - .unwrap_or_else(|| "The agent encountered an error.".to_owned()), + body, NotificationCategory::Error, NotificationSourceAgent::CLI { agent: *agent, diff --git a/app/src/ai/agent_sdk/driver.rs b/app/src/ai/agent_sdk/driver.rs index 942c51460ce..abc392f0744 100644 --- a/app/src/ai/agent_sdk/driver.rs +++ b/app/src/ai/agent_sdk/driver.rs @@ -2853,7 +2853,8 @@ impl AgentDriver { ) { log::info!( "Ignoring runtime failure for {harness_name}: \ - session already marked Success (pattern={}, excerpt={})", + session already marked Success or Failed via plugin \ + (pattern={}, excerpt={})", error.pattern, error.excerpt, ); diff --git a/app/src/ai/blocklist/local_agent_task_sync_model.rs b/app/src/ai/blocklist/local_agent_task_sync_model.rs index 2a65037a4e9..19b28d4c3d4 100644 --- a/app/src/ai/blocklist/local_agent_task_sync_model.rs +++ b/app/src/ai/blocklist/local_agent_task_sync_model.rs @@ -548,7 +548,7 @@ fn map_cli_session_status( match status { CLIAgentSessionStatus::InProgress => (AgentTaskState::InProgress, None), CLIAgentSessionStatus::Success => (AgentTaskState::Succeeded, None), - CLIAgentSessionStatus::Failed { message } => ( + CLIAgentSessionStatus::Failed { message, .. } => ( AgentTaskState::Failed, message.as_ref().map(TaskStatusUpdate::message), ), diff --git a/app/src/terminal/cli_agent_sessions/event/mod.rs b/app/src/terminal/cli_agent_sessions/event/mod.rs index 3efbe89ed79..142c44c2288 100644 --- a/app/src/terminal/cli_agent_sessions/event/mod.rs +++ b/app/src/terminal/cli_agent_sessions/event/mod.rs @@ -47,6 +47,9 @@ pub struct CLIAgentEventPayload { pub tool_name: Option, pub tool_input_preview: Option, pub plugin_version: Option, + /// On Claude Code, this comes from the `StopFailure` hook (e.g. `"rate_limit"`). + /// Not implemented for Codex. + pub error_type: Option, } /// A parsed event from a CLI agent plugin. diff --git a/app/src/terminal/cli_agent_sessions/event/v1.rs b/app/src/terminal/cli_agent_sessions/event/v1.rs index 1fcd1b1c182..c699cfca814 100644 --- a/app/src/terminal/cli_agent_sessions/event/v1.rs +++ b/app/src/terminal/cli_agent_sessions/event/v1.rs @@ -54,6 +54,7 @@ pub(super) fn parse(body: &str) -> Option { tool_name: raw.tool_name, tool_input_preview, plugin_version: raw.plugin_version, + error_type: raw.error_type, }, source: CLIAgentEventSource::RichPlugin, }) @@ -74,4 +75,5 @@ struct RawEvent { tool_name: Option, tool_input: Option, plugin_version: Option, + error_type: Option, } diff --git a/app/src/terminal/cli_agent_sessions/mod.rs b/app/src/terminal/cli_agent_sessions/mod.rs index b4d500f3a8b..0dbc9308728 100644 --- a/app/src/terminal/cli_agent_sessions/mod.rs +++ b/app/src/terminal/cli_agent_sessions/mod.rs @@ -17,7 +17,10 @@ use crate::ai::blocklist::InputConfig; pub enum CLIAgentSessionStatus { InProgress, Success, - Failed { message: Option }, + Failed { + error_type: Option, + message: Option, + }, Blocked { message: Option }, } @@ -211,6 +214,7 @@ impl CLIAgentSession { CLIAgentEventType::StopFailure => { self.clear_permission_scoped_state(); CLIAgentSessionStatus::Failed { + error_type: event.payload.error_type.clone(), message: event.payload.response.clone(), } } From 3f0c160a04c5da081e86b7be9e55eb22334b8028 Mon Sep 17 00:00:00 2001 From: seemeroland Date: Wed, 15 Jul 2026 12:44:51 -0700 Subject: [PATCH 3/7] Update status --- .../ai/blocklist/local_agent_task_sync_model.rs | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/app/src/ai/blocklist/local_agent_task_sync_model.rs b/app/src/ai/blocklist/local_agent_task_sync_model.rs index 19b28d4c3d4..8bcc6a25094 100644 --- a/app/src/ai/blocklist/local_agent_task_sync_model.rs +++ b/app/src/ai/blocklist/local_agent_task_sync_model.rs @@ -548,10 +548,19 @@ fn map_cli_session_status( match status { CLIAgentSessionStatus::InProgress => (AgentTaskState::InProgress, None), CLIAgentSessionStatus::Success => (AgentTaskState::Succeeded, None), - CLIAgentSessionStatus::Failed { message, .. } => ( - AgentTaskState::Failed, - message.as_ref().map(TaskStatusUpdate::message), - ), + CLIAgentSessionStatus::Failed { error_type, message } => { + // User-actionable errors (bad credentials, org restrictions, billing) map to + // FAILED. Everything else (rate limits, server errors, model errors, etc.) + // maps to ERROR since they are typically Anthropic's or Warp's fault. + // The list of error types on Claude Code comes from https://code.claude.com/docs/en/hooks#stopfailure-input + let task_state = match error_type.as_deref() { + Some( + "authentication_failed" | "oauth_org_not_allowed" | "billing_error", + ) => AgentTaskState::Failed, + _ => AgentTaskState::Error, + }; + (task_state, message.as_ref().map(TaskStatusUpdate::message)) + } CLIAgentSessionStatus::Blocked { message } => ( AgentTaskState::Blocked, message.as_ref().map(TaskStatusUpdate::message), From d69c0462ba94590a9b172aa581f58473f8590968 Mon Sep 17 00:00:00 2001 From: seemeroland Date: Wed, 15 Jul 2026 12:47:36 -0700 Subject: [PATCH 4/7] clippy fmt --- app/src/ai/agent_management/agent_management_model.rs | 5 ++++- app/src/ai/blocklist/local_agent_task_sync_model.rs | 11 +++++++---- app/src/terminal/cli_agent_sessions/mod.rs | 4 +++- 3 files changed, 14 insertions(+), 6 deletions(-) diff --git a/app/src/ai/agent_management/agent_management_model.rs b/app/src/ai/agent_management/agent_management_model.rs index ede907e172e..f94da9a3993 100644 --- a/app/src/ai/agent_management/agent_management_model.rs +++ b/app/src/ai/agent_management/agent_management_model.rs @@ -177,7 +177,10 @@ impl AgentNotificationsModel { ctx, ); } - CLIAgentSessionStatus::Failed { error_type, message } => { + CLIAgentSessionStatus::Failed { + error_type, + message, + } => { let title = session_context .display_title() .unwrap_or_else(|| format!("{} failed", agent.display_name())); diff --git a/app/src/ai/blocklist/local_agent_task_sync_model.rs b/app/src/ai/blocklist/local_agent_task_sync_model.rs index 8bcc6a25094..b139bfae038 100644 --- a/app/src/ai/blocklist/local_agent_task_sync_model.rs +++ b/app/src/ai/blocklist/local_agent_task_sync_model.rs @@ -548,15 +548,18 @@ fn map_cli_session_status( match status { CLIAgentSessionStatus::InProgress => (AgentTaskState::InProgress, None), CLIAgentSessionStatus::Success => (AgentTaskState::Succeeded, None), - CLIAgentSessionStatus::Failed { error_type, message } => { + CLIAgentSessionStatus::Failed { + error_type, + message, + } => { // User-actionable errors (bad credentials, org restrictions, billing) map to // FAILED. Everything else (rate limits, server errors, model errors, etc.) // maps to ERROR since they are typically Anthropic's or Warp's fault. // The list of error types on Claude Code comes from https://code.claude.com/docs/en/hooks#stopfailure-input let task_state = match error_type.as_deref() { - Some( - "authentication_failed" | "oauth_org_not_allowed" | "billing_error", - ) => AgentTaskState::Failed, + Some("authentication_failed" | "oauth_org_not_allowed" | "billing_error") => { + AgentTaskState::Failed + } _ => AgentTaskState::Error, }; (task_state, message.as_ref().map(TaskStatusUpdate::message)) diff --git a/app/src/terminal/cli_agent_sessions/mod.rs b/app/src/terminal/cli_agent_sessions/mod.rs index 0dbc9308728..c7b8aa6f18e 100644 --- a/app/src/terminal/cli_agent_sessions/mod.rs +++ b/app/src/terminal/cli_agent_sessions/mod.rs @@ -21,7 +21,9 @@ pub enum CLIAgentSessionStatus { error_type: Option, message: Option, }, - Blocked { message: Option }, + Blocked { + message: Option, + }, } impl CLIAgentSessionStatus { From b404f5f1022746ba235385ceaad2a288a0c7b742 Mon Sep 17 00:00:00 2001 From: seemeroland Date: Wed, 15 Jul 2026 13:00:39 -0700 Subject: [PATCH 5/7] Match codex pattern --- app/src/ai/agent_sdk/driver/harness/codex.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/app/src/ai/agent_sdk/driver/harness/codex.rs b/app/src/ai/agent_sdk/driver/harness/codex.rs index 98004bef72d..136c3b20f0d 100644 --- a/app/src/ai/agent_sdk/driver/harness/codex.rs +++ b/app/src/ai/agent_sdk/driver/harness/codex.rs @@ -87,6 +87,9 @@ impl ThirdPartyHarness for CodexHarness { // OAuth refresh failures — all five Codex variants share this // substring (see upstream session/token messages). "could not be refreshed", + // Generically check for invalid request errors. + // Keep this last so more specific patterns can be matched first. + "\"type\": \"invalid_request_error\"", ] } From b6bc584cccac76fa877865f2923573c38003a979 Mon Sep 17 00:00:00 2001 From: seemeroland Date: Wed, 15 Jul 2026 13:20:17 -0700 Subject: [PATCH 6/7] add comment --- app/src/ai/agent_sdk/driver/harness_output_monitor.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/src/ai/agent_sdk/driver/harness_output_monitor.rs b/app/src/ai/agent_sdk/driver/harness_output_monitor.rs index e7f309c86dd..3afa0778741 100644 --- a/app/src/ai/agent_sdk/driver/harness_output_monitor.rs +++ b/app/src/ai/agent_sdk/driver/harness_output_monitor.rs @@ -197,6 +197,8 @@ pub(crate) async fn watch_block_for_errors( } pub(crate) fn should_suppress_runtime_failure(status: Option<&CLIAgentSessionStatus>) -> bool { + // On CLIAgentSessionStatus::Failed, we directly update the task status, + // so we don't need to do runtime pattern matching to find the failure. matches!( status, Some(CLIAgentSessionStatus::Success) | Some(CLIAgentSessionStatus::Failed { .. }) From d9e925470e40a7a437d5ccb5dbd4319eee7fb9c4 Mon Sep 17 00:00:00 2001 From: seemeroland Date: Wed, 15 Jul 2026 15:57:10 -0700 Subject: [PATCH 7/7] address comment --- app/src/terminal/cli_agent_sessions/mod.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/src/terminal/cli_agent_sessions/mod.rs b/app/src/terminal/cli_agent_sessions/mod.rs index c7b8aa6f18e..6344278287b 100644 --- a/app/src/terminal/cli_agent_sessions/mod.rs +++ b/app/src/terminal/cli_agent_sessions/mod.rs @@ -214,6 +214,8 @@ impl CLIAgentSession { CLIAgentSessionStatus::Success } CLIAgentEventType::StopFailure => { + self.session_context.query = event.payload.query.clone(); + self.session_context.response = event.payload.response.clone(); self.clear_permission_scoped_state(); CLIAgentSessionStatus::Failed { error_type: event.payload.error_type.clone(),