dsh-layered-memory 0.5.3

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.
Files changed (68) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +186 -0
  3. package/assets/readme/hero.svg +58 -0
  4. package/cordis.patch.yml +16 -0
  5. package/dist/client.js +1066 -0
  6. package/dist/config.d.ts +263 -0
  7. package/dist/config.js +65 -0
  8. package/dist/hooks/capture.d.ts +16 -0
  9. package/dist/hooks/capture.js +160 -0
  10. package/dist/hooks/recall.d.ts +31 -0
  11. package/dist/hooks/recall.js +201 -0
  12. package/dist/index.d.ts +190 -0
  13. package/dist/index.js +234 -0
  14. package/dist/llm.d.ts +32 -0
  15. package/dist/llm.js +128 -0
  16. package/dist/pipeline/l1.d.ts +19 -0
  17. package/dist/pipeline/l1.js +226 -0
  18. package/dist/pipeline/l2.d.ts +13 -0
  19. package/dist/pipeline/l2.js +77 -0
  20. package/dist/pipeline/l3.d.ts +15 -0
  21. package/dist/pipeline/l3.js +72 -0
  22. package/dist/pipeline/runner.d.ts +44 -0
  23. package/dist/pipeline/runner.js +132 -0
  24. package/dist/prompts/l1-dedup.d.ts +20 -0
  25. package/dist/prompts/l1-dedup.js +244 -0
  26. package/dist/prompts/l1-extraction.d.ts +18 -0
  27. package/dist/prompts/l1-extraction.js +434 -0
  28. package/dist/prompts/persona.d.ts +23 -0
  29. package/dist/prompts/persona.js +234 -0
  30. package/dist/prompts/scene.d.ts +33 -0
  31. package/dist/prompts/scene.js +408 -0
  32. package/dist/settings.d.ts +27 -0
  33. package/dist/settings.js +73 -0
  34. package/dist/stats.d.ts +46 -0
  35. package/dist/stats.js +234 -0
  36. package/dist/store/bm25.d.ts +19 -0
  37. package/dist/store/bm25.js +62 -0
  38. package/dist/store/embedding.d.ts +68 -0
  39. package/dist/store/embedding.js +133 -0
  40. package/dist/store/io.d.ts +13 -0
  41. package/dist/store/io.js +75 -0
  42. package/dist/store/l0.d.ts +28 -0
  43. package/dist/store/l0.js +148 -0
  44. package/dist/store/l1.d.ts +70 -0
  45. package/dist/store/l1.js +239 -0
  46. package/dist/store/persona.d.ts +15 -0
  47. package/dist/store/persona.js +60 -0
  48. package/dist/store/scenes.d.ts +23 -0
  49. package/dist/store/scenes.js +150 -0
  50. package/dist/store/search-utils.d.ts +22 -0
  51. package/dist/store/search-utils.js +71 -0
  52. package/dist/store/session-modes.d.ts +24 -0
  53. package/dist/store/session-modes.js +102 -0
  54. package/dist/store/sqlite.d.ts +121 -0
  55. package/dist/store/sqlite.js +922 -0
  56. package/dist/store/state.d.ts +35 -0
  57. package/dist/store/state.js +62 -0
  58. package/dist/tools/index.d.ts +21 -0
  59. package/dist/tools/index.js +169 -0
  60. package/dist/types.d.ts +86 -0
  61. package/dist/types.js +7 -0
  62. package/dist/util/filelog.d.ts +4 -0
  63. package/dist/util/filelog.js +43 -0
  64. package/dist/util/sanitize.d.ts +10 -0
  65. package/dist/util/sanitize.js +66 -0
  66. package/dist/util/text.d.ts +11 -0
  67. package/dist/util/text.js +43 -0
  68. package/package.json +80 -0
@@ -0,0 +1,150 @@
1
+ /**
2
+ * L2 场景块存储:Markdown 文件(含 META 块)+ 场景导航。
3
+ * 命名规范移植自 MemoryCore;LLM 输出 [DELETED] 时由工程侧**删除文件**(硬删除),
4
+ * list() 对仍含该标记的遗留文件容错跳过。
5
+ *
6
+ * 分族隔离:目录为 scenes/<family>/;init() 把旧布局(scenes/ 根下散文件)
7
+ * 一次性迁入 scenes/chat/(历史数据由 chat 档蒸馏产出)。
8
+ */
9
+ import { promises as fs } from 'node:fs';
10
+ import * as path from 'node:path';
11
+ import { atomicWriteText, ensureDir, readTextIfExists } from './io.js';
12
+ import { NAV_HEADER } from './persona.js';
13
+ const META_START = '-----META-START-----';
14
+ const META_END = '-----META-END-----';
15
+ const DELETED_MARKER = '[DELETED]';
16
+ export class SceneStore {
17
+ logger;
18
+ dir;
19
+ family;
20
+ constructor(dataDir, family, logger) {
21
+ this.logger = logger;
22
+ this.family = family;
23
+ this.dir = path.join(dataDir, 'scenes', family);
24
+ }
25
+ async init() {
26
+ await ensureDir(this.dir);
27
+ await this.migrateLegacyLayout();
28
+ }
29
+ /** 旧布局迁移:scenes/ 根下的 .md 移入本族目录。仅 chat 族执行(历史数据归属 chat)。 */
30
+ async migrateLegacyLayout() {
31
+ if (this.family !== 'chat')
32
+ return;
33
+ const legacyDir = path.dirname(this.dir);
34
+ let files;
35
+ try {
36
+ files = await fs.readdir(legacyDir);
37
+ }
38
+ catch {
39
+ return;
40
+ }
41
+ let moved = 0;
42
+ for (const f of files) {
43
+ if (!f.endsWith('.md'))
44
+ continue;
45
+ const from = path.join(legacyDir, f);
46
+ const to = path.join(this.dir, f);
47
+ try {
48
+ await fs.rename(from, to);
49
+ moved++;
50
+ }
51
+ catch {
52
+ /* 单文件失败跳过(可能被占用),下次启动重试 */
53
+ }
54
+ }
55
+ if (moved > 0)
56
+ this.logger?.info(`[memory] 场景目录迁移:${moved} 个文件 scenes/ → scenes/chat/`);
57
+ }
58
+ listFiles() {
59
+ return fs.readdir(this.dir).catch(() => []);
60
+ }
61
+ /** 列出场景摘要(解析 META 块)。 */
62
+ async list() {
63
+ const files = await this.listFiles();
64
+ const out = [];
65
+ for (const f of files.sort()) {
66
+ if (!f.endsWith('.md'))
67
+ continue;
68
+ const content = await readTextIfExists(path.join(this.dir, f));
69
+ if (!content || content.trim() === DELETED_MARKER)
70
+ continue;
71
+ out.push(parseMeta(content, f));
72
+ }
73
+ return out;
74
+ }
75
+ async read(name) {
76
+ const safe = sanitizeFilename(name);
77
+ if (!safe)
78
+ return undefined;
79
+ return readTextIfExists(path.join(this.dir, safe));
80
+ }
81
+ /**
82
+ * 写入/重写场景文件。content 为 [DELETED] 时删除该文件(LLM 的 delete 操作)。
83
+ * 文件名自动归一化(空格→短横线、剔除非法字符),非法则抛错。
84
+ */
85
+ async write(name, content) {
86
+ const safe = sanitizeFilename(name);
87
+ if (!safe)
88
+ throw new Error(`非法的场景文件名: ${name}`);
89
+ const file = path.join(this.dir, safe);
90
+ if (content.trim() === DELETED_MARKER) {
91
+ await fs.unlink(file).catch(() => undefined);
92
+ return safe;
93
+ }
94
+ await atomicWriteText(file, content);
95
+ return safe;
96
+ }
97
+ /** 场景导航索引(召回注入用)。 */
98
+ async navigation() {
99
+ const scenes = await this.list();
100
+ if (scenes.length === 0)
101
+ return '';
102
+ const lines = [
103
+ NAV_HEADER,
104
+ '*以下是当前场景记忆索引,可使用 memory_read_scene 读取详细内容。*',
105
+ '',
106
+ ];
107
+ for (const s of scenes) {
108
+ lines.push(`- \`${s.path}\` — ${s.summary || '(无摘要)'}`);
109
+ }
110
+ return lines.join('\n');
111
+ }
112
+ }
113
+ function parseMeta(content, name) {
114
+ const s = { path: name, created: '', updated: '', summary: '', heat: 0 };
115
+ const start = content.indexOf(META_START);
116
+ const end = content.indexOf(META_END);
117
+ if (start !== -1 && end !== -1) {
118
+ const meta = content.slice(start + META_START.length, end);
119
+ for (const line of meta.split('\n')) {
120
+ const m = /^\s*([a-zA-Z_]+)\s*:\s*(.*)$/.exec(line);
121
+ if (!m)
122
+ continue;
123
+ const key = m[1].toLowerCase();
124
+ const value = m[2].trim();
125
+ if (key === 'created')
126
+ s.created = value;
127
+ else if (key === 'updated')
128
+ s.updated = value;
129
+ else if (key === 'summary')
130
+ s.summary = value;
131
+ else if (key === 'heat')
132
+ s.heat = Number.parseInt(value, 10) || 0;
133
+ }
134
+ }
135
+ return s;
136
+ }
137
+ /** 文件名归一化:只允许字母数字 CJK - _ .,以 .md 结尾,去空格/标点。 */
138
+ export function sanitizeFilename(name) {
139
+ let n = name.trim();
140
+ if (!n.toLowerCase().endsWith('.md'))
141
+ n = `${n}.md`;
142
+ n = n
143
+ .replace(/[^\w\u3400-\u9fff\uf900-\ufaff.\-_]/g, '-')
144
+ .replace(/-{2,}/g, '-')
145
+ .replace(/-+\.md$/i, '.md')
146
+ .replace(/^-+|-+$/g, '');
147
+ if (!n || !/^[\w\u3400-\u9fff\uf900-\ufaff.\-_]+\.md$/i.test(n))
148
+ return '';
149
+ return n;
150
+ }
@@ -0,0 +1,22 @@
1
+ /** 标准 RRF 常数(原论文值);k 越大越偏向低排名项(分布更平滑)。 */
2
+ export declare const RRF_K = 60;
3
+ /**
4
+ * RRF 融合多个已排序列表:每项得分 = 各列表 1/(k + rank + 1) 之和。
5
+ * 出现在多个列表的项得分累加,按得分降序返回(附 rrfScore)。
6
+ */
7
+ export declare function rrfMerge<T>(lists: T[][], getId: (item: T) => string, k?: number): Array<T & {
8
+ rrfScore: number;
9
+ }>;
10
+ /** FTS5 bm25 rank(负值=更相关)转 0~1 分数(照搬 MemoryCore 公式)。 */
11
+ export declare function bm25RankToScore(rank: number): number;
12
+ /**
13
+ * 把自然语言查询构造成 FTS5 MATCH 表达式:token 引号化后 OR 连接,
14
+ * 命中任一 token 即返回,BM25 自然把命中多 token 的文档排前——
15
+ * 长查询与纯 FTS 模式(无向量)下召回率显著优于整句匹配。
16
+ */
17
+ export declare function buildFtsQuery(raw: string): string | null;
18
+ /**
19
+ * 写入侧分词:tokenize 后空格连接,交给 FTS5 unicode61 切词建索引。
20
+ * 与 buildFtsQuery 用同一分词器,保证查询 token 在索引中可命中。
21
+ */
22
+ export declare function tokenizeForFts(raw: string): string;
@@ -0,0 +1,71 @@
1
+ /**
2
+ * 检索工具(移植 MemoryCore search-utils + sqlite FTS helpers):
3
+ * - rrfMerge:RRF(Reciprocal Rank Fusion,k=60)多路结果融合,hybrid 检索用;
4
+ * - bm25RankToScore:FTS5 bm25 rank(负值=更相关)转 0~1 分数;
5
+ * - buildFtsQuery / tokenizeForFts:FTS5 查询构造与写入侧分词。
6
+ * 官方用 jieba 分词;这里用项目自带的 CJK 二元组 + 英文词分词(util/text.ts),
7
+ * 读写两侧共用同一分词器,保证查询 token 与索引 token 对齐,且零原生依赖。
8
+ */
9
+ import { tokenize } from '../util/text.js';
10
+ /** 标准 RRF 常数(原论文值);k 越大越偏向低排名项(分布更平滑)。 */
11
+ export const RRF_K = 60;
12
+ /**
13
+ * RRF 融合多个已排序列表:每项得分 = 各列表 1/(k + rank + 1) 之和。
14
+ * 出现在多个列表的项得分累加,按得分降序返回(附 rrfScore)。
15
+ */
16
+ export function rrfMerge(lists, getId, k = RRF_K) {
17
+ const map = new Map();
18
+ for (const list of lists) {
19
+ for (let rank = 0; rank < list.length; rank++) {
20
+ const item = list[rank];
21
+ const id = getId(item);
22
+ const score = 1 / (k + rank + 1);
23
+ const existing = map.get(id);
24
+ if (existing) {
25
+ existing.rrfScore += score;
26
+ }
27
+ else {
28
+ map.set(id, { item, rrfScore: score });
29
+ }
30
+ }
31
+ }
32
+ return [...map.values()]
33
+ .sort((a, b) => b.rrfScore - a.rrfScore)
34
+ .map(({ item, rrfScore }) => ({ ...item, rrfScore }));
35
+ }
36
+ /** FTS5 bm25 rank(负值=更相关)转 0~1 分数(照搬 MemoryCore 公式)。 */
37
+ export function bm25RankToScore(rank) {
38
+ if (!Number.isFinite(rank))
39
+ return 1 / (1 + 999);
40
+ if (rank < 0) {
41
+ const relevance = -rank;
42
+ return relevance / (1 + relevance);
43
+ }
44
+ return 1 / (1 + rank);
45
+ }
46
+ /** 高频中文虚词,进 FTS 查询只添噪声(沿用官方小表)。 */
47
+ const ZH_STOP_WORDS = new Set([
48
+ '的', '了', '在', '是', '我', '有', '和', '就', '不', '人', '都', '一',
49
+ '一个', '上', '也', '很', '到', '说', '要', '去', '你', '会', '着',
50
+ '没有', '看', '好', '自己', '这', '他', '她', '它', '们', '那',
51
+ '吗', '吧', '呢', '啊', '呀', '哦', '嗯',
52
+ ]);
53
+ /**
54
+ * 把自然语言查询构造成 FTS5 MATCH 表达式:token 引号化后 OR 连接,
55
+ * 命中任一 token 即返回,BM25 自然把命中多 token 的文档排前——
56
+ * 长查询与纯 FTS 模式(无向量)下召回率显著优于整句匹配。
57
+ */
58
+ export function buildFtsQuery(raw) {
59
+ const tokens = [...new Set(tokenize(raw).filter((t) => !ZH_STOP_WORDS.has(t)))];
60
+ if (tokens.length === 0)
61
+ return null;
62
+ const quoted = tokens.map((t) => `"${t.replaceAll('"', '')}"`);
63
+ return quoted.join(' OR ');
64
+ }
65
+ /**
66
+ * 写入侧分词:tokenize 后空格连接,交给 FTS5 unicode61 切词建索引。
67
+ * 与 buildFtsQuery 用同一分词器,保证查询 token 在索引中可命中。
68
+ */
69
+ export function tokenizeForFts(raw) {
70
+ return tokenize(raw).join(' ');
71
+ }
@@ -0,0 +1,24 @@
1
+ import type { MemoryLogger, MemoryMode } from '../types.js';
2
+ export declare function isMemoryMode(v: unknown): v is MemoryMode;
3
+ export declare class SessionModeStore {
4
+ private readonly defaultMode;
5
+ private readonly logger?;
6
+ private readonly file;
7
+ private readonly entries;
8
+ private readonly loaded;
9
+ private persistFailed;
10
+ /** 串行化持久化写(避免并发原子写撞临时文件名)。 */
11
+ private writeChain;
12
+ constructor(dataDir: string, defaultMode: Extract<MemoryMode, 'auto' | 'chat' | 'work'>, logger?: MemoryLogger | undefined);
13
+ /** 载入持久化映射(index.ts 启动时 await;失败降级内存态)。 */
14
+ init(): Promise<void>;
15
+ get default(): MemoryMode;
16
+ /** 同步读取:未设置过的会话返回默认档。 */
17
+ get(sessionId: string): MemoryMode;
18
+ /** 设置会话档位(写穿持久化;持久化失败保持内存态生效)。 */
19
+ set(sessionId: string, mode: MemoryMode): void;
20
+ /** 等待在途持久化写完成(测试/停机用)。 */
21
+ flush(): Promise<void>;
22
+ private persist;
23
+ private serialize;
24
+ }
@@ -0,0 +1,102 @@
1
+ /**
2
+ * 会话记忆档位存储:sessionId → MemoryMode 的持久化映射。
3
+ * 热路径(捕获 turn/end、召回 pre-step、工具 execute)需要同步读取,
4
+ * 因此 init() 一次性载入内存 Map,set() 写穿。
5
+ * 存储失败只降级为内存态(warn 不崩),与插件的存储降级不变量一致。
6
+ */
7
+ import * as path from 'node:path';
8
+ import { atomicWriteJson, ensureDir, readJsonIfExists } from './io.js';
9
+ const MODES = ['auto', 'chat', 'work', 'off'];
10
+ const PRUNE_MS = 90 * 24 * 3600_000;
11
+ const MAX_ENTRIES = 500;
12
+ export function isMemoryMode(v) {
13
+ return typeof v === 'string' && MODES.includes(v);
14
+ }
15
+ export class SessionModeStore {
16
+ defaultMode;
17
+ logger;
18
+ file;
19
+ entries = new Map();
20
+ loaded;
21
+ persistFailed = false;
22
+ /** 串行化持久化写(避免并发原子写撞临时文件名)。 */
23
+ writeChain = Promise.resolve();
24
+ constructor(dataDir, defaultMode, logger) {
25
+ this.defaultMode = defaultMode;
26
+ this.logger = logger;
27
+ this.file = path.join(dataDir, 'session-modes.json');
28
+ this.loaded = defaultMode;
29
+ }
30
+ /** 载入持久化映射(index.ts 启动时 await;失败降级内存态)。 */
31
+ async init() {
32
+ const data = await readJsonIfExists(this.file);
33
+ if (!data?.sessions || typeof data.sessions !== 'object')
34
+ return;
35
+ const now = Date.now();
36
+ let count = 0;
37
+ for (const [sid, entry] of Object.entries(data.sessions)) {
38
+ if (!isMemoryMode(entry?.mode))
39
+ continue;
40
+ if (now - (entry.updatedAt ?? 0) > PRUNE_MS)
41
+ continue;
42
+ this.entries.set(sid, { mode: entry.mode, updatedAt: entry.updatedAt ?? now });
43
+ count++;
44
+ }
45
+ if (count > 0)
46
+ this.logger?.info(`[memory] 会话档位载入 ${count} 条(默认档=${this.defaultMode})`);
47
+ }
48
+ get default() {
49
+ return this.loaded;
50
+ }
51
+ /** 同步读取:未设置过的会话返回默认档。 */
52
+ get(sessionId) {
53
+ return this.entries.get(sessionId)?.mode ?? this.loaded;
54
+ }
55
+ /** 设置会话档位(写穿持久化;持久化失败保持内存态生效)。 */
56
+ set(sessionId, mode) {
57
+ this.entries.set(sessionId, { mode, updatedAt: Date.now() });
58
+ this.writeChain = this.writeChain.then(() => this.persist());
59
+ }
60
+ /** 等待在途持久化写完成(测试/停机用)。 */
61
+ flush() {
62
+ return this.writeChain;
63
+ }
64
+ async persist() {
65
+ try {
66
+ await ensureDir(path.dirname(this.file));
67
+ await atomicWriteJson(this.file, this.serialize());
68
+ this.persistFailed = false;
69
+ }
70
+ catch (err) {
71
+ if (!this.persistFailed) {
72
+ this.persistFailed = true;
73
+ this.logger?.warn(`[memory] 会话档位持久化失败(降级内存态): ${err instanceof Error ? err.message : String(err)}`);
74
+ }
75
+ }
76
+ }
77
+ serialize() {
78
+ const now = Date.now();
79
+ // 超期清理 + 条数上限(按 updatedAt 淘汰最旧)
80
+ for (const [sid, e] of this.entries) {
81
+ if (now - e.updatedAt > PRUNE_MS)
82
+ this.entries.delete(sid);
83
+ }
84
+ while (this.entries.size > MAX_ENTRIES) {
85
+ let oldest;
86
+ let oldestAt = Infinity;
87
+ for (const [sid, e] of this.entries) {
88
+ if (e.updatedAt < oldestAt) {
89
+ oldest = sid;
90
+ oldestAt = e.updatedAt;
91
+ }
92
+ }
93
+ if (oldest === undefined)
94
+ break;
95
+ this.entries.delete(oldest);
96
+ }
97
+ const sessions = {};
98
+ for (const [sid, e] of this.entries)
99
+ sessions[sid] = e;
100
+ return { version: 1, sessions };
101
+ }
102
+ }
@@ -0,0 +1,121 @@
1
+ import type { EmbeddingProviderInfo } from './embedding.js';
2
+ import type { L0MessageRecord, MemoryFamily, MemoryLogger, MemoryRecord } from '../types.js';
3
+ export interface StoreInitResult {
4
+ /** embedding 配置(provider/model/维度)变化,需要后台全量重嵌入。 */
5
+ needsReindex: boolean;
6
+ reason?: string;
7
+ }
8
+ export interface StoreCapabilities {
9
+ ftsSearch: boolean;
10
+ vectorSearch: boolean;
11
+ }
12
+ /** L1 检索命中(含 BM25/余弦归一分数)。 */
13
+ export interface L1SearchHit {
14
+ id: string;
15
+ content: string;
16
+ type: string;
17
+ priority: number;
18
+ scene_name: string;
19
+ score: number;
20
+ family: MemoryFamily;
21
+ }
22
+ /** L0 检索命中。 */
23
+ export interface L0SearchHit extends L0MessageRecord {
24
+ score: number;
25
+ }
26
+ export declare class MemoryDb {
27
+ private db;
28
+ private degraded;
29
+ private ftsAvailable;
30
+ private vecLoaded;
31
+ private readonly dimensions;
32
+ private readonly logger?;
33
+ private stmtUpsertL1;
34
+ private stmtGetL1;
35
+ private stmtDeleteL1Meta;
36
+ private stmtDeleteL1Vec?;
37
+ private stmtInsertL1Vec?;
38
+ private stmtSearchL1Vec?;
39
+ private stmtL1FtsInsert;
40
+ private stmtL1FtsDelete;
41
+ private stmtL1FtsSearch;
42
+ private stmtL1FtsSearchFamily;
43
+ private stmtUpsertL0;
44
+ private stmtDeleteL0Vec?;
45
+ private stmtInsertL0Vec?;
46
+ private stmtSearchL0Vec?;
47
+ private stmtL0FtsInsert;
48
+ private stmtL0FtsDelete;
49
+ private stmtL0FtsSearch;
50
+ constructor(dbPath: string, dimensions: number, logger?: MemoryLogger);
51
+ isDegraded(): boolean;
52
+ getCapabilities(): StoreCapabilities;
53
+ /**
54
+ * 加载 sqlite-vec 扩展并建 schema。构造后必须调用一次。
55
+ * providerInfo 变化(provider/model/维度)时 drop 向量表并返回 needsReindex。
56
+ */
57
+ init(providerInfo?: EmbeddingProviderInfo): StoreInitResult;
58
+ private initSchema;
59
+ private prepareL1VecStatements;
60
+ private prepareL0VecStatements;
61
+ private dropVectorTables;
62
+ private tableExists;
63
+ private hasColumn;
64
+ /** 重建后的 l1_fts 从 l1_records 全量回灌(仅在 drop 重建时调用)。 */
65
+ private backfillL1Fts;
66
+ private readEmbeddingMeta;
67
+ private writeEmbeddingMeta;
68
+ /**
69
+ * 标记当前向量与 embedding 配置同步完成(持久化 meta)。
70
+ * 只应在重嵌入成功(或空库无历史向量)后调用——过早写入会让下次启动
71
+ * 比对通过而跳过补齐,向量表永远空着(review P7)。
72
+ */
73
+ markEmbeddingSynced(info: EmbeddingProviderInfo): void;
74
+ /** upsert 一条 L1(元数据 + FTS 同步;embedding 非零时写向量)。失败返回 false 不抛。 */
75
+ upsertL1(record: MemoryRecord, embedding?: Float32Array): boolean;
76
+ /** 批量删除 L1(元数据 + 向量 + FTS),返回删除条数。 */
77
+ deleteL1Batch(ids: string[]): number;
78
+ countL1(): number;
79
+ /** 全量读取(调试/迁移/重嵌入用;检索请走 FTS/向量)。 */
80
+ getAllL1(): MemoryRecord[];
81
+ getL1ByIds(ids: string[]): MemoryRecord[];
82
+ /** 浏览列表(UI 用):按更新时间倒序,支持类型/场景/族过滤与分页。失败返回空。 */
83
+ listL1(opts: {
84
+ type?: string;
85
+ scene?: string;
86
+ family?: string;
87
+ limit: number;
88
+ offset: number;
89
+ }): {
90
+ items: MemoryRecord[];
91
+ total: number;
92
+ };
93
+ /** 场景名去重列表(UI 筛选器数据源)。失败返回空。 */
94
+ distinctL1Scenes(): string[];
95
+ /** FTS5 BM25 检索(family 缺省不过滤)。失败返回空数组(调用方降级)。 */
96
+ searchL1Fts(query: string, limit: number, family?: string): L1SearchHit[];
97
+ /** vec0 余弦 KNN 检索(score = 1 - cosine distance;family 过滤走过度召回 + 回查过滤,vec0 无法 WHERE)。失败返回空数组。 */
98
+ searchL1Vector(embedding: Float32Array, topK: number, family?: string): L1SearchHit[];
99
+ /** 批量 upsert L0 消息(元数据 + FTS;embeddings 与 records 等长,可省略)。 */
100
+ upsertL0Batch(records: L0MessageRecord[], embeddings?: Array<Float32Array | undefined>): boolean;
101
+ countL0(): number;
102
+ /** 统计 recorded_at >= iso 的消息数(状态面板"今日捕获"用)。 */
103
+ countL0Since(iso: string): number;
104
+ /** 向量表行数(backfill 判据:与元数据行数的差值即缺失向量数;不可用时返回 -1)。 */
105
+ countL1Vec(): number;
106
+ countL0Vec(): number;
107
+ searchL0Fts(query: string, limit: number): L0SearchHit[];
108
+ searchL0Vector(embedding: Float32Array, topK: number): L0SearchHit[];
109
+ getL1ForReindex(): Array<{
110
+ id: string;
111
+ content: string;
112
+ }>;
113
+ getL0ForReindex(): Array<{
114
+ id: string;
115
+ text: string;
116
+ }>;
117
+ /** 只更新向量行(重嵌入用)。 */
118
+ updateL1Vec(id: string, embedding: Float32Array): boolean;
119
+ updateL0Vec(id: string, embedding: Float32Array, recordedAt: string): boolean;
120
+ close(): void;
121
+ }