fix: prevent premature memory threshold by tuning summarization params
The "Memory threshold reached" notification was triggering too early, often after just 5 user interactions. Root causes: - Message threshold was 20 (only ~5 interactions with tool calls) → raised to 40 - Token threshold was 75% of context window → raised to 85% - Token estimation used 2.5 chars/token (too aggressive) → changed to 3.5 chars/token - Post-summarization only kept 4 messages → now keeps 8 - Force compression dropped 50% of messages → now drops 35% - Removed the user-facing "⚠️ Memory threshold" notification (now silent background operation) https://claude.ai/code/session_01MYemTMPtHrcgidWs8UdjcG
This commit is contained in:
parent
8c2fa468d2
commit
42be3dec37
1 changed files with 32 additions and 36 deletions
|
|
@ -728,56 +728,48 @@ func (al *AgentLoop) updateToolContexts(channel, chatID string) {
|
|||
func (al *AgentLoop) maybeSummarize(sessionKey, channel, chatID string) {
|
||||
newHistory := al.sessions.GetHistory(sessionKey)
|
||||
tokenEstimate := al.estimateTokens(newHistory)
|
||||
threshold := al.contextWindow * 75 / 100
|
||||
threshold := al.contextWindow * 85 / 100
|
||||
|
||||
if len(newHistory) > 20 || tokenEstimate > threshold {
|
||||
// Use message count threshold of 40 (tool calls mean ~3-5 messages per user interaction,
|
||||
// so 40 messages ≈ 8-13 real interactions before summarization triggers).
|
||||
if len(newHistory) > 40 || tokenEstimate > threshold {
|
||||
if _, loading := al.summarizing.LoadOrStore(sessionKey, true); !loading {
|
||||
go func() {
|
||||
defer al.summarizing.Delete(sessionKey)
|
||||
// Notify user about optimization if not an internal channel
|
||||
if !constants.IsInternalChannel(channel) {
|
||||
al.bus.PublishOutbound(bus.OutboundMessage{
|
||||
Channel: channel,
|
||||
ChatID: chatID,
|
||||
Content: "⚠️ Memory threshold reached. Optimizing conversation history...",
|
||||
})
|
||||
}
|
||||
al.summarizeSession(sessionKey)
|
||||
logger.InfoCF("agent", "Session summarized in background", map[string]interface{}{
|
||||
"session_key": sessionKey,
|
||||
"history_count": len(newHistory),
|
||||
"token_estimate": tokenEstimate,
|
||||
})
|
||||
}()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// forceCompression aggressively reduces context when the limit is hit.
|
||||
// It drops the oldest 50% of messages (keeping system prompt and last user message).
|
||||
// forceCompression reduces context when the limit is hit.
|
||||
// It drops the oldest 35% of messages (keeping system prompt, recent conversation, and last user message).
|
||||
func (al *AgentLoop) forceCompression(sessionKey string) {
|
||||
history := al.sessions.GetHistory(sessionKey)
|
||||
if len(history) <= 4 {
|
||||
if len(history) <= 6 {
|
||||
return
|
||||
}
|
||||
|
||||
// Keep system prompt (usually [0]) and the very last message (user's trigger)
|
||||
// We want to drop the oldest half of the *conversation*
|
||||
// Assuming [0] is system, [1:] is conversation
|
||||
// Drop the oldest 35% of conversation to preserve more recent context.
|
||||
conversation := history[1 : len(history)-1]
|
||||
if len(conversation) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
// Helper to find the mid-point of the conversation
|
||||
mid := len(conversation) / 2
|
||||
// Drop the oldest 35% of conversation
|
||||
dropCount := len(conversation) * 35 / 100
|
||||
if dropCount < 1 {
|
||||
dropCount = 1
|
||||
}
|
||||
|
||||
// New history structure:
|
||||
// 1. System Prompt
|
||||
// 2. [Summary of dropped part] - synthesized
|
||||
// 3. Second half of conversation
|
||||
// 4. Last message
|
||||
|
||||
// Simplified approach for emergency: Drop first half of conversation
|
||||
// and rely on existing summary if present, or create a placeholder.
|
||||
|
||||
droppedCount := mid
|
||||
keptConversation := conversation[mid:]
|
||||
droppedCount := dropCount
|
||||
keptConversation := conversation[dropCount:]
|
||||
|
||||
newHistory := make([]providers.Message, 0)
|
||||
newHistory = append(newHistory, history[0]) // System prompt
|
||||
|
|
@ -884,12 +876,13 @@ func (al *AgentLoop) summarizeSession(sessionKey string) {
|
|||
history := al.sessions.GetHistory(sessionKey)
|
||||
summary := al.sessions.GetSummary(sessionKey)
|
||||
|
||||
// Keep last 4 messages for continuity
|
||||
if len(history) <= 4 {
|
||||
// Keep last 8 messages for continuity (provides ~2-3 recent interactions of context)
|
||||
keepCount := 8
|
||||
if len(history) <= keepCount {
|
||||
return
|
||||
}
|
||||
|
||||
toSummarize := history[:len(history)-4]
|
||||
toSummarize := history[:len(history)-keepCount]
|
||||
|
||||
// Oversized Message Guard
|
||||
// Skip messages larger than 50% of context window to prevent summarizer overflow
|
||||
|
|
@ -946,7 +939,7 @@ func (al *AgentLoop) summarizeSession(sessionKey string) {
|
|||
|
||||
if finalSummary != "" {
|
||||
al.sessions.SetSummary(sessionKey, finalSummary)
|
||||
al.sessions.TruncateHistory(sessionKey, 4)
|
||||
al.sessions.TruncateHistory(sessionKey, 8)
|
||||
al.sessions.Save(sessionKey)
|
||||
}
|
||||
}
|
||||
|
|
@ -973,15 +966,18 @@ func (al *AgentLoop) summarizeBatch(ctx context.Context, batch []providers.Messa
|
|||
}
|
||||
|
||||
// estimateTokens estimates the number of tokens in a message list.
|
||||
// Uses a safe heuristic of 2.5 characters per token to account for CJK and other
|
||||
// overheads better than the previous 3 chars/token.
|
||||
// Uses ~3.5 characters per token as a balanced heuristic:
|
||||
// - English text averages ~4 chars/token
|
||||
// - CJK text averages ~1.5-2 chars/token
|
||||
// - Mixed content (code, URLs, tool results) averages ~3-4 chars/token
|
||||
// The previous 2.5 ratio was too aggressive and triggered summarization too early.
|
||||
func (al *AgentLoop) estimateTokens(messages []providers.Message) int {
|
||||
totalChars := 0
|
||||
for _, m := range messages {
|
||||
totalChars += utf8.RuneCountInString(m.Content)
|
||||
}
|
||||
// 2.5 chars per token = totalChars * 2 / 5
|
||||
return totalChars * 2 / 5
|
||||
// 3.5 chars per token = totalChars * 2 / 7
|
||||
return totalChars * 2 / 7
|
||||
}
|
||||
|
||||
func (al *AgentLoop) handleCommand(ctx context.Context, msg bus.InboundMessage) (string, bool) {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue