mirror of
https://github.com/multica-ai/multica.git
synced 2026-07-06 22:09:44 +02:00
On cancellation/timeout the opencode backend closed the stdout read end immediately, leaving the child writing into a closed pipe. Every write then returns EPIPE and, per anomalyco/opencode#33653, can spin an orphaned process at 100% CPU — surfacing as high idle CPU after a cancelled task or daemon restart (MUL-3655). Cleanup now runs opencode in its own process group and, on cancel, drives a graceful group-wide SIGTERM → grace → SIGKILL, closing the stdout pipe only as a last-resort unblock once the tree has been signalled (SIGKILL is uncatchable, so no member can write again — no EPIPE window). The group signal also reaps tool subprocesses opencode spawned instead of orphaning them. WaitDelay remains the hard backstop. Adds unix tests covering the graceful path and the SIGTERM-ignored → SIGKILL escalation, asserting the whole process group is reaped and the run never deadlocks on the scanner. Windows behaviour is unchanged (no process groups). Co-authored-by: J <j@multica.ai> Co-authored-by: multica-agent <github@multica.ai>
39 lines
1.2 KiB
Go
39 lines
1.2 KiB
Go
//go:build !windows
|
|
|
|
package agent
|
|
|
|
import (
|
|
"os"
|
|
"os/exec"
|
|
"syscall"
|
|
)
|
|
|
|
// hideAgentWindow is a no-op on non-Windows platforms.
|
|
func hideAgentWindow(cmd *exec.Cmd) {}
|
|
|
|
// configureProcessGroup puts the child into its own process group (it becomes
|
|
// the group leader, so the group id equals the child pid). This lets the
|
|
// daemon signal the entire tree — the agent CLI plus any tool subprocess it
|
|
// spawns — in one call, instead of killing only the direct child and leaking
|
|
// grandchildren that keep running (and, for opencode, spinning on EPIPE) after
|
|
// a task is cancelled or the daemon restarts. See signalProcessGroup.
|
|
func configureProcessGroup(cmd *exec.Cmd) {
|
|
if cmd.SysProcAttr == nil {
|
|
cmd.SysProcAttr = &syscall.SysProcAttr{}
|
|
}
|
|
cmd.SysProcAttr.Setpgid = true
|
|
}
|
|
|
|
// signalProcessGroup sends sig to the whole process group led by p (when the
|
|
// command was started with configureProcessGroup), falling back to the single
|
|
// process if the group send fails. Targeting the group (negative pid) reaches
|
|
// the descendants the agent spawned, not just the leader.
|
|
func signalProcessGroup(p *os.Process, sig syscall.Signal) {
|
|
if p == nil {
|
|
return
|
|
}
|
|
if err := syscall.Kill(-p.Pid, sig); err != nil {
|
|
_ = p.Signal(sig)
|
|
}
|
|
}
|