@@ -107,23 +107,25 @@ async function processRepo(
107107 deps : ExtractorDeps ,
108108 qx : QueryExecutor ,
109109) : Promise < void > {
110- // One failing extractor must not sink the repo.
111110 const results = await Promise . allSettled ( EXTRACTORS . map ( ( extract ) => extract ( target , deps ) ) )
112111
113- // Every extractor failed (transient outage) — preserve existing data, just record the attempt.
114- if ( results . every ( ( r ) => r . status === 'rejected' ) ) {
115- log . warn ( { repoId : target . repoId } , 'All extractors failed — preserving existing data' )
112+ // Replace the repo's contacts only when every extractor succeeded. If any failed (transient
113+ // error — non-200s throw), a destructive rewrite would drop contacts a failed tier-A/B extractor
114+ // still has, so preserve existing data and just record the attempt; retried next cadence.
115+ const failed = results . find ( ( r ) => r . status === 'rejected' ) as PromiseRejectedResult | undefined
116+ if ( failed ) {
117+ log . warn (
118+ { repoId : target . repoId , errMsg : failed . reason ?. message } ,
119+ 'Extractor failed — preserving existing data' ,
120+ )
116121 await markRepoAttempted ( qx , target . repoId )
117122 return
118123 }
119124
120125 let contacts : RawContact [ ] = [ ]
121126 const policies : Partial < RepoPolicies > = { }
122127 for ( const r of results ) {
123- if ( r . status !== 'fulfilled' ) {
124- log . warn ( { repoId : target . repoId , errMsg : r . reason ?. message } , 'Extractor failed' )
125- continue
126- }
128+ if ( r . status !== 'fulfilled' ) continue
127129 contacts . push ( ...r . value . contacts )
128130 for ( const [ key , value ] of Object . entries ( r . value . policies ) ) {
129131 if ( ! ( policies as Record < string , unknown > ) [ key ] && value != null ) {
@@ -153,7 +155,16 @@ export async function processBatch(qx: QueryExecutor, config: Config): Promise<B
153155 }
154156
155157 const targets = batch . map ( toTarget )
156- await mapWithConcurrency ( targets , CONCURRENCY , ( target ) => processRepo ( target , deps , qx ) )
158+ // Isolate per-repo failures: mapWithConcurrency is fail-fast, so a single repo's DB error must
159+ // not abort the batch (and, across Temporal retries, halt the whole sweep). Unmarked repos on
160+ // error are simply retried next sweep.
161+ await mapWithConcurrency ( targets , CONCURRENCY , async ( target ) => {
162+ try {
163+ await processRepo ( target , deps , qx )
164+ } catch ( err ) {
165+ log . error ( { repoId : target . repoId , errMsg : ( err as Error ) . message } , 'Repo processing failed' )
166+ }
167+ } )
157168
158169 log . info ( { processed : targets . length } , 'Security contacts batch complete' )
159170 return { processed : targets . length }
0 commit comments