Skip to content
This repository was archived by the owner on Apr 1, 2026. It is now read-only.

Commit 5cf126d

Browse files
fix(edit): add per-file lock to prevent read-before-write race (anomalyco#4388)
1 parent 509f7d9 commit 5cf126d

2 files changed

Lines changed: 30 additions & 4 deletions

File tree

packages/opencode/src/file/time.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,20 @@ import { Log } from "../util/log"
33

44
export namespace FileTime {
55
const log = Log.create({ service: "file.time" })
6+
// Per-session read times plus per-file write locks.
7+
// All tools that overwrite existing files should run their
8+
// assert/read/write/update sequence inside withLock(filepath, ...)
9+
// so concurrent writes to the same file are serialized.
610
export const state = Instance.state(() => {
711
const read: {
812
[sessionID: string]: {
913
[path: string]: Date | undefined
1014
}
1115
} = {}
16+
const locks = new Map<string, Promise<void>>()
1217
return {
1318
read,
19+
locks,
1420
}
1521
})
1622

@@ -25,6 +31,26 @@ export namespace FileTime {
2531
return state().read[sessionID]?.[file]
2632
}
2733

34+
export async function withLock<T>(filepath: string, fn: () => Promise<T>): Promise<T> {
35+
const current = state()
36+
const currentLock = current.locks.get(filepath) ?? Promise.resolve()
37+
let release: () => void = () => {}
38+
const nextLock = new Promise<void>((resolve) => {
39+
release = resolve
40+
})
41+
const chained = currentLock.then(() => nextLock)
42+
current.locks.set(filepath, chained)
43+
await currentLock
44+
try {
45+
return await fn()
46+
} finally {
47+
release()
48+
if (current.locks.get(filepath) === chained) {
49+
current.locks.delete(filepath)
50+
}
51+
}
52+
}
53+
2854
export async function assert(sessionID: string, filepath: string) {
2955
const time = get(sessionID, filepath)
3056
if (!time) throw new Error(`You must read the file ${filepath} before overwriting it. Use the Read tool first`)

packages/opencode/src/tool/edit.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ export const EditTool = Tool.define("edit", {
7676
let diff = ""
7777
let contentOld = ""
7878
let contentNew = ""
79-
await (async () => {
79+
await FileTime.withLock(filePath, async () => {
8080
if (params.oldString === "") {
8181
contentNew = params.newString
8282
diff = trimDiff(createTwoFilesPatch(filePath, filePath, contentOld, contentNew))
@@ -97,6 +97,7 @@ export const EditTool = Tool.define("edit", {
9797
await Bus.publish(File.Event.Edited, {
9898
file: filePath,
9999
})
100+
FileTime.read(ctx.sessionID, filePath)
100101
return
101102
}
102103

@@ -133,9 +134,8 @@ export const EditTool = Tool.define("edit", {
133134
diff = trimDiff(
134135
createTwoFilesPatch(filePath, filePath, normalizeLineEndings(contentOld), normalizeLineEndings(contentNew)),
135136
)
136-
})()
137-
138-
FileTime.read(ctx.sessionID, filePath)
137+
FileTime.read(ctx.sessionID, filePath)
138+
})
139139

140140
let output = ""
141141
await LSP.touchFile(filePath, true)

0 commit comments

Comments
 (0)