@@ -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+
1836export 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
7191export 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 ) {
0 commit comments