@@ -2,6 +2,7 @@ import { mkdirSync, rmSync } from "node:fs";
22import fs from "node:fs/promises" ;
33import os from "node:os" ;
44import path from "node:path" ;
5+ import { resolveSessionTranscriptsDirForAgent } from "openclaw/plugin-sdk/memory-core" ;
56import { afterAll , afterEach , beforeAll , beforeEach , describe , expect , it , vi } from "vitest" ;
67import {
78 clearMemoryEmbeddingProviders as clearRegistry ,
@@ -377,4 +378,123 @@ describe("memory index", () => {
377378 const noResults = await manager . search ( "nonexistent_xyz_keyword" ) ;
378379 expect ( noResults . length ) . toBe ( 0 ) ;
379380 } ) ;
381+
382+ it ( "prefers exact session transcript hits in FTS-only mode" , async ( ) => {
383+ forceNoProvider = true ;
384+ const stateDir = path . join ( workspaceDir , ".state-session-ranking" ) ;
385+ vi . stubEnv ( "OPENCLAW_STATE_DIR" , stateDir ) ;
386+ try {
387+ const cfg = createCfg ( {
388+ storePath : path . join ( workspaceDir , "index-fts-session-ranking.sqlite" ) ,
389+ sources : [ "memory" , "sessions" ] ,
390+ sessionMemory : true ,
391+ minScore : 0 ,
392+ hybrid : { enabled : true , vectorWeight : 0.7 , textWeight : 0.3 } ,
393+ } ) ;
394+ const result = await getMemorySearchManager ( { cfg, agentId : "main" } ) ;
395+ const manager = requireManager ( result ) ;
396+ managersForCleanup . add ( manager ) ;
397+ resetManagerForTest ( manager ) ;
398+
399+ const memoryPath = path . join ( workspaceDir , "MEMORY.md" ) ;
400+ await fs . writeFile ( memoryPath , "Project Nebula stale codename: ORBIT-9.\n" , "utf8" ) ;
401+ const staleAt = new Date ( "2020-01-01T00:00:00.000Z" ) ;
402+ await fs . utimes ( memoryPath , staleAt , staleAt ) ;
403+
404+ const sessionsDir = resolveSessionTranscriptsDirForAgent ( "main" ) ;
405+ await fs . mkdir ( sessionsDir , { recursive : true } ) ;
406+ const transcriptPath = path . join ( sessionsDir , "session-ranking.jsonl" ) ;
407+ const now = Date . parse ( "2026-04-07T15:25:04.113Z" ) ;
408+ await fs . writeFile (
409+ transcriptPath ,
410+ [
411+ JSON . stringify ( {
412+ type : "session" ,
413+ id : "session-ranking" ,
414+ timestamp : new Date ( now - 60_000 ) . toISOString ( ) ,
415+ } ) ,
416+ JSON . stringify ( {
417+ type : "message" ,
418+ message : {
419+ role : "user" ,
420+ timestamp : new Date ( now - 30_000 ) . toISOString ( ) ,
421+ content : [ { type : "text" , text : "What is the current Project Nebula codename?" } ] ,
422+ } ,
423+ } ) ,
424+ JSON . stringify ( {
425+ type : "message" ,
426+ message : {
427+ role : "assistant" ,
428+ timestamp : new Date ( now ) . toISOString ( ) ,
429+ content : [ { type : "text" , text : "The current Project Nebula codename is ORBIT-10." } ] ,
430+ } ,
431+ } ) ,
432+ ] . join ( "\n" ) + "\n" ,
433+ "utf8" ,
434+ ) ;
435+
436+ await manager . sync ( { reason : "test" , force : true } ) ;
437+ const results = await manager . search ( "current Project Nebula codename ORBIT-10" , {
438+ minScore : 0 ,
439+ maxResults : 3 ,
440+ } ) ;
441+
442+ expect ( results [ 0 ] ?. source ) . toBe ( "sessions" ) ;
443+ expect ( results [ 0 ] ?. snippet ) . toContain ( "ORBIT-10" ) ;
444+ } finally {
445+ vi . unstubAllEnvs ( ) ;
446+ }
447+ } ) ;
448+
449+ it ( "bootstraps an empty index on first search so session transcript hits are available" , async ( ) => {
450+ forceNoProvider = true ;
451+ const stateDir = path . join ( workspaceDir , ".state-session-bootstrap" ) ;
452+ vi . stubEnv ( "OPENCLAW_STATE_DIR" , stateDir ) ;
453+ try {
454+ const cfg = createCfg ( {
455+ storePath : path . join ( workspaceDir , "index-fts-session-bootstrap.sqlite" ) ,
456+ sources : [ "memory" , "sessions" ] ,
457+ sessionMemory : true ,
458+ minScore : 0 ,
459+ hybrid : { enabled : true , vectorWeight : 0.7 , textWeight : 0.3 } ,
460+ } ) ;
461+ const result = await getMemorySearchManager ( { cfg, agentId : "main" } ) ;
462+ const manager = requireManager ( result ) ;
463+ managersForCleanup . add ( manager ) ;
464+ resetManagerForTest ( manager ) ;
465+
466+ const sessionsDir = resolveSessionTranscriptsDirForAgent ( "main" ) ;
467+ await fs . mkdir ( sessionsDir , { recursive : true } ) ;
468+ const transcriptPath = path . join ( sessionsDir , "session-bootstrap.jsonl" ) ;
469+ await fs . writeFile (
470+ transcriptPath ,
471+ [
472+ JSON . stringify ( {
473+ type : "session" ,
474+ id : "session-bootstrap" ,
475+ timestamp : "2026-04-07T15:24:04.113Z" ,
476+ } ) ,
477+ JSON . stringify ( {
478+ type : "message" ,
479+ message : {
480+ role : "assistant" ,
481+ timestamp : "2026-04-07T15:25:04.113Z" ,
482+ content : [ { type : "text" , text : "The current Project Nebula codename is ORBIT-10." } ] ,
483+ } ,
484+ } ) ,
485+ ] . join ( "\n" ) + "\n" ,
486+ "utf8" ,
487+ ) ;
488+
489+ const results = await manager . search ( "current Project Nebula codename ORBIT-10" , {
490+ minScore : 0 ,
491+ maxResults : 3 ,
492+ } ) ;
493+
494+ expect ( results [ 0 ] ?. source ) . toBe ( "sessions" ) ;
495+ expect ( results [ 0 ] ?. snippet ) . toContain ( "ORBIT-10" ) ;
496+ } finally {
497+ vi . unstubAllEnvs ( ) ;
498+ }
499+ } ) ;
380500} ) ;
0 commit comments