Skip to content

Commit 1b40887

Browse files
committed
Rename skipAiOnFallback to strictModelSelection, notify on invalid config keys
1 parent 98e90a9 commit 1b40887

File tree

9 files changed

+136
-647
lines changed

9 files changed

+136
-647
lines changed

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)