@@ -257,7 +257,9 @@ export class MemoryUpgrader {
257257 return result ;
258258 }
259259
260- // Process in batches
260+ // Process in batches using two-phase approach (fix #632)
261+ // Phase 1: LLM enrichment - NO lock (parallel)
262+ // Phase 2: Single lock for ALL DB writes
261263 const toProcess = limit
262264 ? legacyMemories . slice ( 0 , limit )
263265 : legacyMemories ;
@@ -268,16 +270,49 @@ export class MemoryUpgrader {
268270 `memory-upgrader: processing batch ${ Math . floor ( i / batchSize ) + 1 } /${ Math . ceil ( toProcess . length / batchSize ) } (${ batch . length } memories)` ,
269271 ) ;
270272
271- for ( const entry of batch ) {
272- try {
273- await this . upgradeEntry ( entry , noLlm ) ;
274- result . upgraded ++ ;
275- } catch ( err ) {
276- const errMsg = `Failed to upgrade ${ entry . id } : ${ String ( err ) } ` ;
277- result . errors . push ( errMsg ) ;
278- this . log ( `memory-upgrader: ERROR — ${ errMsg } ` ) ;
273+ // Phase 1: LLM enrichment (parallel, no lock)
274+ const enrichedEntries = await Promise . all (
275+ batch . map ( async ( entry ) => {
276+ try {
277+ const enriched = await this . prepareEntry ( entry , noLlm ) ;
278+ return { entry, enriched, error : null } ;
279+ } catch ( err ) {
280+ const errMsg = `Failed to enrich ${ entry . id } : ${ String ( err ) } ` ;
281+ this . log ( `memory-upgrader: ERROR — ${ errMsg } ` ) ;
282+ return { entry, enriched : null , error : errMsg } ;
283+ }
284+ } )
285+ ) ;
286+
287+ // Phase 2: Single lock for ALL DB writes
288+ // Note: runWithFileLock is optional (exists in real store, not in test mocks)
289+ const doInLock = async < T > ( fn : ( ) => Promise < T > ) : Promise < T > => {
290+ if ( typeof ( this . store as any ) . runWithFileLock === "function" ) {
291+ return ( this . store as any ) . runWithFileLock ( fn ) ;
279292 }
280- }
293+ // No lock in test environment
294+ return fn ( ) ;
295+ } ;
296+
297+ await doInLock ( async ( ) => {
298+ for ( const { entry, enriched, error } of enrichedEntries ) {
299+ if ( error ) {
300+ result . errors . push ( error ) ;
301+ continue ;
302+ }
303+ if ( ! enriched ) {
304+ continue ;
305+ }
306+ try {
307+ await this . applyEnrichedEntry ( entry , enriched ) ;
308+ result . upgraded ++ ;
309+ } catch ( err ) {
310+ const errMsg = `Failed to apply ${ entry . id } : ${ String ( err ) } ` ;
311+ result . errors . push ( errMsg ) ;
312+ this . log ( `memory-upgrader: ERROR — ${ errMsg } ` ) ;
313+ }
314+ }
315+ } ) ;
281316
282317 // Progress report
283318 this . log (
@@ -292,12 +327,13 @@ export class MemoryUpgrader {
292327 }
293328
294329 /**
295- * Upgrade a single legacy memory entry.
330+ * Phase 1: Prepare enriched metadata (LLM enrichment, NO lock).
331+ * Extracts LLM logic from upgradeEntry for two-phase approach (fix #632).
296332 */
297- private async upgradeEntry (
333+ private async prepareEntry (
298334 entry : MemoryEntry ,
299335 noLlm : boolean ,
300- ) : Promise < void > {
336+ ) : Promise < { enriched : Pick < EnrichedMetadata , "l0_abstract" | "l1_overview" | "l2_content" > ; newCategory : MemoryCategory } > {
301337 // Step 1: Reverse-map category
302338 let newCategory = reverseMapCategory ( entry . category , entry . text ) ;
303339
@@ -344,7 +380,19 @@ export class MemoryUpgrader {
344380 enriched = simpleEnrich ( entry . text , newCategory ) ;
345381 }
346382
347- // Step 3: Build enriched metadata
383+ return { enriched, newCategory } ;
384+ }
385+
386+ /**
387+ * Phase 2: Apply enriched entry to DB (within lock).
388+ */
389+ private async applyEnrichedEntry (
390+ entry : MemoryEntry ,
391+ prepared : { enriched : Pick < EnrichedMetadata , "l0_abstract" | "l1_overview" | "l2_content" > ; newCategory : MemoryCategory } ,
392+ ) : Promise < void > {
393+ const { enriched, newCategory } = prepared ;
394+
395+ // Build enriched metadata
348396 const existingMeta = entry . metadata ? ( ( ) => {
349397 try { return JSON . parse ( entry . metadata ! ) ; } catch { return { } ; }
350398 } ) ( ) : { } ;
@@ -366,13 +414,24 @@ export class MemoryUpgrader {
366414 upgraded_at : Date . now ( ) ,
367415 } ;
368416
369- // Step 4: Update the memory entry
417+ // Update the memory entry
370418 await this . store . update ( entry . id , {
371419 // Update text to L0 abstract for better search indexing
372420 text : enriched . l0_abstract ,
373421 metadata : stringifySmartMetadata ( newMetadata ) ,
374422 } ) ;
375423 }
424+
425+ /**
426+ * Upgrade a single legacy memory entry (legacy method, kept for backward compatibility).
427+ */
428+ private async upgradeEntry (
429+ entry : MemoryEntry ,
430+ noLlm : boolean ,
431+ ) : Promise < void > {
432+ const { enriched, newCategory } = await this . prepareEntry ( entry , noLlm ) ;
433+ await this . applyEnrichedEntry ( entry , { enriched, newCategory } ) ;
434+ }
376435}
377436
378437// ============================================================================
0 commit comments