feat(monitor): agent.started + agent.complete channel notifications

- 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 <virgil@lethean.io>
This commit is contained in:
Snider 2026-03-22 15:14:14 +00:00
parent abe5ef342a
commit 4bcc04d890
3 changed files with 27 additions and 5 deletions

View file

@ -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()

View file

@ -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)
}

View file

@ -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":