Skip to content

Fix task cancellation: abort handlers and kill shell processes#61

Merged
tcdent merged 3 commits intomainfrom
claude/fix-task-cancellation-bugs-POJ68
Feb 6, 2026
Merged

Fix task cancellation: abort handlers and kill shell processes#61
tcdent merged 3 commits intomainfrom
claude/fix-task-cancellation-bugs-POJ68

Conversation

@tcdent
Copy link
Owner

@tcdent tcdent commented Feb 3, 2026

Problem

Cancelling tool execution (Ctrl-C) had two bugs:

  1. Spawned tokio tasks were never abortedJoinHandle was discarded after tokio::spawn, so cancel() couldn't stop running handlers
  2. Shell child processes were orphanedtokio::process::Child does NOT kill on drop, it detaches the process

Solution

KillOnDrop wrapper (io.rs): Wraps tokio::process::Child and calls start_kill() (sync SIGKILL) in its Drop impl. Spawned bash processes can never outlive their task.

JoinHandle stored in WaitingFor::Handler (exec.rs): The handle lives alongside the oneshot receiver in the enum variant — no new fields on ActivePipeline, no way for them to get out of sync.

Background tasks survive cancel: Cancel means "stop the current LLM turn", not "kill everything". Running background tasks keep going.

Kill chain on Ctrl-C

cancel() → handle.abort() → tokio task dropped → execute_shell future dropped
→ KillOnDrop dropped → start_kill() → SIGKILL sent to bash process

No trait changes

  • No changes to EffectHandler trait
  • No cancellation tokens threaded through handlers
  • No changes to any handler signatures

Tests

5 new cancellation tests:

  • test_cancel_clears_pending_queue
  • test_cancel_aborts_running_foreground
  • test_cancel_preserves_completed_background_tasks
  • test_cancel_preserves_running_background_tasks
  • test_cancel_with_mixed_running_and_completed_background

- Add KillOnDrop wrapper around tokio::process::Child in execute_shell()
  to ensure spawned processes are killed when their task is dropped
- Store JoinHandle alongside oneshot receiver in WaitingFor::Handler
  so cancel() can abort running handler tasks
- Cancel only aborts foreground tasks; background tasks keep running
- Kill chain: cancel() → handle.abort() → future dropped → KillOnDrop
  dropped → start_kill() → SIGKILL sent to child process
- Add 5 cancellation tests covering pending queue, foreground abort,
  background preservation, and mixed scenarios
- No changes to EffectHandler trait or handler signatures
@tcdent tcdent force-pushed the claude/fix-task-cancellation-bugs-POJ68 branch from d8a10dd to 9e86a52 Compare February 6, 2026 21:33
@tcdent tcdent changed the title Fix task cancellation bugs with orphaned blocks and panics Fix task cancellation: abort handlers and kill shell processes Feb 6, 2026
- Add cancel_foreground() to ToolExecutor: aborts only the active
  foreground task and returns error events for it
- Add has_running_foreground() query method
- App::cancel() now has two layers:
  1. If foreground tool running → cancel just that tool, error sent
     to LLM which continues its turn
  2. Otherwise → cancel entire turn (agent stream + all tools)
- Esc behavior stacks: deny approval → cancel tool → end turn
SIGKILL to bash alone orphans its children (sleep, find, etc.)
because SIGKILL can't be caught or forwarded. Fix by spawning bash
in its own process group (setpgid) and killing the negative PID
on drop, which kills the entire group including all children.
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.

1 participant