fix: background task notifications not reaching Telegram on startup

The isBackgroundTask check was using constants.IsInternalChannel(opts.Channel),
but heartbeat passes the actual user channel (e.g. "telegram") directly, not an
internal channel. This made isBackgroundTask always false for heartbeat tasks.

Now uses an explicit Background flag on processOptions, set by ProcessHeartbeat
and ProcessDirectWithChannel (via metadata). Also handles the case where the
channel is already a real channel (no need to resolve from state).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
dj-oyu 2026-02-22 02:57:13 +09:00
parent f59fb3f94d
commit 425360715a

View file

@ -95,6 +95,7 @@ type processOptions struct {
SendResponse bool // Whether to send response via bus SendResponse bool // Whether to send response via bus
NoHistory bool // If true, don't load session history (for heartbeat) NoHistory bool // If true, don't load session history (for heartbeat)
TaskID string // Unique task ID for background task status tracking TaskID string // Unique task ID for background task status tracking
Background bool // If true, this is a background task (cron/heartbeat) — enables live task notifications
} }
func NewAgentLoop(cfg *config.Config, msgBus *bus.MessageBus, provider providers.LLMProvider, enableStats ...bool) *AgentLoop { func NewAgentLoop(cfg *config.Config, msgBus *bus.MessageBus, provider providers.LLMProvider, enableStats ...bool) *AgentLoop {
@ -413,6 +414,7 @@ func (al *AgentLoop) ProcessHeartbeat(ctx context.Context, content, channel, cha
EnableSummary: false, EnableSummary: false,
SendResponse: false, SendResponse: false,
NoHistory: true, // Don't load session history for heartbeat NoHistory: true, // Don't load session history for heartbeat
Background: true, // Enable live task notifications on Telegram
}) })
} }
@ -530,6 +532,7 @@ func (al *AgentLoop) processMessage(ctx context.Context, msg bus.InboundMessage)
DefaultResponse: "I've completed processing but have no response to give.", DefaultResponse: "I've completed processing but have no response to give.",
EnableSummary: true, EnableSummary: true,
SendResponse: false, SendResponse: false,
Background: msg.Metadata["background"] == "true",
}) })
} }
@ -630,18 +633,25 @@ func (al *AgentLoop) runAgentLoop(ctx context.Context, agent *AgentInstance, opt
interrupt: make(chan string, 1), interrupt: make(chan string, 1),
} }
// For background tasks (cron/cli), generate a TaskID and resolve notification channel // For background tasks (cron/heartbeat), generate a TaskID and send notification
isBackgroundTask := constants.IsInternalChannel(opts.Channel) && al.state != nil isBackgroundTask := opts.Background && al.state != nil
if isBackgroundTask && opts.TaskID == "" { if isBackgroundTask && opts.TaskID == "" {
opts.TaskID = fmt.Sprintf("task-%s-%d", opts.SessionKey, time.Now().UnixMilli()) opts.TaskID = fmt.Sprintf("task-%s-%d", opts.SessionKey, time.Now().UnixMilli())
// Resolve user's last active channel for notifications // Determine notification channel: use opts.Channel if already a real channel,
// otherwise resolve from last active channel
notifyChannel := opts.Channel
notifyChatID := opts.ChatID
if constants.IsInternalChannel(notifyChannel) || notifyChannel == "" {
if lastChannel := al.state.GetLastChannel(); lastChannel != "" { if lastChannel := al.state.GetLastChannel(); lastChannel != "" {
// lastChannel format: "channel:chatID"
if idx := strings.Index(lastChannel, ":"); idx > 0 { if idx := strings.Index(lastChannel, ":"); idx > 0 {
notifyChannel := lastChannel[:idx] notifyChannel = lastChannel[:idx]
notifyChatID := lastChannel[idx+1:] notifyChatID = lastChannel[idx+1:]
}
}
}
if notifyChannel != "" && notifyChatID != "" && !constants.IsInternalChannel(notifyChannel) {
// Override opts channel/chatID for status updates // Override opts channel/chatID for status updates
opts.Channel = notifyChannel opts.Channel = notifyChannel
opts.ChatID = notifyChatID opts.ChatID = notifyChatID
@ -656,7 +666,6 @@ func (al *AgentLoop) runAgentLoop(ctx context.Context, agent *AgentInstance, opt
}) })
} }
} }
}
// Use TaskID as key if available (for background tasks), else sessionKey // Use TaskID as key if available (for background tasks), else sessionKey
taskKey := opts.SessionKey taskKey := opts.SessionKey
@ -668,7 +677,7 @@ func (al *AgentLoop) runAgentLoop(ctx context.Context, agent *AgentInstance, opt
al.activeTasks.Delete(taskKey) al.activeTasks.Delete(taskKey)
// Publish final task status on completion for background tasks // Publish final task status on completion for background tasks
if opts.TaskID != "" && !constants.IsInternalChannel(opts.Channel) { if opts.TaskID != "" {
elapsed := time.Since(task.StartedAt) elapsed := time.Since(task.StartedAt)
al.bus.PublishOutbound(bus.OutboundMessage{ al.bus.PublishOutbound(bus.OutboundMessage{
Channel: opts.Channel, Channel: opts.Channel,