@@ -493,6 +493,7 @@ describe("QmdMemoryManager", () => {
493493 const initialUpdateCalls = spawnMock . mock . calls . filter ( ( call ) => call [ 1 ] ?. [ 0 ] === "update" ) ;
494494 expect ( initialUpdateCalls ) . toHaveLength ( 0 ) ;
495495 const watchOptions = firstWatchOptions ( ) ;
496+ expect ( watchOptions ) . not . toHaveProperty ( "awaitWriteFinish" ) ;
496497 expect ( watchOptions . ignored ?.( path . join ( workspaceDir , "node_modules" , "pkg" , "note.md" ) ) ) . toBe (
497498 true ,
498499 ) ;
@@ -502,7 +503,14 @@ describe("QmdMemoryManager", () => {
502503 expect ( watchOptions . ignored ?.( path . join ( workspaceDir , "build" , "note.md" ) ) ) . toBe ( true ) ;
503504 expect ( watchOptions . ignored ?.( path . join ( workspaceDir , "notes.md" ) ) ) . toBe ( false ) ;
504505
505- watcher . emit ( "change" , path . join ( workspaceDir , "notes.md" ) ) ;
506+ const notesPath = path . join ( workspaceDir , "notes.md" ) ;
507+ await fs . writeFile ( notesPath , "hello" ) ;
508+ const initialStats = await fs . stat ( notesPath ) ;
509+ watcher . emit ( "change" , notesPath , {
510+ size : initialStats . size ,
511+ mtimeMs : initialStats . mtimeMs ,
512+ isDirectory : ( ) => false ,
513+ } ) ;
506514 expect ( manager . status ( ) . dirty ) . toBe ( true ) ;
507515
508516 await vi . advanceTimersByTimeAsync ( 25 ) ;
@@ -514,6 +522,55 @@ describe("QmdMemoryManager", () => {
514522 await manager . close ( ) ;
515523 } ) ;
516524
525+ it ( "delays qmd watch sync until changed file stats settle" , async ( ) => {
526+ vi . useFakeTimers ( ) ;
527+ cfg = {
528+ agents : {
529+ defaults : {
530+ workspace : workspaceDir ,
531+ memorySearch : {
532+ provider : "openai" ,
533+ model : "mock-embed" ,
534+ store : { path : path . join ( workspaceDir , "index.sqlite" ) , vector : { enabled : false } } ,
535+ sync : { watch : true , watchDebounceMs : 25 , onSessionStart : false , onSearch : false } ,
536+ } ,
537+ } ,
538+ list : [ { id : agentId , default : true , workspace : workspaceDir } ] ,
539+ } ,
540+ memory : {
541+ backend : "qmd" ,
542+ qmd : {
543+ includeDefaultMemory : false ,
544+ update : { interval : "0s" , debounceMs : 0 , onBoot : false } ,
545+ paths : [ { path : workspaceDir , pattern : "**/*.md" , name : "workspace" } ] ,
546+ } ,
547+ } ,
548+ } as OpenClawConfig ;
549+
550+ const notesPath = path . join ( workspaceDir , "notes.md" ) ;
551+ await fs . writeFile ( notesPath , "hello" ) ;
552+ const initialStats = await fs . stat ( notesPath ) ;
553+ const { manager } = await createManager ( { mode : "full" } ) ;
554+ const watcher = watchMock . mock . results [ 0 ] ?. value as {
555+ emit : ( event : string , ...args : unknown [ ] ) => boolean ;
556+ } ;
557+
558+ watcher . emit ( "change" , notesPath , {
559+ size : initialStats . size ,
560+ mtimeMs : initialStats . mtimeMs ,
561+ isDirectory : ( ) => false ,
562+ } ) ;
563+ await fs . writeFile ( notesPath , "hello updated" ) ;
564+
565+ await vi . advanceTimersByTimeAsync ( 25 ) ;
566+ expect ( spawnMock . mock . calls . filter ( ( call ) => call [ 1 ] ?. [ 0 ] === "update" ) ) . toHaveLength ( 0 ) ;
567+
568+ await vi . advanceTimersByTimeAsync ( 25 ) ;
569+ expect ( spawnMock . mock . calls . filter ( ( call ) => call [ 1 ] ?. [ 0 ] === "update" ) ) . toHaveLength ( 1 ) ;
570+
571+ await manager . close ( ) ;
572+ } ) ;
573+
517574 it ( "runs boot update in background by default" , async ( ) => {
518575 cfg = {
519576 ...cfg ,
0 commit comments