Skip to content
Closed
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
28 changes: 28 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,17 @@ DCP uses multiple tools and strategies to reduce context size:

Your session history is never modified—DCP replaces pruned content with placeholders before sending requests to your LLM.

### Preemptive Compaction (Experimental)

**Preemptive Compaction** — An optional multi-phase context reduction system that proactively manages context before it exceeds model limits. When enabled, it monitors token usage after each message and triggers compaction when usage exceeds a configurable threshold (default: 85%).

The compaction flow proceeds in phases:
1. **DCP Strategies** — Runs all enabled DCP strategies (deduplication, supersede writes, purge errors)
2. **Tool Truncation** — If still over threshold, truncates large tool outputs (preserving recent messages)
3. **Summarization** — If still over threshold, triggers OpenCode's built-in summarization

This prevents context overflow errors and maintains session continuity during long conversations. Disabled by default—enable via `preemptiveCompaction.enabled: true`.

## Impact on Prompt Caching

LLM providers like Anthropic and OpenAI cache prompts based on exact prefix matching. When DCP prunes a tool output, it changes the message content, which invalidates cached prefixes from that point forward.
Expand Down Expand Up @@ -121,6 +132,23 @@ DCP uses its own config file:
"protectedTools": [],
},
},
// Preemptive compaction (experimental) - proactively manages context before limits
"preemptiveCompaction": {
// Disabled by default - opt-in feature
"enabled": false,
// Context usage threshold to trigger compaction (0.0 - 1.0)
"threshold": 0.85,
// Cooldown between compaction attempts (ms)
"cooldownMs": 60000,
// Minimum tokens before compaction can trigger
"minTokens": 50000,
// Tool output truncation settings
"truncation": {
"enabled": true,
// Number of recent messages to protect from truncation
"protectedMessages": 3,
},
},
}
```

Expand Down
50 changes: 50 additions & 0 deletions dcp.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,56 @@
}
}
}
},
"preemptiveCompaction": {
"type": "object",
"description": "Preemptive compaction to avoid context overflow. Attempts DCP + truncation before falling back to summarization.",
"additionalProperties": false,
"properties": {
"enabled": {
"type": "boolean",
"default": false,
"description": "Enable preemptive compaction (opt-in feature)"
},
"threshold": {
"type": "number",
"minimum": 0.5,
"maximum": 0.95,
"default": 0.85,
"description": "Context usage ratio that triggers compaction (0.85 = 85%)"
},
"cooldownMs": {
"type": "number",
"minimum": 10000,
"maximum": 300000,
"default": 60000,
"description": "Minimum time between compaction attempts (milliseconds)"
},
"minTokens": {
"type": "number",
"default": 50000,
"description": "Minimum tokens before compaction can trigger"
},
"truncation": {
"type": "object",
"description": "Truncation phase configuration",
"additionalProperties": false,
"properties": {
"enabled": {
"type": "boolean",
"default": true,
"description": "Enable truncation of large tool outputs"
},
"protectedMessages": {
"type": "number",
"minimum": 1,
"maximum": 10,
"default": 3,
"description": "Number of recent messages to protect from truncation"
}
}
}
}
}
}
}
9 changes: 9 additions & 0 deletions index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { Logger } from "./lib/logger"
import { createSessionState } from "./lib/state"
import { createDiscardTool, createExtractTool } from "./lib/strategies"
import { createChatMessageTransformHandler, createSystemPromptHandler } from "./lib/hooks"
import { createPreemptiveCompactionHandler } from "./lib/preemptive"

const plugin: Plugin = (async (ctx) => {
const config = getConfig(ctx)
Expand All @@ -17,8 +18,14 @@ const plugin: Plugin = (async (ctx) => {

logger.info("DCP initialized", {
strategies: config.strategies,
preemptiveCompaction: config.preemptiveCompaction.enabled,
})

// Create preemptive compaction handler if enabled
const preemptiveHandler = config.preemptiveCompaction.enabled
? createPreemptiveCompactionHandler(ctx.client, ctx.directory, state, logger, config)
: null

return {
"experimental.chat.system.transform": createSystemPromptHandler(state, logger, config),

Expand Down Expand Up @@ -81,6 +88,8 @@ const plugin: Plugin = (async (ctx) => {
)
}
},
// Preemptive compaction event hook (handles message.updated events)
...(preemptiveHandler && { event: preemptiveHandler }),
}
}) satisfies Plugin

Expand Down
63 changes: 63 additions & 0 deletions lib/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,19 @@ export interface TurnProtection {
turns: number
}

export interface PreemptiveCompactionTruncation {
enabled: boolean
protectedMessages: number
}

export interface PreemptiveCompaction {
enabled: boolean
threshold: number
cooldownMs: number
minTokens: number
truncation: PreemptiveCompactionTruncation
}

export interface PluginConfig {
enabled: boolean
debug: boolean
Expand All @@ -57,6 +70,7 @@ export interface PluginConfig {
supersedeWrites: SupersedeWrites
purgeErrors: PurgeErrors
}
preemptiveCompaction: PreemptiveCompaction
}

const DEFAULT_PROTECTED_TOOLS = [
Expand Down Expand Up @@ -107,6 +121,15 @@ export const VALID_CONFIG_KEYS = new Set([
"strategies.purgeErrors.enabled",
"strategies.purgeErrors.turns",
"strategies.purgeErrors.protectedTools",
// preemptiveCompaction
"preemptiveCompaction",
"preemptiveCompaction.enabled",
"preemptiveCompaction.threshold",
"preemptiveCompaction.cooldownMs",
"preemptiveCompaction.minTokens",
"preemptiveCompaction.truncation",
"preemptiveCompaction.truncation.enabled",
"preemptiveCompaction.truncation.protectedMessages",
])

// Extract all key paths from a config object for validation
Expand Down Expand Up @@ -421,6 +444,16 @@ const defaultConfig: PluginConfig = {
protectedTools: [...DEFAULT_PROTECTED_TOOLS],
},
},
preemptiveCompaction: {
enabled: false, // Opt-in feature
threshold: 0.85,
cooldownMs: 60000,
minTokens: 50000,
truncation: {
enabled: true,
protectedMessages: 3,
},
},
}

const GLOBAL_CONFIG_DIR = join(homedir(), ".config", "opencode")
Expand Down Expand Up @@ -705,6 +738,16 @@ export function getConfig(ctx: PluginInput): PluginConfig {
],
tools: mergeTools(config.tools, result.data.tools as any),
strategies: mergeStrategies(config.strategies, result.data.strategies as any),
preemptiveCompaction: {
enabled: result.data.preemptiveCompaction?.enabled ?? config.preemptiveCompaction.enabled,
threshold: result.data.preemptiveCompaction?.threshold ?? config.preemptiveCompaction.threshold,
cooldownMs: result.data.preemptiveCompaction?.cooldownMs ?? config.preemptiveCompaction.cooldownMs,
minTokens: result.data.preemptiveCompaction?.minTokens ?? config.preemptiveCompaction.minTokens,
truncation: {
enabled: result.data.preemptiveCompaction?.truncation?.enabled ?? config.preemptiveCompaction.truncation.enabled,
protectedMessages: result.data.preemptiveCompaction?.truncation?.protectedMessages ?? config.preemptiveCompaction.truncation.protectedMessages,
},
},
}
}
} else {
Expand Down Expand Up @@ -747,6 +790,16 @@ export function getConfig(ctx: PluginInput): PluginConfig {
],
tools: mergeTools(config.tools, result.data.tools as any),
strategies: mergeStrategies(config.strategies, result.data.strategies as any),
preemptiveCompaction: {
enabled: result.data.preemptiveCompaction?.enabled ?? config.preemptiveCompaction.enabled,
threshold: result.data.preemptiveCompaction?.threshold ?? config.preemptiveCompaction.threshold,
cooldownMs: result.data.preemptiveCompaction?.cooldownMs ?? config.preemptiveCompaction.cooldownMs,
minTokens: result.data.preemptiveCompaction?.minTokens ?? config.preemptiveCompaction.minTokens,
truncation: {
enabled: result.data.preemptiveCompaction?.truncation?.enabled ?? config.preemptiveCompaction.truncation.enabled,
protectedMessages: result.data.preemptiveCompaction?.truncation?.protectedMessages ?? config.preemptiveCompaction.truncation.protectedMessages,
},
},
}
}
}
Expand Down Expand Up @@ -786,6 +839,16 @@ export function getConfig(ctx: PluginInput): PluginConfig {
],
tools: mergeTools(config.tools, result.data.tools as any),
strategies: mergeStrategies(config.strategies, result.data.strategies as any),
preemptiveCompaction: {
enabled: result.data.preemptiveCompaction?.enabled ?? config.preemptiveCompaction.enabled,
threshold: result.data.preemptiveCompaction?.threshold ?? config.preemptiveCompaction.threshold,
cooldownMs: result.data.preemptiveCompaction?.cooldownMs ?? config.preemptiveCompaction.cooldownMs,
minTokens: result.data.preemptiveCompaction?.minTokens ?? config.preemptiveCompaction.minTokens,
truncation: {
enabled: result.data.preemptiveCompaction?.truncation?.enabled ?? config.preemptiveCompaction.truncation.enabled,
protectedMessages: result.data.preemptiveCompaction?.truncation?.protectedMessages ?? config.preemptiveCompaction.truncation.protectedMessages,
},
},
}
}
}
Expand Down
40 changes: 40 additions & 0 deletions lib/preemptive/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
/**
* Constants for preemptive compaction feature
*/

// Default configuration values
export const DEFAULT_THRESHOLD = 0.85
export const DEFAULT_COOLDOWN_MS = 60000
export const MIN_TOKENS_FOR_COMPACTION = 50000
export const DEFAULT_PROTECTED_MESSAGES = 3
export const CHARS_PER_TOKEN = 4

// Message shown when tool output is truncated
export const TRUNCATION_MESSAGE =
"[TOOL RESULT TRUNCATED - Context limit exceeded. Original output was too large and has been truncated. Re-run this tool if you need the full output.]"

// Model context limits for inference when not configured
export const MODEL_CONTEXT_PATTERNS: Array<{ pattern: RegExp; limit: number }> = [
// Claude models (check for 1M context env vars)
{ pattern: /claude-(opus|sonnet|haiku)/i, limit: 200_000 },
// GPT-5.x models (1M context)
{ pattern: /gpt-5/i, limit: 1_000_000 },
// GPT-4 models
{ pattern: /gpt-4-turbo|gpt-4o/i, limit: 128_000 },
{ pattern: /gpt-4(?!o)/i, limit: 8_192 },
// OpenAI reasoning models
{ pattern: /o1|o3/i, limit: 200_000 },
// Gemini models
{ pattern: /gemini-3/i, limit: 2_000_000 },
{ pattern: /gemini-2\.5-pro/i, limit: 2_000_000 },
{ pattern: /gemini/i, limit: 1_000_000 },
]

// Fallback context limit when model is not recognized
export const DEFAULT_CONTEXT_LIMIT = 200_000

// Extended context for environments with 1M enabled
export const EXTENDED_CONTEXT_LIMIT =
process.env.ANTHROPIC_1M_CONTEXT === "true" || process.env.VERTEX_ANTHROPIC_1M_CONTEXT === "true"
? 1_000_000
: DEFAULT_CONTEXT_LIMIT
Loading