Compact runs the 3-level compaction pipeline. Each level is progressively more aggressive. Most of the time, Level 1 (trim) suffices.
( ctx context.Context, history []models.Message, llmClient client.LLMClient, cfg CompactConfig, )
| 205 | // Compact runs the 3-level compaction pipeline. |
| 206 | // Each level is progressively more aggressive. Most of the time, Level 1 (trim) suffices. |
| 207 | func (hc *HistoryCompactor) Compact( |
| 208 | ctx context.Context, |
| 209 | history []models.Message, |
| 210 | llmClient client.LLMClient, |
| 211 | cfg CompactConfig, |
| 212 | ) ([]models.Message, error) { |
| 213 | budget := hc.CharBudget(cfg) |
| 214 | before := totalChars(history) |
| 215 | beforeMsgs := len(history) |
| 216 | |
| 217 | hc.logger.Info("History compaction triggered", |
| 218 | zap.Int("budget_chars", budget), |
| 219 | zap.Int("current_chars", before), |
| 220 | zap.Int("messages", beforeMsgs), |
| 221 | zap.Int("max_payload_bytes", cfg.MaxPayloadBytes), |
| 222 | ) |
| 223 | hc.emitStatus(CompactStageStart, i18n.T("compact.status.start", |
| 224 | beforeMsgs, FormatPayloadSize(before), FormatPayloadSize(budget))) |
| 225 | |
| 226 | // LEVEL 1: Near-lossless trimming — pure Go, no network |
| 227 | hc.emitStatus(CompactStageTrim, i18n.T("compact.status.trim")) |
| 228 | history = hc.trimmer.TrimHistory(history) |
| 229 | current := totalChars(history) |
| 230 | if current <= budget { |
| 231 | hc.logger.Info("Level 1 (trim) sufficient", |
| 232 | zap.Int("before_chars", before), |
| 233 | zap.Int("after_chars", current), |
| 234 | ) |
| 235 | hc.emitStatus(CompactStageDone, i18n.T("compact.status.trim_sufficient", |
| 236 | FormatPayloadSize(before), FormatPayloadSize(current))) |
| 237 | return history, nil |
| 238 | } |
| 239 | |
| 240 | // LEVEL 2: Structured summarization of old messages (requires LLM call) |
| 241 | hc.emitStatus(CompactStageSummarize, i18n.T("compact.status.summarize")) |
| 242 | summarized, err := hc.structuredSummarize(ctx, history, llmClient, cfg) |
| 243 | if err != nil { |
| 244 | // A cancellation from the user should propagate, not silently fall |
| 245 | // through to emergency truncation — the user's own choice to abort |
| 246 | // must not mangle their history. |
| 247 | if ctx.Err() != nil { |
| 248 | hc.emitStatus(CompactStageDone, i18n.T("compact.status.canceled")) |
| 249 | return nil, ctx.Err() |
| 250 | } |
| 251 | hc.logger.Warn("Level 2 (summarization) failed, falling back", |
| 252 | zap.Error(err), |
| 253 | ) |
| 254 | hc.emitStatus(CompactStageSummarize, |
| 255 | i18n.T("compact.status.summarize_failed", err)) |
| 256 | } else { |
| 257 | history = summarized |
| 258 | current = totalChars(history) |
| 259 | if current <= budget { |
| 260 | hc.logger.Info("Level 2 (structured summarization) sufficient", |
| 261 | zap.Int("before_chars", before), |
| 262 | zap.Int("after_chars", current), |
| 263 | zap.Int("before_msgs", beforeMsgs), |
| 264 | zap.Int("after_msgs", len(history)), |
no test coverage detected