Skip to content

Commit 43add19

Browse files
committed
Add auto and smart pruning modes with dedicated deduplication phase
- Add pruningMode config option: 'auto' (fast, zero LLM cost) or 'smart' (comprehensive, max savings) - Extract deduplication logic into dedicated deduplicator module - Phase 1 (both modes): Automatic duplicate removal based on tool+parameter signature - Phase 2 (smart mode only): LLM analysis for intelligent obsolete detection - Update notifications to show separate sections for duplicates vs LLM-pruned tools - Update README with mode comparison and example notification formats - Add todowrite/todoread to default protected tools (stateful operations)
1 parent 573823b commit 43add19

File tree

6 files changed

+727
-305
lines changed

6 files changed

+727
-305
lines changed

README.md

Lines changed: 73 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,23 @@ Automatically reduces token usage in OpenCode by removing obsolete tool outputs
66

77
## What It Does
88

9-
When your OpenCode session becomes idle, this plugin analyzes your conversation and identifies tool outputs that are no longer relevant (superseded file reads, old errors that were fixed, exploratory searches, etc.). These obsolete outputs are pruned from future requests to save tokens and reduce costs.
9+
This plugin automatically optimizes token usage by identifying and removing redundant or obsolete tool outputs from your conversation history. It operates in two modes:
10+
11+
### Pruning Modes
12+
13+
**Auto Mode** (`"auto"`): Fast, deterministic duplicate removal
14+
- Removes duplicate tool calls (same tool + identical parameters)
15+
- Keeps only the most recent occurrence of each duplicate
16+
- Zero LLM inference costs
17+
- Instant, predictable results
18+
19+
**Smart Mode** (`"smart"`): Comprehensive intelligent pruning (recommended)
20+
- Phase 1: Automatic duplicate removal (same as auto mode)
21+
- Phase 2: AI analysis to identify obsolete outputs (superseded information, dead-end exploration, etc.)
22+
- Maximum token savings
23+
- Small LLM cost for analysis (reduced by deduplication first)
24+
25+
When your session becomes idle, the plugin analyzes your conversation and prunes tool outputs that are no longer relevant, saving tokens and reducing costs.
1026

1127
## Installation
1228

@@ -27,84 +43,92 @@ Restart OpenCode. The plugin will automatically start optimizing your sessions.
2743

2844
## Configuration
2945

30-
The plugin supports both global and project-level configuration:
46+
### Available Options
3147

32-
- **Global:** `~/.config/opencode/dcp.jsonc` - Applies to all OpenCode sessions
33-
- **Project:** `.opencode/dcp.jsonc` - Applies only to the current project
48+
- **`enabled`** (boolean, default: `true`) - Enable/disable the plugin
49+
- **`debug`** (boolean, default: `false`) - Enable detailed logging to `~/.config/opencode/logs/dcp/YYYY-MM-DD.log`
50+
- **`model`** (string, optional) - Specific model for analysis (e.g., `"anthropic/claude-haiku-4-5"`). Uses session model or smart fallbacks when not specified.
51+
- **`showModelErrorToasts`** (boolean, default: `true`) - Show notifications when model selection falls back
52+
- **`pruningMode`** (string, default: `"smart"`) - Pruning strategy:
53+
- `"auto"`: Fast duplicate removal only (zero LLM cost)
54+
- `"smart"`: Deduplication + AI analysis (recommended, maximum savings)
55+
- **`protectedTools`** (string[], default: `["task", "todowrite", "todoread"]`) - Tools that should never be pruned
3456

35-
Project configuration takes precedence over global configuration. The plugin creates a default global configuration file on first run.
57+
Example configuration:
3658

3759
```jsonc
3860
{
39-
// Enable or disable the Dynamic Context Pruning plugin
4061
"enabled": true,
41-
42-
// Enable debug logging to ~/.config/opencode/logs/dcp/YYYY-MM-DD.log
4362
"debug": false,
44-
45-
// Optional: Use a specific model for analysis (otherwise uses session model or smart fallbacks)
46-
// "model": "anthropic/claude-haiku-4-5",
47-
48-
// Show toast notifications when model selection fails and falls back
49-
"showModelErrorToasts": true,
50-
51-
// List of tools that should never be pruned from context
52-
// The 'task' tool is protected by default to preserve subagent coordination
53-
"protectedTools": ["task"]
63+
"pruningMode": "smart",
64+
"protectedTools": ["task", "todowrite", "todoread"]
5465
}
5566
```
5667

5768
### Configuration Hierarchy
5869

59-
1. **Defaults** - Built-in plugin defaults
60-
2. **Global config** (`~/.config/opencode/dcp.jsonc`) - Overrides defaults
61-
3. **Project config** (`.opencode/dcp.jsonc`) - Overrides global config
70+
1. **Built-in defaults** → 2. **Global config** (`~/.config/opencode/dcp.jsonc`) → 3. **Project config** (`.opencode/dcp.jsonc`)
6271

63-
This allows you to:
64-
- Set global defaults for all projects
65-
- Override settings per-project (e.g., disable for sensitive projects, use different models)
66-
- Commit project config to version control for team consistency
67-
68-
### Creating Project-Level Config
69-
70-
To create a project-specific configuration:
71-
72-
1. Create `.opencode` directory in your project root (if it doesn't exist)
73-
2. Create `dcp.jsonc` file inside `.opencode/`
74-
3. Add your project-specific settings
72+
The global config is automatically created on first run. Create project configs manually to override settings per-project:
7573

7674
```bash
77-
# In your project directory
7875
mkdir -p .opencode
7976
cat > .opencode/dcp.jsonc << 'EOF'
8077
{
81-
// Project-specific DCP settings
8278
"debug": true,
83-
"protectedTools": ["task", "read"]
79+
"pruningMode": "auto"
8480
}
8581
EOF
8682
```
8783

88-
The global config (`~/.config/opencode/dcp.jsonc`) is automatically created on first run. Project configs are opt-in and must be created manually.
84+
After modifying configuration, restart OpenCode for changes to take effect.
85+
86+
### Choosing a Pruning Mode
8987

90-
### Configuration Options
88+
**Use Auto Mode (`"auto"`) when:**
89+
- Minimizing costs is critical (zero LLM inference for pruning)
90+
- You have many repetitive tool calls (file re-reads, repeated commands)
91+
- You want predictable, deterministic behavior
92+
- You're debugging or testing and need consistent results
9193

92-
- **`enabled`** (boolean, default: `true`)
93-
Enable or disable the plugin without removing it from your OpenCode configuration.
94+
**Use Smart Mode (`"smart"`) when:**
95+
- You want maximum token savings (recommended for most users)
96+
- Your workflow has both duplicates and obsolete exploration
97+
- You're willing to incur small LLM costs for comprehensive pruning
98+
- You want the plugin to intelligently identify superseded information
9499

95-
- **`debug`** (boolean, default: `false`)
96-
Enable detailed debug logging. Logs are written to `~/.config/opencode/logs/dcp/YYYY-MM-DD.log`.
100+
**Example notification formats:**
97101

98-
- **`model`** (string, optional)
99-
Optional: Specify a model to use for analysis in "provider/model" format (e.g., `"anthropic/claude-haiku-4-5"`). When not specified, the plugin uses the current session model or falls back to authenticated providers in priority order.
102+
Auto mode:
103+
```
104+
🧹 DCP: Saved ~1.2K tokens (5 duplicate tools removed)
100105
101-
- **`showModelErrorToasts`** (boolean, default: `true`)
102-
Show toast notifications when model selection fails and falls back to another model. Set to `false` to disable these informational toasts.
106+
read (3 duplicates):
107+
~/project/src/index.ts (2× duplicate)
108+
~/project/lib/utils.ts (1× duplicate)
103109
104-
- **`protectedTools`** (string[], default: `["task"]`)
105-
List of tool names that should never be pruned from context. The `task` tool is protected by default to ensure subagent coordination works correctly.
110+
bash (2 duplicates):
111+
Run tests (2× duplicate)
112+
```
106113

107-
After modifying the configuration, restart OpenCode for changes to take effect.
114+
Smart mode:
115+
```
116+
🧹 DCP: Saved ~3.4K tokens (8 tools pruned)
117+
118+
📦 Duplicates removed (5):
119+
read:
120+
~/project/src/index.ts (3×)
121+
~/project/lib/utils.ts (2×)
122+
bash:
123+
Run tests (2×)
124+
125+
🤖 LLM analysis (3):
126+
grep (2):
127+
pattern: "old.*function"
128+
pattern: "deprecated"
129+
list (1):
130+
~/project/temp
131+
```
108132

109133
To check the latest available version:
110134

index.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ const plugin: Plugin = (async (ctx) => {
5656
const stateManager = new StateManager()
5757
const toolParametersCache = new Map<string, any>() // callID -> parameters
5858
const modelCache = new Map<string, { providerID: string; modelID: string }>() // sessionID -> model info
59-
const janitor = new Janitor(ctx.client, stateManager, logger, toolParametersCache, config.protectedTools, modelCache, config.model, config.showModelErrorToasts)
59+
const janitor = new Janitor(ctx.client, stateManager, logger, toolParametersCache, config.protectedTools, modelCache, config.model, config.showModelErrorToasts, config.pruningMode)
6060

6161
const cacheToolParameters = (messages: any[], component: string) => {
6262
for (const message of messages) {
@@ -170,6 +170,7 @@ const plugin: Plugin = (async (ctx) => {
170170
debug: config.debug,
171171
protectedTools: config.protectedTools,
172172
model: config.model,
173+
pruningMode: config.pruningMode,
173174
globalConfigFile: join(homedir(), ".config", "opencode", "dcp.jsonc"),
174175
projectConfigFile: ctx.directory ? join(ctx.directory, ".opencode", "dcp.jsonc") : "N/A",
175176
logDirectory: join(homedir(), ".config", "opencode", "logs", "dcp"),

lib/config.ts

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,15 @@ export interface PluginConfig {
1212
protectedTools: string[]
1313
model?: string // Format: "provider/model" (e.g., "anthropic/claude-haiku-4-5")
1414
showModelErrorToasts?: boolean // Show toast notifications when model selection fails
15+
pruningMode: "auto" | "smart" // Pruning strategy: auto (deduplication only) or smart (deduplication + LLM analysis)
1516
}
1617

1718
const defaultConfig: PluginConfig = {
1819
enabled: true, // Plugin is enabled by default
1920
debug: false, // Disable debug logging by default
20-
protectedTools: ['task'], // Tools that should never be pruned
21-
showModelErrorToasts: true // Show model error toasts by default
21+
protectedTools: ['task', 'todowrite', 'todoread'], // Tools that should never be pruned (including stateful tools)
22+
showModelErrorToasts: true, // Show model error toasts by default
23+
pruningMode: 'smart' // Default to smart mode (deduplication + LLM analysis)
2224
}
2325

2426
const GLOBAL_CONFIG_DIR = join(homedir(), '.config', 'opencode')
@@ -99,9 +101,15 @@ function createDefaultConfig(): void {
99101
// Set to false to disable these informational toasts
100102
"showModelErrorToasts": true,
101103
104+
// Pruning strategy:
105+
// "auto": Automatic duplicate removal only (fast, no LLM cost)
106+
// "smart": Deduplication + AI analysis for intelligent pruning (recommended)
107+
"pruningMode": "smart",
108+
102109
// List of tools that should never be pruned from context
103-
// The 'task' tool is protected by default to preserve subagent coordination
104-
"protectedTools": ["task"]
110+
// "task": Each subagent invocation is intentional
111+
// "todowrite"/"todoread": Stateful tools where each call matters
112+
"protectedTools": ["task", "todowrite", "todoread"]
105113
}
106114
`
107115

@@ -149,7 +157,8 @@ export function getConfig(ctx?: PluginInput): PluginConfig {
149157
debug: globalConfig.debug ?? config.debug,
150158
protectedTools: globalConfig.protectedTools ?? config.protectedTools,
151159
model: globalConfig.model ?? config.model,
152-
showModelErrorToasts: globalConfig.showModelErrorToasts ?? config.showModelErrorToasts
160+
showModelErrorToasts: globalConfig.showModelErrorToasts ?? config.showModelErrorToasts,
161+
pruningMode: globalConfig.pruningMode ?? config.pruningMode
153162
}
154163
logger.info('config', 'Loaded global config', { path: configPaths.global })
155164
}
@@ -168,7 +177,8 @@ export function getConfig(ctx?: PluginInput): PluginConfig {
168177
debug: projectConfig.debug ?? config.debug,
169178
protectedTools: projectConfig.protectedTools ?? config.protectedTools,
170179
model: projectConfig.model ?? config.model,
171-
showModelErrorToasts: projectConfig.showModelErrorToasts ?? config.showModelErrorToasts
180+
showModelErrorToasts: projectConfig.showModelErrorToasts ?? config.showModelErrorToasts,
181+
pruningMode: projectConfig.pruningMode ?? config.pruningMode
172182
}
173183
logger.info('config', 'Loaded project config (overrides global)', { path: configPaths.project })
174184
}

0 commit comments

Comments
 (0)