fix(shell): a self-cancelled job stays quiet, and an exited job keeps its exit

Two session-export defects in the background-shell cancel path:

- Cancelling your own background job also queued its completion
  notification. The session then got a "finished" event for a job it had
  just killed, and a new model turn could start from it.
  `ShellManager::kill` now marks a job it killed as
  `completion_reported`, so the completion drain skips it.
- A job that exited between the user's look and the cancel was still
  killed and reported as "Killed" with exit 0.
  `BackgroundShell::kill` now polls first and returns without signalling
  when the process has already finished. The cancel receipt then reports
  the real status and exit ("... Nothing was killed.") instead of "Canceled
  background command".

Evidence: cargo fmt --all -- --check is clean. The compile and the
tools::shell / tool_cancellation tests are pending a local test build and
hosted CI on codewhale-hq/Codewhale#6957.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01BsXVvReYca2UxzimQtj7Mj
This commit is contained in:
Claude committed 2026-10-11 12:53:40 +00:00
1 parent 42c5dccc9a
commit 8eb23adb03
1 file changed
+14 -4
+14 -4
View File
@@ -1586,6 +1586,9 @@ impl BackgroundShell {
/// Kill the process
fn kill(&mut self) -> Result<()> {
if self.poll() && self.status != ShellStatus::Running {
return Ok(());
}
#[cfg(unix)]
if self.ownership == ShellOwnership::PersistPending
&& let Some(process_group_id) = self.child.as_ref().and_then(ShellChild::process_id)
@@ -3204,6 +3207,9 @@ impl ShellManager {
.ok_or_else(|| anyhow!("Task {task_id} not found"))?;
shell.kill()?;
if shell.status == ShellStatus::Killed {
shell.completion_reported = true;
}
shell.snapshot()
}
@@ -7034,10 +7040,14 @@ impl BashTool {
.clone()
.unwrap_or_else(|| task_id.to_string());
Ok(ToolResult {
content: format!(
"Canceled background command: {task_id}. {}",
kill_outcome_note(&format!("{}{}", result.stdout, result.stderr))
),
content: if result.status == ShellStatus::Killed {
format!(
"Canceled background command: {task_id}. {}",
kill_outcome_note(&format!("{}{}", result.stdout, result.stderr))
)
} else {
format!("{} Nothing was killed.", wait_timing_line(&result))
},
success: true,
metadata: Some(json!({
"status": format!("{:?}", result.status),