@@ -325,10 +325,10 @@ impl Worker {
325325 self . diff_emit ( & mut next) ;
326326 }
327327 self . carriers = next;
328- self . refresh_watches ( ) ;
329- // Directories that could not be registered stay blind to events; their carriers fall
330- // back to digest polling at refresh cadence (`RESYNC-R01`).
328+ // Diff paths that were blind before registering newly recovered parents; otherwise the
329+ // new watch suppresses polling of mutations that happened during the blind interval.
331330 self . poll_unwatched ( ) ;
331+ self . refresh_watches ( ) ;
332332 }
333333
334334 /// Digest-diff every entry in `next`, emitting observed transitions. Seeding stays silent
@@ -348,17 +348,18 @@ impl Worker {
348348 if entry. digest . as_deref ( ) == Some ( new_digest. as_str ( ) ) {
349349 continue ;
350350 }
351- let old = entry. digest . take ( ) ;
352- emit_resync (
351+ let old = entry. digest . as_deref ( ) ;
352+ if emit_resync (
353353 & root,
354354 & this_host,
355355 & entry. bus_id ,
356356 & entry. label ,
357357 & path,
358- old. as_deref ( ) ,
358+ old,
359359 & new_digest,
360- ) ;
361- entry. digest = Some ( new_digest) ;
360+ ) {
361+ entry. digest = Some ( new_digest) ;
362+ }
362363 }
363364 }
364365 }
@@ -487,33 +488,45 @@ impl Worker {
487488 }
488489 }
489490
490- /// Flush every subscriber of one path: clear dirty flags, diff digests, emit transitions.
491+ /// Flush every subscriber of one path: diff digests, emit transitions, and retain failed
492+ /// publications for retry with the same event identity.
491493 fn flush_path ( & mut self , path : & Path ) {
492494 let Some ( entries) = self . carriers . get_mut ( path) else {
493495 return ;
494496 } ;
497+ let mut retries = Vec :: new ( ) ;
495498 for entry in entries. iter_mut ( ) {
496499 entry. dirty = false ;
497500 let Some ( new_digest) = read_digest ( path) else {
498501 // Unreadable right now (deleted mid-window): stay quiet and keep the previous
499- // digest, so a later reappearance still counts as a change. The next mutation
500- // re-dirties.
502+ // digest, so a later reappearance still counts as a change.
501503 continue ;
502504 } ;
503505 if entry. digest . as_deref ( ) == Some ( new_digest. as_str ( ) ) {
504506 continue ;
505507 }
506- let old = entry. digest . take ( ) ;
507- emit_resync (
508+ if emit_resync (
508509 & self . root ,
509510 & self . this_host ,
510511 & entry. bus_id ,
511512 & entry. label ,
512513 path,
513- old . as_deref ( ) ,
514+ entry . digest . as_deref ( ) ,
514515 & new_digest,
515- ) ;
516- entry. digest = Some ( new_digest) ;
516+ ) {
517+ entry. digest = Some ( new_digest) ;
518+ } else {
519+ entry. dirty = true ;
520+ retries. push ( entry. class ) ;
521+ }
522+ }
523+ let now = Instant :: now ( ) ;
524+ for class in retries {
525+ let deadline = now + class. window ( ) ;
526+ self . deadlines
527+ . entry ( class)
528+ . and_modify ( |existing| * existing = ( * existing) . min ( deadline) )
529+ . or_insert ( deadline) ;
517530 }
518531 }
519532}
@@ -533,11 +546,11 @@ fn emit_resync(
533546 path : & Path ,
534547 old : Option < & str > ,
535548 new_digest : & str ,
536- ) {
549+ ) -> bool {
537550 let subject = format ! ( "resource {label} changed" ) ;
538551 let body = render_body ( label, path, old, new_digest) ;
539552 let event_id = transition_identity ( & body) ;
540- let receipt = crate :: event:: emit (
553+ match crate :: event:: emit (
541554 root,
542555 this_host,
543556 bus_id,
@@ -547,12 +560,15 @@ fn emit_resync(
547560 Some ( subject. as_str ( ) ) ,
548561 & body,
549562 true ,
550- ) ;
551- if let Err ( error) = receipt {
552- eprintln ! (
553- "st2: resync emit for '{}' failed: {error:#}" ,
554- path. display( )
555- ) ;
563+ ) {
564+ Ok ( _) => true ,
565+ Err ( error) => {
566+ eprintln ! (
567+ "st2: resync emit for '{}' failed: {error:#}" ,
568+ path. display( )
569+ ) ;
570+ false
571+ }
556572 }
557573}
558574
@@ -678,6 +694,36 @@ mod tests {
678694 }
679695 }
680696
697+ #[ test]
698+ fn failed_emit_retains_digest_and_schedules_the_same_transition_for_retry ( ) {
699+ let root = tempfile:: tempdir ( ) . unwrap ( ) ;
700+ let carrier = root. path ( ) . join ( "carrier.md" ) ;
701+ std:: fs:: write ( & carrier, "new bytes" ) . unwrap ( ) ;
702+ let mut worker = Worker {
703+ root : root. path ( ) . to_path_buf ( ) ,
704+ this_host : "host" . to_owned ( ) ,
705+ carriers : BTreeMap :: from ( [ (
706+ carrier. clone ( ) ,
707+ vec ! [ Entry {
708+ bus_id: "host.missing" . to_owned( ) ,
709+ label: "goal" . to_owned( ) ,
710+ class: CarrierClass :: Immediate ,
711+ digest: Some ( "old-digest" . to_owned( ) ) ,
712+ dirty: true ,
713+ } ] ,
714+ ) ] ) ,
715+ deadlines : BTreeMap :: new ( ) ,
716+ watched : BTreeMap :: new ( ) ,
717+ watcher : None ,
718+ } ;
719+
720+ worker. flush_path ( & carrier) ;
721+ let entry = & worker. carriers [ & carrier] [ 0 ] ;
722+ assert_eq ! ( entry. digest. as_deref( ) , Some ( "old-digest" ) ) ;
723+ assert ! ( entry. dirty) ;
724+ assert ! ( worker. deadlines. contains_key( & CarrierClass :: Immediate ) ) ;
725+ }
726+
681727 #[ test]
682728 fn transition_identity_covers_every_rendered_transition_dimension ( ) {
683729 let baseline = render_body (
0 commit comments