Repository navigation
feat(core): refuse Edit/WriteFile when the file changed since last read #3840
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||
|---|---|---|---|---|---|---|---|---|
|
|
@@ -358,6 +358,33 @@ class EditToolInvocation implements ToolInvocation<EditToolParams, ToolResult> { | |||||||
| * @returns Result of the edit operation | ||||||||
| */ | ||||||||
| async execute(signal: AbortSignal): Promise<ToolResult> { | ||||||||
| // Refuse to edit a file that has changed since this session last | ||||||||
| // saw it. The cache only blocks when there *is* a prior interaction | ||||||||
| // (Read or Write) and the on-disk fingerprint no longer matches — | ||||||||
| // first-time edits, brand-new files, and files the model has just | ||||||||
| // had Read into context all fall through. | ||||||||
| if (!this.config.getFileReadCacheDisabled()) { | ||||||||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [Suggestion] Edit's stale check unconditionally calls
Suggested change
— DeepSeek/deepseek-v4-pro via Qwen Code /review |
||||||||
| try { | ||||||||
| const preStats = fs.statSync(this.params.file_path); | ||||||||
| const reason = this.config | ||||||||
| .getFileReadCache() | ||||||||
| .staleWriteReason(this.params.file_path, preStats); | ||||||||
| if (reason !== null) { | ||||||||
| return { | ||||||||
| llmContent: reason, | ||||||||
| returnDisplay: reason, | ||||||||
| error: { | ||||||||
| message: reason, | ||||||||
| type: ToolErrorType.FILE_STALE_BEFORE_WRITE, | ||||||||
| }, | ||||||||
| }; | ||||||||
| } | ||||||||
| } catch { | ||||||||
| // stat failure (file missing, race, etc.) — defer to the | ||||||||
| // existing edit path's error handling. | ||||||||
| } | ||||||||
| } | ||||||||
|
|
||||||||
| let editData: CalculatedEdit; | ||||||||
| try { | ||||||||
| editData = await this.calculateEdit(this.params); | ||||||||
|
|
||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -175,6 +175,33 @@ class WriteFileToolInvocation extends BaseToolInvocation< | |
| let detectedEncoding: string | undefined; | ||
| let detectedLineEnding: LineEnding | undefined; | ||
| const dirName = path.dirname(file_path); | ||
|
|
||
| // Refuse to clobber a file that has changed since this session last | ||
| // saw it. The cache only blocks when there *is* a prior interaction | ||
| // (Read or Write) and the on-disk fingerprint no longer matches — | ||
| // brand-new files and files the model never read fall through. | ||
| if (fileExists && !this.config.getFileReadCacheDisabled()) { | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [Suggestion] WriteFile's stale-check — DeepSeek/deepseek-v4-pro via Qwen Code /review |
||
| try { | ||
| const preStats = fs.statSync(file_path); | ||
| const reason = this.config | ||
| .getFileReadCache() | ||
| .staleWriteReason(file_path, preStats); | ||
| if (reason !== null) { | ||
| return { | ||
| llmContent: reason, | ||
| returnDisplay: reason, | ||
| error: { | ||
| message: reason, | ||
| type: ToolErrorType.FILE_STALE_BEFORE_WRITE, | ||
| }, | ||
| }; | ||
| } | ||
| } catch { | ||
| // stat raced with deletion or transient FS error — let the | ||
| // existing read/write path produce the canonical error. | ||
| } | ||
| } | ||
|
|
||
| if (fileExists) { | ||
| try { | ||
| const fileInfo = await this.config | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[Suggestion]
const prior = entry.lastReadAt ?? entry.lastWriteAtalways preferslastReadAteven whenlastWriteAtis more recent (writes typically happen after reads for the same file). The age shown to the model ((3600s ago)) can be misleading — it may say 300s when the model just wrote the file 5s ago.— DeepSeek/deepseek-v4-pro via Qwen Code /review