diff --git a/packages/opencode/src/file/time.ts b/packages/opencode/src/file/time.ts index 5cba5e82067..770427abe96 100644 --- a/packages/opencode/src/file/time.ts +++ b/packages/opencode/src/file/time.ts @@ -3,14 +3,20 @@ import { Log } from "../util/log" export namespace FileTime { const log = Log.create({ service: "file.time" }) + // Per-session read times plus per-file write locks. + // All tools that overwrite existing files should run their + // assert/read/write/update sequence inside withLock(filepath, ...) + // so concurrent writes to the same file are serialized. export const state = Instance.state(() => { const read: { [sessionID: string]: { [path: string]: Date | undefined } } = {} + const locks = new Map>() return { read, + locks, } }) @@ -25,6 +31,26 @@ export namespace FileTime { return state().read[sessionID]?.[file] } + export async function withLock(filepath: string, fn: () => Promise): Promise { + const current = state() + const currentLock = current.locks.get(filepath) ?? Promise.resolve() + let release: () => void = () => {} + const nextLock = new Promise((resolve) => { + release = resolve + }) + const chained = currentLock.then(() => nextLock) + current.locks.set(filepath, chained) + await currentLock + try { + return await fn() + } finally { + release() + if (current.locks.get(filepath) === chained) { + current.locks.delete(filepath) + } + } + } + export async function assert(sessionID: string, filepath: string) { const time = get(sessionID, filepath) if (!time) throw new Error(`You must read the file ${filepath} before overwriting it. Use the Read tool first`) diff --git a/packages/opencode/src/tool/edit.ts b/packages/opencode/src/tool/edit.ts index a5d34c949ff..0659236e982 100644 --- a/packages/opencode/src/tool/edit.ts +++ b/packages/opencode/src/tool/edit.ts @@ -74,7 +74,7 @@ export const EditTool = Tool.define("edit", { let diff = "" let contentOld = "" let contentNew = "" - await (async () => { + await FileTime.withLock(filePath, async () => { if (params.oldString === "") { contentNew = params.newString diff = trimDiff(createTwoFilesPatch(filePath, filePath, contentOld, contentNew)) @@ -95,6 +95,7 @@ export const EditTool = Tool.define("edit", { await Bus.publish(File.Event.Edited, { file: filePath, }) + FileTime.read(ctx.sessionID, filePath) return } @@ -131,9 +132,8 @@ export const EditTool = Tool.define("edit", { diff = trimDiff( createTwoFilesPatch(filePath, filePath, normalizeLineEndings(contentOld), normalizeLineEndings(contentNew)), ) - })() - - FileTime.read(ctx.sessionID, filePath) + FileTime.read(ctx.sessionID, filePath) + }) let output = "" await LSP.touchFile(filePath, true)