@@ -549,12 +549,13 @@ describe("memory index", () => {
549549 }
550550 } ) ;
551551
552- it ( "does not search stale rows when index metadata is missing " , async ( ) => {
552+ it ( "rebuilds missing metadata with existing chunks on gateway sync " , async ( ) => {
553553 const dbPath = path . join ( workspaceDir , "index-missing-meta-cutover.sqlite" ) ;
554554 const cfg = createCfg ( {
555555 storePath : dbPath ,
556556 hybrid : { enabled : true , vectorWeight : 0.5 , textWeight : 0.5 } ,
557557 } ) ;
558+ await fs . writeFile ( path . join ( memoryDir , "2026-01-13.md" ) , "# Log\nBeta memory line." ) ;
558559 const oldManager = await getFreshManager ( cfg ) ;
559560 await oldManager . sync ( { reason : "test" , force : true } ) ;
560561 await oldManager . close ?.( ) ;
@@ -580,6 +581,19 @@ describe("memory index", () => {
580581 status : "missing" ,
581582 reason : "index metadata is missing" ,
582583 } ) ;
584+
585+ vi . stubEnv ( "OPENCLAW_TEST_MEMORY_UNSAFE_REINDEX" , "0" ) ;
586+ await nextManager . sync ( { reason : "test" } ) ;
587+
588+ expect ( nextManager . status ( ) . dirty ) . toBe ( false ) ;
589+ expect ( nextManager . status ( ) . custom ?. indexIdentity ) . toEqual ( { status : "valid" } ) ;
590+ const repairedAlphaResults = await nextManager . search ( "alpha" ) ;
591+ expect (
592+ repairedAlphaResults . some ( ( result ) => result . path . endsWith ( "memory/2026-01-12.md" ) ) ,
593+ ) . toBe ( false ) ;
594+ const repairedResults = await nextManager . search ( "beta" ) ;
595+ expect ( repairedResults . length ) . toBeGreaterThan ( 0 ) ;
596+ expect ( repairedResults [ 0 ] ?. path ) . toContain ( "memory/2026-01-13.md" ) ;
583597 } finally {
584598 await nextManager . close ?.( ) ;
585599 }
@@ -611,6 +625,46 @@ describe("memory index", () => {
611625 }
612626 } ) ;
613627
628+ it ( "does not rebuild missing semantic metadata when embeddings are unavailable" , async ( ) => {
629+ const dbPath = path . join ( workspaceDir , "index-missing-meta-provider-unavailable.sqlite" ) ;
630+ const oldCfg = createCfg ( {
631+ storePath : dbPath ,
632+ model : "semantic-embed" ,
633+ hybrid : { enabled : true , vectorWeight : 0.5 , textWeight : 0.5 } ,
634+ } ) ;
635+ const oldManager = await getFreshManager ( oldCfg ) ;
636+ await oldManager . sync ( { reason : "test" , force : true } ) ;
637+ await oldManager . close ?.( ) ;
638+
639+ forceNoProvider = true ;
640+ const nextManager = await getFreshManager ( oldCfg ) ;
641+ try {
642+ const db = (
643+ nextManager as unknown as {
644+ db : {
645+ exec : ( sql : string ) => void ;
646+ prepare : ( sql : string ) => {
647+ get : ( ) => { model ?: string } | undefined ;
648+ } ;
649+ } ;
650+ }
651+ ) . db ;
652+ db . exec ( `DELETE FROM meta WHERE key = 'memory_index_meta_v1'` ) ;
653+
654+ await nextManager . sync ( { reason : "test" } ) ;
655+
656+ expect ( nextManager . status ( ) . dirty ) . toBe ( true ) ;
657+ expect ( nextManager . status ( ) . custom ?. indexIdentity ) . toEqual ( {
658+ status : "missing" ,
659+ reason : "index metadata is missing" ,
660+ } ) ;
661+ const row = db . prepare ( "SELECT model FROM chunks LIMIT 1" ) . get ( ) ;
662+ expect ( row ?. model ) . toBe ( "semantic-embed" ) ;
663+ } finally {
664+ await nextManager . close ?.( ) ;
665+ }
666+ } ) ;
667+
614668 it ( "clears dirty after sessions-only identity reindex" , async ( ) => {
615669 try {
616670 vi . stubEnv ( "OPENCLAW_STATE_DIR" , path . join ( workspaceDir , ".state-sessions-only-reindex" ) ) ;
0 commit comments