@@ -9,7 +9,6 @@ import crypto from 'crypto';
99import fs from 'fs' ;
1010import { CancellationToken } from 'vscode-languageserver-protocol' ;
1111import { Result } from '../../../../util/common/result' ;
12- import { coalesce } from '../../../../util/vs/base/common/arrays' ;
1312import { raceCancellationError } from '../../../../util/vs/base/common/async' ;
1413import { CancellationError } from '../../../../util/vs/base/common/errors' ;
1514import { Disposable } from '../../../../util/vs/base/common/lifecycle' ;
@@ -167,19 +166,50 @@ export class ExternalIngestClient extends Disposable implements IExternalIngestC
167166 }
168167
169168 onProgress ?.( l10n . t ( 'Creating snapshot...' ) ) ;
169+
170170 // Create snapshot - this endpoint could return 429 if you already have too many filesets
171- let createIngestResponse : Response ;
172- try {
173- createIngestResponse = await this . post ( authToken , '/external/code/ingest' , {
171+ const createIngest = async ( ) : Promise < Response > => {
172+ return this . post ( authToken , '/external/code/ingest' , {
174173 fileset_name : filesetName ,
175174 new_checkpoint : newCheckpoint ,
176175 geo_filter : Buffer . from ( geoFilter . toBytes ( ) ) . toString ( 'base64' ) ,
177176 coded_symbols : codedSymbols ,
178177 } , token ) ;
178+ } ;
179+
180+ let createIngestResponse : Response ;
181+ try {
182+ createIngestResponse = await createIngest ( ) ;
179183 } catch ( err ) {
180184 throw new Error ( 'Exception during create ingest' , err ) ;
181185 }
182186
187+ // Handle 429 by cleaning up old filesets and retrying
188+ if ( createIngestResponse . status === 429 ) {
189+ this . logService . info ( 'ExternalIngestClient::updateIndex(): Got 429, cleaning up old filesets...' ) ;
190+ onProgress ?.( l10n . t ( "Too many filesets, cleaning up old ones..." ) ) ;
191+
192+ await raceCancellationError ( this . cleanupOldFilesets ( authToken , filesetName , token ) , token ) ;
193+
194+ // Retry the create ingest
195+ this . logService . info ( 'ExternalIngestClient::updateIndex(): Retrying create ingest after cleanup...' ) ;
196+ onProgress ?.( l10n . t ( "Retrying snapshot creation..." ) ) ;
197+ try {
198+ createIngestResponse = await createIngest ( ) ;
199+ } catch ( err ) {
200+ throw new Error ( 'Exception during create ingest retry' , err ) ;
201+ }
202+
203+ // If we still get 429 after cleanup and retry, fail with a clear error
204+ if ( createIngestResponse . status === 429 ) {
205+ throw new Error ( 'Create ingest failed with 429 Too Many Requests even after cleanup.' ) ;
206+ }
207+ }
208+
209+ // Fail fast on non-OK responses before attempting to parse JSON
210+ if ( ! createIngestResponse . ok ) {
211+ throw new Error ( `Create ingest failed with status ${ createIngestResponse . status } ` ) ;
212+ }
183213 interface CodedSymbolRange {
184214 readonly start : number ;
185215 readonly end : number ;
@@ -362,6 +392,11 @@ export class ExternalIngestClient extends Disposable implements IExternalIngestC
362392 return [ ] ;
363393 }
364394
395+ const filesets = await this . listFilesetsWithDetails ( authToken , token ) ;
396+ return filesets . map ( x => x . name ) ;
397+ }
398+
399+ private async listFilesetsWithDetails ( authToken : string , token : CancellationToken ) : Promise < Array < { name : string ; checkpoint : string ; status : string } > > {
365400 const resp = await this . apiClient . makeRequest (
366401 `${ ExternalIngestClient . baseUrl } /external/code/ingest` ,
367402 this . getHeaders ( authToken ) ,
@@ -371,7 +406,20 @@ export class ExternalIngestClient extends Disposable implements IExternalIngestC
371406 ) ;
372407
373408 const body = await resp . json ( ) as { filesets ?: Array < { name : string ; checkpoint : string ; status : string } > ; max_filesets : number } ;
374- return coalesce ( ( body . filesets ?? [ ] ) . map ( x => x . name ) ) ;
409+ return body . filesets ?? [ ] ;
410+ }
411+
412+ /**
413+ * Cleans up old filesets to make room for new ones.
414+ */
415+ private async cleanupOldFilesets ( authToken : string , currentFilesetName : string , token : CancellationToken ) : Promise < void > {
416+ const filesets = await this . listFilesetsWithDetails ( authToken , token ) ;
417+
418+ const candidates = filesets . filter ( f => f . name !== currentFilesetName ) ;
419+ const toDelete = candidates . at ( - 1 ) ;
420+ if ( toDelete ) {
421+ await this . deleteFilesetByName ( authToken , toDelete . name , token ) ;
422+ }
375423 }
376424
377425 async deleteFileset ( filesetName : string , token : CancellationToken ) : Promise < void > {
0 commit comments