@@ -553,6 +553,11 @@ class ResolveRelationsResult:
553553 # Source notes whose links this run resolved; see
554554 # services.read_cache_changes.resolved_relations_read_cache_change.
555555 affected_entity_ids : frozenset [int ] = frozenset ()
556+ # Entities the vector backend could not finish. Failed entities remain marked
557+ # for the backend's retry/readiness path; the result exposes both sets instead
558+ # of silently reporting the relation refresh as fully synced.
559+ vector_sync_failed_entity_ids : frozenset [int ] = frozenset ()
560+ vector_sync_deferred_entity_ids : frozenset [int ] = frozenset ()
556561
557562 @property
558563 def resolved (self ) -> int :
@@ -577,9 +582,11 @@ async def resolve_project_relations(
577582 genuine forward references and remain unresolved until their target note
578583 exists.
579584
580- When ``vector_sync`` is set, callers that embed run it once after every
581- pass settles: resolution rewrites the affected sources' relation search
582- rows, and only changed chunks are re-embedded.
585+ When ``vector_sync`` is set, callers that embed run it after every pass
586+ settles and then drain any deferred shards. Resolution rewrites the affected
587+ sources' relation search rows, and only changed chunks are re-embedded.
588+ Failed entities remain marked by the backend's retry/readiness state and are
589+ exposed on the result.
583590 """
584591 unresolved_before = await runtime .count_unresolved_relations ()
585592 affected_entities : AffectedEntityIds = set ()
@@ -593,12 +600,58 @@ async def resolve_project_relations(
593600 if not affected :
594601 break
595602
603+ remaining = await runtime .count_unresolved_relations ()
604+ failed_entity_ids : set [EntityId ] = set ()
605+ deferred_entity_ids : set [EntityId ] = set ()
606+
607+ # Trigger: the resolution pass rewrote relation search rows for these sources
608+ # after their owners had already embedded them.
609+ # Why: chunk keys follow the row text, so those changed relation rows have no
610+ # current vector until something re-syncs them; nothing else revisits them.
611+ # Outcome: re-embed the affected sources after resolution committed. The batch
612+ # backend reports failed and deferred entities instead of raising, so drain
613+ # every deferred shard here; failed entities stay marked by the backend's
614+ # retry/readiness state and both sets are exposed on the result.
615+ if vector_sync is not None and affected_entities :
616+ pending_entity_ids = sorted (affected_entities )
617+ previous_pending_chunks : int | None = None
618+ while pending_entity_ids :
619+ sync_result = await vector_sync .sync_entity_vectors_batch (pending_entity_ids )
620+ failed_entity_ids .update (sync_result .failed_entity_ids )
621+ pending_entity_ids = list (sync_result .deferred_entity_ids )
622+ if not pending_entity_ids :
623+ break
624+
625+ pending_chunks = sync_result .chunks_total - sync_result .chunks_skipped
626+ if previous_pending_chunks is not None and pending_chunks >= previous_pending_chunks :
627+ logger .warning (
628+ "Stopped draining deferred relation vectors: no progress" ,
629+ deferred_entities = len (pending_entity_ids ),
630+ pending_chunks = pending_chunks ,
631+ )
632+ break
633+ previous_pending_chunks = pending_chunks
634+
635+ deferred_entity_ids .update (pending_entity_ids )
636+ if failed_entity_ids :
637+ logger .warning (
638+ "Relation vector sync left failed entities" ,
639+ failed_entities = len (failed_entity_ids ),
640+ )
641+ if deferred_entity_ids :
642+ logger .warning (
643+ "Relation vector sync left deferred entities after drain" ,
644+ deferred_entities = len (deferred_entity_ids ),
645+ )
646+
596647 result = ResolveRelationsResult (
597648 unresolved_before = unresolved_before ,
598- remaining = await runtime . count_unresolved_relations () ,
649+ remaining = remaining ,
599650 passes = passes ,
600651 affected_entities = len (affected_entities ),
601652 affected_entity_ids = frozenset (affected_entities ),
653+ vector_sync_failed_entity_ids = frozenset (failed_entity_ids ),
654+ vector_sync_deferred_entity_ids = frozenset (deferred_entity_ids ),
602655 )
603656 logger .info (
604657 "Resolved project relations" ,
@@ -608,12 +661,4 @@ async def resolve_project_relations(
608661 passes = result .passes ,
609662 affected_entities = result .affected_entities ,
610663 )
611- # Trigger: the resolution pass rewrote relation search rows for these sources
612- # after their owners had already embedded them.
613- # Why: chunk keys follow the row text, so those changed relation rows have no
614- # current vector until something re-syncs them; nothing else revisits them.
615- # Outcome: one batch re-embeds the affected sources after resolution committed;
616- # a later batch or reindex is not required.
617- if vector_sync is not None and affected_entities :
618- await vector_sync .sync_entity_vectors_batch (sorted (affected_entities ))
619664 return result
0 commit comments