chore: delete filetime module (#22999)

This commit is contained in:
Aiden Cline
2026-04-16 23:31:21 -05:00
committed by GitHub
parent 4bd5a158a5
commit 76a141090e
34 changed files with 59 additions and 766 deletions
@@ -9,7 +9,6 @@ import { Account } from "@/account/account"
import { Config } from "@/config"
import { Git } from "@/git"
import { Ripgrep } from "@/file/ripgrep"
import { FileTime } from "@/file/time"
import { File } from "@/file"
import { FileWatcher } from "@/file/watcher"
import { Storage } from "@/storage"
@@ -58,7 +57,6 @@ export const AppLayer = Layer.mergeAll(
Config.defaultLayer,
Git.defaultLayer,
Ripgrep.defaultLayer,
FileTime.defaultLayer,
File.defaultLayer,
FileWatcher.defaultLayer,
Storage.defaultLayer,
-113
View File
@@ -1,113 +0,0 @@
import { DateTime, Effect, Layer, Option, Semaphore, Context } from "effect"
import { InstanceState } from "@/effect"
import { AppFileSystem } from "@opencode-ai/shared/filesystem"
import { Flag } from "@/flag/flag"
import type { SessionID } from "@/session/schema"
import { Log } from "../util"
const log = Log.create({ service: "file.time" })
export type Stamp = {
readonly read: Date
readonly mtime: number | undefined
readonly size: number | undefined
}
const session = (reads: Map<SessionID, Map<string, Stamp>>, sessionID: SessionID) => {
const value = reads.get(sessionID)
if (value) return value
const next = new Map<string, Stamp>()
reads.set(sessionID, next)
return next
}
interface State {
reads: Map<SessionID, Map<string, Stamp>>
locks: Map<string, Semaphore.Semaphore>
}
export interface Interface {
readonly read: (sessionID: SessionID, file: string) => Effect.Effect<void>
readonly get: (sessionID: SessionID, file: string) => Effect.Effect<Date | undefined>
readonly assert: (sessionID: SessionID, filepath: string) => Effect.Effect<void>
readonly withLock: <T>(filepath: string, fn: () => Effect.Effect<T>) => Effect.Effect<T>
}
export class Service extends Context.Service<Service, Interface>()("@opencode/FileTime") {}
export const layer = Layer.effect(
Service,
Effect.gen(function* () {
const fsys = yield* AppFileSystem.Service
const disableCheck = yield* Flag.OPENCODE_DISABLE_FILETIME_CHECK
const stamp = Effect.fnUntraced(function* (file: string) {
const info = yield* fsys.stat(file).pipe(Effect.catch(() => Effect.void))
return {
read: yield* DateTime.nowAsDate,
mtime: info ? Option.getOrUndefined(info.mtime)?.getTime() : undefined,
size: info ? Number(info.size) : undefined,
}
})
const state = yield* InstanceState.make<State>(
Effect.fn("FileTime.state")(() =>
Effect.succeed({
reads: new Map<SessionID, Map<string, Stamp>>(),
locks: new Map<string, Semaphore.Semaphore>(),
}),
),
)
const getLock = Effect.fn("FileTime.lock")(function* (filepath: string) {
filepath = AppFileSystem.normalizePath(filepath)
const locks = (yield* InstanceState.get(state)).locks
const lock = locks.get(filepath)
if (lock) return lock
const next = Semaphore.makeUnsafe(1)
locks.set(filepath, next)
return next
})
const read = Effect.fn("FileTime.read")(function* (sessionID: SessionID, file: string) {
file = AppFileSystem.normalizePath(file)
const reads = (yield* InstanceState.get(state)).reads
log.info("read", { sessionID, file })
session(reads, sessionID).set(file, yield* stamp(file))
})
const get = Effect.fn("FileTime.get")(function* (sessionID: SessionID, file: string) {
file = AppFileSystem.normalizePath(file)
const reads = (yield* InstanceState.get(state)).reads
return reads.get(sessionID)?.get(file)?.read
})
const assert = Effect.fn("FileTime.assert")(function* (sessionID: SessionID, filepath: string) {
if (disableCheck) return
filepath = AppFileSystem.normalizePath(filepath)
const reads = (yield* InstanceState.get(state)).reads
const time = reads.get(sessionID)?.get(filepath)
if (!time) throw new Error(`You must read file ${filepath} before overwriting it. Use the Read tool first`)
const next = yield* stamp(filepath)
const changed = next.mtime !== time.mtime || next.size !== time.size
if (!changed) return
throw new Error(
`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.`,
)
})
const withLock = Effect.fn("FileTime.withLock")(function* <T>(filepath: string, fn: () => Effect.Effect<T>) {
return yield* fn().pipe((yield* getLock(filepath)).withPermits(1))
})
return Service.of({ read, get, assert, withLock })
}),
).pipe(Layer.orDie)
export const defaultLayer = layer.pipe(Layer.provide(AppFileSystem.defaultLayer))
export * as FileTime from "./time"
-1
View File
@@ -70,7 +70,6 @@ export const Flag = {
OPENCODE_EXPERIMENTAL_OXFMT: OPENCODE_EXPERIMENTAL || truthy("OPENCODE_EXPERIMENTAL_OXFMT"),
OPENCODE_EXPERIMENTAL_LSP_TY: truthy("OPENCODE_EXPERIMENTAL_LSP_TY"),
OPENCODE_EXPERIMENTAL_LSP_TOOL: OPENCODE_EXPERIMENTAL || truthy("OPENCODE_EXPERIMENTAL_LSP_TOOL"),
OPENCODE_DISABLE_FILETIME_CHECK: Config.boolean("OPENCODE_DISABLE_FILETIME_CHECK").pipe(Config.withDefault(false)),
OPENCODE_EXPERIMENTAL_PLAN_MODE: OPENCODE_EXPERIMENTAL || truthy("OPENCODE_EXPERIMENTAL_PLAN_MODE"),
OPENCODE_EXPERIMENTAL_MARKDOWN: !falsy("OPENCODE_EXPERIMENTAL_MARKDOWN"),
OPENCODE_MODELS_URL: process.env["OPENCODE_MODELS_URL"],
-4
View File
@@ -23,7 +23,6 @@ import MAX_STEPS from "../session/prompt/max-steps.txt"
import { ToolRegistry } from "../tool"
import { MCP } from "../mcp"
import { LSP } from "../lsp"
import { FileTime } from "../file/time"
import { Flag } from "../flag/flag"
import { ulid } from "ulid"
import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process"
@@ -94,7 +93,6 @@ export const layer = Layer.effect(
const fsys = yield* AppFileSystem.Service
const mcp = yield* MCP.Service
const lsp = yield* LSP.Service
const filetime = yield* FileTime.Service
const registry = yield* ToolRegistry.Service
const truncate = yield* Truncate.Service
const spawner = yield* ChildProcessSpawner.ChildProcessSpawner
@@ -1183,7 +1181,6 @@ NOTE: At any point in time through this workflow you should feel free to ask the
]
}
yield* filetime.read(input.sessionID, filepath)
return [
{
messageID: info.id,
@@ -1684,7 +1681,6 @@ export const defaultLayer = Layer.suspend(() =>
Layer.provide(Permission.defaultLayer),
Layer.provide(MCP.defaultLayer),
Layer.provide(LSP.defaultLayer),
Layer.provide(FileTime.defaultLayer),
Layer.provide(ToolRegistry.defaultLayer),
Layer.provide(Truncate.defaultLayer),
Layer.provide(Provider.defaultLayer),
+56 -63
View File
@@ -14,7 +14,6 @@ import { File } from "../file"
import { FileWatcher } from "../file/watcher"
import { Bus } from "../bus"
import { Format } from "../format"
import { FileTime } from "../file/time"
import { Instance } from "../project/instance"
import { Snapshot } from "@/snapshot"
import { assertExternalDirectoryEffect } from "./external-directory"
@@ -44,7 +43,6 @@ export const EditTool = Tool.define(
"edit",
Effect.gen(function* () {
const lsp = yield* LSP.Service
const filetime = yield* FileTime.Service
const afs = yield* AppFileSystem.Service
const format = yield* Format.Service
const bus = yield* Bus.Service
@@ -70,52 +68,11 @@ export const EditTool = Tool.define(
let diff = ""
let contentOld = ""
let contentNew = ""
yield* filetime.withLock(filePath, () =>
Effect.gen(function* () {
if (params.oldString === "") {
const existed = yield* afs.existsSafe(filePath)
contentNew = params.newString
diff = trimDiff(createTwoFilesPatch(filePath, filePath, contentOld, contentNew))
yield* ctx.ask({
permission: "edit",
patterns: [path.relative(Instance.worktree, filePath)],
always: ["*"],
metadata: {
filepath: filePath,
diff,
},
})
yield* afs.writeWithDirs(filePath, params.newString)
yield* format.file(filePath)
yield* bus.publish(File.Event.Edited, { file: filePath })
yield* bus.publish(FileWatcher.Event.Updated, {
file: filePath,
event: existed ? "change" : "add",
})
yield* filetime.read(ctx.sessionID, filePath)
return
}
const info = yield* afs.stat(filePath).pipe(Effect.catch(() => Effect.succeed(undefined)))
if (!info) throw new Error(`File ${filePath} not found`)
if (info.type === "Directory") throw new Error(`Path is a directory, not a file: ${filePath}`)
yield* filetime.assert(ctx.sessionID, filePath)
contentOld = yield* afs.readFileString(filePath)
const ending = detectLineEnding(contentOld)
const old = convertToLineEnding(normalizeLineEndings(params.oldString), ending)
const next = convertToLineEnding(normalizeLineEndings(params.newString), ending)
contentNew = replace(contentOld, old, next, params.replaceAll)
diff = trimDiff(
createTwoFilesPatch(
filePath,
filePath,
normalizeLineEndings(contentOld),
normalizeLineEndings(contentNew),
),
)
yield* Effect.gen(function* () {
if (params.oldString === "") {
const existed = yield* afs.existsSafe(filePath)
contentNew = params.newString
diff = trimDiff(createTwoFilesPatch(filePath, filePath, contentOld, contentNew))
yield* ctx.ask({
permission: "edit",
patterns: [path.relative(Instance.worktree, filePath)],
@@ -125,26 +82,62 @@ export const EditTool = Tool.define(
diff,
},
})
yield* afs.writeWithDirs(filePath, contentNew)
yield* afs.writeWithDirs(filePath, params.newString)
yield* format.file(filePath)
yield* bus.publish(File.Event.Edited, { file: filePath })
yield* bus.publish(FileWatcher.Event.Updated, {
file: filePath,
event: "change",
event: existed ? "change" : "add",
})
contentNew = yield* afs.readFileString(filePath)
diff = trimDiff(
createTwoFilesPatch(
filePath,
filePath,
normalizeLineEndings(contentOld),
normalizeLineEndings(contentNew),
),
)
yield* filetime.read(ctx.sessionID, filePath)
}).pipe(Effect.orDie),
)
return
}
const info = yield* afs.stat(filePath).pipe(Effect.catch(() => Effect.succeed(undefined)))
if (!info) throw new Error(`File ${filePath} not found`)
if (info.type === "Directory") throw new Error(`Path is a directory, not a file: ${filePath}`)
contentOld = yield* afs.readFileString(filePath)
const ending = detectLineEnding(contentOld)
const old = convertToLineEnding(normalizeLineEndings(params.oldString), ending)
const next = convertToLineEnding(normalizeLineEndings(params.newString), ending)
contentNew = replace(contentOld, old, next, params.replaceAll)
diff = trimDiff(
createTwoFilesPatch(
filePath,
filePath,
normalizeLineEndings(contentOld),
normalizeLineEndings(contentNew),
),
)
yield* ctx.ask({
permission: "edit",
patterns: [path.relative(Instance.worktree, filePath)],
always: ["*"],
metadata: {
filepath: filePath,
diff,
},
})
yield* afs.writeWithDirs(filePath, contentNew)
yield* format.file(filePath)
yield* bus.publish(File.Event.Edited, { file: filePath })
yield* bus.publish(FileWatcher.Event.Updated, {
file: filePath,
event: "change",
})
contentNew = yield* afs.readFileString(filePath)
diff = trimDiff(
createTwoFilesPatch(
filePath,
filePath,
normalizeLineEndings(contentOld),
normalizeLineEndings(contentNew),
),
)
}).pipe(Effect.orDie)
const filediff: Snapshot.FileDiff = {
file: filePath,
+2 -5
View File
@@ -7,7 +7,6 @@ import { createInterface } from "readline"
import * as Tool from "./tool"
import { AppFileSystem } from "@opencode-ai/shared/filesystem"
import { LSP } from "../lsp"
import { FileTime } from "../file/time"
import DESCRIPTION from "./read.txt"
import { Instance } from "../project/instance"
import { assertExternalDirectoryEffect } from "./external-directory"
@@ -31,7 +30,6 @@ export const ReadTool = Tool.define(
const fs = yield* AppFileSystem.Service
const instruction = yield* Instruction.Service
const lsp = yield* LSP.Service
const time = yield* FileTime.Service
const scope = yield* Scope.Scope
const miss = Effect.fn("ReadTool.miss")(function* (filepath: string) {
@@ -75,9 +73,8 @@ export const ReadTool = Tool.define(
).pipe(Effect.map((items: string[]) => items.sort((a, b) => a.localeCompare(b))))
})
const warm = Effect.fn("ReadTool.warm")(function* (filepath: string, sessionID: Tool.Context["sessionID"]) {
const warm = Effect.fn("ReadTool.warm")(function* (filepath: string) {
yield* lsp.touchFile(filepath, false).pipe(Effect.ignore, Effect.forkIn(scope))
yield* time.read(sessionID, filepath)
})
const run = Effect.fn("ReadTool.execute")(function* (params: z.infer<typeof parameters>, ctx: Tool.Context) {
@@ -196,7 +193,7 @@ export const ReadTool = Tool.define(
}
output += "\n</content>"
yield* warm(filepath, ctx.sessionID)
yield* warm(filepath)
if (loaded.length > 0) {
output += `\n\n<system-reminder>\n${loaded.map((item) => item.content).join("\n\n")}\n</system-reminder>`
-3
View File
@@ -39,7 +39,6 @@ import { InstanceState } from "@/effect"
import { Question } from "../question"
import { Todo } from "../session/todo"
import { LSP } from "../lsp"
import { FileTime } from "../file/time"
import { Instruction } from "../session/instruction"
import { AppFileSystem } from "@opencode-ai/shared/filesystem"
import { Bus } from "../bus"
@@ -80,7 +79,6 @@ export const layer: Layer.Layer<
| Session.Service
| Provider.Service
| LSP.Service
| FileTime.Service
| Instruction.Service
| AppFileSystem.Service
| Bus.Service
@@ -329,7 +327,6 @@ export const defaultLayer = Layer.suspend(() =>
Layer.provide(Session.defaultLayer),
Layer.provide(Provider.defaultLayer),
Layer.provide(LSP.defaultLayer),
Layer.provide(FileTime.defaultLayer),
Layer.provide(Instruction.defaultLayer),
Layer.provide(AppFileSystem.defaultLayer),
Layer.provide(Bus.layer),
-4
View File
@@ -9,7 +9,6 @@ import { Bus } from "../bus"
import { File } from "../file"
import { FileWatcher } from "../file/watcher"
import { Format } from "../format"
import { FileTime } from "../file/time"
import { AppFileSystem } from "@opencode-ai/shared/filesystem"
import { Instance } from "../project/instance"
import { trimDiff } from "./edit"
@@ -22,7 +21,6 @@ export const WriteTool = Tool.define(
Effect.gen(function* () {
const lsp = yield* LSP.Service
const fs = yield* AppFileSystem.Service
const filetime = yield* FileTime.Service
const bus = yield* Bus.Service
const format = yield* Format.Service
@@ -41,7 +39,6 @@ export const WriteTool = Tool.define(
const exists = yield* fs.existsSafe(filepath)
const contentOld = exists ? yield* fs.readFileString(filepath) : ""
if (exists) yield* filetime.assert(ctx.sessionID, filepath)
const diff = trimDiff(createTwoFilesPatch(filepath, filepath, contentOld, params.content))
yield* ctx.ask({
@@ -61,7 +58,6 @@ export const WriteTool = Tool.define(
file: filepath,
event: exists ? "change" : "add",
})
yield* filetime.read(ctx.sessionID, filepath)
let output = "Wrote file successfully."
yield* lsp.touchFile(filepath, true)