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).
This commit is contained in:
Dmitrii Balabanov 2026-03-31 19:29:39 +03:00
parent 841677adca
commit fb7d3a375e

View file

@ -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) { func (al *AgentLoop) runTurn(ctx context.Context, ts *turnState) (turnResult, error) {
turnCtx, turnCancel := context.WithCancel(ctx) turnCtx, turnCancel := context.WithCancel(ctx)
defer turnCancel() 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 // Inject pending undelivered assistant reply into history so regular inbound turns
// see it in LLM context even before OnDelivered persists it to session storage. // 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 !ts.opts.NoHistory {
if pendingRaw, ok := al.pendingDeliveries.Load(ts.sessionKey); ok { history = al.injectPendingDelivery(ts.sessionKey, history)
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})
}
}
}
} }
messages := ts.agent.ContextBuilder.BuildMessages( messages := ts.agent.ContextBuilder.BuildMessages(
@ -1925,6 +1932,7 @@ func (al *AgentLoop) runTurn(ctx context.Context, ts *turnState) (turnResult, er
history = resp.History history = resp.History
summary = resp.Summary summary = resp.Summary
} }
history = al.injectPendingDelivery(ts.sessionKey, history)
messages = ts.agent.ContextBuilder.BuildMessages( messages = ts.agent.ContextBuilder.BuildMessages(
history, summary, ts.userMessage, history, summary, ts.userMessage,
ts.media, ts.channel, ts.chatID, ts.media, ts.channel, ts.chatID,
@ -2316,6 +2324,7 @@ turnLoop:
history = asmResp.History history = asmResp.History
summary = asmResp.Summary summary = asmResp.Summary
} }
history = al.injectPendingDelivery(ts.sessionKey, history)
messages = ts.agent.ContextBuilder.BuildMessages( messages = ts.agent.ContextBuilder.BuildMessages(
history, summary, "", history, summary, "",
nil, ts.channel, ts.chatID, ts.opts.SenderID, ts.opts.SenderDisplayName, nil, ts.channel, ts.chatID, ts.opts.SenderID, ts.opts.SenderDisplayName,