11// Memory Core plugin module implements manager db behavior.
2+ import fs from "node:fs" ;
23import path from "node:path" ;
34import type { DatabaseSync } from "node:sqlite" ;
45import {
@@ -7,7 +8,117 @@ import {
78 ensureDir ,
89 requireNodeSqlite ,
910} from "openclaw/plugin-sdk/memory-core-host-engine-storage" ;
10- import { ensureOpenClawAgentDatabaseSchema } from "openclaw/plugin-sdk/sqlite-runtime" ;
11+ import {
12+ ensureOpenClawAgentDatabaseSchema ,
13+ runSqliteImmediateTransactionSync ,
14+ } from "openclaw/plugin-sdk/sqlite-runtime" ;
15+
16+ const MEMORY_REINDEX_SCHEMA = "memory_reindex" ;
17+ const MEMORY_DATABASE_FILE_SUFFIXES = [ "" , "-wal" , "-shm" , "-journal" ] as const ;
18+
19+ function tableExists ( db : DatabaseSync , schema : string , tableName : string ) : boolean {
20+ const row = db
21+ . prepare ( `SELECT 1 AS ok FROM ${ schema } .sqlite_master WHERE type = 'table' AND name = ?` )
22+ . get ( tableName ) as { ok ?: unknown } | undefined ;
23+ return row ?. ok === 1 ;
24+ }
25+
26+ function readTableSql ( db : DatabaseSync , schema : string , tableName : string ) : string | null {
27+ const row = db
28+ . prepare ( `SELECT sql FROM ${ schema } .sqlite_master WHERE type = 'table' AND name = ?` )
29+ . get ( tableName ) as { sql ?: unknown } | undefined ;
30+ return typeof row ?. sql === "string" && row . sql . trim ( ) ? row . sql : null ;
31+ }
32+
33+ function replaceVirtualTable ( params : {
34+ db : DatabaseSync ;
35+ tableName : "chunks_fts" | "chunks_vec" ;
36+ columns : string ;
37+ dropWhenSourceMissing ?: boolean ;
38+ } ) : void {
39+ const { db, tableName, columns } = params ;
40+ const createSql = readTableSql ( db , MEMORY_REINDEX_SCHEMA , tableName ) ;
41+ if ( ! createSql ) {
42+ if ( params . dropWhenSourceMissing !== false ) {
43+ db . exec ( `DROP TABLE IF EXISTS main.${ tableName } ` ) ;
44+ }
45+ return ;
46+ }
47+ db . exec ( `DROP TABLE IF EXISTS main.${ tableName } ` ) ;
48+ db . exec ( createSql ) ;
49+ db . exec (
50+ `INSERT INTO main.${ tableName } (${ columns } ) ` +
51+ `SELECT ${ columns } FROM ${ MEMORY_REINDEX_SCHEMA } .${ tableName } ` ,
52+ ) ;
53+ }
54+
55+ /** Publish a completed shadow memory index without replacing the shared agent database file. */
56+ export function publishMemoryDatabaseTables ( params : {
57+ targetDb : DatabaseSync ;
58+ sourcePath : string ;
59+ metaKey : string ;
60+ } ) : void {
61+ params . targetDb . prepare ( `ATTACH DATABASE ? AS ${ MEMORY_REINDEX_SCHEMA } ` ) . run ( params . sourcePath ) ;
62+ try {
63+ runSqliteImmediateTransactionSync ( params . targetDb , ( ) => {
64+ params . targetDb . prepare ( "DELETE FROM main.meta WHERE key = ?" ) . run ( params . metaKey ) ;
65+ params . targetDb
66+ . prepare (
67+ `INSERT INTO main.meta (key, value)
68+ SELECT key, value FROM ${ MEMORY_REINDEX_SCHEMA } .meta WHERE key = ?` ,
69+ )
70+ . run ( params . metaKey ) ;
71+
72+ params . targetDb . exec ( `
73+ DELETE FROM main.files;
74+ INSERT INTO main.files (path, source, hash, mtime, size)
75+ SELECT path, source, hash, mtime, size FROM ${ MEMORY_REINDEX_SCHEMA } .files;
76+
77+ DELETE FROM main.chunks;
78+ INSERT INTO main.chunks (
79+ id, path, source, start_line, end_line, hash, model, text, embedding, updated_at
80+ )
81+ SELECT
82+ id, path, source, start_line, end_line, hash, model, text, embedding, updated_at
83+ FROM ${ MEMORY_REINDEX_SCHEMA } .chunks;
84+ ` ) ;
85+
86+ if ( tableExists ( params . targetDb , MEMORY_REINDEX_SCHEMA , "embedding_cache" ) ) {
87+ params . targetDb . exec ( `
88+ DELETE FROM main.embedding_cache;
89+ INSERT INTO main.embedding_cache (
90+ provider, model, provider_key, hash, embedding, dims, updated_at
91+ )
92+ SELECT provider, model, provider_key, hash, embedding, dims, updated_at
93+ FROM ${ MEMORY_REINDEX_SCHEMA } .embedding_cache;
94+ ` ) ;
95+ }
96+
97+ replaceVirtualTable ( {
98+ db : params . targetDb ,
99+ tableName : "chunks_fts" ,
100+ columns : "text, id, path, source, model, start_line, end_line" ,
101+ } ) ;
102+ replaceVirtualTable ( {
103+ db : params . targetDb ,
104+ tableName : "chunks_vec" ,
105+ columns : "id, embedding" ,
106+ // A vector-disabled connection may not have sqlite-vec loaded and cannot
107+ // drop an old virtual table. It is unused and can remain until vec loads.
108+ dropWhenSourceMissing : false ,
109+ } ) ;
110+ } ) ;
111+ } finally {
112+ params . targetDb . exec ( `DETACH DATABASE ${ MEMORY_REINDEX_SCHEMA } ` ) ;
113+ }
114+ }
115+
116+ /** Remove one closed shadow memory database and its journal-mode sidecars. */
117+ export function removeMemoryDatabaseFiles ( dbPath : string ) : void {
118+ for ( const suffix of MEMORY_DATABASE_FILE_SUFFIXES ) {
119+ fs . rmSync ( `${ dbPath } ${ suffix } ` , { force : true } ) ;
120+ }
121+ }
11122
12123export function openMemoryDatabaseAtPath (
13124 dbPath : string ,
0 commit comments