From 178736c2dd16482302de866cf4706bd47595776c Mon Sep 17 00:00:00 2001 From: spoons-and-mirrors <212802214+spoons-and-mirrors@users.noreply.github.com> Date: Tue, 2 Dec 2025 03:37:41 +0100 Subject: [PATCH 1/4] refactor: non-stop silent deduplication --- lib/config.ts | 4 ++-- lib/fetch-wrapper/index.ts | 16 ++++++++++++++++ 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/lib/config.ts b/lib/config.ts index 1c704909..0fb8b0f8 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'] } } diff --git a/lib/fetch-wrapper/index.ts b/lib/fetch-wrapper/index.ts index 0cadb565..57f53ba7 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 > 0) { + 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 > 0) { + 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) } From f80aa375ffe0375d1632204fcb3d6d19b94b8ca2 Mon Sep 17 00:00:00 2001 From: spoons-and-mirrors <212802214+spoons-and-mirrors@users.noreply.github.com> Date: Tue, 2 Dec 2025 04:01:06 +0100 Subject: [PATCH 2/4] docs --- README.md | 18 +++++++++--------- lib/config.ts | 6 +++--- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index c089cfdd..f3ce8a26 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 0fb8b0f8..2d0500bc 100644 --- a/lib/config.ts +++ b/lib/config.ts @@ -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", From 97e825cb024505dc0a0b88032da8fda69f34d1e5 Mon Sep 17 00:00:00 2001 From: Spoon <212802214+spoons-and-mirrors@users.noreply.github.com> Date: Tue, 2 Dec 2025 04:03:22 +0100 Subject: [PATCH 3/4] Change prunable tool check from >0 to >1 --- lib/fetch-wrapper/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/fetch-wrapper/index.ts b/lib/fetch-wrapper/index.ts index 57f53ba7..0f11e580 100644 --- a/lib/fetch-wrapper/index.ts +++ b/lib/fetch-wrapper/index.ts @@ -86,7 +86,7 @@ export function installFetchWrapper( 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 > 0) { + if (unpruned.length > 1) { const { duplicateIds } = detectDuplicates(state.toolParameters, unpruned, config.protectedTools) if (duplicateIds.length > 0) { state.prunedIds.set(sessionId, [...new Set([...alreadyPruned, ...duplicateIds])]) From b7b088b647d8e4f3e6c23ee0299b369e0c76b037 Mon Sep 17 00:00:00 2001 From: Spoon <212802214+spoons-and-mirrors@users.noreply.github.com> Date: Tue, 2 Dec 2025 04:05:38 +0100 Subject: [PATCH 4/4] Change param size from > 0 to > 1 --- lib/fetch-wrapper/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/fetch-wrapper/index.ts b/lib/fetch-wrapper/index.ts index 0f11e580..80d4ce9b 100644 --- a/lib/fetch-wrapper/index.ts +++ b/lib/fetch-wrapper/index.ts @@ -81,7 +81,7 @@ export function installFetchWrapper( // Run deduplication after handlers have populated toolParameters cache const sessionId = state.lastSeenSessionId - if (sessionId && state.toolParameters.size > 0) { + 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()))