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
This commit is contained in:
wassname
2026-04-16 13:23:28 +08:00
parent b825dbcf77
commit ad9c550b41
2 changed files with 77 additions and 34 deletions
+10 -1
View File
@@ -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)
// }
}
`
+67 -33
View File
@@ -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)
})