@@ -397,34 +397,55 @@ pub async fn execute_spawn(
397397 cache_metadata_and_inputs
398398 {
399399 if result. exit_status . success ( ) {
400- // path_reads is empty when inference is disabled (path_accesses is None)
401- let empty_path_reads = HashMap :: default ( ) ;
402- let path_reads = path_accesses. as_ref ( ) . map_or ( & empty_path_reads, |pa| & pa. path_reads ) ;
403-
404- // Execution succeeded — attempt to create fingerprint and update cache
405- match PostRunFingerprint :: create ( path_reads, cache_base_path) {
406- Ok ( post_run_fingerprint) => {
407- let new_cache_value = CacheEntryValue {
408- post_run_fingerprint,
409- std_outputs : std_outputs. unwrap_or_default ( ) . into ( ) ,
410- duration : result. duration ,
411- globbed_inputs,
412- } ;
413- match cache. update ( cache_metadata, new_cache_value) . await {
414- Ok ( ( ) ) => ( CacheUpdateStatus :: Updated , None ) ,
415- Err ( err) => (
416- CacheUpdateStatus :: NotUpdated ( CacheNotUpdatedReason :: CacheDisabled ) ,
417- Some ( ExecutionError :: Cache {
418- kind : CacheErrorKind :: Update ,
419- source : err,
420- } ) ,
421- ) ,
400+ // Check for read-write overlap: if the task wrote to any file it also
401+ // read, the inputs were modified during execution — don't cache.
402+ // Note: this only checks fspy-inferred reads, not globbed_inputs keys.
403+ // A task that writes to a glob-matched file without reading it causes
404+ // perpetual cache misses (glob detects the hash change) but not a
405+ // correctness bug, so we don't handle that case here.
406+ if let Some ( path) = path_accesses
407+ . as_ref ( )
408+ . and_then ( |pa| pa. path_reads . keys ( ) . find ( |p| pa. path_writes . contains ( * p) ) )
409+ {
410+ (
411+ CacheUpdateStatus :: NotUpdated ( CacheNotUpdatedReason :: InputModified {
412+ path : path. clone ( ) ,
413+ } ) ,
414+ None ,
415+ )
416+ } else {
417+ // path_reads is empty when inference is disabled (path_accesses is None)
418+ let empty_path_reads = HashMap :: default ( ) ;
419+ let path_reads =
420+ path_accesses. as_ref ( ) . map_or ( & empty_path_reads, |pa| & pa. path_reads ) ;
421+
422+ // Execution succeeded — attempt to create fingerprint and update cache.
423+ // Paths already in globbed_inputs are skipped: Rule 1 (above) guarantees
424+ // no input modification, so the prerun hash is the correct post-exec hash.
425+ match PostRunFingerprint :: create ( path_reads, cache_base_path, & globbed_inputs) {
426+ Ok ( post_run_fingerprint) => {
427+ let new_cache_value = CacheEntryValue {
428+ post_run_fingerprint,
429+ std_outputs : std_outputs. unwrap_or_default ( ) . into ( ) ,
430+ duration : result. duration ,
431+ globbed_inputs,
432+ } ;
433+ match cache. update ( cache_metadata, new_cache_value) . await {
434+ Ok ( ( ) ) => ( CacheUpdateStatus :: Updated , None ) ,
435+ Err ( err) => (
436+ CacheUpdateStatus :: NotUpdated ( CacheNotUpdatedReason :: CacheDisabled ) ,
437+ Some ( ExecutionError :: Cache {
438+ kind : CacheErrorKind :: Update ,
439+ source : err,
440+ } ) ,
441+ ) ,
442+ }
422443 }
444+ Err ( err) => (
445+ CacheUpdateStatus :: NotUpdated ( CacheNotUpdatedReason :: CacheDisabled ) ,
446+ Some ( ExecutionError :: PostRunFingerprint ( err) ) ,
447+ ) ,
423448 }
424- Err ( err) => (
425- CacheUpdateStatus :: NotUpdated ( CacheNotUpdatedReason :: CacheDisabled ) ,
426- Some ( ExecutionError :: PostRunFingerprint ( err) ) ,
427- ) ,
428449 }
429450 } else {
430451 // Execution failed with non-zero exit status — don't update cache
0 commit comments