Repository navigation
Description
Problem
When shell_kill stops a background job, the kill result is still sent as a follow-up message after the turn ends. The model already knows the job is dead because it asked for the kill, so the follow-up only starts a turn in which the model acknowledges its own kill.
That extra turn adds the kill result and a new assistant message at the end of the conversation. /copy then copies the short kill acknowledgement instead of the real answer from the turn before it, and you have to scroll up past both messages to read that answer.
The bug is present in 0.6.1 (8831169), which is after the #2 fix. pi, interactive session.
Trace
- A background job (from
background: true, or moved there by the auto-background threshold) hasscheduleDelivery(job, settle)attached. - The model calls
shell_kill {id: "bg-1"}. It setskilledByUs = true, callskillTree, and returns "Killing bg-1 (pid …) and its process tree." right away. It never setsjob.deliveredand never removes the job frompending. - The process exits.
settlesetsjob.status = "killed", andscheduleDeliverycallsenqueueDelivery(job). - At
agent_settled,drainDeliveries()finds a finished job withdelivered === falseand sends it with{ deliverAs: "followUp", triggerTurn: true }, which starts a new turn.
The #2 fix made shell_status mark a finished job collected, but shell_kill was not given the same treatment.
Repro
Run through the test harness in test/extension.test.ts (mock pi), Node 24.19:
agent_start
bash {command: "sleep 60", background: true} -> bg-1
shell_kill {id: "bg-1"} -> Killing bg-1 (pid 18144) and its process tree.
(process exits) -> sent: 0
agent_settled -> sent: 1
<shell_background_result id="bg-1">
[bg-1 · killed · killed · 715ms]
(no output)
</shell_background_result>
Suggested fix
Make shell_kill wait for the job to leave running, for at most 3 s (the same grace killTree gives before SIGKILL, and the same wait the foreground timeout path uses). Check job.status in a loop like shell_status does, so it also works for jobs adopted after /reload, which have no settle.
- If the job has finished, set
delivered = true, remove it frompending, persist it, and return the finalformatResultwith a[killed]note. The kill is then reported once, as the tool result. - If it is still running after 3 s, leave delivery on and say the signal was sent but the process has not exited yet. The later follow-up is useful in that case.
Filed by claude-opus-5-5.
Activity
fix(shell-background): a kill is reported once, and a kill from /shel…
fix(shell-background): a job killed after /reload reads as killed on …
Merge pull request #4 from dibin666/main
feat(shell-background): release 0.7.0 — /shell-bg job browser, kill r…
hypnguyen1209 commentedon Nov 3, 2026
Fixed in 0.7.0 via #4, exactly along the lines you suggested — thank you for the precise trace.
shell_killnow holds delivery back using the samewaitersmap ashell_statuswait uses, and pollsjob.statusfor up to 3.5s (pastkillTree's 3s SIGKILL escalation, and poll-based so a job adopted after/reloadwith nosettleworks too). If the job exits within the wait, it is markeddelivered, removed frompending, persisted, and the final result is returned as the tool result with a[stopped with shell_kill]note — so a model-initiated kill is reported once and never starts an extra turn, and/copyno longer grabs the kill acknowledgement. If it is still alive after the wait, delivery stays on (that exit is genuinely news) and the message points atshell_status.As a bonus from the same PR, a kill from the UI (
/shell-bg, the new job browser) is told to the model with a note that the user stopped it and not to restart it — since the model did not ask for that one.Verified against the mock-pi harness in
test/extension.test.tsplus the full suite (110 tests, tsc clean). Closing as fixed in 0.7.0.