fix(compaction): memory flush never triggering due to threshold mismatch

Flush threshold (176K) was higher than compaction threshold (150K), so
flush was always skipped. Removed independent threshold calculation —
flush now always runs before compaction, gated only by enabled check and
per-cycle dedup guard. Also localized hardcoded compaction tooltips
(en/vi/zh), fixed inaccurate Memory Flush description, and removed dead
reserveTokensFloor/softThreshold UI inputs.
This commit is contained in:
viettranx committed 2026-03-10 17:28:55 +07:00
1 parent 9181eebcea
commit 2fed2a57d3
5 files changed
+41 -89

No files matched your search

+12 -35
View File
@@ -21,16 +21,13 @@ const (
DefaultMemoryFlushSystemPrompt = "Pre-compaction memory flush turn. " +
"The session is near auto-compaction; capture durable memories to disk. " +
"You may reply, but usually NO_REPLY is correct."
DefaultSoftThresholdTokens = 4000
)
// MemoryFlushSettings holds resolved flush config with defaults applied.
type MemoryFlushSettings struct {
Enabled bool
SoftThresholdTokens int
Prompt string
SystemPrompt string
Enabled bool
Prompt string
SystemPrompt string
}
// ResolveMemoryFlushSettings resolves flush settings from config, applying defaults.
@@ -39,10 +36,9 @@ func ResolveMemoryFlushSettings(compaction *config.CompactionConfig) *MemoryFlus
if compaction == nil || compaction.MemoryFlush == nil {
// Default: enabled
return &MemoryFlushSettings{
Enabled: true,
SoftThresholdTokens: DefaultSoftThresholdTokens,
Prompt: DefaultMemoryFlushPrompt,
SystemPrompt: DefaultMemoryFlushSystemPrompt,
Enabled: true,
Prompt: DefaultMemoryFlushPrompt,
SystemPrompt: DefaultMemoryFlushSystemPrompt,
}
}
@@ -52,15 +48,11 @@ func ResolveMemoryFlushSettings(compaction *config.CompactionConfig) *MemoryFlus
}
settings := &MemoryFlushSettings{
Enabled: true,
SoftThresholdTokens: DefaultSoftThresholdTokens,
Prompt: DefaultMemoryFlushPrompt,
SystemPrompt: DefaultMemoryFlushSystemPrompt,
Enabled: true,
Prompt: DefaultMemoryFlushPrompt,
SystemPrompt: DefaultMemoryFlushSystemPrompt,
}
if mf.SoftThresholdTokens > 0 {
settings.SoftThresholdTokens = mf.SoftThresholdTokens
}
if mf.Prompt != "" {
settings.Prompt = mf.Prompt
}
@@ -72,7 +64,8 @@ func ResolveMemoryFlushSettings(compaction *config.CompactionConfig) *MemoryFlus
}
// shouldRunMemoryFlush checks whether a memory flush should run before compaction.
// Matching TS memory-flush.ts:shouldRunMemoryFlush.
// Flush always runs when compaction triggers (called inside maybeSummarize),
// gated only by enabled/memory checks and a dedup guard per compaction cycle.
func (l *Loop) shouldRunMemoryFlush(sessionKey string, totalTokens int, settings *MemoryFlushSettings) bool {
if settings == nil || !settings.Enabled || !l.hasMemory {
return false
@@ -82,23 +75,7 @@ func (l *Loop) shouldRunMemoryFlush(sessionKey string, totalTokens int, settings
return false
}
// Threshold = contextWindow - reserveTokensFloor - softThresholdTokens
// When totalTokens >= threshold → flush
reserveFloor := 20000 // default
if l.compactionCfg != nil && l.compactionCfg.ReserveTokensFloor > 0 {
reserveFloor = l.compactionCfg.ReserveTokensFloor
}
threshold := l.contextWindow - reserveFloor - settings.SoftThresholdTokens
if threshold <= 0 {
return false
}
if totalTokens < threshold {
return false
}
// Deduplication: skip if already flushed in this compaction cycle
// Deduplication: skip if already flushed in this compaction cycle.
compactionCount := l.sessions.GetCompactionCount(sessionKey)
lastFlushAt := l.sessions.GetMemoryFlushCompactionCount(sessionKey)
if lastFlushAt >= 0 && lastFlushAt == compactionCount {
+4 -2
View File
@@ -271,12 +271,14 @@
"compaction": {
"title": "Compaction",
"description": "Context window compaction and memory flush settings",
"reserveTokensFloor": "Reserve Tokens Floor",
"maxHistoryShare": "Max History Share (0-1)",
"maxHistoryShareTip": "Maximum fraction of context window for conversation history (e.g. 0.75 = 75%). Compaction triggers when exceeded.",
"minMessages": "Min Messages",
"minMessagesTip": "Minimum messages before compaction can trigger, regardless of token count.",
"keepLastMessages": "Keep Last Messages",
"keepLastMessagesTip": "Recent messages kept after compaction. Older messages are replaced by a summary.",
"memoryFlush": "Memory Flush",
"softThreshold": "Soft Threshold (tokens)"
"memoryFlushTip": "Before compaction, the agent gets a turn to save important context to memory files. Also triggers Knowledge Graph extraction."
},
"contextPruning": {
"title": "Context Pruning",
+6 -4
View File
@@ -270,13 +270,15 @@
},
"compaction": {
"title": "Nén ngữ cảnh",
"description": "Cài đặt nén cửa sổ ngữ cảnh và xả bộ nhớ",
"reserveTokensFloor": "Sàn token dự trữ",
"description": "Cài đặt nén cửa sổ ngữ cảnh và ghi nhớ trước nén",
"maxHistoryShare": "Phần lịch sử tối đa (0-1)",
"maxHistoryShareTip": "Tỷ lệ tối đa của cửa sổ ngữ cảnh dành cho lịch sử hội thoại (vd: 0.75 = 75%). Nén kích hoạt khi vượt quá.",
"minMessages": "Tin nhắn tối thiểu",
"minMessagesTip": "Số tin nhắn tối thiểu trước khi nén có thể kích hoạt, bất kể số token.",
"keepLastMessages": "Giữ tin nhắn cuối",
"memoryFlush": "Xả bộ nhớ",
"softThreshold": "Ngưỡng mềm (token)"
"keepLastMessagesTip": "Số tin nhắn gần nhất giữ lại sau khi nén. Các tin nhắn cũ hơn được thay thế bằng bản tóm tắt.",
"memoryFlush": "Ghi nhớ trước nén",
"memoryFlushTip": "Trước khi nén, agent được một lượt để lưu ngữ cảnh quan trọng vào file bộ nhớ. Cũng kích hoạt trích xuất Knowledge Graph."
},
"contextPruning": {
"title": "Cắt bớt ngữ cảnh",
+6 -4
View File
@@ -270,13 +270,15 @@
},
"compaction": {
"title": "压缩",
"description": "上下文窗口压缩和内存刷新设置",
"reserveTokensFloor": "保留令牌下限",
"description": "上下文窗口压缩和压缩前记忆设置",
"maxHistoryShare": "最大历史份额 (0-1)",
"maxHistoryShareTip": "上下文窗口用于对话历史的最大比例(如0.75 = 75%)。超过时触发压缩。",
"minMessages": "最小消息数",
"minMessagesTip": "压缩触发前所需的最少消息数,与令牌数无关。",
"keepLastMessages": "保留最后消息数",
"memoryFlush": "内存刷新",
"softThreshold": "软阈值(令牌)"
"keepLastMessagesTip": "压缩后保留的最近消息数。旧消息被摘要替换。",
"memoryFlush": "压缩前记忆",
"memoryFlushTip": "压缩前,Agent可以将重要上下文保存到记忆文件。同时触发知识图谱提取。"
},
"contextPruning": {
"title": "上下文裁剪",
@@ -23,16 +23,7 @@ export function CompactionSection({ enabled, value, onToggle, onChange }: Compac
>
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
<div className="space-y-2">
<InfoLabel tip="Minimum tokens reserved for the LLM response. Higher values give more room for output but reduce available context for history.">{t(`${s}.reserveTokensFloor`)}</InfoLabel>
<Input
type="number"
placeholder="20000"
value={value.reserveTokensFloor ?? ""}
onChange={(e) => onChange({ ...value, reserveTokensFloor: numOrUndef(e.target.value) })}
/>
</div>
<div className="space-y-2">
<InfoLabel tip="Maximum fraction of the context window used for conversation history before compaction triggers (e.g. 0.75 = 75%).">{t(`${s}.maxHistoryShare`)}</InfoLabel>
<InfoLabel tip={t(`${s}.maxHistoryShareTip`)}>{t(`${s}.maxHistoryShare`)}</InfoLabel>
<Input
type="number"
step="0.05"
@@ -41,10 +32,8 @@ export function CompactionSection({ enabled, value, onToggle, onChange }: Compac
onChange={(e) => onChange({ ...value, maxHistoryShare: numOrUndef(e.target.value) })}
/>
</div>
</div>
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
<div className="space-y-2">
<InfoLabel tip="Minimum number of messages in history before compaction can trigger, regardless of token usage.">{t(`${s}.minMessages`)}</InfoLabel>
<InfoLabel tip={t(`${s}.minMessagesTip`)}>{t(`${s}.minMessages`)}</InfoLabel>
<Input
type="number"
placeholder="50"
@@ -52,8 +41,10 @@ export function CompactionSection({ enabled, value, onToggle, onChange }: Compac
onChange={(e) => onChange({ ...value, minMessages: numOrUndef(e.target.value) })}
/>
</div>
</div>
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
<div className="space-y-2">
<InfoLabel tip="Number of recent messages to preserve after compaction. The rest is replaced by a summary.">{t(`${s}.keepLastMessages`)}</InfoLabel>
<InfoLabel tip={t(`${s}.keepLastMessagesTip`)}>{t(`${s}.keepLastMessages`)}</InfoLabel>
<Input
type="number"
placeholder="4"
@@ -62,36 +53,14 @@ export function CompactionSection({ enabled, value, onToggle, onChange }: Compac
/>
</div>
</div>
<div className="space-y-3">
<div className="flex items-center gap-2">
<Switch
checked={value.memoryFlush?.enabled ?? true}
onCheckedChange={(v) =>
onChange({ ...value, memoryFlush: { ...value.memoryFlush, enabled: v } })
}
/>
<InfoLabel tip="When enabled, compacted history is also saved to long-term memory for future retrieval.">{t(`${s}.memoryFlush`)}</InfoLabel>
</div>
{(value.memoryFlush?.enabled ?? true) && (
<div className="space-y-2 pl-6">
<InfoLabel tip="Token count threshold that triggers memory flush. When summary exceeds this, older memories are flushed to storage.">{t(`${s}.softThreshold`)}</InfoLabel>
<Input
type="number"
placeholder="4000"
value={value.memoryFlush?.softThresholdTokens ?? ""}
onChange={(e) =>
onChange({
...value,
memoryFlush: {
...value.memoryFlush,
enabled: true,
softThresholdTokens: numOrUndef(e.target.value),
},
})
}
/>
</div>
)}
<div className="flex items-center gap-2">
<Switch
checked={value.memoryFlush?.enabled ?? true}
onCheckedChange={(v) =>
onChange({ ...value, memoryFlush: { ...value.memoryFlush, enabled: v } })
}
/>
<InfoLabel tip={t(`${s}.memoryFlushTip`)}>{t(`${s}.memoryFlush`)}</InfoLabel>
</div>
</ConfigSection>
);