import { existsSync } from "node:fs"; import { join } from "node:path"; import { readJsonFile } from "../../src/shared/agent-paths"; import { loadScopedConfig, resolveScopedPath } from "../../src/shared/config"; import type { CompactionConfig } from "./types"; export const DEFAULT_CONFIG: CompactionConfig = { enabled: true, modelSelection: { mode: "preferred", preferredModels: [ "cpa/glm-5.1", "ccr-claude/claude-haiku-4-5-20251001", "ccr-claude/claude-sonnet-4-6", "ccr-gpt/gpt-5.5", ], allowCurrentSessionModel: true, }, strategy: { usePiPreparationFirst: true, recoverFromBranchEntriesWhenEmpty: true, groupByTurns: true, groupToolCycles: true, keptBudgetChars: 80_000, dropRawThinking: true, toolResults: { mode: "compress", headLines: 20, tailLines: 60, maxChars: 12_000, }, oversizedMessages: { mode: "compress", headChars: 4_000, tailChars: 8_000, maxChars: 16_000, }, historySnip: { enabled: true, targetChars: 120_000, keepHeadUnits: 2, keepTailUnits: 10, minOmittedUnits: 3, maxPreviewChars: 2_400, }, microcompact: { enabled: true, targetChars: 90_000, keepRecentToolResults: 6, maxPlaceholderChars: 240, }, contextCollapse: { enabled: true, targetChars: 60_000, keepFirstUnits: 1, keepRecentUnits: 8, maxSummaryChars: 4_000, maxListItems: 6, }, chunking: { enabled: true, targetChars: 32_000, maxChunks: 4, minMessagesPerChunk: 4, }, }, fallback: { onFailure: "next-model", maxAttempts: 3, }, notifications: { showModelUsed: true, showCompressionStats: true, }, }; function stringArray(value: unknown, fallback: string[]): string[] { if (!Array.isArray(value)) return fallback; return value.filter((item): item is string => typeof item === "string" && item.trim().length > 0); } function mergeConfig(base: CompactionConfig, input: any): CompactionConfig { const modelSelection = input?.modelSelection ?? {}; const strategy = input?.strategy ?? {}; const toolResults = strategy.toolResults ?? {}; const oversizedMessages = strategy.oversizedMessages ?? {}; const chunking = strategy.chunking ?? {}; const historySnip = strategy.historySnip ?? {}; const microcompact = strategy.microcompact ?? {}; const contextCollapse = strategy.contextCollapse ?? {}; const fallback = input?.fallback ?? {}; const notifications = input?.notifications ?? {}; return { enabled: input?.enabled ?? base.enabled, modelSelection: { mode: modelSelection.mode === "current" || modelSelection.mode === "preferred" ? modelSelection.mode : base.modelSelection.mode, preferredModels: stringArray(modelSelection.preferredModels ?? input?.preferredModels, base.modelSelection.preferredModels), allowCurrentSessionModel: modelSelection.allowCurrentSessionModel ?? base.modelSelection.allowCurrentSessionModel, }, strategy: { usePiPreparationFirst: strategy.usePiPreparationFirst ?? base.strategy.usePiPreparationFirst, recoverFromBranchEntriesWhenEmpty: strategy.recoverFromBranchEntriesWhenEmpty ?? base.strategy.recoverFromBranchEntriesWhenEmpty, groupByTurns: strategy.groupByTurns ?? base.strategy.groupByTurns, groupToolCycles: strategy.groupToolCycles ?? base.strategy.groupToolCycles, keptBudgetChars: strategy.keptBudgetChars ?? base.strategy.keptBudgetChars, dropRawThinking: strategy.dropRawThinking ?? base.strategy.dropRawThinking, toolResults: { mode: "compress", headLines: toolResults.headLines ?? base.strategy.toolResults.headLines, tailLines: toolResults.tailLines ?? base.strategy.toolResults.tailLines, maxChars: toolResults.maxChars ?? base.strategy.toolResults.maxChars, }, oversizedMessages: { mode: "compress", headChars: oversizedMessages.headChars ?? base.strategy.oversizedMessages.headChars, tailChars: oversizedMessages.tailChars ?? base.strategy.oversizedMessages.tailChars, maxChars: oversizedMessages.maxChars ?? base.strategy.oversizedMessages.maxChars, }, historySnip: { enabled: historySnip.enabled ?? base.strategy.historySnip.enabled, targetChars: historySnip.targetChars ?? base.strategy.historySnip.targetChars, keepHeadUnits: Math.max(0, historySnip.keepHeadUnits ?? base.strategy.historySnip.keepHeadUnits), keepTailUnits: Math.max(0, historySnip.keepTailUnits ?? base.strategy.historySnip.keepTailUnits), minOmittedUnits: Math.max(1, historySnip.minOmittedUnits ?? base.strategy.historySnip.minOmittedUnits), maxPreviewChars: Math.max(400, historySnip.maxPreviewChars ?? base.strategy.historySnip.maxPreviewChars), }, microcompact: { enabled: microcompact.enabled ?? base.strategy.microcompact.enabled, targetChars: microcompact.targetChars ?? base.strategy.microcompact.targetChars, keepRecentToolResults: Math.max(1, microcompact.keepRecentToolResults ?? base.strategy.microcompact.keepRecentToolResults), maxPlaceholderChars: Math.max(120, microcompact.maxPlaceholderChars ?? base.strategy.microcompact.maxPlaceholderChars), }, contextCollapse: { enabled: contextCollapse.enabled ?? base.strategy.contextCollapse.enabled, targetChars: contextCollapse.targetChars ?? base.strategy.contextCollapse.targetChars, keepFirstUnits: Math.max(0, contextCollapse.keepFirstUnits ?? base.strategy.contextCollapse.keepFirstUnits), keepRecentUnits: Math.max(1, contextCollapse.keepRecentUnits ?? base.strategy.contextCollapse.keepRecentUnits), maxSummaryChars: Math.max(800, contextCollapse.maxSummaryChars ?? base.strategy.contextCollapse.maxSummaryChars), maxListItems: Math.max(2, contextCollapse.maxListItems ?? base.strategy.contextCollapse.maxListItems), }, chunking: { enabled: chunking.enabled ?? base.strategy.chunking.enabled, targetChars: chunking.targetChars ?? base.strategy.chunking.targetChars, maxChunks: Math.max(1, chunking.maxChunks ?? base.strategy.chunking.maxChunks), minMessagesPerChunk: Math.max(1, chunking.minMessagesPerChunk ?? base.strategy.chunking.minMessagesPerChunk), }, }, fallback: { onFailure: fallback.onFailure === "cancel" || fallback.onFailure === "next-model" ? fallback.onFailure : base.fallback.onFailure, maxAttempts: Math.max(1, fallback.maxAttempts ?? base.fallback.maxAttempts), }, notifications: { showModelUsed: notifications.showModelUsed ?? base.notifications.showModelUsed, showCompressionStats: notifications.showCompressionStats ?? base.notifications.showCompressionStats, }, }; } function loadLegacyConfig(cwd: string): { config: CompactionConfig; source: string } | null { const legacyNames = ["compact-kimi.json", "compaction-kimi.json"]; for (const name of legacyNames) { const { projectPath, agentPath } = resolveScopedPath(cwd, join("config", name)); if (existsSync(projectPath)) { return { config: mergeConfig(DEFAULT_CONFIG, readJsonFile(projectPath, null)), source: projectPath }; } if (existsSync(agentPath)) { return { config: mergeConfig(DEFAULT_CONFIG, readJsonFile(agentPath, null)), source: agentPath }; } } return null; } export function loadCompactionConfig(cwd: string): { config: CompactionConfig; source: string } { const loaded = loadScopedConfig( cwd, join("config", "compaction.json"), DEFAULT_CONFIG, (defaults, input) => mergeConfig(defaults, input), ); if (loaded.source !== "defaults") { return loaded; } return loadLegacyConfig(cwd) ?? loaded; }