@tarquinen/opencode-dcp 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 tarquinen
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,90 @@
1
+ # Dynamic Context Pruning Plugin
2
+
3
+ [![npm version](https://img.shields.io/npm/v/@tarquinen/opencode-dcp.svg)](https://www.npmjs.com/package/@tarquinen/opencode-dcp)
4
+
5
+ OpenCode plugin that optimizes token usage by analyzing conversation history and pruning obsolete tool outputs from requests.
6
+
7
+ ## Features
8
+
9
+ - **Zero Configuration**: Uses the free `opencode/big-pickle` model - no API keys required
10
+ - **Automatic Optimization**: Runs in the background when sessions become idle
11
+ - **Smart Analysis**: Uses AI to identify truly obsolete context
12
+ - **Debug Logging**: Optional file-based logging for troubleshooting
13
+
14
+ ## How It Works
15
+
16
+ 1. When a session becomes idle, the Janitor analyzes the conversation using shadow inference
17
+ 2. It identifies tool call outputs that are no longer relevant to the conversation
18
+ 3. These IDs are stored in memory for that session
19
+ 4. On subsequent requests, a custom fetch function filters out the obsolete tool responses
20
+ 5. Result: Fewer tokens sent to the LLM, lower costs, faster responses
21
+
22
+ ## Installation
23
+
24
+ ### Via NPM (Recommended)
25
+
26
+ Add the plugin to your OpenCode configuration:
27
+
28
+ **Global:** `~/.config/opencode/opencode.json`
29
+ **Project:** `.opencode/opencode.json`
30
+
31
+ ```json
32
+ {
33
+ "plugin": [
34
+ "@tarquinen/opencode-dcp"
35
+ ]
36
+ }
37
+ ```
38
+
39
+ Then restart OpenCode. The plugin will automatically:
40
+ - Analyze conversations when sessions become idle
41
+ - Identify obsolete tool outputs
42
+ - Prune them from future requests
43
+ - Save you tokens and money!
44
+
45
+ ### Local Development
46
+
47
+ If you want to modify the plugin, you can clone this repository and use a local path:
48
+
49
+ ```json
50
+ {
51
+ "plugin": [
52
+ "./plugin/dynamic-context-pruning"
53
+ ]
54
+ }
55
+ ```
56
+
57
+ ## Configuration
58
+
59
+ No configuration required! The plugin works out of the box with:
60
+ - Zero-config setup using `opencode/big-pickle` model
61
+ - Automatic background optimization
62
+ - Smart AI-powered analysis
63
+
64
+ Optional: Enable debug logging by creating `~/.config/opencode/dynamic-context-pruning-config.json`:
65
+
66
+ ```json
67
+ {
68
+ "debug": true
69
+ }
70
+ ```
71
+
72
+ Debug logs will be written to the plugin's `logs/` directory.
73
+
74
+ ## Architecture
75
+
76
+ - **Janitor**: Background process that analyzes sessions using opencode/big-pickle model
77
+ - **State Manager**: In-memory store for pruned tool call IDs per session
78
+ - **Fetch Injector**: Injects custom fetch via `chat.params` hook
79
+ - **Pruning Fetch Wrapper**: Filters request bodies based on session state
80
+ - **Logger**: File-based debug logging system
81
+
82
+ ## Development Status
83
+
84
+ Current implementation status: **Step 1 - Project Scaffolding** ✅
85
+
86
+ See `@notes/dynamic-context-pruning/IMPLEMENTATION-PLAN.md` for full implementation details.
87
+
88
+ ## License
89
+
90
+ MIT
@@ -0,0 +1,24 @@
1
+ declare const _default: (ctx: import("@opencode-ai/plugin").PluginInput) => Promise<{
2
+ /**
3
+ * Event Hook: Triggers janitor analysis when session becomes idle
4
+ */
5
+ event: ({ event }: {
6
+ event: import("@opencode-ai/sdk").Event;
7
+ }) => Promise<void>;
8
+ /**
9
+ * Chat Params Hook: Wraps fetch function to filter pruned tool responses
10
+ */
11
+ "chat.params": (input: {
12
+ sessionID: string;
13
+ agent: string;
14
+ model: import("@opencode-ai/sdk").Model;
15
+ provider: import("@opencode-ai/sdk").Provider;
16
+ message: import("@opencode-ai/sdk").UserMessage;
17
+ }, output: {
18
+ temperature: number;
19
+ topP: number;
20
+ options: Record<string, any>;
21
+ }) => Promise<void>;
22
+ }>;
23
+ export default _default;
24
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../index.ts"],"names":[],"mappings":";IAqHQ;;OAEG;;;;IA8BH;;OAEG;;;;;;;;;;;;;AA9IX,wBAgRmB"}
package/dist/index.js ADDED
@@ -0,0 +1,245 @@
1
+ import { getConfig } from "./lib/config";
2
+ import { Logger } from "./lib/logger";
3
+ import { StateManager } from "./lib/state";
4
+ import { Janitor } from "./lib/janitor";
5
+ import { dirname } from "path";
6
+ import { fileURLToPath } from "url";
7
+ export default (async (ctx) => {
8
+ const config = getConfig();
9
+ // Suppress AI SDK warnings about responseFormat (harmless for our use case)
10
+ if (typeof globalThis !== 'undefined') {
11
+ globalThis.AI_SDK_LOG_WARNINGS = false;
12
+ }
13
+ // Get the plugin's own directory (not ctx.directory which is the main OpenCode config dir)
14
+ const pluginDir = dirname(fileURLToPath(import.meta.url));
15
+ const logger = new Logger(config.debug, pluginDir);
16
+ const stateManager = new StateManager();
17
+ const janitor = new Janitor(ctx.client, stateManager, logger);
18
+ // Track pruned counts per session for this request
19
+ const requestPrunedCounts = new Map();
20
+ // Store the original global fetch
21
+ const originalGlobalFetch = globalThis.fetch;
22
+ // Wrap globalThis.fetch to intercept ALL fetch calls
23
+ // This works because even if auth providers set a custom fetch,
24
+ // they ultimately call fetch() which goes through globalThis.fetch
25
+ globalThis.fetch = async (input, init) => {
26
+ // Check if this looks like an AI API request by examining the body
27
+ if (init?.body && typeof init.body === 'string') {
28
+ try {
29
+ const body = JSON.parse(init.body);
30
+ // Only process requests that have a messages array (AI requests)
31
+ if (body.messages && Array.isArray(body.messages)) {
32
+ logger.info("global-fetch", "🔥 AI REQUEST INTERCEPTED via global fetch!", {
33
+ url: typeof input === 'string' ? input.substring(0, 80) : 'URL object',
34
+ messageCount: body.messages.length
35
+ });
36
+ // Try to extract session ID from the request (might be in headers or we track it)
37
+ // For now, we'll use a simpler approach: collect ALL pruned IDs from all sessions
38
+ // This is safe because tool_call_ids are globally unique
39
+ const toolMessages = body.messages.filter((m) => m.role === 'tool');
40
+ if (toolMessages.length > 0) {
41
+ logger.debug("global-fetch", "Found tool messages in request", {
42
+ toolMessageCount: toolMessages.length,
43
+ toolCallIds: toolMessages.map((m) => m.tool_call_id).slice(0, 5)
44
+ });
45
+ // Collect all pruned IDs across all sessions, excluding subagent sessions
46
+ const allSessions = await ctx.client.session.list();
47
+ const allPrunedIds = new Set();
48
+ if (allSessions.data) {
49
+ for (const session of allSessions.data) {
50
+ // Skip subagent sessions (don't log - it's normal and would spam logs)
51
+ if (session.parentID) {
52
+ continue;
53
+ }
54
+ const prunedIds = await stateManager.get(session.id);
55
+ prunedIds.forEach(id => allPrunedIds.add(id));
56
+ }
57
+ }
58
+ if (allPrunedIds.size > 0) {
59
+ let replacedCount = 0;
60
+ body.messages = body.messages.map((m) => {
61
+ if (m.role === 'tool' && allPrunedIds.has(m.tool_call_id)) {
62
+ replacedCount++;
63
+ return {
64
+ ...m,
65
+ content: '[Output removed to save context - information superseded or no longer needed]'
66
+ };
67
+ }
68
+ return m;
69
+ });
70
+ if (replacedCount > 0) {
71
+ logger.info("global-fetch", "✂️ Replaced pruned tool messages", {
72
+ totalPrunedIds: allPrunedIds.size,
73
+ replacedCount: replacedCount,
74
+ totalMessages: body.messages.length
75
+ });
76
+ // Update the request body with modified messages
77
+ init.body = JSON.stringify(body);
78
+ }
79
+ }
80
+ }
81
+ }
82
+ }
83
+ catch (e) {
84
+ // Not a JSON body or not an AI request, ignore
85
+ }
86
+ }
87
+ // Call the original fetch
88
+ return originalGlobalFetch(input, init);
89
+ };
90
+ logger.info("plugin", "Dynamic Context Pruning plugin initialized", {
91
+ debug: config.debug,
92
+ pluginDirectory: pluginDir,
93
+ opencodeDirectory: ctx.directory,
94
+ globalFetchWrapped: true
95
+ });
96
+ return {
97
+ /**
98
+ * Event Hook: Triggers janitor analysis when session becomes idle
99
+ */
100
+ event: async ({ event }) => {
101
+ if (event.type === "session.status" && event.properties.status.type === "idle") {
102
+ // Get session info to check if it's a subagent
103
+ const result = await ctx.client.session.get({ path: { id: event.properties.sessionID } });
104
+ // Skip pruning for subagent sessions
105
+ if (result.data?.parentID) {
106
+ logger.debug("event", "Skipping janitor for subagent session", {
107
+ sessionID: event.properties.sessionID,
108
+ parentID: result.data.parentID
109
+ });
110
+ return;
111
+ }
112
+ logger.debug("event", "Session became idle, triggering janitor", {
113
+ sessionID: event.properties.sessionID
114
+ });
115
+ // Fire and forget the janitor - don't block the event handler
116
+ janitor.run(event.properties.sessionID).catch(err => {
117
+ logger.error("event", "Janitor failed", {
118
+ sessionID: event.properties.sessionID,
119
+ error: err.message,
120
+ stack: err.stack
121
+ });
122
+ });
123
+ }
124
+ },
125
+ /**
126
+ * Chat Params Hook: Wraps fetch function to filter pruned tool responses
127
+ */
128
+ "chat.params": async (input, output) => {
129
+ const sessionId = input.sessionID;
130
+ // Get session info to check if it's a subagent
131
+ const result = await ctx.client.session.get({ path: { id: sessionId } });
132
+ // Skip pruning for subagent sessions
133
+ if (result.data?.parentID) {
134
+ logger.debug("chat.params", "Skipping context pruning for subagent session", {
135
+ sessionID: sessionId,
136
+ parentID: result.data.parentID
137
+ });
138
+ return; // Don't wrap fetch, let it pass through unchanged
139
+ }
140
+ logger.debug("chat.params", "Wrapping fetch for session", {
141
+ sessionID: sessionId,
142
+ hasFetch: !!output.options["fetch"],
143
+ fetchType: output.options["fetch"] ? typeof output.options["fetch"] : "none"
144
+ });
145
+ // Get the existing fetch - this might be from auth provider or globalThis
146
+ const existingFetch = output.options["fetch"] ?? globalThis.fetch;
147
+ logger.debug("chat.params", "Existing fetch captured", {
148
+ sessionID: sessionId,
149
+ isGlobalFetch: existingFetch === globalThis.fetch
150
+ });
151
+ // Wrap the existing fetch with our pruning logic
152
+ output.options["fetch"] = async (fetchInput, init) => {
153
+ logger.info("pruning-fetch", "🔥 FETCH WRAPPER CALLED!", {
154
+ sessionId,
155
+ url: typeof fetchInput === 'string' ? fetchInput.substring(0, 100) : 'URL object'
156
+ });
157
+ logger.debug("pruning-fetch", "Request intercepted", { sessionId });
158
+ // Retrieve the list of pruned tool call IDs from state
159
+ const prunedIds = await stateManager.get(sessionId);
160
+ logger.debug("pruning-fetch", "Retrieved pruned IDs", {
161
+ sessionId,
162
+ prunedCount: prunedIds.length,
163
+ prunedIds: prunedIds.length > 0 ? prunedIds : undefined
164
+ });
165
+ // Log request body details before filtering
166
+ if (init?.body) {
167
+ try {
168
+ const bodyPreview = JSON.parse(init.body);
169
+ const toolMessages = bodyPreview.messages?.filter((m) => m.role === 'tool') || [];
170
+ logger.debug("pruning-fetch", "Request body before filtering", {
171
+ sessionId,
172
+ totalMessages: bodyPreview.messages?.length || 0,
173
+ toolMessages: toolMessages.length,
174
+ toolCallIds: toolMessages.map((m) => m.tool_call_id)
175
+ });
176
+ }
177
+ catch (e) {
178
+ // Ignore parse errors here
179
+ }
180
+ }
181
+ // Reset the count for this request
182
+ let prunedThisRequest = 0;
183
+ // Only attempt filtering if there are pruned IDs and a request body exists
184
+ if (prunedIds.length > 0 && init?.body) {
185
+ try {
186
+ // Parse the request body (expected to be JSON)
187
+ const body = JSON.parse(init.body);
188
+ const originalMessageCount = body.messages?.length || 0;
189
+ if (body.messages && Array.isArray(body.messages)) {
190
+ // Replace tool response messages whose tool_call_id is in the pruned list
191
+ // with a short placeholder message instead of removing them entirely.
192
+ // This preserves the message structure and avoids API validation errors.
193
+ body.messages = body.messages.map((m) => {
194
+ if (m.role === 'tool' && prunedIds.includes(m.tool_call_id)) {
195
+ prunedThisRequest++;
196
+ return {
197
+ ...m,
198
+ content: '[Output removed to save context - information superseded or no longer needed]'
199
+ };
200
+ }
201
+ return m;
202
+ });
203
+ if (prunedThisRequest > 0) {
204
+ logger.info("pruning-fetch", "Replaced pruned tool messages", {
205
+ sessionId,
206
+ totalMessages: originalMessageCount,
207
+ replacedCount: prunedThisRequest,
208
+ prunedIds
209
+ });
210
+ // Log remaining tool messages
211
+ const remainingToolMessages = body.messages.filter((m) => m.role === 'tool');
212
+ logger.debug("pruning-fetch", "Tool messages after replacement", {
213
+ sessionId,
214
+ totalToolCount: remainingToolMessages.length,
215
+ toolCallIds: remainingToolMessages.map((m) => m.tool_call_id)
216
+ });
217
+ // Track how many were pruned for this request
218
+ requestPrunedCounts.set(sessionId, prunedThisRequest);
219
+ // Update the request body with modified messages
220
+ init.body = JSON.stringify(body);
221
+ }
222
+ else {
223
+ logger.debug("pruning-fetch", "No messages replaced", {
224
+ sessionId,
225
+ messageCount: originalMessageCount
226
+ });
227
+ }
228
+ }
229
+ }
230
+ catch (error) {
231
+ logger.error("pruning-fetch", "Failed to parse/filter request body", {
232
+ sessionId,
233
+ error: error.message,
234
+ stack: error.stack
235
+ });
236
+ // Continue with original request if parsing fails - don't break the request
237
+ }
238
+ }
239
+ // Call the EXISTING fetch (which might be from auth provider) with potentially modified body
240
+ return existingFetch(fetchInput, init);
241
+ };
242
+ },
243
+ };
244
+ });
245
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../index.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,SAAS,EAAE,MAAM,cAAc,CAAA;AACxC,OAAO,EAAE,MAAM,EAAE,MAAM,cAAc,CAAA;AACrC,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAA;AAC1C,OAAO,EAAE,OAAO,EAAE,MAAM,eAAe,CAAA;AACvC,OAAO,EAAQ,OAAO,EAAE,MAAM,MAAM,CAAA;AACpC,OAAO,EAAE,aAAa,EAAE,MAAM,KAAK,CAAA;AAEnC,eAAe,CAAC,KAAK,EAAE,GAAG,EAAE,EAAE;IAC1B,MAAM,MAAM,GAAG,SAAS,EAAE,CAAA;IAE1B,4EAA4E;IAC5E,IAAI,OAAO,UAAU,KAAK,WAAW,EAAE,CAAC;QACnC,UAAkB,CAAC,mBAAmB,GAAG,KAAK,CAAA;IACnD,CAAC;IAED,2FAA2F;IAC3F,MAAM,SAAS,GAAG,OAAO,CAAC,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAA;IAEzD,MAAM,MAAM,GAAG,IAAI,MAAM,CAAC,MAAM,CAAC,KAAK,EAAE,SAAS,CAAC,CAAA;IAClD,MAAM,YAAY,GAAG,IAAI,YAAY,EAAE,CAAA;IACvC,MAAM,OAAO,GAAG,IAAI,OAAO,CAAC,GAAG,CAAC,MAAM,EAAE,YAAY,EAAE,MAAM,CAAC,CAAA;IAE7D,mDAAmD;IACnD,MAAM,mBAAmB,GAAG,IAAI,GAAG,EAAkB,CAAA;IAErD,kCAAkC;IAClC,MAAM,mBAAmB,GAAG,UAAU,CAAC,KAAK,CAAA;IAE5C,qDAAqD;IACrD,gEAAgE;IAChE,mEAAmE;IACnE,UAAU,CAAC,KAAK,GAAG,KAAK,EAAE,KAAU,EAAE,IAAU,EAAE,EAAE;QAChD,mEAAmE;QACnE,IAAI,IAAI,EAAE,IAAI,IAAI,OAAO,IAAI,CAAC,IAAI,KAAK,QAAQ,EAAE,CAAC;YAC9C,IAAI,CAAC;gBACD,MAAM,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;gBAElC,iEAAiE;gBACjE,IAAI,IAAI,CAAC,QAAQ,IAAI,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,QAAQ,CAAC,EAAE,CAAC;oBAChD,MAAM,CAAC,IAAI,CAAC,cAAc,EAAE,6CAA6C,EAAE;wBACvE,GAAG,EAAE,OAAO,KAAK,KAAK,QAAQ,CAAC,CAAC,CAAC,KAAK,CAAC,SAAS,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,YAAY;wBACtE,YAAY,EAAE,IAAI,CAAC,QAAQ,CAAC,MAAM;qBACrC,CAAC,CAAA;oBAEF,kFAAkF;oBAClF,kFAAkF;oBAClF,yDAAyD;oBAEzD,MAAM,YAAY,GAAG,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAM,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,MAAM,CAAC,CAAA;oBAExE,IAAI,YAAY,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;wBAC1B,MAAM,CAAC,KAAK,CAAC,cAAc,EAAE,gCAAgC,EAAE;4BAC3D,gBAAgB,EAAE,YAAY,CAAC,MAAM;4BACrC,WAAW,EAAE,YAAY,CAAC,GAAG,CAAC,CAAC,CAAM,EAAE,EAAE,CAAC,CAAC,CAAC,YAAY,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC;yBACxE,CAAC,CAAA;wBAEF,0EAA0E;wBAC1E,MAAM,WAAW,GAAG,MAAM,GAAG,CAAC,MAAM,CAAC,OAAO,CAAC,IAAI,EAAE,CAAA;wBACnD,MAAM,YAAY,GAAG,IAAI,GAAG,EAAU,CAAA;wBAEtC,IAAI,WAAW,CAAC,IAAI,EAAE,CAAC;4BACnB,KAAK,MAAM,OAAO,IAAI,WAAW,CAAC,IAAI,EAAE,CAAC;gCACrC,uEAAuE;gCACvE,IAAI,OAAO,CAAC,QAAQ,EAAE,CAAC;oCACnB,SAAQ;gCACZ,CAAC;gCAED,MAAM,SAAS,GAAG,MAAM,YAAY,CAAC,GAAG,CAAC,OAAO,CAAC,EAAE,CAAC,CAAA;gCACpD,SAAS,CAAC,OAAO,CAAC,EAAE,CAAC,EAAE,CAAC,YAAY,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,CAAA;4BACjD,CAAC;wBACL,CAAC;wBAED,IAAI,YAAY,CAAC,IAAI,GAAG,CAAC,EAAE,CAAC;4BACxB,IAAI,aAAa,GAAG,CAAC,CAAA;4BACrB,IAAI,CAAC,QAAQ,GAAG,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAM,EAAE,EAAE;gCACzC,IAAI,CAAC,CAAC,IAAI,KAAK,MAAM,IAAI,YAAY,CAAC,GAAG,CAAC,CAAC,CAAC,YAAY,CAAC,EAAE,CAAC;oCACxD,aAAa,EAAE,CAAA;oCACf,OAAO;wCACH,GAAG,CAAC;wCACJ,OAAO,EAAE,+EAA+E;qCAC3F,CAAA;gCACL,CAAC;gCACD,OAAO,CAAC,CAAA;4BACZ,CAAC,CAAC,CAAA;4BAEF,IAAI,aAAa,GAAG,CAAC,EAAE,CAAC;gCACpB,MAAM,CAAC,IAAI,CAAC,cAAc,EAAE,kCAAkC,EAAE;oCAC5D,cAAc,EAAE,YAAY,CAAC,IAAI;oCACjC,aAAa,EAAE,aAAa;oCAC5B,aAAa,EAAE,IAAI,CAAC,QAAQ,CAAC,MAAM;iCACtC,CAAC,CAAA;gCAEF,iDAAiD;gCACjD,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,CAAA;4BACpC,CAAC;wBACL,CAAC;oBACL,CAAC;gBACL,CAAC;YACL,CAAC;YAAC,OAAO,CAAC,EAAE,CAAC;gBACT,+CAA+C;YACnD,CAAC;QACL,CAAC;QAED,0BAA0B;QAC1B,OAAO,mBAAmB,CAAC,KAAK,EAAE,IAAI,CAAC,CAAA;IAC3C,CAAC,CAAA;IAED,MAAM,CAAC,IAAI,CAAC,QAAQ,EAAE,4CAA4C,EAAE;QAChE,KAAK,EAAE,MAAM,CAAC,KAAK;QACnB,eAAe,EAAE,SAAS;QAC1B,iBAAiB,EAAE,GAAG,CAAC,SAAS;QAChC,kBAAkB,EAAE,IAAI;KAC3B,CAAC,CAAA;IAEF,OAAO;QACH;;WAEG;QACH,KAAK,EAAE,KAAK,EAAE,EAAE,KAAK,EAAE,EAAE,EAAE;YACvB,IAAI,KAAK,CAAC,IAAI,KAAK,gBAAgB,IAAI,KAAK,CAAC,UAAU,CAAC,MAAM,CAAC,IAAI,KAAK,MAAM,EAAE,CAAC;gBAC7E,+CAA+C;gBAC/C,MAAM,MAAM,GAAG,MAAM,GAAG,CAAC,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,EAAE,IAAI,EAAE,EAAE,EAAE,EAAE,KAAK,CAAC,UAAU,CAAC,SAAS,EAAE,EAAE,CAAC,CAAA;gBAEzF,qCAAqC;gBACrC,IAAI,MAAM,CAAC,IAAI,EAAE,QAAQ,EAAE,CAAC;oBACxB,MAAM,CAAC,KAAK,CAAC,OAAO,EAAE,uCAAuC,EAAE;wBAC3D,SAAS,EAAE,KAAK,CAAC,UAAU,CAAC,SAAS;wBACrC,QAAQ,EAAE,MAAM,CAAC,IAAI,CAAC,QAAQ;qBACjC,CAAC,CAAA;oBACF,OAAM;gBACV,CAAC;gBAED,MAAM,CAAC,KAAK,CAAC,OAAO,EAAE,yCAAyC,EAAE;oBAC7D,SAAS,EAAE,KAAK,CAAC,UAAU,CAAC,SAAS;iBACxC,CAAC,CAAA;gBAEF,8DAA8D;gBAC9D,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,UAAU,CAAC,SAAS,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC,EAAE;oBAChD,MAAM,CAAC,KAAK,CAAC,OAAO,EAAE,gBAAgB,EAAE;wBACpC,SAAS,EAAE,KAAK,CAAC,UAAU,CAAC,SAAS;wBACrC,KAAK,EAAE,GAAG,CAAC,OAAO;wBAClB,KAAK,EAAE,GAAG,CAAC,KAAK;qBACnB,CAAC,CAAA;gBACN,CAAC,CAAC,CAAA;YACN,CAAC;QACL,CAAC;QAED;;WAEG;QACH,aAAa,EAAE,KAAK,EAAE,KAAK,EAAE,MAAM,EAAE,EAAE;YACnC,MAAM,SAAS,GAAG,KAAK,CAAC,SAAS,CAAA;YAEjC,+CAA+C;YAC/C,MAAM,MAAM,GAAG,MAAM,GAAG,CAAC,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,EAAE,IAAI,EAAE,EAAE,EAAE,EAAE,SAAS,EAAE,EAAE,CAAC,CAAA;YAExE,qCAAqC;YACrC,IAAI,MAAM,CAAC,IAAI,EAAE,QAAQ,EAAE,CAAC;gBACxB,MAAM,CAAC,KAAK,CAAC,aAAa,EAAE,+CAA+C,EAAE;oBACzE,SAAS,EAAE,SAAS;oBACpB,QAAQ,EAAE,MAAM,CAAC,IAAI,CAAC,QAAQ;iBACjC,CAAC,CAAA;gBACF,OAAM,CAAE,kDAAkD;YAC9D,CAAC;YAED,MAAM,CAAC,KAAK,CAAC,aAAa,EAAE,4BAA4B,EAAE;gBACtD,SAAS,EAAE,SAAS;gBACpB,QAAQ,EAAE,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,OAAO,CAAC;gBACnC,SAAS,EAAE,MAAM,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,OAAO,MAAM,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,MAAM;aAC/E,CAAC,CAAA;YAEF,0EAA0E;YAC1E,MAAM,aAAa,GAAG,MAAM,CAAC,OAAO,CAAC,OAAO,CAAC,IAAI,UAAU,CAAC,KAAK,CAAA;YAEjE,MAAM,CAAC,KAAK,CAAC,aAAa,EAAE,yBAAyB,EAAE;gBACnD,SAAS,EAAE,SAAS;gBACpB,aAAa,EAAE,aAAa,KAAK,UAAU,CAAC,KAAK;aACpD,CAAC,CAAA;YAEF,iDAAiD;YACjD,MAAM,CAAC,OAAO,CAAC,OAAO,CAAC,GAAG,KAAK,EAAE,UAAe,EAAE,IAAU,EAAE,EAAE;gBAC5D,MAAM,CAAC,IAAI,CAAC,eAAe,EAAE,0BAA0B,EAAE;oBACrD,SAAS;oBACT,GAAG,EAAE,OAAO,UAAU,KAAK,QAAQ,CAAC,CAAC,CAAC,UAAU,CAAC,SAAS,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC,CAAC,CAAC,YAAY;iBACpF,CAAC,CAAA;gBACF,MAAM,CAAC,KAAK,CAAC,eAAe,EAAE,qBAAqB,EAAE,EAAE,SAAS,EAAE,CAAC,CAAA;gBAEnE,uDAAuD;gBACvD,MAAM,SAAS,GAAG,MAAM,YAAY,CAAC,GAAG,CAAC,SAAS,CAAC,CAAA;gBACnD,MAAM,CAAC,KAAK,CAAC,eAAe,EAAE,sBAAsB,EAAE;oBAClD,SAAS;oBACT,WAAW,EAAE,SAAS,CAAC,MAAM;oBAC7B,SAAS,EAAE,SAAS,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,SAAS;iBAC1D,CAAC,CAAA;gBAEF,4CAA4C;gBAC5C,IAAI,IAAI,EAAE,IAAI,EAAE,CAAC;oBACb,IAAI,CAAC;wBACD,MAAM,WAAW,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,IAAc,CAAC,CAAA;wBACnD,MAAM,YAAY,GAAG,WAAW,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAC,CAAM,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,MAAM,CAAC,IAAI,EAAE,CAAA;wBACtF,MAAM,CAAC,KAAK,CAAC,eAAe,EAAE,+BAA+B,EAAE;4BAC3D,SAAS;4BACT,aAAa,EAAE,WAAW,CAAC,QAAQ,EAAE,MAAM,IAAI,CAAC;4BAChD,YAAY,EAAE,YAAY,CAAC,MAAM;4BACjC,WAAW,EAAE,YAAY,CAAC,GAAG,CAAC,CAAC,CAAM,EAAE,EAAE,CAAC,CAAC,CAAC,YAAY,CAAC;yBAC5D,CAAC,CAAA;oBACN,CAAC;oBAAC,OAAO,CAAC,EAAE,CAAC;wBACT,2BAA2B;oBAC/B,CAAC;gBACL,CAAC;gBAED,mCAAmC;gBACnC,IAAI,iBAAiB,GAAG,CAAC,CAAA;gBAEzB,2EAA2E;gBAC3E,IAAI,SAAS,CAAC,MAAM,GAAG,CAAC,IAAI,IAAI,EAAE,IAAI,EAAE,CAAC;oBACrC,IAAI,CAAC;wBACD,+CAA+C;wBAC/C,MAAM,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,IAAc,CAAC,CAAA;wBAC5C,MAAM,oBAAoB,GAAG,IAAI,CAAC,QAAQ,EAAE,MAAM,IAAI,CAAC,CAAA;wBAEvD,IAAI,IAAI,CAAC,QAAQ,IAAI,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,QAAQ,CAAC,EAAE,CAAC;4BAChD,0EAA0E;4BAC1E,sEAAsE;4BACtE,yEAAyE;4BACzE,IAAI,CAAC,QAAQ,GAAG,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAM,EAAE,EAAE;gCACzC,IAAI,CAAC,CAAC,IAAI,KAAK,MAAM,IAAI,SAAS,CAAC,QAAQ,CAAC,CAAC,CAAC,YAAY,CAAC,EAAE,CAAC;oCAC1D,iBAAiB,EAAE,CAAA;oCACnB,OAAO;wCACH,GAAG,CAAC;wCACJ,OAAO,EAAE,+EAA+E;qCAC3F,CAAA;gCACL,CAAC;gCACD,OAAO,CAAC,CAAA;4BACZ,CAAC,CAAC,CAAA;4BAEF,IAAI,iBAAiB,GAAG,CAAC,EAAE,CAAC;gCACxB,MAAM,CAAC,IAAI,CAAC,eAAe,EAAE,+BAA+B,EAAE;oCAC1D,SAAS;oCACT,aAAa,EAAE,oBAAoB;oCACnC,aAAa,EAAE,iBAAiB;oCAChC,SAAS;iCACZ,CAAC,CAAA;gCAEF,8BAA8B;gCAC9B,MAAM,qBAAqB,GAAG,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAM,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,MAAM,CAAC,CAAA;gCACjF,MAAM,CAAC,KAAK,CAAC,eAAe,EAAE,iCAAiC,EAAE;oCAC7D,SAAS;oCACT,cAAc,EAAE,qBAAqB,CAAC,MAAM;oCAC5C,WAAW,EAAE,qBAAqB,CAAC,GAAG,CAAC,CAAC,CAAM,EAAE,EAAE,CAAC,CAAC,CAAC,YAAY,CAAC;iCACrE,CAAC,CAAA;gCAEF,8CAA8C;gCAC9C,mBAAmB,CAAC,GAAG,CAAC,SAAS,EAAE,iBAAiB,CAAC,CAAA;gCAErD,iDAAiD;gCACjD,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,CAAA;4BACpC,CAAC;iCAAM,CAAC;gCACJ,MAAM,CAAC,KAAK,CAAC,eAAe,EAAE,sBAAsB,EAAE;oCAClD,SAAS;oCACT,YAAY,EAAE,oBAAoB;iCACrC,CAAC,CAAA;4BACN,CAAC;wBACL,CAAC;oBACL,CAAC;oBAAC,OAAO,KAAU,EAAE,CAAC;wBAClB,MAAM,CAAC,KAAK,CAAC,eAAe,EAAE,qCAAqC,EAAE;4BACjE,SAAS;4BACT,KAAK,EAAE,KAAK,CAAC,OAAO;4BACpB,KAAK,EAAE,KAAK,CAAC,KAAK;yBACrB,CAAC,CAAA;wBACF,4EAA4E;oBAChF,CAAC;gBACL,CAAC;gBAED,6FAA6F;gBAC7F,OAAO,aAAa,CAAC,UAAU,EAAE,IAAI,CAAC,CAAA;YAC1C,CAAC,CAAA;QACL,CAAC;KACJ,CAAA;AACL,CAAC,CAAkB,CAAA"}
@@ -0,0 +1,5 @@
1
+ export interface PluginConfig {
2
+ debug: boolean;
3
+ }
4
+ export declare function getConfig(): PluginConfig;
5
+ //# sourceMappingURL=config.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../lib/config.ts"],"names":[],"mappings":"AACA,MAAM,WAAW,YAAY;IACzB,KAAK,EAAE,OAAO,CAAA;CACjB;AAMD,wBAAgB,SAAS,IAAI,YAAY,CAGxC"}
@@ -0,0 +1,8 @@
1
+ const defaultConfig = {
2
+ debug: true // Set to true to enable debug logging
3
+ };
4
+ export function getConfig() {
5
+ // Could be extended to read from a config file or environment
6
+ return defaultConfig;
7
+ }
8
+ //# sourceMappingURL=config.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.js","sourceRoot":"","sources":["../../lib/config.ts"],"names":[],"mappings":"AAKA,MAAM,aAAa,GAAiB;IAChC,KAAK,EAAE,IAAI,CAAC,sCAAsC;CACrD,CAAA;AAED,MAAM,UAAU,SAAS;IACrB,8DAA8D;IAC9D,OAAO,aAAa,CAAA;AACxB,CAAC"}
@@ -0,0 +1,2 @@
1
+ export declare function createShadowModel(): import("@ai-sdk/provider").LanguageModelV2;
2
+ //# sourceMappingURL=factory.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"factory.d.ts","sourceRoot":"","sources":["../../lib/factory.ts"],"names":[],"mappings":"AAGA,wBAAgB,iBAAiB,+CAShC"}
@@ -0,0 +1,12 @@
1
+ // lib/factory.ts
2
+ import { createOpenAICompatible } from "@ai-sdk/openai-compatible";
3
+ export function createShadowModel() {
4
+ // No credentials needed - big-pickle is public and free
5
+ const openai = createOpenAICompatible({
6
+ baseURL: "https://opencode.ai/zen/v1",
7
+ // No apiKey required - the endpoint is public
8
+ name: "opencode",
9
+ });
10
+ return openai("big-pickle");
11
+ }
12
+ //# sourceMappingURL=factory.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"factory.js","sourceRoot":"","sources":["../../lib/factory.ts"],"names":[],"mappings":"AAAA,iBAAiB;AACjB,OAAO,EAAE,sBAAsB,EAAE,MAAM,2BAA2B,CAAA;AAElE,MAAM,UAAU,iBAAiB;IAC7B,wDAAwD;IACxD,MAAM,MAAM,GAAG,sBAAsB,CAAC;QAClC,OAAO,EAAE,4BAA4B;QACrC,8CAA8C;QAC9C,IAAI,EAAE,UAAU;KACnB,CAAC,CAAA;IAEF,OAAO,MAAM,CAAC,YAAY,CAAC,CAAA;AAC/B,CAAC"}
@@ -0,0 +1,10 @@
1
+ import type { Logger } from "./logger";
2
+ import type { StateManager } from "./state";
3
+ export declare class Janitor {
4
+ private client;
5
+ private stateManager;
6
+ private logger;
7
+ constructor(client: any, stateManager: StateManager, logger: Logger);
8
+ run(sessionID: string): Promise<void>;
9
+ }
10
+ //# sourceMappingURL=janitor.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"janitor.d.ts","sourceRoot":"","sources":["../../lib/janitor.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,UAAU,CAAA;AACtC,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,SAAS,CAAA;AAE3C,qBAAa,OAAO;IAEZ,OAAO,CAAC,MAAM;IACd,OAAO,CAAC,YAAY;IACpB,OAAO,CAAC,MAAM;gBAFN,MAAM,EAAE,GAAG,EACX,YAAY,EAAE,YAAY,EAC1B,MAAM,EAAE,MAAM;IAGpB,GAAG,CAAC,SAAS,EAAE,MAAM;CAkP9B"}
@@ -0,0 +1,233 @@
1
+ import { generateObject } from "ai";
2
+ import { createShadowModel } from "./factory";
3
+ import { z } from "zod";
4
+ export class Janitor {
5
+ client;
6
+ stateManager;
7
+ logger;
8
+ constructor(client, stateManager, logger) {
9
+ this.client = client;
10
+ this.stateManager = stateManager;
11
+ this.logger = logger;
12
+ }
13
+ async run(sessionID) {
14
+ this.logger.info("janitor", "Starting analysis", { sessionID });
15
+ try {
16
+ // Fetch session history from OpenCode API
17
+ this.logger.debug("janitor", "Fetching session messages", { sessionID });
18
+ const response = await this.client.session.messages({
19
+ path: { id: sessionID },
20
+ query: { limit: 100 }
21
+ });
22
+ // Handle the response format - it should be { data: Array<{info, parts}> } or just the array
23
+ const messages = response.data || response;
24
+ this.logger.debug("janitor", "Retrieved messages", {
25
+ sessionID,
26
+ messageCount: messages.length
27
+ });
28
+ // If there are no messages or very few, skip analysis
29
+ if (!messages || messages.length < 3) {
30
+ this.logger.debug("janitor", "Too few messages to analyze, skipping", {
31
+ sessionID,
32
+ messageCount: messages?.length || 0
33
+ });
34
+ return;
35
+ }
36
+ // Extract tool call IDs from the session and track their output sizes
37
+ // Also track batch tool relationships
38
+ const toolCallIds = [];
39
+ const toolOutputs = new Map();
40
+ const batchToolChildren = new Map(); // batchID -> [childIDs]
41
+ let currentBatchId = null;
42
+ for (const msg of messages) {
43
+ if (msg.parts) {
44
+ for (const part of msg.parts) {
45
+ if (part.type === "tool" && part.callID) {
46
+ toolCallIds.push(part.callID);
47
+ // Track the output content for size calculation
48
+ if (part.state?.status === "completed" && part.state.output) {
49
+ toolOutputs.set(part.callID, part.state.output);
50
+ }
51
+ // Check if this is a batch tool by looking at the tool name
52
+ if (part.tool === "batch") {
53
+ const batchId = part.callID;
54
+ currentBatchId = batchId;
55
+ batchToolChildren.set(batchId, []);
56
+ this.logger.debug("janitor", "Found batch tool", {
57
+ sessionID,
58
+ batchID: currentBatchId
59
+ });
60
+ }
61
+ // If we're inside a batch and this is a prt_ (parallel) tool call, it's a child
62
+ else if (currentBatchId && part.callID.startsWith('prt_')) {
63
+ const children = batchToolChildren.get(currentBatchId);
64
+ children.push(part.callID);
65
+ this.logger.debug("janitor", "Added child to batch tool", {
66
+ sessionID,
67
+ batchID: currentBatchId,
68
+ childID: part.callID,
69
+ totalChildren: children.length
70
+ });
71
+ }
72
+ // If we hit a non-batch, non-prt_ tool, we're out of the batch
73
+ else if (currentBatchId && !part.callID.startsWith('prt_')) {
74
+ this.logger.debug("janitor", "Batch tool ended", {
75
+ sessionID,
76
+ batchID: currentBatchId,
77
+ totalChildren: batchToolChildren.get(currentBatchId).length
78
+ });
79
+ currentBatchId = null;
80
+ }
81
+ }
82
+ }
83
+ }
84
+ }
85
+ // Log summary of batch tools found
86
+ if (batchToolChildren.size > 0) {
87
+ this.logger.debug("janitor", "Batch tool summary", {
88
+ sessionID,
89
+ batchCount: batchToolChildren.size,
90
+ batches: Array.from(batchToolChildren.entries()).map(([id, children]) => ({
91
+ batchID: id,
92
+ childCount: children.length,
93
+ childIDs: children
94
+ }))
95
+ });
96
+ }
97
+ // Get already pruned IDs to filter them out
98
+ const alreadyPrunedIds = await this.stateManager.get(sessionID);
99
+ const unprunedToolCallIds = toolCallIds.filter(id => !alreadyPrunedIds.includes(id));
100
+ this.logger.debug("janitor", "Found tool calls in session", {
101
+ sessionID,
102
+ toolCallCount: toolCallIds.length,
103
+ toolCallIds,
104
+ alreadyPrunedCount: alreadyPrunedIds.length,
105
+ unprunedCount: unprunedToolCallIds.length
106
+ });
107
+ // If there are no unpruned tool calls, skip analysis
108
+ if (unprunedToolCallIds.length === 0) {
109
+ this.logger.debug("janitor", "No unpruned tool calls found, skipping analysis", { sessionID });
110
+ return;
111
+ }
112
+ // Use big-pickle model - no auth needed!
113
+ const model = createShadowModel();
114
+ this.logger.debug("janitor", "Starting shadow inference", { sessionID });
115
+ // Analyze which tool calls are obsolete
116
+ const result = await generateObject({
117
+ model,
118
+ mode: "json", // Use JSON mode instead of native structured outputs
119
+ schema: z.object({
120
+ pruned_tool_call_ids: z.array(z.string()),
121
+ reasoning: z.string(),
122
+ }),
123
+ prompt: `You are a conversation analyzer that identifies obsolete tool outputs in a coding session.
124
+
125
+ Your task: Analyze the session history and identify tool call IDs whose outputs are NO LONGER RELEVANT to the current conversation context.
126
+
127
+ Guidelines for identifying obsolete tool calls:
128
+ 1. Tool outputs that were superseded by newer reads of the same file/resource
129
+ 2. Exploratory reads that didn't lead to actual edits or meaningful discussion
130
+ 3. Tool calls from >10 turns ago that are no longer referenced
131
+ 4. Error outputs that were subsequently fixed
132
+ 5. Tool calls whose information has been replaced by more recent operations
133
+
134
+ DO NOT prune:
135
+ - Recent tool calls (within last 5 turns)
136
+ - Tool calls that modified state (edits, writes, etc.)
137
+ - Tool calls whose outputs are actively being discussed
138
+ - Tool calls that produced errors still being debugged
139
+
140
+ Available tool call IDs in this session (not yet pruned): ${unprunedToolCallIds.join(", ")}
141
+
142
+ Session history:
143
+ ${JSON.stringify(messages, null, 2)}
144
+
145
+ You MUST respond with valid JSON matching this exact schema:
146
+ {
147
+ "pruned_tool_call_ids": ["id1", "id2", ...],
148
+ "reasoning": "explanation of why these IDs were selected"
149
+ }
150
+
151
+ Return ONLY the tool call IDs that should be pruned (removed from future LLM requests).`
152
+ });
153
+ // Expand batch tool IDs to include their children
154
+ const expandedPrunedIds = new Set();
155
+ for (const prunedId of result.object.pruned_tool_call_ids) {
156
+ expandedPrunedIds.add(prunedId);
157
+ // If this is a batch tool, add all its children
158
+ const children = batchToolChildren.get(prunedId);
159
+ if (children) {
160
+ this.logger.debug("janitor", "Expanding batch tool to include children", {
161
+ sessionID,
162
+ batchID: prunedId,
163
+ childCount: children.length,
164
+ childIDs: children
165
+ });
166
+ children.forEach(childId => expandedPrunedIds.add(childId));
167
+ }
168
+ }
169
+ const finalPrunedIds = Array.from(expandedPrunedIds);
170
+ this.logger.info("janitor", "Analysis complete", {
171
+ sessionID,
172
+ prunedCount: finalPrunedIds.length,
173
+ originalPrunedCount: result.object.pruned_tool_call_ids.length,
174
+ prunedIds: finalPrunedIds,
175
+ reasoning: result.object.reasoning
176
+ });
177
+ // Calculate approximate size saved from newly pruned tool outputs (using expanded IDs)
178
+ let totalCharsSaved = 0;
179
+ for (const prunedId of finalPrunedIds) {
180
+ const output = toolOutputs.get(prunedId);
181
+ if (output) {
182
+ totalCharsSaved += output.length;
183
+ }
184
+ }
185
+ // Rough token estimate (1 token ≈ 4 characters for English text)
186
+ const estimatedTokensSaved = Math.round(totalCharsSaved / 4);
187
+ // Merge newly pruned IDs with existing ones (using expanded IDs)
188
+ const allPrunedIds = [...new Set([...alreadyPrunedIds, ...finalPrunedIds])];
189
+ await this.stateManager.set(sessionID, allPrunedIds);
190
+ this.logger.debug("janitor", "Updated state manager", {
191
+ sessionID,
192
+ totalPrunedCount: allPrunedIds.length,
193
+ newlyPrunedCount: finalPrunedIds.length
194
+ });
195
+ // Show toast notification if we pruned anything
196
+ if (finalPrunedIds.length > 0) {
197
+ try {
198
+ await this.client.tui.showToast({
199
+ body: {
200
+ title: "Context Pruned",
201
+ message: `Removed ${finalPrunedIds.length} tool output${finalPrunedIds.length > 1 ? 's' : ''} (~${estimatedTokensSaved.toLocaleString()} tokens saved)`,
202
+ variant: "success",
203
+ duration: 5000
204
+ }
205
+ });
206
+ this.logger.info("janitor", "Toast notification shown", {
207
+ sessionID,
208
+ prunedCount: finalPrunedIds.length,
209
+ estimatedTokensSaved,
210
+ totalCharsSaved
211
+ });
212
+ }
213
+ catch (toastError) {
214
+ this.logger.error("janitor", "Failed to show toast notification", {
215
+ sessionID,
216
+ error: toastError.message
217
+ });
218
+ // Don't fail the whole pruning operation if toast fails
219
+ }
220
+ }
221
+ }
222
+ catch (error) {
223
+ this.logger.error("janitor", "Analysis failed", {
224
+ sessionID,
225
+ error: error.message,
226
+ stack: error.stack
227
+ });
228
+ // Don't throw - this is a fire-and-forget background process
229
+ // Silently fail and try again on next idle event
230
+ }
231
+ }
232
+ }
233
+ //# sourceMappingURL=janitor.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"janitor.js","sourceRoot":"","sources":["../../lib/janitor.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,12 @@
1
+ export declare class Logger {
2
+ private logDir;
3
+ private enabled;
4
+ constructor(enabled: boolean, pluginDir: string);
5
+ private ensureLogDir;
6
+ private write;
7
+ info(component: string, message: string, data?: any): Promise<void>;
8
+ debug(component: string, message: string, data?: any): Promise<void>;
9
+ warn(component: string, message: string, data?: any): Promise<void>;
10
+ error(component: string, message: string, data?: any): Promise<void>;
11
+ }
12
+ //# sourceMappingURL=logger.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"logger.d.ts","sourceRoot":"","sources":["../../lib/logger.ts"],"names":[],"mappings":"AAKA,qBAAa,MAAM;IACf,OAAO,CAAC,MAAM,CAAQ;IACtB,OAAO,CAAC,OAAO,CAAS;gBAEZ,OAAO,EAAE,OAAO,EAAE,SAAS,EAAE,MAAM;YAKjC,YAAY;YAMZ,KAAK;IAoBnB,IAAI,CAAC,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,GAAG;IAInD,KAAK,CAAC,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,GAAG;IAIpD,IAAI,CAAC,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,GAAG;IAInD,KAAK,CAAC,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,GAAG;CAGvD"}
@@ -0,0 +1,46 @@
1
+ // lib/logger.ts
2
+ import { writeFile, mkdir } from "fs/promises";
3
+ import { join } from "path";
4
+ import { existsSync } from "fs";
5
+ export class Logger {
6
+ logDir;
7
+ enabled;
8
+ constructor(enabled, pluginDir) {
9
+ this.enabled = enabled;
10
+ this.logDir = join(pluginDir, "logs");
11
+ }
12
+ async ensureLogDir() {
13
+ if (!existsSync(this.logDir)) {
14
+ await mkdir(this.logDir, { recursive: true });
15
+ }
16
+ }
17
+ async write(level, component, message, data) {
18
+ if (!this.enabled)
19
+ return;
20
+ await this.ensureLogDir();
21
+ const timestamp = new Date().toISOString();
22
+ const logEntry = {
23
+ timestamp,
24
+ level,
25
+ component,
26
+ message,
27
+ ...(data && { data })
28
+ };
29
+ const logFile = join(this.logDir, `${new Date().toISOString().split('T')[0]}.log`);
30
+ const logLine = JSON.stringify(logEntry) + "\n";
31
+ await writeFile(logFile, logLine, { flag: "a" });
32
+ }
33
+ info(component, message, data) {
34
+ return this.write("INFO", component, message, data);
35
+ }
36
+ debug(component, message, data) {
37
+ return this.write("DEBUG", component, message, data);
38
+ }
39
+ warn(component, message, data) {
40
+ return this.write("WARN", component, message, data);
41
+ }
42
+ error(component, message, data) {
43
+ return this.write("ERROR", component, message, data);
44
+ }
45
+ }
46
+ //# sourceMappingURL=logger.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"logger.js","sourceRoot":"","sources":["../../lib/logger.ts"],"names":[],"mappings":"AAAA,gBAAgB;AAChB,OAAO,EAAE,SAAS,EAAE,KAAK,EAAE,MAAM,aAAa,CAAA;AAC9C,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAA;AAC3B,OAAO,EAAE,UAAU,EAAE,MAAM,IAAI,CAAA;AAE/B,MAAM,OAAO,MAAM;IACP,MAAM,CAAQ;IACd,OAAO,CAAS;IAExB,YAAY,OAAgB,EAAE,SAAiB;QAC3C,IAAI,CAAC,OAAO,GAAG,OAAO,CAAA;QACtB,IAAI,CAAC,MAAM,GAAG,IAAI,CAAC,SAAS,EAAE,MAAM,CAAC,CAAA;IACzC,CAAC;IAEO,KAAK,CAAC,YAAY;QACtB,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC,MAAM,CAAC,EAAE,CAAC;YAC3B,MAAM,KAAK,CAAC,IAAI,CAAC,MAAM,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAA;QACjD,CAAC;IACL,CAAC;IAEO,KAAK,CAAC,KAAK,CAAC,KAAa,EAAE,SAAiB,EAAE,OAAe,EAAE,IAAU;QAC7E,IAAI,CAAC,IAAI,CAAC,OAAO;YAAE,OAAM;QAEzB,MAAM,IAAI,CAAC,YAAY,EAAE,CAAA;QAEzB,MAAM,SAAS,GAAG,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE,CAAA;QAC1C,MAAM,QAAQ,GAAG;YACb,SAAS;YACT,KAAK;YACL,SAAS;YACT,OAAO;YACP,GAAG,CAAC,IAAI,IAAI,EAAE,IAAI,EAAE,CAAC;SACxB,CAAA;QAED,MAAM,OAAO,GAAG,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,GAAG,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAA;QAClF,MAAM,OAAO,GAAG,IAAI,CAAC,SAAS,CAAC,QAAQ,CAAC,GAAG,IAAI,CAAA;QAE/C,MAAM,SAAS,CAAC,OAAO,EAAE,OAAO,EAAE,EAAE,IAAI,EAAE,GAAG,EAAE,CAAC,CAAA;IACpD,CAAC;IAED,IAAI,CAAC,SAAiB,EAAE,OAAe,EAAE,IAAU;QAC/C,OAAO,IAAI,CAAC,KAAK,CAAC,MAAM,EAAE,SAAS,EAAE,OAAO,EAAE,IAAI,CAAC,CAAA;IACvD,CAAC;IAED,KAAK,CAAC,SAAiB,EAAE,OAAe,EAAE,IAAU;QAChD,OAAO,IAAI,CAAC,KAAK,CAAC,OAAO,EAAE,SAAS,EAAE,OAAO,EAAE,IAAI,CAAC,CAAA;IACxD,CAAC;IAED,IAAI,CAAC,SAAiB,EAAE,OAAe,EAAE,IAAU;QAC/C,OAAO,IAAI,CAAC,KAAK,CAAC,MAAM,EAAE,SAAS,EAAE,OAAO,EAAE,IAAI,CAAC,CAAA;IACvD,CAAC;IAED,KAAK,CAAC,SAAiB,EAAE,OAAe,EAAE,IAAU;QAChD,OAAO,IAAI,CAAC,KAAK,CAAC,OAAO,EAAE,SAAS,EAAE,OAAO,EAAE,IAAI,CAAC,CAAA;IACxD,CAAC;CACJ"}
@@ -0,0 +1,13 @@
1
+ export interface PruningMetadata {
2
+ prunedIds: string[];
3
+ lastPruneCount: number;
4
+ }
5
+ export declare class StateManager {
6
+ private state;
7
+ get(sessionID: string): Promise<string[]>;
8
+ set(sessionID: string, prunedIds: string[]): Promise<void>;
9
+ getLastPruneCount(sessionID: string): Promise<number>;
10
+ resetLastPruneCount(sessionID: string): Promise<void>;
11
+ clear(sessionID: string): Promise<void>;
12
+ }
13
+ //# sourceMappingURL=state.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"state.d.ts","sourceRoot":"","sources":["../../lib/state.ts"],"names":[],"mappings":"AAEA,MAAM,WAAW,eAAe;IAC5B,SAAS,EAAE,MAAM,EAAE,CAAA;IACnB,cAAc,EAAE,MAAM,CAAA;CACzB;AAED,qBAAa,YAAY;IACrB,OAAO,CAAC,KAAK,CAA0C;IAEjD,GAAG,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC;IAIzC,GAAG,CAAC,SAAS,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC;IAW1D,iBAAiB,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAIrD,mBAAmB,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAOrD,KAAK,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;CAGhD"}
@@ -0,0 +1,29 @@
1
+ // lib/state.ts
2
+ export class StateManager {
3
+ state = new Map();
4
+ async get(sessionID) {
5
+ return this.state.get(sessionID)?.prunedIds ?? [];
6
+ }
7
+ async set(sessionID, prunedIds) {
8
+ const existing = this.state.get(sessionID);
9
+ const previousCount = existing?.prunedIds.length ?? 0;
10
+ const newCount = prunedIds.length - previousCount;
11
+ this.state.set(sessionID, {
12
+ prunedIds,
13
+ lastPruneCount: newCount
14
+ });
15
+ }
16
+ async getLastPruneCount(sessionID) {
17
+ return this.state.get(sessionID)?.lastPruneCount ?? 0;
18
+ }
19
+ async resetLastPruneCount(sessionID) {
20
+ const existing = this.state.get(sessionID);
21
+ if (existing) {
22
+ existing.lastPruneCount = 0;
23
+ }
24
+ }
25
+ async clear(sessionID) {
26
+ this.state.delete(sessionID);
27
+ }
28
+ }
29
+ //# sourceMappingURL=state.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"state.js","sourceRoot":"","sources":["../../lib/state.ts"],"names":[],"mappings":"AAAA,eAAe;AAOf,MAAM,OAAO,YAAY;IACb,KAAK,GAAiC,IAAI,GAAG,EAAE,CAAA;IAEvD,KAAK,CAAC,GAAG,CAAC,SAAiB;QACvB,OAAO,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,SAAS,CAAC,EAAE,SAAS,IAAI,EAAE,CAAA;IACrD,CAAC;IAED,KAAK,CAAC,GAAG,CAAC,SAAiB,EAAE,SAAmB;QAC5C,MAAM,QAAQ,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,SAAS,CAAC,CAAA;QAC1C,MAAM,aAAa,GAAG,QAAQ,EAAE,SAAS,CAAC,MAAM,IAAI,CAAC,CAAA;QACrD,MAAM,QAAQ,GAAG,SAAS,CAAC,MAAM,GAAG,aAAa,CAAA;QAEjD,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,SAAS,EAAE;YACtB,SAAS;YACT,cAAc,EAAE,QAAQ;SAC3B,CAAC,CAAA;IACN,CAAC;IAED,KAAK,CAAC,iBAAiB,CAAC,SAAiB;QACrC,OAAO,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,SAAS,CAAC,EAAE,cAAc,IAAI,CAAC,CAAA;IACzD,CAAC;IAED,KAAK,CAAC,mBAAmB,CAAC,SAAiB;QACvC,MAAM,QAAQ,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,SAAS,CAAC,CAAA;QAC1C,IAAI,QAAQ,EAAE,CAAC;YACX,QAAQ,CAAC,cAAc,GAAG,CAAC,CAAA;QAC/B,CAAC;IACL,CAAC;IAED,KAAK,CAAC,KAAK,CAAC,SAAiB;QACzB,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,SAAS,CAAC,CAAA;IAChC,CAAC;CACJ"}
package/package.json ADDED
@@ -0,0 +1,46 @@
1
+ {
2
+ "$schema": "https://json.schemastore.org/package.json",
3
+ "name": "@tarquinen/opencode-dcp",
4
+ "version": "0.1.0",
5
+ "type": "module",
6
+ "description": "OpenCode plugin that optimizes token usage by pruning obsolete tool outputs from conversation context",
7
+ "main": "./dist/index.js",
8
+ "types": "./dist/index.d.ts",
9
+ "scripts": {
10
+ "build": "tsc",
11
+ "postbuild": "rm -rf dist/logs",
12
+ "prepublishOnly": "npm run build",
13
+ "dev": "opencode plugin dev",
14
+ "typecheck": "tsc --noEmit"
15
+ },
16
+ "keywords": [
17
+ "opencode",
18
+ "opencode-plugin",
19
+ "plugin",
20
+ "context",
21
+ "pruning",
22
+ "optimization",
23
+ "tokens"
24
+ ],
25
+ "author": "tarquinen",
26
+ "license": "MIT",
27
+ "peerDependencies": {
28
+ "@opencode-ai/plugin": "^0.13.7"
29
+ },
30
+ "dependencies": {
31
+ "@ai-sdk/openai-compatible": "^1.0.0",
32
+ "@opencode-ai/sdk": "latest",
33
+ "ai": "^5.0.0",
34
+ "zod": "^3.23.0"
35
+ },
36
+ "devDependencies": {
37
+ "@opencode-ai/plugin": "latest",
38
+ "@types/node": "^22.0.0",
39
+ "typescript": "^5.7.0"
40
+ },
41
+ "files": [
42
+ "dist/",
43
+ "README.md",
44
+ "LICENSE"
45
+ ]
46
+ }