From 3f3d547583a92dcc37f06295282b493afa31565a Mon Sep 17 00:00:00 2001 From: brandonkachen Date: Tue, 23 Sep 2025 13:10:10 -0700 Subject: [PATCH 01/10] Resolve merge conflict in stream-parser.ts and improve benchify integration error handling; implement batch execution for deferred str_replace operations to improve reliability and observability. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 🤖 Generated with Codebuff Co-Authored-By: Codebuff --- backend/package.json | 1 + backend/src/run-agent-step.ts | 3 +- backend/src/tools/batch-str-replace.ts | 538 ++++++++++++++++++ backend/src/tools/handlers/tool/write-file.ts | 4 +- backend/src/tools/stream-parser.ts | 192 ++++++- bun.lock | 7 + npm-app/src/tool-handlers.ts | 16 +- packages/internal/src/env.ts | 2 + 8 files changed, 726 insertions(+), 37 deletions(-) create mode 100644 backend/src/tools/batch-str-replace.ts diff --git a/backend/package.json b/backend/package.json index aeac65984e..c2c1bfbac6 100644 --- a/backend/package.json +++ b/backend/package.json @@ -24,6 +24,7 @@ }, "dependencies": { "@ai-sdk/google-vertex": "3.0.6", + "benchify": "^0.1.0-alpha.41", "@ai-sdk/openai": "2.0.11", "@codebuff/billing": "workspace:*", "@codebuff/common": "workspace:*", diff --git a/backend/src/run-agent-step.ts b/backend/src/run-agent-step.ts index 2537f215c5..b63b9ee84a 100644 --- a/backend/src/run-agent-step.ts +++ b/backend/src/run-agent-step.ts @@ -327,7 +327,6 @@ export const runAgentStep = async ( state, fullResponse: fullResponseAfterStream, fullResponseChunks, - messageId, } = await processStreamWithTools({ stream, ws, @@ -435,7 +434,7 @@ export const runAgentStep = async ( agentState, fullResponse, shouldEndTurn, - messageId, + messageId: null, } } diff --git a/backend/src/tools/batch-str-replace.ts b/backend/src/tools/batch-str-replace.ts new file mode 100644 index 0000000000..ed1562ee31 --- /dev/null +++ b/backend/src/tools/batch-str-replace.ts @@ -0,0 +1,538 @@ +import { handleStrReplace } from './handlers/tool/str-replace' +import { getFileProcessingValues } from './handlers/tool/write-file' +import { logger } from '../util/logger' +import { Benchify } from 'benchify' +import { env } from '@codebuff/internal/env' +import { requestToolCall } from '../websockets/websocket-action' +import { createPatch } from 'diff' +import type { CodebuffToolCall } from '@codebuff/common/tools/list' +import type { ToolResultPart } from '@codebuff/common/types/messages/content-part' +import type { PrintModeEvent } from '@codebuff/common/types/print-mode' +import type { AgentTemplate } from '../templates/types' +import type { ProjectFileContext } from '@codebuff/common/util/file' +import type { WebSocket } from 'ws' +import { file } from 'bun' + +export type DeferredStrReplace = { + toolCall: CodebuffToolCall<'str_replace'> +} + +export type BatchStrReplaceState = { + deferredStrReplaces: DeferredStrReplace[] + otherToolsQueue: any[] + strReplacePhaseComplete: boolean + failures: any[] +} + +const BENCHIFY_FILE_TYPES = ['tsx', 'ts', 'jsx', 'js'] + +// Global Benchify client instance +let benchifyClient: Benchify | null = null + +function getBenchifyClient(): Benchify | null { + if (!benchifyClient) { + let benchifyApiKey: string | undefined + try { + benchifyApiKey = env.BENCHIFY_API_KEY + } catch (error) { + logger.warn( + { + error: error instanceof Error ? error.message : String(error), + }, + 'Failed to access BENCHIFY_API_KEY from environment', + ) + return null + } + + if (!benchifyApiKey) { + return null + } + + benchifyClient = new Benchify({ + apiKey: benchifyApiKey, + }) + } + return benchifyClient +} + +export async function executeBatchStrReplaces({ + deferredStrReplaces, + toolCalls, + toolResults, + ws, + agentTemplate, + fileContext, + agentStepId, + clientSessionId, + userInputId, + fullResponse, + onResponseChunk, + state, + userId, +}: { + deferredStrReplaces: DeferredStrReplace[] + toolCalls: (CodebuffToolCall | any)[] + toolResults: ToolResultPart[] + ws: WebSocket + agentTemplate: AgentTemplate + fileContext: ProjectFileContext + agentStepId: string + clientSessionId: string + userInputId: string + fullResponse: string + onResponseChunk: (chunk: string | PrintModeEvent) => void + state: Record + userId: string | undefined +}) { + if (deferredStrReplaces.length === 0) { + return + } + + const batchPromises: Promise[] = [] + let previousPromise = Promise.resolve() + + // Track successfully edited files for benchify call + const editedFiles: { path: string; contents: string }[] = [] + // Track intended changes from LLM for benchify call (even if str_replace fails) + const intendedChanges: { path: string; contents: string }[] = [] + // Track original file contents before any modifications + const originalContents: Record = {} + + // Execute all str_replace calls in sequence to maintain file consistency + for (let i = 0; i < deferredStrReplaces.length; i++) { + const { toolCall } = deferredStrReplaces[i] + + // Read original content before any modifications (only once per file) + if ( + benchifyCanFixLanguage(toolCall.input.path) && + !originalContents[toolCall.input.path] + ) { + try { + const originalContent = await extractOriginalContent( + toolCall.input.path, + fileContext, + ) + if (originalContent) { + originalContents[toolCall.input.path] = originalContent + } + } catch (error) { + logger.warn( + { + error: error instanceof Error ? error.message : String(error), + path: toolCall.input.path, + }, + 'Failed to read original content for benchify', + ) + } + } + + // Extract intended content from str_replace operation before attempting execution + if ( + benchifyCanFixLanguage(toolCall.input.path) && + originalContents[toolCall.input.path] + ) { + try { + const intendedContent = await extractIntendedContent( + toolCall, + originalContents[toolCall.input.path], + ) + if (intendedContent) { + const existingIndex = intendedChanges.findIndex( + (f) => f.path === toolCall.input.path, + ) + if (existingIndex >= 0) { + intendedChanges[existingIndex].contents = intendedContent + } else { + intendedChanges.push({ + path: toolCall.input.path, + contents: intendedContent, + }) + } + } + } catch (error) { + logger.warn( + { + error: error instanceof Error ? error.message : String(error), + path: toolCall.input.path, + }, + 'Failed to extract intended content for benchify', + ) + } + } + + // Chain each str_replace to the previous one to ensure proper ordering + const strReplacePromise = previousPromise.then(async () => { + try { + const { result } = handleStrReplace({ + previousToolCallFinished: Promise.resolve(), + toolCall, + requestClientToolCall: async () => { + throw new Error('Client tool calls not supported in batch mode') + }, + writeToClient: onResponseChunk, + getLatestState: () => getFileProcessingValues(state), + state: { ...state, ws }, + }) + + const toolResult = await result + + if (toolResult) { + const toolResultPart: ToolResultPart = { + type: 'tool-result', + toolName: 'str_replace', + toolCallId: toolCall.toolCallId, + output: toolResult, + } + + toolResults.push(toolResultPart) + + onResponseChunk({ + type: 'tool_result', + toolCallId: toolCall.toolCallId, + output: toolResult, + }) + + // Add to message history + state.messages.push({ + role: 'tool' as const, + content: toolResultPart, + }) + + // Track successfully edited files + if ( + Array.isArray(toolResult) && + toolResult.length > 0 && + benchifyCanFixLanguage(toolCall.input.path) + ) { + const result = toolResult[0] + if ( + result.type === 'json' && + result.value && + 'content' in result.value + ) { + const existingFileIndex = editedFiles.findIndex( + (f) => f.path === toolCall.input.path, + ) + const fileContent = result.value.content as string + + if (existingFileIndex >= 0) { + // Update existing file with latest content + editedFiles[existingFileIndex].contents = fileContent + } else { + // Add new file to tracking + editedFiles.push({ + path: toolCall.input.path, + contents: fileContent, + }) + } + } + } + } + } catch (error) { + logger.error( + { + error: + error instanceof Error + ? { + message: error.message, + stack: error.stack, + name: error.name, + } + : error, + toolCallId: toolCall.toolCallId, + toolCallInput: JSON.stringify(toolCall.input, null, 2), + agentStepId, + userInputId, + }, + `Error executing batched str_replace ${i + 1}/${deferredStrReplaces.length}`, + ) + + // Create error result + const errorResult: ToolResultPart = { + type: 'tool-result', + toolName: 'str_replace', + toolCallId: toolCall.toolCallId, + output: [ + { + type: 'json', + value: { + errorMessage: `Batched str_replace failed: ${error instanceof Error ? error.message : String(error)}`, + }, + }, + ], + } + + toolResults.push(errorResult) + onResponseChunk({ + type: 'tool_result', + toolCallId: toolCall.toolCallId, + output: errorResult.output, + }) + } + }) + + // Add to toolCalls array + toolCalls.push(toolCall) + batchPromises.push(strReplacePromise) + previousPromise = strReplacePromise + } + + // Wait for all batched operations to complete + await Promise.all(batchPromises) + + // Call benchify with intended changes (even if str_replace operations failed) + const client = getBenchifyClient() + if (!client || intendedChanges.length === 0) { + return + } + + try { + const benchifyResult = await callBenchify(intendedChanges, { + agentStepId, + clientSessionId, + userInputId, + userId, + }) + + if (benchifyResult && benchifyResult.length > 0) { + logger.info( + { + benchifyResultCount: benchifyResult.length, + resultFiles: benchifyResult.map((r) => r.path), + agentStepId, + userInputId, + }, + `executeBatchStrReplaces: Benchify returned ${benchifyResult.length} results, applying them`, + ) + + // Apply benchify results back to files + await applyBenchifyResults(benchifyResult, { + ws, + onResponseChunk, + state: { ...state, originalContents }, + toolResults, + toolCalls: deferredStrReplaces.map((d) => d.toolCall), + userInputId, + }) + } + } catch (error) { + logger.error( + { + error: error instanceof Error ? error.message : String(error), + intendedChangeFiles: intendedChanges.map((f) => f.path), + agentStepId, + userInputId, + }, + 'executeBatchStrReplaces: Failed to call benchify with intended changes', + ) + } +} + +/** + * Calls benchify API with the list of edited files + */ +async function callBenchify( + editedFiles: { path: string; contents: string }[], + context: { + agentStepId: string + clientSessionId: string + userInputId: string + userId: string | undefined + }, +): Promise<{ path: string; contents: string }[] | null> { + const client = getBenchifyClient() + if (!client) { + return null + } + + const response = await client.runFixer(editedFiles, { + fix_types: ['string_literals'], + }) + + logger.info( + { + responseReceived: !!response, + responseLength: response?.length || 0, + responseFiles: response?.map((r) => r.path) || [], + ...context, + }, + 'Benchify runFixer API response received', + ) + + return response +} + +/** + * Applies benchify results back to the file system and updates tool results + */ +async function applyBenchifyResults( + benchifyFiles: { path: string; contents: string }[], + context: { + ws: WebSocket + onResponseChunk: (chunk: string | PrintModeEvent) => void + state: Record + toolResults: ToolResultPart[] + toolCalls: CodebuffToolCall<'str_replace'>[] + userInputId: string + }, +) { + for (const benchifyFile of benchifyFiles) { + try { + // Find the corresponding tool call for this file + const relatedToolCall = context.toolCalls.find( + (tc) => tc.input.path === benchifyFile.path, + ) + + if (!relatedToolCall) { + logger.warn( + { fileName: benchifyFile.path }, + 'No matching tool call found for benchify result', + ) + continue + } + + // Get the original file content from our stored contents + const originalContent = + context.state.originalContents?.[benchifyFile.path] + + if (!originalContent) { + logger.error( + { path: benchifyFile.path }, + 'Could not find original file content for diff generation', + ) + continue + } + + // Generate a proper unified diff patch + const patch = createPatch( + benchifyFile.path, + originalContent, + benchifyFile.contents, + '', + '', + ) + + // Request the client to apply the benchify changes as a patch + const toolCallResult = await requestToolCall( + context.ws, + context.userInputId, + 'str_replace', + { + type: 'patch', + path: benchifyFile.path, + content: patch, + }, + ) + + // Create a tool result indicating benchify was applied + const benchifyToolResult: ToolResultPart = { + type: 'tool-result', + toolName: 'str_replace', + toolCallId: relatedToolCall.toolCallId, + output: toolCallResult.output, + } + + // Update the existing tool result + const existingResultIndex = context.toolResults.findIndex( + (tr) => tr.toolCallId === relatedToolCall.toolCallId, + ) + + if (existingResultIndex >= 0) { + context.toolResults[existingResultIndex] = benchifyToolResult + } else { + context.toolResults.push(benchifyToolResult) + } + + // Notify client about the benchify update + context.onResponseChunk({ + type: 'tool_result', + toolCallId: relatedToolCall.toolCallId, + output: benchifyToolResult.output, + }) + } catch (error) { + logger.error( + { error, fileName: benchifyFile.path }, + 'Failed to apply benchify result to file', + ) + } + } +} + +/** + * Extracts the original file content before any modifications + */ +async function extractOriginalContent( + filePath: string, + fileContext: ProjectFileContext, +): Promise { + try { + const absolutePath = `${fileContext.projectRoot}/${filePath}` + const currentFile = await file(absolutePath) + return await currentFile.text() + } catch (error) { + logger.warn( + { + error: error instanceof Error ? error.message : String(error), + path: filePath, + }, + 'Failed to read original file content', + ) + return null + } +} + +/** + * Extracts the intended file content by applying str_replace operations to the current file + */ +async function extractIntendedContent( + toolCall: CodebuffToolCall<'str_replace'>, + originalContent: string, +): Promise { + try { + let currentContent = originalContent + + // Apply all replacements to get the intended content + for (const replacement of toolCall.input.replacements) { + const { old, new: newStr, allowMultiple } = replacement + + if (allowMultiple) { + currentContent = currentContent.replaceAll(old, newStr) + } else { + // Find the first occurrence and replace it + const index = currentContent.indexOf(old) + if (index !== -1) { + currentContent = + currentContent.substring(0, index) + + newStr + + currentContent.substring(index + old.length) + } else { + // If we can't find the old string, log it but continue with other replacements + logger.warn( + { + old, + new: newStr, + allowMultiple, + currentContent, + }, + 'Failed to find old string in currentContent', + ) + } + } + } + + return currentContent + } catch (error) { + logger.warn( + { + error: error instanceof Error ? error.message : String(error), + path: toolCall.input.path, + }, + 'Failed to apply replacements for intended content extraction', + ) + return null + } +} + +function benchifyCanFixLanguage(path: string): boolean { + return BENCHIFY_FILE_TYPES.some((extension) => path.endsWith(`.${extension}`)) +} diff --git a/backend/src/tools/handlers/tool/write-file.ts b/backend/src/tools/handlers/tool/write-file.ts index 4b912a0615..261cff72d0 100644 --- a/backend/src/tools/handlers/tool/write-file.ts +++ b/backend/src/tools/handlers/tool/write-file.ts @@ -230,7 +230,7 @@ export async function postStreamProcessing( if (errors.length > 0) { if (errors.length > 1) { throw new Error( - `Internal error: Unexpected number of matching errors for ${{ toolCall }}, found ${errors.length}, expected 1`, + `Internal error: Unexpected number of matching errors for ${JSON.stringify(toolCall)}, found ${errors.length}, expected 1`, ) } @@ -251,7 +251,7 @@ export async function postStreamProcessing( ) if (changes.length !== 1) { throw new Error( - `Internal error: Unexpected number of matching changes for ${{ toolCall }}, found ${changes.length}, expected 1`, + `Internal error: Unexpected number of matching changes for ${JSON.stringify(toolCall)}, found ${changes.length}, expected 1`, ) } diff --git a/backend/src/tools/stream-parser.ts b/backend/src/tools/stream-parser.ts index 69d1bb15e0..26bd8ae1aa 100644 --- a/backend/src/tools/stream-parser.ts +++ b/backend/src/tools/stream-parser.ts @@ -9,9 +9,14 @@ import { generateCompactId } from '@codebuff/common/util/string' import { cloneDeep } from 'lodash' import { expireMessages } from '../util/messages' +import { logger } from '../util/logger' import { sendAction } from '../websockets/websocket-action' import { processStreamWithTags } from '../xml-stream-parser' import { executeCustomToolCall, executeToolCall } from './tool-executor' +import { + executeBatchStrReplaces, + BatchStrReplaceState, +} from './batch-str-replace' import type { CustomToolCall } from './tool-executor' import type { StreamChunk } from '../llm-apis/vercel-ai-sdk/ai-sdk' @@ -36,7 +41,7 @@ export type ToolCallError = { } & Omit export async function processStreamWithTools(options: { - stream: AsyncGenerator + stream: AsyncGenerator ws: WebSocket agentStepId: string clientSessionId: string @@ -79,6 +84,15 @@ export async function processStreamWithTools(options: { const { promise: streamDonePromise, resolve: resolveStreamDonePromise } = Promise.withResolvers() let previousToolCallFinished = streamDonePromise + + // Two-phase execution state + const batchState: BatchStrReplaceState = { + deferredStrReplaces: [], + otherToolsQueue: [], + strReplacePhaseComplete: false, + failures: [], + } + const state: Record = { ws, fingerprintId, @@ -108,25 +122,96 @@ export async function processStreamWithTools(options: { return { onTagStart: () => {}, onTagEnd: async (_: string, input: Record) => { - // delegated to reusable helper - previousToolCallFinished = executeToolCall({ - toolName, - input, - toolCalls, - toolResults, - toolResultsToAddAfterStream, - previousToolCallFinished, - ws, - agentTemplate, - fileContext, - agentStepId, - clientSessionId, - userInputId, - fullResponse: fullResponseChunks.join(''), - onResponseChunk, - state, - userId, - }) + // Two-phase execution: defer str_replace tools, queue others + if (toolName === 'str_replace' && !batchState.strReplacePhaseComplete) { + // Defer str_replace execution + const toolCallId = generateCompactId() + const toolCall: CodebuffToolCall<'str_replace'> = { + toolName: 'str_replace', + input: input as any, + toolCallId, + } + + batchState.deferredStrReplaces.push({ toolCall }) + + logger.debug( + { + toolCallId, + filePath: input.path, + replacementsCount: input.replacements?.length || 0, + currentDeferredCount: batchState.deferredStrReplaces.length, + agentStepId, + userInputId, + }, + 'stream-parser: Deferring str_replace tool for batch execution', + ) + + // Still emit the tool call event + onResponseChunk({ + type: 'tool_call', + toolCallId, + toolName, + input, + }) + } else { + // First non-str_replace tool marks end of str_replace phase + if ( + !batchState.strReplacePhaseComplete && + batchState.deferredStrReplaces.length > 0 + ) { + logger.info( + { + triggeringTool: toolName, + deferredCount: batchState.deferredStrReplaces.length, + agentStepId, + userInputId, + }, + `toolCallback: Triggering batch str_replace execution (${batchState.deferredStrReplaces.length} deferred tools) due to ${toolName}`, + ) + + batchState.strReplacePhaseComplete = true + + // Execute all deferred str_replace tools as a batch + previousToolCallFinished = previousToolCallFinished.then( + async () => { + await executeBatchStrReplaces({ + deferredStrReplaces: batchState.deferredStrReplaces, + toolCalls, + toolResults, + ws, + agentTemplate, + fileContext, + agentStepId, + clientSessionId, + userInputId, + fullResponse: fullResponseChunks.join(''), + onResponseChunk, + state, + userId, + }) + }, + ) + } + + previousToolCallFinished = executeToolCall({ + toolName, + input, + toolCalls, + toolResults, + toolResultsToAddAfterStream, + previousToolCallFinished, + ws, + agentTemplate, + fileContext, + agentStepId, + clientSessionId, + userInputId, + fullResponse: fullResponseChunks.join(''), + onResponseChunk, + state, + userId, + }) + } }, } } @@ -186,14 +271,7 @@ export async function processStreamWithTools(options: { ) let reasoning = false - let messageId: string | null = null - while (true) { - const { value: chunk, done } = await streamWithTags.next() - if (done) { - messageId = chunk - break - } - + for await (const chunk of streamWithTags) { if (chunk.type === 'reasoning') { if (!reasoning) { reasoning = true @@ -231,14 +309,68 @@ export async function processStreamWithTools(options: { ]) resolveStreamDonePromise() - await previousToolCallFinished + // Handle case where only str_replace tools were generated and stream ended + if ( + !batchState.strReplacePhaseComplete && + batchState.deferredStrReplaces.length > 0 + ) { + logger.info( + { + triggeringEvent: 'stream_end', + deferredCount: batchState.deferredStrReplaces.length, + deferredFiles: batchState.deferredStrReplaces.map( + (d) => d.toolCall.input.path, + ), + agentStepId, + userInputId, + }, + `stream-parser: Triggering batch str_replace execution (${batchState.deferredStrReplaces.length} deferred tools) due to stream end`, + ) + + batchState.strReplacePhaseComplete = true + + // Execute all deferred str_replace tools as a batch + previousToolCallFinished = previousToolCallFinished.then(async () => { + logger.info( + { + agentStepId, + userInputId, + deferredCount: batchState.deferredStrReplaces.length, + }, + 'stream-parser: About to call executeBatchStrReplaces from stream end handler', + ) + await executeBatchStrReplaces({ + deferredStrReplaces: batchState.deferredStrReplaces, + toolCalls, + toolResults, + ws, + agentTemplate, + fileContext, + agentStepId, + clientSessionId, + userInputId, + fullResponse: fullResponseChunks.join(''), + onResponseChunk, + state, + userId, + }) + logger.info( + { + agentStepId, + userInputId, + }, + 'stream-parser: Completed executeBatchStrReplaces from stream end handler', + ) + }) + } + + await previousToolCallFinished return { toolCalls, toolResults, state, fullResponse: fullResponseChunks.join(''), fullResponseChunks, - messageId, } } diff --git a/bun.lock b/bun.lock index 1b8053bba8..9c2bd540fe 100644 --- a/bun.lock +++ b/bun.lock @@ -49,6 +49,7 @@ "@jitl/quickjs-wasmfile-release-sync": "0.31.0", "@openrouter/ai-sdk-provider": "1.1.2", "ai": "5.0.0", + "benchify": "^0.1.0-alpha.41", "cors": "^2.8.5", "diff": "5.2.0", "dotenv": "16.4.5", @@ -1632,6 +1633,8 @@ "basic-ftp": ["basic-ftp@5.0.5", "", {}, "sha512-4Bcg1P8xhUuqcii/S0Z9wiHIrQVPMermM1any+MX5GeGD7faD3/msQUDGLol9wOcz4/jbg/WJnGqoJF6LiBdtg=="], + "benchify": ["benchify@0.1.0-alpha.41", "", { "dependencies": { "minimatch": "^9.0.3" }, "peerDependencies": { "react": ">=16.8.0" }, "optionalPeers": ["react"] }, "sha512-iZAH2JFcGld/lruJEZKO9dv7XAU8ozEznPtxNLQj+6s1CQMIohzRLnEbvCWLWkMoqSQlJOIp2gCY6N9gt956yQ=="], + "bidi-js": ["bidi-js@1.0.3", "", { "dependencies": { "require-from-string": "^2.0.2" } }, "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw=="], "big.js": ["big.js@6.2.2", "", {}, "sha512-y/ie+Faknx7sZA5MfGA2xKlu0GDv8RWrXGsmlteyJQ2lvoKv9GBK/fpRMc2qlSoBAgNxrixICFCBefIq8WCQpQ=="], @@ -4230,6 +4233,8 @@ "babel-plugin-istanbul/istanbul-lib-instrument": ["istanbul-lib-instrument@5.2.1", "", { "dependencies": { "@babel/core": "^7.12.3", "@babel/parser": "^7.14.7", "@istanbuljs/schema": "^0.1.2", "istanbul-lib-coverage": "^3.2.0", "semver": "^6.3.0" } }, "sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg=="], + "benchify/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="], + "bl/buffer": ["buffer@5.7.1", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.1.13" } }, "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ=="], "bl/readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="], @@ -5010,6 +5015,8 @@ "babel-plugin-istanbul/istanbul-lib-instrument/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], + "benchify/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="], + "body-parser/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="], "chalk/ansi-styles/color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], diff --git a/npm-app/src/tool-handlers.ts b/npm-app/src/tool-handlers.ts index 7e90d81262..cf8c947c83 100644 --- a/npm-app/src/tool-handlers.ts +++ b/npm-app/src/tool-handlers.ts @@ -67,16 +67,26 @@ export const handleUpdateFile = async < console.log(green(`- Created ${file} ${counts}`)) } for (const file of modified) { - // Calculate added/deleted lines from the diff content + // Calculate added/deleted lines from the diff content, excluding metadata let addedLines = 0 let deletedLines = 0 - lines.forEach((line) => { + + for (const line of lines) { + // Skip all diff metadata lines (headers, hunk headers, etc.) + if ( + line.startsWith('---') || + line.startsWith('+++') || + line.startsWith('@@') + ) { + continue + } + // Count actual added/removed code lines if (line.startsWith('+')) { addedLines++ } else if (line.startsWith('-')) { deletedLines++ } - }) + } const counts = `(${green(`+${addedLines}`)}, ${red(`-${deletedLines}`)})` result.push([ diff --git a/packages/internal/src/env.ts b/packages/internal/src/env.ts index ecc4510305..44db06004b 100644 --- a/packages/internal/src/env.ts +++ b/packages/internal/src/env.ts @@ -10,6 +10,7 @@ const envSchema = { server: { // Backend variables CODEBUFF_API_KEY: z.string().optional(), + BENCHIFY_API_KEY: z.string().optional(), OPEN_ROUTER_API_KEY: z.string().min(1), RELACE_API_KEY: z.string().min(1), LINKUP_API_KEY: z.string().min(1), @@ -51,6 +52,7 @@ const envSchema = { runtimeEnv: { // Backend variables CODEBUFF_API_KEY: process.env.CODEBUFF_API_KEY, + BENCHIFY_API_KEY: process.env.BENCHIFY_API_KEY, OPEN_ROUTER_API_KEY: process.env.OPEN_ROUTER_API_KEY, RELACE_API_KEY: process.env.RELACE_API_KEY, LINKUP_API_KEY: process.env.LINKUP_API_KEY, From 8fcca4ab173ea644c5d80feb995f8f7b267a069f Mon Sep 17 00:00:00 2001 From: brandonkachen Date: Tue, 23 Sep 2025 18:28:16 -0700 Subject: [PATCH 02/10] Update npm-app-release-staging.yml --- .github/workflows/npm-app-release-staging.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/npm-app-release-staging.yml b/.github/workflows/npm-app-release-staging.yml index 58c6a1ade0..1d556cea6c 100644 --- a/.github/workflows/npm-app-release-staging.yml +++ b/.github/workflows/npm-app-release-staging.yml @@ -134,7 +134,7 @@ jobs: new-version: ${{ needs.prepare-and-commit-staging.outputs.new_version }} artifact-name: updated-staging-package checkout-ref: ${{ github.event.pull_request.head.sha }} - env-overrides: '{"NEXT_PUBLIC_CB_ENVIRONMENT": "prod", "NEXT_PUBLIC_CODEBUFF_BACKEND_URL": "backend-pr-221-we0m.onrender.com"}' + env-overrides: '{"NEXT_PUBLIC_CB_ENVIRONMENT": "prod", "NEXT_PUBLIC_CODEBUFF_BACKEND_URL": "backend-pr-312-3hui.onrender.com"}' secrets: inherit # Create GitHub prerelease with all binaries From abe0c396a63002024e1b1047c3e0907fe2b6c864 Mon Sep 17 00:00:00 2001 From: brandonkachen Date: Wed, 24 Sep 2025 11:51:12 -0700 Subject: [PATCH 03/10] Update batch-str-replace.ts --- backend/src/tools/batch-str-replace.ts | 570 ++++++++++++++++--------- 1 file changed, 365 insertions(+), 205 deletions(-) diff --git a/backend/src/tools/batch-str-replace.ts b/backend/src/tools/batch-str-replace.ts index ed1562ee31..e2c9615b5f 100644 --- a/backend/src/tools/batch-str-replace.ts +++ b/backend/src/tools/batch-str-replace.ts @@ -5,7 +5,10 @@ import { Benchify } from 'benchify' import { env } from '@codebuff/internal/env' import { requestToolCall } from '../websockets/websocket-action' import { createPatch } from 'diff' -import type { CodebuffToolCall } from '@codebuff/common/tools/list' +import type { + CodebuffToolCall, + CodebuffToolOutput, +} from '@codebuff/common/tools/list' import type { ToolResultPart } from '@codebuff/common/types/messages/content-part' import type { PrintModeEvent } from '@codebuff/common/types/print-mode' import type { AgentTemplate } from '../templates/types' @@ -55,6 +58,16 @@ function getBenchifyClient(): Benchify | null { return benchifyClient } +type BatchContext = { + ws: WebSocket + userInputId: string + onResponseChunk: (chunk: string | PrintModeEvent) => void + state: Record + originalContents: Record + editedFiles: Map + intendedChanges: Map +} + export async function executeBatchStrReplaces({ deferredStrReplaces, toolCalls, @@ -88,240 +101,388 @@ export async function executeBatchStrReplaces({ return } - const batchPromises: Promise[] = [] - let previousPromise = Promise.resolve() + // Group operations by file path for per-path processing + const operationsByPath = new Map() + for (const operation of deferredStrReplaces) { + const path = operation.toolCall.input.path + if (!operationsByPath.has(path)) { + operationsByPath.set(path, []) + } + operationsByPath.get(path)!.push(operation) + } + + // Initialize batch context + const batchContext: BatchContext = { + ws, + userInputId, + onResponseChunk, + state, + originalContents: {}, + editedFiles: new Map(), + intendedChanges: new Map(), + } + + // Pre-load original content for all paths that support benchify + await preloadOriginalContent(operationsByPath, fileContext, batchContext) + + // Extract intended changes for benchify (before execution) + await extractAllIntendedChanges(operationsByPath, batchContext) + + // Execute operations grouped by path for better parallelization + const pathPromises = new Map>() + + for (const [path, operations] of operationsByPath) { + pathPromises.set( + path, + processPathOperations(path, operations, { + toolCalls, + toolResults, + agentStepId, + batchContext, + }), + ) + } + + // Wait for all path-based operations to complete + await Promise.all(pathPromises.values()) + + // Apply benchify if we have intended changes + await applyBenchifyIfNeeded(batchContext, { + agentStepId, + clientSessionId, + userInputId, + userId, + toolResults, + toolCalls: deferredStrReplaces.map((d) => d.toolCall), + }) +} - // Track successfully edited files for benchify call - const editedFiles: { path: string; contents: string }[] = [] - // Track intended changes from LLM for benchify call (even if str_replace fails) - const intendedChanges: { path: string; contents: string }[] = [] - // Track original file contents before any modifications - const originalContents: Record = {} - - // Execute all str_replace calls in sequence to maintain file consistency - for (let i = 0; i < deferredStrReplaces.length; i++) { - const { toolCall } = deferredStrReplaces[i] - - // Read original content before any modifications (only once per file) - if ( - benchifyCanFixLanguage(toolCall.input.path) && - !originalContents[toolCall.input.path] - ) { +/** + * Pre-loads original file content for all paths that support benchify + */ +async function preloadOriginalContent( + operationsByPath: Map, + fileContext: ProjectFileContext, + batchContext: BatchContext, +) { + const pathsToLoad = Array.from(operationsByPath.keys()).filter( + benchifyCanFixLanguage, + ) + + await Promise.all( + pathsToLoad.map(async (path) => { try { - const originalContent = await extractOriginalContent( - toolCall.input.path, - fileContext, - ) - if (originalContent) { - originalContents[toolCall.input.path] = originalContent + const content = await extractOriginalContent(path, fileContext) + if (content) { + batchContext.originalContents[path] = content } } catch (error) { logger.warn( { error: error instanceof Error ? error.message : String(error), - path: toolCall.input.path, + path, }, 'Failed to read original content for benchify', ) } + }), + ) +} + +/** + * Extracts intended changes for all operations (for benchify) + */ +async function extractAllIntendedChanges( + operationsByPath: Map, + batchContext: BatchContext, +) { + for (const [path, operations] of operationsByPath) { + if (!benchifyCanFixLanguage(path) || !batchContext.originalContents[path]) { + continue } - // Extract intended content from str_replace operation before attempting execution - if ( - benchifyCanFixLanguage(toolCall.input.path) && - originalContents[toolCall.input.path] - ) { - try { - const intendedContent = await extractIntendedContent( - toolCall, - originalContents[toolCall.input.path], - ) - if (intendedContent) { - const existingIndex = intendedChanges.findIndex( - (f) => f.path === toolCall.input.path, - ) - if (existingIndex >= 0) { - intendedChanges[existingIndex].contents = intendedContent - } else { - intendedChanges.push({ - path: toolCall.input.path, - contents: intendedContent, - }) - } - } - } catch (error) { - logger.warn( - { - error: error instanceof Error ? error.message : String(error), - path: toolCall.input.path, - }, - 'Failed to extract intended content for benchify', - ) + try { + let currentContent = batchContext.originalContents[path] + + // Apply all operations sequentially to get final intended content + for (const { toolCall } of operations) { + currentContent = + (await extractIntendedContent(toolCall, currentContent)) || + currentContent } + + batchContext.intendedChanges.set(path, currentContent) + } catch (error) { + logger.warn( + { error: error instanceof Error ? error.message : String(error), path }, + 'Failed to extract intended content for benchify', + ) } + } +} - // Chain each str_replace to the previous one to ensure proper ordering - const strReplacePromise = previousPromise.then(async () => { - try { - const { result } = handleStrReplace({ - previousToolCallFinished: Promise.resolve(), - toolCall, - requestClientToolCall: async () => { - throw new Error('Client tool calls not supported in batch mode') - }, - writeToClient: onResponseChunk, - getLatestState: () => getFileProcessingValues(state), - state: { ...state, ws }, - }) - - const toolResult = await result - - if (toolResult) { - const toolResultPart: ToolResultPart = { - type: 'tool-result', - toolName: 'str_replace', - toolCallId: toolCall.toolCallId, - output: toolResult, - } - - toolResults.push(toolResultPart) - - onResponseChunk({ - type: 'tool_result', - toolCallId: toolCall.toolCallId, - output: toolResult, - }) - - // Add to message history - state.messages.push({ - role: 'tool' as const, - content: toolResultPart, - }) - - // Track successfully edited files - if ( - Array.isArray(toolResult) && - toolResult.length > 0 && - benchifyCanFixLanguage(toolCall.input.path) - ) { - const result = toolResult[0] - if ( - result.type === 'json' && - result.value && - 'content' in result.value - ) { - const existingFileIndex = editedFiles.findIndex( - (f) => f.path === toolCall.input.path, - ) - const fileContent = result.value.content as string - - if (existingFileIndex >= 0) { - // Update existing file with latest content - editedFiles[existingFileIndex].contents = fileContent - } else { - // Add new file to tracking - editedFiles.push({ - path: toolCall.input.path, - contents: fileContent, - }) - } - } - } - } - } catch (error) { - logger.error( - { - error: - error instanceof Error - ? { - message: error.message, - stack: error.stack, - name: error.name, - } - : error, - toolCallId: toolCall.toolCallId, - toolCallInput: JSON.stringify(toolCall.input, null, 2), - agentStepId, - userInputId, - }, - `Error executing batched str_replace ${i + 1}/${deferredStrReplaces.length}`, - ) +/** + * Processes all operations for a single file path sequentially + */ +async function processPathOperations( + path: string, + operations: DeferredStrReplace[], + context: { + toolCalls: (CodebuffToolCall | any)[] + toolResults: ToolResultPart[] + agentStepId: string + batchContext: BatchContext + }, +) { + let previousPromise = Promise.resolve() - // Create error result - const errorResult: ToolResultPart = { - type: 'tool-result', - toolName: 'str_replace', - toolCallId: toolCall.toolCallId, - output: [ - { - type: 'json', - value: { - errorMessage: `Batched str_replace failed: ${error instanceof Error ? error.message : String(error)}`, - }, - }, - ], - } + for (let i = 0; i < operations.length; i++) { + const { toolCall } = operations[i] - toolResults.push(errorResult) - onResponseChunk({ - type: 'tool_result', - toolCallId: toolCall.toolCallId, - output: errorResult.output, - }) - } + previousPromise = previousPromise.then(() => + executeSingleStrReplace(toolCall, i + 1, operations.length, context), + ) + } + + await previousPromise +} + +/** + * Executes a single str_replace operation with proper error handling + */ +async function executeSingleStrReplace( + toolCall: CodebuffToolCall<'str_replace'>, + operationIndex: number, + totalOperations: number, + context: { + toolCalls: (CodebuffToolCall | any)[] + toolResults: ToolResultPart[] + agentStepId: string + batchContext: BatchContext + }, +) { + const { batchContext, toolCalls, toolResults, agentStepId } = context + + try { + // Create isolated state for each operation + const isolatedState = { + ...batchContext.state, + ws: batchContext.ws, + promisesByPath: {}, + allPromises: [], + fileChangeErrors: [], + fileChanges: [], + firstFileProcessed: false, + } + + const { result } = handleStrReplace({ + previousToolCallFinished: Promise.resolve(), + toolCall, + requestClientToolCall: createRequestClientToolCall(batchContext), + writeToClient: batchContext.onResponseChunk, + getLatestState: () => getFileProcessingValues(isolatedState), + state: isolatedState, }) - // Add to toolCalls array + const toolResult = await result + + if (toolResult) { + const toolResultPart = createToolResultPart(toolCall, toolResult) + + toolResults.push(toolResultPart) + batchContext.onResponseChunk({ + type: 'tool_result', + toolCallId: toolCall.toolCallId, + output: toolResult, + }) + + // Add to message history + batchContext.state.messages.push({ + role: 'tool' as const, + content: toolResultPart, + }) + + // Track edited files for benchify + trackEditedFile(toolCall, toolResult, batchContext) + } + toolCalls.push(toolCall) - batchPromises.push(strReplacePromise) - previousPromise = strReplacePromise + } catch (error) { + handleStrReplaceError(error, toolCall, operationIndex, totalOperations, { + toolResults, + agentStepId, + batchContext, + }) + } +} + +/** + * Creates a typed requestClientToolCall function for batch mode + */ +function createRequestClientToolCall(batchContext: BatchContext) { + return async ( + clientToolCall: any, + ): Promise> => { + const result = await requestToolCall( + batchContext.ws, + batchContext.userInputId, + clientToolCall.toolName, + clientToolCall.input, + ) + return result.output as CodebuffToolOutput<'str_replace'> + } +} + +/** + * Creates a properly typed tool result part + */ +function createToolResultPart( + toolCall: CodebuffToolCall<'str_replace'>, + toolResult: CodebuffToolOutput<'str_replace'>, +): ToolResultPart { + return { + type: 'tool-result', + toolName: 'str_replace', + toolCallId: toolCall.toolCallId, + output: toolResult, + } +} + +/** + * Tracks successfully edited files for benchify processing + */ +function trackEditedFile( + toolCall: CodebuffToolCall<'str_replace'>, + toolResult: CodebuffToolOutput<'str_replace'>, + batchContext: BatchContext, +) { + if ( + Array.isArray(toolResult) && + toolResult.length > 0 && + benchifyCanFixLanguage(toolCall.input.path) + ) { + const result = toolResult[0] + if (result.type === 'json' && result.value && 'content' in result.value) { + batchContext.editedFiles.set( + toolCall.input.path, + result.value.content as string, + ) + } } +} + +/** + * Handles errors from str_replace operations with proper logging and error results + */ +function handleStrReplaceError( + error: unknown, + toolCall: CodebuffToolCall<'str_replace'>, + operationIndex: number, + totalOperations: number, + context: { + toolResults: ToolResultPart[] + agentStepId: string + batchContext: BatchContext + }, +) { + const { toolResults, agentStepId, batchContext } = context - // Wait for all batched operations to complete - await Promise.all(batchPromises) + logger.error( + { + error: + error instanceof Error + ? { + message: error.message, + stack: error.stack, + name: error.name, + } + : error, + toolCallId: toolCall.toolCallId, + path: toolCall.input.path, + agentStepId, + userInputId: batchContext.userInputId, + }, + `Error executing batched str_replace ${operationIndex}/${totalOperations}`, + ) + + const errorResult: ToolResultPart = { + type: 'tool-result', + toolName: 'str_replace', + toolCallId: toolCall.toolCallId, + output: [ + { + type: 'json', + value: { + errorMessage: `Batched str_replace failed: ${error instanceof Error ? error.message : String(error)}`, + }, + }, + ], + } - // Call benchify with intended changes (even if str_replace operations failed) + toolResults.push(errorResult) + batchContext.onResponseChunk({ + type: 'tool_result', + toolCallId: toolCall.toolCallId, + output: errorResult.output, + }) +} + +/** + * Applies benchify results if there are intended changes + */ +async function applyBenchifyIfNeeded( + batchContext: BatchContext, + options: { + agentStepId: string + clientSessionId: string + userInputId: string + userId: string | undefined + toolResults: ToolResultPart[] + toolCalls: CodebuffToolCall<'str_replace'>[] + }, +) { const client = getBenchifyClient() - if (!client || intendedChanges.length === 0) { + if (!client || batchContext.intendedChanges.size === 0) { return } try { - const benchifyResult = await callBenchify(intendedChanges, { - agentStepId, - clientSessionId, - userInputId, - userId, - }) + const intendedChangesArray = Array.from( + batchContext.intendedChanges.entries(), + ).map(([path, contents]) => ({ path, contents })) + + const benchifyResult = await callBenchify(intendedChangesArray, options) if (benchifyResult && benchifyResult.length > 0) { logger.info( { benchifyResultCount: benchifyResult.length, resultFiles: benchifyResult.map((r) => r.path), - agentStepId, - userInputId, + agentStepId: options.agentStepId, + userInputId: options.userInputId, }, `executeBatchStrReplaces: Benchify returned ${benchifyResult.length} results, applying them`, ) - // Apply benchify results back to files await applyBenchifyResults(benchifyResult, { - ws, - onResponseChunk, - state: { ...state, originalContents }, - toolResults, - toolCalls: deferredStrReplaces.map((d) => d.toolCall), - userInputId, + ws: batchContext.ws, + onResponseChunk: batchContext.onResponseChunk, + state: { + ...batchContext.state, + originalContents: batchContext.originalContents, + }, + toolResults: options.toolResults, + toolCalls: options.toolCalls, + userInputId: options.userInputId, }) } } catch (error) { logger.error( { error: error instanceof Error ? error.message : String(error), - intendedChangeFiles: intendedChanges.map((f) => f.path), - agentStepId, - userInputId, + intendedChangeFiles: Array.from(batchContext.intendedChanges.keys()), + agentStepId: options.agentStepId, + userInputId: options.userInputId, }, 'executeBatchStrReplaces: Failed to call benchify with intended changes', ) @@ -482,45 +643,44 @@ async function extractOriginalContent( } /** - * Extracts the intended file content by applying str_replace operations to the current file + * Extracts the intended file content by applying str_replace operations to the current content */ async function extractIntendedContent( toolCall: CodebuffToolCall<'str_replace'>, - originalContent: string, + currentContent: string, ): Promise { try { - let currentContent = originalContent + let content = currentContent // Apply all replacements to get the intended content for (const replacement of toolCall.input.replacements) { const { old, new: newStr, allowMultiple } = replacement if (allowMultiple) { - currentContent = currentContent.replaceAll(old, newStr) + content = content.replaceAll(old, newStr) } else { // Find the first occurrence and replace it - const index = currentContent.indexOf(old) + const index = content.indexOf(old) if (index !== -1) { - currentContent = - currentContent.substring(0, index) + + content = + content.substring(0, index) + newStr + - currentContent.substring(index + old.length) + content.substring(index + old.length) } else { - // If we can't find the old string, log it but continue with other replacements - logger.warn( + // Log warning but continue - this might be expected if operations are interdependent + logger.debug( { - old, - new: newStr, - allowMultiple, - currentContent, + old: old.substring(0, 100), // Truncate for logging + new: newStr.substring(0, 100), + path: toolCall.input.path, }, - 'Failed to find old string in currentContent', + 'String not found in content during intended content extraction', ) } } } - return currentContent + return content } catch (error) { logger.warn( { From e1b363e7372ca57a4d9074ef2e5e378b74b748fd Mon Sep 17 00:00:00 2001 From: brandonkachen Date: Wed, 24 Sep 2025 13:59:59 -0700 Subject: [PATCH 04/10] Unify Benchify resilience and batched str_replace refactors; simplify validation and streaming integration MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Consolidates recent improvements to Benchify post-processing and batched str_replace execution. Reworks validateBenchifyResponse to use structural pattern match with clear business logic checks (no nested matches/continues), simplifies stream-parser integration, and expands tests for reliability. 🤖 Generated with Codebuff Co-Authored-By: Codebuff --- .../src/__tests__/process-str-replace.test.ts | 166 +++++- backend/src/process-str-replace.ts | 8 +- backend/src/tools/batch-str-replace.ts | 517 ++++++++++++++---- backend/src/tools/stream-parser.ts | 16 - 4 files changed, 589 insertions(+), 118 deletions(-) diff --git a/backend/src/__tests__/process-str-replace.test.ts b/backend/src/__tests__/process-str-replace.test.ts index 1d5230690c..f594896dd3 100644 --- a/backend/src/__tests__/process-str-replace.test.ts +++ b/backend/src/__tests__/process-str-replace.test.ts @@ -1,7 +1,13 @@ -import { describe, expect, it } from 'bun:test' +import * as envModule from '@codebuff/internal/env' +import { describe, expect, it, spyOn } from 'bun:test' import { applyPatch } from 'diff' import { processStrReplace } from '../process-str-replace' +import { mockFileContext } from './test-utils' +import { + executeBatchStrReplaces, + benchifyCanFixLanguage, +} from '../tools/batch-str-replace' describe('processStrReplace', () => { it('should replace exact string matches', async () => { @@ -213,6 +219,25 @@ describe('processStrReplace', () => { } }) + it('should handle replacement where old string equals new string', async () => { + const initialContent = 'const x = 1;\nconst y = 2;\n' + const oldStr = 'const y = 2;' + const newStr = 'const y = 2;' // Same as old string + + const result = await processStrReplace( + 'test.ts', + [{ old: oldStr, new: newStr, allowMultiple: false }], + Promise.resolve(initialContent), + ) + + expect(result).not.toBeNull() + expect('content' in result).toBe(true) + if ('content' in result) { + expect(result.content).toBe('const x = 1;\nconst y = 2;\n') + expect(result.messages).toEqual([]) + } + }) + // New comprehensive tests for allowMultiple functionality describe('allowMultiple functionality', () => { it('should error when multiple occurrences exist and allowMultiple is false', async () => { @@ -417,3 +442,142 @@ function test3() { ) }) }) + +// Tests for Benchify resilience +describe('Benchify resilience', () => { + describe('happy path', () => { + it('should identify Benchify-supported file types correctly', () => { + const testCases = [ + { path: 'component.tsx', expected: true }, + { path: 'utils.ts', expected: true }, + { path: 'script.js', expected: true }, + { path: 'styles.jsx', expected: true }, + { path: 'README.md', expected: false }, + { path: 'config.json', expected: false }, + { path: 'styles.css', expected: false }, + { path: 'index.html', expected: false }, + { path: 'test.py', expected: false }, + ] + + for (const { path, expected } of testCases) { + const result = benchifyCanFixLanguage(path) + expect(result).toBe(expected) + } + }) + + it('should handle file extensions case sensitivity', () => { + expect(benchifyCanFixLanguage('Component.TSX')).toBe(false) // Wrong case + expect(benchifyCanFixLanguage('component.tsx')).toBe(true) // Correct case + expect(benchifyCanFixLanguage('utils.TS')).toBe(false) // Wrong case + expect(benchifyCanFixLanguage('utils.ts')).toBe(true) // Correct case + }) + + it('should handle file paths with multiple dots', () => { + expect(benchifyCanFixLanguage('component.test.tsx')).toBe(true) + expect(benchifyCanFixLanguage('utils.spec.ts')).toBe(true) + expect(benchifyCanFixLanguage('config.local.js')).toBe(true) + expect(benchifyCanFixLanguage('styles.module.css')).toBe(false) + }) + + it('should handle files without extensions', () => { + expect(benchifyCanFixLanguage('Dockerfile')).toBe(false) + expect(benchifyCanFixLanguage('Makefile')).toBe(false) + expect(benchifyCanFixLanguage('README')).toBe(false) + }) + }) + + it('should fall back gracefully when Benchify is disabled', async () => { + // Test with no API key - spy on the env object directly + spyOn(envModule, 'env', 'get').mockReturnValue({ + // Empty object simulates no BENCHIFY_API_KEY + } as any) + + const result = await executeBatchStrReplaces({ + deferredStrReplaces: [ + { + toolCall: { + toolName: 'str_replace' as const, + toolCallId: 'test-call', + input: { + path: 'test.ts', + replacements: [{ old: 'old', new: 'new', allowMultiple: false }], + }, + }, + }, + ], + toolCalls: [], + toolResults: [], + ws: {} as any, + fileContext: mockFileContext, + agentStepId: 'test-step', + clientSessionId: 'test-session', + userInputId: 'test-input', + onResponseChunk: () => {}, + state: { messages: [] }, + userId: 'test-user', + }) + + // Should complete without error even when Benchify is unavailable + expect(result).toBeUndefined() // Function returns void + }) + + describe('Batch str_replace integration tests', () => { + it('should handle empty deferred list without error', async () => { + // Simple test that doesn't require complex mocking + expect( + executeBatchStrReplaces({ + deferredStrReplaces: [], + toolCalls: [], + toolResults: [], + ws: {} as any, + fileContext: mockFileContext, + agentStepId: 'test-step', + clientSessionId: 'test-session', + userInputId: 'test-input', + onResponseChunk: () => {}, + state: { messages: [] }, + userId: 'test-user', + }), + ).resolves.toBeUndefined() // Should complete without throwing + }) + }) + + it('should identify Benchify-supported file types correctly', () => { + const testCases = [ + { path: 'component.tsx', expected: true }, + { path: 'utils.ts', expected: true }, + { path: 'script.js', expected: true }, + { path: 'styles.jsx', expected: true }, + { path: 'README.md', expected: false }, + { path: 'config.json', expected: false }, + { path: 'styles.css', expected: false }, + { path: 'index.html', expected: false }, + { path: 'test.py', expected: false }, + ] + + for (const { path, expected } of testCases) { + const result = benchifyCanFixLanguage(path) + expect(result).toBe(expected) + } + }) + + it('should handle executeBatchStrReplaces with empty list', async () => { + // Simple test that doesn't require complex mocking + const result = await executeBatchStrReplaces({ + deferredStrReplaces: [], + toolCalls: [], + toolResults: [], + ws: {} as any, + fileContext: mockFileContext, + agentStepId: 'test-step', + clientSessionId: 'test-session', + userInputId: 'test-input', + onResponseChunk: () => {}, + state: { messages: [] }, + userId: 'test-user', + }) + + // Should complete without throwing an error + expect(result).toBeUndefined() // Function returns void + }) +}) diff --git a/backend/src/process-str-replace.ts b/backend/src/process-str-replace.ts index 16821ac71e..ad26ab4e1c 100644 --- a/backend/src/process-str-replace.ts +++ b/backend/src/process-str-replace.ts @@ -35,6 +35,7 @@ export async function processStrReplace( let currentContent = initialContent let messages: string[] = [] const lineEnding = currentContent.includes('\r\n') ? '\r\n' : '\n' + let anyReplacementSuccessful = false for (const { old: oldStr, new: newStr, allowMultiple } of replacements) { // Regular case: require oldStr for replacements @@ -59,6 +60,7 @@ export async function processStrReplace( if (match.success) { updatedOldStr = match.oldStr + anyReplacementSuccessful = true } else { messages.push(match.error) updatedOldStr = null @@ -72,15 +74,15 @@ export async function processStrReplace( currentContent = currentContent.replaceAll('\n', lineEnding) - if (initialContent === currentContent) { + // If no successful replacements occurred, return error + if (!anyReplacementSuccessful) { logger.debug( { path, initialContent, }, - `processStrReplace: No change to ${path}`, + `processStrReplace: No successful replacements for ${path}`, ) - messages.push('No change to the file.') return { tool: 'str_replace' as const, path, diff --git a/backend/src/tools/batch-str-replace.ts b/backend/src/tools/batch-str-replace.ts index e2c9615b5f..e152abb423 100644 --- a/backend/src/tools/batch-str-replace.ts +++ b/backend/src/tools/batch-str-replace.ts @@ -5,13 +5,15 @@ import { Benchify } from 'benchify' import { env } from '@codebuff/internal/env' import { requestToolCall } from '../websockets/websocket-action' import { createPatch } from 'diff' +import { withRetry, withTimeout } from '@codebuff/common/util/promise' +import { match, P } from 'ts-pattern' import type { CodebuffToolCall, CodebuffToolOutput, } from '@codebuff/common/tools/list' import type { ToolResultPart } from '@codebuff/common/types/messages/content-part' import type { PrintModeEvent } from '@codebuff/common/types/print-mode' -import type { AgentTemplate } from '../templates/types' + import type { ProjectFileContext } from '@codebuff/common/util/file' import type { WebSocket } from 'ws' import { file } from 'bun' @@ -28,10 +30,24 @@ export type BatchStrReplaceState = { } const BENCHIFY_FILE_TYPES = ['tsx', 'ts', 'jsx', 'js'] +const BENCHIFY_TIMEOUT_MS = 3000 // 3 second timeout for Benchify calls +const BENCHIFY_MAX_FILES = 10 // Maximum files to send to Benchify +const BENCHIFY_MAX_FILE_SIZE = 1024 * 1024 // 1MB max file size // Global Benchify client instance let benchifyClient: Benchify | null = null +// Circuit breaker state for Benchify +let benchifyCircuitBreaker = { + failureCount: 0, + lastFailureTime: 0, + isOpen: false, + openUntil: 0, +} + +const CIRCUIT_BREAKER_THRESHOLD = 3 // Open circuit after 3 consecutive failures +const CIRCUIT_BREAKER_TIMEOUT = 60000 // Keep circuit open for 1 minute + function getBenchifyClient(): Benchify | null { if (!benchifyClient) { let benchifyApiKey: string | undefined @@ -73,12 +89,10 @@ export async function executeBatchStrReplaces({ toolCalls, toolResults, ws, - agentTemplate, fileContext, agentStepId, clientSessionId, userInputId, - fullResponse, onResponseChunk, state, userId, @@ -87,12 +101,10 @@ export async function executeBatchStrReplaces({ toolCalls: (CodebuffToolCall | any)[] toolResults: ToolResultPart[] ws: WebSocket - agentTemplate: AgentTemplate fileContext: ProjectFileContext agentStepId: string clientSessionId: string userInputId: string - fullResponse: string onResponseChunk: (chunk: string | PrintModeEvent) => void state: Record userId: string | undefined @@ -428,7 +440,7 @@ function handleStrReplaceError( } /** - * Applies benchify results if there are intended changes + * Applies benchify results if there are intended changes (with graceful failure handling) */ async function applyBenchifyIfNeeded( batchContext: BatchContext, @@ -441,17 +453,47 @@ async function applyBenchifyIfNeeded( toolCalls: CodebuffToolCall<'str_replace'>[] }, ) { + // Early exit conditions - fail gracefully without blocking user edits const client = getBenchifyClient() if (!client || batchContext.intendedChanges.size === 0) { return } + // Check circuit breaker + if (isBenchifyCircuitOpen()) { + logger.debug( + { + circuitState: benchifyCircuitBreaker, + agentStepId: options.agentStepId, + userInputId: options.userInputId, + }, + 'Benchify circuit breaker is open, skipping call', + ) + return + } + try { - const intendedChangesArray = Array.from( - batchContext.intendedChanges.entries(), - ).map(([path, contents]) => ({ path, contents })) + // Filter and validate intended changes for Benchify + const filteredChanges = filterBenchifyFiles( + Array.from(batchContext.intendedChanges.entries()).map( + ([path, contents]) => ({ path, contents }), + ), + options.agentStepId, + ) + + if (filteredChanges.length === 0) { + logger.debug( + { agentStepId: options.agentStepId }, + 'No valid files for Benchify after filtering', + ) + return + } - const benchifyResult = await callBenchify(intendedChangesArray, options) + // Call Benchify with timeout and retry logic + const benchifyResult = await callBenchifyWithResilience( + filteredChanges, + options, + ) if (benchifyResult && benchifyResult.length > 0) { logger.info( @@ -464,7 +506,8 @@ async function applyBenchifyIfNeeded( `executeBatchStrReplaces: Benchify returned ${benchifyResult.length} results, applying them`, ) - await applyBenchifyResults(benchifyResult, { + // Apply results with individual error handling to prevent one failure from blocking others + await applyBenchifyResultsGracefully(benchifyResult, { ws: batchContext.ws, onResponseChunk: batchContext.onResponseChunk, state: { @@ -474,25 +517,67 @@ async function applyBenchifyIfNeeded( toolResults: options.toolResults, toolCalls: options.toolCalls, userInputId: options.userInputId, + agentStepId: options.agentStepId, }) } + + // Reset circuit breaker on success + resetBenchifyCircuitBreaker() } catch (error) { - logger.error( + // Handle Benchify failure gracefully without blocking user edits + handleBenchifyFailure(error, { + intendedChangeFiles: Array.from(batchContext.intendedChanges.keys()), + agentStepId: options.agentStepId, + userInputId: options.userInputId, + }) + } +} + +/** + * Filters files for Benchify processing based on size and count limits + */ +function filterBenchifyFiles( + files: { path: string; contents: string }[], + agentStepId: string, +): { path: string; contents: string }[] { + const filtered = files.filter((file) => { + // Check file size limit + if (file.contents.length > BENCHIFY_MAX_FILE_SIZE) { + logger.debug( + { path: file.path, size: file.contents.length, agentStepId }, + 'Skipping large file for Benchify', + ) + return false + } + + // Check if it's a supported file type + if (!benchifyCanFixLanguage(file.path)) { + return false + } + + return true + }) + + // Limit the number of files sent to Benchify + if (filtered.length > BENCHIFY_MAX_FILES) { + logger.debug( { - error: error instanceof Error ? error.message : String(error), - intendedChangeFiles: Array.from(batchContext.intendedChanges.keys()), - agentStepId: options.agentStepId, - userInputId: options.userInputId, + totalFiles: filtered.length, + maxFiles: BENCHIFY_MAX_FILES, + agentStepId, }, - 'executeBatchStrReplaces: Failed to call benchify with intended changes', + 'Limiting files sent to Benchify', ) + return filtered.slice(0, BENCHIFY_MAX_FILES) } + + return filtered } /** - * Calls benchify API with the list of edited files + * Calls benchify API with timeout and retry logic using common utilities */ -async function callBenchify( +async function callBenchifyWithResilience( editedFiles: { path: string; contents: string }[], context: { agentStepId: string @@ -506,27 +591,122 @@ async function callBenchify( return null } - const response = await client.runFixer(editedFiles, { - fix_types: ['string_literals'], - }) + return await withRetry( + async () => { + const response = await withTimeout( + client.runFixer(editedFiles, { + fix_types: ['string_literals'], + }), + BENCHIFY_TIMEOUT_MS, + `Benchify call timed out after ${BENCHIFY_TIMEOUT_MS}ms`, + ) + + // Validate response + if (response && Array.isArray(response)) { + return validateBenchifyResponse( + response, + editedFiles, + context.agentStepId, + ) + } - logger.info( + return null + }, { - responseReceived: !!response, - responseLength: response?.length || 0, - responseFiles: response?.map((r) => r.path) || [], - ...context, + maxRetries: 2, + retryIf: shouldRetryBenchifyError, + onRetry: (error, attempt) => { + logger.debug( + { + error: error instanceof Error ? error.message : String(error), + attempt, + agentStepId: context.agentStepId, + }, + 'Retrying Benchify call', + ) + }, + retryDelayMs: 100, }, - 'Benchify runFixer API response received', ) +} + +/** + * Validates Benchify API response using pattern matching + */ +function validateBenchifyResponse( + response: any[], + originalFiles: { path: string; contents: string }[], + agentStepId: string, +): { path: string; contents: string }[] { + const originalPaths = new Set(originalFiles.map((f) => f.path)) + + return response.flatMap((result) => + match(result) + .with({ path: P.string, contents: P.string }, (res) => { + if (!originalPaths.has(res.path)) { + logger.warn( + { path: res.path, agentStepId }, + 'Benchify returned result for unexpected path', + ) + return [] + } + if (res.contents.length > BENCHIFY_MAX_FILE_SIZE * 2) { + logger.warn( + { + path: res.path, + size: res.contents.length, + agentStepId, + }, + 'Benchify result exceeds size limit', + ) + return [] + } + return [{ path: res.path, contents: res.contents }] + }) + .otherwise(() => { + logger.warn( + { + result: JSON.stringify(result).substring(0, 100), + agentStepId, + }, + 'Invalid Benchify result structure', + ) + return [] + }), + ) +} + +/** + * Determines if a Benchify error should trigger a retry + */ +function shouldRetryBenchifyError(error: Error): boolean { + const message = error.message.toLowerCase() + + // Retry on network/timeout errors + if ( + message.includes('timeout') || + message.includes('network') || + message.includes('econnreset') + ) { + return true + } - return response + // Retry on 5xx server errors (but not 4xx client errors) + if ( + message.includes('5') && + (message.includes('error') || message.includes('server')) + ) { + return true + } + + // Don't retry on authentication, rate limit, or client errors + return false } /** - * Applies benchify results back to the file system and updates tool results + * Applies benchify results back to the file system with individual error handling */ -async function applyBenchifyResults( +async function applyBenchifyResultsGracefully( benchifyFiles: { path: string; contents: string }[], context: { ws: WebSocket @@ -535,87 +715,158 @@ async function applyBenchifyResults( toolResults: ToolResultPart[] toolCalls: CodebuffToolCall<'str_replace'>[] userInputId: string + agentStepId: string }, ) { - for (const benchifyFile of benchifyFiles) { - try { - // Find the corresponding tool call for this file - const relatedToolCall = context.toolCalls.find( - (tc) => tc.input.path === benchifyFile.path, - ) + const results = await Promise.allSettled( + benchifyFiles.map((benchifyFile) => + applyBenchifyResultSafely(benchifyFile, context), + ), + ) - if (!relatedToolCall) { - logger.warn( - { fileName: benchifyFile.path }, - 'No matching tool call found for benchify result', - ) - continue - } + // Log any failures but don't throw - individual file failures shouldn't block the batch + const failures = results.filter((result) => result.status === 'rejected') + if (failures.length > 0) { + logger.warn( + { + failureCount: failures.length, + totalFiles: benchifyFiles.length, + agentStepId: context.agentStepId, + }, + 'Some Benchify results failed to apply', + ) + } +} - // Get the original file content from our stored contents - const originalContent = - context.state.originalContents?.[benchifyFile.path] +/** + * Safely applies a single Benchify result with comprehensive error handling + */ +async function applyBenchifyResultSafely( + benchifyFile: { path: string; contents: string }, + context: { + ws: WebSocket + onResponseChunk: (chunk: string | PrintModeEvent) => void + state: Record + toolResults: ToolResultPart[] + toolCalls: CodebuffToolCall<'str_replace'>[] + userInputId: string + agentStepId: string + }, +): Promise { + try { + // Find the corresponding tool call for this file + const relatedToolCall = context.toolCalls.find( + (tc) => tc.input.path === benchifyFile.path, + ) - if (!originalContent) { - logger.error( - { path: benchifyFile.path }, - 'Could not find original file content for diff generation', - ) - continue + if (!relatedToolCall) { + logger.debug( + { fileName: benchifyFile.path, agentStepId: context.agentStepId }, + 'No matching tool call found for benchify result', + ) + return + } + + // Get the original content, preferring the latest applied content if available + let baseContent = context.state.originalContents?.[benchifyFile.path] + + // Try to get more recent content from tool results if available + const latestToolResult = context.toolResults + .filter( + (tr) => + tr.toolName === 'str_replace' && + tr.toolCallId === relatedToolCall.toolCallId, + ) + .pop() + + if (latestToolResult?.output?.[0]?.type === 'json') { + const toolValue = latestToolResult.output[0].value + if ( + toolValue && + typeof toolValue === 'object' && + 'content' in toolValue + ) { + baseContent = (toolValue as { content: string }).content } + } - // Generate a proper unified diff patch - const patch = createPatch( - benchifyFile.path, - originalContent, - benchifyFile.contents, - '', - '', + if (!baseContent) { + logger.debug( + { path: benchifyFile.path, agentStepId: context.agentStepId }, + 'Could not find base content for Benchify diff generation', ) + return + } - // Request the client to apply the benchify changes as a patch - const toolCallResult = await requestToolCall( - context.ws, - context.userInputId, - 'str_replace', - { - type: 'patch', - path: benchifyFile.path, - content: patch, - }, + // Skip if content is unchanged + if (baseContent === benchifyFile.contents) { + logger.debug( + { path: benchifyFile.path, agentStepId: context.agentStepId }, + 'Benchify result identical to current content, skipping', ) + return + } - // Create a tool result indicating benchify was applied - const benchifyToolResult: ToolResultPart = { - type: 'tool-result', - toolName: 'str_replace', - toolCallId: relatedToolCall.toolCallId, - output: toolCallResult.output, - } + // Generate a proper unified diff patch + const patch = createPatch( + benchifyFile.path, + baseContent, + benchifyFile.contents, + '', + '', + ) - // Update the existing tool result - const existingResultIndex = context.toolResults.findIndex( - (tr) => tr.toolCallId === relatedToolCall.toolCallId, - ) + // Apply with timeout to prevent hanging + const toolCallResult = await withTimeout( + requestToolCall(context.ws, context.userInputId, 'str_replace', { + type: 'patch', + path: benchifyFile.path, + content: patch, + }), + 5000, + 'Benchify patch application timed out', + ) - if (existingResultIndex >= 0) { - context.toolResults[existingResultIndex] = benchifyToolResult - } else { - context.toolResults.push(benchifyToolResult) - } + // Create a tool result indicating benchify was applied + const benchifyToolResult: ToolResultPart = { + type: 'tool-result', + toolName: 'str_replace', + toolCallId: relatedToolCall.toolCallId, + output: toolCallResult.output, + } - // Notify client about the benchify update - context.onResponseChunk({ - type: 'tool_result', - toolCallId: relatedToolCall.toolCallId, - output: benchifyToolResult.output, - }) - } catch (error) { - logger.error( - { error, fileName: benchifyFile.path }, - 'Failed to apply benchify result to file', - ) + // Update the existing tool result + const existingResultIndex = context.toolResults.findIndex( + (tr) => tr.toolCallId === relatedToolCall.toolCallId, + ) + + if (existingResultIndex >= 0) { + context.toolResults[existingResultIndex] = benchifyToolResult + } else { + context.toolResults.push(benchifyToolResult) } + + // Notify client about the benchify update + context.onResponseChunk({ + type: 'tool_result', + toolCallId: relatedToolCall.toolCallId, + output: benchifyToolResult.output, + }) + + logger.debug( + { path: benchifyFile.path, agentStepId: context.agentStepId }, + 'Successfully applied Benchify result', + ) + } catch (error) { + // Log but don't throw - individual failures shouldn't block the entire batch + logger.warn( + { + error: error instanceof Error ? error.message : String(error), + fileName: benchifyFile.path, + agentStepId: context.agentStepId, + }, + 'Failed to apply individual Benchify result', + ) } } @@ -693,6 +944,76 @@ async function extractIntendedContent( } } -function benchifyCanFixLanguage(path: string): boolean { +/** + * Circuit breaker functions for Benchify resilience + */ +function isBenchifyCircuitOpen(): boolean { + const now = Date.now() + + // Check if circuit should be half-open (reset after timeout) + if (benchifyCircuitBreaker.isOpen && now > benchifyCircuitBreaker.openUntil) { + benchifyCircuitBreaker.isOpen = false + benchifyCircuitBreaker.failureCount = 0 + logger.debug('Benchify circuit breaker reset to closed state') + } + + return benchifyCircuitBreaker.isOpen +} + +function handleBenchifyFailure( + error: unknown, + context: { + intendedChangeFiles: string[] + agentStepId: string + userInputId: string + }, +): void { + benchifyCircuitBreaker.failureCount++ + benchifyCircuitBreaker.lastFailureTime = Date.now() + + // Open circuit if failure threshold exceeded + if (benchifyCircuitBreaker.failureCount >= CIRCUIT_BREAKER_THRESHOLD) { + benchifyCircuitBreaker.isOpen = true + benchifyCircuitBreaker.openUntil = Date.now() + CIRCUIT_BREAKER_TIMEOUT + + logger.warn( + { + failureCount: benchifyCircuitBreaker.failureCount, + circuitOpenUntil: new Date( + benchifyCircuitBreaker.openUntil, + ).toISOString(), + agentStepId: context.agentStepId, + }, + 'Benchify circuit breaker opened due to consecutive failures', + ) + } + + // Log error but continue gracefully + logger.warn( + { + error: error instanceof Error ? error.message : String(error), + failureCount: benchifyCircuitBreaker.failureCount, + intendedChangeFiles: context.intendedChangeFiles, + agentStepId: context.agentStepId, + userInputId: context.userInputId, + }, + 'Benchify call failed, continuing without fixes', + ) +} + +function resetBenchifyCircuitBreaker(): void { + if (benchifyCircuitBreaker.failureCount > 0) { + logger.debug( + { previousFailures: benchifyCircuitBreaker.failureCount }, + 'Benchify circuit breaker reset after successful call', + ) + } + + benchifyCircuitBreaker.failureCount = 0 + benchifyCircuitBreaker.isOpen = false + benchifyCircuitBreaker.openUntil = 0 +} + +export function benchifyCanFixLanguage(path: string): boolean { return BENCHIFY_FILE_TYPES.some((extension) => path.endsWith(`.${extension}`)) } diff --git a/backend/src/tools/stream-parser.ts b/backend/src/tools/stream-parser.ts index 26bd8ae1aa..94a9c6af93 100644 --- a/backend/src/tools/stream-parser.ts +++ b/backend/src/tools/stream-parser.ts @@ -134,18 +134,6 @@ export async function processStreamWithTools(options: { batchState.deferredStrReplaces.push({ toolCall }) - logger.debug( - { - toolCallId, - filePath: input.path, - replacementsCount: input.replacements?.length || 0, - currentDeferredCount: batchState.deferredStrReplaces.length, - agentStepId, - userInputId, - }, - 'stream-parser: Deferring str_replace tool for batch execution', - ) - // Still emit the tool call event onResponseChunk({ type: 'tool_call', @@ -179,12 +167,10 @@ export async function processStreamWithTools(options: { toolCalls, toolResults, ws, - agentTemplate, fileContext, agentStepId, clientSessionId, userInputId, - fullResponse: fullResponseChunks.join(''), onResponseChunk, state, userId, @@ -345,12 +331,10 @@ export async function processStreamWithTools(options: { toolCalls, toolResults, ws, - agentTemplate, fileContext, agentStepId, clientSessionId, userInputId, - fullResponse: fullResponseChunks.join(''), onResponseChunk, state, userId, From be6cf8217dd323b27c0c0f89f4b7c0a76fa67e48 Mon Sep 17 00:00:00 2001 From: brandonkachen Date: Wed, 24 Sep 2025 17:28:58 -0700 Subject: [PATCH 05/10] Improve backend tests by mocking the env module to simulate missing BENCHIFY_API_KEY. This replaces brittle spy usage and ensures resilience when Benchify is disabled. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 🤖 Generated with Codebuff Co-Authored-By: Codebuff --- .../src/__tests__/process-str-replace.test.ts | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/backend/src/__tests__/process-str-replace.test.ts b/backend/src/__tests__/process-str-replace.test.ts index f594896dd3..843a167993 100644 --- a/backend/src/__tests__/process-str-replace.test.ts +++ b/backend/src/__tests__/process-str-replace.test.ts @@ -487,10 +487,19 @@ describe('Benchify resilience', () => { }) it('should fall back gracefully when Benchify is disabled', async () => { - // Test with no API key - spy on the env object directly - spyOn(envModule, 'env', 'get').mockReturnValue({ - // Empty object simulates no BENCHIFY_API_KEY - } as any) + // Test with no API key - mock the entire env module + const mockEnv = { + BENCHIFY_API_KEY: undefined, + // Add other required env properties that might be accessed + PORT: 3001, + OPEN_ROUTER_API_KEY: 'mock-key', + RELACE_API_KEY: 'mock-key', + LINKUP_API_KEY: 'mock-key', + } as any + Object.defineProperty(envModule, 'env', { + get: () => mockEnv, + configurable: true, + }) const result = await executeBatchStrReplaces({ deferredStrReplaces: [ From 0969d7f7cf427112a7031972b7fa54321fe82335 Mon Sep 17 00:00:00 2001 From: brandonkachen Date: Wed, 24 Sep 2025 18:23:30 -0700 Subject: [PATCH 06/10] Improve Benchify resilience by enabling env-mocking in tests and exporting getBenchifyClient for easier mocking; this prevents brittle failures when BENCHIFY_API_KEY is missing and stabilizes batch-str-replace tests. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 🤖 Generated with Codebuff Co-Authored-By: Codebuff --- .../src/__tests__/process-str-replace.test.ts | 90 ++++++++++--------- backend/src/tools/batch-str-replace.ts | 2 +- 2 files changed, 50 insertions(+), 42 deletions(-) diff --git a/backend/src/__tests__/process-str-replace.test.ts b/backend/src/__tests__/process-str-replace.test.ts index 843a167993..e10d9e16b3 100644 --- a/backend/src/__tests__/process-str-replace.test.ts +++ b/backend/src/__tests__/process-str-replace.test.ts @@ -1,7 +1,16 @@ -import * as envModule from '@codebuff/internal/env' -import { describe, expect, it, spyOn } from 'bun:test' +import { describe, expect, it, spyOn, beforeEach, afterEach, mock } from 'bun:test' import { applyPatch } from 'diff' +// Mock the benchify module to simulate missing API key +mock.module('benchify', () => ({ + Benchify: class MockBenchify { + constructor() {} + runFixer() { + return Promise.resolve([]) + } + } +})) + import { processStrReplace } from '../process-str-replace' import { mockFileContext } from './test-utils' import { @@ -487,47 +496,46 @@ describe('Benchify resilience', () => { }) it('should fall back gracefully when Benchify is disabled', async () => { - // Test with no API key - mock the entire env module - const mockEnv = { - BENCHIFY_API_KEY: undefined, - // Add other required env properties that might be accessed - PORT: 3001, - OPEN_ROUTER_API_KEY: 'mock-key', - RELACE_API_KEY: 'mock-key', - LINKUP_API_KEY: 'mock-key', - } as any - Object.defineProperty(envModule, 'env', { - get: () => mockEnv, - configurable: true, - }) - - const result = await executeBatchStrReplaces({ - deferredStrReplaces: [ - { - toolCall: { - toolName: 'str_replace' as const, - toolCallId: 'test-call', - input: { - path: 'test.ts', - replacements: [{ old: 'old', new: 'new', allowMultiple: false }], + // Mock the process.env to simulate missing BENCHIFY_API_KEY + const originalEnv = process.env.BENCHIFY_API_KEY + delete process.env.BENCHIFY_API_KEY + + try { + const result = await executeBatchStrReplaces({ + deferredStrReplaces: [ + { + toolCall: { + toolName: 'str_replace' as const, + toolCallId: 'test-call', + input: { + path: 'test.ts', + replacements: [ + { old: 'old', new: 'new', allowMultiple: false }, + ], + }, }, }, - }, - ], - toolCalls: [], - toolResults: [], - ws: {} as any, - fileContext: mockFileContext, - agentStepId: 'test-step', - clientSessionId: 'test-session', - userInputId: 'test-input', - onResponseChunk: () => {}, - state: { messages: [] }, - userId: 'test-user', - }) - - // Should complete without error even when Benchify is unavailable - expect(result).toBeUndefined() // Function returns void + ], + toolCalls: [], + toolResults: [], + ws: {} as any, + fileContext: mockFileContext, + agentStepId: 'test-step', + clientSessionId: 'test-session', + userInputId: 'test-input', + onResponseChunk: () => {}, + state: { messages: [] }, + userId: 'test-user', + }) + + // Should complete without error even when Benchify is unavailable + expect(result).toBeUndefined() // Function returns void + } finally { + // Restore the original environment variable + if (originalEnv !== undefined) { + process.env.BENCHIFY_API_KEY = originalEnv + } + } }) describe('Batch str_replace integration tests', () => { diff --git a/backend/src/tools/batch-str-replace.ts b/backend/src/tools/batch-str-replace.ts index e152abb423..2c438c362e 100644 --- a/backend/src/tools/batch-str-replace.ts +++ b/backend/src/tools/batch-str-replace.ts @@ -48,7 +48,7 @@ let benchifyCircuitBreaker = { const CIRCUIT_BREAKER_THRESHOLD = 3 // Open circuit after 3 consecutive failures const CIRCUIT_BREAKER_TIMEOUT = 60000 // Keep circuit open for 1 minute -function getBenchifyClient(): Benchify | null { +export function getBenchifyClient(): Benchify | null { if (!benchifyClient) { let benchifyApiKey: string | undefined try { From b717bec78f392b34388aa15de6daa44ddb99aeaa Mon Sep 17 00:00:00 2001 From: Cole Vick Date: Mon, 29 Sep 2025 16:42:29 -0500 Subject: [PATCH 07/10] Switch to using DIFF response format and apply patches directly (#320) --- backend/src/tools/batch-str-replace.ts | 104 +++++++++++-------------- bun.lock | 69 ++-------------- package.json | 1 + 3 files changed, 52 insertions(+), 122 deletions(-) diff --git a/backend/src/tools/batch-str-replace.ts b/backend/src/tools/batch-str-replace.ts index 2c438c362e..d694b6fe82 100644 --- a/backend/src/tools/batch-str-replace.ts +++ b/backend/src/tools/batch-str-replace.ts @@ -4,7 +4,7 @@ import { logger } from '../util/logger' import { Benchify } from 'benchify' import { env } from '@codebuff/internal/env' import { requestToolCall } from '../websockets/websocket-action' -import { createPatch } from 'diff' +import { ParsedDiff, parsePatch } from 'diff' import { withRetry, withTimeout } from '@codebuff/common/util/promise' import { match, P } from 'ts-pattern' import type { @@ -454,8 +454,7 @@ async function applyBenchifyIfNeeded( }, ) { // Early exit conditions - fail gracefully without blocking user edits - const client = getBenchifyClient() - if (!client || batchContext.intendedChanges.size === 0) { + if (batchContext.intendedChanges.size === 0) { return } @@ -499,15 +498,15 @@ async function applyBenchifyIfNeeded( logger.info( { benchifyResultCount: benchifyResult.length, - resultFiles: benchifyResult.map((r) => r.path), + diffResults: benchifyResult.length, agentStepId: options.agentStepId, userInputId: options.userInputId, }, - `executeBatchStrReplaces: Benchify returned ${benchifyResult.length} results, applying them`, + `executeBatchStrReplaces: Benchify returned ${benchifyResult.length} diff results, applying them`, ) // Apply results with individual error handling to prevent one failure from blocking others - await applyBenchifyResultsGracefully(benchifyResult, { + await applyBenchifyResultsGracefully(filteredChanges, benchifyResult, { ws: batchContext.ws, onResponseChunk: batchContext.onResponseChunk, state: { @@ -585,32 +584,34 @@ async function callBenchifyWithResilience( userInputId: string userId: string | undefined }, -): Promise<{ path: string; contents: string }[] | null> { +): Promise { const client = getBenchifyClient() if (!client) { - return null + return [] } return await withRetry( async () => { - const response = await withTimeout( + const diff_response = await withTimeout( client.runFixer(editedFiles, { - fix_types: ['string_literals'], + fixes: ['parsing'], + mode: 'files', + response_format: 'DIFF', }), BENCHIFY_TIMEOUT_MS, `Benchify call timed out after ${BENCHIFY_TIMEOUT_MS}ms`, ) // Validate response - if (response && Array.isArray(response)) { + if (diff_response) { return validateBenchifyResponse( - response, + diff_response, editedFiles, context.agentStepId, ) } - return null + return [] }, { maxRetries: 2, @@ -634,42 +635,34 @@ async function callBenchifyWithResilience( * Validates Benchify API response using pattern matching */ function validateBenchifyResponse( - response: any[], + response: string, originalFiles: { path: string; contents: string }[], agentStepId: string, -): { path: string; contents: string }[] { +): ParsedDiff[] { const originalPaths = new Set(originalFiles.map((f) => f.path)) - return response.flatMap((result) => - match(result) - .with({ path: P.string, contents: P.string }, (res) => { - if (!originalPaths.has(res.path)) { + const patches = parsePatch(response) + return patches.flatMap((patch) => + match(patch) + .with({ oldFileName: P.string }, (res) => { + // drop prefix a/ adding by diff patch + const actualFileName = res.oldFileName.replace('a/', '') + if (!originalPaths.has(actualFileName)) { logger.warn( - { path: res.path, agentStepId }, + { path: actualFileName, agentStepId }, 'Benchify returned result for unexpected path', ) return [] } - if (res.contents.length > BENCHIFY_MAX_FILE_SIZE * 2) { - logger.warn( - { - path: res.path, - size: res.contents.length, - agentStepId, - }, - 'Benchify result exceeds size limit', - ) - return [] - } - return [{ path: res.path, contents: res.contents }] + return [patch] }) .otherwise(() => { logger.warn( { - result: JSON.stringify(result).substring(0, 100), + result: JSON.stringify(patch).substring(0, 100), agentStepId, }, - 'Invalid Benchify result structure', + 'Invalid Benchify patch', ) return [] }), @@ -707,7 +700,8 @@ function shouldRetryBenchifyError(error: Error): boolean { * Applies benchify results back to the file system with individual error handling */ async function applyBenchifyResultsGracefully( - benchifyFiles: { path: string; contents: string }[], + editedFiles: { path: string; contents: string }[], + benchifyDiffs: ParsedDiff[], context: { ws: WebSocket onResponseChunk: (chunk: string | PrintModeEvent) => void @@ -719,9 +713,20 @@ async function applyBenchifyResultsGracefully( }, ) { const results = await Promise.allSettled( - benchifyFiles.map((benchifyFile) => - applyBenchifyResultSafely(benchifyFile, context), - ), + editedFiles.map((editedFile) => { + // again, we have to replace the a/ that the ParsedDiff introduced + const diff = benchifyDiffs.find( + (v) => v.oldFileName?.replace('a/', '') == editedFile.path, + ) + if (diff) { + applyBenchifyResultSafely(editedFile, diff, context) + } else { + logger.warn( + { file: editedFile.path }, + 'No Benchify diff found for file.', + ) + } + }), ) // Log any failures but don't throw - individual file failures shouldn't block the batch @@ -730,7 +735,7 @@ async function applyBenchifyResultsGracefully( logger.warn( { failureCount: failures.length, - totalFiles: benchifyFiles.length, + totalFiles: editedFiles.length, agentStepId: context.agentStepId, }, 'Some Benchify results failed to apply', @@ -743,6 +748,7 @@ async function applyBenchifyResultsGracefully( */ async function applyBenchifyResultSafely( benchifyFile: { path: string; contents: string }, + benchifyDiff: ParsedDiff, context: { ws: WebSocket onResponseChunk: (chunk: string | PrintModeEvent) => void @@ -798,30 +804,12 @@ async function applyBenchifyResultSafely( return } - // Skip if content is unchanged - if (baseContent === benchifyFile.contents) { - logger.debug( - { path: benchifyFile.path, agentStepId: context.agentStepId }, - 'Benchify result identical to current content, skipping', - ) - return - } - - // Generate a proper unified diff patch - const patch = createPatch( - benchifyFile.path, - baseContent, - benchifyFile.contents, - '', - '', - ) - // Apply with timeout to prevent hanging const toolCallResult = await withTimeout( requestToolCall(context.ws, context.userInputId, 'str_replace', { type: 'patch', path: benchifyFile.path, - content: patch, + content: benchifyDiff, }), 5000, 'Benchify patch application timed out', diff --git a/bun.lock b/bun.lock index 9c2bd540fe..68f28a0f4d 100644 --- a/bun.lock +++ b/bun.lock @@ -5,6 +5,7 @@ "name": "codebuff-project", "dependencies": { "@t3-oss/env-nextjs": "^0.7.3", + "benchify": "^0.1.0-alpha.44", "zod": "3.25.67", }, "devDependencies": { @@ -79,7 +80,6 @@ "version": "1.0.0", "dependencies": { "@auth/drizzle-adapter": "^1.5.0", - "@modelcontextprotocol/sdk": "^1.18.2", "@types/pg": "^8.11.10", "@types/readable-stream": "^4.0.18", "@types/seedrandom": "^3.0.8", @@ -239,12 +239,11 @@ }, "sdk": { "name": "@codebuff/sdk", - "version": "0.3.3", + "version": "0.3.1", "dependencies": { "@vscode/tree-sitter-wasm": "0.1.4", "ai": "^5.0.0", "diff": "8.0.2", - "ignore": "7.0.5", "web-tree-sitter": "0.25.6", "ws": "^8.18.0", "zod": "^4.0.0", @@ -865,8 +864,6 @@ "@mermaid-js/parser": ["@mermaid-js/parser@0.6.2", "", { "dependencies": { "langium": "3.3.1" } }, "sha512-+PO02uGF6L6Cs0Bw8RpGhikVvMWEysfAyl27qTlroUB8jSWr1lL0Sf6zi78ZxlSnmgSY2AMMKVgghnN9jTtwkQ=="], - "@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.18.2", "", { "dependencies": { "ajv": "^6.12.6", "content-type": "^1.0.5", "cors": "^2.8.5", "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", "eventsource-parser": "^3.0.0", "express": "^5.0.1", "express-rate-limit": "^7.5.0", "pkce-challenge": "^5.0.0", "raw-body": "^3.0.0", "zod": "^3.23.8", "zod-to-json-schema": "^3.24.1" } }, "sha512-beedclIvFcCnPrYgHsylqiYJVJ/CI47Vyc4tY8no1/Li/O8U4BTlJfy6ZwxkYwx+Mx10nrgwSVrA7VBbhh4slg=="], - "@monogrid/gainmap-js": ["@monogrid/gainmap-js@3.1.0", "", { "dependencies": { "promise-worker-transferable": "^1.0.4" }, "peerDependencies": { "three": ">= 0.159.0" } }, "sha512-Obb0/gEd/HReTlg8ttaYk+0m62gQJmCblMOjHSMHRrBP2zdfKMHLCRbh/6ex9fSUJMKdjjIEiohwkbGD3wj2Nw=="], "@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@0.2.4", "", { "dependencies": { "@emnapi/core": "^1.1.0", "@emnapi/runtime": "^1.1.0", "@tybys/wasm-util": "^0.9.0" } }, "sha512-9zESzOO5aDByvhIAsOy9TbpZ0Ur2AJbUI7UT73kcUTS2mxAMHOBaa1st/jAymNoCtvrit99kkzT1FZuXVcgfIQ=="], @@ -1633,7 +1630,7 @@ "basic-ftp": ["basic-ftp@5.0.5", "", {}, "sha512-4Bcg1P8xhUuqcii/S0Z9wiHIrQVPMermM1any+MX5GeGD7faD3/msQUDGLol9wOcz4/jbg/WJnGqoJF6LiBdtg=="], - "benchify": ["benchify@0.1.0-alpha.41", "", { "dependencies": { "minimatch": "^9.0.3" }, "peerDependencies": { "react": ">=16.8.0" }, "optionalPeers": ["react"] }, "sha512-iZAH2JFcGld/lruJEZKO9dv7XAU8ozEznPtxNLQj+6s1CQMIohzRLnEbvCWLWkMoqSQlJOIp2gCY6N9gt956yQ=="], + "benchify": ["benchify@0.1.0-alpha.44", "", { "dependencies": { "minimatch": "^9.0.3" }, "peerDependencies": { "react": ">=16.8.0" }, "optionalPeers": ["react"] }, "sha512-sGjAPgGKRCNB5h2fTIMHfKGLDBlGT+wUxVNOPJ5Ss5m0PDdtXdlE60CJAcnb2Z620gk5z9P8xppSjZuxKB731w=="], "bidi-js": ["bidi-js@1.0.3", "", { "dependencies": { "require-from-string": "^2.0.2" } }, "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw=="], @@ -2165,8 +2162,6 @@ "events": ["events@3.3.0", "", {}, "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q=="], - "eventsource": ["eventsource@3.0.7", "", { "dependencies": { "eventsource-parser": "^3.0.1" } }, "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA=="], - "eventsource-parser": ["eventsource-parser@3.0.6", "", {}, "sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg=="], "execa": ["execa@7.2.0", "", { "dependencies": { "cross-spawn": "^7.0.3", "get-stream": "^6.0.1", "human-signals": "^4.3.0", "is-stream": "^3.0.0", "merge-stream": "^2.0.0", "npm-run-path": "^5.1.0", "onetime": "^6.0.0", "signal-exit": "^3.0.7", "strip-final-newline": "^3.0.0" } }, "sha512-UduyVP7TLB5IcAQl+OzLyLcS/l32W/GLg+AhHJ+ow40FOk2U3SAllPwR44v4vmdFwIWqpdwxxpQbF1n5ta9seA=="], @@ -2183,8 +2178,6 @@ "express": ["express@4.19.2", "", { "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", "body-parser": "1.20.2", "content-disposition": "0.5.4", "content-type": "~1.0.4", "cookie": "0.6.0", "cookie-signature": "1.0.6", "debug": "2.6.9", "depd": "2.0.0", "encodeurl": "~1.0.2", "escape-html": "~1.0.3", "etag": "~1.8.1", "finalhandler": "1.2.0", "fresh": "0.5.2", "http-errors": "2.0.0", "merge-descriptors": "1.0.1", "methods": "~1.1.2", "on-finished": "2.4.1", "parseurl": "~1.3.3", "path-to-regexp": "0.1.7", "proxy-addr": "~2.0.7", "qs": "6.11.0", "range-parser": "~1.2.1", "safe-buffer": "5.2.1", "send": "0.18.0", "serve-static": "1.15.0", "setprototypeof": "1.2.0", "statuses": "2.0.1", "type-is": "~1.6.18", "utils-merge": "1.0.1", "vary": "~1.1.2" } }, "sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q=="], - "express-rate-limit": ["express-rate-limit@7.5.1", "", { "peerDependencies": { "express": ">= 4.11" } }, "sha512-7iN8iPMDzOMHPUYllBEsQdWVB6fPDMPqwjBaFrgr4Jgr/+okjvzAy+UHlYYL/Vs0OsOrMkwS6PJDkFlJwoxUnw=="], - "exsolve": ["exsolve@1.0.7", "", {}, "sha512-VO5fQUzZtI6C+vx4w/4BWJpg3s/5l+6pRQEHzFRM8WFi4XffSP1Z+4qi7GbjWbvRQEbdIco5mIMq+zX4rPuLrw=="], "extend": ["extend@3.0.2", "", {}, "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g=="], @@ -2543,7 +2536,7 @@ "is-potential-custom-element-name": ["is-potential-custom-element-name@1.0.1", "", {}, "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ=="], - "is-promise": ["is-promise@4.0.0", "", {}, "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ=="], + "is-promise": ["is-promise@2.2.2", "", {}, "sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ=="], "is-reference": ["is-reference@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.6" } }, "sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw=="], @@ -3205,8 +3198,6 @@ "pixelmatch": ["pixelmatch@5.3.0", "", { "dependencies": { "pngjs": "^6.0.0" }, "bin": { "pixelmatch": "bin/pixelmatch" } }, "sha512-o8mkY4E/+LNUf6LzX96ht6k6CEDi65k9G2rjMtBe9Oo+VPKSvl+0GKHuH/AlG+GA5LPG/i5hrekkxUc3s2HU+Q=="], - "pkce-challenge": ["pkce-challenge@5.0.0", "", {}, "sha512-ueGLflrrnvwB3xuo/uGob5pd5FN7l0MsLf0Z87o/UQmRtwjvfylfc9MurIxRAWywCYTgrvpXBcqjV4OfCYGCIQ=="], - "pkg-dir": ["pkg-dir@4.2.0", "", { "dependencies": { "find-up": "^4.0.0" } }, "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ=="], "pkg-types": ["pkg-types@2.3.0", "", { "dependencies": { "confbox": "^0.2.2", "exsolve": "^1.0.7", "pathe": "^2.0.3" } }, "sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig=="], @@ -3329,7 +3320,7 @@ "range-parser": ["range-parser@1.2.1", "", {}, "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg=="], - "raw-body": ["raw-body@3.0.1", "", { "dependencies": { "bytes": "3.1.2", "http-errors": "2.0.0", "iconv-lite": "0.7.0", "unpipe": "1.0.0" } }, "sha512-9G8cA+tuMS75+6G/TzW8OtLzmBDMo8p1JRxN5AZ+LAp8uxGA8V8GZm4GQ4/N5QNQEnLmg6SS7wyuSmbKepiKqA=="], + "raw-body": ["raw-body@2.5.2", "", { "dependencies": { "bytes": "3.1.2", "http-errors": "2.0.0", "iconv-lite": "0.4.24", "unpipe": "1.0.0" } }, "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA=="], "rc": ["rc@1.2.8", "", { "dependencies": { "deep-extend": "^0.6.0", "ini": "~1.3.0", "minimist": "^1.2.0", "strip-json-comments": "~2.0.1" }, "bin": { "rc": "./cli.js" } }, "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw=="], @@ -3445,8 +3436,6 @@ "roughjs": ["roughjs@4.6.6", "", { "dependencies": { "hachure-fill": "^0.5.2", "path-data-parser": "^0.1.0", "points-on-curve": "^0.2.0", "points-on-path": "^0.2.1" } }, "sha512-ZUz/69+SYpFN/g/lUlo2FXcIjRkSu3nDarreVdGGndHEBJ6cXPdKguS8JGxwj5HA5xIbVKSmLgr5b3AWxtRfvQ=="], - "router": ["router@2.2.0", "", { "dependencies": { "debug": "^4.4.0", "depd": "^2.0.0", "is-promise": "^4.0.0", "parseurl": "^1.3.3", "path-to-regexp": "^8.0.0" } }, "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ=="], - "run-applescript": ["run-applescript@7.1.0", "", {}, "sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q=="], "run-parallel": ["run-parallel@1.2.0", "", { "dependencies": { "queue-microtask": "^1.2.2" } }, "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA=="], @@ -4015,8 +4004,6 @@ "@codebuff/sdk/diff": ["diff@8.0.2", "", {}, "sha512-sSuxWU5j5SR9QQji/o2qMvqRNYRDOcBTgsJ/DeCf4iSN4gW+gNMXM7wFIP+fdXZxoNiAnHUTGjCr+TSWXdRDKg=="], - "@codebuff/sdk/ignore": ["ignore@7.0.5", "", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="], - "@codebuff/sdk/ws": ["ws@8.18.3", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg=="], "@codebuff/web/@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.42.0", "", { "dependencies": { "@eslint-community/regexpp": "^4.10.0", "@typescript-eslint/scope-manager": "8.42.0", "@typescript-eslint/type-utils": "8.42.0", "@typescript-eslint/utils": "8.42.0", "@typescript-eslint/visitor-keys": "8.42.0", "graphemer": "^1.4.0", "ignore": "^7.0.0", "natural-compare": "^1.4.0", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.42.0", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-Aq2dPqsQkxHOLfb2OPv43RnIvfj05nw8v/6n3B2NABIPpHnjQnaLo9QGMTvml+tv4korl/Cjfrb/BYhoL8UUTQ=="], @@ -4107,8 +4094,6 @@ "@mdx-js/esbuild/vfile": ["vfile@5.3.7", "", { "dependencies": { "@types/unist": "^2.0.0", "is-buffer": "^2.0.0", "unist-util-stringify-position": "^3.0.0", "vfile-message": "^3.0.0" } }, "sha512-r7qlzkgErKjobAmyNIkkSpizsFPYiUPuJb5pNW1RB4JcYVZhs4lIbVqk8XPk033CV/1z8ss5pkax8SuhGpcG8g=="], - "@modelcontextprotocol/sdk/express": ["express@5.1.0", "", { "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.0", "content-disposition": "^1.0.0", "content-type": "^1.0.5", "cookie": "^0.7.1", "cookie-signature": "^1.2.1", "debug": "^4.4.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "finalhandler": "^2.1.0", "fresh": "^2.0.0", "http-errors": "^2.0.0", "merge-descriptors": "^2.0.0", "mime-types": "^3.0.0", "on-finished": "^2.4.1", "once": "^1.4.0", "parseurl": "^1.3.3", "proxy-addr": "^2.0.7", "qs": "^6.14.0", "range-parser": "^1.2.1", "router": "^2.2.0", "send": "^1.1.0", "serve-static": "^2.2.0", "statuses": "^2.0.1", "type-is": "^2.0.1", "vary": "^1.1.2" } }, "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA=="], - "@nx/devkit/ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="], "@nx/devkit/minimatch": ["minimatch@9.0.3", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg=="], @@ -4241,8 +4226,6 @@ "body-parser/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="], - "body-parser/raw-body": ["raw-body@2.5.2", "", { "dependencies": { "bytes": "3.1.2", "http-errors": "2.0.0", "iconv-lite": "0.4.24", "unpipe": "1.0.0" } }, "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA=="], - "caller-callsite/callsites": ["callsites@2.0.0", "", {}, "sha512-ksWePWBloaWPxJYQ8TL0JHvtci6G5QTKwQ95RcWAa/lzoAKuAOflGdAK92hpHXjkwb8zLxoLNUoNYZgVsaJzvQ=="], "chalk/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], @@ -4565,8 +4548,6 @@ "pretty-format/ansi-styles": ["ansi-styles@5.2.0", "", {}, "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA=="], - "promise-worker-transferable/is-promise": ["is-promise@2.2.2", "", {}, "sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ=="], - "prop-types/react-is": ["react-is@16.13.1", "", {}, "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="], "proxy-agent/http-proxy-agent": ["http-proxy-agent@7.0.2", "", { "dependencies": { "agent-base": "^7.1.0", "debug": "^4.3.4" } }, "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig=="], @@ -4575,8 +4556,6 @@ "puppeteer-core/ws": ["ws@8.18.3", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg=="], - "raw-body/iconv-lite": ["iconv-lite@0.7.0", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ=="], - "rc/ini": ["ini@1.3.8", "", {}, "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew=="], "rc/strip-json-comments": ["strip-json-comments@2.0.1", "", {}, "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ=="], @@ -4621,8 +4600,6 @@ "rimraf/glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="], - "router/path-to-regexp": ["path-to-regexp@8.3.0", "", {}, "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA=="], - "send/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="], "shadcn-ui/chalk": ["chalk@5.6.0", "", {}, "sha512-46QrSQFyVSEyYAgQ22hQ+zDa60YHA4fBstHmtSApj1Y5vKtG27fWowW03jCk5KcbXEWPZUIR894aARCA/G1kfQ=="], @@ -4905,34 +4882,6 @@ "@mdx-js/esbuild/vfile/vfile-message": ["vfile-message@3.1.4", "", { "dependencies": { "@types/unist": "^2.0.0", "unist-util-stringify-position": "^3.0.0" } }, "sha512-fa0Z6P8HUrQN4BZaX05SIVXic+7kE3b05PWAtPuYP9QLHsLKYR7/AlLW3NtOrpXRLeawpDLMsVkmk5DG0NXgWw=="], - "@modelcontextprotocol/sdk/express/accepts": ["accepts@2.0.0", "", { "dependencies": { "mime-types": "^3.0.0", "negotiator": "^1.0.0" } }, "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng=="], - - "@modelcontextprotocol/sdk/express/body-parser": ["body-parser@2.2.0", "", { "dependencies": { "bytes": "^3.1.2", "content-type": "^1.0.5", "debug": "^4.4.0", "http-errors": "^2.0.0", "iconv-lite": "^0.6.3", "on-finished": "^2.4.1", "qs": "^6.14.0", "raw-body": "^3.0.0", "type-is": "^2.0.0" } }, "sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg=="], - - "@modelcontextprotocol/sdk/express/content-disposition": ["content-disposition@1.0.0", "", { "dependencies": { "safe-buffer": "5.2.1" } }, "sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg=="], - - "@modelcontextprotocol/sdk/express/cookie": ["cookie@0.7.2", "", {}, "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w=="], - - "@modelcontextprotocol/sdk/express/cookie-signature": ["cookie-signature@1.2.2", "", {}, "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg=="], - - "@modelcontextprotocol/sdk/express/encodeurl": ["encodeurl@2.0.0", "", {}, "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg=="], - - "@modelcontextprotocol/sdk/express/finalhandler": ["finalhandler@2.1.0", "", { "dependencies": { "debug": "^4.4.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "on-finished": "^2.4.1", "parseurl": "^1.3.3", "statuses": "^2.0.1" } }, "sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q=="], - - "@modelcontextprotocol/sdk/express/fresh": ["fresh@2.0.0", "", {}, "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A=="], - - "@modelcontextprotocol/sdk/express/merge-descriptors": ["merge-descriptors@2.0.0", "", {}, "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g=="], - - "@modelcontextprotocol/sdk/express/mime-types": ["mime-types@3.0.1", "", { "dependencies": { "mime-db": "^1.54.0" } }, "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA=="], - - "@modelcontextprotocol/sdk/express/qs": ["qs@6.14.0", "", { "dependencies": { "side-channel": "^1.1.0" } }, "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w=="], - - "@modelcontextprotocol/sdk/express/send": ["send@1.2.0", "", { "dependencies": { "debug": "^4.3.5", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "fresh": "^2.0.0", "http-errors": "^2.0.0", "mime-types": "^3.0.1", "ms": "^2.1.3", "on-finished": "^2.4.1", "range-parser": "^1.2.1", "statuses": "^2.0.1" } }, "sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw=="], - - "@modelcontextprotocol/sdk/express/serve-static": ["serve-static@2.2.0", "", { "dependencies": { "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "parseurl": "^1.3.3", "send": "^1.2.0" } }, "sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ=="], - - "@modelcontextprotocol/sdk/express/type-is": ["type-is@2.0.1", "", { "dependencies": { "content-type": "^1.0.5", "media-typer": "^1.1.0", "mime-types": "^3.0.0" } }, "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw=="], - "@nx/devkit/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="], "@oclif/core/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="], @@ -5373,14 +5322,6 @@ "@mdx-js/esbuild/@mdx-js/mdx/unist-util-visit/unist-util-visit-parents": ["unist-util-visit-parents@5.1.3", "", { "dependencies": { "@types/unist": "^2.0.0", "unist-util-is": "^5.0.0" } }, "sha512-x6+y8g7wWMyQhL1iZfhIPhDAs7Xwbn9nRosDXl7qoPTSCy0yNxnKc+hWokFifWQIDGi154rdUqKvbCa4+1kLhg=="], - "@modelcontextprotocol/sdk/express/accepts/negotiator": ["negotiator@1.0.0", "", {}, "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg=="], - - "@modelcontextprotocol/sdk/express/body-parser/iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="], - - "@modelcontextprotocol/sdk/express/mime-types/mime-db": ["mime-db@1.54.0", "", {}, "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ=="], - - "@modelcontextprotocol/sdk/express/type-is/media-typer": ["media-typer@1.1.0", "", {}, "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw=="], - "@oclif/core/string-width/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], "@oclif/core/wrap-ansi/ansi-styles/color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], diff --git a/package.json b/package.json index 7249f38774..4280d0c611 100644 --- a/package.json +++ b/package.json @@ -39,6 +39,7 @@ }, "dependencies": { "@t3-oss/env-nextjs": "^0.7.3", + "benchify": "^0.1.0-alpha.44", "zod": "3.25.67" }, "overrides": { From dc9d8d9d0740b4ff36d13163cce5bb7f2e8a9b91 Mon Sep 17 00:00:00 2001 From: brandonkachen Date: Mon, 29 Sep 2025 15:00:22 -0700 Subject: [PATCH 08/10] fix: bun.lock --- bun.lock | 66 +++++++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 63 insertions(+), 3 deletions(-) diff --git a/bun.lock b/bun.lock index 68f28a0f4d..b05b588d6c 100644 --- a/bun.lock +++ b/bun.lock @@ -80,6 +80,7 @@ "version": "1.0.0", "dependencies": { "@auth/drizzle-adapter": "^1.5.0", + "@modelcontextprotocol/sdk": "^1.18.2", "@types/pg": "^8.11.10", "@types/readable-stream": "^4.0.18", "@types/seedrandom": "^3.0.8", @@ -239,11 +240,12 @@ }, "sdk": { "name": "@codebuff/sdk", - "version": "0.3.1", + "version": "0.3.3", "dependencies": { "@vscode/tree-sitter-wasm": "0.1.4", "ai": "^5.0.0", "diff": "8.0.2", + "ignore": "7.0.5", "web-tree-sitter": "0.25.6", "ws": "^8.18.0", "zod": "^4.0.0", @@ -864,6 +866,8 @@ "@mermaid-js/parser": ["@mermaid-js/parser@0.6.2", "", { "dependencies": { "langium": "3.3.1" } }, "sha512-+PO02uGF6L6Cs0Bw8RpGhikVvMWEysfAyl27qTlroUB8jSWr1lL0Sf6zi78ZxlSnmgSY2AMMKVgghnN9jTtwkQ=="], + "@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.18.2", "", { "dependencies": { "ajv": "^6.12.6", "content-type": "^1.0.5", "cors": "^2.8.5", "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", "eventsource-parser": "^3.0.0", "express": "^5.0.1", "express-rate-limit": "^7.5.0", "pkce-challenge": "^5.0.0", "raw-body": "^3.0.0", "zod": "^3.23.8", "zod-to-json-schema": "^3.24.1" } }, "sha512-beedclIvFcCnPrYgHsylqiYJVJ/CI47Vyc4tY8no1/Li/O8U4BTlJfy6ZwxkYwx+Mx10nrgwSVrA7VBbhh4slg=="], + "@monogrid/gainmap-js": ["@monogrid/gainmap-js@3.1.0", "", { "dependencies": { "promise-worker-transferable": "^1.0.4" }, "peerDependencies": { "three": ">= 0.159.0" } }, "sha512-Obb0/gEd/HReTlg8ttaYk+0m62gQJmCblMOjHSMHRrBP2zdfKMHLCRbh/6ex9fSUJMKdjjIEiohwkbGD3wj2Nw=="], "@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@0.2.4", "", { "dependencies": { "@emnapi/core": "^1.1.0", "@emnapi/runtime": "^1.1.0", "@tybys/wasm-util": "^0.9.0" } }, "sha512-9zESzOO5aDByvhIAsOy9TbpZ0Ur2AJbUI7UT73kcUTS2mxAMHOBaa1st/jAymNoCtvrit99kkzT1FZuXVcgfIQ=="], @@ -2162,6 +2166,8 @@ "events": ["events@3.3.0", "", {}, "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q=="], + "eventsource": ["eventsource@3.0.7", "", { "dependencies": { "eventsource-parser": "^3.0.1" } }, "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA=="], + "eventsource-parser": ["eventsource-parser@3.0.6", "", {}, "sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg=="], "execa": ["execa@7.2.0", "", { "dependencies": { "cross-spawn": "^7.0.3", "get-stream": "^6.0.1", "human-signals": "^4.3.0", "is-stream": "^3.0.0", "merge-stream": "^2.0.0", "npm-run-path": "^5.1.0", "onetime": "^6.0.0", "signal-exit": "^3.0.7", "strip-final-newline": "^3.0.0" } }, "sha512-UduyVP7TLB5IcAQl+OzLyLcS/l32W/GLg+AhHJ+ow40FOk2U3SAllPwR44v4vmdFwIWqpdwxxpQbF1n5ta9seA=="], @@ -2178,6 +2184,8 @@ "express": ["express@4.19.2", "", { "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", "body-parser": "1.20.2", "content-disposition": "0.5.4", "content-type": "~1.0.4", "cookie": "0.6.0", "cookie-signature": "1.0.6", "debug": "2.6.9", "depd": "2.0.0", "encodeurl": "~1.0.2", "escape-html": "~1.0.3", "etag": "~1.8.1", "finalhandler": "1.2.0", "fresh": "0.5.2", "http-errors": "2.0.0", "merge-descriptors": "1.0.1", "methods": "~1.1.2", "on-finished": "2.4.1", "parseurl": "~1.3.3", "path-to-regexp": "0.1.7", "proxy-addr": "~2.0.7", "qs": "6.11.0", "range-parser": "~1.2.1", "safe-buffer": "5.2.1", "send": "0.18.0", "serve-static": "1.15.0", "setprototypeof": "1.2.0", "statuses": "2.0.1", "type-is": "~1.6.18", "utils-merge": "1.0.1", "vary": "~1.1.2" } }, "sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q=="], + "express-rate-limit": ["express-rate-limit@7.5.1", "", { "peerDependencies": { "express": ">= 4.11" } }, "sha512-7iN8iPMDzOMHPUYllBEsQdWVB6fPDMPqwjBaFrgr4Jgr/+okjvzAy+UHlYYL/Vs0OsOrMkwS6PJDkFlJwoxUnw=="], + "exsolve": ["exsolve@1.0.7", "", {}, "sha512-VO5fQUzZtI6C+vx4w/4BWJpg3s/5l+6pRQEHzFRM8WFi4XffSP1Z+4qi7GbjWbvRQEbdIco5mIMq+zX4rPuLrw=="], "extend": ["extend@3.0.2", "", {}, "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g=="], @@ -2536,7 +2544,7 @@ "is-potential-custom-element-name": ["is-potential-custom-element-name@1.0.1", "", {}, "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ=="], - "is-promise": ["is-promise@2.2.2", "", {}, "sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ=="], + "is-promise": ["is-promise@4.0.0", "", {}, "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ=="], "is-reference": ["is-reference@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.6" } }, "sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw=="], @@ -3198,6 +3206,8 @@ "pixelmatch": ["pixelmatch@5.3.0", "", { "dependencies": { "pngjs": "^6.0.0" }, "bin": { "pixelmatch": "bin/pixelmatch" } }, "sha512-o8mkY4E/+LNUf6LzX96ht6k6CEDi65k9G2rjMtBe9Oo+VPKSvl+0GKHuH/AlG+GA5LPG/i5hrekkxUc3s2HU+Q=="], + "pkce-challenge": ["pkce-challenge@5.0.0", "", {}, "sha512-ueGLflrrnvwB3xuo/uGob5pd5FN7l0MsLf0Z87o/UQmRtwjvfylfc9MurIxRAWywCYTgrvpXBcqjV4OfCYGCIQ=="], + "pkg-dir": ["pkg-dir@4.2.0", "", { "dependencies": { "find-up": "^4.0.0" } }, "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ=="], "pkg-types": ["pkg-types@2.3.0", "", { "dependencies": { "confbox": "^0.2.2", "exsolve": "^1.0.7", "pathe": "^2.0.3" } }, "sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig=="], @@ -3320,7 +3330,7 @@ "range-parser": ["range-parser@1.2.1", "", {}, "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg=="], - "raw-body": ["raw-body@2.5.2", "", { "dependencies": { "bytes": "3.1.2", "http-errors": "2.0.0", "iconv-lite": "0.4.24", "unpipe": "1.0.0" } }, "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA=="], + "raw-body": ["raw-body@3.0.1", "", { "dependencies": { "bytes": "3.1.2", "http-errors": "2.0.0", "iconv-lite": "0.7.0", "unpipe": "1.0.0" } }, "sha512-9G8cA+tuMS75+6G/TzW8OtLzmBDMo8p1JRxN5AZ+LAp8uxGA8V8GZm4GQ4/N5QNQEnLmg6SS7wyuSmbKepiKqA=="], "rc": ["rc@1.2.8", "", { "dependencies": { "deep-extend": "^0.6.0", "ini": "~1.3.0", "minimist": "^1.2.0", "strip-json-comments": "~2.0.1" }, "bin": { "rc": "./cli.js" } }, "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw=="], @@ -3436,6 +3446,8 @@ "roughjs": ["roughjs@4.6.6", "", { "dependencies": { "hachure-fill": "^0.5.2", "path-data-parser": "^0.1.0", "points-on-curve": "^0.2.0", "points-on-path": "^0.2.1" } }, "sha512-ZUz/69+SYpFN/g/lUlo2FXcIjRkSu3nDarreVdGGndHEBJ6cXPdKguS8JGxwj5HA5xIbVKSmLgr5b3AWxtRfvQ=="], + "router": ["router@2.2.0", "", { "dependencies": { "debug": "^4.4.0", "depd": "^2.0.0", "is-promise": "^4.0.0", "parseurl": "^1.3.3", "path-to-regexp": "^8.0.0" } }, "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ=="], + "run-applescript": ["run-applescript@7.1.0", "", {}, "sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q=="], "run-parallel": ["run-parallel@1.2.0", "", { "dependencies": { "queue-microtask": "^1.2.2" } }, "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA=="], @@ -4004,6 +4016,8 @@ "@codebuff/sdk/diff": ["diff@8.0.2", "", {}, "sha512-sSuxWU5j5SR9QQji/o2qMvqRNYRDOcBTgsJ/DeCf4iSN4gW+gNMXM7wFIP+fdXZxoNiAnHUTGjCr+TSWXdRDKg=="], + "@codebuff/sdk/ignore": ["ignore@7.0.5", "", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="], + "@codebuff/sdk/ws": ["ws@8.18.3", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg=="], "@codebuff/web/@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.42.0", "", { "dependencies": { "@eslint-community/regexpp": "^4.10.0", "@typescript-eslint/scope-manager": "8.42.0", "@typescript-eslint/type-utils": "8.42.0", "@typescript-eslint/utils": "8.42.0", "@typescript-eslint/visitor-keys": "8.42.0", "graphemer": "^1.4.0", "ignore": "^7.0.0", "natural-compare": "^1.4.0", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.42.0", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-Aq2dPqsQkxHOLfb2OPv43RnIvfj05nw8v/6n3B2NABIPpHnjQnaLo9QGMTvml+tv4korl/Cjfrb/BYhoL8UUTQ=="], @@ -4094,6 +4108,8 @@ "@mdx-js/esbuild/vfile": ["vfile@5.3.7", "", { "dependencies": { "@types/unist": "^2.0.0", "is-buffer": "^2.0.0", "unist-util-stringify-position": "^3.0.0", "vfile-message": "^3.0.0" } }, "sha512-r7qlzkgErKjobAmyNIkkSpizsFPYiUPuJb5pNW1RB4JcYVZhs4lIbVqk8XPk033CV/1z8ss5pkax8SuhGpcG8g=="], + "@modelcontextprotocol/sdk/express": ["express@5.1.0", "", { "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.0", "content-disposition": "^1.0.0", "content-type": "^1.0.5", "cookie": "^0.7.1", "cookie-signature": "^1.2.1", "debug": "^4.4.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "finalhandler": "^2.1.0", "fresh": "^2.0.0", "http-errors": "^2.0.0", "merge-descriptors": "^2.0.0", "mime-types": "^3.0.0", "on-finished": "^2.4.1", "once": "^1.4.0", "parseurl": "^1.3.3", "proxy-addr": "^2.0.7", "qs": "^6.14.0", "range-parser": "^1.2.1", "router": "^2.2.0", "send": "^1.1.0", "serve-static": "^2.2.0", "statuses": "^2.0.1", "type-is": "^2.0.1", "vary": "^1.1.2" } }, "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA=="], + "@nx/devkit/ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="], "@nx/devkit/minimatch": ["minimatch@9.0.3", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg=="], @@ -4226,6 +4242,8 @@ "body-parser/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="], + "body-parser/raw-body": ["raw-body@2.5.2", "", { "dependencies": { "bytes": "3.1.2", "http-errors": "2.0.0", "iconv-lite": "0.4.24", "unpipe": "1.0.0" } }, "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA=="], + "caller-callsite/callsites": ["callsites@2.0.0", "", {}, "sha512-ksWePWBloaWPxJYQ8TL0JHvtci6G5QTKwQ95RcWAa/lzoAKuAOflGdAK92hpHXjkwb8zLxoLNUoNYZgVsaJzvQ=="], "chalk/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], @@ -4548,6 +4566,8 @@ "pretty-format/ansi-styles": ["ansi-styles@5.2.0", "", {}, "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA=="], + "promise-worker-transferable/is-promise": ["is-promise@2.2.2", "", {}, "sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ=="], + "prop-types/react-is": ["react-is@16.13.1", "", {}, "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="], "proxy-agent/http-proxy-agent": ["http-proxy-agent@7.0.2", "", { "dependencies": { "agent-base": "^7.1.0", "debug": "^4.3.4" } }, "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig=="], @@ -4556,6 +4576,8 @@ "puppeteer-core/ws": ["ws@8.18.3", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg=="], + "raw-body/iconv-lite": ["iconv-lite@0.7.0", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ=="], + "rc/ini": ["ini@1.3.8", "", {}, "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew=="], "rc/strip-json-comments": ["strip-json-comments@2.0.1", "", {}, "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ=="], @@ -4600,6 +4622,8 @@ "rimraf/glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="], + "router/path-to-regexp": ["path-to-regexp@8.3.0", "", {}, "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA=="], + "send/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="], "shadcn-ui/chalk": ["chalk@5.6.0", "", {}, "sha512-46QrSQFyVSEyYAgQ22hQ+zDa60YHA4fBstHmtSApj1Y5vKtG27fWowW03jCk5KcbXEWPZUIR894aARCA/G1kfQ=="], @@ -4882,6 +4906,34 @@ "@mdx-js/esbuild/vfile/vfile-message": ["vfile-message@3.1.4", "", { "dependencies": { "@types/unist": "^2.0.0", "unist-util-stringify-position": "^3.0.0" } }, "sha512-fa0Z6P8HUrQN4BZaX05SIVXic+7kE3b05PWAtPuYP9QLHsLKYR7/AlLW3NtOrpXRLeawpDLMsVkmk5DG0NXgWw=="], + "@modelcontextprotocol/sdk/express/accepts": ["accepts@2.0.0", "", { "dependencies": { "mime-types": "^3.0.0", "negotiator": "^1.0.0" } }, "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng=="], + + "@modelcontextprotocol/sdk/express/body-parser": ["body-parser@2.2.0", "", { "dependencies": { "bytes": "^3.1.2", "content-type": "^1.0.5", "debug": "^4.4.0", "http-errors": "^2.0.0", "iconv-lite": "^0.6.3", "on-finished": "^2.4.1", "qs": "^6.14.0", "raw-body": "^3.0.0", "type-is": "^2.0.0" } }, "sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg=="], + + "@modelcontextprotocol/sdk/express/content-disposition": ["content-disposition@1.0.0", "", { "dependencies": { "safe-buffer": "5.2.1" } }, "sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg=="], + + "@modelcontextprotocol/sdk/express/cookie": ["cookie@0.7.2", "", {}, "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w=="], + + "@modelcontextprotocol/sdk/express/cookie-signature": ["cookie-signature@1.2.2", "", {}, "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg=="], + + "@modelcontextprotocol/sdk/express/encodeurl": ["encodeurl@2.0.0", "", {}, "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg=="], + + "@modelcontextprotocol/sdk/express/finalhandler": ["finalhandler@2.1.0", "", { "dependencies": { "debug": "^4.4.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "on-finished": "^2.4.1", "parseurl": "^1.3.3", "statuses": "^2.0.1" } }, "sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q=="], + + "@modelcontextprotocol/sdk/express/fresh": ["fresh@2.0.0", "", {}, "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A=="], + + "@modelcontextprotocol/sdk/express/merge-descriptors": ["merge-descriptors@2.0.0", "", {}, "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g=="], + + "@modelcontextprotocol/sdk/express/mime-types": ["mime-types@3.0.1", "", { "dependencies": { "mime-db": "^1.54.0" } }, "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA=="], + + "@modelcontextprotocol/sdk/express/qs": ["qs@6.14.0", "", { "dependencies": { "side-channel": "^1.1.0" } }, "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w=="], + + "@modelcontextprotocol/sdk/express/send": ["send@1.2.0", "", { "dependencies": { "debug": "^4.3.5", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "fresh": "^2.0.0", "http-errors": "^2.0.0", "mime-types": "^3.0.1", "ms": "^2.1.3", "on-finished": "^2.4.1", "range-parser": "^1.2.1", "statuses": "^2.0.1" } }, "sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw=="], + + "@modelcontextprotocol/sdk/express/serve-static": ["serve-static@2.2.0", "", { "dependencies": { "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "parseurl": "^1.3.3", "send": "^1.2.0" } }, "sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ=="], + + "@modelcontextprotocol/sdk/express/type-is": ["type-is@2.0.1", "", { "dependencies": { "content-type": "^1.0.5", "media-typer": "^1.1.0", "mime-types": "^3.0.0" } }, "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw=="], + "@nx/devkit/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="], "@oclif/core/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="], @@ -5322,6 +5374,14 @@ "@mdx-js/esbuild/@mdx-js/mdx/unist-util-visit/unist-util-visit-parents": ["unist-util-visit-parents@5.1.3", "", { "dependencies": { "@types/unist": "^2.0.0", "unist-util-is": "^5.0.0" } }, "sha512-x6+y8g7wWMyQhL1iZfhIPhDAs7Xwbn9nRosDXl7qoPTSCy0yNxnKc+hWokFifWQIDGi154rdUqKvbCa4+1kLhg=="], + "@modelcontextprotocol/sdk/express/accepts/negotiator": ["negotiator@1.0.0", "", {}, "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg=="], + + "@modelcontextprotocol/sdk/express/body-parser/iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="], + + "@modelcontextprotocol/sdk/express/mime-types/mime-db": ["mime-db@1.54.0", "", {}, "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ=="], + + "@modelcontextprotocol/sdk/express/type-is/media-typer": ["media-typer@1.1.0", "", {}, "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw=="], + "@oclif/core/string-width/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], "@oclif/core/wrap-ansi/ansi-styles/color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], From 9ad33691a101579f5ccce095db82192fd5096a09 Mon Sep 17 00:00:00 2001 From: Cole Vick Date: Mon, 29 Sep 2025 17:39:25 -0500 Subject: [PATCH 09/10] Brandon/benchify (#325) Co-authored-by: brandonkachen Co-authored-by: Codebuff --- backend/src/tools/batch-str-replace.ts | 64 ++++---------------------- 1 file changed, 8 insertions(+), 56 deletions(-) diff --git a/backend/src/tools/batch-str-replace.ts b/backend/src/tools/batch-str-replace.ts index d694b6fe82..446a928cbc 100644 --- a/backend/src/tools/batch-str-replace.ts +++ b/backend/src/tools/batch-str-replace.ts @@ -584,10 +584,10 @@ async function callBenchifyWithResilience( userInputId: string userId: string | undefined }, -): Promise { +): Promise { const client = getBenchifyClient() if (!client) { - return [] + return null } return await withRetry( @@ -601,17 +601,11 @@ async function callBenchifyWithResilience( BENCHIFY_TIMEOUT_MS, `Benchify call timed out after ${BENCHIFY_TIMEOUT_MS}ms`, ) - - // Validate response if (diff_response) { - return validateBenchifyResponse( - diff_response, - editedFiles, - context.agentStepId, - ) + return diff_response } - return [] + return null }, { maxRetries: 2, @@ -631,44 +625,6 @@ async function callBenchifyWithResilience( ) } -/** - * Validates Benchify API response using pattern matching - */ -function validateBenchifyResponse( - response: string, - originalFiles: { path: string; contents: string }[], - agentStepId: string, -): ParsedDiff[] { - const originalPaths = new Set(originalFiles.map((f) => f.path)) - - const patches = parsePatch(response) - return patches.flatMap((patch) => - match(patch) - .with({ oldFileName: P.string }, (res) => { - // drop prefix a/ adding by diff patch - const actualFileName = res.oldFileName.replace('a/', '') - if (!originalPaths.has(actualFileName)) { - logger.warn( - { path: actualFileName, agentStepId }, - 'Benchify returned result for unexpected path', - ) - return [] - } - return [patch] - }) - .otherwise(() => { - logger.warn( - { - result: JSON.stringify(patch).substring(0, 100), - agentStepId, - }, - 'Invalid Benchify patch', - ) - return [] - }), - ) -} - /** * Determines if a Benchify error should trigger a retry */ @@ -701,7 +657,7 @@ function shouldRetryBenchifyError(error: Error): boolean { */ async function applyBenchifyResultsGracefully( editedFiles: { path: string; contents: string }[], - benchifyDiffs: ParsedDiff[], + benchifyDiff: string, context: { ws: WebSocket onResponseChunk: (chunk: string | PrintModeEvent) => void @@ -714,12 +670,8 @@ async function applyBenchifyResultsGracefully( ) { const results = await Promise.allSettled( editedFiles.map((editedFile) => { - // again, we have to replace the a/ that the ParsedDiff introduced - const diff = benchifyDiffs.find( - (v) => v.oldFileName?.replace('a/', '') == editedFile.path, - ) - if (diff) { - applyBenchifyResultSafely(editedFile, diff, context) + if (benchifyDiff) { + applyBenchifyResultSafely(editedFile, benchifyDiff, context) } else { logger.warn( { file: editedFile.path }, @@ -748,7 +700,7 @@ async function applyBenchifyResultsGracefully( */ async function applyBenchifyResultSafely( benchifyFile: { path: string; contents: string }, - benchifyDiff: ParsedDiff, + benchifyDiff: string, context: { ws: WebSocket onResponseChunk: (chunk: string | PrintModeEvent) => void From 065bc0f95663beb274a302a9166ff5183b9732ed Mon Sep 17 00:00:00 2001 From: brandonkachen Date: Tue, 30 Sep 2025 16:50:12 -0700 Subject: [PATCH 10/10] Update batch-str-replace.ts --- backend/src/tools/batch-str-replace.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/backend/src/tools/batch-str-replace.ts b/backend/src/tools/batch-str-replace.ts index 446a928cbc..b151040ede 100644 --- a/backend/src/tools/batch-str-replace.ts +++ b/backend/src/tools/batch-str-replace.ts @@ -592,6 +592,16 @@ async function callBenchifyWithResilience( return await withRetry( async () => { + logger.info( + { + fileCount: editedFiles.length, + filePaths: editedFiles.map((f) => f.path), + agentStepId: context.agentStepId, + userInputId: context.userInputId, + }, + 'Calling Benchify API', + ) + const diff_response = await withTimeout( client.runFixer(editedFiles, { fixes: ['parsing'],