Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
75 changes: 27 additions & 48 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,83 +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.

## Configuration
> **Note:** Project `plugin` arrays override global completely—include all desired plugins in project config if using both.

## How It Works

DCP uses its own configuration file, separate from OpenCode's `opencode.json`:
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.

- **Global:** `~/.config/opencode/dcp.jsonc`
- **Project:** `.opencode/dcp.jsonc`
## Configuration

The global config is automatically created on first run. Create a project config to override settings per-project.
DCP uses its own config file (`~/.config/opencode/dcp.jsonc` or `.opencode/dcp.jsonc`), created automatically on first run.

### Available Options
### Options

- **`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
| 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` |

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/[email protected]"
]
}
{ "plugin": ["@tarquinen/[email protected]"] }
```

## License
Expand Down
49 changes: 31 additions & 18 deletions index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -216,36 +216,49 @@ 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.

## 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

<example>
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"]
</example>

<example>
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"]
</example>

<example>
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"]
</example>`,
args: {
reason: tool.schema.string().optional().describe(
Expand All @@ -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,
Expand Down
36 changes: 24 additions & 12 deletions lib/janitor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -307,19 +307,27 @@ export class Janitor {
return null
}

// Expand batch tool IDs to include their children
const expandedPrunedIds = new Set<string>()
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<string>()
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))

Expand Down Expand Up @@ -348,7 +356,7 @@ export class Janitor {
sessionID,
deduplicatedIds,
deduplicationDetails,
llmPrunedIds,
expandedLlmPrunedIds,
toolMetadata,
tokensSaved,
sessionStats
Expand Down Expand Up @@ -389,7 +397,7 @@ export class Janitor {
prunedCount: finalNewlyPrunedIds.length,
tokensSaved,
deduplicatedIds,
llmPrunedIds,
llmPrunedIds: expandedLlmPrunedIds,
deduplicationDetails,
toolMetadata,
sessionStats
Expand Down Expand Up @@ -546,6 +554,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, [])
}
Expand Down Expand Up @@ -578,6 +588,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, [])
}
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down