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"
)
// 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 {
bus *bus.MessageBus
cfg *config.Config
@ -413,6 +417,10 @@ func (al *AgentLoop) runAgentLoop(ctx context.Context, agent *AgentInstance, opt
if !opts.NoHistory {
history = agent.Sessions.GetHistory(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(
history,
@ -573,11 +581,10 @@ func (al *AgentLoop) runLLMIteration(
})
}
al.forceCompression(agent, opts.SessionKey)
newHistory := agent.Sessions.GetHistory(opts.SessionKey)
compressed := al.forceCompression(agent, opts.SessionKey)
newSummary := agent.Sessions.GetSummary(opts.SessionKey)
messages = agent.ContextBuilder.BuildMessages(
newHistory, newSummary, "",
compressed, newSummary, "",
nil, opts.Channel, opts.ChatID,
)
continue
@ -775,10 +782,11 @@ func (al *AgentLoop) maybeSummarize(agent *AgentInstance, sessionKey, channel, c
// forceCompression aggressively reduces context when the limit is hit.
// 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)
if len(history) <= 4 {
return
return history
}
// 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
conversation := history[1 : len(history)-1]
if len(conversation) == 0 {
return
return history
}
// 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, history[len(history)-1]) // Last message
// Update session
agent.Sessions.SetHistory(sessionKey, newHistory)
agent.Sessions.Save(sessionKey)
logger.WarnCF("agent", "Forced compression executed", map[string]any{
logger.WarnCF("agent", "Forced compression executed (in-memory only, not persisted)", map[string]any{
"session_key": sessionKey,
"dropped_msgs": droppedCount,
"new_count": len(newHistory),
})
return newHistory
}
// GetStartupInfo returns information about loaded tools and skills for logging.
@ -981,7 +986,6 @@ func (al *AgentLoop) summarizeSession(agent *AgentInstance, sessionKey string) {
if finalSummary != "" {
agent.Sessions.SetSummary(sessionKey, finalSummary)
agent.Sessions.TruncateHistory(sessionKey, 4)
agent.Sessions.Save(sessionKey)
}
}

View file

@ -11,6 +11,10 @@ import (
"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 {
Key string `json:"key"`
Messages []providers.Message `json:"messages"`
@ -184,8 +188,12 @@ func (sm *SessionManager) Save(key string) error {
Updated: stored.Updated,
}
if len(stored.Messages) > 0 {
snapshot.Messages = make([]providers.Message, len(stored.Messages))
copy(snapshot.Messages, stored.Messages)
toStore := stored.Messages
if len(toStore) > maxStoredMessages {
toStore = toStore[len(toStore)-maxStoredMessages:]
}
snapshot.Messages = make([]providers.Message, len(toStore))
copy(snapshot.Messages, toStore)
} else {
snapshot.Messages = []providers.Message{}
}