{"record":{"id":"12ad2db95776a0a2","repo":"chenhg5/cc-connect","slug":"process-tree-pid-d-still-alive-10s-after-sigkil","errorCode":null,"errorMessage":"process tree (pid %d) still alive 10s after SIGKILL reported success","messagePattern":"process tree \\(pid (.+?)\\) still alive 10s after SIGKILL reported success","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"agent/claudecode/session.go","lineNumber":1311,"sourceCode":"\t\tcase <-cs.done:\n\t\t\tslog.Info(\"claudeSession: exited during force-kill retries\")\n\t\t\treturn nil\n\t\tcase <-time.After(2 * time.Second):\n\t\t}\n\t}\n\n\tselect {\n\tcase <-cs.done:\n\t\treturn nil\n\tcase <-time.After(10 * time.Second):\n\t\tpid := -1\n\t\tif cs.cmd != nil && cs.cmd.Process != nil {\n\t\t\tpid = cs.cmd.Process.Pid\n\t\t}\n\t\tif killErr != nil {\n\t\t\treturn fmt.Errorf(\"process tree (pid %d) still alive after SIGKILL retries: %w\", pid, killErr)\n\t\t}\n\t\treturn fmt.Errorf(\"process tree (pid %d) still alive 10s after SIGKILL reported success\", pid)\n\t}\n}\n\n// shellJoinArgs joins args into a single string, quoting any arg that\n// contains whitespace so that a shell-style splitter (like my_cli's\n// splitCommandLine) preserves each arg as one token.\n//\n// Uses single quotes because some splitters (e.g. my_cli) don't support\n// backslash escapes inside double quotes. For values containing single\n// quotes, we close the single-quoted segment, add an escaped single\n// quote, and reopen: 'it'\\”s' → it's\nfunc shellJoinArgs(args []string) string {\n\tvar b strings.Builder\n\tfor i, a := range args {\n\t\tif i > 0 {\n\t\t\tb.WriteByte(' ')\n\t\t}\n\t\tif !strings.ContainsAny(a, \" \\t\\n\\r'\\\"\\\\\") {","sourceCodeStart":1293,"sourceCodeEnd":1329,"githubUrl":"https://github.com/chenhg5/cc-connect/blob/4000b2338aa6e850c99df54f8b0ed6ed7460b401/agent/claudecode/session.go#L1293-L1329","documentation":"This error is returned by (*ClaudeSession).Close after the session has sent SIGKILL to the agent process and, despite the kill reporting success, the process (or its process tree) was still alive after a 10-second grace period. It indicates an unkillable or restarted child process, typically a zombie state or a process stuck in uninterruptible kernel sleep (D state). The library throws it so callers know cleanup did not actually succeed and resources (pid, pipes) may still be held.","triggerScenarios":"Calling Close() on a ClaudeSession whose underlying cmd process was sent SIGKILL, the kill syscall returned success, but a subsequent liveness check (e.g. signal 0 or /proc lookup) found the pid still alive 10 seconds later. Also exercised by TestClaudeSessionClose_IdempotentNoPanic when Close is invoked on sessions with live/stale process handles.","commonSituations":"Claude CLI process hung in uninterruptible I/O (D state) so SIGKILL cannot take effect; a zombie child whose parent has not reaped it; container/namespace setups where the pid belongs to a different namespace; PID reuse causing the liveness probe to check an unrelated process.","solutions":["Check the process state (ps -o stat= -p <pid>); if in D state, resolve the blocking I/O (NFS/FS hang) or reboot the host — SIGKILL cannot preempt D state.","Ensure the parent reaps children: verify Close/waitToExit reaps the process (cmd.Wait) so zombies do not keep the pid 'alive'.","In containers, kill from inside the same PID namespace; killing a namespace-mapped pid from the host can miss the real process.","If PID reuse is suspected, compare the process name/cmdline against the expected agent binary before concluding it is still alive.","As a last resort, log the pid and continue: the error is advisory for cleanup, and the OS will eventually reclaim the process."],"exampleFix":"// before\nif err := cs.Close(); err != nil {\n    return fmt.Errorf(\"close failed: %w\", err)\n}\n// after\nif err := cs.Close(); err != nil {\n    var stillAlive *os.ProcessState // advisory: process may be in D-state\n    slog.Warn(\"claude session close did not fully terminate process\", \"err\", err)\n    // do not retry SIGKILL in a loop; investigate pid state out-of-band\n}","handlingStrategy":"try-catch","validationCode":"// Before closing, confirm the process exists and check its state\nout, _ := exec.Command(\"ps\", \"-o\", \"stat=\", \"-p\", strconv.Itoa(pid)).Output()\nif strings.HasPrefix(strings.TrimSpace(string(out)), \"D\") {\n    slog.Warn(\"agent process in uninterruptible sleep; kill may not complete\")\n}","typeGuard":"func isStillAlive(p *os.Process) bool {\n    return p != nil && p.Signal(syscall.Signal(0)) == nil\n}","tryCatchPattern":"if err := session.Close(); err != nil {\n    if strings.Contains(err.Error(), \"still alive 10s after SIGKILL\") {\n        slog.Warn(\"process not reaped; inspect pid state out-of-band\", \"err\", err)\n        // do not tight-loop retry; schedule a delayed recheck\n    } else {\n        return err\n    }\n}","preventionTips":["Always let Close complete its wait/reap logic; never SIGKILL children manually in parallel.","Monitor for processes stuck in D state on hosts using network filesystems.","Run in the same PID namespace as the agent when using containers.","Set reasonable timeouts and log pids so stuck processes can be investigated."],"tags":["process-management","go","sigkill","cleanup"],"backgroundTag":"process-kill-failed","analyzedSha":"4000b2338aa6e850c99df54f8b0ed6ed7460b401","analyzedAt":"2026-09-06T11:45:09.575Z","contentChangedAt":"2026-09-06T11:45:09.575Z","schemaVersion":2},"datasetVersion":"2026-09-14T00:17:10.932Z"}