Skip to content

Commit c2ae062

Browse files
committed
fix(file-time): await read tracking and compare file snapshots
1 parent 86aa331 commit c2ae062

5 files changed

Lines changed: 32 additions & 13 deletions

File tree

packages/opencode/src/file/time.ts

Lines changed: 27 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -15,14 +15,32 @@ export namespace FileTimeService {
1515
}
1616
}
1717

18+
type Stamp = {
19+
readonly read: Date
20+
readonly mtime: number | undefined
21+
readonly ctime: number | undefined
22+
readonly size: number | undefined
23+
}
24+
25+
function stamp(file: string): Stamp {
26+
const stat = Filesystem.stat(file)
27+
const size = typeof stat?.size === "bigint" ? Number(stat.size) : stat?.size
28+
return {
29+
read: new Date(),
30+
mtime: stat?.mtime?.getTime(),
31+
ctime: stat?.ctime?.getTime(),
32+
size,
33+
}
34+
}
35+
1836
export class FileTimeService extends ServiceMap.Service<FileTimeService, FileTimeService.Service>()(
1937
"@opencode/FileTime",
2038
) {
2139
static readonly layer = Layer.effect(
2240
FileTimeService,
2341
Effect.gen(function* () {
2442
const disableCheck = yield* Flag.OPENCODE_DISABLE_FILETIME_CHECK
25-
const reads: { [sessionID: string]: { [path: string]: Date | undefined } } = {}
43+
const reads: { [sessionID: string]: { [path: string]: Stamp | undefined } } = {}
2644
const locks = new Map<string, Semaphore.Semaphore>()
2745

2846
function getLock(filepath: string) {
@@ -38,22 +56,24 @@ export class FileTimeService extends ServiceMap.Service<FileTimeService, FileTim
3856
read: Effect.fn("FileTimeService.read")(function* (sessionID: string, file: string) {
3957
log.info("read", { sessionID, file })
4058
reads[sessionID] = reads[sessionID] || {}
41-
reads[sessionID][file] = new Date()
59+
reads[sessionID][file] = stamp(file)
4260
}),
4361

4462
get: Effect.fn("FileTimeService.get")(function* (sessionID: string, file: string) {
45-
return reads[sessionID]?.[file]
63+
return reads[sessionID]?.[file]?.read
4664
}),
4765

4866
assert: Effect.fn("FileTimeService.assert")(function* (sessionID: string, filepath: string) {
4967
if (disableCheck) return
5068

5169
const time = reads[sessionID]?.[filepath]
5270
if (!time) throw new Error(`You must read file ${filepath} before overwriting it. Use the Read tool first`)
53-
const mtime = Filesystem.stat(filepath)?.mtime
54-
if (mtime && mtime.getTime() > time.getTime() + 50) {
71+
const next = stamp(filepath)
72+
const changed = next.mtime !== time.mtime || next.ctime !== time.ctime || next.size !== time.size
73+
74+
if (changed) {
5575
throw new Error(
56-
`File ${filepath} has been modified since it was last read.\nLast modification: ${mtime.toISOString()}\nLast read: ${time.toISOString()}\n\nPlease read the file again before modifying it.`,
76+
`File ${filepath} has been modified since it was last read.\nLast modification: ${new Date(next.mtime ?? next.read.getTime()).toISOString()}\nLast read: ${time.read.toISOString()}\n\nPlease read the file again before modifying it.`,
5777
)
5878
}
5979
}),
@@ -70,8 +90,7 @@ export class FileTimeService extends ServiceMap.Service<FileTimeService, FileTim
7090
// Legacy facade — callers don't need to change
7191
export namespace FileTime {
7292
export function read(sessionID: string, file: string) {
73-
// Fire-and-forget — callers never await this
74-
runPromiseInstance(FileTimeService.use((s) => s.read(sessionID, file)))
93+
return runPromiseInstance(FileTimeService.use((s) => s.read(sessionID, file)))
7594
}
7695

7796
export function get(sessionID: string, file: string) {

packages/opencode/src/session/prompt.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1245,7 +1245,7 @@ export namespace SessionPrompt {
12451245
]
12461246
}
12471247

1248-
FileTime.read(input.sessionID, filepath)
1248+
await FileTime.read(input.sessionID, filepath)
12491249
return [
12501250
{
12511251
messageID: info.id,

packages/opencode/src/tool/edit.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ export const EditTool = Tool.define("edit", {
7878
file: filePath,
7979
event: existed ? "change" : "add",
8080
})
81-
FileTime.read(ctx.sessionID, filePath)
81+
await FileTime.read(ctx.sessionID, filePath)
8282
return
8383
}
8484

@@ -119,7 +119,7 @@ export const EditTool = Tool.define("edit", {
119119
diff = trimDiff(
120120
createTwoFilesPatch(filePath, filePath, normalizeLineEndings(contentOld), normalizeLineEndings(contentNew)),
121121
)
122-
FileTime.read(ctx.sessionID, filePath)
122+
await FileTime.read(ctx.sessionID, filePath)
123123
})
124124

125125
const filediff: Snapshot.FileDiff = {

packages/opencode/src/tool/read.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -214,7 +214,7 @@ export const ReadTool = Tool.define("read", {
214214

215215
// just warms the lsp client
216216
LSP.touchFile(filepath, false)
217-
FileTime.read(ctx.sessionID, filepath)
217+
await FileTime.read(ctx.sessionID, filepath)
218218

219219
if (instructions.length > 0) {
220220
output += `\n\n<system-reminder>\n${instructions.map((i) => i.content).join("\n\n")}\n</system-reminder>`

packages/opencode/src/tool/write.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ export const WriteTool = Tool.define("write", {
4949
file: filepath,
5050
event: exists ? "change" : "add",
5151
})
52-
FileTime.read(ctx.sessionID, filepath)
52+
await FileTime.read(ctx.sessionID, filepath)
5353

5454
let output = "Wrote file successfully."
5555
await LSP.touchFile(filepath, true)

0 commit comments

Comments
 (0)