Skip to content

A job stopped with shell_kill is still delivered, so the kill costs an extra turn #3

Closed

Description

@chigkim

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

  1. A background job (from background: true, or moved there by the auto-background threshold) has scheduleDelivery(job, settle) attached.
  2. The model calls shell_kill {id: "bg-1"}. It sets killedByUs = true, calls killTree, and returns "Killing bg-1 (pid …) and its process tree." right away. It never sets job.delivered and never removes the job from pending.
  3. The process exits. settle sets job.status = "killed", and scheduleDelivery calls enqueueDelivery(job).
  4. At agent_settled, drainDeliveries() finds a finished job with delivered === false and 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 from pending, persist it, and return the final formatResult with 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

  1. added a commit that references this issue on Nov 3, 2026
  2. hypnguyen1209 commented on Nov 3, 2026

    @hypnguyen1209
    Contributor

    Fixed in 0.7.0 via #4, exactly along the lines you suggested — thank you for the precise trace.

    shell_kill now holds delivery back using the same waiters map a shell_status wait uses, and polls job.status for up to 3.5s (past killTree's 3s SIGKILL escalation, and poll-based so a job adopted after /reload with no settle works too). If the job exits within the wait, it is marked delivered, removed from pending, 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 /copy no 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 at shell_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.ts plus the full suite (110 tests, tsc clean). Closing as fixed in 0.7.0.

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions