@@ -213,6 +213,9 @@ export function computeEffectiveHalfLife(
213213 */
214214export class AccessTracker {
215215 private readonly pending : Map < string , number > = new Map ( ) ;
216+ // Tracks retry count per ID so that delta is never amplified across failures.
217+ private readonly _retryCount = new Map < string , number > ( ) ;
218+ private readonly _maxRetries = 5 ;
216219 private debounceTimer : ReturnType < typeof setTimeout > | null = null ;
217220 private flushPromise : Promise < void > | null = null ;
218221 private readonly debounceMs : number ;
@@ -291,10 +294,22 @@ export class AccessTracker {
291294 this . clearTimer ( ) ;
292295 if ( this . pending . size > 0 ) {
293296 this . logger . warn (
294- `access-tracker: destroying with ${ this . pending . size } pending writes` ,
297+ `access-tracker: destroying with ${ this . pending . size } pending writes — attempting final flush (3s timeout) ` ,
295298 ) ;
299+ // Fire-and-forget final flush with a hard 3s timeout. Uses Promise.race
300+ // to guarantee we always clear pending/_retryCount even if flush hangs.
301+ const flushWithTimeout = Promise . race ( [
302+ this . doFlush ( ) ,
303+ new Promise < void > ( ( resolve ) => setTimeout ( resolve , 3_000 ) ) ,
304+ ] ) ;
305+ void flushWithTimeout . finally ( ( ) => {
306+ this . pending . clear ( ) ;
307+ this . _retryCount . clear ( ) ;
308+ } ) ;
309+ } else {
310+ this . pending . clear ( ) ;
311+ this . _retryCount . clear ( ) ;
296312 }
297- this . pending . clear ( ) ;
298313 }
299314
300315 // --------------------------------------------------------------------------
@@ -308,18 +323,33 @@ export class AccessTracker {
308323 for ( const [ id , delta ] of batch ) {
309324 try {
310325 const current = await this . store . getById ( id ) ;
311- if ( ! current ) continue ;
326+ if ( ! current ) {
327+ // ID not found — memory was deleted or outside current scope.
328+ // Do NOT retry or warn; just drop silently and clear any retry counter.
329+ this . _retryCount . delete ( id ) ;
330+ continue ;
331+ }
312332
313333 const updatedMeta = buildUpdatedMetadata ( current . metadata , delta ) ;
314334 await this . store . update ( id , { metadata : updatedMeta } ) ;
335+ this . _retryCount . delete ( id ) ; // success — clear retry counter
315336 } catch ( err ) {
316- // Requeue failed delta for retry on next flush
317- const existing = this . pending . get ( id ) ?? 0 ;
318- this . pending . set ( id , existing + delta ) ;
319- this . logger . warn (
320- `access-tracker: write-back failed for ${ id . slice ( 0 , 8 ) } :` ,
321- err ,
322- ) ;
337+ const retryCount = ( this . _retryCount . get ( id ) ?? 0 ) + 1 ;
338+ if ( retryCount > this . _maxRetries ) {
339+ // Exceeded max retries — drop and log error.
340+ this . _retryCount . delete ( id ) ;
341+ this . logger . error (
342+ `access-tracker: dropping ${ id . slice ( 0 , 8 ) } after ${ retryCount } failed retries` ,
343+ ) ;
344+ } else {
345+ this . _retryCount . set ( id , retryCount ) ;
346+ // Requeue with the original delta only (NOT accumulated) for next flush.
347+ this . pending . set ( id , delta ) ;
348+ this . logger . warn (
349+ `access-tracker: write-back failed for ${ id . slice ( 0 , 8 ) } (attempt ${ retryCount } /${ this . _maxRetries } ):` ,
350+ err ,
351+ ) ;
352+ }
323353 }
324354 }
325355 }
0 commit comments