@@ -21,6 +21,10 @@ const DEFAULT_INTERVAL_MS = 5 * 60 * 1000;
2121const DEFAULT_FULL_VERIFICATION_INTERVAL_MS = 60 * 60 * 1000 ;
2222const DEFAULT_BACKGROUND_DELAY_MS = 1000 ;
2323const RECONCILIATION_CURSOR_STORAGE_KEY = 'agentHost.catalogReconciliation.cursor' ;
24+ const VERIFICATION_CURSOR_STORAGE_KEY = 'agentHost.catalogReconciliation.verificationCursor' ;
25+ const VERIFICATION_VERSION_STORAGE_KEY = 'agentHost.catalogReconciliation.verificationVersion' ;
26+ const LAST_VERIFICATION_STORAGE_KEY = 'agentHost.catalogReconciliation.lastVerification' ;
27+ const CATALOG_VERIFICATION_VERSION = 1 ;
2428type AgentHostCatalogSyncPendingReason = Extract < AgentHostCatalogSyncResult , { status : 'pending' } > [ 'reason' ] ;
2529type ScheduledPassKind = 'background' | 'periodic' ;
2630
@@ -80,8 +84,9 @@ export class AgentHostCatalogReconciliationService extends Disposable {
8084 private readonly _scheduledPass = this . _register ( new MutableDisposable < IDisposable > ( ) ) ;
8185 private _scheduledPassKind : ScheduledPassKind | undefined ;
8286 private _payloadDirtyMark : Promise < void > | undefined ;
83- private _initialPayloadDirtyMarkPending = true ;
84- private _lastFullVerification = 0 ;
87+ private _initialPayloadDirtyMarkPending : boolean ;
88+ private _lastCompatibilityVerification : number ;
89+ private _verificationCursor : string | undefined ;
8590 private _running : Promise < IAgentHostCatalogReconciliationReport > | undefined ;
8691 private _rerunRequested = false ;
8792 private _periodic = false ;
@@ -104,6 +109,11 @@ export class AgentHostCatalogReconciliationService extends Disposable {
104109 this . _backgroundDelayMs = this . _nonNegativeInteger ( options . backgroundDelayMs , DEFAULT_BACKGROUND_DELAY_MS , 'backgroundDelayMs' ) ;
105110 this . _schedule = options . schedule ?? ( ( callback , delay ) => disposableTimeout ( callback , delay ) ) ;
106111 this . _now = options . now ?? Date . now ;
112+ this . _initialPayloadDirtyMarkPending = this . _storageService . get < number > ( VERIFICATION_VERSION_STORAGE_KEY ) !== CATALOG_VERIFICATION_VERSION ;
113+ const lastVerification = this . _storageService . get < number > ( LAST_VERIFICATION_STORAGE_KEY ) ;
114+ this . _lastCompatibilityVerification = typeof lastVerification === 'number' && Number . isFinite ( lastVerification ) && lastVerification <= this . _now ( ) ? lastVerification : 0 ;
115+ const verificationCursor = this . _storageService . get < string > ( VERIFICATION_CURSOR_STORAGE_KEY ) ;
116+ this . _verificationCursor = typeof verificationCursor === 'string' ? verificationCursor : undefined ;
107117 }
108118
109119 schedule ( ) : void {
@@ -207,21 +217,20 @@ export class AgentHostCatalogReconciliationService extends Disposable {
207217
208218 private async _runSinglePass ( token : CancellationToken ) : Promise < IAgentHostCatalogReconciliationReport > {
209219 await this . _ensureInitialPayloadDirtyMark ( ) ;
210- if ( this . _now ( ) - this . _lastFullVerification >= this . _fullVerificationIntervalMs ) {
211- await this . _markAllPayloadsDirty ( ) ;
212- this . _lastFullVerification = this . _now ( ) ;
220+ if ( this . _now ( ) - this . _lastCompatibilityVerification >= this . _fullVerificationIntervalMs ) {
221+ await this . _markVerificationSampleDirty ( token ) ;
213222 }
214223 const { sessions, receiptBySession } = await this . _listDirtySessions ( ) ;
215224 if ( sessions . length === 0 ) {
216- this . _storageService . delete ( this . _cursorStorageKey ) ;
225+ this . _tryDeleteStorage ( this . _cursorStorageKey ) ;
217226 return { outcomes : [ ] , cursor : undefined } ;
218227 }
219228
220229 const selected = this . _selectBatch ( sessions , this . _readCursor ( ) ) ;
221230 const outcomes = await this . _runBatch ( selected , receiptBySession , token ) ;
222231 const cursor = selected . at ( - 1 ) ?. session . toString ( ) ;
223232 if ( cursor && ! token . isCancellationRequested ) {
224- this . _storageService . set ( this . _cursorStorageKey , cursor ) ;
233+ this . _trySetStorage ( this . _cursorStorageKey , cursor ) ;
225234 }
226235 return { outcomes, cursor } ;
227236 }
@@ -235,11 +244,11 @@ export class AgentHostCatalogReconciliationService extends Disposable {
235244 outcomes . push ( ...await this . _runBatch ( selected , receiptBySession , token ) ) ;
236245 cursor = selected . at ( - 1 ) ?. session . toString ( ) ;
237246 if ( cursor && ! token . isCancellationRequested ) {
238- this . _storageService . set ( this . _cursorStorageKey , cursor ) ;
247+ this . _trySetStorage ( this . _cursorStorageKey , cursor ) ;
239248 }
240249 }
241250 if ( sessions . length === 0 ) {
242- this . _storageService . delete ( this . _cursorStorageKey ) ;
251+ this . _tryDeleteStorage ( this . _cursorStorageKey ) ;
243252 }
244253 return { outcomes, cursor } ;
245254 }
@@ -556,13 +565,65 @@ export class AgentHostCatalogReconciliationService extends Disposable {
556565 }
557566 await this . _markAllPayloadsDirty ( ) ;
558567 this . _initialPayloadDirtyMarkPending = false ;
559- this . _lastFullVerification = this . _now ( ) ;
568+ this . _trySetStorage ( VERIFICATION_VERSION_STORAGE_KEY , CATALOG_VERIFICATION_VERSION ) ;
569+ this . _recordCompatibilityVerification ( ) ;
560570 }
561571
562572 private async _prepareFullVerification ( ) : Promise < void > {
563573 await this . _markAllPayloadsDirty ( ) ;
564574 this . _initialPayloadDirtyMarkPending = false ;
565- this . _lastFullVerification = this . _now ( ) ;
575+ this . _trySetStorage ( VERIFICATION_VERSION_STORAGE_KEY , CATALOG_VERIFICATION_VERSION ) ;
576+ this . _recordCompatibilityVerification ( ) ;
577+ }
578+
579+ private async _markVerificationSampleDirty ( token : CancellationToken ) : Promise < void > {
580+ if ( token . isCancellationRequested ) {
581+ return ;
582+ }
583+ const receipts = ( await this . _catalogDatabase . listSessionsV2Receipts ( ) )
584+ . filter ( receipt => receipt . payloadDirty === 0 )
585+ . sort ( ( first , second ) => compareSessionKeys ( first . session , second . session ) ) ;
586+ const selected = this . _selectVerificationSample ( receipts , this . _verificationCursor ) ;
587+ await this . _catalogDatabase . markSessionsV2PayloadsDirty ( selected . map ( receipt => receipt . session ) ) ;
588+ if ( token . isCancellationRequested ) {
589+ return ;
590+ }
591+ const cursor = selected . at ( - 1 ) ?. session ;
592+ if ( cursor ) {
593+ this . _verificationCursor = cursor ;
594+ this . _trySetStorage ( VERIFICATION_CURSOR_STORAGE_KEY , cursor ) ;
595+ }
596+ this . _recordCompatibilityVerification ( ) ;
597+ }
598+
599+ private _selectVerificationSample ( receipts : readonly IAgentHostDatabaseSessionV2Receipt [ ] , cursor : string | undefined ) : readonly IAgentHostDatabaseSessionV2Receipt [ ] {
600+ if ( receipts . length === 0 ) {
601+ return [ ] ;
602+ }
603+ const start = cursor === undefined ? 0 : Math . max ( 0 , receipts . findIndex ( receipt => compareSessionKeys ( receipt . session , cursor ) > 0 ) ) ;
604+ const ordered = start === 0 ? receipts : [ ...receipts . slice ( start ) , ...receipts . slice ( 0 , start ) ] ;
605+ return ordered . slice ( 0 , this . _batchSize ) ;
606+ }
607+
608+ private _recordCompatibilityVerification ( ) : void {
609+ this . _lastCompatibilityVerification = this . _now ( ) ;
610+ this . _trySetStorage ( LAST_VERIFICATION_STORAGE_KEY , this . _lastCompatibilityVerification ) ;
611+ }
612+
613+ private _trySetStorage < T > ( key : string , value : T ) : void {
614+ try {
615+ this . _storageService . set ( key , value ) ;
616+ } catch ( error ) {
617+ this . _logService . warn ( `[AgentHostCatalogReconciliation] Failed to persist '${ key } '` , error ) ;
618+ }
619+ }
620+
621+ private _tryDeleteStorage ( key : string ) : void {
622+ try {
623+ this . _storageService . delete ( key ) ;
624+ } catch ( error ) {
625+ this . _logService . warn ( `[AgentHostCatalogReconciliation] Failed to delete '${ key } '` , error ) ;
626+ }
566627 }
567628
568629 private _markAllPayloadsDirty ( ) : Promise < void > {
0 commit comments