fix: merge sub-agent readFileState to prevent cross-agent write conflicts
子代理完成后将其 readFileState 合并回主代理缓存,防止主代理写文件时因 缓存过期而误报 FILE_UNEXPECTEDLY_MODIFIED_ERROR。 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
5c2e1918ca
commit
233971c6ce
|
|
@ -854,6 +854,11 @@ export async function* runAgent({
|
|||
if (feature('PROMPT_CACHE_BREAK_DETECTION')) {
|
||||
cleanupAgentTracking(agentId)
|
||||
}
|
||||
if (agentToolUseContext.readFileState !== toolUseContext.readFileState) {
|
||||
toolUseContext.readFileState.mergeFrom(
|
||||
agentToolUseContext.readFileState,
|
||||
)
|
||||
}
|
||||
// Release cloned file state cache memory
|
||||
agentToolUseContext.readFileState.clear()
|
||||
// Release the cloned fork context messages
|
||||
|
|
|
|||
|
|
@ -146,3 +146,80 @@ describe('coerceToolContentToString', () => {
|
|||
expect(coerceToolContentToString(nested)).toBe('{"a":{"b":[1,2,3]}}')
|
||||
})
|
||||
})
|
||||
|
||||
describe('FileStateCache.mergeFrom', () => {
|
||||
test('merges new entries from other cache into target', () => {
|
||||
const target = createFileStateCacheWithSizeLimit(100)
|
||||
const other = createFileStateCacheWithSizeLimit(100)
|
||||
|
||||
target.set('/a.txt', makeEntry('old-a', { timestamp: 100 }))
|
||||
other.set('/b.txt', makeEntry('new-b', { timestamp: 200 }))
|
||||
|
||||
target.mergeFrom(other)
|
||||
|
||||
expect(target.get('/a.txt')?.content).toBe('old-a')
|
||||
expect(target.get('/b.txt')?.content).toBe('new-b')
|
||||
expect(target.size).toBe(2)
|
||||
})
|
||||
|
||||
test('overrides entries with newer timestamp from other', () => {
|
||||
const target = createFileStateCacheWithSizeLimit(100)
|
||||
const other = createFileStateCacheWithSizeLimit(100)
|
||||
|
||||
target.set('/a.txt', makeEntry('old-content', { timestamp: 100 }))
|
||||
other.set('/a.txt', makeEntry('new-content', { timestamp: 200 }))
|
||||
|
||||
target.mergeFrom(other)
|
||||
|
||||
expect(target.get('/a.txt')?.content).toBe('new-content')
|
||||
})
|
||||
|
||||
test('does not override entries with older timestamp', () => {
|
||||
const target = createFileStateCacheWithSizeLimit(100)
|
||||
const other = createFileStateCacheWithSizeLimit(100)
|
||||
|
||||
target.set('/a.txt', makeEntry('newer-content', { timestamp: 200 }))
|
||||
other.set('/a.txt', makeEntry('older-content', { timestamp: 100 }))
|
||||
|
||||
target.mergeFrom(other)
|
||||
|
||||
expect(target.get('/a.txt')?.content).toBe('newer-content')
|
||||
})
|
||||
|
||||
test('handles empty other cache (no-op)', () => {
|
||||
const target = createFileStateCacheWithSizeLimit(100)
|
||||
const other = createFileStateCacheWithSizeLimit(100)
|
||||
|
||||
target.set('/a.txt', makeEntry('content'))
|
||||
target.mergeFrom(other)
|
||||
|
||||
expect(target.size).toBe(1)
|
||||
expect(target.get('/a.txt')?.content).toBe('content')
|
||||
})
|
||||
|
||||
test('merges into empty target cache', () => {
|
||||
const target = createFileStateCacheWithSizeLimit(100)
|
||||
const other = createFileStateCacheWithSizeLimit(100)
|
||||
|
||||
other.set('/a.txt', makeEntry('content-a'))
|
||||
other.set('/b.txt', makeEntry('content-b'))
|
||||
target.mergeFrom(other)
|
||||
|
||||
expect(target.size).toBe(2)
|
||||
expect(target.get('/a.txt')?.content).toBe('content-a')
|
||||
expect(target.get('/b.txt')?.content).toBe('content-b')
|
||||
})
|
||||
|
||||
test('preserves existing entries when merging new ones', () => {
|
||||
const target = createFileStateCacheWithSizeLimit(100)
|
||||
const other = createFileStateCacheWithSizeLimit(100)
|
||||
|
||||
target.set('/keep.txt', makeEntry('keep-me'))
|
||||
other.set('/new.txt', makeEntry('new-file'))
|
||||
|
||||
target.mergeFrom(other)
|
||||
|
||||
expect(target.has('/keep.txt')).toBe(true)
|
||||
expect(target.has('/new.txt')).toBe(true)
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -101,6 +101,15 @@ export class FileStateCache {
|
|||
load(entries: ReturnType<LRUCache<string, FileState>['dump']>): void {
|
||||
this.cache.load(entries)
|
||||
}
|
||||
|
||||
mergeFrom(other: FileStateCache): void {
|
||||
for (const [filePath, fileState] of other.entries()) {
|
||||
const existing = this.get(filePath)
|
||||
if (!existing || fileState.timestamp > existing.timestamp) {
|
||||
this.set(filePath, fileState)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user