From 6ff41a872226acb0b3282f87fa935f333afdf18d Mon Sep 17 00:00:00 2001 From: Daniel Smolsky Date: Wed, 26 Nov 2025 13:39:20 -0500 Subject: [PATCH 1/5] refactor: extract extractParameterKey to display-utils module --- README.md | 15 ++++----- lib/deduplicator.ts | 75 ++----------------------------------------- lib/display-utils.ts | 76 ++++++++++++++++++++++++++++++++++++++++++++ lib/janitor.ts | 3 +- 4 files changed, 86 insertions(+), 83 deletions(-) create mode 100644 lib/display-utils.ts diff --git a/README.md b/README.md index 72e4a15..2fcc145 100644 --- a/README.md +++ b/README.md @@ -6,14 +6,6 @@ Automatically reduces token usage in OpenCode by removing obsolete tool outputs ![DCP in action](dcp-demo.png) -## Pruning Strategies - -DCP implements two complementary strategies: - -**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. - -**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. - ## Installation Add to your OpenCode config: @@ -29,8 +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. -> **Note:** Project `plugin` arrays override global completely—include all desired plugins in project config if using both. +## Pruning Strategies + +DCP implements two complementary strategies: +**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. + +**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. ## How It Works DCP is **non-destructive**—pruning state is kept in memory only. When requests go to your LLM, DCP replaces pruned outputs with a placeholder; original session data stays intact. diff --git a/lib/deduplicator.ts b/lib/deduplicator.ts index 2ba2c7d..1d64940 100644 --- a/lib/deduplicator.ts +++ b/lib/deduplicator.ts @@ -1,3 +1,5 @@ +import { extractParameterKey } from "./display-utils" + export interface DuplicateDetectionResult { duplicateIds: string[] // IDs to prune (older duplicates) deduplicationDetails: Map 50 - ? parameters.command.substring(0, 50) + "..." - : parameters.command - } - } - - if (tool === "webfetch" && parameters.url) { - return parameters.url - } - if (tool === "websearch" && parameters.query) { - return `"${parameters.query}"` - } - if (tool === "codesearch" && parameters.query) { - return `"${parameters.query}"` - } - - if (tool === "todowrite") { - return `${parameters.todos?.length || 0} todos` - } - if (tool === "todoread") { - return "read todo list" - } - - if (tool === "task" && parameters.description) { - return parameters.description - } - if (tool === "batch") { - return `${parameters.tool_calls?.length || 0} parallel tools` - } - - const paramStr = JSON.stringify(parameters) - if (paramStr === '{}' || paramStr === '[]' || paramStr === 'null') { - return '' - } - return paramStr.substring(0, 50) -} diff --git a/lib/display-utils.ts b/lib/display-utils.ts new file mode 100644 index 0000000..8006830 --- /dev/null +++ b/lib/display-utils.ts @@ -0,0 +1,76 @@ +/** + * Extracts a human-readable key from tool metadata for display purposes. + * Used by both deduplication and AI analysis to show what was pruned. + */ +export function extractParameterKey(metadata: { tool: string, parameters?: any }): string { + if (!metadata.parameters) return '' + + const { tool, parameters } = metadata + + if (tool === "read" && parameters.filePath) { + return parameters.filePath + } + if (tool === "write" && parameters.filePath) { + return parameters.filePath + } + if (tool === "edit" && parameters.filePath) { + return parameters.filePath + } + + if (tool === "list") { + return parameters.path || '(current directory)' + } + if (tool === "glob") { + if (parameters.pattern) { + const pathInfo = parameters.path ? ` in ${parameters.path}` : "" + return `"${parameters.pattern}"${pathInfo}` + } + return '(unknown pattern)' + } + if (tool === "grep") { + if (parameters.pattern) { + const pathInfo = parameters.path ? ` in ${parameters.path}` : "" + return `"${parameters.pattern}"${pathInfo}` + } + return '(unknown pattern)' + } + + if (tool === "bash") { + if (parameters.description) return parameters.description + if (parameters.command) { + return parameters.command.length > 50 + ? parameters.command.substring(0, 50) + "..." + : parameters.command + } + } + + if (tool === "webfetch" && parameters.url) { + return parameters.url + } + if (tool === "websearch" && parameters.query) { + return `"${parameters.query}"` + } + if (tool === "codesearch" && parameters.query) { + return `"${parameters.query}"` + } + + if (tool === "todowrite") { + return `${parameters.todos?.length || 0} todos` + } + if (tool === "todoread") { + return "read todo list" + } + + if (tool === "task" && parameters.description) { + return parameters.description + } + if (tool === "batch") { + return `${parameters.tool_calls?.length || 0} parallel tools` + } + + const paramStr = JSON.stringify(parameters) + if (paramStr === '{}' || paramStr === '[]' || paramStr === 'null') { + return '' + } + return paramStr.substring(0, 50) +} diff --git a/lib/janitor.ts b/lib/janitor.ts index 7013f84..b52926b 100644 --- a/lib/janitor.ts +++ b/lib/janitor.ts @@ -4,7 +4,8 @@ import type { PruningStrategy } from "./config" import { buildAnalysisPrompt } from "./prompt" import { selectModel, extractModelFromSession } from "./model-selector" import { estimateTokensBatch, formatTokenCount } from "./tokenizer" -import { detectDuplicates, extractParameterKey } from "./deduplicator" +import { detectDuplicates } from "./deduplicator" +import { extractParameterKey } from "./display-utils" export interface SessionStats { totalToolsPruned: number From 998a8d9eb4c5ebf188e589508c11a7e5e14b83d4 Mon Sep 17 00:00:00 2001 From: Daniel Smolsky Date: Wed, 26 Nov 2025 14:52:24 -0500 Subject: [PATCH 2/5] fix: access tool parameters from part.state.input instead of part.parameters Session message ToolPart stores input in part.state.input, not part.parameters. When parameters were undefined, all tools of the same type got identical signatures, causing the deduplicator to incorrectly mark them all as duplicates. --- lib/janitor.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/janitor.ts b/lib/janitor.ts index b52926b..ecc4491 100644 --- a/lib/janitor.ts +++ b/lib/janitor.ts @@ -109,7 +109,8 @@ export class Janitor { toolCallIds.push(normalizedId) const cachedData = this.toolParametersCache.get(part.callID) || this.toolParametersCache.get(normalizedId) - const parameters = cachedData?.parameters || part.parameters + // Session messages store input in part.state.input, not part.parameters + const parameters = cachedData?.parameters ?? part.state?.input ?? part.parameters toolMetadata.set(normalizedId, { tool: part.tool, From e0c505a29f373c82f35f211acdf6e5566edd9c3e Mon Sep 17 00:00:00 2001 From: Daniel Smolsky Date: Wed, 26 Nov 2025 15:07:26 -0500 Subject: [PATCH 3/5] fix: exclude batch tools from unknown metadata count in notification --- lib/janitor.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/janitor.ts b/lib/janitor.ts index ecc4491..c995576 100644 --- a/lib/janitor.ts +++ b/lib/janitor.ts @@ -109,7 +109,6 @@ export class Janitor { toolCallIds.push(normalizedId) const cachedData = this.toolParametersCache.get(part.callID) || this.toolParametersCache.get(normalizedId) - // Session messages store input in part.state.input, not part.parameters const parameters = cachedData?.parameters ?? part.state?.input ?? part.parameters toolMetadata.set(normalizedId, { @@ -670,6 +669,8 @@ export class Janitor { const missingTools = llmPrunedIds.filter(id => { const normalizedId = id.toLowerCase() const metadata = toolMetadata.get(normalizedId) + // batch tools are intentionally excluded from the summary, so don't count them as missing + if (metadata?.tool === 'batch') return false return !metadata || !foundToolNames.has(metadata.tool) }) From 16650b31b098647981c984ea16dfe5a6ff8ea38a Mon Sep 17 00:00:00 2001 From: Daniel Smolsky Date: Wed, 26 Nov 2025 15:08:38 -0500 Subject: [PATCH 4/5] refactor: remove redundant comment --- lib/janitor.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/janitor.ts b/lib/janitor.ts index c995576..738802b 100644 --- a/lib/janitor.ts +++ b/lib/janitor.ts @@ -669,7 +669,6 @@ export class Janitor { const missingTools = llmPrunedIds.filter(id => { const normalizedId = id.toLowerCase() const metadata = toolMetadata.get(normalizedId) - // batch tools are intentionally excluded from the summary, so don't count them as missing if (metadata?.tool === 'batch') return false return !metadata || !foundToolNames.has(metadata.tool) }) From 005e463abf4fb69b247627017ac7109edf45381c Mon Sep 17 00:00:00 2001 From: Daniel Smolsky Date: Wed, 26 Nov 2025 15:18:42 -0500 Subject: [PATCH 5/5] v0.3.18 - Bump version --- README.md | 2 +- package-lock.json | 4 ++-- package.json | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 2fcc145..d0907ca 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ Add to your OpenCode config: ```jsonc // opencode.jsonc { - "plugins": ["@tarquinen/opencode-dcp@0.3.17"] + "plugins": ["@tarquinen/opencode-dcp@0.3.18"] } ``` diff --git a/package-lock.json b/package-lock.json index 86a6bc1..0d58fa7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@tarquinen/opencode-dcp", - "version": "0.3.17", + "version": "0.3.18", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@tarquinen/opencode-dcp", - "version": "0.3.17", + "version": "0.3.18", "license": "MIT", "dependencies": { "@ai-sdk/openai-compatible": "^1.0.27", diff --git a/package.json b/package.json index d101121..857cd24 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package.json", "name": "@tarquinen/opencode-dcp", - "version": "0.3.17", + "version": "0.3.18", "type": "module", "description": "OpenCode plugin that optimizes token usage by pruning obsolete tool outputs from conversation context", "main": "./dist/index.js",