From ad9c550b4151f2191679d52c31d7a06c43d6448c Mon Sep 17 00:00:00 2001 From: wassname <1103714+wassname@users.noreply.github.com> Date: Thu, 16 Apr 2026 13:23:28 +0800 Subject: [PATCH] feat: auto-compaction when DCP blocks exceed threshold - Add session_compact handler: deactivate all DCP blocks when pi compacts - Add context handler threshold: trigger ctx.compact() when DCP summary blocks occupy >= configurable fraction of context tokens (default 50%) - Add config.compact.autoCompactThreshold (0 = disabled, 0.5 = default) - Blocks are deactivated immediately on trigger to prevent re-triggering - Includes earlier fix: gap calculation scoped to requested range, clearer error text for overlap messages --- config.ts | 11 +++++- index.ts | 100 ++++++++++++++++++++++++++++++++++++------------------ 2 files changed, 77 insertions(+), 34 deletions(-) diff --git a/config.ts b/config.ts index 2abf735..e8209ac 100644 --- a/config.ts +++ b/config.ts @@ -23,6 +23,9 @@ export interface DcpConfig { protectedTools: string[] // these tool outputs always protected from pruning protectUserMessages: boolean } + compact: { + autoCompactThreshold: number // 0-1, fraction of context that DCP blocks must occupy before triggering compaction (0 = disabled) + } strategies: { deduplication: { enabled: boolean @@ -58,6 +61,9 @@ const DEFAULT_CONFIG: DcpConfig = { protectedTools: ["compress", "write", "edit"], protectUserMessages: false, }, + compact: { + autoCompactThreshold: 0.5, // trigger compaction when DCP blocks occupy >= 50% of context tokens + }, strategies: { deduplication: { enabled: true, @@ -101,7 +107,10 @@ const DEFAULT_CONFIG_FILE_CONTENT = `{ // "purgeErrors": { "enabled": true, "turns": 4, "protectedTools": [] } // }, // "protectedFilePatterns": [], - // "pruneNotification": "detailed" + // "pruneNotification": "detailed", + // "compact": { + // "autoCompactThreshold": 0.5 // trigger pi compaction when DCP blocks >= 50% of context (0 = disabled) + // } } ` diff --git a/index.ts b/index.ts index 5da0f80..c7dbe4c 100644 --- a/index.ts +++ b/index.ts @@ -201,49 +201,83 @@ export default function (pi: ExtensionAPI) { // In manual mode we still apply pruning strategies (if // automaticStrategies is on) but skip autonomous nudge injection. const usage = ctx.getContextUsage() - if (usage && usage.tokens !== null && !state.manualMode) { - const contextPercent = usage.tokens / usage.contextWindow + if (usage && usage.tokens !== null) { + // ── Auto-compaction: if DCP blocks exceed threshold, trigger pi compaction ── + if (!state.manualMode && config.compact.autoCompactThreshold > 0) { + const activeBlocks = state.compressionBlocks.filter((b) => b.active) + const dcpBlockTokens = activeBlocks.reduce((sum, b) => sum + b.summaryTokenEstimate, 0) + const blockFraction = dcpBlockTokens / usage.tokens - // Count tool calls since the last user message (used for iteration nudge). - let toolCallsSinceLastUser = 0 - for (let i = prunedMessages.length - 1; i >= 0; i--) { - const msg = prunedMessages[i] as any - if (msg.role === "user") break - if (msg.role === "toolResult") toolCallsSinceLastUser++ + if (blockFraction >= config.compact.autoCompactThreshold) { + ctx.compact({ + customInstructions: "Include all DCP compression block summaries in the compaction summary.", + }) + // Deactivate blocks immediately so we don't trigger again before compaction completes + for (const block of activeBlocks) { + block.active = false + } + saveState(pi, state) + } } - const nudgeType = getNudgeType( - contextPercent, - state, - config, - toolCallsSinceLastUser, - ) + if (!state.manualMode) { + const contextPercent = usage.tokens / usage.contextWindow - if (nudgeType) { - let nudgeText: string - - if (nudgeType === "context-strong") { - nudgeText = CONTEXT_LIMIT_NUDGE_STRONG - } else if (nudgeType === "context-soft") { - nudgeText = CONTEXT_LIMIT_NUDGE_SOFT - } else if (nudgeType === "iteration") { - nudgeText = ITERATION_NUDGE - } else { - // "turn" - nudgeText = TURN_NUDGE + // Count tool calls since the last user message (used for iteration nudge). + let toolCallsSinceLastUser = 0 + for (let i = prunedMessages.length - 1; i >= 0; i--) { + const msg = prunedMessages[i] as any + if (msg.role === "user") break + if (msg.role === "toolResult") toolCallsSinceLastUser++ } - injectNudge(prunedMessages, nudgeText) - state.nudgeCounter = 0 - } else { - state.nudgeCounter++ - } - } + const nudgeType = getNudgeType( + contextPercent, + state, + config, + toolCallsSinceLastUser, + ) + + if (nudgeType) { + let nudgeText: string + + if (nudgeType === "context-strong") { + nudgeText = CONTEXT_LIMIT_NUDGE_STRONG + } else if (nudgeType === "context-soft") { + nudgeText = CONTEXT_LIMIT_NUDGE_SOFT + } else if (nudgeType === "iteration") { + nudgeText = ITERATION_NUDGE + } else { + // "turn" + nudgeText = TURN_NUDGE + } + + injectNudge(prunedMessages, nudgeText) + state.nudgeCounter = 0 + } else { + state.nudgeCounter++ + } + } // end !manualMode nudge block + } // end usage check return { messages: prunedMessages } }) - // ── 11. agent_end: persist state after each agent run ──────────────────── + // ── 11. session_compact: deactivate all DCP blocks ─────────────────────── + // When pi's built-in compaction runs, it folds all prior context (including + // DCP summary blocks) into a single compaction summary. All active DCP + // blocks are now redundant — deactivate them. + pi.on("session_compact", async (_event, _ctx) => { + const activeBlocks = state.compressionBlocks.filter((b) => b.active) + if (activeBlocks.length > 0) { + for (const block of activeBlocks) { + block.active = false + } + saveState(pi, state) + } + }) + + // ── 12. agent_end: persist state after each agent run ──────────────────── pi.on("agent_end", async (_event, _ctx) => { saveState(pi, state) })