@@ -212,6 +212,7 @@ import {
212212} from "../dreaming-state.js" ;
213213import { resolveQmdSessionArtifactIdentity } from "../qmd-session-artifacts.js" ;
214214import { QmdMemoryManager , resolveQmdMcporterSearchProcessTimeoutMs } from "./qmd-manager.js" ;
215+ import { MEMORY_SEARCH_DEADLINE_CONTROL } from "./search-deadline.js" ;
215216
216217const spawnMock = mockedSpawn as unknown as Mock ;
217218const originalPath = process . env . PATH ;
@@ -292,117 +293,6 @@ describe("QmdMemoryManager", () => {
292293 expect ( mockMessages ( mock ) . join ( "\n" ) ) . not . toContain ( text ) ;
293294 }
294295
295- function createClosableQmdManagerForTest ( params ?: {
296- mode ?: "full" | "status" | "cli" ;
297- pendingUpdate ?: Promise < void > | null ;
298- queuedForcedUpdate ?: Promise < void > | null ;
299- } ) : QmdMemoryManager {
300- const manager = Object . create ( QmdMemoryManager . prototype ) as QmdMemoryManager ;
301- const mutable = manager as unknown as {
302- closed : boolean ;
303- resolveCloseSignal : ( ) => void ;
304- updateTimer : NodeJS . Timeout | null ;
305- embedTimer : NodeJS . Timeout | null ;
306- watchTimer : NodeJS . Timeout | null ;
307- watcher : { close : ( ) => Promise < void > } | null ;
308- queuedForcedRuns : number ;
309- pendingUpdate : Promise < void > | null ;
310- queuedForcedUpdate : Promise < void > | null ;
311- mode : "full" | "status" | "cli" ;
312- db : { close : ( ) => void } | null ;
313- } ;
314- mutable . closed = false ;
315- mutable . resolveCloseSignal = vi . fn ( ) ;
316- mutable . updateTimer = null ;
317- mutable . embedTimer = null ;
318- mutable . watchTimer = null ;
319- mutable . watcher = null ;
320- mutable . queuedForcedRuns = 0 ;
321- mutable . pendingUpdate = params ?. pendingUpdate ?? null ;
322- mutable . queuedForcedUpdate = params ?. queuedForcedUpdate ?? null ;
323- mutable . mode = params ?. mode ?? "cli" ;
324- mutable . db = null ;
325- return manager ;
326- }
327-
328- it ( "clears qmd CLI close timeout timers when update waits settle first" , async ( ) => {
329- vi . useFakeTimers ( ) ;
330- try {
331- const manager = createClosableQmdManagerForTest ( {
332- pendingUpdate : Promise . resolve ( ) ,
333- queuedForcedUpdate : Promise . resolve ( ) ,
334- } ) ;
335-
336- await manager . close ( ) ;
337- await vi . advanceTimersByTimeAsync ( 5_000 ) ;
338-
339- expectMockMessageNotContains ( logWarnMock , "qmd close timed out" ) ;
340- expect ( vi . getTimerCount ( ) ) . toBe ( 0 ) ;
341- } finally {
342- vi . useRealTimers ( ) ;
343- }
344- } ) ;
345-
346- it ( "uses one qmd CLI close timeout across pending and queued update waits" , async ( ) => {
347- vi . useFakeTimers ( ) ;
348- try {
349- const manager = createClosableQmdManagerForTest ( {
350- pendingUpdate : new Promise ( ( ) => { } ) ,
351- queuedForcedUpdate : new Promise ( ( ) => { } ) ,
352- } ) ;
353- let closed = false ;
354-
355- const closePromise = manager . close ( ) . then ( ( ) => {
356- closed = true ;
357- } ) ;
358-
359- await vi . advanceTimersByTimeAsync ( 4_999 ) ;
360- await Promise . resolve ( ) ;
361- expect ( closed ) . toBe ( false ) ;
362-
363- await vi . advanceTimersByTimeAsync ( 1 ) ;
364- await closePromise ;
365-
366- expect ( closed ) . toBe ( true ) ;
367- expectMockMessageContains (
368- logWarnMock ,
369- "qmd close timed out waiting for pending update and queued forced update after 5000ms" ,
370- ) ;
371- } finally {
372- vi . useRealTimers ( ) ;
373- }
374- } ) ;
375-
376- it ( "waits for pending update cleanup without timeout for full qmd managers" , async ( ) => {
377- vi . useFakeTimers ( ) ;
378- try {
379- let resolvePending ! : ( ) => void ;
380- const manager = createClosableQmdManagerForTest ( {
381- mode : "full" ,
382- pendingUpdate : new Promise < void > ( ( resolve ) => {
383- resolvePending = resolve ;
384- } ) ,
385- } ) ;
386- let closed = false ;
387-
388- const closePromise = manager . close ( ) . then ( ( ) => {
389- closed = true ;
390- } ) ;
391-
392- await vi . advanceTimersByTimeAsync ( 5_000 ) ;
393- await Promise . resolve ( ) ;
394- expect ( closed ) . toBe ( false ) ;
395-
396- resolvePending ( ) ;
397- await closePromise ;
398-
399- expect ( closed ) . toBe ( true ) ;
400- expectMockMessageNotContains ( logWarnMock , "qmd close timed out" ) ;
401- } finally {
402- vi . useRealTimers ( ) ;
403- }
404- } ) ;
405-
406296 it ( "caps mcporter search process timeout grace" , ( ) => {
407297 expect ( resolveQmdMcporterSearchProcessTimeoutMs ( 1_000 ) ) . toBe ( 5_000 ) ;
408298 expect ( resolveQmdMcporterSearchProcessTimeoutMs ( 10_000 ) ) . toBe ( 12_000 ) ;
@@ -529,7 +419,9 @@ describe("QmdMemoryManager", () => {
529419 const { manager } = await createManager ( ) ;
530420 spawnMock . mockClear ( ) ;
531421 let searchAttempts = 0 ;
422+ const events : string [ ] = [ ] ;
532423 spawnMock . mockImplementation ( ( _cmd : string , args : string [ ] ) => {
424+ events . push ( `command:${ args [ 0 ] } ${ args [ 1 ] ? `:${ args [ 1 ] } ` : "" } ` ) ;
533425 if ( args [ 0 ] === "query" || args [ 0 ] === "search" || args [ 0 ] === "vsearch" ) {
534426 const child = createMockChild ( { autoClose : false } ) ;
535427 searchAttempts += 1 ;
@@ -549,11 +441,38 @@ describe("QmdMemoryManager", () => {
549441 onDebug : ( entry ) => {
550442 debug . push ( entry ) ;
551443 } ,
444+ [ MEMORY_SEARCH_DEADLINE_CONTROL ] : ( action ) => {
445+ events . push ( `phase:${ action } ` ) ;
446+ } ,
552447 } ) ;
553448
554449 expect ( searchAttempts ) . toBe ( 2 ) ;
555450 expect ( countQmdCommand ( ( args ) => args [ 0 ] === "collection" && args [ 1 ] === "list" ) ) . toBe ( 1 ) ;
556451 expect ( debug . at ( - 1 ) ?. qmd ?. collectionValidation ?. cacheState ) . toBe ( "bypass-force" ) ;
452+ expect ( events . filter ( ( event ) => event . startsWith ( "phase:" ) ) ) . toEqual ( [
453+ "phase:pause" ,
454+ "phase:resume" ,
455+ "phase:pause" ,
456+ "phase:resume" ,
457+ ] ) ;
458+ const isSearchCommand = ( event : string ) =>
459+ [ "command:query:" , "command:search:" , "command:vsearch:" ] . some ( ( prefix ) =>
460+ event . startsWith ( prefix ) ,
461+ ) ;
462+ const firstSearch = events . findIndex ( isSearchCommand ) ;
463+ const firstSearchEnd = events . indexOf ( "phase:resume" ) ;
464+ const collectionRepair = events . findIndex (
465+ ( event , index ) => index > firstSearchEnd && event . startsWith ( "command:collection:" ) ,
466+ ) ;
467+ const retryStart = events . indexOf ( "phase:pause" , firstSearchEnd + 1 ) ;
468+ const retrySearch = events . findIndex (
469+ ( event , index ) => index > firstSearch && isSearchCommand ( event ) ,
470+ ) ;
471+ expect ( events . indexOf ( "phase:pause" ) ) . toBeLessThan ( firstSearch ) ;
472+ expect ( firstSearch ) . toBeLessThan ( firstSearchEnd ) ;
473+ expect ( firstSearchEnd ) . toBeLessThan ( collectionRepair ) ;
474+ expect ( collectionRepair ) . toBeLessThan ( retryStart ) ;
475+ expect ( retryStart ) . toBeLessThan ( retrySearch ) ;
557476 } ) ;
558477
559478 it ( "reuses persisted qmd multi-collection support probe across managers" , async ( ) => {
@@ -4107,9 +4026,11 @@ describe("QmdMemoryManager", () => {
41074026 } ,
41084027 } as OpenClawConfig ;
41094028
4029+ const commandPhases : string [ ] = [ ] ;
41104030 spawnMock . mockImplementation ( ( cmd : string , args : string [ ] ) => {
41114031 const child = createMockChild ( { autoClose : false } ) ;
41124032 if ( isMcporterCommand ( cmd ) && args [ 0 ] === "call" ) {
4033+ expect ( commandPhases ) . toEqual ( [ "pause" ] ) ;
41134034 // Verify it calls qmd.query (v2) not qmd.deep_search (v1)
41144035 expect ( args [ 1 ] ) . toBe ( "qmd.query" ) ;
41154036 const callArgs = JSON . parse ( args [ args . indexOf ( "--args" ) + 1 ] ) ;
@@ -4134,7 +4055,13 @@ describe("QmdMemoryManager", () => {
41344055 } ) ;
41354056
41364057 const { manager } = await createManager ( ) ;
4137- await manager . search ( "hello" , { sessionKey : "agent:main:slack:dm:u123" } ) ;
4058+ await manager . search ( "hello" , {
4059+ sessionKey : "agent:main:slack:dm:u123" ,
4060+ [ MEMORY_SEARCH_DEADLINE_CONTROL ] : ( action ) => {
4061+ commandPhases . push ( action ) ;
4062+ } ,
4063+ } ) ;
4064+ expect ( commandPhases ) . toEqual ( [ "pause" , "resume" ] ) ;
41384065 await manager . close ( ) ;
41394066 } ) ;
41404067
0 commit comments