Skip to content

Conversation

@lifeizhou-ap
Copy link
Collaborator

@lifeizhou-ap lifeizhou-ap commented Jul 21, 2025

Handle CLI CTRL+C gracefully to cancel the tasks

fixes issue #3553

@lifeizhou-ap lifeizhou-ap requested a review from Copilot July 21, 2025 07:26
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.

@lifeizhou-ap lifeizhou-ap requested a review from Copilot July 21, 2025 07:26

This comment was marked as outdated.

@lifeizhou-ap lifeizhou-ap requested a review from Copilot July 21, 2025 07:37
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

This PR fixes error message suppression when Ctrl+C is pressed during task execution. The changes implement proper cancellation handling to prevent spurious error messages that occur when channels are closed due to task cancellation rather than actual errors.

Key changes:

  • Added cancellation token support throughout the task execution pipeline
  • Implemented error message suppression logic when tasks are cancelled and channels are closed
  • Added cancellation handling in the CLI session to properly clean up subagent executions

Reviewed Changes

Copilot reviewed 8 out of 8 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
workers.rs Added conditional error logging to suppress "channel closed" errors when task is cancelled
tasks_manager.rs Added cancellation token tracking and management functionality
task_execution_tracker.rs Added cancellation state tracking and error suppression logic
subagent_execute_task_tool.rs Integrated cancellation token support into task execution flow
lib/mod.rs Updated function signatures to accept cancellation tokens
executor/mod.rs Added cancellation handling setup and tokio::select! for graceful cancellation
agent.rs Added public method to cancel all subagent executions
session/mod.rs Added Ctrl+C handler to cancel subagent executions

matches!(error, tokio::sync::mpsc::error::TrySendError::Closed(_))
}

pub fn should_suppress_error(
Copy link

Copilot AI Jul 21, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The should_suppress_error method is marked as public but appears to be an internal implementation detail. Consider making this method private or documenting its public API contract if it's intended for external use.

Suggested change
pub fn should_suppress_error(
fn should_suppress_error(

Copilot uses AI. Check for mistakes.

pub async fn register_execution(&self, cancellation_token: CancellationToken) {
let mut tokens = self.active_tokens.write().await;
tokens.retain(|token| !token.is_cancelled());
Copy link

Copilot AI Jul 21, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The cleanup of cancelled tokens happens on every registration. For high-frequency registrations, this could become inefficient. Consider implementing a periodic cleanup strategy or cleanup threshold to avoid O(n) operations on every registration.

Copilot uses AI. Check for mistakes.
Copy link
Collaborator Author

@lifeizhou-ap lifeizhou-ap Jul 21, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is not high-frequency registrations

if let Err(e) = state.result_sender.send(result).await {
tracing::error!("Worker failed to send result: {}", e);
let is_cancelled_channel_closed = state.task_execution_tracker.is_cancelled()
&& e.to_string().contains("channel closed");
Copy link

Copilot AI Jul 21, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using string matching on error messages is fragile and could break if the error message format changes. Consider using pattern matching on the error type instead: matches!(e, tokio::sync::mpsc::error::SendError(_))

Suggested change
&& e.to_string().contains("channel closed");
&& matches!(e, tokio::sync::mpsc::error::SendError(_));

Copilot uses AI. Check for mistakes.
@lifeizhou-ap lifeizhou-ap changed the title Lifei/fix error message when ctrlc fix: unexpected error message when ctrl+c is entered while running recipe with sub-recipes Jul 21, 2025
@lifeizhou-ap lifeizhou-ap requested a review from wendytang July 22, 2025 00:21
@michaelneale
Copy link
Collaborator

just flagging this PR #3554 which may collide with this (but not sure)

@lifeizhou-ap
Copy link
Collaborator Author

just flagging this PR #3554 which may collide with this (but not sure)

oh! thank you @michaelneale for the heads-up

@lifeizhou-ap lifeizhou-ap requested a review from DOsinga July 22, 2025 03:25
* main: (32 commits)
  fix: use sequential when sub recipe task is 1. (#3573)
  fix: track message id to keep like with like (#3572)
  Replace mcp_core::prompt with rmcp::model types (#3561)
  feat (ui): close recipe modals with esc key (#3568)
  feat: recipes can retry with success criteria (#3474)
  Env var to set Ollama request timeout (#3516)
  Updating docs to match new UI (#3552)
  Improve Claude Code provider error message for missing CLI (#3363)
  feat: Work around Gemini API tool call quirks (#3328)
  feat(ui): Source CashSans-Bold and improve overall text rendering (#3091)
  refactor: Use openapi for recipe endpoint types and in frontend (#3548)
  Fix Google Analytics error for local dev (#3544)
  Extension Library Improvements (#3541)
  fix(ui): enable selection of zero-config providers in desktop GUI (#3378)
  refactor: Renames recipe route to recipes to be consistent (#3540)
  Blog: Orchestrating 6 Subagents to Build a Collaborative API Playground (#3528)
  Catch json errors a little better (#3437)
  Rust debug (#3510)
  refactor: Centralise deeplink encode and decode into server (#3489)
  feat: deprecate jetbrains extension in favor of public one (#2589)
  ...
@wendytang
Copy link

wendytang commented Jul 22, 2025

Should PR #3550 and PR #3554 Be Combined?
Analysis: Can They Coexist?
Looking at both PRs, they have overlapping but different approaches to cancellation. Let me analyze the compatibility:

My Recommendation: Enhanced PR #3554
I recommend Option 1 (Enhanced PR #3554) because:

Advantages:
✅ Single source of truth: One cancellation token flows through the entire system
✅ Consistent behavior: All operations respect the same cancellation signal
✅ Future-proof: New subsystems automatically get cancellation support
✅ Clean architecture: Clear separation between session management and execution
✅ Comprehensive: Handles both general agent operations AND sub-recipe specifics
Implementation Strategy:
Start with PR #3554's foundation
Add the sub-recipe cancellation token propagation from PR #3550
Remove the redundant cancel_all_subagent_executions() method
Ensure all subsystems receive and respect the same cancellation token


Option 1: Enhanced PR #3554 (Recommended)
Build on PR #3554's general approach but add the granular sub-recipe handling from PR #3550:


// Core Agent interface (from PR #3554)
pub async fn reply(
    &self,
    messages: &[Message],
    session: Option<SessionConfig>,
    cancel_token: Option<CancellationToken>, // ✅ General cancellation
) -> Result<BoxStream<'_, Result<AgentEvent>>>

// Enhanced session handling
async fn process_agent_response(&mut self, interactive: bool) -> Result<()> {
    let cancel_token = CancellationToken::new();
    let cancel_token_clone = cancel_token.clone();
    
    // ✅ Pass cancellation to both general agent AND sub-recipes
    self.agent.set_cancellation_token(cancel_token.clone()).await;
    
    let mut stream = self
        .agent
        .reply(&self.messages, session_config.clone(), Some(cancel_token))
        .await?;
        
    tokio::select! {
        // ... existing stream handling ...
        _ = tokio::signal::ctrl_c() => {
            // ✅ Single cancellation point that handles everything
            cancel_token_clone.cancel();
            drop(stream);
            // ... handle interruption ...
        }
    }
}
Enhanced Agent Implementation:

impl Agent {
    // ✅ Store cancellation token for sub-systems to use
    async fn set_cancellation_token(&self, token: CancellationToken) {
        // Pass to tasks manager
        self.tasks_manager.set_cancellation_token(token.clone()).await;
        
        // Pass to any other long-running subsystems
        // self.extension_manager.set_cancellation_token(token.clone()).await;
    }
    
    pub async fn reply(
        &self,
        messages: &[Message],
        session: Option<SessionConfig>,
        cancel_token: Option<CancellationToken>,
    ) -> Result<BoxStream<'_, Result<AgentEvent>>> {
        // ✅ Check cancellation in main loop (from PR #3554)
        loop {
            if cancel_token.as_ref().is_some_and(|t| t.is_cancelled()) {
                break;
            }
            
            // ... existing logic ...
            // Sub-recipes will automatically respect the same token
        }
    }
}
Enhanced TasksManager:

impl TasksManager {
    // ✅ Unified cancellation token management
    pub async fn set_cancellation_token(&self, token: CancellationToken) {
        let mut current_token = self.cancellation_token.write().await;
        *current_token = Some(token);
        
        // Cancel any existing executions
        self.cancel_all_executions().await;
    }
    
    pub async fn register_execution(&self, execution_token: CancellationToken) {
        // ✅ Link execution token to global cancellation
        if let Some(global_token) = &*self.cancellation_token.read().await {
            let linked_token = CancellationToken::new();
            
            // Cancel execution if either global OR execution-specific cancellation occurs
            tokio::spawn({
                let global = global_token.clone();
                let execution = execution_token.clone();
                let linked = linked_token.clone();
                async move {
                    tokio::select! {
                        _ = global.cancelled() => linked.cancel(),
                        _ = execution.cancelled() => linked.cancel(),
                    }
                }
            });
        }
        
        // Store the linked token
        let mut tokens = self.active_tokens.write().await;
        tokens.push(linked_token);
    }
}

Copy link

@wendytang wendytang left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nice! definitely think this is needed.

@DOsinga's #3554 seems to be an overarching solution for clean ups, we could propagate the main agent cancellation token from to the subagent

@lifeizhou-ap
Copy link
Collaborator Author

I created a new PR #3599 to reuse the cancellation token created in agent.reply

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants