Skip to content

cancel never signals a foreground companion on Linux/macOS: process-group kill fails with ESRCH and there is no fallback to the pid #786

Open

Description

@matts-dt

Environment

  • Plugin codex@openai-codex 1.0.6; main @ db52e28 has the same code
  • Linux, Node 24 (the code path is the same on every non-Windows platform)

Summary

On non-Windows platforms, terminateProcessTree() (plugins/codex/scripts/lib/process.mjs) signals the job's process group with process.kill(-pid, "SIGTERM"). That only works when the recorded pid leads its own process group. Detached background workers do (spawn(..., { detached: true })), but a companion started in the foreground from a shell (for example from Claude Code's Bash tool) does not: the shell does not give it its own group, so kill(-pid) fails with ESRCH.

The catch block falls back to process.kill(pid) only for errors other than ESRCH:

try {
  killImpl(-pid, "SIGTERM");
  return { attempted: true, delivered: true, method: "process-group" };
} catch (error) {
  if (error?.code !== "ESRCH") {
    // fallback to killImpl(pid) ...
  }
  return { attempted: true, delivered: false, method: "process-group" };
}

So for a foreground companion nothing is signalled. cancel (codex-companion.mjs, terminateProcessTree(job.pid ?? Number.NaN)) ignores the result and records the job as cancelled anyway. When the turn interrupt also fails (the situation in #540, Codex turn interrupt failed: ...), the companion keeps running while its job reports cancelled. We ran into this while cleaning up stuck foreground jobs.

Minimal demonstration

node -e 'const {spawn}=require("child_process"); const c=spawn("sleep",["30"]); try{process.kill(-c.pid,"SIGTERM")}catch(e){console.log("kill(-pid):", e.code)} process.kill(c.pid,"SIGTERM"); console.log("kill(pid): delivered")'

prints

kill(-pid): ESRCH
kill(pid): delivered

Suggested fix

  • Fall back to the pid whenever the group kill fails, including ESRCH, and report delivered: false only when that also fails with ESRCH:

    try {
      killImpl(-pid, "SIGTERM");
      return { attempted: true, delivered: true, method: "process-group" };
    } catch {
      // Not a process-group leader (ESRCH) or not permitted: signal the process itself.
    }
    try {
      killImpl(pid, "SIGTERM");
      return { attempted: true, delivered: true, method: "process" };
    } catch (error) {
      if (error?.code === "ESRCH") return { attempted: true, delivered: false, method: "process" };
      throw error;
    }
  • Have cancel check delivered before recording the job as cancelled, or record that the process could not be signalled.

  • Signalling only the pid does not reach a foreground companion's own children (for example a direct-mode app-server). Starting those children in a process group the companion owns, or tracking their pids in the job record, would let cancel stop the whole tree.

Related: #540 (how jobs get stuck mid-turn), #222 and #517 (no pid liveness check for running jobs).

Activity

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