Skip to content

Commit 78d2d71

Browse files
anandgupta42claude
andcommitted
fix: [AI-450] auto-re-read stale files in agent loop so model sees current content
When `edit` or `write` tools fail with "modified since it was last read", the agent loop now auto-re-reads the file and includes its current content in the error response. This gives the model the fresh file state so it can adjust its next edit accordingly, preventing infinite retry loops. This approach preserves the original safety check in `FileTime.assert()` (the tool still throws on stale files) but recovers at the agent level by: 1. Detecting stale file errors via regex match on the error message 2. Re-reading the file and updating `FileTime` read timestamp 3. Appending fresh file content to the error so the model sees current state Also handles "must read file before overwriting" errors the same way. Closes #450 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 6688a43 commit 78d2d71

1 file changed

Lines changed: 22 additions & 1 deletion

File tree

packages/opencode/src/session/processor.ts

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,10 @@ import type { SessionID, MessageID } from "./schema"
1919
// altimate_change start — import Telemetry for per-generation token tracking
2020
import { Telemetry } from "@/altimate/telemetry"
2121
// altimate_change end
22+
// altimate_change start — import FileTime and Filesystem for stale file recovery (#450)
23+
import { FileTime } from "@/file/time"
24+
import { Filesystem } from "@/util/filesystem"
25+
// altimate_change end
2226

2327
export namespace SessionProcessor {
2428
const DOOM_LOOP_THRESHOLD = 3
@@ -211,12 +215,29 @@ export namespace SessionProcessor {
211215
case "tool-error": {
212216
const match = toolcalls[value.toolCallId]
213217
if (match && match.state.status === "running") {
218+
// altimate_change start — auto-read stale files so model sees current content (#450)
219+
let errorStr = (value.error as any).toString()
220+
const staleFileMatch =
221+
errorStr.match(/File (.+) has been modified since it was last read/) ??
222+
errorStr.match(/You must read file (.+) before overwriting it/)
223+
if (staleFileMatch) {
224+
const staleFilePath = staleFileMatch[1].trim()
225+
try {
226+
const freshContent = await Filesystem.readText(staleFilePath)
227+
FileTime.read(input.sessionID, staleFilePath)
228+
errorStr += `\n\nThe file has been auto-re-read. Here is the current content:\n<file path="${staleFilePath}">\n${freshContent}\n</file>`
229+
log.info("stale file auto-re-read", { file: staleFilePath, sessionID: input.sessionID })
230+
} catch (readErr) {
231+
log.warn("failed to auto-re-read stale file", { file: staleFilePath, error: readErr })
232+
}
233+
}
234+
// altimate_change end
214235
await Session.updatePart({
215236
...match,
216237
state: {
217238
status: "error",
218239
input: value.input ?? match.state.input,
219-
error: (value.error as any).toString(),
240+
error: errorStr,
220241
time: {
221242
start: match.state.time.start,
222243
end: Date.now(),

0 commit comments

Comments
 (0)