From 36dc76cce1dd8316b3ba9c02560e17cbd4dab0d8 Mon Sep 17 00:00:00 2001 From: Snider Date: Sat, 21 Mar 2026 19:31:11 +0000 Subject: [PATCH] feat(monitor): ID-based inbox detection + channels fully working Track inbox by highest message ID instead of unread count. Fixes: - API pagination limit (max 20) no longer causes missed notifications - Restart no longer floods with all existing unread messages (seeded) - Each new message fires exactly once regardless of read state Added MONITOR_INTERVAL env override and debugChannel helper for faster iteration during channel development. All three channel types confirmed working: - agent.complete: workspace status changes - inbox.message: new messages by ID tracking - monitor.debug: real-time debug trace Co-Authored-By: Virgil --- pkg/monitor/monitor.go | 33 ++++++++++++++++++++++++--------- 1 file changed, 24 insertions(+), 9 deletions(-) diff --git a/pkg/monitor/monitor.go b/pkg/monitor/monitor.go index 2cdaeca..174efdd 100644 --- a/pkg/monitor/monitor.go +++ b/pkg/monitor/monitor.go @@ -42,8 +42,9 @@ type Subsystem struct { wg sync.WaitGroup // Track last seen state to only notify on changes - seenCompleted map[string]bool // workspace names we've already notified about - lastInboxCount int + seenCompleted map[string]bool // workspace names we've already notified about + lastInboxMaxID int // highest message ID seen + inboxSeeded bool // true after first inbox check (suppresses initial flood) lastSyncTimestamp int64 mu sync.Mutex @@ -316,6 +317,7 @@ func (m *Subsystem) checkInbox() string { var resp struct { Data []struct { + ID int `json:"id"` Read bool `json:"read"` From string `json:"from"` Subject string `json:"subject"` @@ -326,12 +328,15 @@ func (m *Subsystem) checkInbox() string { return "" } - m.debugChannel(fmt.Sprintf("checkInbox: got %d messages", len(resp.Data))) - + // Find max ID and count unread + maxID := 0 unread := 0 senders := make(map[string]int) latestSubject := "" for _, msg := range resp.Data { + if msg.ID > maxID { + maxID = msg.ID + } if !msg.Read { unread++ if msg.From != "" { @@ -344,11 +349,21 @@ func (m *Subsystem) checkInbox() string { } m.mu.Lock() - prevInbox := m.lastInboxCount - m.lastInboxCount = unread + prevMaxID := m.lastInboxMaxID + seeded := m.inboxSeeded + m.lastInboxMaxID = maxID + m.inboxSeeded = true m.mu.Unlock() - if unread <= 0 || unread == prevInbox { + m.debugChannel(fmt.Sprintf("checkInbox: unread=%d, maxID=%d, prevMaxID=%d", unread, maxID, prevMaxID)) + + // First check after startup: seed, don't fire + if !seeded { + return "" + } + + // Only fire if there are new messages (higher ID than last seen) + if maxID <= prevMaxID || unread == 0 { return "" } @@ -362,10 +377,10 @@ func (m *Subsystem) checkInbox() string { } } // Push channel event for new messages + newCount := maxID - prevMaxID if m.notifier != nil { - fmt.Fprintf(os.Stderr, "monitor: pushing inbox.message channel event (new=%d)\n", unread-prevInbox) m.notifier.ChannelSend(context.Background(), "inbox.message", map[string]any{ - "new": unread - prevInbox, + "new": newCount, "total": unread, "senders": senderList, "subject": latestSubject,