From 7730dc43f1b430d49803e098b7d00098211eced7 Mon Sep 17 00:00:00 2001 From: Jorgen Henriksen Date: Sat, 13 Dec 2025 20:43:51 +0100 Subject: [PATCH 1/7] readme --- README.md | 83 ++++++++++++++++++++++++++++++++++++------------------- 1 file changed, 55 insertions(+), 28 deletions(-) diff --git a/README.md b/README.md index 6992da2..99fba49 100644 --- a/README.md +++ b/README.md @@ -23,17 +23,15 @@ Restart OpenCode. The plugin will automatically start optimizing your sessions. ## How Pruning Works -DCP uses two complementary techniques: +DCP uses multiple strategies to reduce context size: -**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. +**Deduplication** — Identifies repeated tool calls (e.g., reading the same file multiple times) and keeps only the most recent output. Runs automatically 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. +**Prune Thinking Blocks** — Removes LLM thinking/reasoning blocks from the conversation history. -## Context Pruning Tool +**On Idle Analysis** — Uses a language model to semantically analyze conversation context during idle periods and identify tool outputs that are no longer relevant. -When `strategies.onTool` is enabled, DCP exposes a `prune` tool to Opencode that the AI can call to trigger pruning on demand. - -Adjust `nudgeFreq` to control how aggressively the AI is prompted to prune — lower values trigger reminders sooner and more often. +**Prune Tool** — Exposes a `prune` tool that the AI can call to manually trigger pruning when it determines context cleanup is needed. ## How It Works @@ -49,35 +47,64 @@ LLM providers like Anthropic and OpenAI cache prompts based on exact prefix matc DCP uses its own config file (`~/.config/opencode/dcp.jsonc` or `.opencode/dcp.jsonc`), created automatically on first run. -### 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 | -| `showUpdateToasts` | `true` | Show notifications when a new version is available | -| `strictModelSelection` | `false` | Only run AI analysis with session or configured model (disables fallback models) | -| `pruningSummary` | `"detailed"` | `"off"`, `"minimal"`, or `"detailed"` | -| `nudgeFreq` | `10` | How often to remind AI to prune (lower = more frequent) | -| `protectedTools` | `["task", "todowrite", "todoread", "prune", "batch", "write", "edit"]` | Tools that are never pruned | -| `strategies.onIdle` | `["ai-analysis"]` | Strategies for automatic pruning | -| `strategies.onTool` | `["ai-analysis"]` | Strategies when AI calls `prune` | - -**Strategies:** `"ai-analysis"` uses LLM to identify prunable outputs. Empty array disables that trigger. Deduplication runs automatically on every request. +
+Default Configuration (click to expand) ```jsonc { + // Enable or disable the plugin "enabled": true, + // Enable debug logging to ~/.config/opencode/logs/dcp/ + "debug": false, + // Show toast notifications when a new version is available + "showUpdateToasts": true, + // Summary display: "off", "minimal", or "detailed" + "pruningSummary": "detailed", + // Strategies for pruning tokens from chat history "strategies": { - "onIdle": ["ai-analysis"], - "onTool": ["ai-analysis"] - }, - "protectedTools": ["task", "todowrite", "todoread", "prune", "batch", "write", "edit"] + // Remove duplicate tool calls (same tool with same arguments) + "deduplication": { + "enabled": true, + // Additional tools to protect from pruning + "protectedTools": [] + }, + // Remove thinking/reasoning LLM blocks + "pruneThinkingBlocks": { + "enabled": true + }, + // Run an LLM to analyze what tool calls are no longer relevant on idle + "onIdle": { + "enabled": true, + // Override model for analysis (format: "provider/model") + // "model": "anthropic/claude-haiku-4-5", + // Show toast notifications when model selection fails + "showModelErrorToasts": true, + // When true, fallback models are not permitted + "strictModelSelection": false, + // Additional tools to protect from pruning + "protectedTools": [] + }, + // Exposes a prune tool to your LLM to call when it determines pruning is necessary + "pruneTool": { + "enabled": false, + // Additional tools to protect from pruning + "protectedTools": [], + // How often to nudge the AI to prune (every N tool results, 0 = disabled) + "nudgeFrequency": 10 + } + } } ``` +
+ +### Protected Tools + +By default, these tools are always protected from pruning across all strategies: +`task`, `todowrite`, `todoread`, `prune`, `batch`, `write`, `edit` + +The `protectedTools` arrays in each strategy add to this default list. + ### Config Precedence Settings are merged in order: **Defaults** → **Global** (`~/.config/opencode/dcp.jsonc`) → **Project** (`.opencode/dcp.jsonc`). Each level overrides the previous, so project settings take priority over global, which takes priority over defaults. From 0914d82ef23cf418f8009261473a4ca6ac0ce52d Mon Sep 17 00:00:00 2001 From: Jorgen Henriksen Date: Sat, 13 Dec 2025 20:45:44 +0100 Subject: [PATCH 2/7] reamde --- README.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/README.md b/README.md index 99fba49..56eaee3 100644 --- a/README.md +++ b/README.md @@ -33,8 +33,6 @@ DCP uses multiple strategies to reduce context size: **Prune Tool** — Exposes a `prune` tool that the AI can call to manually trigger pruning when it determines context cleanup is needed. -## How It Works - Your session history is never modified. DCP replaces pruned outputs with a placeholder before sending requests to your LLM. ## Impact on Prompt Caching From 7c900345c817910340f68dac7029d5410dd10bf2 Mon Sep 17 00:00:00 2001 From: Jorgen Henriksen Date: Sat, 13 Dec 2025 21:02:51 +0100 Subject: [PATCH 3/7] sync --- lib/hooks.ts | 3 ++- lib/messages/prune.ts | 10 +++++++++- lib/state/state.ts | 4 +++- lib/state/tool-cache.ts | 6 ++++++ lib/state/types.ts | 1 + lib/strategies/prune-tool.ts | 1 + 6 files changed, 22 insertions(+), 3 deletions(-) diff --git a/lib/hooks.ts b/lib/hooks.ts index 92abeb2..b3dc9da 100644 --- a/lib/hooks.ts +++ b/lib/hooks.ts @@ -18,7 +18,8 @@ export function createChatMessageTransformHandler( output: { messages: WithParts[] } ) => { checkSession(state, logger, output.messages); - syncToolCache(state, logger, output.messages); + syncToolCache(state, config, logger, output.messages); + deduplicate(state, logger, config, output.messages) diff --git a/lib/messages/prune.ts b/lib/messages/prune.ts index e4a7fe9..34a78d3 100644 --- a/lib/messages/prune.ts +++ b/lib/messages/prune.ts @@ -3,8 +3,10 @@ import type { Logger } from "../logger" import type { PluginConfig } from "../config" import { buildToolIdList } from "../utils" import { getLastUserMessage, extractParameterKey } from "./utils" +import { loadPrompt } from "../prompt" const PRUNED_TOOL_OUTPUT_REPLACEMENT = '[Output removed to save context - information superseded or no longer needed]' +const NUDGE_STRING = loadPrompt("nudge") const buildPrunableToolsList = ( state: SessionState, @@ -45,6 +47,12 @@ export const insertPruneToolContext = ( const prunableToolsList = buildPrunableToolsList(state, config, logger, messages) + let nudgeString = "" + if (state.nudgeCounter >= config.strategies.pruneTool.nudgeFrequency) { + logger.info("Inserting prune nudge message") + nudgeString = "\n" + NUDGE_STRING + } + const userMessage: WithParts = { info: { id: "msg_01234567890123456789012345", @@ -63,7 +71,7 @@ export const insertPruneToolContext = ( sessionID: lastUserMessage.info.sessionID, messageID: "msg_01234567890123456789012345", type: "text", - text: prunableToolsList, + text: prunableToolsList + nudgeString, } ] } diff --git a/lib/state/state.ts b/lib/state/state.ts index 3e727a0..6682d6f 100644 --- a/lib/state/state.ts +++ b/lib/state/state.ts @@ -38,7 +38,8 @@ export function createSessionState(): SessionState { pruneTokenCounter: 0, totalPruneTokens: 0, }, - toolParameters: new Map() + toolParameters: new Map(), + nudgeCounter: 0 } } @@ -52,6 +53,7 @@ export function resetSessionState(state: SessionState): void { totalPruneTokens: 0, } state.toolParameters.clear() + state.nudgeCounter = 0 } export async function ensureSessionInitialized( diff --git a/lib/state/tool-cache.ts b/lib/state/tool-cache.ts index 215f9a5..d86005f 100644 --- a/lib/state/tool-cache.ts +++ b/lib/state/tool-cache.ts @@ -1,5 +1,6 @@ import type { SessionState, ToolStatus, WithParts } from "./index" import type { Logger } from "../logger" +import { PluginConfig } from "../config" const MAX_TOOL_CACHE_SIZE = 500 @@ -10,6 +11,7 @@ const MAX_TOOL_CACHE_SIZE = 500 */ export async function syncToolCache( state: SessionState, + config: PluginConfig, logger: Logger, messages: WithParts[], ): Promise { @@ -30,6 +32,10 @@ export async function syncToolCache( error: part.state.status === "error" ? part.state.error : undefined, } ) + + if (!config.strategies.pruneTool.protectedTools.includes(part.tool)) { + state.nudgeCounter++ + } } } diff --git a/lib/state/types.ts b/lib/state/types.ts index b0b6b69..750ca38 100644 --- a/lib/state/types.ts +++ b/lib/state/types.ts @@ -28,4 +28,5 @@ export interface SessionState { prune: Prune stats: SessionStats toolParameters: Map + nudgeCounter: number } diff --git a/lib/strategies/prune-tool.ts b/lib/strategies/prune-tool.ts index c48af54..08e680c 100644 --- a/lib/strategies/prune-tool.ts +++ b/lib/strategies/prune-tool.ts @@ -102,6 +102,7 @@ export function createPruneTool( ) state.stats.totalPruneTokens += state.stats.pruneTokenCounter state.stats.pruneTokenCounter = 0 + state.nudgeCounter = 0 return formatPruningResultForTool( pruneToolIds, From 8227d64143d8e52e0aa40e4696b619583126220b Mon Sep 17 00:00:00 2001 From: Jorgen Henriksen Date: Sat, 13 Dec 2025 21:13:02 +0100 Subject: [PATCH 4/7] ai nudge implementation --- README.md | 7 +++++-- lib/config.ts | 40 +++++++++++++++++++++++++++++++--------- lib/messages/prune.ts | 2 +- 3 files changed, 37 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 56eaee3..c2d63bb 100644 --- a/README.md +++ b/README.md @@ -87,8 +87,11 @@ DCP uses its own config file (`~/.config/opencode/dcp.jsonc` or `.opencode/dcp.j "enabled": false, // Additional tools to protect from pruning "protectedTools": [], - // How often to nudge the AI to prune (every N tool results, 0 = disabled) - "nudgeFrequency": 10 + // Nudge the LLM to use the prune tool (every tool results) + "nudge": { + "enabled": true, + "frequency": 10 + } } } } diff --git a/lib/config.ts b/lib/config.ts index 6352076..0a05235 100644 --- a/lib/config.ts +++ b/lib/config.ts @@ -21,10 +21,15 @@ export interface OnIdle { protectedTools: string[] } +export interface PruneToolNudge { + enabled: boolean + frequency: number +} + export interface PruneTool { enabled: boolean protectedTools: string[] - nudgeFrequency: number + nudge: PruneToolNudge } export interface PluginConfig { @@ -68,7 +73,9 @@ export const VALID_CONFIG_KEYS = new Set([ 'strategies.pruneTool', 'strategies.pruneTool.enabled', 'strategies.pruneTool.protectedTools', - 'strategies.pruneTool.nudgeFrequency', + 'strategies.pruneTool.nudge', + 'strategies.pruneTool.nudge.enabled', + 'strategies.pruneTool.nudge.frequency', ]) // Extract all key paths from a config object for validation @@ -160,8 +167,13 @@ function validateConfigTypes(config: Record): ValidationError[] { if (strategies.pruneTool.protectedTools !== undefined && !Array.isArray(strategies.pruneTool.protectedTools)) { errors.push({ key: 'strategies.pruneTool.protectedTools', expected: 'string[]', actual: typeof strategies.pruneTool.protectedTools }) } - if (strategies.pruneTool.nudgeFrequency !== undefined && typeof strategies.pruneTool.nudgeFrequency !== 'number') { - errors.push({ key: 'strategies.pruneTool.nudgeFrequency', expected: 'number', actual: typeof strategies.pruneTool.nudgeFrequency }) + if (strategies.pruneTool.nudge) { + if (strategies.pruneTool.nudge.enabled !== undefined && typeof strategies.pruneTool.nudge.enabled !== 'boolean') { + errors.push({ key: 'strategies.pruneTool.nudge.enabled', expected: 'boolean', actual: typeof strategies.pruneTool.nudge.enabled }) + } + if (strategies.pruneTool.nudge.frequency !== undefined && typeof strategies.pruneTool.nudge.frequency !== 'number') { + errors.push({ key: 'strategies.pruneTool.nudge.frequency', expected: 'number', actual: typeof strategies.pruneTool.nudge.frequency }) + } } } } @@ -237,7 +249,10 @@ const defaultConfig: PluginConfig = { pruneTool: { enabled: false, protectedTools: [...DEFAULT_PROTECTED_TOOLS], - nudgeFrequency: 10 + nudge: { + enabled: true, + frequency: 10 + } } } } @@ -328,8 +343,11 @@ function createDefaultConfig(): void { "enabled": false, // Additional tools to protect from pruning "protectedTools": [], - // How often to nudge the AI to prune (every N tool results, 0 = disabled) - "nudgeFrequency": 10 + // Nudge the LLM to use the prune tool (every tool results) + "nudge": { + "enabled": true, + "frequency": 10 + } } } } @@ -401,7 +419,10 @@ function mergeStrategies( ...(override.pruneTool?.protectedTools ?? []) ]) ], - nudgeFrequency: override.pruneTool?.nudgeFrequency ?? base.pruneTool.nudgeFrequency + nudge: { + enabled: override.pruneTool?.nudge?.enabled ?? base.pruneTool.nudge.enabled, + frequency: override.pruneTool?.nudge?.frequency ?? base.pruneTool.nudge.frequency + } } } } @@ -421,7 +442,8 @@ function deepCloneConfig(config: PluginConfig): PluginConfig { }, pruneTool: { ...config.strategies.pruneTool, - protectedTools: [...config.strategies.pruneTool.protectedTools] + protectedTools: [...config.strategies.pruneTool.protectedTools], + nudge: { ...config.strategies.pruneTool.nudge } } } } diff --git a/lib/messages/prune.ts b/lib/messages/prune.ts index 34a78d3..cca70d1 100644 --- a/lib/messages/prune.ts +++ b/lib/messages/prune.ts @@ -48,7 +48,7 @@ export const insertPruneToolContext = ( const prunableToolsList = buildPrunableToolsList(state, config, logger, messages) let nudgeString = "" - if (state.nudgeCounter >= config.strategies.pruneTool.nudgeFrequency) { + if (config.strategies.pruneTool.nudge.enabled && state.nudgeCounter >= config.strategies.pruneTool.nudge.frequency) { logger.info("Inserting prune nudge message") nudgeString = "\n" + NUDGE_STRING } From 75539fde9876fd5365213bef043d7fd530f6a3a9 Mon Sep 17 00:00:00 2001 From: Jorgen Henriksen Date: Sat, 13 Dec 2025 21:16:46 +0100 Subject: [PATCH 5/7] set onIdle and pruneThinkingBlock strategies to disabled by default --- README.md | 4 ++-- lib/config.ts | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index c2d63bb..cef186c 100644 --- a/README.md +++ b/README.md @@ -68,11 +68,11 @@ DCP uses its own config file (`~/.config/opencode/dcp.jsonc` or `.opencode/dcp.j }, // Remove thinking/reasoning LLM blocks "pruneThinkingBlocks": { - "enabled": true + "enabled": false }, // Run an LLM to analyze what tool calls are no longer relevant on idle "onIdle": { - "enabled": true, + "enabled": false, // Override model for analysis (format: "provider/model") // "model": "anthropic/claude-haiku-4-5", // Show toast notifications when model selection fails diff --git a/lib/config.ts b/lib/config.ts index 0a05235..b6c20a2 100644 --- a/lib/config.ts +++ b/lib/config.ts @@ -238,10 +238,10 @@ const defaultConfig: PluginConfig = { protectedTools: [...DEFAULT_PROTECTED_TOOLS] }, pruneThinkingBlocks: { - enabled: true + enabled: false }, onIdle: { - enabled: true, + enabled: false, showModelErrorToasts: true, strictModelSelection: false, protectedTools: [...DEFAULT_PROTECTED_TOOLS] @@ -324,11 +324,11 @@ function createDefaultConfig(): void { }, // Remove thinking/reasoning LLM blocks "pruneThinkingBlocks": { - "enabled": true + "enabled": false }, // Run an LLM to analyze what tool calls are no longer relevant on idle "onIdle": { - "enabled": true, + "enabled": false, // Override model for analysis (format: "provider/model") // "model": "anthropic/claude-haiku-4-5", // Show toast notifications when model selection fails From 5b9493da97d394384ee45d759bd29dbbc2ee875a Mon Sep 17 00:00:00 2001 From: Jorgen Henriksen Date: Sat, 13 Dec 2025 21:22:13 +0100 Subject: [PATCH 6/7] say onIdle is legacy --- README.md | 2 +- lib/config.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index cef186c..965f5f2 100644 --- a/README.md +++ b/README.md @@ -70,7 +70,7 @@ DCP uses its own config file (`~/.config/opencode/dcp.jsonc` or `.opencode/dcp.j "pruneThinkingBlocks": { "enabled": false }, - // Run an LLM to analyze what tool calls are no longer relevant on idle + // (Legacy) Run an LLM to analyze what tool calls are no longer relevant on idle "onIdle": { "enabled": false, // Override model for analysis (format: "provider/model") diff --git a/lib/config.ts b/lib/config.ts index b6c20a2..3dfdba2 100644 --- a/lib/config.ts +++ b/lib/config.ts @@ -326,7 +326,7 @@ function createDefaultConfig(): void { "pruneThinkingBlocks": { "enabled": false }, - // Run an LLM to analyze what tool calls are no longer relevant on idle + // (Legacy) Run an LLM to analyze what tool calls are no longer relevant on idle "onIdle": { "enabled": false, // Override model for analysis (format: "provider/model") From 07fa3c4784c6636db3a04d8530832a17d1f8a2a7 Mon Sep 17 00:00:00 2001 From: Jorgen Henriksen Date: Sat, 13 Dec 2025 21:55:17 +0100 Subject: [PATCH 7/7] workflow fix --- .github/workflows/pr-checks.yml | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/.github/workflows/pr-checks.yml b/.github/workflows/pr-checks.yml index f9c7804..e665cfc 100644 --- a/.github/workflows/pr-checks.yml +++ b/.github/workflows/pr-checks.yml @@ -2,9 +2,7 @@ name: PR Checks on: pull_request: - branches: [main, master] - push: - branches: [main, master] + branches: [master, dev] jobs: validate: