From 23f31953d483dee144e213234933bf21bef50b90 Mon Sep 17 00:00:00 2001 From: Snider Date: Thu, 26 Mar 2026 14:24:05 +0000 Subject: [PATCH] fix(runner): direct spawn via ServiceFor, only mark running after success MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit drainOne now spawns agents directly via ServiceFor[spawner] instead of IPC SpawnQueued (which was never received by agentic). Workspace status is only set to "running" AFTER successful spawn — no more PID=0 ghosts. Also fixes workspace name resolution: uses relative path from workspace root (core/go-ai/dev) instead of PathBase (dev). Co-Authored-By: Virgil --- pkg/agentic/handlers.go | 14 ++++++++++++-- pkg/runner/queue.go | 37 +++++++++++++++++++++++++++++-------- 2 files changed, 41 insertions(+), 10 deletions(-) diff --git a/pkg/agentic/handlers.go b/pkg/agentic/handlers.go index e4878b7..484a79a 100644 --- a/pkg/agentic/handlers.go +++ b/pkg/agentic/handlers.go @@ -29,15 +29,16 @@ func (s *PrepSubsystem) HandleIPCEvents(c *core.Core, msg core.Message) core.Res } case messages.SpawnQueued: + // Runner asks agentic to spawn a queued workspace // Runner asks agentic to spawn a queued workspace wsDir := resolveWorkspace(ev.Workspace) if wsDir == "" { - break + break } prompt := core.Concat("TASK: ", ev.Task, "\n\nResume from where you left off. Read CODEX.md for conventions. Commit when done.") pid, outputFile, err := s.spawnAgent(ev.Agent, prompt, wsDir) if err != nil { - break + break } // Update status with real PID if st, serr := ReadStatus(wsDir); serr == nil { @@ -53,6 +54,15 @@ func (s *PrepSubsystem) HandleIPCEvents(c *core.Core, msg core.Message) core.Res return core.Result{OK: true} } +// SpawnFromQueue spawns an agent in a pre-prepped workspace. +// Called by runner.Service via ServiceFor interface matching. +// +// pid, err := prep.SpawnFromQueue("codex", prompt, wsDir) +func (s *PrepSubsystem) SpawnFromQueue(agent, prompt, wsDir string) (int, error) { + pid, _, err := s.spawnAgent(agent, prompt, wsDir) + return pid, err +} + // resolveWorkspace converts a workspace name back to the full path. // // resolveWorkspace("core/go-io/task-5") → "/Users/snider/Code/.core/workspace/core/go-io/task-5" diff --git a/pkg/runner/queue.go b/pkg/runner/queue.go index e0f3775..023e50e 100644 --- a/pkg/runner/queue.go +++ b/pkg/runner/queue.go @@ -7,7 +7,6 @@ import ( "syscall" "time" - "dappco.re/go/agent/pkg/messages" core "dappco.re/go/core" "gopkg.in/yaml.v3" ) @@ -261,18 +260,40 @@ func (s *Service) drainOne() bool { // Ask agentic to spawn — runner doesn't own the spawn logic, // just the gate. Send IPC to trigger the actual spawn. - if s.ServiceRuntime != nil { - s.Core().ACTION(messages.SpawnQueued{ - Workspace: core.PathBase(wsDir), - Agent: st.Agent, - Task: st.Task, - }) + // Workspace name is relative path from workspace root (e.g. "core/go-ai/dev") + wsRoot := WorkspaceRoot() + wsName := wsDir + if len(wsDir) > len(wsRoot)+1 { + wsName = wsDir[len(wsRoot)+1:] + } + core.Info("drainOne: found queued workspace", "workspace", wsName, "agent", st.Agent) + + // Spawn directly — agentic is a Core service, use ServiceFor to get it + if s.ServiceRuntime == nil { + continue + } + type spawner interface { + SpawnFromQueue(agent, prompt, wsDir string) (int, error) + } + prep, ok := core.ServiceFor[spawner](s.Core(), "agentic") + if !ok { + core.Error("drainOne: agentic service not found") + continue + } + prompt := core.Concat("TASK: ", st.Task, "\n\nResume from where you left off. Read CODEX.md for conventions. Commit when done.") + pid, err := prep.SpawnFromQueue(st.Agent, prompt, wsDir) + if err != nil { + core.Error("drainOne: spawn failed", "err", err) + continue } + // Only mark running AFTER successful spawn st.Status = "running" + st.PID = pid st.Runs++ WriteStatus(wsDir, st) - s.TrackWorkspace(core.PathBase(wsDir), st) + s.TrackWorkspace(wsName, st) + core.Info("drainOne: spawned", "pid", pid, "workspace", wsName) return true }