dsh-prime-memory 0.11.0

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 (151) hide show
  1. package/CHANGELOG.en.md +28 -0
  2. package/CHANGELOG.ja.md +30 -0
  3. package/CHANGELOG.ko.md +30 -0
  4. package/CHANGELOG.md +1220 -0
  5. package/ENGINEERING-NOTES.md +452 -0
  6. package/INSTALL.en.md +92 -0
  7. package/INSTALL.ja.md +92 -0
  8. package/INSTALL.ko.md +92 -0
  9. package/INSTALL.md +92 -0
  10. package/LICENSE +21 -0
  11. package/README.en.md +458 -0
  12. package/README.ja.md +306 -0
  13. package/README.ko.md +306 -0
  14. package/README.md +425 -0
  15. package/assets/changelog/0.8.10/01-write-only-pill.png +0 -0
  16. package/assets/changelog/0.8.9/01-panel.png +0 -0
  17. package/assets/changelog/0.8.9/02-halo.png +0 -0
  18. package/assets/changelog/0.8.9/03-layer-segmented-panel.png +0 -0
  19. package/assets/changelog/0.8.9/04-layer-l1-panel.png +0 -0
  20. package/assets/img/EmbeddingSource.png +0 -0
  21. package/assets/img/Hero.png +0 -0
  22. package/assets/img/Layers.png +0 -0
  23. package/assets/img/MemoryTools.png +0 -0
  24. package/assets/img/Modes.png +0 -0
  25. package/assets/img/ToolTrajectory.png +0 -0
  26. package/assets/img/ui-dark.jpg +0 -0
  27. package/assets/img/ui-light.jpg +0 -0
  28. package/assets/readme/bench-dialog.svg +70 -0
  29. package/assets/readme/bench-workflow.svg +79 -0
  30. package/assets/readme/flow.svg +189 -0
  31. package/assets/readme/storage.svg +115 -0
  32. package/cordis.patch.yml +16 -0
  33. package/dist/bench-control.d.ts +34 -0
  34. package/dist/bench-control.js +16 -0
  35. package/dist/client.js +4293 -0
  36. package/dist/config.d.ts +683 -0
  37. package/dist/config.js +129 -0
  38. package/dist/contract.d.ts +820 -0
  39. package/dist/contract.js +1 -0
  40. package/dist/embedding-worker.cjs +176 -0
  41. package/dist/graph/apply.d.ts +37 -0
  42. package/dist/graph/apply.js +270 -0
  43. package/dist/graph/constraints.d.ts +47 -0
  44. package/dist/graph/constraints.js +38 -0
  45. package/dist/graph/search.d.ts +16 -0
  46. package/dist/graph/search.js +115 -0
  47. package/dist/graph/types.d.ts +142 -0
  48. package/dist/graph/types.js +14 -0
  49. package/dist/hooks/capture.d.ts +32 -0
  50. package/dist/hooks/capture.js +194 -0
  51. package/dist/hooks/recall.d.ts +63 -0
  52. package/dist/hooks/recall.js +429 -0
  53. package/dist/index.d.ts +534 -0
  54. package/dist/index.js +344 -0
  55. package/dist/llm-usage.d.ts +26 -0
  56. package/dist/llm-usage.js +37 -0
  57. package/dist/llm.d.ts +153 -0
  58. package/dist/llm.js +530 -0
  59. package/dist/pipeline/graph.d.ts +35 -0
  60. package/dist/pipeline/graph.js +104 -0
  61. package/dist/pipeline/l1.d.ts +19 -0
  62. package/dist/pipeline/l1.js +271 -0
  63. package/dist/pipeline/l2.d.ts +13 -0
  64. package/dist/pipeline/l2.js +83 -0
  65. package/dist/pipeline/l3.d.ts +15 -0
  66. package/dist/pipeline/l3.js +78 -0
  67. package/dist/pipeline/rebuild.d.ts +61 -0
  68. package/dist/pipeline/rebuild.js +307 -0
  69. package/dist/pipeline/ruminate.d.ts +89 -0
  70. package/dist/pipeline/ruminate.js +298 -0
  71. package/dist/pipeline/runner.d.ts +167 -0
  72. package/dist/pipeline/runner.js +638 -0
  73. package/dist/pipeline/trigger.d.ts +40 -0
  74. package/dist/pipeline/trigger.js +75 -0
  75. package/dist/prompts/graph-projection.d.ts +70 -0
  76. package/dist/prompts/graph-projection.js +167 -0
  77. package/dist/prompts/l1-dedup.d.ts +22 -0
  78. package/dist/prompts/l1-dedup.js +251 -0
  79. package/dist/prompts/l1-extraction.d.ts +22 -0
  80. package/dist/prompts/l1-extraction.js +457 -0
  81. package/dist/prompts/persona.d.ts +23 -0
  82. package/dist/prompts/persona.js +240 -0
  83. package/dist/prompts/scene.d.ts +32 -0
  84. package/dist/prompts/scene.js +414 -0
  85. package/dist/runtime-package-lock.json +982 -0
  86. package/dist/settings.d.ts +50 -0
  87. package/dist/settings.js +355 -0
  88. package/dist/stats.d.ts +109 -0
  89. package/dist/stats.js +929 -0
  90. package/dist/store/bm25.d.ts +19 -0
  91. package/dist/store/bm25.js +63 -0
  92. package/dist/store/cost-ledger.d.ts +75 -0
  93. package/dist/store/cost-ledger.js +171 -0
  94. package/dist/store/download-queue.d.ts +79 -0
  95. package/dist/store/download-queue.js +424 -0
  96. package/dist/store/embedding-source.d.ts +118 -0
  97. package/dist/store/embedding-source.js +443 -0
  98. package/dist/store/embedding.d.ts +90 -0
  99. package/dist/store/embedding.js +206 -0
  100. package/dist/store/graph-store.d.ts +94 -0
  101. package/dist/store/graph-store.js +641 -0
  102. package/dist/store/l0.d.ts +40 -0
  103. package/dist/store/l0.js +197 -0
  104. package/dist/store/l1.d.ts +93 -0
  105. package/dist/store/l1.js +297 -0
  106. package/dist/store/local-embedding.d.ts +89 -0
  107. package/dist/store/local-embedding.js +227 -0
  108. package/dist/store/model-catalog.d.ts +48 -0
  109. package/dist/store/model-catalog.js +81 -0
  110. package/dist/store/occupancy.d.ts +30 -0
  111. package/dist/store/occupancy.js +134 -0
  112. package/dist/store/pending.d.ts +36 -0
  113. package/dist/store/pending.js +103 -0
  114. package/dist/store/persona.d.ts +15 -0
  115. package/dist/store/persona.js +60 -0
  116. package/dist/store/recall-dedupe.d.ts +26 -0
  117. package/dist/store/recall-dedupe.js +138 -0
  118. package/dist/store/runtime-installer.d.ts +59 -0
  119. package/dist/store/runtime-installer.js +243 -0
  120. package/dist/store/scenes.d.ts +24 -0
  121. package/dist/store/scenes.js +160 -0
  122. package/dist/store/search-utils.d.ts +38 -0
  123. package/dist/store/search-utils.js +100 -0
  124. package/dist/store/session-modes.d.ts +35 -0
  125. package/dist/store/session-modes.js +144 -0
  126. package/dist/store/sqlite.d.ts +246 -0
  127. package/dist/store/sqlite.js +1491 -0
  128. package/dist/store/state.d.ts +41 -0
  129. package/dist/store/state.js +72 -0
  130. package/dist/token-cost.d.ts +23 -0
  131. package/dist/token-cost.js +185 -0
  132. package/dist/tools/index.d.ts +34 -0
  133. package/dist/tools/index.js +758 -0
  134. package/dist/types.d.ts +139 -0
  135. package/dist/types.js +38 -0
  136. package/dist/util/context-occupancy.d.ts +68 -0
  137. package/dist/util/context-occupancy.js +92 -0
  138. package/dist/util/filelog.d.ts +6 -0
  139. package/dist/util/filelog.js +108 -0
  140. package/dist/util/io.d.ts +18 -0
  141. package/dist/util/io.js +97 -0
  142. package/dist/util/recall-budget.d.ts +31 -0
  143. package/dist/util/recall-budget.js +84 -0
  144. package/dist/util/sanitize.d.ts +11 -0
  145. package/dist/util/sanitize.js +67 -0
  146. package/dist/util/text.d.ts +16 -0
  147. package/dist/util/text.js +61 -0
  148. package/dist/util/tokenizer.d.ts +9 -0
  149. package/dist/util/tokenizer.js +50 -0
  150. package/dsh.plugin.json +22 -0
  151. package/package.json +118 -0
@@ -0,0 +1,307 @@
1
+ /**
2
+ * 重建控制器:从 L0 事实源重新推导 L1/L2/L3(用户主动动作,设置页按钮触发)。
3
+ *
4
+ * 语义(CONTEXT.md「重建」):
5
+ * - L0 永不改动;旧派生层归档保留(records/ scenes/ persona-*.md → *.bak.<ts>,不硬删);
6
+ * - 检索库 L1 三表清空、checkpoint 原地重置;
7
+ * - 统一按 auto 档、按会话分块重蒸馏;分块经 runner 的低优先级队列让位于正常轮次;
8
+ * - 收尾强制一轮 L2(各族残余记录)+ L3(重建后 hasPersona=false → 冷启动触发)。
9
+ *
10
+ * 失败语义:准备/归档任一步失败 → phase=failed,绝不拖垮宿主;
11
+ * 单块蒸馏失败继续下一块(消息留在未蒸馏缓冲,下轮对话/重启补跑自愈)。
12
+ */
13
+ import { promises as fs } from 'node:fs';
14
+ import * as path from 'node:path';
15
+ import { resolveDataDir } from '../config.js';
16
+ import { errDetail } from '../util/filelog.js';
17
+ import { runSceneConsolidation } from './l2.js';
18
+ import { runPersona } from './l3.js';
19
+ import { effectiveCfg } from './runner.js';
20
+ export function groupL0Sessions(records) {
21
+ const bySession = new Map();
22
+ for (const r of records) {
23
+ if (!r || typeof r.id !== 'string' || typeof r.content !== 'string')
24
+ continue;
25
+ if (r.role !== 'user' && r.role !== 'assistant')
26
+ continue;
27
+ if (!r.content.trim())
28
+ continue;
29
+ const key = r.sessionId || 'default';
30
+ const arr = bySession.get(key) ?? [];
31
+ arr.push({ id: r.id, role: r.role, content: r.content, timestamp: r.timestamp ?? 0 });
32
+ bySession.set(key, arr);
33
+ }
34
+ const chunks = [];
35
+ for (const [sessionId, messages] of bySession) {
36
+ messages.sort((a, b) => a.timestamp - b.timestamp);
37
+ chunks.push({ sessionId, messages });
38
+ }
39
+ // 会话按首条消息时间升序:情境链按时间顺序衔接(与原始发生顺序一致)
40
+ chunks.sort((a, b) => a.messages[0].timestamp - b.messages[0].timestamp);
41
+ return chunks;
42
+ }
43
+ /** 抽取调用数下界估算(与 l1.ts 的 perChunk 同式;会话数与字符预算取大)。 */
44
+ export function estimateCalls(sessions, messages, chars, maxInputChars) {
45
+ if (messages === 0)
46
+ return 0;
47
+ const perChunk = Math.max(20_000, maxInputChars - 42_000);
48
+ return Math.max(sessions, Math.ceil((chars + 64 * messages) / perChunk));
49
+ }
50
+ function idleStatus() {
51
+ return {
52
+ running: false,
53
+ phase: 'idle',
54
+ done: 0,
55
+ total: 0,
56
+ sessionCount: 0,
57
+ messageCount: 0,
58
+ estCalls: 0,
59
+ recordsBuilt: 0,
60
+ cancelRequested: false,
61
+ startedAt: null,
62
+ finishedAt: null,
63
+ error: null,
64
+ archiveNote: null,
65
+ };
66
+ }
67
+ /** 时间戳后缀(归档命名,秒级防撞)。 */
68
+ function stamp(now = new Date()) {
69
+ const p = (n) => String(n).padStart(2, '0');
70
+ return `${now.getFullYear()}${p(now.getMonth() + 1)}${p(now.getDate())}-${p(now.getHours())}${p(now.getMinutes())}${p(now.getSeconds())}`;
71
+ }
72
+ export class RebuildController {
73
+ ctx;
74
+ cfg;
75
+ stores;
76
+ db;
77
+ runner;
78
+ logger;
79
+ live;
80
+ status = idleStatus();
81
+ chunks = [];
82
+ cancelRequested = false;
83
+ /** 快照时刻(收尾时按它区分重建产物与重建后新对话的记录)。 */
84
+ rebuildStartMs = 0;
85
+ constructor(ctx, cfg, stores, db, runner, logger, live) {
86
+ this.ctx = ctx;
87
+ this.cfg = cfg;
88
+ this.stores = stores;
89
+ this.db = db;
90
+ this.runner = runner;
91
+ this.logger = logger;
92
+ this.live = live;
93
+ }
94
+ /** 状态快照(idle 时附带实时 L0 预估,供确认弹窗显示成本)。 */
95
+ getStatus() {
96
+ if (this.status.phase === 'idle') {
97
+ const est = this.db.l0RebuildEstimate();
98
+ return {
99
+ ...this.status,
100
+ sessionCount: est.sessions,
101
+ messageCount: est.messages,
102
+ estCalls: estimateCalls(est.sessions, est.messages, est.chars, effectiveCfg(this.cfg, this.live).llm.maxInputChars),
103
+ };
104
+ }
105
+ return { ...this.status };
106
+ }
107
+ /** 内存中尚未处理的会话快照块数(收尾后应为 0——快照即弃,诊断/冒烟用)。 */
108
+ get chunkCount() {
109
+ return this.chunks.length;
110
+ }
111
+ /** 启动重建(校验后入队准备任务;真正的清库/归档在管线队列里串行执行,避开并发竞态)。 */
112
+ start() {
113
+ if (this.status.running)
114
+ throw new Error('重建已在进行中');
115
+ const est = this.db.l0RebuildEstimate();
116
+ if (est.messages === 0)
117
+ throw new Error('L0 无任何消息,无需重建');
118
+ this.cancelRequested = false;
119
+ this.chunks = [];
120
+ this.status = {
121
+ ...idleStatus(),
122
+ running: true,
123
+ phase: 'preparing',
124
+ sessionCount: est.sessions,
125
+ messageCount: est.messages,
126
+ estCalls: estimateCalls(est.sessions, est.messages, est.chars, effectiveCfg(this.cfg, this.live).llm.maxInputChars),
127
+ startedAt: Date.now(),
128
+ };
129
+ this.runner.enqueueRebuildTask(() => this.prepare());
130
+ this.logger.info(`[memory] 重建开始:${est.sessions} 个会话 / ${est.messages} 条 L0 消息(预计 ≥${this.status.estCalls} 次抽取调用)`);
131
+ return { ...this.status };
132
+ }
133
+ /** 请求取消:当前块完成后停止,已重建部分保留并照常收尾 L2/L3。 */
134
+ requestCancel() {
135
+ if (!this.status.running)
136
+ return this.getStatus();
137
+ this.cancelRequested = true;
138
+ this.status.cancelRequested = true;
139
+ this.logger.info('[memory] 重建取消已请求(当前块完成后停止)');
140
+ return { ...this.status };
141
+ }
142
+ async prepare() {
143
+ try {
144
+ // 快照:从检索库读全量 L0(事务一致;重建期间新捕获的消息走正常轮次,天然不重不漏)
145
+ this.rebuildStartMs = Date.now();
146
+ this.chunks = groupL0Sessions(this.db.listL0All());
147
+ if (this.chunks.length === 0) {
148
+ this.finish('failed', 'L0 快照为空');
149
+ return;
150
+ }
151
+ this.status.total = this.chunks.length;
152
+ // 归档旧派生层(改名不硬删;任一失败即终止——半清半留会破坏"全量重导"语义)
153
+ const archiveNote = await this.archiveDerived();
154
+ this.status.archiveNote = archiveNote ?? null;
155
+ // 清检索库 + 重置 checkpoint;归档后重建空目录(records/ 由 appendNew 自动重建)
156
+ if (!this.db.clearL1())
157
+ throw new Error('L1 检索库清空失败');
158
+ this.stores.state.reset();
159
+ await this.stores.state.save();
160
+ await Promise.all([
161
+ this.stores.scenes.chat.init(),
162
+ this.stores.scenes.work.init(),
163
+ this.stores.persona.chat.init(),
164
+ this.stores.persona.work.init(),
165
+ ]);
166
+ this.status.phase = 'distilling';
167
+ this.logger.info(`[memory] 重建准备完成(归档:${archiveNote ?? '无旧产物'},${this.chunks.length} 个会话块)`);
168
+ this.scheduleChunk(0);
169
+ }
170
+ catch (err) {
171
+ this.finish('failed', `准备阶段失败: ${errDetail(err)}`);
172
+ }
173
+ }
174
+ /** 分块链:一次只挂一个重建块,跑完再挂下一块——正常轮次可随时插队。 */
175
+ scheduleChunk(i) {
176
+ if (this.cancelRequested || i >= this.chunks.length) {
177
+ this.runner.enqueueRebuildTask(() => this.finalize());
178
+ return;
179
+ }
180
+ const chunk = this.chunks[i];
181
+ this.runner.enqueueRebuildTask(async () => {
182
+ // 入队后开跑前可能已收到取消(等待插队的正常轮次期间),直接跳到收尾
183
+ if (this.cancelRequested) {
184
+ this.runner.enqueueRebuildTask(() => this.finalize());
185
+ return;
186
+ }
187
+ try {
188
+ const n = await this.runner.runRebuildTurn(chunk.sessionId, chunk.messages);
189
+ this.status.recordsBuilt += n;
190
+ }
191
+ catch (err) {
192
+ this.logger.warn(`[memory] 重建块失败(session=${chunk.sessionId},跳过继续): ${errDetail(err)}`);
193
+ }
194
+ this.status.done = i + 1;
195
+ this.scheduleChunk(i + 1);
196
+ });
197
+ }
198
+ async finalize() {
199
+ try {
200
+ this.status.phase = 'finalizing';
201
+ const cfg = effectiveCfg(this.cfg, this.live);
202
+ const liveNow = this.live.get();
203
+ const distillOn = liveNow.enabled && liveNow.distill;
204
+ // 强制 L2:把重建窗口内该族尚未整合的残余记录补一轮(正常轮次语义里差几条
205
+ // 不触发是常态,但"重建"应把已有记录全部落进场景)
206
+ if (cfg.l2.enabled && distillOn) {
207
+ for (const family of ['chat', 'work']) {
208
+ const fstate = this.runner.states[family];
209
+ if (fstate.newMemoriesSinceL2 <= 0)
210
+ continue;
211
+ const leftovers = this.collectRebuildRecords(family);
212
+ if (leftovers.length === 0)
213
+ continue;
214
+ try {
215
+ const t = Date.now();
216
+ const result = await runSceneConsolidation(this.ctx, cfg, this.stores.scenes[family], leftovers, this.logger, family);
217
+ fstate.lastL2At = Date.now();
218
+ fstate.newMemoriesSinceL2 = 0;
219
+ if (result.personaRequestedReason)
220
+ fstate.personaRequestedReason = result.personaRequestedReason;
221
+ this.logger.info(`[memory] 重建收尾 L2 完成(family=${family},${Date.now() - t}ms,${leftovers.length} 条残余记录)`);
222
+ }
223
+ catch (err) {
224
+ this.logger.warn(`[memory] 重建收尾 L2 失败(family=${family}): ${errDetail(err)}`);
225
+ }
226
+ }
227
+ }
228
+ // 强制 L3:checkpoint 已重置(hasPersona=false)→ 冷启动触发;无场景的族跳过
229
+ if (cfg.l3.enabled && distillOn) {
230
+ for (const family of ['chat', 'work']) {
231
+ try {
232
+ const scenes = await this.stores.scenes[family].list();
233
+ if (scenes.length === 0)
234
+ continue;
235
+ await runPersona(this.ctx, cfg, this.stores.scenes[family], this.stores.persona[family], this.runner.states[family], this.logger, family);
236
+ }
237
+ catch (err) {
238
+ this.logger.warn(`[memory] 重建收尾 L3 失败(family=${family}): ${errDetail(err)}`);
239
+ }
240
+ }
241
+ }
242
+ await this.stores.state.save();
243
+ this.finish(this.cancelRequested ? 'cancelled' : 'done', null);
244
+ }
245
+ catch (err) {
246
+ this.finish('failed', `收尾失败: ${errDetail(err)}`);
247
+ }
248
+ }
249
+ /**
250
+ * 收集重建窗口内某族的记录:重建产物全部是新插入(updated==created),
251
+ * 按 updated_time 倒序翻页、越过 rebuildStartMs 即停。
252
+ */
253
+ collectRebuildRecords(family) {
254
+ const out = [];
255
+ const PAGE = 200;
256
+ for (let offset = 0;; offset += PAGE) {
257
+ const { items } = this.stores.l1.list({ family, limit: PAGE, offset });
258
+ if (items.length === 0)
259
+ break;
260
+ let beyond = 0;
261
+ for (const r of items) {
262
+ if (r.createdAt >= this.rebuildStartMs)
263
+ out.push(r);
264
+ else
265
+ beyond++;
266
+ }
267
+ if (beyond > 0 || items.length < PAGE)
268
+ break;
269
+ }
270
+ return out;
271
+ }
272
+ finish(phase, error) {
273
+ this.status.running = false;
274
+ this.status.phase = phase;
275
+ this.status.error = error;
276
+ this.status.finishedAt = Date.now();
277
+ // 快照即弃:全量 L0 消息(可能几十 MB)在重建结束/取消/失败后必须释放,
278
+ // 不能滞留到下一次 start() 覆盖(宿主长跑内存只增不减)
279
+ this.chunks = [];
280
+ const cost = this.status.finishedAt - (this.status.startedAt ?? this.status.finishedAt);
281
+ this.logger.info(`[memory] 重建结束(${phase}):${this.status.done}/${this.status.total} 会话,产出 ${this.status.recordsBuilt} 条记录,耗时 ${Math.round(cost / 1000)}s` +
282
+ (error ? `,错误:${error}` : ''));
283
+ }
284
+ /** 归档旧派生层:records/ scenes/ persona-*.md 改名 .bak.<ts>。不存在则跳过。 */
285
+ async archiveDerived() {
286
+ const dataDir = resolveDataDir(this.cfg);
287
+ const ts = stamp();
288
+ const items = [
289
+ [path.join(dataDir, 'records'), path.join(dataDir, `records.bak.${ts}`)],
290
+ [path.join(dataDir, 'scenes'), path.join(dataDir, `scenes.bak.${ts}`)],
291
+ [path.join(dataDir, 'persona-chat.md'), path.join(dataDir, `persona-chat.md.bak.${ts}`)],
292
+ [path.join(dataDir, 'persona-work.md'), path.join(dataDir, `persona-work.md.bak.${ts}`)],
293
+ ];
294
+ const archived = [];
295
+ for (const [from, to] of items) {
296
+ try {
297
+ await fs.access(from);
298
+ }
299
+ catch {
300
+ continue;
301
+ }
302
+ await fs.rename(from, to);
303
+ archived.push(path.basename(to));
304
+ }
305
+ return archived.length > 0 ? archived.join(', ') : undefined;
306
+ }
307
+ }
@@ -0,0 +1,89 @@
1
+ /**
2
+ * 反刍控制器:按用户指令把未蒸馏缓冲冲刷出来,跑一轮 L1 抽取 → L2 场景整合 → L3 画像更新,
3
+ * 可选图谱投影。与 rebuild 不同:不清库、不改 L0、不归档旧产物;仅消化"攒而未蒸馏"的切片。
4
+ *
5
+ * 语义:
6
+ * - 扫描 pending 三桶(auto/chat/work),按会话分组,逐个入队强制蒸馏(force=true);
7
+ * - 蒸馏完成后按族强制 L2(把未整合的残余记录全部落进场景);
8
+ * - L3 画像冷启动更新(hasPersona=false 时自动触发,已有人格则轻量刷新);
9
+ * - 若有图谱投影,新记录自动入队(由 runner 的图谱泵接管)。
10
+ * - 支持中途取消:已蒸馏部分保留,pending 切片中未被消费的维持原状。
11
+ *
12
+ * 调度:任务入队走 runner 的优先级队列(live 任务优先于 ruminate 任务),
13
+ * 不阻塞宿主正常对话。
14
+ */
15
+ import type { Context } from '@deepseek-ai/cordis';
16
+ import type { MemoryConfig } from '../config.js';
17
+ import type { L1Store } from '../store/l1.js';
18
+ import type { PersonaStore } from '../store/persona.js';
19
+ import type { SceneStore } from '../store/scenes.js';
20
+ import type { StateStore } from '../store/state.js';
21
+ import type { LiveSettingsHandle } from '../settings.js';
22
+ import type { MemoryFamily, MemoryLogger } from '../types.js';
23
+ import type { MemoryRunner } from './runner.js';
24
+ export interface RuminateStatus {
25
+ running: boolean;
26
+ /** 当前阶段:idle / refreshing / distilling / consolidating / updating / done / cancelled / failed */
27
+ phase: string;
28
+ /** 已完成会话数(轻量刷新为已完成步骤数) */
29
+ done: number;
30
+ /** 待处理会话数(轻量刷新为待执行步骤数) */
31
+ total: number;
32
+ /** 蒸馏产出记录总数 */
33
+ recordsBuilt: number;
34
+ /** 当前动作的人类可读描述(L2/L3 单次可达分钟级) */
35
+ detail: string | null;
36
+ /** 取消请求标志 */
37
+ cancelRequested: boolean;
38
+ /** 开始时间 */
39
+ startedAt: number | null;
40
+ /** 结束时间 */
41
+ finishedAt: number | null;
42
+ /** 错误信息(失败时) */
43
+ error: string | null;
44
+ }
45
+ export declare class RuminateController {
46
+ private readonly ctx;
47
+ private readonly cfg;
48
+ private readonly runner;
49
+ private readonly stores;
50
+ private readonly logger;
51
+ private readonly live;
52
+ private status;
53
+ private cancelRequested;
54
+ /** 启动守卫:置位早于 status.running,封住"await 读取"期间的双击窗口。 */
55
+ private starting;
56
+ private sessions;
57
+ private totalL1;
58
+ private pendingFile;
59
+ constructor(ctx: Context, cfg: MemoryConfig, runner: Pick<MemoryRunner, 'enqueue' | 'states'>, stores: {
60
+ l1: L1Store;
61
+ scenes: Record<MemoryFamily, SceneStore>;
62
+ persona: Record<MemoryFamily, PersonaStore>;
63
+ state: StateStore;
64
+ }, logger: MemoryLogger, live: LiveSettingsHandle, pendingFile: string);
65
+ /** 状态快照 */
66
+ getStatus(): RuminateStatus;
67
+ /**
68
+ * 启动反刍:扫描 pending 三桶,按会话分组,逐个入队蒸馏;完成后强制 L2+L3。
69
+ * 桶形状的唯一权威是 store/pending.ts 的 loadPending——不得在本层再解析该文件。
70
+ */
71
+ start(): Promise<RuminateStatus>;
72
+ /** 请求取消:当前块完成后停止,已蒸馏部分保留。 */
73
+ requestCancel(): RuminateStatus;
74
+ /** 入队一个会话。 */
75
+ private doEnqueue;
76
+ /** 收尾:强制 L2 + L3。 */
77
+ private finalize;
78
+ /**
79
+ * 轻量刷新:没有 pending 时仅跑一轮 L2/L3。
80
+ *
81
+ * 为什么必须先置 running/phase:此处每次 runSceneConsolidation/runPersona 都是**真实 LLM 调用**
82
+ * (实测单次可达 70s+),而本方法是 `await` 的、期间 `start()` 一直挂着并占住守卫。
83
+ * 原先不置 running,导致刷新期间界面既无进度也无取消按钮,再点一次只得到"反刍已在进行中"
84
+ * 却看不到任何进展——即用户反馈的"管线进度和状态提示仍然欠缺"。
85
+ * 现在:先把步骤数登记为 total、每完成一步自增 done,并给出当前动作描述(detail)。
86
+ */
87
+ private doLightRefresh;
88
+ private finish;
89
+ }