@@ -97,15 +97,33 @@ export class MetadataManager {
9797 lastUpdated : new Date ( ) . toISOString ( ) ,
9898 } ;
9999
100+ // Per LLM writing to a file is not an atomic operation while rename is, so nobody should get partial content.
101+ // Sounds reasonable.
102+ const dir = path . dirname ( this . filePath ) ;
103+ const base = path . basename ( this . filePath ) ;
104+
105+ // Keep temp file in the same directory to maximize chance that rename is atomic
106+ const tmpPath = path . join ( dir , `.${ base } .tmp-${ process . pid } -${ Date . now ( ) } ` ) ;
107+
108+ const json = JSON . stringify ( metadata , null , 2 ) ;
109+
100110 await withTimeout (
101- fsPromises . writeFile (
102- this . filePath ,
103- JSON . stringify ( metadata , null , 2 ) ,
104- "utf-8" ,
105- ) ,
111+ fsPromises . writeFile ( tmpPath , json , "utf-8" ) ,
106112 DEFAULT_TIMEOUTS . METADATA ,
107- "Save metadata" ,
113+ "Save metadata (tmp write) " ,
108114 ) ;
115+
116+ try {
117+ // TODO (AleksandrSl 14/12/2025): LLM says that we may want to remove older file first for windows, but it seems lo work fine as is.
118+ await withTimeout (
119+ fsPromises . rename ( tmpPath , this . filePath ) ,
120+ DEFAULT_TIMEOUTS . METADATA ,
121+ "Save metadata (atomic rename)" ,
122+ ) ;
123+ } finally {
124+ // Best-effort cleanup if rename failed for some reason
125+ await fsPromises . unlink ( tmpPath ) . catch ( ( ) => { } ) ;
126+ }
109127 }
110128
111129 /**
@@ -120,29 +138,25 @@ export class MetadataManager {
120138 ) : Promise < MetadataSchema > {
121139 const lockDir = path . dirname ( this . filePath ) ;
122140
123- // Ensure directory exists before locking
124141 await fsPromises . mkdir ( lockDir , { recursive : true } ) ;
125142
126- // Create lock file if it doesn't exist (lockfile needs a file to lock)
127143 try {
128144 await fsPromises . access ( this . filePath ) ;
129145 } catch {
130- // TODO (AleksandrSl 10/12/2025): Should I use another file as a lock?
131146 await fsPromises . writeFile (
132147 this . filePath ,
133148 JSON . stringify ( createEmptyMetadata ( ) , null , 2 ) ,
134149 "utf-8" ,
135150 ) ;
136151 }
137152
138- // Acquire lock with retry options
139153 const release = await lockfile . lock ( this . filePath , {
140154 retries : {
141155 retries : 10 ,
142156 minTimeout : 50 ,
143157 maxTimeout : 1000 ,
144158 } ,
145- stale : 2000 , // Consider lock stale after 5 seconds
159+ stale : 2000 ,
146160 } ) ;
147161
148162 try {
0 commit comments