fix: suppress duplicate heartbeat notifications with 24h TTL
Prevent heartbeat from spamming the user with repeated interview/review messages. After sending a non-silent result, further notifications are suppressed for 24 hours. Suppression resets when a real user message arrives via OnUserMessage callback. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
dcbb88066b
commit
666f56d2ee
3 changed files with 41 additions and 8 deletions
|
|
@ -123,6 +123,9 @@ func gatewayCmd() {
|
||||||
return tools.SilentResult(response)
|
return tools.SilentResult(response)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Reset heartbeat suppression when a real user message arrives
|
||||||
|
agentLoop.OnUserMessage = heartbeatService.ResetSuppression
|
||||||
|
|
||||||
channelManager, err := channels.NewManager(cfg, msgBus)
|
channelManager, err := channels.NewManager(cfg, msgBus)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Printf("Error creating channel manager: %v\n", err)
|
fmt.Printf("Error creating channel manager: %v\n", err)
|
||||||
|
|
|
||||||
|
|
@ -91,6 +91,7 @@ type AgentLoop struct {
|
||||||
activeTasks sync.Map // sessionKey → *activeTask
|
activeTasks sync.Map // sessionKey → *activeTask
|
||||||
sessions *SessionTracker
|
sessions *SessionTracker
|
||||||
OnStateChange func() // called on plan/session/skills mutations
|
OnStateChange func() // called on plan/session/skills mutations
|
||||||
|
OnUserMessage func() // called when a real user message is processed
|
||||||
}
|
}
|
||||||
|
|
||||||
// processOptions configures how a message is processed
|
// processOptions configures how a message is processed
|
||||||
|
|
@ -533,6 +534,11 @@ func (al *AgentLoop) processMessage(ctx context.Context, msg bus.InboundMessage)
|
||||||
return al.processSystemMessage(ctx, msg)
|
return al.processSystemMessage(ctx, msg)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Notify listeners that a real user message arrived (e.g. reset heartbeat suppression)
|
||||||
|
if al.OnUserMessage != nil {
|
||||||
|
al.OnUserMessage()
|
||||||
|
}
|
||||||
|
|
||||||
// Expand /skill command: inject SKILL.md content into message, then continue to LLM
|
// Expand /skill command: inject SKILL.md content into message, then continue to LLM
|
||||||
var expansionCompact string
|
var expansionCompact string
|
||||||
if expanded, compact, ok := al.expandSkillCommand(msg); ok {
|
if expanded, compact, ok := al.expandSkillCommand(msg); ok {
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,7 @@ import (
|
||||||
const (
|
const (
|
||||||
minIntervalMinutes = 5
|
minIntervalMinutes = 5
|
||||||
defaultIntervalMinutes = 30
|
defaultIntervalMinutes = 30
|
||||||
|
suppressionTTL = 24 * time.Hour
|
||||||
)
|
)
|
||||||
|
|
||||||
// HeartbeatHandler is the function type for handling heartbeat.
|
// HeartbeatHandler is the function type for handling heartbeat.
|
||||||
|
|
@ -41,6 +42,7 @@ type HeartbeatService struct {
|
||||||
enabled bool
|
enabled bool
|
||||||
mu sync.RWMutex
|
mu sync.RWMutex
|
||||||
stopChan chan struct{}
|
stopChan chan struct{}
|
||||||
|
lastNotifiedAt time.Time // when a non-silent result was last sent to user
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewHeartbeatService creates a new heartbeat service
|
// NewHeartbeatService creates a new heartbeat service
|
||||||
|
|
@ -76,6 +78,15 @@ func (hs *HeartbeatService) SetHandler(handler HeartbeatHandler) {
|
||||||
hs.handler = handler
|
hs.handler = handler
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ResetSuppression clears the notification suppression so the next
|
||||||
|
// non-silent heartbeat result will be delivered to the user again.
|
||||||
|
// Typically called when a user message arrives.
|
||||||
|
func (hs *HeartbeatService) ResetSuppression() {
|
||||||
|
hs.mu.Lock()
|
||||||
|
defer hs.mu.Unlock()
|
||||||
|
hs.lastNotifiedAt = time.Time{}
|
||||||
|
}
|
||||||
|
|
||||||
// Start begins the heartbeat service
|
// Start begins the heartbeat service
|
||||||
func (hs *HeartbeatService) Start() error {
|
func (hs *HeartbeatService) Start() error {
|
||||||
hs.mu.Lock()
|
hs.mu.Lock()
|
||||||
|
|
@ -205,6 +216,15 @@ func (hs *HeartbeatService) executeHeartbeat() {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Suppress duplicate notifications within the TTL window
|
||||||
|
hs.mu.RLock()
|
||||||
|
suppressed := !hs.lastNotifiedAt.IsZero() && time.Since(hs.lastNotifiedAt) < suppressionTTL
|
||||||
|
hs.mu.RUnlock()
|
||||||
|
if suppressed {
|
||||||
|
hs.logInfo("Heartbeat suppressed (already notified user recently)")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// Send result to user
|
// Send result to user
|
||||||
if result.ForUser != "" {
|
if result.ForUser != "" {
|
||||||
hs.sendResponse(result.ForUser)
|
hs.sendResponse(result.ForUser)
|
||||||
|
|
@ -212,6 +232,10 @@ func (hs *HeartbeatService) executeHeartbeat() {
|
||||||
hs.sendResponse(result.ForLLM)
|
hs.sendResponse(result.ForLLM)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
hs.mu.Lock()
|
||||||
|
hs.lastNotifiedAt = time.Now()
|
||||||
|
hs.mu.Unlock()
|
||||||
|
|
||||||
hs.logInfo("Heartbeat completed: %s", result.ForLLM)
|
hs.logInfo("Heartbeat completed: %s", result.ForLLM)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue