Skip to content

Commit 4c1e08e

Browse files
authored
Merge pull request #12 from Tarquinen/release/v0.3.11
Release v0.3.11
2 parents 21e2262 + 575659b commit 4c1e08e

File tree

7 files changed

+75
-154
lines changed

7 files changed

+75
-154
lines changed

README.md

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

77
## What It Does
88

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:
9+
This plugin automatically optimizes token usage by identifying and removing redundant or obsolete tool outputs from your conversation history.
1010

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.
11+
![DCP in action](dcp-demo.png)
2612

2713
## Installation
2814

@@ -39,10 +25,19 @@ Add to your OpenCode configuration:
3925
}
4026
```
4127

28+
> **Note:** OpenCode's `plugin` arrays are not merged between global and project configs—project config completely overrides global. If you have plugins in your global config and add a project config, include all desired plugins in the project config.
29+
4230
Restart OpenCode. The plugin will automatically start optimizing your sessions.
4331

4432
## Configuration
4533

34+
DCP uses its own configuration file, separate from OpenCode's `opencode.json`:
35+
36+
- **Global:** `~/.config/opencode/dcp.jsonc`
37+
- **Project:** `.opencode/dcp.jsonc`
38+
39+
The global config is automatically created on first run. Create a project config to override settings per-project.
40+
4641
### Available Options
4742

4843
- **`enabled`** (boolean, default: `true`) - Enable/disable the plugin
@@ -72,95 +67,18 @@ Example configuration:
7267

7368
### Configuration Hierarchy
7469

75-
1. **Built-in defaults** → 2. **Global config** (`~/.config/opencode/dcp.jsonc`) → 3. **Project config** (`.opencode/dcp.jsonc`)
76-
77-
The global config is automatically created on first run. Create project configs manually to override settings per-project:
78-
79-
```bash
80-
mkdir -p .opencode
81-
cat > .opencode/dcp.jsonc << 'EOF'
82-
{
83-
"debug": true,
84-
"pruningMode": "auto"
85-
}
86-
EOF
87-
```
70+
Settings are merged in order: **Built-in defaults****Global config****Project config**
8871

8972
After modifying configuration, restart OpenCode for changes to take effect.
9073

91-
### Choosing a Pruning Mode
92-
93-
**Use Auto Mode (`"auto"`) when:**
94-
- Minimizing costs is critical (zero LLM inference for pruning)
95-
- You have many repetitive tool calls (file re-reads, repeated commands)
96-
- You want predictable, deterministic behavior
97-
- You're debugging or testing and need consistent results
98-
99-
**Use Smart Mode (`"smart"`) when:**
100-
- You want maximum token savings (recommended for most users)
101-
- Your workflow has both duplicates and obsolete exploration
102-
- You're willing to incur small LLM costs for comprehensive pruning
103-
- You want the plugin to intelligently identify superseded information
104-
105-
**Example notification formats:**
106-
107-
**Detailed mode** (default):
108-
109-
Auto mode:
110-
```
111-
🧹 DCP: Saved ~1.2K tokens (5 duplicate tools removed)
112-
113-
read (3 duplicates):
114-
~/project/src/index.ts (2× duplicate)
115-
~/project/lib/utils.ts (1× duplicate)
116-
117-
bash (2 duplicates):
118-
Run tests (2× duplicate)
119-
```
120-
121-
Smart mode:
122-
```
123-
🧹 DCP: Saved ~3.4K tokens (8 tools pruned)
124-
125-
📦 Duplicates removed (5):
126-
read:
127-
~/project/src/index.ts (3×)
128-
~/project/lib/utils.ts (2×)
129-
bash:
130-
Run tests (2×)
131-
132-
🤖 LLM analysis (3):
133-
grep (2):
134-
pattern: "old.*function"
135-
pattern: "deprecated"
136-
list (1):
137-
~/project/temp
138-
```
139-
140-
**Minimal mode** (`"pruning_summary": "minimal"`):
141-
```
142-
🧹 DCP: Saved ~3.4K tokens (8 tools pruned)
143-
```
144-
145-
**Off mode** (`"pruning_summary": "off"`):
146-
```
147-
(No notification displayed - silent pruning)
148-
```
149-
150-
To check the latest available version:
151-
152-
```bash
153-
npm view @tarquinen/opencode-dcp version
154-
```
155-
15674
### Version Pinning
15775

15876
If you want to ensure a specific version is always used or update your version, you can pin it in your config:
15977

16078
```json
16179
{
16280
"plugin": [
163-
"@tarquinen/opencode-dcp@0.2.7"
81+
"@tarquinen/opencode-dcp@0.3.10"
16482
]
16583
}
16684
```

dcp-demo.png

130 KB
Loading

index.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,7 @@
22
import type { Plugin } from "@opencode-ai/plugin"
33
import { getConfig } from "./lib/config"
44
import { Logger } from "./lib/logger"
5-
import { StateManager } from "./lib/state"
6-
import { Janitor } from "./lib/janitor"
5+
import { Janitor, type SessionStats } from "./lib/janitor"
76

87
/**
98
* Checks if a session is a subagent (child session)
@@ -34,10 +33,11 @@ const plugin: Plugin = (async (ctx) => {
3433

3534
// Logger uses ~/.config/opencode/logs/dcp/ for consistent log location
3635
const logger = new Logger(config.debug)
37-
const stateManager = new StateManager()
36+
const prunedIdsState = new Map<string, string[]>()
37+
const statsState = new Map<string, SessionStats>()
3838
const toolParametersCache = new Map<string, any>() // callID -> parameters
3939
const modelCache = new Map<string, { providerID: string; modelID: string }>() // sessionID -> model info
40-
const janitor = new Janitor(ctx.client, stateManager, logger, toolParametersCache, config.protectedTools, modelCache, config.model, config.showModelErrorToasts, config.pruningMode, config.pruning_summary, ctx.directory)
40+
const janitor = new Janitor(ctx.client, prunedIdsState, statsState, logger, toolParametersCache, config.protectedTools, modelCache, config.model, config.showModelErrorToasts, config.pruningMode, config.pruning_summary, ctx.directory)
4141

4242
const cacheToolParameters = (messages: any[]) => {
4343
for (const message of messages) {
@@ -87,8 +87,8 @@ const plugin: Plugin = (async (ctx) => {
8787
if (allSessions.data) {
8888
for (const session of allSessions.data) {
8989
if (session.parentID) continue // Skip subagent sessions
90-
const prunedIds = await stateManager.get(session.id)
91-
prunedIds.forEach(id => allPrunedIds.add(id))
90+
const prunedIds = prunedIdsState.get(session.id) ?? []
91+
prunedIds.forEach((id: string) => allPrunedIds.add(id))
9292
}
9393
}
9494

lib/janitor.ts

Lines changed: 53 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,20 @@
11
import { z } from "zod"
22
import type { Logger } from "./logger"
3-
import type { StateManager, SessionStats } from "./state"
43
import { buildAnalysisPrompt } from "./prompt"
54
import { selectModel, extractModelFromSession } from "./model-selector"
65
import { estimateTokensBatch, formatTokenCount } from "./tokenizer"
76
import { detectDuplicates, extractParameterKey } from "./deduplicator"
87

8+
export interface SessionStats {
9+
totalToolsPruned: number
10+
totalTokensSaved: number
11+
}
12+
913
export class Janitor {
1014
constructor(
1115
private client: any,
12-
private stateManager: StateManager,
16+
private prunedIdsState: Map<string, string[]>,
17+
private statsState: Map<string, SessionStats>,
1318
private logger: Logger,
1419
private toolParametersCache: Map<string, any>,
1520
private protectedTools: string[],
@@ -114,7 +119,7 @@ export class Janitor {
114119
}
115120

116121
// Get already pruned IDs to filter them out
117-
const alreadyPrunedIds = await this.stateManager.get(sessionID)
122+
const alreadyPrunedIds = this.prunedIdsState.get(sessionID) ?? []
118123
const unprunedToolCallIds = toolCallIds.filter(id => !alreadyPrunedIds.includes(id))
119124

120125
// If there are no unpruned tool calls, skip analysis
@@ -269,7 +274,12 @@ export class Janitor {
269274
const tokensSaved = await this.calculateTokensSaved(finalNewlyPrunedIds, toolOutputs)
270275

271276
// Accumulate session stats (for showing cumulative totals in UI)
272-
const sessionStats = await this.stateManager.addStats(sessionID, finalNewlyPrunedIds.length, tokensSaved)
277+
const currentStats = this.statsState.get(sessionID) ?? { totalToolsPruned: 0, totalTokensSaved: 0 }
278+
const sessionStats: SessionStats = {
279+
totalToolsPruned: currentStats.totalToolsPruned + finalNewlyPrunedIds.length,
280+
totalTokensSaved: currentStats.totalTokensSaved + tokensSaved
281+
}
282+
this.statsState.set(sessionID, sessionStats)
273283

274284
if (this.pruningMode === "auto") {
275285
await this.sendAutoModeNotification(
@@ -296,7 +306,7 @@ export class Janitor {
296306
// ============================================================
297307
// Merge newly pruned IDs with existing ones (using expanded IDs)
298308
const allPrunedIds = [...new Set([...alreadyPrunedIds, ...finalPrunedIds])]
299-
await this.stateManager.set(sessionID, allPrunedIds)
309+
this.prunedIdsState.set(sessionID, allPrunedIds)
300310

301311
// Log final summary
302312
// Format: "Pruned 5/5 tools (~4.2K tokens), 0 kept" or with breakdown if both duplicate and llm
@@ -318,9 +328,38 @@ export class Janitor {
318328
/**
319329
* Helper function to shorten paths for display
320330
*/
321-
private shortenPath(path: string): string {
322-
// Replace home directory with ~
331+
private shortenPath(input: string): string {
332+
// Handle compound strings like: "pattern" in /absolute/path
333+
// Extract and shorten just the path portion
334+
const inPathMatch = input.match(/^(.+) in (.+)$/)
335+
if (inPathMatch) {
336+
const prefix = inPathMatch[1]
337+
const pathPart = inPathMatch[2]
338+
const shortenedPath = this.shortenSinglePath(pathPart)
339+
return `${prefix} in ${shortenedPath}`
340+
}
341+
342+
return this.shortenSinglePath(input)
343+
}
344+
345+
/**
346+
* Shorten a single path string
347+
*/
348+
private shortenSinglePath(path: string): string {
323349
const homeDir = require('os').homedir()
350+
351+
// Strip working directory FIRST (before ~ replacement) for cleaner relative paths
352+
if (this.workingDirectory) {
353+
if (path.startsWith(this.workingDirectory + '/')) {
354+
return path.slice(this.workingDirectory.length + 1)
355+
}
356+
// Exact match (the directory itself)
357+
if (path === this.workingDirectory) {
358+
return '.'
359+
}
360+
}
361+
362+
// Replace home directory with ~
324363
if (path.startsWith(homeDir)) {
325364
path = '~' + path.slice(homeDir.length)
326365
}
@@ -331,21 +370,18 @@ export class Janitor {
331370
return `${nodeModulesMatch[1]}/${nodeModulesMatch[2]}`
332371
}
333372

334-
// Strip working directory to show relative paths
373+
// Try matching against ~ version of working directory (for paths already with ~)
335374
if (this.workingDirectory) {
336-
// Try to match against the absolute working directory first
337-
if (path.startsWith(this.workingDirectory + '/')) {
338-
return path.slice(this.workingDirectory.length + 1)
339-
}
340-
341-
// Also try matching against ~ version of working directory
342375
const workingDirWithTilde = this.workingDirectory.startsWith(homeDir)
343376
? '~' + this.workingDirectory.slice(homeDir.length)
344377
: null
345378

346379
if (workingDirWithTilde && path.startsWith(workingDirWithTilde + '/')) {
347380
return path.slice(workingDirWithTilde.length + 1)
348381
}
382+
if (workingDirWithTilde && path === workingDirWithTilde) {
383+
return '.'
384+
}
349385
}
350386

351387
return path
@@ -468,7 +504,7 @@ export class Janitor {
468504

469505
// Add session totals if there's been more than one pruning run
470506
if (sessionStats.totalToolsPruned > totalPruned) {
471-
message += ` │ Session: ${sessionStats.totalToolsPruned} tools, ~${formatTokenCount(sessionStats.totalTokensSaved)} tokens`
507+
message += ` │ Session: ~${formatTokenCount(sessionStats.totalTokensSaved)} tokens, ${sessionStats.totalToolsPruned} tools`
472508
}
473509

474510
await this.sendIgnoredMessage(sessionID, message)
@@ -503,7 +539,7 @@ export class Janitor {
503539

504540
// Add session totals if there's been more than one pruning run
505541
if (sessionStats.totalToolsPruned > deduplicatedIds.length) {
506-
message += ` │ Session: ${sessionStats.totalToolsPruned} tools, ~${formatTokenCount(sessionStats.totalTokensSaved)} tokens`
542+
message += ` │ Session: ~${formatTokenCount(sessionStats.totalTokensSaved)} tokens, ${sessionStats.totalToolsPruned} tools`
507543
}
508544
message += '\n'
509545

@@ -570,7 +606,7 @@ export class Janitor {
570606

571607
// Add session totals if there's been more than one pruning run
572608
if (sessionStats.totalToolsPruned > totalPruned) {
573-
message += ` │ Session: ${sessionStats.totalToolsPruned} tools, ~${formatTokenCount(sessionStats.totalTokensSaved)} tokens`
609+
message += ` │ Session: ~${formatTokenCount(sessionStats.totalTokensSaved)} tokens, ${sessionStats.totalToolsPruned} tools`
574610
}
575611
message += '\n'
576612

lib/state.ts

Lines changed: 0 additions & 33 deletions
This file was deleted.

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"$schema": "https://json.schemastore.org/package.json",
33
"name": "@tarquinen/opencode-dcp",
4-
"version": "0.3.10",
4+
"version": "0.3.11",
55
"type": "module",
66
"description": "OpenCode plugin that optimizes token usage by pruning obsolete tool outputs from conversation context",
77
"main": "./dist/index.js",

0 commit comments

Comments
 (0)