diff --git a/index.ts b/index.ts index 6516206..7888d8e 100644 --- a/index.ts +++ b/index.ts @@ -89,7 +89,7 @@ export default function (pi: ExtensionAPI) { const argsStr = args.trim().toLowerCase(); if (argsStr === "compress") { state.forceCompressNext = true; - ctx.ui.notify("Manual compression triggered. It will run in the background on the next agent turn.", "info"); + ctx.ui.notify("Manual compression scheduled. It will run when you send your next message.", "info"); return; } @@ -110,16 +110,17 @@ export default function (pi: ExtensionAPI) { } const lines = [ - "Auto-Compressor (Hermes) Stats:", - ` Total Compressions: ${state.compressionCount}`, - ` Tokens Saved (Compaction): ~${state.tokensSaved.toLocaleString()}`, - ` Tokens Saved (Tool Pruning): ~${prunedToolTokens.toLocaleString()}`, - ` Total Tool Calls Tracked: ${state.toolCalls.size}`, - ` Pruned Tool Outputs (Deduplication/Errors): ${state.prunedToolIds.size}`, - ` Total Tool Tokens Generated: ~${totalToolTokens.toLocaleString()}`, - ` Current User Turn: ${state.currentTurn}`, - ` Summary Exists (Has Compressed): ${state.previousSummary !== null ? "Yes" : "No"}`, - ` Current Context Tokens: ${tokenStr}`, + `Auto-Compressor (Hermes) Stats:`, + ` Total Compressions: ${state.compressionCount}`, + ` Pending Compression: ${state.forceCompressNext ? "YES (Scheduled for next turn)" : "No"}`, + ` Tokens Saved (Compaction): ~${state.tokensSaved.toLocaleString()}`, + ` Tokens Saved (Tool Pruning): ~${prunedToolTokens.toLocaleString()}`, + ` Total Tool Calls Tracked: ${state.toolCalls.size}`, + ` Pruned Tool Outputs (Deduplication/Errors): ${state.prunedToolIds.size}`, + ` Total Tool Tokens Generated: ~${totalToolTokens.toLocaleString()}`, + ` Current User Turn: ${state.currentTurn}`, + ` Summary Exists (Has Compressed): ${state.previousSummary !== null ? "Yes" : "No"}`, + ` Current Context Tokens: ${tokenStr}`, "", "Type '/acp compress' to force a compression on the next turn." ]; diff --git a/pruner.ts b/pruner.ts index c33c3d6..6ceefa1 100644 --- a/pruner.ts +++ b/pruner.ts @@ -372,18 +372,26 @@ export async function applyPruning( state.forceCompressNext || (totalTokens >= thresholdTokens && msgs.length > AUTO_COMPRESS_CONFIG.protectFirstN + 4) ) { + const wasForced = state.forceCompressNext; + state.forceCompressNext = false; + let tailBudget = Math.floor(thresholdTokens * AUTO_COMPRESS_CONFIG.summaryTargetRatio); - if (state.forceCompressNext) { - // Force compression: reduce tail budget to ensure we have something to compress - tailBudget = Math.min(tailBudget, Math.floor(totalTokens * 0.3)); + if (wasForced) { + // Force compression: use a tiny tail budget to ensure we summarize almost everything + tailBudget = Math.max(100, Math.floor(totalTokens * 0.05)); } - state.forceCompressNext = false; const compressStart = alignBoundaryForward(msgs, AUTO_COMPRESS_CONFIG.protectFirstN); const compressEnd = findTailCutByTokens(msgs, compressStart, tailBudget); - if (compressStart < compressEnd) { - const middle = msgs.slice(compressStart, compressEnd); + // If forced, we MUST compress something if we have any messages after protectFirstN + let finalCompressEnd = compressEnd; + if (wasForced && finalCompressEnd <= compressStart && msgs.length > compressStart + 1) { + finalCompressEnd = msgs.length - 1; + } + + if (compressStart < finalCompressEnd) { + const middle = msgs.slice(compressStart, finalCompressEnd); const summary = await generateSummary(middle, state.previousSummary, null, model); if (summary) { @@ -394,7 +402,7 @@ export async function applyPruning( } const lastHeadRole = msgs[compressStart - 1]?.role || "user"; - const firstTailRole = msgs[compressEnd]?.role || "user"; + const firstTailRole = msgs[finalCompressEnd]?.role || "user"; let summaryRole = lastHeadRole === "assistant" ? "user" : "assistant"; if (summaryRole === firstTailRole) { @@ -402,13 +410,13 @@ export async function applyPruning( if (flipped !== lastHeadRole) { summaryRole = flipped; } else { - const tailMsg = { ...msgs[compressEnd], timestamp: msgs[compressEnd].timestamp || Date.now() }; + const tailMsg = { ...msgs[finalCompressEnd], timestamp: msgs[finalCompressEnd].timestamp || Date.now() }; const originalContent = tailMsg.content || ""; tailMsg.content = "## Goal\n" + summary + "\n\n--- END OF CONTEXT SUMMARY ---\n\n" + (typeof originalContent === "string" ? originalContent : ""); compressed.push(tailMsg); - for (let i = compressEnd + 1; i < msgs.length; i++) { + for (let i = finalCompressEnd + 1; i < msgs.length; i++) { compressed.push(msgs[i]); } state.previousSummary = summary; @@ -430,7 +438,7 @@ export async function applyPruning( timestamp: Date.now(), }); - for (let i = compressEnd; i < msgs.length; i++) { + for (let i = finalCompressEnd; i < msgs.length; i++) { compressed.push(msgs[i]); } @@ -440,11 +448,10 @@ export async function applyPruning( return sanitizeToolPairs(compressed); } } else { - if (config.debug) { - console.log(`[ACP] Compression skipped: conversation too short (start: ${compressStart}, end: ${compressEnd})`); + if (config.debug || wasForced) { + console.log(`[ACP] Compression skipped: conversation too short (start: ${compressStart}, end: ${finalCompressEnd})`); } } - } - + } return sanitizeToolPairs(msgs); }