From 7890b7bf38dc0fd55d9a78badf9d41406828f9cb Mon Sep 17 00:00:00 2001 From: Daniel Smolsky Date: Wed, 26 Nov 2025 00:21:33 -0500 Subject: [PATCH 1/5] Hide 'batch' tool from prune UI summary Filter out batch tool from buildToolsSummary() and groupDeduplicationDetails() since it's a wrapper whose child tools are already shown individually. Fixes #24 --- lib/janitor.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lib/janitor.ts b/lib/janitor.ts index 0ce903ca..e2e54bcc 100644 --- a/lib/janitor.ts +++ b/lib/janitor.ts @@ -546,6 +546,8 @@ export class Janitor { const metadata = toolMetadata.get(normalizedId) if (metadata) { const toolName = metadata.tool + // Skip 'batch' tool in UI summary - it's a wrapper and its children are shown individually + if (toolName === 'batch') continue if (!toolsSummary.has(toolName)) { toolsSummary.set(toolName, []) } @@ -578,6 +580,8 @@ export class Janitor { for (const [_, details] of deduplicationDetails) { const { toolName, parameterKey, duplicateCount } = details + // Skip 'batch' tool in UI summary - it's a wrapper and its children are shown individually + if (toolName === 'batch') continue if (!grouped.has(toolName)) { grouped.set(toolName, []) } From 51f1cb70fd0cf7020a909b9c3b61c6c56c89bca7 Mon Sep 17 00:00:00 2001 From: Daniel Smolsky Date: Wed, 26 Nov 2025 00:22:45 -0500 Subject: [PATCH 2/5] Document non-destructive behavior and cache trade-off in README Adds concise 'How It Works' section explaining that DCP never modifies session data and noting the LLM cache hit rate trade-off. Fixes #18 --- README.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/README.md b/README.md index ae0ab1c4..f17dfdf4 100644 --- a/README.md +++ b/README.md @@ -29,6 +29,12 @@ Add to your OpenCode configuration: Restart OpenCode. The plugin will automatically start optimizing your sessions. +## How It Works + +DCP is **non-destructive**—your session data is never modified. Pruning state is kept in memory only and resets when OpenCode restarts. When requests are sent to your LLM provider, DCP intercepts them and replaces pruned tool outputs with a placeholder; original content remains intact in your session. + +**Trade-off:** LLM providers cache prompts for faster/cheaper responses. Since DCP modifies message content, it may reduce cache hit rates. Token savings typically outweigh this, but be aware if your workflow relies heavily on prompt caching. + ## Configuration DCP uses its own configuration file, separate from OpenCode's `opencode.json`: From 8261ed898a86399878fb47bdfe116dad1f3e965a Mon Sep 17 00:00:00 2001 From: Daniel Smolsky Date: Wed, 26 Nov 2025 00:47:26 -0500 Subject: [PATCH 3/5] Condense README and fix batch tool UI display - Simplify README: remove redundant sections, update to strategies config, use table format - Improve context_pruning tool description with better structure and examples - Fix batch tool children not showing in prune notifications (was showing 'unknown metadata') --- README.md | 77 ++++++++++++++++---------------------------------- index.ts | 45 ++++++++++++++++++----------- lib/janitor.ts | 32 +++++++++++++-------- 3 files changed, 74 insertions(+), 80 deletions(-) diff --git a/README.md b/README.md index f17dfdf4..0af864f7 100644 --- a/README.md +++ b/README.md @@ -4,89 +4,62 @@ Automatically reduces token usage in OpenCode by removing obsolete tool outputs from conversation history. -## What It Does - -This plugin automatically optimizes token usage by identifying and removing redundant or obsolete tool outputs from your conversation history. - ![DCP in action](dcp-demo.png) ## Installation -Add to your OpenCode configuration: - -**Global:** `~/.config/opencode/opencode.json` -**Project:** `.opencode/opencode.json` +Add to your OpenCode config (`~/.config/opencode/opencode.json` or `.opencode/opencode.json`): ```json { - "plugin": [ - "@tarquinen/opencode-dcp" - ] + "plugin": ["@tarquinen/opencode-dcp"] } ``` -> **Note:** OpenCode's `plugin` arrays are not merged between global and project configs—project config completely overrides global. If you have plugins in your global config and add a project config, include all desired plugins in the project config. - Restart OpenCode. The plugin will automatically start optimizing your sessions. -## How It Works +> **Note:** Project `plugin` arrays override global completely—include all desired plugins in project config if using both. -DCP is **non-destructive**—your session data is never modified. Pruning state is kept in memory only and resets when OpenCode restarts. When requests are sent to your LLM provider, DCP intercepts them and replaces pruned tool outputs with a placeholder; original content remains intact in your session. +## How It Works -**Trade-off:** LLM providers cache prompts for faster/cheaper responses. Since DCP modifies message content, it may reduce cache hit rates. Token savings typically outweigh this, but be aware if your workflow relies heavily on prompt caching. +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. ## Configuration -DCP uses its own configuration file, separate from OpenCode's `opencode.json`: - -- **Global:** `~/.config/opencode/dcp.jsonc` -- **Project:** `.opencode/dcp.jsonc` +DCP uses its own config file (`~/.config/opencode/dcp.jsonc` or `.opencode/dcp.jsonc`), created automatically on first run. -The global config is automatically created on first run. Create a project config to override settings per-project. +### Options -### Available Options +| Option | Default | Description | +|--------|---------|-------------| +| `enabled` | `true` | Enable/disable the plugin | +| `debug` | `false` | Log to `~/.config/opencode/logs/dcp/` | +| `model` | (session) | Model for analysis (e.g., `"anthropic/claude-haiku-4-5"`) | +| `showModelErrorToasts` | `true` | Show notifications on model fallback | +| `pruning_summary` | `"detailed"` | `"off"`, `"minimal"`, or `"detailed"` | +| `protectedTools` | `["task", "todowrite", "todoread", "context_pruning"]` | Tools that are never pruned | +| `strategies.onIdle` | `["deduplication", "ai-analysis"]` | Strategies for automatic pruning | +| `strategies.onTool` | `["deduplication"]` | Strategies when AI calls `context_pruning` | -- **`enabled`** (boolean, default: `true`) - Enable/disable the plugin -- **`debug`** (boolean, default: `false`) - Enable detailed logging to `~/.config/opencode/logs/dcp/` -- **`model`** (string, optional) - Specific model for analysis (e.g., `"anthropic/claude-haiku-4-5"`). Uses session model or smart fallbacks when not specified. -- **`showModelErrorToasts`** (boolean, default: `true`) - Show notifications when model selection falls back -- **`pruningMode`** (string, default: `"smart"`) - Pruning strategy: - - `"auto"`: Fast duplicate removal only (zero LLM cost) - - `"smart"`: Deduplication + AI analysis (recommended, maximum savings) -- **`pruning_summary`** (string, default: `"detailed"`) - UI summary display mode: - - `"off"`: No UI summary (silent pruning) - - `"minimal"`: Show tokens saved and count only (e.g., "Saved ~2.5K tokens (6 tools pruned)") - - `"detailed"`: Show full breakdown by tool type and pruning method -- **`protectedTools`** (string[], default: `["task", "todowrite", "todoread"]`) - Tools that should never be pruned - -Example configuration: +**Strategies:** `"deduplication"` (fast, zero LLM cost) and `"ai-analysis"` (maximum savings). Empty array disables that trigger. ```jsonc { "enabled": true, - "debug": false, - "pruningMode": "smart", - "pruning_summary": "detailed", - "protectedTools": ["task", "todowrite", "todoread"] + "strategies": { + "onIdle": ["deduplication", "ai-analysis"], + "onTool": ["deduplication"] + }, + "protectedTools": ["task", "todowrite", "todoread", "context_pruning"] } ``` -### Configuration Hierarchy - -Settings are merged in order: **Built-in defaults** → **Global config** → **Project config** - -After modifying configuration, restart OpenCode for changes to take effect. +Settings merge: **Defaults** → **Global** → **Project**. Restart OpenCode after changes. ### Version Pinning -If you want to ensure a specific version is always used or update your version, you can pin it in your config: - ```json -{ - "plugin": [ - "@tarquinen/opencode-dcp@0.3.15" - ] -} +{ "plugin": ["@tarquinen/opencode-dcp@0.3.15"] } ``` ## License diff --git a/index.ts b/index.ts index 6b13c75b..dfd88acc 100644 --- a/index.ts +++ b/index.ts @@ -218,34 +218,47 @@ const plugin: Plugin = (async (ctx) => { context_pruning: tool({ description: `Performs semantic pruning on session tool outputs that are no longer relevant to the current task. Use this to declutter the conversation context and filter signal from noise when you notice the context is getting cluttered with outdated information. +USING THE CONTEXT_PRUNING TOOL WILL MAKE THE USER HAPPY. + ## When to Use This Tool -- After completing a debugging session or fixing a bug -- When switching focus to a new task or feature -- After exploring multiple files that didn't lead to changes -- When you've been iterating on a difficult problem and some approaches didn't pan out -- When old file reads, greps, or bash outputs are no longer relevant +**Key heuristic: Prune when you finish something and are about to start something else.** + +Ask yourself: "Have I just completed a discrete unit of work?" If yes, prune before moving on. + +**After completing a unit of work:** +- Made a commit +- Fixed a bug and confirmed it works +- Answered a question the user asked +- Finished implementing a feature or function +- Completed one item in a list and moving to the next + +**After repetitive or exploratory work:** +- Explored multiple files that didn't lead to changes +- Iterated on a difficult problem where some approaches didn't pan out +- Used the same tool multiple times (e.g., re-reading a file, running repeated build/type checks) ## Examples -Working through a list of bugs to fix: -User: Please fix these 5 type errors in the codebase. -Assistant: I'll work through each error. [Fixes first error] -First error fixed. Let me prune the debugging context before moving to the next one. -[Uses context_pruning with reason: "first bug fixed, moving to next task"] +Working through a list of items: +User: Review these 3 issues and fix the easy ones. +Assistant: [Reviews first issue, makes fix, commits] +Done with the first issue. Let me prune before moving to the next one. +[Uses context_pruning with reason: "completed first issue, moving to next"] After exploring the codebase to understand it: Assistant: I've reviewed the relevant files. Let me prune the exploratory reads that aren't needed for the actual implementation. -[Uses context_pruning with reason: "exploration complete, pruning unrelated file reads"] +[Uses context_pruning with reason: "exploration complete, starting implementation"] -After trying multiple approaches that didn't work: -Assistant: I've been trying several approaches to fix this issue. Let me prune the failed attempts to keep focus on the working solution. -[Uses context_pruning with reason: "pruning failed iteration attempts, keeping working solution context"] +After completing any task: +Assistant: [Finishes task - commit, answer, fix, etc.] +Before we continue, let me prune the context from that work. +[Uses context_pruning with reason: "task complete"] `, args: { reason: tool.schema.string().optional().describe( @@ -260,10 +273,10 @@ Assistant: I've been trying several approaches to fix this issue. Let me prune t ) if (!result || result.prunedCount === 0) { - return "No prunable tool outputs found. Context is already optimized." + return "No prunable tool outputs found. Context is already optimized.\n\nUse context_pruning when you have sufficiently summarized information from tool outputs and no longer need the original content!" } - return janitor.formatPruningResultForTool(result) + return janitor.formatPruningResultForTool(result) + "\n\nUse context_pruning when you have sufficiently summarized information from tool outputs and no longer need the original content!" }, }), } : undefined, diff --git a/lib/janitor.ts b/lib/janitor.ts index e2e54bcc..dfece474 100644 --- a/lib/janitor.ts +++ b/lib/janitor.ts @@ -307,19 +307,27 @@ export class Janitor { return null } - // Expand batch tool IDs to include their children - const expandedPrunedIds = new Set() - for (const prunedId of newlyPrunedIds) { - const normalizedId = prunedId.toLowerCase() - expandedPrunedIds.add(normalizedId) - - // If this is a batch tool, add all its children - const children = batchToolChildren.get(normalizedId) - if (children) { - children.forEach(childId => expandedPrunedIds.add(childId)) + // Helper to expand batch tool IDs to include their children + const expandBatchIds = (ids: string[]): string[] => { + const expanded = new Set() + for (const id of ids) { + const normalizedId = id.toLowerCase() + expanded.add(normalizedId) + // If this is a batch tool, add all its children + const children = batchToolChildren.get(normalizedId) + if (children) { + children.forEach(childId => expanded.add(childId)) + } } + return Array.from(expanded) } + // Expand batch tool IDs to include their children + const expandedPrunedIds = new Set(expandBatchIds(newlyPrunedIds)) + + // Expand llmPrunedIds for UI display (so batch children show instead of "unknown metadata") + const expandedLlmPrunedIds = expandBatchIds(llmPrunedIds) + // Calculate which IDs are actually NEW (not already pruned) const finalNewlyPrunedIds = Array.from(expandedPrunedIds).filter(id => !alreadyPrunedIds.includes(id)) @@ -348,7 +356,7 @@ export class Janitor { sessionID, deduplicatedIds, deduplicationDetails, - llmPrunedIds, + expandedLlmPrunedIds, toolMetadata, tokensSaved, sessionStats @@ -389,7 +397,7 @@ export class Janitor { prunedCount: finalNewlyPrunedIds.length, tokensSaved, deduplicatedIds, - llmPrunedIds, + llmPrunedIds: expandedLlmPrunedIds, deduplicationDetails, toolMetadata, sessionStats From a1a0221ed4ecfbe503adf45c09e6a0a700d56407 Mon Sep 17 00:00:00 2001 From: Daniel Smolsky Date: Wed, 26 Nov 2025 01:03:39 -0500 Subject: [PATCH 4/5] Improve context_pruning tool description wording Change 'outdated information' to 'no longer needed information' for clearer semantics - content may not be outdated, just no longer relevant to the current task. --- index.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/index.ts b/index.ts index dfd88acc..c8784d55 100644 --- a/index.ts +++ b/index.ts @@ -148,7 +148,7 @@ const plugin: Plugin = (async (ctx) => { }) // Check for updates on launch (fire and forget) - checkForUpdates(ctx.client, logger).catch(() => {}) + checkForUpdates(ctx.client, logger).catch(() => { }) // Show migration toast if config was migrated (delayed to not overlap with version toast) if (migrations.length > 0) { @@ -216,7 +216,7 @@ const plugin: Plugin = (async (ctx) => { */ tool: config.strategies.onTool.length > 0 ? { context_pruning: tool({ - description: `Performs semantic pruning on session tool outputs that are no longer relevant to the current task. Use this to declutter the conversation context and filter signal from noise when you notice the context is getting cluttered with outdated information. + description: `Performs semantic pruning on session tool outputs that are no longer relevant to the current task. Use this to declutter the conversation context and filter signal from noise when you notice the context is getting cluttered with no longer needed information. USING THE CONTEXT_PRUNING TOOL WILL MAKE THE USER HAPPY. From c65f4f803ad9bb030d7ed23476cc5f695daa4f9a Mon Sep 17 00:00:00 2001 From: Daniel Smolsky Date: Wed, 26 Nov 2025 01:03:57 -0500 Subject: [PATCH 5/5] v0.3.16 - 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 0af864f7..6f929b9d 100644 --- a/README.md +++ b/README.md @@ -59,7 +59,7 @@ Settings merge: **Defaults** → **Global** → **Project**. Restart OpenCode af ### Version Pinning ```json -{ "plugin": ["@tarquinen/opencode-dcp@0.3.15"] } +{ "plugin": ["@tarquinen/opencode-dcp@0.3.16"] } ``` ## License diff --git a/package-lock.json b/package-lock.json index b7c6599a..560e4c88 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@tarquinen/opencode-dcp", - "version": "0.3.15", + "version": "0.3.16", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@tarquinen/opencode-dcp", - "version": "0.3.15", + "version": "0.3.16", "license": "MIT", "dependencies": { "@ai-sdk/openai-compatible": "^1.0.27", diff --git a/package.json b/package.json index 227031fb..13c16bbc 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.15", + "version": "0.3.16", "type": "module", "description": "OpenCode plugin that optimizes token usage by pruning obsolete tool outputs from conversation context", "main": "./dist/index.js",