time.ts 2.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869
  1. import { Instance } from "../project/instance"
  2. import { Log } from "../util/log"
  3. import { Flag } from "../flag/flag"
  4. export namespace FileTime {
  5. const log = Log.create({ service: "file.time" })
  6. // Per-session read times plus per-file write locks.
  7. // All tools that overwrite existing files should run their
  8. // assert/read/write/update sequence inside withLock(filepath, ...)
  9. // so concurrent writes to the same file are serialized.
  10. export const state = Instance.state(() => {
  11. const read: {
  12. [sessionID: string]: {
  13. [path: string]: Date | undefined
  14. }
  15. } = {}
  16. const locks = new Map<string, Promise<void>>()
  17. return {
  18. read,
  19. locks,
  20. }
  21. })
  22. export function read(sessionID: string, file: string) {
  23. log.info("read", { sessionID, file })
  24. const { read } = state()
  25. read[sessionID] = read[sessionID] || {}
  26. read[sessionID][file] = new Date()
  27. }
  28. export function get(sessionID: string, file: string) {
  29. return state().read[sessionID]?.[file]
  30. }
  31. export async function withLock<T>(filepath: string, fn: () => Promise<T>): Promise<T> {
  32. const current = state()
  33. const currentLock = current.locks.get(filepath) ?? Promise.resolve()
  34. let release: () => void = () => {}
  35. const nextLock = new Promise<void>((resolve) => {
  36. release = resolve
  37. })
  38. const chained = currentLock.then(() => nextLock)
  39. current.locks.set(filepath, chained)
  40. await currentLock
  41. try {
  42. return await fn()
  43. } finally {
  44. release()
  45. if (current.locks.get(filepath) === chained) {
  46. current.locks.delete(filepath)
  47. }
  48. }
  49. }
  50. export async function assert(sessionID: string, filepath: string) {
  51. if (Flag.OPENCODE_DISABLE_FILETIME_CHECK === true) {
  52. return
  53. }
  54. const time = get(sessionID, filepath)
  55. if (!time) throw new Error(`You must read file ${filepath} before overwriting it. Use the Read tool first`)
  56. const stats = await Bun.file(filepath).stat()
  57. if (stats.mtime.getTime() > time.getTime()) {
  58. throw new Error(
  59. `File ${filepath} has been modified since it was last read.\nLast modification: ${stats.mtime.toISOString()}\nLast read: ${time.toISOString()}\n\nPlease read the file again before modifying it.`,
  60. )
  61. }
  62. }
  63. }