1- // index.ts - Main plugin entry point for Dynamic Context Pruning
21import type { Plugin } from "@opencode-ai/plugin"
32import { tool } from "@opencode-ai/plugin"
43import { getConfig } from "./lib/config"
54import { Logger } from "./lib/logger"
65import { Janitor , type SessionStats } from "./lib/janitor"
76import { 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- */
138async 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
2317const 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