cortico-world-memory 0.1.8 → 0.1.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cortico-world-memory",
3
- "version": "0.1.8",
3
+ "version": "0.1.9",
4
4
  "type": "module",
5
5
  "description": "Cortico World 扩展:五层记忆(事实库/知识库/反思/人格/档案)+ 三级记忆底座 + Hybrid Recall 向量混合检索(移植自 E:/qq_bot 的记忆系统与 memory_server 向量 sidecar,qq_bot 源码零改动)。",
6
6
  "license": "MIT",
package/src/backend.ts CHANGED
@@ -10,7 +10,7 @@
10
10
  * SQLite 驱动优先级:先试 `better-sqlite3`(原生,若已安装),缺失则回退 Node 内置
11
11
  * `node:sqlite`(v24+ 自带,零安装)。两者 API 兼容(.prepare().get()/.run()、.exec())。
12
12
  */
13
- import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from 'node:fs';
13
+ import { existsSync, mkdirSync, readFileSync, renameSync, statSync, writeFileSync } from 'node:fs';
14
14
  import { dirname, join } from 'node:path';
15
15
 
16
16
  export type BackendKind = 'json' | 'sqlite';
@@ -164,3 +164,69 @@ export async function createBackend(kind: BackendKind, dataDir: string, log?: Lo
164
164
  log?.('[memory] 存储后端=json');
165
165
  return new JsonBackend(dataDir);
166
166
  }
167
+
168
+ /**
169
+ * 只读打开 memory.db,导出 KV 行为 Map<stem, obj>。供「切回 JSON」时回填用。
170
+ * 优先 better-sqlite3,缺失则回退 Node 内置 node:sqlite(本环境走这条)。
171
+ */
172
+ export async function readSqliteData(dataDir: string, stems: readonly string[]): Promise<Map<string, unknown>> {
173
+ const dbFile = join(dataDir, 'memory.db');
174
+ if (!existsSync(dbFile)) return new Map();
175
+ let db: any;
176
+ try {
177
+ const mod: any = await import('better-sqlite3');
178
+ const Database = mod.default ?? mod;
179
+ db = new Database(dbFile, { readonly: true });
180
+ } catch {
181
+ const { DatabaseSync } = await import('node:sqlite');
182
+ db = new DatabaseSync(dbFile, { readOnly: true });
183
+ }
184
+ const out = new Map<string, unknown>();
185
+ try {
186
+ let rows: any[];
187
+ if (stems.length) {
188
+ const ph = stems.map(() => '?').join(',');
189
+ rows = db.prepare(`SELECT name, data FROM kv WHERE name IN (${ph})`).all(...stems);
190
+ } else {
191
+ rows = db.prepare('SELECT name, data FROM kv').all();
192
+ }
193
+ for (const r of rows) {
194
+ try { out.set(r.name, JSON.parse(r.data)); } catch { /* 跳过损坏行 */ }
195
+ }
196
+ } finally {
197
+ try { db.close(); } catch { /* ignore */ }
198
+ }
199
+ return out;
200
+ }
201
+
202
+ /**
203
+ * 切回 JSON 时,若 memory.db 存在且其中数据比对应 JSON 文件更新,回填到 JSON 文件,
204
+ * 消除 json↔sqlite 切换的数据不对称(此前 sqlite→json 会丢 SQLite 期间的写入)。
205
+ * 返回回填的 store 数量。仅当 JSON 缺文件或比 DB 更旧时才覆盖,避免冲掉用户在 JSON 上的后续手动编辑。
206
+ */
207
+ export async function backfillSqliteToJson(dataDir: string, log?: LogFn): Promise<number> {
208
+ const dbFile = join(dataDir, 'memory.db');
209
+ if (!existsSync(dbFile)) return 0;
210
+ let fromSqlite: Map<string, unknown>;
211
+ try {
212
+ fromSqlite = await readSqliteData(dataDir, ALL_STEMS);
213
+ } catch (e: any) {
214
+ log?.(`[memory] 读 SQLite 失败,跳过回填: ${e?.message ?? e}`);
215
+ return 0;
216
+ }
217
+ if (!fromSqlite.size) return 0;
218
+ const json = new JsonBackend(dataDir);
219
+ let filled = 0;
220
+ const dbMtime = statSync(dbFile).mtimeMs;
221
+ for (const stem of ALL_STEMS) {
222
+ if (!fromSqlite.has(stem)) continue;
223
+ const jsonFile = join(dataDir, stem + '.json');
224
+ const jsonMtime = existsSync(jsonFile) ? statSync(jsonFile).mtimeMs : 0;
225
+ if (jsonMtime === 0 || dbMtime > jsonMtime) {
226
+ json.writeJson(stem, fromSqlite.get(stem));
227
+ filled++;
228
+ }
229
+ }
230
+ if (filled) log?.(`[memory] 已从 SQLite 回填 ${filled} 个 store 到 JSON(不丢切换期间写入)`);
231
+ return filled;
232
+ }
package/src/config.ts CHANGED
@@ -98,7 +98,7 @@ export const MEMORY_CONFIG_GROUP: ConfigGroup = {
98
98
  'worlds.memory.embeddingTimeoutMs': { type: 'number', title: 'Sidecar 超时(ms)', description: 'qq_bot 向量模型首载慢,默认 3 分钟。' },
99
99
  'worlds.memory.storageBackend': {
100
100
  type: 'string', title: '存储后端', enum: ['json', 'sqlite'], 'x-hot': false,
101
- description: 'json=原生 JSON 文件(默认);sqlite=单库 KV(自动 better-sqlite3→node:sqlite 回退)。切 sqlite 首次自动迁移旧 JSON。改后须点「重启」生效。',
101
+ description: 'json=原生 JSON 文件(默认);sqlite=单库 KV(自动 better-sqlite3→node:sqlite 回退)。切 sqlite 首次自动迁移旧 JSON;切回 json 若 memory.db 数据更新会自动回填,不丢切换期间写入。改后须点「重启」生效。',
102
102
  },
103
103
  },
104
104
  },
package/src/world.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { readFileSync } from 'node:fs';
1
+ import { readFileSync, statSync } from 'node:fs';
2
2
  import { fileURLToPath } from 'node:url';
3
3
  import type { World, WorldHost, ToolDef, ToolTag, EventEnvelope } from 'cortico/core/types.ts';
4
4
  import type { WorldContext } from 'cortico/world.ts';
@@ -12,7 +12,7 @@ import { KnowledgeStore } from './knowledge-store.ts';
12
12
  import { ReflectionStore } from './reflection-store.ts';
13
13
  import { PersonaStore } from './persona-store.ts';
14
14
  import { ProfileStore } from './profile-store.ts';
15
- import { createBackend, StoreBackend, ALL_STEMS } from './backend.ts';
15
+ import { createBackend, StoreBackend, ALL_STEMS, backfillSqliteToJson } from './backend.ts';
16
16
  import { buildEventText, buildView, promptVars } from './observe.ts';
17
17
 
18
18
  const ENV_PROMPT_FILE = fileURLToPath(new URL('./ENV_PROMPT.md', import.meta.url));
@@ -126,8 +126,13 @@ export class MemoryWorld implements World {
126
126
  this.host = host;
127
127
  const ctxData = this.ctx.dataDir;
128
128
  const dataDir = typeof ctxData === 'string' && ctxData ? ctxData : '.';
129
- this.backend = await createBackend(this.cfg.storageBackend ?? 'json', dataDir, (m) => host.log.info(m));
130
- this.backend.migrateFromJson(ALL_STEMS); // 首次切 sqlite 自动迁移旧 JSON
129
+ const kind = this.cfg.storageBackend ?? 'json';
130
+ this.backend = await createBackend(kind, dataDir, (m) => host.log.info(m));
131
+ if (kind === 'sqlite') {
132
+ this.backend.migrateFromJson(ALL_STEMS); // json→sqlite:首次自动迁移旧 JSON
133
+ } else {
134
+ await backfillSqliteToJson(dataDir, (m) => host.log.info(m)); // sqlite→json:回填较新数据,消除切换丢写
135
+ }
131
136
  this.mem = new MemoryBase(dataDir, this.cfg.maxHistory, this.backend);
132
137
  this.vectors = new VectorStore(dataDir, this.backend);
133
138
  this.facts = new FactStore(dataDir, this.backend);