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
18 changes: 9 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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"]
}
Expand Down
10 changes: 5 additions & 5 deletions lib/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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']
}
}

Expand Down Expand Up @@ -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",
Expand Down
16 changes: 16 additions & 0 deletions lib/fetch-wrapper/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand Down Expand Up @@ -78,6 +79,21 @@ export function installFetchWrapper(
}
}

// Run deduplication after handlers have populated toolParameters cache
const sessionId = state.lastSeenSessionId
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()))
const unpruned = toolIds.filter(id => !alreadyPrunedLower.has(id.toLowerCase()))
if (unpruned.length > 1) {
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)
}
Expand Down