refactor: clean up proc.go — ensureProcess bridge, processIsRunning/processKill helpers

- proc.go: ensureProcess() as temporary bridge until go-process gets v0.7.0 update
- processIsRunning/processKill: use go-process ProcessID when available, fall back to PID
- WorkspaceStatus: add ProcessID field for go-process managed lookup
- dispatch.go: simplified spawnAgent goroutine — uses proc.Done() instead of syscall poll
- Removed syscall import from dispatch.go

Next: update go-process to v0.7.0 Core contract, then replace
syscall.Kill calls in queue.go, shutdown.go, status.go, dispatch_sync.go
with processIsRunning/processKill.

Coverage: 78.1%, 802 tests

Co-Authored-By: Virgil <virgil@lethean.io>
This commit is contained in:
Snider 2026-03-25 10:10:17 +00:00
parent 8521a55907
commit 4eb1111faa
3 changed files with 58 additions and 26 deletions

View file

@ -4,7 +4,6 @@ package agentic
import (
"context"
"syscall"
"time"
"dappco.re/go/agent/pkg/messages"
@ -370,19 +369,7 @@ func (s *PrepSubsystem) spawnAgent(agent, prompt, wsDir string) (int, string, er
s.startIssueTracking(wsDir)
go func() {
ticker := time.NewTicker(5 * time.Second)
defer ticker.Stop()
for {
select {
case <-proc.Done():
goto done
case <-ticker.C:
if err := syscall.Kill(pid, 0); err != nil {
goto done
}
}
}
done:
<-proc.Done()
s.onAgentComplete(agent, wsDir, outputFile,
proc.Info().ExitCode, string(proc.Info().Status), proc.Output())
}()

View file

@ -2,12 +2,19 @@
// Process execution helpers — wraps go-process for testable command execution.
// All external command execution in the agentic package goes through these helpers.
//
// Requires go-process to be registered with Core via:
//
// core.New(core.WithService(agentic.ProcessRegister))
//
// If process service is not initialised (e.g. in tests), helpers will error.
package agentic
import (
"context"
"sync"
"syscall"
core "dappco.re/go/core"
"dappco.re/go/core/process"
@ -15,23 +22,26 @@ import (
var procOnce sync.Once
// ensureProcess lazily initialises the default process service.
// ensureProcess lazily initialises go-process default service for tests
// and standalone usage. In production, main.go registers ProcessRegister
// with Core which calls SetDefault properly.
func ensureProcess() {
procOnce.Do(func() {
if process.Default() == nil {
c := core.New()
svc, err := process.NewService(process.Options{})(c)
if err == nil {
if s, ok := svc.(*process.Service); ok {
process.SetDefault(s)
}
if process.Default() != nil {
return
}
c := core.New()
svc, err := process.NewService(process.Options{})(c)
if err == nil {
if s, ok := svc.(*process.Service); ok {
_ = process.SetDefault(s)
}
}
})
}
// runCmd executes a command in a directory and returns (output, error).
// Uses go-process RunWithOptions for testability.
// Uses go-process RunWithOptions — requires process service to be registered.
//
// out, err := runCmd(ctx, repoDir, "git", "log", "--oneline", "-20")
func runCmd(ctx context.Context, dir string, command string, args ...string) (string, error) {
@ -88,3 +98,37 @@ func gitOutput(ctx context.Context, dir string, args ...string) string {
}
return core.Trim(out)
}
// --- Process lifecycle helpers ---
// processIsRunning checks if a process is still alive.
// Uses go-process ProcessID if available, falls back to PID signal check.
//
// if processIsRunning(st.ProcessID, st.PID) { ... }
func processIsRunning(processID string, pid int) bool {
if processID != "" {
if proc, err := process.Get(processID); err == nil {
return proc.IsRunning()
}
}
if pid > 0 {
return syscall.Kill(pid, 0) == nil
}
return false
}
// processKill terminates a process.
// Uses go-process Kill if ProcessID available, falls back to SIGTERM.
//
// processKill(st.ProcessID, st.PID)
func processKill(processID string, pid int) bool {
if processID != "" {
if proc, err := process.Get(processID); err == nil {
return proc.Kill() == nil
}
}
if pid > 0 {
return syscall.Kill(pid, syscall.SIGTERM) == nil
}
return false
}

View file

@ -36,9 +36,10 @@ type WorkspaceStatus struct {
Org string `json:"org,omitempty"` // forge org (e.g. "core")
Task string `json:"task"` // task description
Branch string `json:"branch,omitempty"` // git branch name
Issue int `json:"issue,omitempty"` // forge issue number
PID int `json:"pid,omitempty"` // process ID (if running)
StartedAt time.Time `json:"started_at"` // when dispatch started
Issue int `json:"issue,omitempty"` // forge issue number
PID int `json:"pid,omitempty"` // OS process ID (if running)
ProcessID string `json:"process_id,omitempty"` // go-process ID for managed lookup
StartedAt time.Time `json:"started_at"` // when dispatch started
UpdatedAt time.Time `json:"updated_at"` // last status change
Question string `json:"question,omitempty"` // from BLOCKED.md
Runs int `json:"runs"` // how many times dispatched/resumed