Skip to content

Commit 747e6f6

Browse files
Add per-file locking to FileTime and update edit/write/patch tools
Co-authored-by: spoons-and-mirrors <212802214+spoons-and-mirrors@users.noreply.github.com>
1 parent 5e940c9 commit 747e6f6

File tree

4 files changed

+108
-67
lines changed

4 files changed

+108
-67
lines changed

packages/opencode/src/file/time.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,10 @@ export namespace FileTime {
99
[path: string]: Date | undefined
1010
}
1111
} = {}
12+
const locks = new Map<string, Promise<void>>()
1213
return {
1314
read,
15+
locks,
1416
}
1517
})
1618

@@ -35,4 +37,30 @@ export namespace FileTime {
3537
)
3638
}
3739
}
40+
41+
export async function withLock<T>(filepath: string, fn: () => Promise<T>): Promise<T> {
42+
const { locks } = state()
43+
const key = filepath
44+
45+
const previous = locks.get(key) || Promise.resolve()
46+
let resolveNext: () => void
47+
48+
const next = new Promise<void>((resolve) => {
49+
resolveNext = resolve
50+
})
51+
locks.set(
52+
key,
53+
previous.then(() => next),
54+
)
55+
56+
try {
57+
await previous
58+
const result = await fn()
59+
resolveNext!()
60+
return result
61+
} catch (error) {
62+
resolveNext!()
63+
throw error
64+
}
65+
}
3866
}

packages/opencode/src/tool/edit.ts

Lines changed: 29 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -83,35 +83,37 @@ export const EditTool = Tool.define("edit", {
8383
return
8484
}
8585

86-
const file = Bun.file(filePath)
87-
const stats = await file.stat().catch(() => {})
88-
if (!stats) throw new Error(`File ${filePath} not found`)
89-
if (stats.isDirectory()) throw new Error(`Path is a directory, not a file: ${filePath}`)
90-
await FileTime.assert(ctx.sessionID, filePath)
91-
contentOld = await file.text()
92-
contentNew = replace(contentOld, params.oldString, params.newString, params.replaceAll)
93-
94-
diff = trimDiff(createTwoFilesPatch(filePath, filePath, contentOld, contentNew))
95-
if (agent.permission.edit === "ask") {
96-
await Permission.ask({
97-
type: "edit",
98-
sessionID: ctx.sessionID,
99-
messageID: ctx.messageID,
100-
callID: ctx.callID,
101-
title: "Edit this file: " + filePath,
102-
metadata: {
103-
filePath,
104-
diff,
105-
},
106-
})
107-
}
86+
await FileTime.withLock(filePath, async () => {
87+
const file = Bun.file(filePath)
88+
const stats = await file.stat().catch(() => {})
89+
if (!stats) throw new Error(`File ${filePath} not found`)
90+
if (stats.isDirectory()) throw new Error(`Path is a directory, not a file: ${filePath}`)
91+
await FileTime.assert(ctx.sessionID, filePath)
92+
contentOld = await file.text()
93+
contentNew = replace(contentOld, params.oldString, params.newString, params.replaceAll)
10894

109-
await file.write(contentNew)
110-
await Bus.publish(File.Event.Edited, {
111-
file: filePath,
95+
diff = trimDiff(createTwoFilesPatch(filePath, filePath, contentOld, contentNew))
96+
if (agent.permission.edit === "ask") {
97+
await Permission.ask({
98+
type: "edit",
99+
sessionID: ctx.sessionID,
100+
messageID: ctx.messageID,
101+
callID: ctx.callID,
102+
title: "Edit this file: " + filePath,
103+
metadata: {
104+
filePath,
105+
diff,
106+
},
107+
})
108+
}
109+
110+
await file.write(contentNew)
111+
await Bus.publish(File.Event.Edited, {
112+
file: filePath,
113+
})
114+
contentNew = await file.text()
115+
diff = trimDiff(createTwoFilesPatch(filePath, filePath, contentOld, contentNew))
112116
})
113-
contentNew = await file.text()
114-
diff = trimDiff(createTwoFilesPatch(filePath, filePath, contentOld, contentNew))
115117
})()
116118

117119
FileTime.read(ctx.sessionID, filePath)

packages/opencode/src/tool/patch.ts

Lines changed: 29 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -167,39 +167,48 @@ export const PatchTool = Tool.define("patch", {
167167
}
168168
await fs.writeFile(change.filePath, change.newContent, "utf-8")
169169
changedFiles.push(change.filePath)
170+
// Update file time tracking
171+
FileTime.read(ctx.sessionID, change.filePath)
170172
break
171173

172174
case "update":
173-
await fs.writeFile(change.filePath, change.newContent, "utf-8")
174-
changedFiles.push(change.filePath)
175+
await FileTime.withLock(change.filePath, async () => {
176+
await fs.writeFile(change.filePath, change.newContent, "utf-8")
177+
changedFiles.push(change.filePath)
178+
// Update file time tracking
179+
FileTime.read(ctx.sessionID, change.filePath)
180+
})
175181
break
176182

177183
case "move":
178184
if (change.movePath) {
179-
// Create parent directories for destination
180-
const moveDir = path.dirname(change.movePath)
181-
if (moveDir !== "." && moveDir !== "/") {
182-
await fs.mkdir(moveDir, { recursive: true })
183-
}
184-
// Write to new location
185-
await fs.writeFile(change.movePath, change.newContent, "utf-8")
186-
// Remove original
187-
await fs.unlink(change.filePath)
188-
changedFiles.push(change.movePath)
185+
await FileTime.withLock(change.filePath, async () => {
186+
// Create parent directories for destination
187+
const moveDir = path.dirname(change.movePath!)
188+
if (moveDir !== "." && moveDir !== "/") {
189+
await fs.mkdir(moveDir, { recursive: true })
190+
}
191+
// Write to new location
192+
await fs.writeFile(change.movePath!, change.newContent, "utf-8")
193+
// Remove original
194+
await fs.unlink(change.filePath)
195+
changedFiles.push(change.movePath!)
196+
// Update file time tracking
197+
FileTime.read(ctx.sessionID, change.filePath)
198+
FileTime.read(ctx.sessionID, change.movePath!)
199+
})
189200
}
190201
break
191202

192203
case "delete":
193-
await fs.unlink(change.filePath)
194-
changedFiles.push(change.filePath)
204+
await FileTime.withLock(change.filePath, async () => {
205+
await fs.unlink(change.filePath)
206+
changedFiles.push(change.filePath)
207+
// Update file time tracking
208+
FileTime.read(ctx.sessionID, change.filePath)
209+
})
195210
break
196211
}
197-
198-
// Update file time tracking
199-
FileTime.read(ctx.sessionID, change.filePath)
200-
if (change.movePath) {
201-
FileTime.read(ctx.sessionID, change.movePath)
202-
}
203212
}
204213

205214
// Publish file change events

packages/opencode/src/tool/write.ts

Lines changed: 22 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -39,29 +39,31 @@ export const WriteTool = Tool.define("write", {
3939
}
4040
}
4141

42-
const file = Bun.file(filepath)
43-
const exists = await file.exists()
44-
if (exists) await FileTime.assert(ctx.sessionID, filepath)
42+
await FileTime.withLock(filepath, async () => {
43+
const file = Bun.file(filepath)
44+
const exists = await file.exists()
45+
if (exists) await FileTime.assert(ctx.sessionID, filepath)
4546

46-
if (agent.permission.edit === "ask")
47-
await Permission.ask({
48-
type: "write",
49-
sessionID: ctx.sessionID,
50-
messageID: ctx.messageID,
51-
callID: ctx.callID,
52-
title: exists ? "Overwrite this file: " + filepath : "Create new file: " + filepath,
53-
metadata: {
54-
filePath: filepath,
55-
content: params.content,
56-
exists,
57-
},
58-
})
47+
if (agent.permission.edit === "ask")
48+
await Permission.ask({
49+
type: "write",
50+
sessionID: ctx.sessionID,
51+
messageID: ctx.messageID,
52+
callID: ctx.callID,
53+
title: exists ? "Overwrite this file: " + filepath : "Create new file: " + filepath,
54+
metadata: {
55+
filePath: filepath,
56+
content: params.content,
57+
exists,
58+
},
59+
})
5960

60-
await Bun.write(filepath, params.content)
61-
await Bus.publish(File.Event.Edited, {
62-
file: filepath,
61+
await Bun.write(filepath, params.content)
62+
await Bus.publish(File.Event.Edited, {
63+
file: filepath,
64+
})
65+
FileTime.read(ctx.sessionID, filepath)
6366
})
64-
FileTime.read(ctx.sessionID, filepath)
6567

6668
let output = ""
6769
await LSP.touchFile(filepath, true)

0 commit comments

Comments
 (0)