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')) {
|
if (feature('PROMPT_CACHE_BREAK_DETECTION')) {
|
||||||
cleanupAgentTracking(agentId)
|
cleanupAgentTracking(agentId)
|
||||||
}
|
}
|
||||||
|
if (agentToolUseContext.readFileState !== toolUseContext.readFileState) {
|
||||||
|
toolUseContext.readFileState.mergeFrom(
|
||||||
|
agentToolUseContext.readFileState,
|
||||||
|
)
|
||||||
|
}
|
||||||
// Release cloned file state cache memory
|
// Release cloned file state cache memory
|
||||||
agentToolUseContext.readFileState.clear()
|
agentToolUseContext.readFileState.clear()
|
||||||
// Release the cloned fork context messages
|
// Release the cloned fork context messages
|
||||||
|
|
|
||||||
|
|
@ -146,3 +146,80 @@ describe('coerceToolContentToString', () => {
|
||||||
expect(coerceToolContentToString(nested)).toBe('{"a":{"b":[1,2,3]}}')
|
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 {
|
load(entries: ReturnType<LRUCache<string, FileState>['dump']>): void {
|
||||||
this.cache.load(entries)
|
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