fix(ax): centralise pid lifecycle checks

Co-Authored-By: Virgil <virgil@lethean.io>
This commit is contained in:
Virgil 2026-03-30 00:28:11 +00:00
parent 6c246a7165
commit eae4d3f904
8 changed files with 31 additions and 44 deletions

View file

@ -4,7 +4,6 @@ package agentic
import (
"context"
"syscall"
"time"
core "dappco.re/go/core"
@ -80,7 +79,7 @@ func (s *PrepSubsystem) DispatchSync(ctx context.Context, input DispatchSyncInpu
case <-ctx.Done():
return DispatchSyncResult{Error: "cancelled"}
case <-ticker.C:
if pid > 0 && syscall.Kill(pid, 0) != nil {
if pid > 0 && !PIDAlive(pid) {
// Process exited — read final status
st, err := ReadStatus(wsDir)
if err != nil {

View file

@ -65,20 +65,20 @@ func (s *PrepSubsystem) gitOutput(ctx context.Context, dir string, args ...strin
// --- Process lifecycle helpers ---
// processIsRunning checks if a process is still alive via PID signal check.
// PIDAlive checks if an OS process is still alive via PID signal check.
//
// if processIsRunning(st.ProcessID, st.PID) { ... }
func processIsRunning(processID string, pid int) bool {
// if agentic.PIDAlive(st.PID) { ... }
func PIDAlive(pid int) bool {
if pid > 0 {
return syscall.Kill(pid, 0) == nil
}
return false
}
// processKill terminates a process via SIGTERM.
// PIDTerminate terminates a process via SIGTERM.
//
// processKill(st.ProcessID, st.PID)
func processKill(processID string, pid int) bool {
// if agentic.PIDTerminate(st.PID) { ... }
func PIDTerminate(pid int) bool {
if pid > 0 {
return syscall.Kill(pid, syscall.SIGTERM) == nil
}

View file

@ -179,38 +179,38 @@ func TestProc_GitOutput_Ugly(t *testing.T) {
assert.Equal(t, "", out)
}
// --- processIsRunning ---
// --- PIDAlive ---
func TestProc_ProcessIsRunning_Good(t *testing.T) {
func TestProc_PIDAlive_Good(t *testing.T) {
// Own PID should be running
pid, _ := strconv.Atoi(core.Env("PID"))
assert.True(t, processIsRunning("", pid))
assert.True(t, PIDAlive(pid))
}
func TestProc_ProcessIsRunning_Bad(t *testing.T) {
func TestProc_PIDAlive_Bad(t *testing.T) {
// PID 999999 should not be running (extremely unlikely to exist)
assert.False(t, processIsRunning("", 999999))
assert.False(t, PIDAlive(999999))
}
func TestProc_ProcessIsRunning_Ugly(t *testing.T) {
func TestProc_PIDAlive_Ugly(t *testing.T) {
// PID 0 — should return false (invalid PID guard: pid > 0 is false for 0)
assert.False(t, processIsRunning("", 0))
assert.False(t, PIDAlive(0))
}
// --- processKill ---
// --- PIDTerminate ---
func TestProc_ProcessKill_Good(t *testing.T) {
func TestProc_PIDTerminate_Good(t *testing.T) {
t.Skip("would need real process to kill")
}
func TestProc_ProcessKill_Bad(t *testing.T) {
func TestProc_PIDTerminate_Bad(t *testing.T) {
// PID 999999 should fail to kill
assert.False(t, processKill("", 999999))
assert.False(t, PIDTerminate(999999))
}
func TestProc_ProcessKill_Ugly(t *testing.T) {
func TestProc_PIDTerminate_Ugly(t *testing.T) {
// PID 0 — pid > 0 guard returns false
assert.False(t, processKill("", 0))
assert.False(t, PIDTerminate(0))
}
// --- initTestRepo creates a git repo with commits for proc tests ---

View file

@ -4,7 +4,6 @@ package agentic
import (
"strconv"
"syscall"
"time"
core "dappco.re/go/core"
@ -167,10 +166,8 @@ func (s *PrepSubsystem) countRunningByAgent(agent string) int {
if s.workspaces != nil && s.workspaces.Len() > 0 {
count := 0
s.workspaces.Each(func(_ string, st *WorkspaceStatus) {
if st.Status == "running" && baseAgent(st.Agent) == agent {
if st.PID > 0 && syscall.Kill(st.PID, 0) == nil {
count++
}
if st.Status == "running" && baseAgent(st.Agent) == agent && PIDAlive(st.PID) {
count++
}
})
return count
@ -192,7 +189,7 @@ func (s *PrepSubsystem) countRunningByAgentDisk(agent string) int {
if baseAgent(st.Agent) != agent {
continue
}
if st.PID > 0 && syscall.Kill(st.PID, 0) == nil {
if PIDAlive(st.PID) {
count++
}
}
@ -207,10 +204,8 @@ func (s *PrepSubsystem) countRunningByModel(agent string) int {
if s.workspaces != nil && s.workspaces.Len() > 0 {
count := 0
s.workspaces.Each(func(_ string, st *WorkspaceStatus) {
if st.Status == "running" && st.Agent == agent {
if st.PID > 0 && syscall.Kill(st.PID, 0) == nil {
count++
}
if st.Status == "running" && st.Agent == agent && PIDAlive(st.PID) {
count++
}
})
return count
@ -226,7 +221,7 @@ func (s *PrepSubsystem) countRunningByModel(agent string) int {
if st.Agent != agent {
continue
}
if st.PID > 0 && syscall.Kill(st.PID, 0) == nil {
if PIDAlive(st.PID) {
count++
}
}

View file

@ -4,7 +4,6 @@ package agentic
import (
"context"
"syscall"
"time"
core "dappco.re/go/core"
@ -142,7 +141,7 @@ func (s *PrepSubsystem) status(ctx context.Context, _ *mcp.CallToolRequest, inpu
// If status is "running", check if PID is still alive
if st.Status == "running" && st.PID > 0 {
if err := syscall.Kill(st.PID, 0); err != nil {
if !PIDAlive(st.PID) {
blockedPath := workspaceBlockedPath(wsDir)
if r := fs.Read(blockedPath); r.OK {
st.Status = "blocked"

View file

@ -10,7 +10,6 @@ package monitor
import (
"context"
"sync"
"syscall"
"time"
"dappco.re/go/agent/pkg/agentic"
@ -297,10 +296,7 @@ func (m *Subsystem) countLiveWorkspaces() (running, queued int) {
// pidAlive checks whether a process is still running.
func pidAlive(pid int) bool {
if pid <= 0 {
return false
}
return syscall.Kill(pid, 0) == nil
return agentic.PIDAlive(pid)
}
func (m *Subsystem) loop(ctx context.Context) {

View file

@ -4,7 +4,6 @@ package runner
import (
"strconv"
"syscall"
"time"
"dappco.re/go/agent/pkg/agentic"
@ -170,7 +169,7 @@ func (s *Service) countRunningByAgent(agent string) int {
switch {
case st.PID < 0:
count++
case st.PID > 0 && syscall.Kill(st.PID, 0) == nil:
case st.PID > 0 && agentic.PIDAlive(st.PID):
count++
}
})
@ -189,7 +188,7 @@ func (s *Service) countRunningByModel(agent string) int {
switch {
case st.PID < 0:
count++
case st.PID > 0 && syscall.Kill(st.PID, 0) == nil:
case st.PID > 0 && agentic.PIDAlive(st.PID):
count++
}
})

View file

@ -10,7 +10,6 @@ package runner
import (
"context"
"sync"
"syscall"
"time"
"dappco.re/go/agent/pkg/agentic"
@ -338,7 +337,7 @@ func (s *Service) actionKill(_ context.Context, _ core.Options) core.Result {
killed := 0
s.workspaces.Each(func(_ string, st *WorkspaceStatus) {
if st.Status == "running" && st.PID > 0 {
if syscall.Kill(st.PID, syscall.SIGTERM) == nil {
if agentic.PIDTerminate(st.PID) {
killed++
}
st.Status = "failed"