From 4bcc04d8900a4b9088110a707c4e61573ad9c3d3 Mon Sep 17 00:00:00 2001 From: Snider Date: Sun, 22 Mar 2026 15:14:14 +0000 Subject: [PATCH] feat(monitor): agent.started + agent.complete channel notifications MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - emitStartEvent fires when agent spawns (dispatch.go) - Monitor detects new "running" workspaces and pushes agent.started channel notification with repo and agent info - agent.complete already included blocked/failed status — no change - Both old and new workspace layouts supported Co-Authored-By: Virgil --- pkg/agentic/dispatch.go | 3 +++ pkg/agentic/events.go | 18 +++++++++++++----- pkg/monitor/monitor.go | 11 +++++++++++ 3 files changed, 27 insertions(+), 5 deletions(-) diff --git a/pkg/agentic/dispatch.go b/pkg/agentic/dispatch.go index 915e33f..f73b9d4 100644 --- a/pkg/agentic/dispatch.go +++ b/pkg/agentic/dispatch.go @@ -145,6 +145,9 @@ func (s *PrepSubsystem) spawnAgent(agent, prompt, wsDir string) (int, string, er proc.CloseStdin() pid := proc.Info().PID + // Emit start event for channel notifications + emitStartEvent(agent, core.PathBase(wsDir)) + go func() { ticker := time.NewTicker(5 * time.Second) defer ticker.Stop() diff --git a/pkg/agentic/events.go b/pkg/agentic/events.go index be02918..e549e84 100644 --- a/pkg/agentic/events.go +++ b/pkg/agentic/events.go @@ -22,14 +22,12 @@ type CompletionEvent struct { Timestamp string `json:"timestamp"` } -// emitCompletionEvent appends a completion event to the events log. -// The plugin's hook watches this file to notify the orchestrating agent. -// Status should be the actual terminal state: completed, failed, or blocked. -func emitCompletionEvent(agent, workspace, status string) { +// emitEvent appends an event to the events log. +func emitEvent(eventType, agent, workspace, status string) { eventsFile := core.JoinPath(WorkspaceRoot(), "events.jsonl") event := CompletionEvent{ - Type: "agent_completed", + Type: eventType, Agent: agent, Workspace: workspace, Status: status, @@ -50,3 +48,13 @@ func emitCompletionEvent(agent, workspace, status string) { defer wc.Close() wc.Write(append(data, '\n')) } + +// emitStartEvent logs that an agent has been spawned. +func emitStartEvent(agent, workspace string) { + emitEvent("agent_started", agent, workspace, "running") +} + +// emitCompletionEvent logs that an agent has finished. +func emitCompletionEvent(agent, workspace, status string) { + emitEvent("agent_completed", agent, workspace, status) +} diff --git a/pkg/monitor/monitor.go b/pkg/monitor/monitor.go index 4c8512b..1b4ab84 100644 --- a/pkg/monitor/monitor.go +++ b/pkg/monitor/monitor.go @@ -78,6 +78,7 @@ type Subsystem struct { // Track last seen state to only notify on changes lastCompletedCount int // completed workspaces seen on the last scan seenCompleted map[string]bool // workspace names we've already notified about + seenRunning map[string]bool // workspace names we've already sent start notification for completionsSeeded bool // true after first completions check lastInboxMaxID int // highest message ID seen inboxSeeded bool // true after first inbox check @@ -124,6 +125,7 @@ func New(opts ...Options) *Subsystem { interval: interval, poke: make(chan struct{}, 1), seenCompleted: make(map[string]bool), + seenRunning: make(map[string]bool), } } @@ -304,6 +306,15 @@ func (m *Subsystem) checkCompletions() string { } case "running": running++ + if !m.seenRunning[wsName] && seeded { + m.seenRunning[wsName] = true + if m.notifier != nil { + m.notifier.ChannelSend(context.Background(), "agent.started", map[string]any{ + "repo": st.Repo, + "agent": st.Agent, + }) + } + } case "queued": queued++ case "blocked", "failed":