Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions packages/opencode/src/file/time.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, Promise<void>>()
return {
read,
locks,
}
})

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

export async function withLock<T>(filepath: string, fn: () => Promise<T>): Promise<T> {
const current = state()
const currentLock = current.locks.get(filepath) ?? Promise.resolve()
let release: () => void = () => {}
const nextLock = new Promise<void>((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`)
Expand Down
8 changes: 4 additions & 4 deletions packages/opencode/src/tool/edit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand All @@ -95,6 +95,7 @@ export const EditTool = Tool.define("edit", {
await Bus.publish(File.Event.Edited, {
file: filePath,
})
FileTime.read(ctx.sessionID, filePath)
return
}

Expand Down Expand Up @@ -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)
Expand Down