2025-09-01 21:15:49 +00:00
import { Instance } from "../project/instance"
2025-07-04 21:57:48 +00:00
import { Log } from "../util/log"
2026-01-21 05:03:07 +00:00
import { Flag } from "../flag/flag"
2026-02-18 21:20:58 +00:00
import { Filesystem } from "../util/filesystem"
2025-05-19 23:29:38 +00:00
2025-06-27 15:29:20 +00:00
export namespace FileTime {
2025-07-04 21:57:48 +00:00
const log = Log . create ( { service : "file.time" } )
2025-12-15 05:01:50 +00:00
// 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.
2025-09-10 03:43:37 +00:00
export const state = Instance . state ( ( ) = > {
const read : {
[ sessionID : string ] : {
[ path : string ] : Date | undefined
2025-06-03 00:24:32 +00:00
}
2025-09-10 03:43:37 +00:00
} = { }
2025-12-15 05:01:50 +00:00
const locks = new Map < string , Promise < void > > ( )
2025-09-10 03:43:37 +00:00
return {
read ,
2025-12-15 05:01:50 +00:00
locks ,
2025-09-10 03:43:37 +00:00
}
} )
2025-05-19 23:29:38 +00:00
2025-06-03 00:24:32 +00:00
export function read ( sessionID : string , file : string ) {
2025-07-04 21:57:48 +00:00
log . info ( "read" , { sessionID , file } )
2025-06-03 00:24:32 +00:00
const { read } = state ( )
read [ sessionID ] = read [ sessionID ] || { }
read [ sessionID ] [ file ] = new Date ( )
2025-05-19 23:29:38 +00:00
}
2025-06-03 00:24:32 +00:00
export function get ( sessionID : string , file : string ) {
return state ( ) . read [ sessionID ] ? . [ file ]
2025-05-19 23:29:38 +00:00
}
2025-06-04 17:33:25 +00:00
2025-12-15 05:01:50 +00:00
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 )
}
}
}
2025-06-04 17:33:25 +00:00
export async function assert ( sessionID : string , filepath : string ) {
2026-01-21 05:03:07 +00:00
if ( Flag . OPENCODE_DISABLE_FILETIME_CHECK === true ) {
return
}
2025-06-04 17:33:25 +00:00
const time = get ( sessionID , filepath )
2026-01-21 05:03:07 +00:00
if ( ! time ) throw new Error ( ` You must read file ${ filepath } before overwriting it. Use the Read tool first ` )
2026-02-18 21:20:58 +00:00
const mtime = Filesystem . stat ( filepath ) ? . mtime
2026-02-24 13:10:10 +00:00
// Allow a 50ms tolerance for Windows NTFS timestamp fuzziness / async flushing
if ( mtime && mtime . getTime ( ) > time . getTime ( ) + 50 ) {
2025-06-04 17:33:25 +00:00
throw new Error (
2026-02-18 21:20:58 +00:00
` 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. ` ,
2025-06-04 17:33:25 +00:00
)
}
}
2025-05-19 23:29:38 +00:00
}