@@ -495,6 +495,104 @@ describe("memory tools", () => {
495495 expect ( getMemorySearchManagerMockCalls ( ) ) . toBe ( 1 ) ;
496496 } ) ;
497497
498+ it ( "runs the memory and wiki branches concurrently for corpus=all (#92633)" , async ( ) => {
499+ // Regression: corpus=all awaited the memory/sessions branch fully before
500+ // starting the wiki/supplement branch, so two searches that each finished
501+ // well under the 15s tool deadline summed past it and the tool returned
502+ // nothing. Running them concurrently keeps the combined wall time near
503+ // max(branch), so the same two 9s searches now resolve at ~9s.
504+ vi . useFakeTimers ( ) ;
505+ try {
506+ const delay = ( ms : number ) =>
507+ new Promise < void > ( ( resolve ) => {
508+ setTimeout ( resolve , ms ) ;
509+ } ) ;
510+ setMemorySearchImpl ( async ( ) => {
511+ await delay ( 9_000 ) ;
512+ return [
513+ {
514+ path : "MEMORY.md" ,
515+ startLine : 5 ,
516+ endLine : 7 ,
517+ score : 0.9 ,
518+ snippet : "@@ -5,3 @@\nAssistant: noted" ,
519+ source : "memory" as const ,
520+ } ,
521+ ] ;
522+ } ) ;
523+ registerMemoryCorpusSupplement ( "memory-wiki" , {
524+ search : async ( ) => {
525+ await delay ( 9_000 ) ;
526+ return [
527+ {
528+ corpus : "wiki" ,
529+ path : "entities/alpha.md" ,
530+ title : "Alpha" ,
531+ kind : "entity" ,
532+ score : 1.1 ,
533+ snippet : "Alpha wiki entry" ,
534+ } ,
535+ ] ;
536+ } ,
537+ get : async ( ) => null ,
538+ } ) ;
539+
540+ const tool = createMemorySearchToolOrThrow ( ) ;
541+ const resultPromise = tool . execute ( "call_all_concurrent" , {
542+ query : "alpha" ,
543+ corpus : "all" ,
544+ } ) ;
545+ // Sequentially the two branches take 18s and trip the 15s deadline;
546+ // concurrently the tool resolves once the shared 9s elapses.
547+ await vi . advanceTimersByTimeAsync ( 9_000 ) ;
548+ const result = await resultPromise ;
549+ const details = result . details as {
550+ disabled ?: boolean ;
551+ results : Array < { corpus : string ; path : string } > ;
552+ } ;
553+ expect ( details . disabled ) . toBeUndefined ( ) ;
554+ expect ( details . results . map ( ( entry ) => [ entry . corpus , entry . path ] ) ) . toEqual ( [
555+ [ "wiki" , "entities/alpha.md" ] ,
556+ [ "memory" , "MEMORY.md" ] ,
557+ ] ) ;
558+ } finally {
559+ vi . useRealTimers ( ) ;
560+ }
561+ } ) ;
562+
563+ it ( "records the memory cooldown when corpus=all memory and wiki both fail (#92633)" , async ( ) => {
564+ // Concurrency must not let a wiki failure mask a real memory backend failure:
565+ // when both branches throw, the memory cooldown still has to be recorded so a
566+ // broken embedding provider is suppressed rather than re-hit on the next turn.
567+ let searchCalls = 0 ;
568+ setMemorySearchImpl ( async ( ) => {
569+ searchCalls += 1 ;
570+ throw new Error ( "openai embeddings failed: 500 server_error" ) ;
571+ } ) ;
572+ registerMemoryCorpusSupplement ( "memory-wiki" , {
573+ search : async ( ) => {
574+ throw new Error ( "wiki supplement unavailable" ) ;
575+ } ,
576+ get : async ( ) => null ,
577+ } ) ;
578+
579+ const tool = createMemorySearchToolOrThrow ( ) ;
580+ const bothFail = await tool . execute ( "call_all_both_fail" , {
581+ query : "alpha" ,
582+ corpus : "all" ,
583+ } ) ;
584+ expectUnavailableMemorySearchDetails ( bothFail . details , {
585+ error : "openai embeddings failed: 500 server_error" ,
586+ warning : "Memory search is unavailable due to an embedding/provider error." ,
587+ action : "Check embedding provider configuration and retry memory_search." ,
588+ } ) ;
589+
590+ // The next memory search is suppressed by the cooldown (no second backend hit).
591+ const followup = await tool . execute ( "call_memory_after_both_fail" , { query : "alpha" } ) ;
592+ expect ( ( followup . details as { disabled ?: boolean } ) . disabled ) . toBe ( true ) ;
593+ expect ( searchCalls ) . toBe ( 1 ) ;
594+ } ) ;
595+
498596 it ( "does not cooldown primary memory when a corpus=all wiki supplement stalls" , async ( ) => {
499597 vi . useFakeTimers ( ) ;
500598 try {
0 commit comments