Skip to content

Commit 44f4bd5

Browse files
committed
fix: implement proactive file caching for read_file deduplication
- Changed from reactive to proactive deduplication approach - Added getRecentFileContent method to check cache before reading files - Modified readFileTool to use cached content when available - Added comprehensive tests for the new caching functionality - Fixed legacy format handling in getRecentFileContent - Updated test mocks to include new methods
1 parent 76bca9d commit 44f4bd5

File tree

4 files changed

+450
-1
lines changed

4 files changed

+450
-1
lines changed

src/core/task/Task.ts

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -329,6 +329,79 @@ export class Task extends EventEmitter<ClineEvents> {
329329
return readApiMessages({ taskId: this.taskId, globalStoragePath: this.globalStoragePath })
330330
}
331331

332+
public async getRecentFileContent(filePath: string): Promise<string | null> {
333+
// Check if the experimental feature is enabled
334+
const state = await this.providerRef.deref()?.getState()
335+
if (!state?.experiments || !experiments.isEnabled(state.experiments, EXPERIMENT_IDS.READ_FILE_DEDUPLICATION)) {
336+
return null
337+
}
338+
339+
// Get the cache window from settings
340+
const cacheMinutes = state?.readFileDeduplicationCacheMinutes ?? 5
341+
if (cacheMinutes === 0) {
342+
// Cache is disabled
343+
return null
344+
}
345+
346+
const cacheWindowMs = cacheMinutes * 60 * 1000
347+
const now = Date.now()
348+
349+
// Check recent conversation history for this file
350+
for (let i = this.apiConversationHistory.length - 1; i >= 0; i--) {
351+
const message = this.apiConversationHistory[i]
352+
353+
// Only process user messages
354+
if (message.role !== "user") {
355+
continue
356+
}
357+
358+
// Skip messages outside the cache window
359+
if (message.ts && now - message.ts > cacheWindowMs) {
360+
break
361+
}
362+
363+
// Process content blocks
364+
if (Array.isArray(message.content)) {
365+
for (const block of message.content) {
366+
if (block.type === "text" && typeof block.text === "string") {
367+
// Check for read_file results in text blocks
368+
const readFileMatch = block.text.match(/\[read_file(?:\s+for\s+'([^']+)')?.*?\]\s*Result:/i)
369+
370+
if (readFileMatch) {
371+
// Extract file paths from the result content
372+
const resultContent = block.text.substring(block.text.indexOf("Result:") + 7).trim()
373+
374+
// Handle new XML format
375+
const xmlFileMatches = resultContent.matchAll(
376+
/<file>\s*<path>([^<]+)<\/path>[\s\S]*?<content[^>]*?>([\s\S]*?)<\/content>/g,
377+
)
378+
for (const match of xmlFileMatches) {
379+
const matchedPath = match[1].trim()
380+
const content = match[2].trim()
381+
if (matchedPath === filePath) {
382+
return content
383+
}
384+
}
385+
386+
// Handle legacy format (single file)
387+
if (
388+
readFileMatch[1] &&
389+
readFileMatch[1] === filePath &&
390+
!resultContent.includes("<files>")
391+
) {
392+
// For legacy format, the content is directly after "Result:"
393+
// Remove any leading/trailing whitespace
394+
return resultContent.trim()
395+
}
396+
}
397+
}
398+
}
399+
}
400+
}
401+
402+
return null
403+
}
404+
332405
public async deduplicateReadFileHistory(): Promise<void> {
333406
// Check if the experimental feature is enabled
334407
const state = await this.providerRef.deref()?.getState()

0 commit comments

Comments
 (0)