From fb7d3a375e1b305befb6d6488bb8d06541c3f0d3 Mon Sep 17 00:00:00 2001 From: Dmitrii Balabanov Date: Tue, 31 Mar 2026 19:29:39 +0300 Subject: [PATCH] fix: re-inject pending delivery into history after compression/retry rebuilds After proactive-compression or context-error retry, messages is rebuilt from session storage via GetHistory, which doesn't include the still-undelivered assistant reply. The initial injection before BuildMessages was skipped in both paths, so long sessions hitting compression or LLM context errors lost the last assistant turn from LLM context. Fix: extract injectPendingDelivery helper and call it at all three history rebuild sites (initial, proactive-compression, context-error-retry). --- pkg/agent/loop.go | 27 ++++++++++++++++++--------- 1 file changed, 18 insertions(+), 9 deletions(-) diff --git a/pkg/agent/loop.go b/pkg/agent/loop.go index 30037b069..10d0d8a1b 100644 --- a/pkg/agent/loop.go +++ b/pkg/agent/loop.go @@ -1819,6 +1819,21 @@ func (al *AgentLoop) handleReasoning( } } +// injectPendingDelivery appends the last undelivered assistant reply to history +// when the slot is occupied and history doesn't already end with an assistant message. +// Must be called after every GetHistory so that proactive-compression and +// context-error-retry rebuilds do not lose the pending content. +func (al *AgentLoop) injectPendingDelivery(sessionKey string, history []providers.Message) []providers.Message { + if pendingRaw, ok := al.pendingDeliveries.Load(sessionKey); ok { + if pd, ok := pendingRaw.(pendingDelivery); ok && pd.content != "" { + if len(history) == 0 || history[len(history)-1].Role != "assistant" { + return append(history, providers.Message{Role: "assistant", Content: pd.content}) + } + } + } + return history +} + func (al *AgentLoop) runTurn(ctx context.Context, ts *turnState) (turnResult, error) { turnCtx, turnCancel := context.WithCancel(ctx) defer turnCancel() @@ -1873,16 +1888,8 @@ func (al *AgentLoop) runTurn(ctx context.Context, ts *turnState) (turnResult, er // 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 pd, ok := pendingRaw.(pendingDelivery); ok && pd.content != "" { - if len(history) == 0 || history[len(history)-1].Role != "assistant" { - history = append(history, providers.Message{Role: "assistant", Content: pd.content}) - } - } - } + history = al.injectPendingDelivery(ts.sessionKey, history) } messages := ts.agent.ContextBuilder.BuildMessages( @@ -1925,6 +1932,7 @@ func (al *AgentLoop) runTurn(ctx context.Context, ts *turnState) (turnResult, er history = resp.History summary = resp.Summary } + history = al.injectPendingDelivery(ts.sessionKey, history) messages = ts.agent.ContextBuilder.BuildMessages( history, summary, ts.userMessage, ts.media, ts.channel, ts.chatID, @@ -2316,6 +2324,7 @@ turnLoop: history = asmResp.History summary = asmResp.Summary } + history = al.injectPendingDelivery(ts.sessionKey, history) messages = ts.agent.ContextBuilder.BuildMessages( history, summary, "", nil, ts.channel, ts.chatID, ts.opts.SenderID, ts.opts.SenderDisplayName,