@modusensus/dsh-mneme 0.8.4 → 0.8.6
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/README.md +45 -8
- package/bin/dsh-mneme-mcp.mjs +11 -3
- package/lib/api-standalone.js +11 -0
- package/lib/api.js +21 -0
- package/lib/client.js +213 -31
- package/lib/config.js +106 -5
- package/lib/document.js +221 -0
- package/lib/dream/sleep.js +128 -12
- package/lib/dream.js +46 -11
- package/lib/guide.js +44 -0
- package/lib/heat.js +2 -0
- package/lib/index.js +65 -3
- package/lib/inject.js +91 -15
- package/lib/lang.js +6 -0
- package/lib/local-embedder.js +44 -6
- package/lib/organize.js +450 -0
- package/lib/recall-stats.js +23 -1
- package/lib/runtime/layout.js +30 -0
- package/lib/runtime/loader.js +4 -1
- package/lib/runtime/verify.js +19 -6
- package/lib/search/adaptive.js +38 -0
- package/lib/service.js +165 -12
- package/lib/settings.js +35 -4
- package/lib/store.js +126 -8
- package/lib/summarize.js +253 -8
- package/lib/tools.js +136 -8
- package/package.json +6 -6
- package/runtime-manifest.json +104 -174
- package/scripts/build-runtime-manifest.mjs +8 -1
- package/scripts/sync-test-badge.mjs +18 -12
- package/src/api-standalone.js +11 -0
- package/src/api.js +21 -0
- package/src/config.js +106 -5
- package/src/document.js +221 -0
- package/src/dream/sleep.js +128 -12
- package/src/dream.js +46 -11
- package/src/guide.js +44 -0
- package/src/heat.js +2 -0
- package/src/index.js +65 -3
- package/src/inject.js +91 -15
- package/src/lang.js +6 -0
- package/src/local-embedder.js +44 -6
- package/src/organize.js +450 -0
- package/src/recall-stats.js +23 -1
- package/src/runtime/layout.js +30 -0
- package/src/runtime/loader.js +4 -1
- package/src/runtime/verify.js +19 -6
- package/src/search/adaptive.js +38 -0
- package/src/service.js +165 -12
- package/src/settings.js +35 -4
- package/src/store.js +126 -8
- package/src/summarize.js +253 -8
- package/src/tools.js +136 -8
- package/test/adaptive-inject-budget.test.js +85 -0
- package/test/api.test.js +66 -3
- package/test/client.test.js +63 -2
- package/test/document.test.js +523 -0
- package/test/dream.test.js +51 -0
- package/test/inject-content-cap.test.js +5 -3
- package/test/inject-pools.test.js +170 -0
- package/test/inject-recall.test.js +105 -0
- package/test/inject.test.js +35 -1
- package/test/lastrunat-persist.test.js +101 -0
- package/test/llm-audit.test.js +247 -0
- package/test/local-embedder.test.js +45 -2
- package/test/organize.test.js +418 -0
- package/test/recall-stats.test.js +7 -1
- package/test/runtime-loader.test.js +18 -0
- package/test/runtime-manifest.test.js +2 -1
- package/test/runtime-verify.test.js +65 -0
- package/test/sleep.test.js +154 -0
- package/test/store.test.js +13 -0
- package/test/summarize.test.js +407 -3
- package/test/tools-dto-schema-matrix.test.js +325 -0
- package/test/tools.test.js +36 -3
package/lib/config.js
CHANGED
|
@@ -24,6 +24,31 @@ export const Config = z.object({
|
|
|
24
24
|
// 同一事实 6 小时铸出 28+ 条。阀门交给用户按需拧,升级本身不改行为。
|
|
25
25
|
summarizeMinIntervalMinutes: z.natural().min(0).max(10080).default(0),
|
|
26
26
|
summarizeMaxEntriesPerRun: z.natural().min(0).max(50).default(0),
|
|
27
|
+
// Issue #239(成本感知级联第一级):蒸馏前的零 LLM 预判——窗口内可蒸馏文本
|
|
28
|
+
// 不足此字符数时直接跳过 LLM 调用(0 = 关闭,行为与现状逐字节一致)。判定
|
|
29
|
+
// 纯规则、无模型参与;被挡下的窗口照常消费游标(否则每个 turn/end 都会重新
|
|
30
|
+
// 评估同一段短文本),并留一行 status='skipped' 审计,error_message 写明原因
|
|
31
|
+
// ——「这一窗为什么没蒸馏」必须可观测,否则阀门等于黑盒。
|
|
32
|
+
summarizeMinWindowChars: z.natural().min(0).max(100000).default(0),
|
|
33
|
+
// Issue #239(有界检查点):同一会话最多发起多少次蒸馏(0 = 不限,等同现状)。
|
|
34
|
+
// 只统计真正发起过 LLM 调用的 run——被预判挡下的窗口不占预算,否则阀门会把
|
|
35
|
+
// 额度浪费在零成本窗口上。计数是进程内 per-session(与最小间隔闸门同生命
|
|
36
|
+
// 周期),宿主重启即清零,与 #229 的游标持久化是两件事。
|
|
37
|
+
summarizeMaxRunsPerSession: z.natural().min(0).max(1000).default(0),
|
|
38
|
+
// Issue #239(第 4 项,错峰队列):高峰期蒸馏顺延。空串 = 关闭,行为与现状逐
|
|
39
|
+
// 字节一致。取值是本地时间的时段列表(逗号分隔,支持跨零点),可带**星期前缀**
|
|
40
|
+
// (可省;ISO 1=周一…7=周日,也认 mon..sun;省略 = 每天):
|
|
41
|
+
// "09:00-18:00" 每天 09:00-18:00
|
|
42
|
+
// "mon-fri 08:00-12:00,14:00-18:00" 工作日两段(按高峰计费的供应商即此形态)
|
|
43
|
+
// "sat,sun 23:00-06:00" 周末跨零点段
|
|
44
|
+
// 命中高峰时:不调 LLM、不消费游标(窗口继续累积,留到非高峰一次性蒸馏——批量
|
|
45
|
+
// 比逐轮碎蒸更省),登记一行 status='skipped' / error_message='peak-hours' 审计,
|
|
46
|
+
// 并按下面的上限择时补跑。任一写法非法则整串按「未配置」处理:排程是省钱
|
|
47
|
+
// 手段,绝不该因为写错格式把蒸馏停掉。
|
|
48
|
+
summarizePeakHours: z.string().default(""),
|
|
49
|
+
// 高峰顺延上限(分钟,0 = 不设上限):到点仍处高峰就照常跑,避免整天高峰把蒸馏
|
|
50
|
+
// 饿死。默认 120 分钟。仅在上面的时段串非空时生效。
|
|
51
|
+
summarizePeakMaxDeferMinutes: z.natural().min(0).max(1440).default(120),
|
|
27
52
|
// 落库前去重档位:off(默认,等同现状)/ title(零成本,仅拦完全同名)/
|
|
28
53
|
// vector(复用已有 embedding 列做同会话语义近邻,无 LLM 调用)。
|
|
29
54
|
summarizeDedupeMode: z.union([z.const("off"), z.const("title"), z.const("vector")]).default("off"),
|
|
@@ -47,6 +72,25 @@ export const Config = z.object({
|
|
|
47
72
|
// 默认 300 = 与既有行为一致。
|
|
48
73
|
injectContentMaxChars: z.natural().min(60).max(4000).default(300),
|
|
49
74
|
importanceThreshold: z.natural().min(1).max(5).default(3),
|
|
75
|
+
// Issue #239(第 5 项):注入条数的查询自适应(默认关)。确定性强的话题收缩注入
|
|
76
|
+
// 条数(减半、下限 1),模糊话题(回指/时间线索,或极短查询)维持
|
|
77
|
+
// maxInjectedItems 上限——只做**单向收缩**,绝不越过用户配置的上限;判据只看
|
|
78
|
+
// 查询本身,不做额外检索(先探针检索等于白付一次 fuseRecall)。
|
|
79
|
+
injectUncertaintyAdaptive: z.boolean().default(false),
|
|
80
|
+
// #249(第一批):能力说明——「怎么用记忆」的判断指引。落两个零注入成本的
|
|
81
|
+
// 位:①`memory_search` / `memory_save` 的工具描述补一句判断指引(工具描述是
|
|
82
|
+
// 常驻文本,不进每轮上下文);②一段 order 150 的系统提示段(一次性、同会话
|
|
83
|
+
// 内不随轮次变化,因此不作废前缀缓存),只讲总则(优先序、何时查、何时写、
|
|
84
|
+
// 何时 no-op)。默认关:关闭时工具描述与提示段与既有行为逐字节一致。
|
|
85
|
+
injectGuidanceEnabled: z.boolean().default(false),
|
|
86
|
+
// #249(第一批):B1 pin 池预算——约束/偏好类注入条目的独立小上限。约束与
|
|
87
|
+
// 偏好被静默降级是本议题的立项核心(同类知识与情景日志同池同速率摘要,实测
|
|
88
|
+
// 一轮压缩后仅保 53%、五轮 10%),故这两类不进相关性竞争、不参与跨轮轮换、
|
|
89
|
+
// 逐字保真(仅受超大条目的硬顶保护,截断仍带提示)并排在块内排序之前。
|
|
90
|
+
// 独立预算的意义:pin 不占 maxInjectedItems 名额,不会把当前任务需要的情景
|
|
91
|
+
// 候选挤出预算(另一种「批量塞历史」)。0(默认)= 关闭,注入块构成与既有
|
|
92
|
+
// 行为逐字节一致;超出预算的条数在块内如实标注未展示条数,绝不静默。
|
|
93
|
+
pinnedInjectBudget: z.natural().min(0).max(5).default(0),
|
|
50
94
|
// 编码记忆蒸馏(codingRetrospect,opt-in,默认关)。开启时,turn/end 蒸馏
|
|
51
95
|
// 额外提取三类编码专属记忆:rejected_solution(被否决方案)/ pitfall(踩坑)/
|
|
52
96
|
// constraint(工程约束)。蒸馏上下文为整轮完整对话(用户输入 → 助手思考/回答
|
|
@@ -135,6 +179,17 @@ export const Config = z.object({
|
|
|
135
179
|
]).default("window"),
|
|
136
180
|
// hybrid 的候选总量上限;0 = 复用 dreamMaxSnapshotSize(不迁移,需要时显式覆盖)。
|
|
137
181
|
dreamCandidateMax: z.natural().min(0).max(5000).default(0),
|
|
182
|
+
// Issue #258:总览(dream_summarize)独立路由。consolidate 有 dreamMaxSnapshotSize
|
|
183
|
+
// 窗口、总览原为全库无界——两者对 ctx 的需求差 4 倍以上却强制共用 dream 路由,
|
|
184
|
+
// dreamProvider 指向小 ctx 模型时总览当场 CONTEXT_WINDOW_EXCEEDED(实测 120,969
|
|
185
|
+
// tokens > 32,768),指向大 ctx 模型则 consolidate 的卸载收益归零。未配置 = 回落
|
|
186
|
+
// dream 路由,行为逐字节不变。
|
|
187
|
+
dreamSummaryProvider: z.string().default("").description("记忆总览(dream_summarize)专用模型服务商,留空用巩固模型。总览输入为全库(或 dreamSummaryMaxInputs 上限),ctx 需求远大于 consolidation,建议大 ctx 模型(issue #258)。"),
|
|
188
|
+
dreamSummaryModel: z.string().default("").description("记忆总览(dream_summarize)专用模型,留空用巩固模型;总览输入随库增长,建议大 ctx 非思考模型(issue #258)。"),
|
|
189
|
+
// Issue #258:总览输入条数硬上限。0 = 不设上限(历史行为,库增长可能撑爆小
|
|
190
|
+
// ctx 模型);>0 时按 updated_at 倒序保留最新的 N 条(与 consolidate 窗口同一
|
|
191
|
+
// 排序口径)。总览是常驻叙述而非逐条巩固,限输入只影响口径脚注里的条数。
|
|
192
|
+
dreamSummaryMaxInputs: z.natural().min(0).max(100000).default(0),
|
|
138
193
|
// hybrid 判"高相似"的阈值;0.85 与 sleep normal 档对齐——两个模块对"高相似"
|
|
139
194
|
// 保持同一个定义。
|
|
140
195
|
dreamCandidateMinSim: z.number().min(0.5).max(0.99).default(0.85),
|
|
@@ -145,6 +200,14 @@ export const Config = z.object({
|
|
|
145
200
|
dreamNarrativeEnabled: z.boolean().default(false),
|
|
146
201
|
// 成簇门槛:共享同一 tag 的记忆 ≥ 此值才合成叙述条。
|
|
147
202
|
dreamNarrativeMinCluster: z.natural().min(2).max(20).default(3),
|
|
203
|
+
// document 型记忆(#164/#230,opt-in):agent 产长文档的指针行——注册校验
|
|
204
|
+
// (文件存在 + 路径合法 + evidence 求交)、摘要 + doc_path 落库、C2 比对
|
|
205
|
+
// 去重、supersede 记账。全文归 agent,管线零触碰;默认关=行为与此前一致;
|
|
206
|
+
// lightMode 强制关闭。
|
|
207
|
+
documentMemoryEnabled: z.boolean().default(false),
|
|
208
|
+
// document 摘要行的注入预算(#230 拍板):次优先档内最多注入的 document
|
|
209
|
+
// 行数,超预算跳过由后续候选补位。只约束注入,不约束检索。
|
|
210
|
+
documentInjectBudget: z.natural().min(1).max(5).default(2),
|
|
148
211
|
// 隐式 keep(v0.4.4):LLM 未提及的 snapshot 记忆自动补 {action:"keep"},
|
|
149
212
|
// 避免"未覆盖即全拒"白白浪费整轮 run。设为 false 时保留旧的严格校验
|
|
150
213
|
// (未覆盖即拒绝整单)。
|
|
@@ -185,6 +248,18 @@ export const Config = z.object({
|
|
|
185
248
|
localEmbedDimension: z.natural().default(512),
|
|
186
249
|
localEmbedDevice: z.union([z.const("cpu"), z.const("gpu")]).default("cpu"),
|
|
187
250
|
localEmbedBatchSize: z.natural().min(1).max(64).default(8),
|
|
251
|
+
// 本地嵌入的池化方式(**bug 修复,不是新能力**):BGE 系是按 CLS 池化训练的——
|
|
252
|
+
// 模型自带的 `1_Pooling/config.json` 明确写着 `pooling_mode_cls_token: true` /
|
|
253
|
+
// `pooling_mode_mean_tokens: false`,官方 README 也是「select the last hidden state of
|
|
254
|
+
// the first token」+ L2 normalize;而 transformers.js 的 `feature-extraction` 默认 mean。
|
|
255
|
+
// ⇒ 此前本地嵌入对 BGE 系一直用错池化:**不报错、只是向量系统性偏差**,检索质量静默受损。
|
|
256
|
+
// 'auto' = 按模型族判定(BGE → cls,其余 → mean,未受影响的模型行为不变);也可显式钉住。
|
|
257
|
+
// ⚠️ 池化决定向量空间,改它会改变 modelHash(见 local-embedder.js),既有索引会被判失配并重建。
|
|
258
|
+
localEmbedPooling: z.union([
|
|
259
|
+
z.const("auto"),
|
|
260
|
+
z.const("cls"),
|
|
261
|
+
z.const("mean")
|
|
262
|
+
]).default("auto"),
|
|
188
263
|
|
|
189
264
|
// Ollama embedder.
|
|
190
265
|
ollamaBaseUrl: z.string().default("http://localhost:11434"),
|
|
@@ -388,6 +463,12 @@ export const Config = z.object({
|
|
|
388
463
|
z.const("high"),
|
|
389
464
|
z.const("none")
|
|
390
465
|
]).description("同 dreamReasoningEffort:sleep 各阶段 LLM 的推理档位,未配置 = 自动取模型支持的最低档;显式 'none' = 不发送字段、用服务商自带默认。"),
|
|
466
|
+
// Issue #257:sleep 冲突/模式两阶段的输出预算(原硬编码 2048)。实测默认档
|
|
467
|
+
// 每对裁决约 90 token、24 对 2097——2048 恰好压在边界(53 次运行 48 败);
|
|
468
|
+
// full 档六分支实测约 290 token/对、24 对 6967,2048 必然截断。默认 8192
|
|
469
|
+
// 覆盖实测峰值(候选对按「每记忆至多一对」去重,饱和在 ~25 对、不随库无限
|
|
470
|
+
// 增长);流式计费按实际用量,不按上限。
|
|
471
|
+
sleepMaxTokens: z.natural().min(256).max(131072).default(8192).description("sleep 冲突消解与模式发现阶段的 LLM 输出预算上限(token)。原为硬编码 2048,sleepActionSet=full 实测需约 7000 导致裁决被截断而整轮失败;流式计费按实际用量,调大不增加成本。"),
|
|
391
472
|
|
|
392
473
|
// --- epistemic trust: memory source credibility (v0.4.5) -----------------
|
|
393
474
|
// Distinguish memories by source: observation (measured / witnessed),
|
|
@@ -419,11 +500,18 @@ export const Config = z.object({
|
|
|
419
500
|
}).default({}),
|
|
420
501
|
|
|
421
502
|
// --- LLM audit trail (Bug8) ------------------------------------------------
|
|
422
|
-
// Records every background LLM call
|
|
423
|
-
//
|
|
424
|
-
// and
|
|
425
|
-
//
|
|
426
|
-
//
|
|
503
|
+
// Records every background LLM call into llm_audit_logs: tokens, duration,
|
|
504
|
+
// status and which trigger produced it. Failures are recorded as
|
|
505
|
+
// status='error' and never block the feature. retentionDays bounds the table:
|
|
506
|
+
// older rows are purged on boot.
|
|
507
|
+
//
|
|
508
|
+
// Covered trigger_source values: autoDream (consolidation + summary),
|
|
509
|
+
// autoSummarize (compression), sleep (conflict + pattern) and entityExtract
|
|
510
|
+
// (issue #250 — the last two were structurally missing the hook, not a
|
|
511
|
+
// deliberate narrowing). entityExtract runs on every memory write, so row
|
|
512
|
+
// growth scales with write volume; retentionDays caps the ceiling but not the
|
|
513
|
+
// rate. If that turns out too fast, sample by operation_type or give it a
|
|
514
|
+
// separate retention — do not add a second gate: this flag is the single one.
|
|
427
515
|
llmAudit: z.object({
|
|
428
516
|
enabled: z.boolean().default(true),
|
|
429
517
|
retentionDays: z.natural().min(1).max(3650).default(90)
|
|
@@ -481,6 +569,17 @@ export const Config = z.object({
|
|
|
481
569
|
// recall_runs 滚动清理保留天数。
|
|
482
570
|
recallRetentionDays: z.natural().min(1).max(3650).default(90),
|
|
483
571
|
|
|
572
|
+
// --- tool exposure: 慢模型/轻量模型的工具往返节流(v0.8.5)----------------
|
|
573
|
+
// 跨会话记忆已由 inject.js 每轮自动注入系统提示词,memory_search 只用于
|
|
574
|
+
// 「注入里没有、需要深挖」的补充检索;memory_archive 是隐藏/恢复条目的整理
|
|
575
|
+
// 操作,正常会话里很少需要。轻量模型对「何时该用工具」判断弱,容易每轮
|
|
576
|
+
// 顺手调一遍——每次工具调用都是一次串行往返(生成参数→执行→回填→再生成),
|
|
577
|
+
// 在慢模型上会被放大成明显卡顿。这两个开关允许直接隐藏对应工具(默认全
|
|
578
|
+
// 开=行为不变),隐藏后模型根本看不到它,也就不会调。也走 feature_flags
|
|
579
|
+
// 白名单,面板可启停=线上回滚开关。
|
|
580
|
+
disableMemorySearch: z.boolean().default(false),
|
|
581
|
+
disableMemoryArchive: z.boolean().default(false),
|
|
582
|
+
|
|
484
583
|
// --- scope: v0.8.0 A1 存储层(issue #17)--------------------------------
|
|
485
584
|
// 总开关默认关:关闭时写入不标注 scope、去重维持 (type, title) 现状,行为
|
|
486
585
|
// 逐字节不变。开启后 memory_save 写入 agent_scope(session header 的
|
|
@@ -515,6 +614,8 @@ const LIGHT_MODE_OFF = [
|
|
|
515
614
|
"entityRecallEnabled",
|
|
516
615
|
// 轻量模式不开叙述条(额外 LLM 调用;#164 对齐,opt-in)。
|
|
517
616
|
"dreamNarrativeEnabled",
|
|
617
|
+
// 轻量模式不开 document 指针行(#230,opt-in:注册/注入/检索增强全随闸)。
|
|
618
|
+
"documentMemoryEnabled",
|
|
518
619
|
// 轻量模式不开热计算(heat 属于重型增强;关掉后 sleep 降级也退回纯时间分层)。
|
|
519
620
|
"heatEnabled"
|
|
520
621
|
];
|
package/lib/document.js
ADDED
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
// dsh-mneme/src/document.js
|
|
2
|
+
// document 型记忆注册(issue #230,口径基线 = #164 设计稿评审线):agent 产
|
|
3
|
+
// 长文档的指针行铸造口。写入权分离——全文归 agent(管线对文件零读零写零改),
|
|
4
|
+
// 管线只做四件事:注册校验(文件存在 + 路径合法 + evidence 求交)、摘要 +
|
|
5
|
+
// doc_path 落库(库里只存这三样)、C2 vector 档比对去重、supersede 记账
|
|
6
|
+
// (出新版 = 新摘要行 supersede 旧行,旧文件不删,content_history 可追溯)。
|
|
7
|
+
//
|
|
8
|
+
// 独立成模块(AGENTS.md 尺寸约定,同 recallStats 先例):service.js 已过
|
|
9
|
+
// 2000 行参考线,这里只依赖注入 service 内部件,barrel 出口在 service.js,
|
|
10
|
+
// 调用方零改动。防绕过三层兜底:service 两条业务守卫(saveWithDedupe /
|
|
11
|
+
// updateMemory)+ 存储层唯一铸造口(store.saveDocument,通用 save/update/
|
|
12
|
+
// CAS 整类拒绝 document)——注册是唯一铸造口。
|
|
13
|
+
|
|
14
|
+
import { statSync } from "node:fs";
|
|
15
|
+
import { homedir } from "node:os";
|
|
16
|
+
import { isAbsolute, resolve } from "node:path";
|
|
17
|
+
import { scopeKeyOf } from "./scope.js";
|
|
18
|
+
import { cosineSimilarity } from "./dream/clustering.js";
|
|
19
|
+
|
|
20
|
+
// C2 vector 档阈值(#164 拍板 0.92,与 findSessionDuplicate 的 vector 档同源)。
|
|
21
|
+
// 刻意不做 24h 时间窗:那个窗是会话去重的「近邻界」(findSessionDuplicate 注释),
|
|
22
|
+
// 不是相似度语义本身;document 版本更新天然跨天/跨周,套窗会把一周后重注册的
|
|
23
|
+
// 新版判成「无近重复」,留下两行活跃摘要漂移——supersede 探测必须全时段。
|
|
24
|
+
const DOCUMENT_MIN_SIM = 0.92;
|
|
25
|
+
const CANDIDATE_LIMIT = 200;
|
|
26
|
+
|
|
27
|
+
// 追加到被取代行正文的指针注记。刻意不带语言分支:id 是跨语言稳定键(用户
|
|
28
|
+
// 对 tag/章节名建议用英文同理——指针性内容不翻译);可追溯链 = 注记里的新行
|
|
29
|
+
// id + 旧行 content_history 里的旧摘要(source=superseded)。
|
|
30
|
+
const supersededByPointer = (id) => `\n\n[superseded by ${id}]`;
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Build the document registrar. Injected deps are service.js closure members
|
|
34
|
+
* (store / embedQuery / transaction / finalize) so this module stays free of
|
|
35
|
+
* service-internal wiring; `finalize` is the single write epilogue (mirror
|
|
36
|
+
* sync + notify + re-embed) so callers never duplicate it.
|
|
37
|
+
*
|
|
38
|
+
* @returns {(payload: object) => Promise<object>}
|
|
39
|
+
* `{ action: "created"|"superseded", memory, superseded?, evidence_kept,
|
|
40
|
+
* evidence_dropped, degraded }`
|
|
41
|
+
* @throws flag 关 / 路径非法或文件缺失 / title 或 summary 为空 / evidence 全部
|
|
42
|
+
* 捏造 / 仅 vector 近重复(疑似重复外档,交 agent 裁决而不是静默二选一)。
|
|
43
|
+
*/
|
|
44
|
+
export function createDocumentRegistrar({ store, config, embedQuery, pushContentHistory, transaction, finalize }) {
|
|
45
|
+
return async function registerDocument(payload, { hiddenEvidenceIds = [] } = {}) {
|
|
46
|
+
// opt-in 总闸(#230 对齐 #228 形态):关 = 注册入口整体不存在,错误信息
|
|
47
|
+
// 指回配置键,agent 能准确转告用户去开。
|
|
48
|
+
if (config?.documentMemoryEnabled !== true) {
|
|
49
|
+
throw new Error("document memory is disabled (documentMemoryEnabled)");
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// --- 1. 路径合法 + 文件存在 --------------------------------------------
|
|
53
|
+
// 只认展开 ~ 后的绝对路径:相对路径依赖进程 cwd,注册时的 cwd 与读取时
|
|
54
|
+
// 的 cwd 不保证一致,指针必须自带完整定位。校验存在 + 常规文件 + 非空——
|
|
55
|
+
// 空文件/被截断成 0 字节是「文字结构意外破坏」的最早信号,注册时拦住
|
|
56
|
+
// 而不是落一条死指针(回退路径:agent 修好文件后重注册,同 path/title
|
|
57
|
+
// 自动 supersede 旧行)。
|
|
58
|
+
const rawPath = String(payload?.path ?? "").trim();
|
|
59
|
+
if (!rawPath) throw new Error("registerDocument: path is required");
|
|
60
|
+
// 只展开 ~ / ~/(~user 形式拒收为普通相对路径走 isAbsolute 拦截——不猜测
|
|
61
|
+
// 其他用户的 home,错展开比报错更糟)。
|
|
62
|
+
const isTilde = rawPath === "~" || rawPath.startsWith("~/") || rawPath.startsWith("~\\");
|
|
63
|
+
const absolute = isTilde ? resolve(homedir(), rawPath.slice(2)) : rawPath;
|
|
64
|
+
if (!isAbsolute(absolute)) {
|
|
65
|
+
throw new Error(`registerDocument: path must be absolute (got "${rawPath}")`);
|
|
66
|
+
}
|
|
67
|
+
// 所有绝对路径统一 resolve 归一化(. / .. / 重复分隔符)——归一化不做的话,
|
|
68
|
+
// D:\a\.\b.md 与 D:\a\b.md 会被判成两个路径,同文件绕过 same-path
|
|
69
|
+
// supersede,留下双活跃指针。realpathSync 刻意不用:符号链接合并会改写
|
|
70
|
+
// agent 提交的路径字面量,指针的可读性比追符号链更重要。
|
|
71
|
+
const expanded = resolve(absolute);
|
|
72
|
+
let stat = null;
|
|
73
|
+
try { stat = statSync(expanded); } catch { /* missing/unreadable → null */ }
|
|
74
|
+
if (!stat?.isFile() || stat.size <= 0) {
|
|
75
|
+
throw new Error(`registerDocument: document file not found or empty: ${expanded}`);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// --- 2. evidence 求交(sleep.js 扫描集求交同款语义)---------------------
|
|
79
|
+
// 引用的原子条必须真实存在且未归档(归档行已被降级,不再为新鲜 document
|
|
80
|
+
// 背书)。红线 4 宽容形态:合法子集落库 + evidence_degraded 系统标记;
|
|
81
|
+
// 提供了 evidence 但全军覆没 = 捏造信号,整单拒绝,agent 修正后重试。
|
|
82
|
+
const wanted = [...new Set(
|
|
83
|
+
(Array.isArray(payload?.evidence) ? payload.evidence : [])
|
|
84
|
+
.map((id) => String(id ?? "").trim())
|
|
85
|
+
.filter(Boolean)
|
|
86
|
+
)];
|
|
87
|
+
// strictScope(tools 层传入,#170 复核项 4 同款):调用者 scope 看不见的
|
|
88
|
+
// 行按「不存在」处理——不得为跨 scope id 建立引用(存在性泄漏),与
|
|
89
|
+
// unknown/archived 同落 dropped,evidence_kept/dropped 计数保持诚实。
|
|
90
|
+
const hidden = new Set((Array.isArray(hiddenEvidenceIds) ? hiddenEvidenceIds : []).map((id) => String(id)));
|
|
91
|
+
const kept = [];
|
|
92
|
+
const dropped = [];
|
|
93
|
+
for (const id of wanted) {
|
|
94
|
+
const row = hidden.has(id) ? null : store.getById(id);
|
|
95
|
+
(row && !row.archived ? kept : dropped).push(id);
|
|
96
|
+
}
|
|
97
|
+
if (wanted.length > 0 && kept.length === 0) {
|
|
98
|
+
throw new Error(
|
|
99
|
+
`registerDocument: all ${wanted.length} evidence ids are unknown, archived or out of scope — fabricated evidence is rejected`
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const title = String(payload?.title ?? "").trim();
|
|
104
|
+
if (!title) throw new Error("registerDocument: title is required");
|
|
105
|
+
const summary = String(payload?.summary ?? "").trim();
|
|
106
|
+
if (!summary) throw new Error("registerDocument: summary is required");
|
|
107
|
+
|
|
108
|
+
// --- 3. supersede 目标探测(三层)---------------------------------------
|
|
109
|
+
// 同 doc_path / 同标题 = agent 显式的「出新版」意图 → supersede;仅 vector
|
|
110
|
+
// 近重复而路径标题都不同 = 疑似重复外档 → 拒绝并指路。C2×C1 硬线:比对
|
|
111
|
+
// 不得越过矛盾裁决替 agent 二选一——改既有行还是归档后重注册,判断权留在
|
|
112
|
+
// agent 手里。去重键带 scope(scopeKeyOf):跨作用域永不互判(防泄漏,与
|
|
113
|
+
// saveWithDedupe 的 scopeMatches 同口径)。
|
|
114
|
+
const scopeMatches = (m) =>
|
|
115
|
+
scopeKeyOf(m.agent_scope) === scopeKeyOf(payload?.agent_scope) &&
|
|
116
|
+
scopeKeyOf(m.workspace_scope) === scopeKeyOf(payload?.workspace_scope) &&
|
|
117
|
+
scopeKeyOf(m.sensitivity) === scopeKeyOf(payload?.sensitivity);
|
|
118
|
+
const samePath = (a, b) => {
|
|
119
|
+
if (!a || !b) return false;
|
|
120
|
+
// Windows 文件系统大小写不敏感:比较前归一,否则 C:\a.md 与 c:\A.md
|
|
121
|
+
// 会被判成两个版本。
|
|
122
|
+
const norm = (p) => (process.platform === "win32" ? String(p).toLowerCase() : String(p));
|
|
123
|
+
return norm(a) === norm(b);
|
|
124
|
+
};
|
|
125
|
+
// 精确层(同路径/同标题)必须全量扫描、不允许窗口截断:>200 行活跃
|
|
126
|
+
// document 时,窗口外的同路径旧版漏检会留下双活跃版本——supersede 判定
|
|
127
|
+
// 是正确性要求,不是性能优化对象。vector 档比对保留 CANDIDATE_LIMIT
|
|
128
|
+
// (getEmbeddings + 两两 cosine 才是有界对象)。
|
|
129
|
+
const scopeMatched = store
|
|
130
|
+
.list({ type: "document", limit: null })
|
|
131
|
+
.filter((m) => scopeMatches(m));
|
|
132
|
+
const explicitTarget = scopeMatched.find((m) => samePath(m.doc_path, expanded))
|
|
133
|
+
?? scopeMatched.find((m) => m.title.trim() === title);
|
|
134
|
+
if (!explicitTarget) {
|
|
135
|
+
// vector 档:embedder 不可用/向量缺失一律跳过——去重是增强不是写入依赖
|
|
136
|
+
// (findSessionDuplicate 同原则)。probe 用 title+summary,与行向量
|
|
137
|
+
// (同两段拼接 embed)同构。
|
|
138
|
+
const candidates = scopeMatched.slice(0, CANDIDATE_LIMIT);
|
|
139
|
+
try {
|
|
140
|
+
const vecs = store.getEmbeddings(candidates.map((m) => m.id));
|
|
141
|
+
if (vecs.size) {
|
|
142
|
+
const probe = await embedQuery([title, summary].filter(Boolean).join("\n"));
|
|
143
|
+
if (probe) {
|
|
144
|
+
let best = null;
|
|
145
|
+
for (const m of candidates) {
|
|
146
|
+
const v = vecs.get(m.id);
|
|
147
|
+
if (!v) continue; // 无向量的既有行不参与比对(无信号 = 不判定)
|
|
148
|
+
const sim = cosineSimilarity(probe, v);
|
|
149
|
+
if (sim >= DOCUMENT_MIN_SIM && (!best || sim > best.sim)) best = { m, sim };
|
|
150
|
+
}
|
|
151
|
+
if (best) {
|
|
152
|
+
throw new Error(
|
|
153
|
+
`registerDocument: summary is a near duplicate (C2 >= ${DOCUMENT_MIN_SIM}, sim ${best.sim.toFixed(3)}) ` +
|
|
154
|
+
`of document row ${best.m.id} "${best.m.title}" — update that row via memory_update, or archive it ` +
|
|
155
|
+
`before registering a separate document`
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
} catch (e) {
|
|
161
|
+
// 近重复拒绝是业务结果,原样上抛;embedder 故障吞掉回落(只做路径/标题层)。
|
|
162
|
+
if (e instanceof Error && e.message.startsWith("registerDocument:")) throw e;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// --- 4. 落库 + supersede 记账(同事务)----------------------------------
|
|
167
|
+
// 新摘要行:库里只存 summary + doc_path + evidence(+ scope 标注),旧文件
|
|
168
|
+
// 零触碰。supersede:旧行 content_history 存旧摘要(source=superseded)、
|
|
169
|
+
// 正文追加指向新行的注记、归档(降可及性可复活,无物理删除)。
|
|
170
|
+
const userTags = (Array.isArray(payload?.tags) ? payload.tags : [])
|
|
171
|
+
.map((t) => String(t ?? "").trim())
|
|
172
|
+
.filter(Boolean);
|
|
173
|
+
const tags = [...new Set([...userTags, ...(dropped.length ? ["evidence_degraded"] : [])])];
|
|
174
|
+
const result = transaction(() => {
|
|
175
|
+
// saveDocument 是存储层唯一 document 铸造口(通用 save/update/CAS 拒绝
|
|
176
|
+
// document——CodeRabbit 复核 #8/#882):registerDocument 传入的 doc_path
|
|
177
|
+
// 已过归一化与文件校验。
|
|
178
|
+
const created = store.saveDocument({
|
|
179
|
+
type: "document",
|
|
180
|
+
title,
|
|
181
|
+
content: summary,
|
|
182
|
+
tags,
|
|
183
|
+
importance: Number.isInteger(payload?.importance)
|
|
184
|
+
? Math.min(5, Math.max(1, payload.importance))
|
|
185
|
+
: 3,
|
|
186
|
+
source: payload?.source ?? "tool",
|
|
187
|
+
evidence: kept,
|
|
188
|
+
doc_path: expanded,
|
|
189
|
+
// sensitivity 进 supersede 匹配键(scopeKeyOf 第三维):不落库的话,
|
|
190
|
+
// 首注册带 sensitivity、重注册同键会在匹配时判成两 scope,旧版漏检。
|
|
191
|
+
...(payload?.sensitivity !== undefined ? { sensitivity: payload.sensitivity } : {}),
|
|
192
|
+
...(payload?.agent_scope !== undefined
|
|
193
|
+
? { agent_scope: payload.agent_scope, agent_scope_source: payload.agent_scope_source }
|
|
194
|
+
: {}),
|
|
195
|
+
...(payload?.workspace_scope !== undefined
|
|
196
|
+
? { workspace_scope: payload.workspace_scope, workspace_scope_source: payload.workspace_scope_source }
|
|
197
|
+
: {})
|
|
198
|
+
});
|
|
199
|
+
let superseded;
|
|
200
|
+
const loser = explicitTarget ? store.getById(explicitTarget.id) : null;
|
|
201
|
+
if (loser && !loser.archived) {
|
|
202
|
+
store.update(loser.id, {
|
|
203
|
+
content: `${loser.content}${supersededByPointer(created.id)}`,
|
|
204
|
+
content_history: pushContentHistory(loser, "superseded")
|
|
205
|
+
});
|
|
206
|
+
store.setArchived(loser.id, true);
|
|
207
|
+
superseded = store.getById(loser.id);
|
|
208
|
+
}
|
|
209
|
+
return { created, superseded };
|
|
210
|
+
});
|
|
211
|
+
finalize(result.superseded ? [result.created, result.superseded] : [result.created]);
|
|
212
|
+
return {
|
|
213
|
+
action: result.superseded ? "superseded" : "created",
|
|
214
|
+
memory: result.created,
|
|
215
|
+
...(result.superseded ? { superseded: result.superseded } : {}),
|
|
216
|
+
evidence_kept: kept.length,
|
|
217
|
+
evidence_dropped: dropped.length,
|
|
218
|
+
degraded: dropped.length > 0
|
|
219
|
+
};
|
|
220
|
+
};
|
|
221
|
+
}
|
package/lib/dream/sleep.js
CHANGED
|
@@ -49,12 +49,22 @@ function parseJsonArray(text) {
|
|
|
49
49
|
}
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
-
/**
|
|
53
|
-
|
|
52
|
+
/**
|
|
53
|
+
* Same stream consumption contract as dream.js.
|
|
54
|
+
*
|
|
55
|
+
* `onUsage`(optional, issue #250)receives any usage chunk for token accounting.
|
|
56
|
+
* dream.js 那份一直有它;sleep 这份副本没有,导致 conflict / pattern 两条链路的
|
|
57
|
+
* token 从未进 llm_audit_logs(面板因此只看到 autoDream 与 autoSummarize)。
|
|
58
|
+
*/
|
|
59
|
+
async function streamText(ctx, options, onUsage, onStreamError) {
|
|
54
60
|
if (!ctx?.llm?.stream) return undefined;
|
|
55
61
|
let text = "";
|
|
56
62
|
for await (const chunk of ctx.llm.stream(options)) {
|
|
57
63
|
if (chunk.type === "text-delta" && typeof chunk.text === "string") text += chunk.text;
|
|
64
|
+
// DSH 的 StreamChunk 契约把用量嵌在 chunk.usage(TokenUsage:inputTokens /
|
|
65
|
+
// outputTokens / totalTokens);兼容直接平铺在 chunk 上的旧形态。归一放在
|
|
66
|
+
// 这里,调用侧的 reporter 只需认平铺形状——与 dream.js:239 同一处理。
|
|
67
|
+
if (chunk.type === "usage" && typeof onUsage === "function") onUsage(chunk.usage ?? chunk);
|
|
58
68
|
if (chunk.type === "finish" && (chunk.reason?.kind === "error" || chunk.reason?.kind === "aborted")) {
|
|
59
69
|
// Same rc.1 error-as-finish-chunk behavior as dream.js — surface the
|
|
60
70
|
// cause instead of discarding it.
|
|
@@ -67,6 +77,79 @@ async function streamText(ctx, options, onStreamError) {
|
|
|
67
77
|
return text;
|
|
68
78
|
}
|
|
69
79
|
|
|
80
|
+
/**
|
|
81
|
+
* Issue #250:给 sleep 的一条 LLM 调用记一笔 llm_audit_logs(token / 时长 / 状态 /
|
|
82
|
+
* 触发源)。与 dream.js 的 runAuditedLlm 同口径但各自独立一份——dream 那两个
|
|
83
|
+
* checker 的调用形状绑定着它自己的排程细节,硬抽共享抽象只会收敛回 dream 的形态。
|
|
84
|
+
*
|
|
85
|
+
* 记账是 best-effort:写审计行失败只 warn,绝不反噬 sleep 本体(CONTRIBUTING 的
|
|
86
|
+
* fail-safe 硬约定)。`body(reportUsage)` 负责真实消费流,usage chunk 经
|
|
87
|
+
* reportUsage 上报。
|
|
88
|
+
*
|
|
89
|
+
* 审计诚实性:流被中止/报错时 body 返回 undefined,记 status='error';流式成功但
|
|
90
|
+
* 输出不可用时 `spec.auditError` 给出原因,同样记 error——否则同一轮调用会在
|
|
91
|
+
* 「phase 报 failed」与「audit 报 success」之间自相矛盾(CONTRIBUTING 的审计诚实性
|
|
92
|
+
* 硬约定)。`auditError` 必须是**纯判据函数**:调用方的控制流不能依赖它的副作用,
|
|
93
|
+
* 因为审计关闭时这里整段早退、钩子根本不会被调用(dream 正是那样踩的,见 PR 正文)。
|
|
94
|
+
*/
|
|
95
|
+
async function auditedSleepLlm(ctx, service, config, logger, spec, body) {
|
|
96
|
+
if (config?.llmAudit?.enabled === false || typeof service?.saveLlmAudit !== "function") {
|
|
97
|
+
return body(() => {});
|
|
98
|
+
}
|
|
99
|
+
const startedAt = Date.now();
|
|
100
|
+
const timestamp = new Date(startedAt).toISOString();
|
|
101
|
+
let inputTokens = 0;
|
|
102
|
+
let outputTokens = 0;
|
|
103
|
+
let status = "success";
|
|
104
|
+
let errorMessage = null;
|
|
105
|
+
try {
|
|
106
|
+
const result = await body((usage) => {
|
|
107
|
+
if (!usage) return;
|
|
108
|
+
const i = usage.input_tokens ?? usage.inputTokens ?? usage.prompt_tokens ?? usage.promptTokens;
|
|
109
|
+
const o = usage.output_tokens ?? usage.outputTokens ?? usage.completion_tokens ?? usage.completionTokens;
|
|
110
|
+
if (Number.isFinite(i)) inputTokens = i;
|
|
111
|
+
if (Number.isFinite(o)) outputTokens = o;
|
|
112
|
+
});
|
|
113
|
+
if (result === undefined) {
|
|
114
|
+
status = "error";
|
|
115
|
+
const streamErr = typeof spec.streamError === "function" ? String(spec.streamError() ?? "") : "";
|
|
116
|
+
errorMessage = streamErr ? `llm stream aborted or errored (${streamErr})` : "llm stream aborted or errored";
|
|
117
|
+
} else if (typeof spec.auditError === "function") {
|
|
118
|
+
// 流式成功但输出不可用,仍然是一次失败的调用——记 error,不与调用方的
|
|
119
|
+
// failed 返回值打架。
|
|
120
|
+
const message = spec.auditError(result);
|
|
121
|
+
if (message) {
|
|
122
|
+
status = "error";
|
|
123
|
+
errorMessage = message;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
return result;
|
|
127
|
+
} catch (error) {
|
|
128
|
+
status = "error";
|
|
129
|
+
errorMessage = String(error?.message ?? error);
|
|
130
|
+
throw error;
|
|
131
|
+
} finally {
|
|
132
|
+
try {
|
|
133
|
+
service.saveLlmAudit({
|
|
134
|
+
timestamp,
|
|
135
|
+
trigger_source: spec.triggerSource,
|
|
136
|
+
operation_type: spec.operationType,
|
|
137
|
+
model_id: spec.modelId,
|
|
138
|
+
input_tokens: inputTokens,
|
|
139
|
+
output_tokens: outputTokens,
|
|
140
|
+
total_tokens: inputTokens + outputTokens,
|
|
141
|
+
cost_usd: 0,
|
|
142
|
+
duration_ms: Date.now() - startedAt,
|
|
143
|
+
status,
|
|
144
|
+
error_message: errorMessage,
|
|
145
|
+
related_memory_ids: spec.relatedMemoryIds ?? []
|
|
146
|
+
});
|
|
147
|
+
} catch (auditError) {
|
|
148
|
+
logger?.warn?.(`dsh-mneme sleep: llm audit write failed: ${String(auditError)}`);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
70
153
|
/** LLM route (Issue #25): explicit sleepProvider/Model wins, then the dream
|
|
71
154
|
* route as a shared explicit fallback, then the agent default model. Sleep
|
|
72
155
|
* can pin a cheaper model for its bulk passes without disturbing the dream
|
|
@@ -106,7 +189,7 @@ async function phaseConflicts(ctx, service, config, logger, runId, semantic = nu
|
|
|
106
189
|
}
|
|
107
190
|
const strictness = config.sleepConflictStrictness ?? "normal";
|
|
108
191
|
const threshold = CONFLICT_THRESHOLDS[strictness] ?? CONFLICT_THRESHOLDS.normal;
|
|
109
|
-
const memories = service.all().filter((m) => !m.archived && !m.forgotten && m.type !== "summary");
|
|
192
|
+
const memories = service.all().filter((m) => !m.archived && !m.forgotten && m.type !== "summary" && m.type !== "document");
|
|
110
193
|
if (memories.length < 2) return { status: "skipped", reason: "too few memories" };
|
|
111
194
|
if (signal?.aborted) return { status: "aborted", reason: "user activity" };
|
|
112
195
|
|
|
@@ -232,17 +315,29 @@ async function phaseConflicts(ctx, service, config, logger, runId, semantic = nu
|
|
|
232
315
|
const conflictPrompt = actionSet === "full" ? STR.prompts.conflictFull[language] : STR.prompts.conflict[language];
|
|
233
316
|
const runConflict = (withEffort) => {
|
|
234
317
|
conflictStreamFailure = "";
|
|
235
|
-
return
|
|
318
|
+
return auditedSleepLlm(ctx, service, config, logger, {
|
|
319
|
+
triggerSource: "sleep",
|
|
320
|
+
operationType: "sleep_conflict",
|
|
321
|
+
modelId: `${route.provider}:${route.model}`,
|
|
322
|
+
// 候选对的双方 id:面板的活动流用 related_memory_ids 解析「沉淀条数」。
|
|
323
|
+
relatedMemoryIds: samePairs.flatMap((p) => [p.a.id, p.b.id]),
|
|
324
|
+
streamError: () => conflictStreamFailure,
|
|
325
|
+
// 纯判据:输出里没有 JSON 数组 ⇒ 这轮是失败的调用,审计记 error。**不要**把
|
|
326
|
+
// 解析结果存进闭包给下面的控制流用——审计关闭时 auditedSleepLlm 整段早退、
|
|
327
|
+
// 这个钩子不会被调用,控制流会跟着失效(dream 的既有坑)。代价是审计开启时
|
|
328
|
+
// 多解析一次,换来两条路径彻底解耦。
|
|
329
|
+
auditError: (text) => (parseJsonArray(text) ? null : "no json array in llm output")
|
|
330
|
+
}, (reportUsage) => streamText(ctx, {
|
|
236
331
|
provider: route.provider,
|
|
237
332
|
model: route.model,
|
|
238
333
|
purpose: "sleep-conflict",
|
|
239
|
-
maxTokens: 2048,
|
|
334
|
+
maxTokens: config.sleepMaxTokens ?? 2048,
|
|
240
335
|
...(withEffort && sleepEffort ? { reasoningEffort: sleepEffort } : {}),
|
|
241
336
|
messages: [
|
|
242
337
|
{ role: "system", content: [{ type: "text", text: conflictPrompt }], source: { kind: "plugin", plugin: "dsh-mneme" } },
|
|
243
338
|
{ role: "user", content: [{ type: "text", text: listText }], source: { kind: "plugin", plugin: "dsh-mneme" } }
|
|
244
339
|
]
|
|
245
|
-
}, (reason) => { conflictStreamFailure = describeStreamFailure(reason); });
|
|
340
|
+
}, reportUsage, (reason) => { conflictStreamFailure = describeStreamFailure(reason); }));
|
|
246
341
|
};
|
|
247
342
|
const text = await withEffortFallback(ctx, sleepEffort, () => runConflict(true), () => runConflict(false), () => conflictStreamFailure);
|
|
248
343
|
if (text === undefined) {
|
|
@@ -368,8 +463,10 @@ async function phasePatterns(ctx, service, config, logger, runId, signal = null)
|
|
|
368
463
|
const route = resolveSleepRoute(ctx, config, logger);
|
|
369
464
|
if (!route) return { status: "skipped", reason: "no llm route" };
|
|
370
465
|
const limit = config.sleepPatternMinMemories ?? 100;
|
|
466
|
+
// #230:document 在查询层排除(excludeTypes)而不是截断后过滤——LIMIT 200
|
|
467
|
+
// 先生效的话,指针行一多就会把普通记忆挤出扫描窗,池子饿到门槛以下。
|
|
371
468
|
const memories = service
|
|
372
|
-
.list({ limit: 200, includeForgotten: false })
|
|
469
|
+
.list({ limit: 200, includeForgotten: false, excludeTypes: ["document"] })
|
|
373
470
|
.filter((m) => !m.archived && m.type !== "summary" && m.type !== "pattern")
|
|
374
471
|
.sort((a, b) => (a.updated_at < b.updated_at ? 1 : -1))
|
|
375
472
|
.slice(0, limit);
|
|
@@ -383,17 +480,26 @@ async function phasePatterns(ctx, service, config, logger, runId, signal = null)
|
|
|
383
480
|
let patternStreamFailure = "";
|
|
384
481
|
const runPattern = (withEffort) => {
|
|
385
482
|
patternStreamFailure = "";
|
|
386
|
-
return
|
|
483
|
+
return auditedSleepLlm(ctx, service, config, logger, {
|
|
484
|
+
triggerSource: "sleep",
|
|
485
|
+
operationType: "sleep_pattern",
|
|
486
|
+
modelId: `${route.provider}:${route.model}`,
|
|
487
|
+
// 本轮扫描的记忆 id:与 dream 的 relatedMemoryIds 同义,供面板解析沉淀条数。
|
|
488
|
+
relatedMemoryIds: memories.map((m) => m.id),
|
|
489
|
+
streamError: () => patternStreamFailure,
|
|
490
|
+
// 纯判据,同 conflict 阶段:不把解析结果存进闭包供控制流使用。
|
|
491
|
+
auditError: (text) => (parseJsonArray(text) ? null : "no json array in llm output")
|
|
492
|
+
}, (reportUsage) => streamText(ctx, {
|
|
387
493
|
provider: route.provider,
|
|
388
494
|
model: route.model,
|
|
389
495
|
purpose: "sleep-pattern",
|
|
390
|
-
maxTokens: 2048,
|
|
496
|
+
maxTokens: config.sleepMaxTokens ?? 2048,
|
|
391
497
|
...(withEffort && sleepEffort ? { reasoningEffort: sleepEffort } : {}),
|
|
392
498
|
messages: [
|
|
393
499
|
{ role: "system", content: [{ type: "text", text: STR.prompts.pattern[language].replace("N", String(maxPatterns)) }], source: { kind: "plugin", plugin: "dsh-mneme" } },
|
|
394
500
|
{ role: "user", content: [{ type: "text", text: listText }], source: { kind: "plugin", plugin: "dsh-mneme" } }
|
|
395
501
|
]
|
|
396
|
-
}, (reason) => { patternStreamFailure = describeStreamFailure(reason); });
|
|
502
|
+
}, reportUsage, (reason) => { patternStreamFailure = describeStreamFailure(reason); }));
|
|
397
503
|
};
|
|
398
504
|
const text = await withEffortFallback(ctx, sleepEffort, () => runPattern(true), () => runPattern(false), () => patternStreamFailure);
|
|
399
505
|
if (text === undefined) {
|
|
@@ -505,6 +611,9 @@ function deriveStatus(phases) {
|
|
|
505
611
|
*/
|
|
506
612
|
export async function runSleep(ctx, service, config, logger, semantic = null, signal = null) {
|
|
507
613
|
const runId = randomUUID();
|
|
614
|
+
// Issue #89:开跑时刻——审计行 created_at 的基准(= 冷却种子),run 耗时
|
|
615
|
+
// (LLM 各 phase)不应计入重启后的冷却窗口。
|
|
616
|
+
const sleepStartedAt = Date.now();
|
|
508
617
|
const phases = {};
|
|
509
618
|
const attempt = async (name, fn) => {
|
|
510
619
|
if (signal?.aborted) return; // user resumed activity — stop before next phase
|
|
@@ -541,6 +650,9 @@ export async function runSleep(ctx, service, config, logger, semantic = null, si
|
|
|
541
650
|
try {
|
|
542
651
|
service.saveDreamRun({
|
|
543
652
|
id: runId,
|
|
653
|
+
// Issue #89:审计行记开跑时刻而非完成时刻——run 耗时(含 LLM 各 phase)
|
|
654
|
+
// 不该计入重启后的 sleep 冷却种子。
|
|
655
|
+
created_at: new Date(sleepStartedAt).toISOString(),
|
|
544
656
|
status,
|
|
545
657
|
provider: route?.provider,
|
|
546
658
|
model: route?.model,
|
|
@@ -580,10 +692,14 @@ export function createSleepScheduler({
|
|
|
580
692
|
onRun = null,
|
|
581
693
|
now = () => Date.now(),
|
|
582
694
|
setTimeoutFn = setTimeout,
|
|
583
|
-
clearTimeoutFn = clearTimeout
|
|
695
|
+
clearTimeoutFn = clearTimeout,
|
|
696
|
+
// Issue #89 同族:冷却时刻由调用方从 dream_runs 审计表恢复(run_type='sleep')——
|
|
697
|
+
// 内存变量进程重启即归零,冷却闸对新实例放行 → 重启后立即进入 sleep 冷却盲区。
|
|
698
|
+
// 测试注入 0(默认)即保持旧行为。
|
|
699
|
+
lastRunAtSeed = 0
|
|
584
700
|
}) {
|
|
585
701
|
let lastWriteAt = now();
|
|
586
|
-
let lastRunAt =
|
|
702
|
+
let lastRunAt = lastRunAtSeed;
|
|
587
703
|
let running = false;
|
|
588
704
|
let disposed = false;
|
|
589
705
|
let idleTimer = null;
|