From 942c3d3f6e539cf57d35baa60a1c0c90256ed5fb Mon Sep 17 00:00:00 2001 From: Dmitrii Balabanov Date: Tue, 31 Mar 2026 17:26:04 +0300 Subject: [PATCH] fix: inject pending assistant reply into inbound turns and fill ReplyToMessageID Gap 1: regular inbound turns now see the previous assistant reply in LLM context even when OnDelivered hasn't fired yet (fast follow-up scenario). Achieved by loading from pendingDeliveries sync.Map after captureRestorePoint and appending to history before BuildMessages. Gap 2: HandleMessage in BaseChannel now fills ReplyToMessageID from metadata so the first-class struct field is populated for all channels that set the key in metadata (e.g. Telegram). --- pkg/agent/loop.go | 44 ++++++++++++++++++++++++++++++++------------ pkg/channels/base.go | 21 +++++++++++---------- 2 files changed, 43 insertions(+), 22 deletions(-) diff --git a/pkg/agent/loop.go b/pkg/agent/loop.go index 75572db4b..f9a88acff 100644 --- a/pkg/agent/loop.go +++ b/pkg/agent/loop.go @@ -47,18 +47,19 @@ type AgentLoop struct { hooks *HookManager // Runtime state - running atomic.Bool - contextManager ContextManager - fallback *providers.FallbackChain - channelManager *channels.Manager - mediaStore media.MediaStore - transcriber asr.Transcriber - cmdRegistry *commands.Registry - mcp mcpRuntime - hookRuntime hookRuntime - steering *steeringQueue - pendingSkills sync.Map - mu sync.RWMutex + running atomic.Bool + contextManager ContextManager + fallback *providers.FallbackChain + channelManager *channels.Manager + mediaStore media.MediaStore + transcriber asr.Transcriber + cmdRegistry *commands.Registry + mcp mcpRuntime + hookRuntime hookRuntime + steering *steeringQueue + pendingSkills sync.Map + pendingDeliveries sync.Map // sessionKey -> string: last undelivered assistant content + mu sync.RWMutex // Concurrent turn management (from HEAD) activeTurnStates sync.Map // key: sessionKey (string), value: *turnState @@ -1697,9 +1698,14 @@ func (al *AgentLoop) runAgentLoop( } if !opts.NoHistory && result.finalContent != "" { + // Register the pending content so that fast follow-up inbound turns can + // inject it into their LLM context before OnDelivered persists it. + al.pendingDeliveries.Store(opts.SessionKey, result.finalContent) + var once sync.Once response.OnDelivered = func(msgIDs []string) { once.Do(func() { + al.pendingDeliveries.Delete(opts.SessionKey) assistantMsg := providers.Message{ Role: "assistant", Content: result.finalContent, @@ -1843,6 +1849,20 @@ func (al *AgentLoop) runTurn(ctx context.Context, ts *turnState) (turnResult, er } ts.captureRestorePoint(history, summary) + // Inject pending undelivered assistant reply into history so regular inbound turns + // see it in LLM context even before OnDelivered persists it to session storage. + // Only inject if history doesn't already end with an assistant message (avoids duplication + // when OnDelivered fires before the next turn starts). + if !ts.opts.NoHistory { + if pendingRaw, ok := al.pendingDeliveries.Load(ts.sessionKey); ok { + if pendingContent, _ := pendingRaw.(string); pendingContent != "" { + if len(history) == 0 || history[len(history)-1].Role != "assistant" { + history = append(history, providers.Message{Role: "assistant", Content: pendingContent}) + } + } + } + } + messages := ts.agent.ContextBuilder.BuildMessages( history, summary, diff --git a/pkg/channels/base.go b/pkg/channels/base.go index bd4ced849..e67bcd39b 100644 --- a/pkg/channels/base.go +++ b/pkg/channels/base.go @@ -276,16 +276,17 @@ func (c *BaseChannel) HandleMessage( scope := BuildMediaScope(c.name, chatID, messageID) msg := bus.InboundMessage{ - Channel: c.name, - SenderID: resolvedSenderID, - Sender: sender, - ChatID: chatID, - Content: content, - Media: media, - Peer: peer, - MessageID: messageID, - MediaScope: scope, - Metadata: metadata, + Channel: c.name, + SenderID: resolvedSenderID, + Sender: sender, + ChatID: chatID, + Content: content, + Media: media, + Peer: peer, + MessageID: messageID, + MediaScope: scope, + Metadata: metadata, + ReplyToMessageID: metadata["reply_to_message_id"], } // Auto-trigger typing indicator, message reaction, and placeholder before publishing.