Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ const plugin: Plugin = (async (ctx) => {
const stateManager = new StateManager()
const toolParametersCache = new Map<string, any>() // callID -> parameters
const modelCache = new Map<string, { providerID: string; modelID: string }>() // sessionID -> model info
const janitor = new Janitor(ctx.client, stateManager, logger, toolParametersCache, config.protectedTools, modelCache, config.model, config.showModelErrorToasts, config.pruningMode, config.pruning_summary)
const janitor = new Janitor(ctx.client, stateManager, logger, toolParametersCache, config.protectedTools, modelCache, config.model, config.showModelErrorToasts, config.pruningMode, config.pruning_summary, ctx.directory)

const cacheToolParameters = (messages: any[], component: string) => {
for (const message of messages) {
Expand Down
33 changes: 23 additions & 10 deletions lib/deduplicator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -151,16 +151,24 @@ export function extractParameterKey(metadata: { tool: string, parameters?: any }
}

// ===== Directory/Search Tools =====
if (tool === "list" && parameters.path) {
return parameters.path
}
if (tool === "glob" && parameters.pattern) {
const pathInfo = parameters.path ? ` in ${parameters.path}` : ""
return `"${parameters.pattern}"${pathInfo}`
if (tool === "list") {
// path is optional, defaults to current directory
return parameters.path || '(current directory)'
}
if (tool === "glob") {
// pattern is required for glob
if (parameters.pattern) {
const pathInfo = parameters.path ? ` in ${parameters.path}` : ""
return `"${parameters.pattern}"${pathInfo}`
}
return '(unknown pattern)'
}
if (tool === "grep" && parameters.pattern) {
const pathInfo = parameters.path ? ` in ${parameters.path}` : ""
return `"${parameters.pattern}"${pathInfo}`
if (tool === "grep") {
if (parameters.pattern) {
const pathInfo = parameters.path ? ` in ${parameters.path}` : ""
return `"${parameters.pattern}"${pathInfo}`
}
return '(unknown pattern)'
}

// ===== Execution Tools =====
Expand Down Expand Up @@ -205,5 +213,10 @@ export function extractParameterKey(metadata: { tool: string, parameters?: any }

// ===== Fallback =====
// For unknown tools, custom tools, or tools without extractable keys
return JSON.stringify(parameters).substring(0, 50)
// Check if parameters is empty or only has empty values
const paramStr = JSON.stringify(parameters)
if (paramStr === '{}' || paramStr === '[]' || paramStr === 'null') {
return '' // Return empty to trigger (default) fallback in UI
}
return paramStr.substring(0, 50)
}
51 changes: 40 additions & 11 deletions lib/janitor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@ export class Janitor {
private configModel?: string, // Format: "provider/model"
private showModelErrorToasts: boolean = true, // Whether to show toast for model errors
private pruningMode: "auto" | "smart" = "smart", // Pruning strategy
private pruningSummary: "off" | "minimal" | "detailed" = "detailed" // UI summary display mode
private pruningSummary: "off" | "minimal" | "detailed" = "detailed", // UI summary display mode
private workingDirectory?: string // Current working directory for relative path display
) { }

/**
Expand Down Expand Up @@ -490,6 +491,23 @@ export class Janitor {
return `${nodeModulesMatch[1]}/${nodeModulesMatch[2]}`
}

// Strip working directory to show relative paths
if (this.workingDirectory) {
// Try to match against the absolute working directory first
if (path.startsWith(this.workingDirectory + '/')) {
return path.slice(this.workingDirectory.length + 1)
}

// Also try matching against ~ version of working directory
const workingDirWithTilde = this.workingDirectory.startsWith(homeDir)
? '~' + this.workingDirectory.slice(homeDir.length)
: null

if (workingDirWithTilde && path.startsWith(workingDirWithTilde + '/')) {
return path.slice(workingDirWithTilde.length + 1)
}
}

return path
}

Expand Down Expand Up @@ -553,6 +571,8 @@ export class Janitor {
/**
* Build a summary of tools by grouping them
* Uses shared extractParameterKey logic for consistent parameter extraction
*
* Note: prunedIds may be in original case (from LLM) but toolMetadata uses lowercase keys
*/
private buildToolsSummary(prunedIds: string[], toolMetadata: Map<string, { tool: string, parameters?: any }>): Map<string, string[]> {
const toolsSummary = new Map<string, string[]>()
Expand All @@ -564,7 +584,9 @@ export class Janitor {
}

for (const prunedId of prunedIds) {
const metadata = toolMetadata.get(prunedId)
// Normalize ID to lowercase for lookup (toolMetadata uses lowercase keys)
const normalizedId = prunedId.toLowerCase()
const metadata = toolMetadata.get(normalizedId)
if (metadata) {
const toolName = metadata.tool
if (!toolsSummary.has(toolName)) {
Expand All @@ -577,6 +599,10 @@ export class Janitor {
// Apply path shortening and truncation for display
const displayKey = truncate(this.shortenPath(paramKey), 80)
toolsSummary.get(toolName)!.push(displayKey)
} else {
// For tools with no extractable parameter key, add a placeholder
// This ensures the tool still shows up in the summary
toolsSummary.get(toolName)!.push('(default)')
}
}
}
Expand Down Expand Up @@ -736,17 +762,20 @@ export class Janitor {
for (const param of params) {
message += ` ${param}\n`
}
} else {
// For tools with no specific params (like batch), just show the tool name and count
const count = llmPrunedIds.filter(id => {
const m = toolMetadata.get(id)
return m && m.tool === toolName
}).length
if (count > 0) {
message += ` ${toolName} (${count})\n`
}
}
}

// Handle any tools that weren't found in metadata (edge case)
const foundToolNames = new Set(toolsSummary.keys())
const missingTools = llmPrunedIds.filter(id => {
const normalizedId = id.toLowerCase()
const metadata = toolMetadata.get(normalizedId)
return !metadata || !foundToolNames.has(metadata.tool)
})

if (missingTools.length > 0) {
message += ` (${missingTools.length} tool${missingTools.length > 1 ? 's' : ''} with unknown metadata)\n`
}
}

await this.sendIgnoredMessage(sessionID, message.trim())
Expand Down
46 changes: 41 additions & 5 deletions lib/model-selector.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,42 @@ function shouldSkipProvider(providerID: string): boolean {
return SKIP_PROVIDERS.some(skip => normalized.includes(skip.toLowerCase()));
}

/**
* Attempts to import OpencodeAI with retry logic to handle plugin initialization timing issues.
* Some providers (like openai via @openhax/codex) may not be fully initialized on first attempt.
*/
async function importOpencodeAI(logger?: Logger, maxRetries: number = 3, delayMs: number = 100): Promise<any> {
let lastError: Error | undefined;

for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
const { OpencodeAI } = await import('@tarquinen/opencode-auth-provider');
return new OpencodeAI();
} catch (error: any) {
lastError = error;

// Check if this is the specific initialization error we're handling
if (error.message?.includes('before initialization')) {
logger?.debug('model-selector', `Import attempt ${attempt}/${maxRetries} failed, will retry`, {
error: error.message
});

if (attempt < maxRetries) {
// Wait before retrying, with exponential backoff
await new Promise(resolve => setTimeout(resolve, delayMs * attempt));
continue;
}
}

// For other errors, don't retry
throw error;
}
}

// All retries exhausted
throw lastError;
}

/**
* Main model selection function with intelligent fallback logic
*
Expand All @@ -85,9 +121,9 @@ export async function selectModel(
): Promise<ModelSelectionResult> {
logger?.info('model-selector', 'Model selection started', { currentModel, configModel });

// Lazy import - only load the 812KB auth provider package when actually needed
const { OpencodeAI } = await import('@tarquinen/opencode-auth-provider');
const opencodeAI = new OpencodeAI();
// Lazy import with retry logic - handles plugin initialization timing issues
// Some providers (like openai via @openhax/codex) may not be ready on first attempt
const opencodeAI = await importOpencodeAI(logger);

let failedModelInfo: ModelInfo | undefined;

Expand Down Expand Up @@ -178,10 +214,10 @@ export async function selectModel(
logger?.info('model-selector', 'Available authenticated providers', {
providerCount: availableProviderIDs.length,
providerIDs: availableProviderIDs,
providers: Object.entries(providers).map(([id, info]) => ({
providers: Object.entries(providers).map(([id, info]: [string, any]) => ({
id,
source: info.source,
name: info.info.name
name: info.info?.name
}))
});

Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@tarquinen/opencode-dcp",
"version": "0.3.5",
"version": "0.3.6",
"type": "module",
"description": "OpenCode plugin that optimizes token usage by pruning obsolete tool outputs from conversation context",
"main": "./dist/index.js",
Expand Down