Skip to content

Commit 38796d3

Browse files
authored
Merge pull request #29 from Tarquinen/docs/improve-readme-pruning-strategies
docs: Improve README with pruning strategies documentation
2 parents 98e90a9 + ef76025 commit 38796d3

File tree

12 files changed

+157
-658
lines changed

12 files changed

+157
-658
lines changed

README.md

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,16 +6,27 @@ Automatically reduces token usage in OpenCode by removing obsolete tool outputs
66

77
![DCP in action](dcp-demo.png)
88

9+
## Pruning Strategies
10+
11+
DCP implements two complementary strategies:
12+
13+
**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.
14+
15+
**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.
16+
917
## Installation
1018

11-
Add to your OpenCode config (`~/.config/opencode/opencode.json` or `.opencode/opencode.json`):
19+
Add to your OpenCode config:
1220

13-
```json
21+
```jsonc
22+
// opencode.jsonc
1423
{
15-
"plugin": ["@tarquinen/opencode-dcp"]
24+
"plugins": ["@tarquinen/opencode-dcp@0.3.17"]
1625
}
1726
```
1827

28+
When a new version is available, DCP will show a toast notification. Update by changing the version number in your config.
29+
1930
Restart OpenCode. The plugin will automatically start optimizing your sessions.
2031

2132
> **Note:** Project `plugin` arrays override global completely—include all desired plugins in project config if using both.
@@ -36,6 +47,7 @@ DCP uses its own config file (`~/.config/opencode/dcp.jsonc` or `.opencode/dcp.j
3647
| `debug` | `false` | Log to `~/.config/opencode/logs/dcp/` |
3748
| `model` | (session) | Model for analysis (e.g., `"anthropic/claude-haiku-4-5"`) |
3849
| `showModelErrorToasts` | `true` | Show notifications on model fallback |
50+
| `strictModelSelection` | `false` | Only run AI analysis with session or configured model (disables fallback models) |
3951
| `pruning_summary` | `"detailed"` | `"off"`, `"minimal"`, or `"detailed"` |
4052
| `protectedTools` | `["task", "todowrite", "todoread", "context_pruning"]` | Tools that are never pruned |
4153
| `strategies.onIdle` | `["deduplication", "ai-analysis"]` | Strategies for automatic pruning |
@@ -54,13 +66,11 @@ DCP uses its own config file (`~/.config/opencode/dcp.jsonc` or `.opencode/dcp.j
5466
}
5567
```
5668

57-
Settings merge: **Defaults****Global****Project**. Restart OpenCode after changes.
69+
### Config Precedence
5870

59-
### Version Pinning
71+
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.
6072

61-
```json
62-
{ "plugin": ["@tarquinen/[email protected]"] }
63-
```
73+
Restart OpenCode after making config changes.
6474

6575
## License
6676

index.ts

Lines changed: 6 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1,45 +1,36 @@
1-
// index.ts - Main plugin entry point for Dynamic Context Pruning
21
import type { Plugin } from "@opencode-ai/plugin"
32
import { tool } from "@opencode-ai/plugin"
43
import { getConfig } from "./lib/config"
54
import { Logger } from "./lib/logger"
65
import { Janitor, type SessionStats } from "./lib/janitor"
76
import { checkForUpdates } from "./lib/version-checker"
87

9-
/**
10-
* Checks if a session is a subagent (child session)
11-
* Subagent sessions should skip pruning operations
12-
*/
138
async function isSubagentSession(client: any, sessionID: string): Promise<boolean> {
149
try {
1510
const result = await client.session.get({ path: { id: sessionID } })
1611
return !!result.data?.parentID
1712
} catch (error: any) {
18-
// On error, assume it's not a subagent and continue (fail open)
1913
return false
2014
}
2115
}
2216

2317
const plugin: Plugin = (async (ctx) => {
2418
const { config, migrations } = getConfig(ctx)
2519

26-
// Exit early if plugin is disabled
2720
if (!config.enabled) {
2821
return {}
2922
}
3023

31-
// Suppress AI SDK warnings about responseFormat (harmless for our use case)
3224
if (typeof globalThis !== 'undefined') {
3325
(globalThis as any).AI_SDK_LOG_WARNINGS = false
3426
}
3527

36-
// Logger uses ~/.config/opencode/logs/dcp/ for consistent log location
3728
const logger = new Logger(config.debug)
3829
const prunedIdsState = new Map<string, string[]>()
3930
const statsState = new Map<string, SessionStats>()
40-
const toolParametersCache = new Map<string, any>() // callID -> parameters
41-
const modelCache = new Map<string, { providerID: string; modelID: string }>() // sessionID -> model info
42-
const janitor = new Janitor(ctx.client, prunedIdsState, statsState, logger, toolParametersCache, config.protectedTools, modelCache, config.model, config.showModelErrorToasts, config.pruning_summary, ctx.directory)
31+
const toolParametersCache = new Map<string, any>()
32+
const modelCache = new Map<string, { providerID: string; modelID: string }>()
33+
const janitor = new Janitor(ctx.client, prunedIdsState, statsState, logger, toolParametersCache, config.protectedTools, modelCache, config.model, config.showModelErrorToasts, config.strictModelSelection, config.pruning_summary, ctx.directory)
4334

4435
const cacheToolParameters = (messages: any[]) => {
4536
for (const message of messages) {
@@ -61,45 +52,37 @@ const plugin: Plugin = (async (ctx) => {
6152
parameters: params
6253
})
6354
} catch (error) {
64-
// Ignore JSON parse errors for individual tool calls
6555
}
6656
}
6757
}
6858
}
6959

70-
// Global fetch wrapper that both caches tool parameters AND performs pruning
71-
// This works because all providers ultimately call globalThis.fetch
60+
// Global fetch wrapper - caches tool parameters and performs pruning
7261
const originalGlobalFetch = globalThis.fetch
7362
globalThis.fetch = async (input: any, init?: any) => {
7463
if (init?.body && typeof init.body === 'string') {
7564
try {
7665
const body = JSON.parse(init.body)
7766
if (body.messages && Array.isArray(body.messages)) {
78-
// Cache tool parameters for janitor metadata
7967
cacheToolParameters(body.messages)
8068

81-
// Check for tool messages that might need pruning
8269
const toolMessages = body.messages.filter((m: any) => m.role === 'tool')
8370

84-
// Collect all pruned IDs across all sessions (excluding subagents)
85-
// This is safe because tool_call_ids are globally unique
8671
const allSessions = await ctx.client.session.list()
8772
const allPrunedIds = new Set<string>()
8873

8974
if (allSessions.data) {
9075
for (const session of allSessions.data) {
91-
if (session.parentID) continue // Skip subagent sessions
76+
if (session.parentID) continue
9277
const prunedIds = prunedIdsState.get(session.id) ?? []
9378
prunedIds.forEach((id: string) => allPrunedIds.add(id))
9479
}
9580
}
9681

97-
// Only process tool message replacement if there are tool messages and pruned IDs
9882
if (toolMessages.length > 0 && allPrunedIds.size > 0) {
9983
let replacedCount = 0
10084

10185
body.messages = body.messages.map((m: any) => {
102-
// Normalize ID to lowercase for case-insensitive matching
10386
if (m.role === 'tool' && allPrunedIds.has(m.tool_call_id?.toLowerCase())) {
10487
replacedCount++
10588
return {
@@ -116,7 +99,6 @@ const plugin: Plugin = (async (ctx) => {
11699
total: toolMessages.length
117100
})
118101

119-
// Save wrapped context to file if debug is enabled
120102
if (logger.enabled) {
121103
await logger.saveWrappedContext(
122104
"global",
@@ -129,13 +111,11 @@ const plugin: Plugin = (async (ctx) => {
129111
)
130112
}
131113

132-
// Update the request body with modified messages
133114
init.body = JSON.stringify(body)
134115
}
135116
}
136117
}
137118
} catch (e) {
138-
// Ignore parse errors and fall through to original fetch
139119
}
140120
}
141121

@@ -147,10 +127,8 @@ const plugin: Plugin = (async (ctx) => {
147127
model: config.model || "auto"
148128
})
149129

150-
// Check for updates on launch (fire and forget)
151130
checkForUpdates(ctx.client, logger).catch(() => { })
152131

153-
// Show migration toast if config was migrated (delayed to not overlap with version toast)
154132
if (migrations.length > 0) {
155133
setTimeout(async () => {
156134
try {
@@ -163,42 +141,27 @@ const plugin: Plugin = (async (ctx) => {
163141
}
164142
})
165143
} catch {
166-
// Silently fail - toast is non-critical
167144
}
168-
}, 7000) // 7s delay to show after version toast (6s) completes
145+
}, 7000)
169146
}
170147

171148
return {
172-
/**
173-
* Event Hook: Triggers janitor analysis when session becomes idle
174-
*/
175149
event: async ({ event }) => {
176150
if (event.type === "session.status" && event.properties.status.type === "idle") {
177-
// Skip pruning for subagent sessions
178151
if (await isSubagentSession(ctx.client, event.properties.sessionID)) return
179-
180-
// Skip if no idle strategies configured
181152
if (config.strategies.onIdle.length === 0) return
182153

183-
// Fire and forget the janitor - don't block the event handler
184154
janitor.runOnIdle(event.properties.sessionID, config.strategies.onIdle).catch(err => {
185155
logger.error("janitor", "Failed", { error: err.message })
186156
})
187157
}
188158
},
189159

190-
/**
191-
* Chat Params Hook: Caches model info for janitor
192-
*/
193160
"chat.params": async (input, _output) => {
194161
const sessionId = input.sessionID
195-
196-
// Cache model information for this session so janitor can access it
197-
// The provider.id is actually nested at provider.info.id (not in SDK types)
198162
let providerID = (input.provider as any)?.info?.id || input.provider?.id
199163
const modelID = input.model?.id
200164

201-
// If provider.id is not available, try to get it from the message
202165
if (!providerID && input.message?.model?.providerID) {
203166
providerID = input.message.model.providerID
204167
}
@@ -211,9 +174,6 @@ const plugin: Plugin = (async (ctx) => {
211174
}
212175
},
213176

214-
/**
215-
* Tool Hook: Exposes context_pruning tool to AI (if configured)
216-
*/
217177
tool: config.strategies.onTool.length > 0 ? {
218178
context_pruning: tool({
219179
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.

0 commit comments

Comments
 (0)