source dump of claude code
at main 142 lines 4.2 kB view raw
1import { LRUCache } from 'lru-cache' 2import { normalize } from 'path' 3 4export type FileState = { 5 content: string 6 timestamp: number 7 offset: number | undefined 8 limit: number | undefined 9 // True when this entry was populated by auto-injection (e.g. CLAUDE.md) and 10 // the injected content did not match disk (stripped HTML comments, stripped 11 // frontmatter, truncated MEMORY.md). The model has only seen a partial view; 12 // Edit/Write must require an explicit Read first. `content` here holds the 13 // RAW disk bytes (for getChangedFiles diffing), not what the model saw. 14 isPartialView?: boolean 15} 16 17// Default max entries for read file state caches 18export const READ_FILE_STATE_CACHE_SIZE = 100 19 20// Default size limit for file state caches (25MB) 21// This prevents unbounded memory growth from large file contents 22const DEFAULT_MAX_CACHE_SIZE_BYTES = 25 * 1024 * 1024 23 24/** 25 * A file state cache that normalizes all path keys before access. 26 * This ensures consistent cache hits regardless of whether callers pass 27 * relative vs absolute paths with redundant segments (e.g. /foo/../bar) 28 * or mixed path separators on Windows (/ vs \). 29 */ 30export class FileStateCache { 31 private cache: LRUCache<string, FileState> 32 33 constructor(maxEntries: number, maxSizeBytes: number) { 34 this.cache = new LRUCache<string, FileState>({ 35 max: maxEntries, 36 maxSize: maxSizeBytes, 37 sizeCalculation: value => Math.max(1, Buffer.byteLength(value.content)), 38 }) 39 } 40 41 get(key: string): FileState | undefined { 42 return this.cache.get(normalize(key)) 43 } 44 45 set(key: string, value: FileState): this { 46 this.cache.set(normalize(key), value) 47 return this 48 } 49 50 has(key: string): boolean { 51 return this.cache.has(normalize(key)) 52 } 53 54 delete(key: string): boolean { 55 return this.cache.delete(normalize(key)) 56 } 57 58 clear(): void { 59 this.cache.clear() 60 } 61 62 get size(): number { 63 return this.cache.size 64 } 65 66 get max(): number { 67 return this.cache.max 68 } 69 70 get maxSize(): number { 71 return this.cache.maxSize 72 } 73 74 get calculatedSize(): number { 75 return this.cache.calculatedSize 76 } 77 78 keys(): Generator<string> { 79 return this.cache.keys() 80 } 81 82 entries(): Generator<[string, FileState]> { 83 return this.cache.entries() 84 } 85 86 dump(): ReturnType<LRUCache<string, FileState>['dump']> { 87 return this.cache.dump() 88 } 89 90 load(entries: ReturnType<LRUCache<string, FileState>['dump']>): void { 91 this.cache.load(entries) 92 } 93} 94 95/** 96 * Factory function to create a size-limited FileStateCache. 97 * Uses LRUCache's built-in size-based eviction to prevent memory bloat. 98 * Note: Images are not cached (see FileReadTool) so size limit is mainly 99 * for large text files, notebooks, and other editable content. 100 */ 101export function createFileStateCacheWithSizeLimit( 102 maxEntries: number, 103 maxSizeBytes: number = DEFAULT_MAX_CACHE_SIZE_BYTES, 104): FileStateCache { 105 return new FileStateCache(maxEntries, maxSizeBytes) 106} 107 108// Helper function to convert cache to object (used by compact.ts) 109export function cacheToObject( 110 cache: FileStateCache, 111): Record<string, FileState> { 112 return Object.fromEntries(cache.entries()) 113} 114 115// Helper function to get all keys from cache (used by several components) 116export function cacheKeys(cache: FileStateCache): string[] { 117 return Array.from(cache.keys()) 118} 119 120// Helper function to clone a FileStateCache 121// Preserves size limit configuration from the source cache 122export function cloneFileStateCache(cache: FileStateCache): FileStateCache { 123 const cloned = createFileStateCacheWithSizeLimit(cache.max, cache.maxSize) 124 cloned.load(cache.dump()) 125 return cloned 126} 127 128// Merge two file state caches, with more recent entries (by timestamp) overriding older ones 129export function mergeFileStateCaches( 130 first: FileStateCache, 131 second: FileStateCache, 132): FileStateCache { 133 const merged = cloneFileStateCache(first) 134 for (const [filePath, fileState] of second.entries()) { 135 const existing = merged.get(filePath) 136 // Only override if the new entry is more recent 137 if (!existing || fileState.timestamp > existing.timestamp) { 138 merged.set(filePath, fileState) 139 } 140 } 141 return merged 142}