feat(session): read-time truncation only, preserve full history on disk

- summarizeSession: remove TruncateHistory(sessionKey, 4); keep SetSummary + Save
- forceCompression: return []providers.Message, do not SetHistory/Save; use for retry only
- runLLMIteration: use forceCompression return value for BuildMessages, no disk write
- Process: cap history to maxHistoryMessagesForContext (200) before BuildMessages
- session.Save: optional cap at maxStoredMessages (500) when writing to limit file size

Session file no longer truncated to last 4 messages; truncation happens only when
building context for the LLM (read-time), aligning with OpenClaw behavior.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
YS Liu 2026-02-25 22:55:01 +08:00
parent ec6da7a530
commit 95847bca97
2 changed files with 26 additions and 14 deletions

View file

@ -29,6 +29,10 @@ import (
"github.com/sipeed/picoclaw/pkg/utils" "github.com/sipeed/picoclaw/pkg/utils"
) )
// maxHistoryMessagesForContext is the maximum number of history messages to send to the LLM.
// Read-time cap only; session storage keeps full history.
const maxHistoryMessagesForContext = 200
type AgentLoop struct { type AgentLoop struct {
bus *bus.MessageBus bus *bus.MessageBus
cfg *config.Config cfg *config.Config
@ -413,6 +417,10 @@ func (al *AgentLoop) runAgentLoop(ctx context.Context, agent *AgentInstance, opt
if !opts.NoHistory { if !opts.NoHistory {
history = agent.Sessions.GetHistory(opts.SessionKey) history = agent.Sessions.GetHistory(opts.SessionKey)
summary = agent.Sessions.GetSummary(opts.SessionKey) summary = agent.Sessions.GetSummary(opts.SessionKey)
// Read-time cap: only the last N messages go to the LLM; session storage is unchanged.
if len(history) > maxHistoryMessagesForContext {
history = history[len(history)-maxHistoryMessagesForContext:]
}
} }
messages := agent.ContextBuilder.BuildMessages( messages := agent.ContextBuilder.BuildMessages(
history, history,
@ -573,11 +581,10 @@ func (al *AgentLoop) runLLMIteration(
}) })
} }
al.forceCompression(agent, opts.SessionKey) compressed := al.forceCompression(agent, opts.SessionKey)
newHistory := agent.Sessions.GetHistory(opts.SessionKey)
newSummary := agent.Sessions.GetSummary(opts.SessionKey) newSummary := agent.Sessions.GetSummary(opts.SessionKey)
messages = agent.ContextBuilder.BuildMessages( messages = agent.ContextBuilder.BuildMessages(
newHistory, newSummary, "", compressed, newSummary, "",
nil, opts.Channel, opts.ChatID, nil, opts.Channel, opts.ChatID,
) )
continue continue
@ -775,10 +782,11 @@ func (al *AgentLoop) maybeSummarize(agent *AgentInstance, sessionKey, channel, c
// forceCompression aggressively reduces context when the limit is hit. // forceCompression aggressively reduces context when the limit is hit.
// It drops the oldest 50% of messages (keeping system prompt and last user message). // It drops the oldest 50% of messages (keeping system prompt and last user message).
func (al *AgentLoop) forceCompression(agent *AgentInstance, sessionKey string) { // Returns the compressed history for use in the current request only; does not persist to session.
func (al *AgentLoop) forceCompression(agent *AgentInstance, sessionKey string) []providers.Message {
history := agent.Sessions.GetHistory(sessionKey) history := agent.Sessions.GetHistory(sessionKey)
if len(history) <= 4 { if len(history) <= 4 {
return return history
} }
// Keep system prompt (usually [0]) and the very last message (user's trigger) // Keep system prompt (usually [0]) and the very last message (user's trigger)
@ -786,7 +794,7 @@ func (al *AgentLoop) forceCompression(agent *AgentInstance, sessionKey string) {
// Assuming [0] is system, [1:] is conversation // Assuming [0] is system, [1:] is conversation
conversation := history[1 : len(history)-1] conversation := history[1 : len(history)-1]
if len(conversation) == 0 { if len(conversation) == 0 {
return return history
} }
// Helper to find the mid-point of the conversation // Helper to find the mid-point of the conversation
@ -815,15 +823,12 @@ func (al *AgentLoop) forceCompression(agent *AgentInstance, sessionKey string) {
newHistory = append(newHistory, keptConversation...) newHistory = append(newHistory, keptConversation...)
newHistory = append(newHistory, history[len(history)-1]) // Last message newHistory = append(newHistory, history[len(history)-1]) // Last message
// Update session logger.WarnCF("agent", "Forced compression executed (in-memory only, not persisted)", map[string]any{
agent.Sessions.SetHistory(sessionKey, newHistory)
agent.Sessions.Save(sessionKey)
logger.WarnCF("agent", "Forced compression executed", map[string]any{
"session_key": sessionKey, "session_key": sessionKey,
"dropped_msgs": droppedCount, "dropped_msgs": droppedCount,
"new_count": len(newHistory), "new_count": len(newHistory),
}) })
return newHistory
} }
// GetStartupInfo returns information about loaded tools and skills for logging. // GetStartupInfo returns information about loaded tools and skills for logging.
@ -981,7 +986,6 @@ func (al *AgentLoop) summarizeSession(agent *AgentInstance, sessionKey string) {
if finalSummary != "" { if finalSummary != "" {
agent.Sessions.SetSummary(sessionKey, finalSummary) agent.Sessions.SetSummary(sessionKey, finalSummary)
agent.Sessions.TruncateHistory(sessionKey, 4)
agent.Sessions.Save(sessionKey) agent.Sessions.Save(sessionKey)
} }
} }

View file

@ -11,6 +11,10 @@ import (
"github.com/sipeed/picoclaw/pkg/providers" "github.com/sipeed/picoclaw/pkg/providers"
) )
// maxStoredMessages is the maximum number of messages to persist per session (safety cap to limit file size).
// Older messages are dropped only at write time; in-memory session may hold more until next Save.
const maxStoredMessages = 500
type Session struct { type Session struct {
Key string `json:"key"` Key string `json:"key"`
Messages []providers.Message `json:"messages"` Messages []providers.Message `json:"messages"`
@ -184,8 +188,12 @@ func (sm *SessionManager) Save(key string) error {
Updated: stored.Updated, Updated: stored.Updated,
} }
if len(stored.Messages) > 0 { if len(stored.Messages) > 0 {
snapshot.Messages = make([]providers.Message, len(stored.Messages)) toStore := stored.Messages
copy(snapshot.Messages, stored.Messages) if len(toStore) > maxStoredMessages {
toStore = toStore[len(toStore)-maxStoredMessages:]
}
snapshot.Messages = make([]providers.Message, len(toStore))
copy(snapshot.Messages, toStore)
} else { } else {
snapshot.Messages = []providers.Message{} snapshot.Messages = []providers.Message{}
} }