fix: Multiple bug fixes for agent lifecycle and task creation (#122)
This commit is contained in:
@@ -225,6 +225,10 @@ func (m *Manager) Start(ctx context.Context) error {
|
||||
m.cmd = exec.Command(m.cfg.AgentArgs[0], m.cfg.AgentArgs[1:]...)
|
||||
m.cmd.Dir = m.cfg.WorkDir
|
||||
m.cmd.Env = m.cfg.AgentEnv
|
||||
// Create a new process group so we can kill all child processes together.
|
||||
// This is important for adapters like OpenCode that spawn child processes
|
||||
// (npx -> sh -> node -> opencode binary).
|
||||
setProcGroup(m.cmd)
|
||||
|
||||
// Set up pipes
|
||||
var err error
|
||||
@@ -476,52 +480,85 @@ func (m *Manager) Stop(ctx context.Context) error {
|
||||
|
||||
status := m.Status()
|
||||
if status == StatusStopped || status == StatusStopping {
|
||||
m.logger.Info("Stop called but already stopped/stopping", zap.String("status", string(status)))
|
||||
return nil
|
||||
}
|
||||
|
||||
m.logger.Info("stopping agent process")
|
||||
m.logger.Info("stopping agent process - START")
|
||||
m.status.Store(StatusStopping)
|
||||
|
||||
// Stop shell session
|
||||
m.logger.Debug("stopping shell session")
|
||||
if m.shell != nil {
|
||||
if err := m.shell.Stop(); err != nil {
|
||||
m.logger.Debug("failed to stop shell session", zap.Error(err))
|
||||
}
|
||||
m.shell = nil
|
||||
}
|
||||
m.logger.Debug("shell session stopped")
|
||||
|
||||
// Stop all running workspace processes (dev server, setup, cleanup, custom).
|
||||
m.logger.Debug("stopping workspace processes")
|
||||
if m.processRunner != nil {
|
||||
if err := m.processRunner.StopAll(ctx); err != nil {
|
||||
m.logger.Debug("failed to stop workspace processes", zap.Error(err))
|
||||
}
|
||||
}
|
||||
m.logger.Debug("workspace processes stopped")
|
||||
|
||||
// Stop workspace tracker
|
||||
m.logger.Debug("stopping workspace tracker")
|
||||
if m.workspaceTracker != nil {
|
||||
m.workspaceTracker.Stop()
|
||||
}
|
||||
m.logger.Debug("workspace tracker stopped")
|
||||
|
||||
// Close the adapter
|
||||
m.logger.Debug("closing adapter")
|
||||
if m.adapter != nil {
|
||||
if err := m.adapter.Close(); err != nil {
|
||||
m.logger.Debug("failed to close adapter", zap.Error(err))
|
||||
}
|
||||
}
|
||||
m.logger.Debug("adapter closed")
|
||||
|
||||
// Close stop channel to signal readers
|
||||
m.logger.Debug("closing stop channel")
|
||||
if m.stopCh != nil {
|
||||
close(m.stopCh)
|
||||
}
|
||||
m.logger.Debug("stop channel closed")
|
||||
|
||||
// Close stdin to signal EOF to agent
|
||||
m.logger.Debug("closing stdin")
|
||||
if m.stdin != nil {
|
||||
if err := m.stdin.Close(); err != nil {
|
||||
m.logger.Debug("failed to close stdin", zap.Error(err))
|
||||
}
|
||||
}
|
||||
m.logger.Debug("stdin closed")
|
||||
|
||||
// Some adapters (like OpenCode) run as HTTP servers and don't exit on stdin close.
|
||||
// For these, we need to kill the process group immediately.
|
||||
// We kill the process group to ensure all child processes are killed too.
|
||||
// This is important because OpenCode spawns: npx -> sh -> node -> opencode binary
|
||||
if m.adapter != nil && m.adapter.RequiresProcessKill() {
|
||||
if m.cmd != nil && m.cmd.Process != nil {
|
||||
pid := m.cmd.Process.Pid
|
||||
m.logger.Debug("killing process group", zap.Int("pgid", pid))
|
||||
// Kill the entire process group (platform-specific implementation)
|
||||
if err := killProcessGroup(pid); err != nil {
|
||||
m.logger.Debug("failed to kill process group, trying single process", zap.Error(err))
|
||||
// Fallback to killing just the process
|
||||
if err := m.cmd.Process.Kill(); err != nil {
|
||||
m.logger.Warn("failed to kill process", zap.Error(err))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Wait for process to exit with timeout
|
||||
m.logger.Debug("waiting for process to exit")
|
||||
done := make(chan struct{})
|
||||
go func() {
|
||||
m.wg.Wait()
|
||||
@@ -542,6 +579,7 @@ func (m *Manager) Stop(ctx context.Context) error {
|
||||
}
|
||||
|
||||
m.status.Store(StatusStopped)
|
||||
m.logger.Info("stopping agent process - COMPLETE")
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
//go:build unix
|
||||
|
||||
package process
|
||||
|
||||
import (
|
||||
"os/exec"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
// setProcGroup configures the command to run in its own process group.
|
||||
// This allows us to kill all child processes together.
|
||||
func setProcGroup(cmd *exec.Cmd) {
|
||||
cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}
|
||||
}
|
||||
|
||||
// killProcessGroup kills the entire process group for the given PID.
|
||||
// Returns nil if successful, or an error if the kill failed.
|
||||
func killProcessGroup(pid int) error {
|
||||
// Kill the entire process group by using negative PID
|
||||
return syscall.Kill(-pid, syscall.SIGKILL)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
//go:build windows
|
||||
|
||||
package process
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os/exec"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
// setProcGroup configures the command to run in its own process group.
|
||||
// On Windows, we use CREATE_NEW_PROCESS_GROUP flag.
|
||||
func setProcGroup(cmd *exec.Cmd) {
|
||||
cmd.SysProcAttr = &syscall.SysProcAttr{
|
||||
CreationFlags: syscall.CREATE_NEW_PROCESS_GROUP,
|
||||
}
|
||||
}
|
||||
|
||||
// killProcessGroup kills the entire process tree for the given PID.
|
||||
// On Windows, we use taskkill with /T flag to kill the process tree.
|
||||
func killProcessGroup(pid int) error {
|
||||
// taskkill /F /T /PID <pid>
|
||||
// /F = force kill
|
||||
// /T = kill child processes (tree)
|
||||
// /PID = process ID
|
||||
kill := exec.Command("taskkill", "/F", "/T", "/PID", fmt.Sprintf("%d", pid))
|
||||
return kill.Run()
|
||||
}
|
||||
|
||||
@@ -697,8 +697,12 @@ func (wt *WorkspaceTracker) enrichWithDiffData(ctx context.Context, update *type
|
||||
// uncommitted changes in the "files changed" count.
|
||||
}
|
||||
|
||||
// Also check for staged changes
|
||||
stagedCmd := exec.CommandContext(ctx, "git", "diff", "--cached", "--numstat")
|
||||
// For staged files that don't have unstaged changes, we need to get the diff from the index.
|
||||
// The first diff (git diff baseRef) shows worktree vs baseRef, but if a file is staged
|
||||
// and has no additional unstaged changes, its diff won't appear there.
|
||||
// We use git diff --cached baseRef to get the staged diff content for such files,
|
||||
// using the same baseRef for consistency.
|
||||
stagedCmd := exec.CommandContext(ctx, "git", "diff", "--cached", "--numstat", baseRef)
|
||||
stagedCmd.Dir = wt.workDir
|
||||
stagedOut, err := stagedCmd.Output()
|
||||
if err != nil {
|
||||
@@ -723,18 +727,19 @@ func (wt *WorkspaceTracker) enrichWithDiffData(ctx context.Context, update *type
|
||||
|
||||
// Update existing file info if it exists
|
||||
if fileInfo, exists := update.Files[filePath]; exists {
|
||||
// Add staged changes to the counts
|
||||
fileInfo.Additions += additions
|
||||
fileInfo.Deletions += deletions
|
||||
// Only set additions/deletions if they weren't already set by the first diff.
|
||||
// This prevents double-counting when changes appear in both diffs.
|
||||
if fileInfo.Additions == 0 && fileInfo.Deletions == 0 {
|
||||
fileInfo.Additions = additions
|
||||
fileInfo.Deletions = deletions
|
||||
}
|
||||
|
||||
// Get the staged diff content
|
||||
diffCmd := exec.CommandContext(ctx, "git", "diff", "--cached", "--", filePath)
|
||||
diffCmd.Dir = wt.workDir
|
||||
if diffOut, err := diffCmd.Output(); err == nil {
|
||||
// Append staged diff to existing diff
|
||||
if fileInfo.Diff != "" {
|
||||
fileInfo.Diff += "\n\n--- Staged changes ---\n" + string(diffOut)
|
||||
} else {
|
||||
// Get the staged diff content if we don't have diff content yet
|
||||
// Use the same baseRef for consistency with unstaged files
|
||||
if fileInfo.Diff == "" {
|
||||
diffCmd := exec.CommandContext(ctx, "git", "diff", "--cached", baseRef, "--", filePath)
|
||||
diffCmd.Dir = wt.workDir
|
||||
if diffOut, err := diffCmd.Output(); err == nil {
|
||||
fileInfo.Diff = string(diffOut)
|
||||
}
|
||||
}
|
||||
@@ -1115,12 +1120,9 @@ func (wt *WorkspaceTracker) buildFileTreeNode(fullPath, relPath string, info os.
|
||||
// Build children
|
||||
node.Children = make([]*types.FileTreeNode, 0, len(entries))
|
||||
for _, entry := range entries {
|
||||
// Skip hidden files and common ignore patterns
|
||||
// Skip specific directories that should be ignored
|
||||
name := entry.Name()
|
||||
if strings.HasPrefix(name, ".") && name != "." && name != ".." {
|
||||
continue
|
||||
}
|
||||
if name == "node_modules" || name == ".next" || name == "dist" || name == "build" {
|
||||
if name == ".git" || name == "node_modules" || name == ".next" || name == "dist" || name == "build" {
|
||||
continue
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user