From 0cffbf5bfacbec3b3b7dbc17e1d266edb76c592d Mon Sep 17 00:00:00 2001 From: viettranx Date: Fri, 20 Mar 2026 21:46:22 +0700 Subject: [PATCH] fix(agent): loop improvements, history compaction, config tweaks --- internal/agent/loop.go | 52 ++++++++++++++++++------- internal/agent/loop_history.go | 2 +- internal/agent/loop_run.go | 3 ++ internal/agent/loop_types.go | 1 + internal/agent/title_generate.go | 2 +- internal/channels/history.go | 2 +- internal/channels/history_compaction.go | 2 +- internal/config/config.go | 2 +- 8 files changed, 46 insertions(+), 20 deletions(-) diff --git a/internal/agent/loop.go b/internal/agent/loop.go index a37499c2..493779a7 100644 --- a/internal/agent/loop.go +++ b/internal/agent/loop.go @@ -1333,11 +1333,46 @@ func (l *Loop) runLoop(ctx context.Context, req RunRequest) (*RunResult, error) finalContent += req.ContentSuffix } - pendingMsgs = append(pendingMsgs, providers.Message{ + // Collect forwarded media + dedup + populate sizes BEFORE saving to session, + // so we can attach output MediaRefs to the assistant message for history reload. + for _, mf := range req.ForwardMedia { + ct := mf.MimeType + if ct == "" { + ct = mimeFromExt(filepath.Ext(mf.Path)) + } + mediaResults = append(mediaResults, MediaResult{Path: mf.Path, ContentType: ct}) + } + mediaResults = deduplicateMedia(mediaResults) + for i := range mediaResults { + if mediaResults[i].Size == 0 { + if info, err := os.Stat(mediaResults[i].Path); err == nil { + mediaResults[i].Size = info.Size() + } + } + } + + // Build final assistant message with output media refs for history persistence. + assistantMsg := providers.Message{ Role: "assistant", Content: finalContent, Thinking: finalThinking, - }) + } + for _, mr := range mediaResults { + kind := "document" + if strings.HasPrefix(mr.ContentType, "image/") { + kind = "image" + } else if strings.HasPrefix(mr.ContentType, "audio/") { + kind = "audio" + } else if strings.HasPrefix(mr.ContentType, "video/") { + kind = "video" + } + assistantMsg.MediaRefs = append(assistantMsg.MediaRefs, providers.MediaRef{ + ID: filepath.Base(mr.Path), + MimeType: mr.ContentType, + Kind: kind, + }) + } + pendingMsgs = append(pendingMsgs, assistantMsg) // Bootstrap nudge: if model didn't call write_file on turn 2+, inject reminder // into session history so the next turn sees it. Appended to pendingMsgs so it's @@ -1414,19 +1449,6 @@ func (l *Loop) runLoop(ctx context.Context, req RunRequest) (*RunResult, error) // 5. Maybe summarize l.maybeSummarize(ctx, req.SessionKey) - // Include forwarded media from delegation results (not cleaned up like req.Media) - for _, mf := range req.ForwardMedia { - ct := mf.MimeType - if ct == "" { - ct = mimeFromExt(filepath.Ext(mf.Path)) - } - mediaResults = append(mediaResults, MediaResult{Path: mf.Path, ContentType: ct}) - } - - // Deduplicate media by path — prevents the same image being sent twice - // (e.g. once via ForwardMedia and again when the LLM reads the file). - mediaResults = deduplicateMedia(mediaResults) - return &RunResult{ Content: finalContent, RunID: req.RunID, diff --git a/internal/agent/loop_history.go b/internal/agent/loop_history.go index 5bfa70a4..7e67d883 100644 --- a/internal/agent/loop_history.go +++ b/internal/agent/loop_history.go @@ -482,7 +482,7 @@ func (l *Loop) maybeSummarize(ctx context.Context, sessionKey string) { if l.compactionCfg != nil && l.compactionCfg.MaxHistoryShare > 0 { historyShare = l.compactionCfg.MaxHistoryShare } - minMessages := 50 + minMessages := 200 if l.compactionCfg != nil && l.compactionCfg.MinMessages > 0 { minMessages = l.compactionCfg.MinMessages } diff --git a/internal/agent/loop_run.go b/internal/agent/loop_run.go index 24367a3f..8b329335 100644 --- a/internal/agent/loop_run.go +++ b/internal/agent/loop_run.go @@ -178,6 +178,9 @@ func (l *Loop) Run(ctx context.Context, req RunRequest) (*RunResult, error) { "cache_read_tokens": result.Usage.CacheReadTokens, } } + if len(result.Media) > 0 { + completedPayload["media"] = result.Media + } emitRun(AgentEvent{ Type: protocol.AgentEventRunCompleted, AgentID: l.id, diff --git a/internal/agent/loop_types.go b/internal/agent/loop_types.go index ecdbaa31..bf459027 100644 --- a/internal/agent/loop_types.go +++ b/internal/agent/loop_types.go @@ -413,5 +413,6 @@ type RunResult struct { type MediaResult struct { Path string `json:"path"` // local file path ContentType string `json:"content_type,omitempty"` // MIME type + Size int64 `json:"size,omitempty"` // file size in bytes AsVoice bool `json:"as_voice,omitempty"` // send as voice message (Telegram OGG) } diff --git a/internal/agent/title_generate.go b/internal/agent/title_generate.go index 261e1336..92ef2da3 100644 --- a/internal/agent/title_generate.go +++ b/internal/agent/title_generate.go @@ -9,7 +9,7 @@ import ( "github.com/nextlevelbuilder/goclaw/internal/providers" ) -const titleGenerateTimeout = 10 * time.Second +const titleGenerateTimeout = 30 * time.Second const titleSystemPrompt = `Generate a short title (max 15 words) for this conversation based on the user's message. Reply with only the title, no quotes or punctuation wrapping.` diff --git a/internal/channels/history.go b/internal/channels/history.go index 1206bc40..a5470e30 100644 --- a/internal/channels/history.go +++ b/internal/channels/history.go @@ -23,7 +23,7 @@ import ( const maxHistoryKeys = 1000 // DefaultGroupHistoryLimit is the default pending message limit per group. -const DefaultGroupHistoryLimit = 50 +const DefaultGroupHistoryLimit = 100 const ( flushInterval = 3 * time.Second // periodic flush interval diff --git a/internal/channels/history_compaction.go b/internal/channels/history_compaction.go index a0b172d8..62349794 100644 --- a/internal/channels/history_compaction.go +++ b/internal/channels/history_compaction.go @@ -15,7 +15,7 @@ import ( // CompactionConfig configures LLM-based history compaction. type CompactionConfig struct { - Threshold int // trigger compaction when entries exceed this (default 50) + Threshold int // trigger compaction when entries exceed this (default 100) KeepRecent int // keep this many recent raw messages (default 15) MaxTokens int // max output tokens for summarization (default 4096) Provider providers.Provider // LLM provider for summarization diff --git a/internal/config/config.go b/internal/config/config.go index 56019689..f8f6fb32 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -132,7 +132,7 @@ type AgentDefaults struct { type CompactionConfig struct { ReserveTokensFloor int `json:"reserveTokensFloor,omitempty"` // min reserve tokens (default 20000) MaxHistoryShare float64 `json:"maxHistoryShare,omitempty"` // max share of context for history (default 0.75) - MinMessages int `json:"minMessages,omitempty"` // min messages before compaction triggers (default 50) + MinMessages int `json:"minMessages,omitempty"` // min messages before compaction triggers (default 200) KeepLastMessages int `json:"keepLastMessages,omitempty"` // messages to keep after compaction (default 4) MemoryFlush *MemoryFlushConfig `json:"memoryFlush,omitempty"` // pre-compaction flush }