@@ -254,13 +254,30 @@ const make = Effect.gen(function* () {
254254 // Pointing at T3 Code's own model folder must not replace a file with a link to itself.
255255 if ( ( yield * realPath ( file ) ) === ( yield * realPath ( finalPath ) ) ) continue ;
256256 if ( yield * verifiedPath ( model , finalPath ) ) continue ;
257- if ( ! ( yield * digestMatches ( model , file ) ) ) continue ;
258- yield * fs . remove ( finalPath , { force : true } ) ;
257+ const hashed = yield * expectedFile ( model , file ) ;
258+ if ( ! hashed || ! ( yield * digestMatches ( model , file ) ) ) continue ;
259+ // Staged then renamed, so a failed link never removes the file already installed.
260+ const staged = `${ finalPath } .${ yield * random . uuidV4 ( ) } .part` ;
259261 // Windows grants symlinks only to privileged accounts; a hard link also shares the bytes.
260- yield * fs . symlink ( file , finalPath ) . pipe ( Effect . catch ( ( ) => fs . link ( file , finalPath ) ) ) ;
261- const info = yield * fs . stat ( finalPath ) ;
262+ const hardLinked = yield * fs . symlink ( file , staged ) . pipe (
263+ Effect . as ( false ) ,
264+ Effect . catch ( ( ) => fs . link ( file , staged ) . pipe ( Effect . as ( true ) ) ) ,
265+ ) ;
266+ const current = yield * expectedFile ( model , staged ) ;
267+ const marker = identity ( model , hashed ) ;
268+ if ( ! current || identity ( model , current ) !== marker ) {
269+ yield * fs . remove ( staged , { force : true } ) ;
270+ continue ;
271+ }
272+ yield * fs . rename ( staged , finalPath ) ;
273+ // A hard link cannot be read back, so its origin is recorded beside it.
274+ yield * (
275+ hardLinked
276+ ? fs . writeFileString ( `${ finalPath } .source` , file , { mode : 0o600 } )
277+ : fs . remove ( `${ finalPath } .source` , { force : true } )
278+ ) . pipe ( Effect . ignore ) ;
262279 yield * fs
263- . writeFileString ( `${ finalPath } .verified` , identity ( model , info ) , { mode : 0o600 } )
280+ . writeFileString ( `${ finalPath } .verified` , marker , { mode : 0o600 } )
264281 . pipe ( Effect . ignore ) ;
265282 linked . push ( model . id ) ;
266283 }
@@ -305,6 +322,7 @@ const make = Effect.gen(function* () {
305322 if ( ! complete || ! ( yield * digestMatches ( model , partialPath ) ) )
306323 return yield * new SpeechModelDownloadError ( { modelId : model . id , reason : "verification" } ) ;
307324 yield * fs . rename ( partialPath , finalPath ) ;
325+ yield * fs . remove ( `${ finalPath } .source` , { force : true } ) . pipe ( Effect . ignore ) ;
308326 // Rename changes file metadata, so record the verified file's final identity.
309327 const info = yield * fs . stat ( finalPath ) ;
310328 yield * fs
@@ -324,7 +342,10 @@ const make = Effect.gen(function* () {
324342 return SpeechModelStore . of ( {
325343 link,
326344 linkedFrom : Effect . fn ( "SpeechModelStore.linkedFrom" ) ( ( model ) =>
327- fs . readLink ( modelPath ( model ) ) . pipe ( Effect . orElseSucceed ( ( ) => undefined ) ) ,
345+ fs . readLink ( modelPath ( model ) ) . pipe (
346+ Effect . catch ( ( ) => fs . readFileString ( `${ modelPath ( model ) } .source` ) ) ,
347+ Effect . orElseSucceed ( ( ) => undefined ) ,
348+ ) ,
328349 ) ,
329350 isReady : Effect . fn ( "SpeechModelStore.isReady" ) ( ( model ) =>
330351 expectedFile ( model , modelPath ( model ) ) . pipe (
@@ -341,6 +362,7 @@ const make = Effect.gen(function* () {
341362 const file = modelPath ( model ) ;
342363 yield * fs . remove ( file , { force : true } ) ;
343364 yield * fs . remove ( `${ file } .verified` , { force : true } ) ;
365+ yield * fs . remove ( `${ file } .source` , { force : true } ) ;
344366 } ) ,
345367 } ) ;
346368} ) ;
0 commit comments