dsh-prime-memory 0.26.9 → 0.26.11
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/CHANGELOG.de.md +1638 -0
- package/CHANGELOG.en.md +15 -0
- package/CHANGELOG.es.md +1641 -0
- package/CHANGELOG.fr.md +1643 -0
- package/CHANGELOG.it.md +1643 -0
- package/CHANGELOG.ja.md +15 -0
- package/CHANGELOG.ko.md +15 -0
- package/CHANGELOG.md +62 -0
- package/CHANGELOG.ru.md +1640 -0
- package/README.de.md +1 -1
- package/README.en.md +3 -3
- package/README.es.md +1 -1
- package/README.fr.md +1 -1
- package/README.it.md +1 -1
- package/README.ja.md +1 -1
- package/README.ko.md +1 -1
- package/README.md +17 -4
- package/README.ru.md +1 -1
- package/dist/bench-control.js +3 -1
- package/dist/client.js +121 -12
- package/dist/clients/drsc.d.ts +34 -0
- package/dist/clients/drsc.js +56 -11
- package/dist/config.d.ts +10 -1
- package/dist/config.js +37 -18
- package/dist/contract.d.ts +48 -1
- package/dist/graph/gate.d.ts +17 -0
- package/dist/graph/gate.js +11 -0
- package/dist/graph/preview.d.ts +5 -0
- package/dist/graph/preview.js +2 -0
- package/dist/index.js +1 -1
- package/dist/pipeline/fact-source.d.ts +127 -0
- package/dist/pipeline/fact-source.js +192 -0
- package/dist/pipeline/rebuild-preserve.d.ts +31 -2
- package/dist/pipeline/rebuild-preserve.js +40 -14
- package/dist/pipeline/rebuild.d.ts +47 -13
- package/dist/pipeline/rebuild.js +153 -57
- package/dist/pipeline/runner.d.ts +4 -1
- package/dist/pipeline/runner.js +58 -21
- package/dist/pipeline/workspace-relink.js +7 -2
- package/dist/room-review.d.ts +1 -1
- package/dist/room-review.js +10 -5
- package/dist/settings.js +4 -0
- package/dist/stats.js +10 -3
- package/dist/store/graph-store.d.ts +20 -0
- package/dist/store/graph-store.js +50 -1
- package/dist/store/sqlite.d.ts +42 -0
- package/dist/store/sqlite.js +98 -0
- package/dist/tools/index.js +4 -3
- package/dsh.plugin.json +2 -2
- package/package.json +26 -21
package/dist/config.js
CHANGED
|
@@ -82,6 +82,17 @@ export function liveSettingsSchema() {
|
|
|
82
82
|
conflictFreeze: Schema.boolean().default(false),
|
|
83
83
|
// 轨2 技能结晶总开关(看板可拨):默认 false。开=注册 memory_skill_* 工具 + 注入 active 技能 + skills-* 端点可用。
|
|
84
84
|
skillsEnabled: Schema.boolean().default(false),
|
|
85
|
+
/**
|
|
86
|
+
* 知识图谱**投影**开关(issue #16,概览区可拨)。默认 false。
|
|
87
|
+
*
|
|
88
|
+
* 与部署 `graph.enabled` **取或**(照 keywordEnabled / validityTiering 先例):
|
|
89
|
+
* 部署可强制开,面板只能追加开。
|
|
90
|
+
*
|
|
91
|
+
* 为什么它必须在 live:投影是**写侧派生**,关了只是不入队、不丢数据;
|
|
92
|
+
* 而它的入口以前只住 YAML,用户根本找不到(面板上那个“图谱投影”其实是蒸馏预算的
|
|
93
|
+
* 分层标签,点它改的是 token 预算)。
|
|
94
|
+
*/
|
|
95
|
+
graphEnabled: Schema.boolean().default(false),
|
|
85
96
|
// 存量回填/进阶功能的启用开关(概览「存量回填与整形」区可拨):默认 false,
|
|
86
97
|
// 与部署 cfg.<x>.enabled 取或(经 effectiveCfg)——部署可强制开,live 只能追加开。
|
|
87
98
|
// 存在的理由:这些面都有对应按钮/端点,若开关只住 YAML,用户点了只会报“未开启”却没处翻。
|
|
@@ -184,6 +195,16 @@ export const memorySchema = Schema.object({
|
|
|
184
195
|
// 单源扫描上限(防极端大包在事件循环里阻塞 RPC;scan 是手动触发,仍留护栏)。
|
|
185
196
|
scanMaxRecords: Schema.number().min(0).max(20_000).default(5000),
|
|
186
197
|
}),
|
|
198
|
+
// 重建事实源(issue #14-B):默认 local(行为与改前一致);drsc 需显式选,
|
|
199
|
+
// 且 collector 缺席时**报错中止**(不清库)——不静默回退本地,避免“以为用了新源其实还是旧表”。
|
|
200
|
+
rebuild: Schema.object({
|
|
201
|
+
/**
|
|
202
|
+
* 'local' = 本地 l0_conversations;'drsc' = reference-session-collector 投影。
|
|
203
|
+
*
|
|
204
|
+
* drsc 路径恒带 `redact=1`(collector 的 session 缺省是原文臂,而 capture 是写前脱敏)。
|
|
205
|
+
*/
|
|
206
|
+
factSource: Schema.union(['local', 'drsc']).default('local'),
|
|
207
|
+
}),
|
|
187
208
|
// 轨2 技能结晶:默认关 + allowPromote 关(先只计数不晋升)。
|
|
188
209
|
skills: Schema.object({
|
|
189
210
|
enabled: Schema.boolean().default(false),
|
|
@@ -210,7 +231,19 @@ export const memorySchema = Schema.object({
|
|
|
210
231
|
}),
|
|
211
232
|
/**
|
|
212
233
|
* 内容路由:默认**开**(用户口径:谈到本仓/dsh 生态的记忆要归到本仓 workspace,
|
|
213
|
-
* 而不是全托底到 `<dataDir>/memories
|
|
234
|
+
* 而不是全托底到 `<dataDir>/memories`)。
|
|
235
|
+
*
|
|
236
|
+
* ⚠️ **默认路由表为空**(issue #13)。此前默认值写死了作者本机的 `E:\test\rewrite-agently`
|
|
237
|
+
* 与 `E:\test\agent-no-dsh`:装好即生效,凡正文含 `dsh`/`cordis`/`prime-memory` 等词的记忆
|
|
238
|
+
* 都被写上了别人机器上的路径。别人的机器没这个盘,于是
|
|
239
|
+
* - `targetFor` 的护栏判定目录不存在 → 投射落点静默退回单根(日志一条 warn 而已);
|
|
240
|
+
* - 更严重的是 `scope: workspace` 时这些记录在**真实工作区**里按 workspace 过滤不可见,
|
|
241
|
+
* 表现为"记忆莫名其妙丢了",而根源是默认值指向别人的机器。
|
|
242
|
+
*
|
|
243
|
+
* 默认值属于**可移植性契约**:不得含任何具体主机的绝对路径。要么由用户显式配置路由、
|
|
244
|
+
* 要么用 `$workspace` 之类的占位(由调用方在运行期替换为当前会话工作区),
|
|
245
|
+
* 要么不路由(`routes: []` → 全部落单根,行为与 `enabled:false` 等价但语义更诚实)。
|
|
246
|
+
* 想恢复旧的按内容归类,请在配置里自行写下自己的两条规则。
|
|
214
247
|
*/
|
|
215
248
|
workspaceSource: Schema.object({
|
|
216
249
|
enabled: Schema.boolean().default(true),
|
|
@@ -219,31 +252,17 @@ export const memorySchema = Schema.object({
|
|
|
219
252
|
* 归到哪个 workspace(**workspace 根**,绝对路径;写入前过 normalizeWorkspacePath)。
|
|
220
253
|
*
|
|
221
254
|
* 存的不包含 `.agents/memory`:投射落点由 `targetFor` 算,它会在本值后拼上
|
|
222
|
-
* `WORKSPACE_SUBDIR = ['.agents','memory']
|
|
255
|
+
* `WORKSPACE_SUBDIR = ['.agents','memory']`。旧默认值曾把它写成
|
|
223
256
|
* `E:\test\rewrite-agently\.agents`,于是落点变成 `.agents\.agents\memory\`——
|
|
224
257
|
* 而 `.agents` 目录确实存在,"目录不存在则退回单根"的护栏救不了这种形状,
|
|
225
|
-
*
|
|
258
|
+
* 等于在仓里挂一棵没人认领的孤儿树。有用例钉住这个不变式。
|
|
226
259
|
*/
|
|
227
260
|
path: Schema.string().default(''),
|
|
228
261
|
/** 命中任一关键词即归此路(小写子串匹配,已 NFKC 归一)。 */
|
|
229
262
|
anyOf: Schema.array(Schema.string()).default([]),
|
|
230
263
|
/** 出现任一排除词则此路不命中(用于「无 dsh 才算非 dsh 项目」)。 */
|
|
231
264
|
noneOf: Schema.array(Schema.string()).default([]),
|
|
232
|
-
})).default([
|
|
233
|
-
{
|
|
234
|
-
path: 'E:\\test\\rewrite-agently',
|
|
235
|
-
// 'dsh' 子串已覆盖 mine-dsh-plugins 下所有插件名;把目录名单列出来是因为
|
|
236
|
-
// 有些记忆只写了"某个插件"而没写 dsh- 前缀,那时这一项才顶用。
|
|
237
|
-
anyOf: ['dsh', 'mine-dsh-plugins', 'cordis', 'prime-memory', 'brief-sidebar', 'host farm', 'inferglow-dsh'],
|
|
238
|
-
noneOf: [],
|
|
239
|
-
},
|
|
240
|
-
{
|
|
241
|
-
path: 'E:\\test\\agent-no-dsh',
|
|
242
|
-
anyOf: ['hanako', 'hanaagent', 'openhanako', 'inferglow', 'pi-agent'],
|
|
243
|
-
// 同一段内容又谈到 dsh 时属于 dsh 生态(如 Hanako 逆向用于反推 dsh 机制),归 dsh 侧。
|
|
244
|
-
noneOf: ['dsh'],
|
|
245
|
-
},
|
|
246
|
-
]),
|
|
265
|
+
})).default([]),
|
|
247
266
|
maxRecords: Schema.number().min(1).max(10_000).default(300),
|
|
248
267
|
timeBudgetMs: Schema.number().min(1000).max(1_800_000).default(120_000),
|
|
249
268
|
}),
|
package/dist/contract.d.ts
CHANGED
|
@@ -111,6 +111,16 @@ export interface MemoryLiveSettings {
|
|
|
111
111
|
/** 轨2 技能结晶总开关(看板可拨):true = 注册 memory_skill_* 工具、注入 active 技能、skills-* 端点可用。
|
|
112
112
|
* 默认 false(新功能默认关);与部署 cfg.skills.enabled 取或(任一开则生效)。 */
|
|
113
113
|
skillsEnabled: boolean;
|
|
114
|
+
/**
|
|
115
|
+
* 知识图谱**投影**开关(概览区可拨,issue #16)。
|
|
116
|
+
*
|
|
117
|
+
* true = 把 L1 记忆投影成 graph_nodes/graph_edges,并在启动/周期补齐存量;
|
|
118
|
+
* 与部署 `cfg.graph.enabled` **取或**(任一开则生效)。
|
|
119
|
+
*
|
|
120
|
+
* 注意与 `toolGraph` 的区别:后者封印的是**读**工具(memory_search_graph 等),
|
|
121
|
+
* 本键控的是**写**面(生成图谱)。两者同名接近,UI 文案必须分开写。
|
|
122
|
+
*/
|
|
123
|
+
graphEnabled: boolean;
|
|
114
124
|
/** 关键词回填启用开关(概览区可拨):true = 放行 dsh-memory/keyword-backfill。与部署 cfg.keyword.enabled 取或。 */
|
|
115
125
|
keywordEnabled: boolean;
|
|
116
126
|
/** 重蒸馏整形启用开关(概览区可拨):true = 放行 dsh-memory/redistill-backfill。与部署 cfg.redistill.enabled 取或。 */
|
|
@@ -185,6 +195,34 @@ export interface RebuildStatus {
|
|
|
185
195
|
* null 表示尚未进入准备阶段。
|
|
186
196
|
*/
|
|
187
197
|
preserveNote: string | null;
|
|
198
|
+
/**
|
|
199
|
+
* 本次重建**能够重蒸出来**的条数(来源全部落在 L0 快照内)。
|
|
200
|
+
*
|
|
201
|
+
* 与 `preserveCount` 一起构成"点下去会发生什么"的完整交代:
|
|
202
|
+
* 用户看得到多少条会被重造、多少条会被原样保住。
|
|
203
|
+
* null = 尚未出计划(未进入准备阶段)。
|
|
204
|
+
*/
|
|
205
|
+
rederivableCount: number | null;
|
|
206
|
+
/** 本次重建**必须保留**的条数(无来源 / 来源不在快照 / 来源无从证明)。 */
|
|
207
|
+
preserveCount: number | null;
|
|
208
|
+
/**
|
|
209
|
+
* 事实源**覆盖到的时刻**(ISO;本地 L0 写停后即不再前移)。
|
|
210
|
+
*
|
|
211
|
+
* 这是给"这个按钮会不会倒退"提供可判依据的字段:重建只能重造到这个时刻
|
|
212
|
+
* 之前的正文。null = 本地 L0 一条正文都没有。
|
|
213
|
+
*/
|
|
214
|
+
factWatermark: string | null;
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* 重建启动请求。
|
|
218
|
+
*
|
|
219
|
+
* `force` 只用于跨过**水位落差的二次确认**(见 `STALE_RATIO_THRESHOLD`):
|
|
220
|
+
* L0 写停后“记忆晚于水位”是常态,把它做成硬拒会把重建永久锁死;
|
|
221
|
+
* 因此这里只要求调用方显式确认一次。
|
|
222
|
+
*/
|
|
223
|
+
export interface RebuildStartRequest {
|
|
224
|
+
/** true = 已确认“这批记录只保留、不重蒸”。 */
|
|
225
|
+
force?: boolean;
|
|
188
226
|
}
|
|
189
227
|
/** 反刍阶段。 */
|
|
190
228
|
export type RuminatePhase = 'idle' | 'refreshing' | 'distilling' | 'consolidating' | 'updating' | 'relabeling' | 'done' | 'cancelled' | 'failed';
|
|
@@ -856,6 +894,8 @@ export interface SettingsSetRequest {
|
|
|
856
894
|
memoryMutate?: boolean;
|
|
857
895
|
/** §C 人工冲突裁决总开关(true = 冻结冲突对,停放到待人工裁决区)。 */
|
|
858
896
|
conflictFreeze?: boolean;
|
|
897
|
+
/** 知识图谱投影开关(issue #16;与部署 cfg.graph.enabled 取或)。 */
|
|
898
|
+
graphEnabled?: boolean;
|
|
859
899
|
}
|
|
860
900
|
export interface SettingsSetResponse {
|
|
861
901
|
ok: true;
|
|
@@ -1286,6 +1326,13 @@ export interface GraphPreviewResponse {
|
|
|
1286
1326
|
jobsTotal: number;
|
|
1287
1327
|
/** 永久失败作业数(dead),可选(零漂移先例);>0 → UI 拆成"完成/失败/排队"三档显示。 */
|
|
1288
1328
|
deadJobs?: number;
|
|
1329
|
+
/**
|
|
1330
|
+
* 尚未投影的 L1 记录条数(issue #16),可选:为 0 时不写键。
|
|
1331
|
+
*
|
|
1332
|
+
* 首次开启投影前用它算“大概要花多少”:条数 ÷ 每批 8 条 = job 数,
|
|
1333
|
+
* 每个 job 一次 LLM 抽取。
|
|
1334
|
+
*/
|
|
1335
|
+
missing?: number;
|
|
1289
1336
|
};
|
|
1290
1337
|
/** mode==='full' 才出现;全图星图轻量块(可见节点×active 边,截断标记 truncated)。
|
|
1291
1338
|
* 不带此键时响应与旧形状逐字节一致——消费方按需读 `res.full?`。 */
|
|
@@ -1868,7 +1915,7 @@ export interface DshMemoryRequestMap {
|
|
|
1868
1915
|
'dsh-memory/log-tail': LogTailRequest;
|
|
1869
1916
|
'dsh-memory/trace-tail': TraceTailRequest;
|
|
1870
1917
|
'dsh-memory/rebuild-status': Record<string, never>;
|
|
1871
|
-
'dsh-memory/rebuild-start':
|
|
1918
|
+
'dsh-memory/rebuild-start': RebuildStartRequest;
|
|
1872
1919
|
'dsh-memory/rebuild-cancel': Record<string, never>;
|
|
1873
1920
|
'dsh-memory/ruminate-status': Record<string, never>;
|
|
1874
1921
|
'dsh-memory/ruminate-start': Record<string, never>;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 知识图谱投影的**唯一开关判据**(issue #16)。
|
|
3
|
+
*
|
|
4
|
+
* 为什么单独一个模块而不是塞进 runner:
|
|
5
|
+
* - `runner` 的五处门(启动补投影 / 周期泵 / enqueue / reproject / maybeQueue)与
|
|
6
|
+
* `tools` 的写面门(memory_reproject / memory_migrate_relation)必须用**同一个**
|
|
7
|
+
* 判据。判据分散 = 半开态:只改 enqueue 不改泵 → 队列只涨不取;只改泵不改启动 timer
|
|
8
|
+
* → 运行时开启后存量永不进图;只改 runner 不改工具 → 工具说"未开启"、runner 却在跑。
|
|
9
|
+
* - `tools` 不该为此拉入整个 `pipeline/runner` 的依赖图。
|
|
10
|
+
*
|
|
11
|
+
* 语义:部署 `cfg.graph.enabled` **或** 面板 `live.graphEnabled`(取或,照
|
|
12
|
+
* keywordEnabled / validityTiering 先例)。部署可以硬开、面板只能追加开 ——
|
|
13
|
+
* 反过来(取与)会让面板开关变成"只能关不能开",而它本来就只能关到部署默认。
|
|
14
|
+
*/
|
|
15
|
+
import type { MemoryConfig } from '../config.js';
|
|
16
|
+
import type { LiveSettingsHandle } from '../settings.js';
|
|
17
|
+
export declare function graphFeatureOn(cfg: MemoryConfig, live?: LiveSettingsHandle): boolean;
|
package/dist/graph/preview.d.ts
CHANGED
|
@@ -46,6 +46,11 @@ export interface PreviewOpts {
|
|
|
46
46
|
pendingJobs?: number;
|
|
47
47
|
jobsTotal?: number;
|
|
48
48
|
deadJobs?: number;
|
|
49
|
+
/**
|
|
50
|
+
* 尚未投影的 L1 条数(issue #16):面板“首次开启投影大概要花多少”的分母。
|
|
51
|
+
* 与 `queueMissing` 同一 WHERE,见 `GraphStore.countMissing`。
|
|
52
|
+
*/
|
|
53
|
+
missing?: number;
|
|
49
54
|
}
|
|
50
55
|
/** S-2:邻接统计——active 边且两端都可见才计度(双向)。 */
|
|
51
56
|
export interface DegreeMaps {
|
package/dist/graph/preview.js
CHANGED
|
@@ -101,6 +101,8 @@ export function buildGraphPreview(nodes, edges, opts = {}) {
|
|
|
101
101
|
pendingJobs: opts.pendingJobs ?? 0,
|
|
102
102
|
jobsTotal: opts.jobsTotal ?? 0,
|
|
103
103
|
...(opts.deadJobs != null && opts.deadJobs > 0 ? { deadJobs: opts.deadJobs } : {}),
|
|
104
|
+
// 零漂移:为 0 时不写键(旧消费方拿到的形状与改前逐字一致)。
|
|
105
|
+
...(opts.missing != null && opts.missing > 0 ? { missing: opts.missing } : {}),
|
|
104
106
|
};
|
|
105
107
|
const seedLimit = Math.min(Math.max(Math.floor(opts.seedLimit ?? GRAPH_PREVIEW_SEEDS), 1), 20);
|
|
106
108
|
const neighborLimit = Math.min(Math.max(Math.floor(opts.neighborLimit ?? GRAPH_PREVIEW_NEIGHBORS), 1), 40);
|
package/dist/index.js
CHANGED
|
@@ -399,7 +399,7 @@ export async function apply(ctx, config) {
|
|
|
399
399
|
modes.setModeChangeHandler((sessionId, oldMode, newMode) => runner.onModeChange(sessionId, oldMode, newMode));
|
|
400
400
|
// 闲置兜底:静默达标会话的未蒸馏切片自动落袋(idleSeconds=0 关闭)
|
|
401
401
|
runner.startIdleTimer();
|
|
402
|
-
//
|
|
402
|
+
// 图谱周期补投影(开关在 tick 内判:部署 graph.enabled 或面板 live.graphEnabled;30 分钟一次低速收敛)
|
|
403
403
|
runner.startGraphBackfillTimer();
|
|
404
404
|
// 时效标注存量回填(expiry-tiering task_6.3:tieringEnabled 门控,低频、让位蒸馏)
|
|
405
405
|
runner.startValidityBackfillTimer();
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 重建事实源抽象(issue #14-B)。
|
|
3
|
+
*
|
|
4
|
+
* ## 为什么需要这一层
|
|
5
|
+
*
|
|
6
|
+
* 重建原先硬编码 `MemoryDb.listL0All()`:写入侧(capture)自 T8.4 起已把 L0 正文停写、
|
|
7
|
+
* 历史面交给 DRSC 投影,读侧却仍只认本地那张**冻结表**。这层把「事实源」抽成可替换的
|
|
8
|
+
* provider,让本地 L0 与 DRSC 两套实现共用同一条重建管线。
|
|
9
|
+
*
|
|
10
|
+
* ## 两个刻意的设计约束
|
|
11
|
+
*
|
|
12
|
+
* 1. **`estimate()` 必须同步**:`RebuildController.start()` 是同步 RPC,要在返回前就
|
|
13
|
+
* 给出体量与"能不能重建"的判定。DRSC 做不到同步报数,故 `FactEstimate.known=false`
|
|
14
|
+
* 表示"未知",调用方据此**跳过**同步前置门(改在 prepare 阶段用实际结果判定)。
|
|
15
|
+
* 2. **`chunks()` 而不是"先全量再分组"**:`listL0All()` 在 3 万条量级上峰值约 88MB 且
|
|
16
|
+
* 整个重建期常驻。按会话产出块,峰值降到单会话量级。`groupL0Sessions` 仍保留为
|
|
17
|
+
* 纯函数(测试与本地实现的组装点),但生产路径按会话流式拉取。
|
|
18
|
+
*
|
|
19
|
+
* ## 不丢优先
|
|
20
|
+
*
|
|
21
|
+
* `snapshotIds()` 给 `planPreserve` 判 `post-watermark` 用。DRSC 实现**无法**枚举消息 id
|
|
22
|
+
* (id 是采集端合成的 `drsc:<sid>:<seq>`,与事实源里的 `source_message_ids` 不同域),
|
|
23
|
+
* 因此返回空集 —— 效果是**全部判为不可重蒸、全部保留**。这是安全侧失败:多保留只是多
|
|
24
|
+
* 一条待去重,丢掉是不可逆。
|
|
25
|
+
*/
|
|
26
|
+
import type { ConversationMessage, MemoryLogger } from '../types.js';
|
|
27
|
+
import type { DrscClient } from '../clients/drsc.js';
|
|
28
|
+
import type { MemoryDb } from '../store/sqlite.js';
|
|
29
|
+
/** 重建分块:一个会话的全部消息(按时间升序)。 */
|
|
30
|
+
export interface RebuildChunk {
|
|
31
|
+
sessionId: string;
|
|
32
|
+
messages: ConversationMessage[];
|
|
33
|
+
}
|
|
34
|
+
/** 事实源侧的一条消息(与 `L0MessageRecord` 结构兼容,但不带 recordedAt/turn/step)。 */
|
|
35
|
+
export interface FactMessage {
|
|
36
|
+
id: string;
|
|
37
|
+
sessionId: string;
|
|
38
|
+
role: 'user' | 'assistant';
|
|
39
|
+
content: string;
|
|
40
|
+
timestamp: number;
|
|
41
|
+
}
|
|
42
|
+
/** 体量预估。`known=false` = 事实源无法同步报数(DRSC),字段值不可作为判定依据。 */
|
|
43
|
+
export interface FactEstimate {
|
|
44
|
+
sessions: number;
|
|
45
|
+
messages: number;
|
|
46
|
+
chars: number;
|
|
47
|
+
known: boolean;
|
|
48
|
+
}
|
|
49
|
+
/** 事实源覆盖范围(面板"重建只覆盖到 X"的依据)。 */
|
|
50
|
+
export interface FactCoverage {
|
|
51
|
+
/** 覆盖到的时刻(ISO);null = 未知/无正文。 */
|
|
52
|
+
untilIso: string | null;
|
|
53
|
+
sessions: number;
|
|
54
|
+
}
|
|
55
|
+
export interface FactSourceProvider {
|
|
56
|
+
readonly kind: 'l0-local' | 'drsc';
|
|
57
|
+
/** 同步体量预估;`known=false` 时调用方不得据此做拒绝判定。 */
|
|
58
|
+
estimate(): FactEstimate;
|
|
59
|
+
/**
|
|
60
|
+
* 异步精确预估 + 内部预取(DRSC 在这里一次性枚举会话,`chunks()` 复用缓存)。
|
|
61
|
+
*
|
|
62
|
+
* 与 `estimate()` 的分工:后者是同步门(`start()` 返回前就要有结论),
|
|
63
|
+
* 前者是 prepare 阶段的真实体量(能发请求、能报准确会话数)。
|
|
64
|
+
*/
|
|
65
|
+
prepare(): Promise<FactEstimate>;
|
|
66
|
+
/** 消息级流(诊断/测试用;生产走 chunks)。 */
|
|
67
|
+
snapshot(): AsyncIterable<FactMessage>;
|
|
68
|
+
/** 会话级流 —— 生产路径:单会话内存,不驻留全量正文。 */
|
|
69
|
+
chunks(): AsyncIterable<RebuildChunk>;
|
|
70
|
+
/**
|
|
71
|
+
* 本次事实源的**消息 id 集**,供 `post-watermark` 判据。
|
|
72
|
+
* 空集 = "判不出来" ⇒ 所有有来源的记录都会被保守判为不可重蒸并保留。
|
|
73
|
+
*/
|
|
74
|
+
snapshotIds(): Promise<ReadonlySet<string>>;
|
|
75
|
+
coverage(): FactCoverage;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* 把一批事实消息按会话分组(纯函数)。
|
|
79
|
+
*
|
|
80
|
+
* 保留这个签名是因为它有两个真实用途:① 本地实现按"一次全量"路径时可复用;
|
|
81
|
+
* ② 测试要能直接断言分组与排序语义而不必起 provider。生产路径用 `chunks()` 流式,
|
|
82
|
+
* 不经过这里 —— 否则又把全量正文攒回内存了。
|
|
83
|
+
*
|
|
84
|
+
* **仍然做归一 + 过滤**(与迁移前同口径):入参允许是原始 L0 行,非法行(id 空、
|
|
85
|
+
* role 非 user/assistant、正文空)一律丢弃。过滤不能只留在 provider 里,
|
|
86
|
+
* 否则任何直接调本函数的调用方(含测试夹具)都得自己重复一遍。
|
|
87
|
+
*/
|
|
88
|
+
export declare function groupL0Sessions(records: Iterable<FactMessage | Readonly<Record<string, unknown>>>): RebuildChunk[];
|
|
89
|
+
/** 本地 L0 事实源(默认):已停写的冻结存量,读面走会话点查避免全量驻留。 */
|
|
90
|
+
export declare class LocalL0FactSource implements FactSourceProvider {
|
|
91
|
+
private readonly db;
|
|
92
|
+
private readonly logger;
|
|
93
|
+
readonly kind: "l0-local";
|
|
94
|
+
constructor(db: MemoryDb, logger: MemoryLogger);
|
|
95
|
+
estimate(): FactEstimate;
|
|
96
|
+
prepare(): Promise<FactEstimate>;
|
|
97
|
+
coverage(): FactCoverage;
|
|
98
|
+
snapshotIds(): Promise<ReadonlySet<string>>;
|
|
99
|
+
snapshot(): AsyncIterable<FactMessage>;
|
|
100
|
+
chunks(): AsyncIterable<RebuildChunk>;
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* DRSC 事实源(实验性,默认不用):枚举会话 → 逐会话拉全量。
|
|
104
|
+
*
|
|
105
|
+
* 顺序说明:collector 的 `op=sessions` 按 `last_time_ms` **降序**返回,这里显式反转成
|
|
106
|
+
* 升序,与本地实现的"会话按首条时间升序"对齐(上下文链的衔接方向一致)。
|
|
107
|
+
*/
|
|
108
|
+
export declare class DrscFactSource implements FactSourceProvider {
|
|
109
|
+
private readonly client;
|
|
110
|
+
private readonly logger;
|
|
111
|
+
/** 会话分页大小(collector 侧钳 1..500)。 */
|
|
112
|
+
private readonly pageSize;
|
|
113
|
+
readonly kind: "drsc";
|
|
114
|
+
constructor(client: DrscClient, logger: MemoryLogger,
|
|
115
|
+
/** 会话分页大小(collector 侧钳 1..500)。 */
|
|
116
|
+
pageSize?: number);
|
|
117
|
+
estimate(): FactEstimate;
|
|
118
|
+
/** 会话枚举缓存(prepare 拉一次,chunks/snapshot 复用,不重复请求)。 */
|
|
119
|
+
private sessionsCache;
|
|
120
|
+
prepare(): Promise<FactEstimate>;
|
|
121
|
+
coverage(): FactCoverage;
|
|
122
|
+
snapshotIds(): Promise<ReadonlySet<string>>;
|
|
123
|
+
private listAllSessions;
|
|
124
|
+
private sessions;
|
|
125
|
+
snapshot(): AsyncIterable<FactMessage>;
|
|
126
|
+
chunks(): AsyncIterable<RebuildChunk>;
|
|
127
|
+
}
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import { drscRoleOf } from '../clients/drsc.js';
|
|
2
|
+
/** L0 记录 → 事实消息(角色/内容过滤与 `groupL0Sessions` 同口径)。 */
|
|
3
|
+
function factMessageOf(rec) {
|
|
4
|
+
const id = typeof rec.id === 'string' ? rec.id : '';
|
|
5
|
+
if (id === '')
|
|
6
|
+
return undefined;
|
|
7
|
+
const role = rec.role === 'user' || rec.role === 'assistant' ? rec.role : undefined;
|
|
8
|
+
if (role === undefined)
|
|
9
|
+
return undefined;
|
|
10
|
+
const content = typeof rec.content === 'string' ? rec.content : '';
|
|
11
|
+
if (content.trim() === '')
|
|
12
|
+
return undefined;
|
|
13
|
+
return {
|
|
14
|
+
id,
|
|
15
|
+
sessionId: typeof rec.sessionId === 'string' && rec.sessionId !== '' ? rec.sessionId : 'default',
|
|
16
|
+
role,
|
|
17
|
+
content,
|
|
18
|
+
timestamp: typeof rec.timestamp === 'number' && Number.isFinite(rec.timestamp) ? rec.timestamp : 0,
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* 把一批事实消息按会话分组(纯函数)。
|
|
23
|
+
*
|
|
24
|
+
* 保留这个签名是因为它有两个真实用途:① 本地实现按"一次全量"路径时可复用;
|
|
25
|
+
* ② 测试要能直接断言分组与排序语义而不必起 provider。生产路径用 `chunks()` 流式,
|
|
26
|
+
* 不经过这里 —— 否则又把全量正文攒回内存了。
|
|
27
|
+
*
|
|
28
|
+
* **仍然做归一 + 过滤**(与迁移前同口径):入参允许是原始 L0 行,非法行(id 空、
|
|
29
|
+
* role 非 user/assistant、正文空)一律丢弃。过滤不能只留在 provider 里,
|
|
30
|
+
* 否则任何直接调本函数的调用方(含测试夹具)都得自己重复一遍。
|
|
31
|
+
*/
|
|
32
|
+
export function groupL0Sessions(
|
|
33
|
+
// 入参保持宽松:原始 L0 行(多带 recordedAt/turn/step)与 FactMessage 都能喂。
|
|
34
|
+
records) {
|
|
35
|
+
const bySession = new Map();
|
|
36
|
+
for (const raw of records) {
|
|
37
|
+
const msg = factMessageOf(raw);
|
|
38
|
+
if (msg === undefined)
|
|
39
|
+
continue;
|
|
40
|
+
const arr = bySession.get(msg.sessionId) ?? [];
|
|
41
|
+
arr.push({ id: msg.id, role: msg.role, content: msg.content, timestamp: msg.timestamp });
|
|
42
|
+
bySession.set(msg.sessionId, arr);
|
|
43
|
+
}
|
|
44
|
+
const chunks = [];
|
|
45
|
+
for (const [sessionId, messages] of bySession) {
|
|
46
|
+
messages.sort((a, b) => a.timestamp - b.timestamp);
|
|
47
|
+
chunks.push({ sessionId, messages });
|
|
48
|
+
}
|
|
49
|
+
// 会话按首条消息时间升序:情境链按时间顺序衔接(与原始发生顺序一致)
|
|
50
|
+
chunks.sort((a, b) => a.messages[0].timestamp - b.messages[0].timestamp);
|
|
51
|
+
return chunks;
|
|
52
|
+
}
|
|
53
|
+
/** 本地 L0 事实源(默认):已停写的冻结存量,读面走会话点查避免全量驻留。 */
|
|
54
|
+
export class LocalL0FactSource {
|
|
55
|
+
db;
|
|
56
|
+
logger;
|
|
57
|
+
kind = 'l0-local';
|
|
58
|
+
constructor(db, logger) {
|
|
59
|
+
this.db = db;
|
|
60
|
+
this.logger = logger;
|
|
61
|
+
}
|
|
62
|
+
estimate() {
|
|
63
|
+
const est = this.db.l0RebuildEstimate();
|
|
64
|
+
return { ...est, known: true };
|
|
65
|
+
}
|
|
66
|
+
async prepare() {
|
|
67
|
+
return this.estimate();
|
|
68
|
+
}
|
|
69
|
+
coverage() {
|
|
70
|
+
const wm = this.db.rebuildWatermark();
|
|
71
|
+
return { untilIso: wm.l0Iso, sessions: wm.l0SessionCount };
|
|
72
|
+
}
|
|
73
|
+
async snapshotIds() {
|
|
74
|
+
// 只读 record_id 列,不碰正文 —— 这是"不发 88MB 也要能判 post-watermark"的关键。
|
|
75
|
+
return this.db.l0MessageIds();
|
|
76
|
+
}
|
|
77
|
+
async *snapshot() {
|
|
78
|
+
for (const sessionId of this.db.l0SessionIds()) {
|
|
79
|
+
for (const rec of this.db.l0MessagesBySession(sessionId)) {
|
|
80
|
+
const msg = factMessageOf({ ...rec, id: rec.id });
|
|
81
|
+
if (msg !== undefined)
|
|
82
|
+
yield msg;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
async *chunks() {
|
|
87
|
+
const sessions = this.db.l0SessionIds();
|
|
88
|
+
this.logger.debug?.(`[memory] 重建事实源:本地 L0,${sessions.length} 个会话(按会话流式读)`);
|
|
89
|
+
for (const sessionId of sessions) {
|
|
90
|
+
const messages = [];
|
|
91
|
+
for (const rec of this.db.l0MessagesBySession(sessionId)) {
|
|
92
|
+
const msg = factMessageOf({ ...rec, id: rec.id });
|
|
93
|
+
if (msg !== undefined)
|
|
94
|
+
messages.push({ id: msg.id, role: msg.role, content: msg.content, timestamp: msg.timestamp });
|
|
95
|
+
}
|
|
96
|
+
if (messages.length > 0)
|
|
97
|
+
yield { sessionId, messages };
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* DRSC 事实源(实验性,默认不用):枚举会话 → 逐会话拉全量。
|
|
103
|
+
*
|
|
104
|
+
* 顺序说明:collector 的 `op=sessions` 按 `last_time_ms` **降序**返回,这里显式反转成
|
|
105
|
+
* 升序,与本地实现的"会话按首条时间升序"对齐(上下文链的衔接方向一致)。
|
|
106
|
+
*/
|
|
107
|
+
export class DrscFactSource {
|
|
108
|
+
client;
|
|
109
|
+
logger;
|
|
110
|
+
pageSize;
|
|
111
|
+
kind = 'drsc';
|
|
112
|
+
constructor(client, logger,
|
|
113
|
+
/** 会话分页大小(collector 侧钳 1..500)。 */
|
|
114
|
+
pageSize = 200) {
|
|
115
|
+
this.client = client;
|
|
116
|
+
this.logger = logger;
|
|
117
|
+
this.pageSize = pageSize;
|
|
118
|
+
}
|
|
119
|
+
estimate() {
|
|
120
|
+
// 同步拿不到(要发请求)。known=false 让调用方跳过同步前置门,
|
|
121
|
+
// 真正的“空事实源”判定落在 prepare 的会话数上。
|
|
122
|
+
return { sessions: 0, messages: 0, chars: 0, known: false };
|
|
123
|
+
}
|
|
124
|
+
/** 会话枚举缓存(prepare 拉一次,chunks/snapshot 复用,不重复请求)。 */
|
|
125
|
+
sessionsCache = null;
|
|
126
|
+
async prepare() {
|
|
127
|
+
this.sessionsCache = await this.listAllSessions();
|
|
128
|
+
return { sessions: this.sessionsCache.length, messages: 0, chars: 0, known: true };
|
|
129
|
+
}
|
|
130
|
+
coverage() {
|
|
131
|
+
return { untilIso: null, sessions: 0 };
|
|
132
|
+
}
|
|
133
|
+
async snapshotIds() {
|
|
134
|
+
this.logger.warn('[memory] DRSC 事实源无法枚举消息 id(drsc:<sid>:<seq> 与 source_message_ids 不同域)——'
|
|
135
|
+
+ '本轮重建会把所有有来源的记忆判为不可重蒸并原样保留(保守侧失败,不丢数据)');
|
|
136
|
+
return new Set();
|
|
137
|
+
}
|
|
138
|
+
async listAllSessions() {
|
|
139
|
+
const ids = [];
|
|
140
|
+
let offset = 0;
|
|
141
|
+
for (;;) {
|
|
142
|
+
const page = await this.client.listSessions(offset, this.pageSize);
|
|
143
|
+
for (const row of page.rows) {
|
|
144
|
+
if (typeof row.session_id === 'string' && row.session_id !== '') {
|
|
145
|
+
ids.push({ id: row.session_id, last: row.last_time_ms });
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
offset += page.rows.length;
|
|
149
|
+
if (page.rows.length === 0 || offset >= page.total)
|
|
150
|
+
break;
|
|
151
|
+
}
|
|
152
|
+
// 反转 collector 的 last_time_ms 降序 → 升序(见类注释)
|
|
153
|
+
return ids.sort((a, b) => a.last - b.last).map((r) => r.id);
|
|
154
|
+
}
|
|
155
|
+
async sessions() {
|
|
156
|
+
if (this.sessionsCache !== null)
|
|
157
|
+
return this.sessionsCache;
|
|
158
|
+
this.sessionsCache = await this.listAllSessions();
|
|
159
|
+
return this.sessionsCache;
|
|
160
|
+
}
|
|
161
|
+
async *snapshot() {
|
|
162
|
+
for (const sessionId of await this.sessions()) {
|
|
163
|
+
for (const row of await this.client.sessionAll(sessionId)) {
|
|
164
|
+
const role = drscRoleOf(row.kind);
|
|
165
|
+
if (role === undefined)
|
|
166
|
+
continue;
|
|
167
|
+
if (row.text.trim() === '')
|
|
168
|
+
continue;
|
|
169
|
+
yield { id: `drsc:${sessionId}:${row.seq}`, sessionId, role, content: row.text, timestamp: row.time };
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
async *chunks() {
|
|
174
|
+
const sessions = await this.sessions();
|
|
175
|
+
this.logger.info(`[memory] 重建事实源:DRSC,${sessions.length} 个会话(逐会话拉全量,redact=1)`);
|
|
176
|
+
for (const sessionId of sessions) {
|
|
177
|
+
const rows = await this.client.sessionAll(sessionId);
|
|
178
|
+
const messages = [];
|
|
179
|
+
for (const row of rows) {
|
|
180
|
+
const role = drscRoleOf(row.kind);
|
|
181
|
+
if (role === undefined)
|
|
182
|
+
continue;
|
|
183
|
+
if (row.text.trim() === '')
|
|
184
|
+
continue;
|
|
185
|
+
messages.push({ id: `drsc:${sessionId}:${row.seq}`, role, content: row.text, timestamp: row.time });
|
|
186
|
+
}
|
|
187
|
+
messages.sort((a, b) => a.timestamp - b.timestamp);
|
|
188
|
+
if (messages.length > 0)
|
|
189
|
+
yield { sessionId, messages };
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
@@ -54,8 +54,19 @@ export interface FactSourceRead {
|
|
|
54
54
|
export type PreserveReason =
|
|
55
55
|
/** 事实源里没有 `source_message_ids` —— 不是从 L0 蒸馏来的。 */
|
|
56
56
|
'no-source'
|
|
57
|
-
/** 内容为空白 ——
|
|
57
|
+
/** 内容为空白 —— 重蒸馏不会产出空记忆,保留它也无从重造。 */
|
|
58
58
|
| 'empty-content'
|
|
59
|
+
/**
|
|
60
|
+
* 来源消息**不在本次 L0 快照内**。
|
|
61
|
+
*
|
|
62
|
+
* 与 `no-source` 的区别:这条记录**有**来源 id,但那些 id 指向的消息不在本次
|
|
63
|
+
* 重建的事实源里——结构上重蒸不出来,故必须保留。
|
|
64
|
+
*
|
|
65
|
+
* 典型成因是 L0 写停(T8.4 后 l0 正文三表退役):停写之后新增的 L1 记录
|
|
66
|
+
* 全部落这一类。判据用**集合包含**而不是时间差——L1 的 `created_time` 会随
|
|
67
|
+
* 每次蒸馏前移,任何时间阈值终将被跳过,用它做门槛会把重建**永久锁死**。
|
|
68
|
+
*/
|
|
69
|
+
| 'post-watermark'
|
|
59
70
|
/**
|
|
60
71
|
* 检索库里有、事实源里**完全没有**这一行 —— 来源无从证明。
|
|
61
72
|
*
|
|
@@ -86,6 +97,14 @@ export interface PreservePlan {
|
|
|
86
97
|
}
|
|
87
98
|
/** 判定单条记录是否可由 L0 重蒸馏出来(事实源侧判据)。 */
|
|
88
99
|
export declare function isRederivable(record: MemoryRecord): boolean;
|
|
100
|
+
/**
|
|
101
|
+
* 来源是否**全部**落在本次快照内。
|
|
102
|
+
*
|
|
103
|
+
* `every` 而不是 `some`:只要有一张原料缺失,重蒸馏就只会产出一个缺角的新记录,
|
|
104
|
+
* 不如原样保留。无来源时返回 `false`——但那一类由 `no-source` 先行接住,
|
|
105
|
+
* 不应依赖本函数的返回值。
|
|
106
|
+
*/
|
|
107
|
+
export declare function sourcesCoveredBySnapshot(record: MemoryRecord, snapshotIds: ReadonlySet<string>): boolean;
|
|
89
108
|
/**
|
|
90
109
|
* 读取事实源目录(`<dataDir>/records/*.jsonl`)。
|
|
91
110
|
*
|
|
@@ -103,7 +122,17 @@ export declare function readFactSource(recordsDir: string): Promise<FactSourceRe
|
|
|
103
122
|
* `liveDbRecords` 由调用方从**检索库**取(它没有 `source_message_ids` 列,
|
|
104
123
|
* 只用来回答"这条还在不在"),恢复时以检索库副本为准 —— 那是当前检索态。
|
|
105
124
|
*/
|
|
106
|
-
export
|
|
125
|
+
export interface PreserveOptions {
|
|
126
|
+
/**
|
|
127
|
+
* 本次重建事实源(L0 快照)的**消息 id 集**。
|
|
128
|
+
*
|
|
129
|
+
* 不给 = 保持旧口径(只看"有无来源")。给了才能判 `post-watermark`,
|
|
130
|
+
* 而**给了才是安全的**:L0 已写停的库只看"有无来源"会把停写后的记录
|
|
131
|
+
* 当成可重蒸,清库后真的丢。
|
|
132
|
+
*/
|
|
133
|
+
readonly snapshotIds?: ReadonlySet<string>;
|
|
134
|
+
}
|
|
135
|
+
export declare function planPreserve(factRecords: readonly MemoryRecord[], liveDbRecords: readonly MemoryRecord[], opts?: PreserveOptions): PreservePlan;
|
|
107
136
|
/** 清空闸门的判定码。 */
|
|
108
137
|
export type ClearGateCode =
|
|
109
138
|
/** 放行。 */
|