diff --git a/README.md b/README.md index c089cfd..f3ce8a2 100644 --- a/README.md +++ b/README.md @@ -21,13 +21,13 @@ When a new version is available, DCP will show a toast notification. Update by c Restart OpenCode. The plugin will automatically start optimizing your sessions. -## Pruning Strategies +## How Pruning Works -DCP implements two complementary strategies: +DCP uses two complementary techniques: -**Deduplication** — Fast, zero-cost pruning that identifies repeated tool calls (e.g., reading the same file multiple times) and keeps only the most recent output. Runs instantly with no LLM calls. +**Automatic Deduplication** — Silently identifies repeated tool calls (e.g., reading the same file multiple times) and keeps only the most recent output. Runs on every request with zero LLM cost. -**AI Analysis** — Uses a language model to semantically analyze conversation context and identify tool outputs that are no longer relevant to the current task. More thorough but incurs LLM cost. +**AI Analysis** — Uses a language model to semantically analyze conversation context and identify tool outputs that are no longer relevant to the current task. More thorough but incurs LLM cost. Configurable via `strategies`. ## Context Pruning Tool @@ -61,17 +61,17 @@ DCP uses its own config file (`~/.config/opencode/dcp.jsonc` or `.opencode/dcp.j | `pruning_summary` | `"detailed"` | `"off"`, `"minimal"`, or `"detailed"` | | `nudge_freq` | `10` | How often to remind AI to prune (lower = more frequent) | | `protectedTools` | `["task", "todowrite", "todoread", "prune"]` | Tools that are never pruned | -| `strategies.onIdle` | `["deduplication", "ai-analysis"]` | Strategies for automatic pruning | -| `strategies.onTool` | `["deduplication", "ai-analysis"]` | Strategies when AI calls `prune` | +| `strategies.onIdle` | `["ai-analysis"]` | Strategies for automatic pruning | +| `strategies.onTool` | `["ai-analysis"]` | Strategies when AI calls `prune` | -**Strategies:** `"deduplication"` (fast, zero LLM cost) and `"ai-analysis"` (maximum savings). Empty array disables that trigger. +**Strategies:** `"ai-analysis"` uses LLM to identify prunable outputs. Empty array disables that trigger. Deduplication runs automatically on every request. ```jsonc { "enabled": true, "strategies": { - "onIdle": ["deduplication", "ai-analysis"], - "onTool": ["deduplication", "ai-analysis"] + "onIdle": ["ai-analysis"], + "onTool": ["ai-analysis"] }, "protectedTools": ["task", "todowrite", "todoread", "prune"] } diff --git a/lib/config.ts b/lib/config.ts index 1c70490..2d0500b 100644 --- a/lib/config.ts +++ b/lib/config.ts @@ -36,8 +36,8 @@ const defaultConfig: PluginConfig = { pruning_summary: 'detailed', nudge_freq: 10, strategies: { - onIdle: ['deduplication', 'ai-analysis'], - onTool: ['deduplication', 'ai-analysis'] + onIdle: ['ai-analysis'], + onTool: ['ai-analysis'] } } @@ -112,12 +112,12 @@ function createDefaultConfig(): void { "showModelErrorToasts": true, // Only run AI analysis with session model or configured model (disables fallback models) "strictModelSelection": false, - // Pruning strategies: "deduplication", "ai-analysis" (empty array = disabled) + // AI analysis strategies (deduplication runs automatically on every request) "strategies": { // Strategies to run when session goes idle - "onIdle": ["deduplication", "ai-analysis"], + "onIdle": ["ai-analysis"], // Strategies to run when AI calls prune tool - "onTool": ["deduplication", "ai-analysis"] + "onTool": ["ai-analysis"] }, // Summary display: "off", "minimal", or "detailed" "pruning_summary": "detailed", diff --git a/lib/fetch-wrapper/index.ts b/lib/fetch-wrapper/index.ts index 0cadb56..80d4ce9 100644 --- a/lib/fetch-wrapper/index.ts +++ b/lib/fetch-wrapper/index.ts @@ -6,6 +6,7 @@ import type { PluginConfig } from "../config" import { handleOpenAIChatAndAnthropic } from "./openai-chat" import { handleGemini } from "./gemini" import { handleOpenAIResponses } from "./openai-responses" +import { detectDuplicates } from "../deduplicator" export type { FetchHandlerContext, FetchHandlerResult, SynthPrompts } from "./types" @@ -78,6 +79,21 @@ export function installFetchWrapper( } } + // Run deduplication after handlers have populated toolParameters cache + const sessionId = state.lastSeenSessionId + if (sessionId && state.toolParameters.size > 1) { + const toolIds = Array.from(state.toolParameters.keys()) + const alreadyPruned = state.prunedIds.get(sessionId) ?? [] + const alreadyPrunedLower = new Set(alreadyPruned.map(id => id.toLowerCase())) + const unpruned = toolIds.filter(id => !alreadyPrunedLower.has(id.toLowerCase())) + if (unpruned.length > 1) { + const { duplicateIds } = detectDuplicates(state.toolParameters, unpruned, config.protectedTools) + if (duplicateIds.length > 0) { + state.prunedIds.set(sessionId, [...new Set([...alreadyPruned, ...duplicateIds])]) + } + } + } + if (modified) { init.body = JSON.stringify(body) }