Skip to content

Commit 9eb2707

Browse files
fix(edit): add per-file lock to prevent read-before-write race
1 parent 1e65895 commit 9eb2707

File tree

2 files changed

+33
-4
lines changed

2 files changed

+33
-4
lines changed

packages/opencode/src/file/time.ts

Lines changed: 29 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,29 @@ 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+
current.locks.set(
42+
filepath,
43+
currentLock.then(() => nextLock),
44+
)
45+
await currentLock
46+
try {
47+
const result = await fn()
48+
return result
49+
} finally {
50+
release()
51+
if (current.locks.get(filepath) === nextLock) {
52+
current.locks.delete(filepath)
53+
}
54+
}
55+
}
56+
2857
export async function assert(sessionID: string, filepath: string) {
2958
const time = get(sessionID, filepath)
3059
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
@@ -63,7 +63,7 @@ export const EditTool = Tool.define("edit", {
6363
let diff = ""
6464
let contentOld = ""
6565
let contentNew = ""
66-
await (async () => {
66+
await FileTime.withLock(filePath, async () => {
6767
if (params.oldString === "") {
6868
contentNew = params.newString
6969
diff = trimDiff(createTwoFilesPatch(filePath, filePath, contentOld, contentNew))
@@ -84,6 +84,7 @@ export const EditTool = Tool.define("edit", {
8484
await Bus.publish(File.Event.Edited, {
8585
file: filePath,
8686
})
87+
FileTime.read(ctx.sessionID, filePath)
8788
return
8889
}
8990

@@ -120,9 +121,8 @@ export const EditTool = Tool.define("edit", {
120121
diff = trimDiff(
121122
createTwoFilesPatch(filePath, filePath, normalizeLineEndings(contentOld), normalizeLineEndings(contentNew)),
122123
)
123-
})()
124-
125-
FileTime.read(ctx.sessionID, filePath)
124+
FileTime.read(ctx.sessionID, filePath)
125+
})
126126

127127
let output = ""
128128
await LSP.touchFile(filePath, true)

0 commit comments

Comments
 (0)