From 19d849aa751ed688bab297858de18802c1cd884c Mon Sep 17 00:00:00 2001 From: Snider Date: Thu, 26 Mar 2026 07:16:48 +0000 Subject: [PATCH] =?UTF-8?q?fix:=20use=20Core=20PerformAsync=20for=20agent?= =?UTF-8?q?=20monitoring=20=E2=80=94=20app=20stays=20alive?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The monitoring goroutine that waits for agent completion was a raw `go func()` that Core didn't know about. ServiceShutdown killed it immediately on CLI exit. Now uses PerformAsync which registers with Core's WaitGroup: - ServiceShutdown waits for all async tasks to drain - `core-agent workspace dispatch` blocks until agent completes - Agent lifecycle properly tracked by the framework Also whitelist agentic.monitor.* and agentic.complete in entitlement checker. Co-Authored-By: Virgil --- pkg/agentic/dispatch.go | 9 +++++++-- pkg/agentic/prep.go | 5 ++++- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/pkg/agentic/dispatch.go b/pkg/agentic/dispatch.go index 8666d2b..5018614 100644 --- a/pkg/agentic/dispatch.go +++ b/pkg/agentic/dispatch.go @@ -383,11 +383,16 @@ func (s *PrepSubsystem) spawnAgent(agent, prompt, wsDir string) (int, string, er s.broadcastStart(agent, wsDir) s.startIssueTracking(wsDir) - go func() { + // Register a one-shot Action that monitors this agent, then run it via PerformAsync. + // PerformAsync tracks it in Core's WaitGroup — ServiceShutdown waits for it. + monitorAction := core.Concat("agentic.monitor.", core.PathBase(wsDir)) + s.Core().Action(monitorAction, func(_ context.Context, _ core.Options) core.Result { <-proc.Done() s.onAgentComplete(agent, wsDir, outputFile, proc.Info().ExitCode, string(proc.Info().Status), proc.Output()) - }() + return core.Result{OK: true} + }) + s.Core().PerformAsync(monitorAction, core.NewOptions()) return pid, outputFile, nil } diff --git a/pkg/agentic/prep.go b/pkg/agentic/prep.go index c800388..6d14f13 100644 --- a/pkg/agentic/prep.go +++ b/pkg/agentic/prep.go @@ -105,7 +105,10 @@ func (s *PrepSubsystem) OnStartup(ctx context.Context) core.Result { if !core.HasPrefix(action, "agentic.") { return core.Entitlement{Allowed: true, Unlimited: true} } - // Read-only actions always allowed + // Read-only + internal actions always allowed + if core.HasPrefix(action, "agentic.monitor.") || core.HasPrefix(action, "agentic.complete") { + return core.Entitlement{Allowed: true, Unlimited: true} + } switch action { case "agentic.status", "agentic.scan", "agentic.watch", "agentic.issue.get", "agentic.issue.list", "agentic.pr.get", "agentic.pr.list",