@modusensus/dsh-mneme 0.8.16 → 0.9.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.
- package/README.md +16 -3
- package/assets/icon.svg +2 -2
- package/docs/CONFIGURATION.md +2 -1
- package/docs/CONTINUITY.md +18 -3
- package/docs/WORKFLOWS.md +299 -0
- package/lib/api.js +124 -8
- package/lib/candidates.js +454 -0
- package/lib/capabilities.js +819 -0
- package/lib/client.js +1703 -182
- package/lib/config.js +31 -5
- package/lib/continuity.js +35 -6
- package/lib/dream/decisions.js +17 -1
- package/lib/dream/sleep.js +10 -2
- package/lib/dream.js +5 -1
- package/lib/entities/extractor.js +4 -0
- package/lib/index.js +49 -12
- package/lib/inject.js +6 -1
- package/lib/pipeline.js +534 -0
- package/lib/scope.js +16 -4
- package/lib/serve.js +1 -1
- package/lib/service.js +59 -18
- package/lib/session-usage.js +350 -0
- package/lib/settings.js +7 -1
- package/lib/store.js +55 -3
- package/lib/summarize.js +31 -0
- package/lib/tools.js +11 -2
- package/package.json +1 -1
- package/scripts/publish-gate.mjs +197 -0
- package/src/api.js +124 -8
- package/src/candidates.js +454 -0
- package/src/capabilities.js +819 -0
- package/src/config.js +31 -5
- package/src/continuity.js +35 -6
- package/src/dream/decisions.js +17 -1
- package/src/dream/sleep.js +10 -2
- package/src/dream.js +5 -1
- package/src/entities/extractor.js +4 -0
- package/src/index.js +49 -12
- package/src/inject.js +6 -1
- package/src/pipeline.js +534 -0
- package/src/scope.js +16 -4
- package/src/serve.js +1 -1
- package/src/service.js +59 -18
- package/src/session-usage.js +350 -0
- package/src/settings.js +7 -1
- package/src/store.js +55 -3
- package/src/summarize.js +31 -0
- package/src/tools.js +11 -2
- package/test/api.test.js +147 -7
- package/test/candidates.test.js +345 -0
- package/test/capabilities.test.js +577 -0
- package/test/client.test.js +361 -2
- package/test/continuity.test.js +84 -0
- package/test/embed-ready-poll.test.js +94 -0
- package/test/entities.test.js +13 -2
- package/test/failure-query.test.js +147 -0
- package/test/failure-retention.test.js +105 -0
- package/test/graph-api.test.js +36 -2
- package/test/inject-recall.test.js +32 -4
- package/test/llm-audit.test.js +92 -1
- package/test/mint-source.test.js +202 -0
- package/test/pipeline.test.js +505 -0
- package/test/placement.test.js +1 -1
- package/test/publish-gate.test.js +272 -0
- package/test/recall-retention.test.js +128 -0
- package/test/release-workflow.test.js +77 -0
- package/test/scope-distill-stamp.test.js +135 -0
- package/test/serve.test.js +1 -1
- package/test/session-usage.test.js +308 -0
- package/test/store-write-path-columns.test.js +191 -0
- package/test/summarize.test.js +176 -0
- package/test/supersede-state-tracking.test.js +4 -4
- package/test/tools.test.js +16 -1
package/lib/config.js
CHANGED
|
@@ -91,7 +91,8 @@ export const Config = z.object({
|
|
|
91
91
|
// feature_flags 的值永远优先于这里的默认值。
|
|
92
92
|
injectGuidanceEnabled: z.boolean().default(true),
|
|
93
93
|
// #249 N3(压缩边缘双落点):上下文即将大幅精简前抢救「正在做什么」。默认关。
|
|
94
|
-
//
|
|
94
|
+
// 触发只认「历史真的被折叠了」的压缩事件(summary / prune / 不带 error 的 end;
|
|
95
|
+
// 判定见 continuity.js 的 isCompactionEdge —— start 只是「试过」,带 error 的 end 是失败收尾),
|
|
95
96
|
// 不自定一套阈值参数——「压缩边缘」由宿主定义,我们再校准一份只会与之漂移。
|
|
96
97
|
// 为什么是「新时机 + 新表面」因而默认关:它往对话里**追加消息**(新的注入表面,
|
|
97
98
|
// 参照实现里最容易累积成一堆历史的那类),并多写一张提案表。按 §10 判据,引入
|
|
@@ -481,6 +482,13 @@ export const Config = z.object({
|
|
|
481
482
|
reflectionFailureTracking: z.boolean().default(true),
|
|
482
483
|
reflectionUpdateMaxPerRun: z.natural().min(0).max(5).default(2),
|
|
483
484
|
reflectionUpdateMinAgeHours: z.natural().min(0).max(168).default(24),
|
|
485
|
+
// failure_memories 的启动期保留期(天)。此前在 index.js 里写死 90,是这一族
|
|
486
|
+
// 四张启动期清理表里唯一没有键可陈述的一条(#275 伞下的欠账)。形态对齐
|
|
487
|
+
// recallRetentionDays:顶层扁平键、只从配置文件改;暂不进 settings.js 白名单,
|
|
488
|
+
// 面板可调是后续的事(那要同时补 api.js 的 NESTED 表与 client.js 的面板行)。
|
|
489
|
+
// 下限取 1 而非 0:这张表的用途是「挖掘重复出现的纠正模式」,窗口太短就只剩
|
|
490
|
+
// 最近一次;要彻底停掉清理应当显式关掉 reflectionFailureTracking。
|
|
491
|
+
reflectionFailureRetentionDays: z.natural().min(1).max(3650).default(90),
|
|
484
492
|
|
|
485
493
|
// --- conflict freeze: manual review for conflicting memories (v0.2.1) ---
|
|
486
494
|
// Opt-in by default: when true, conflicting memories are not auto-merged
|
|
@@ -828,10 +836,15 @@ export const Config = z.object({
|
|
|
828
836
|
|
|
829
837
|
// --- scope: v0.8.0 A1 存储层(issue #17)--------------------------------
|
|
830
838
|
// 总开关默认关:关闭时写入不标注 scope、去重维持 (type, title) 现状,行为
|
|
831
|
-
//
|
|
832
|
-
//
|
|
833
|
-
//
|
|
834
|
-
//
|
|
839
|
+
// 逐字节不变。开启后**两条写入路径都自动盖章**(*_scope_source='auto'):
|
|
840
|
+
// ① 工具调用(tools.js:memory_save / memory_update,显式参数改标 'explicit')
|
|
841
|
+
// ② 机器提炼(summarize.js:会话蒸馏;合并进既有行时不盖章,作用域属于那一行
|
|
842
|
+
// 原来被提炼出来的语境——v0.9.0 补,此前只有工具路径盖章,于是「项目记忆」
|
|
843
|
+
// 在库里几乎不存在)。
|
|
844
|
+
// 两路的值同源:agent 维取 session header 的 agentPreset,workspace 维取
|
|
845
|
+
// registry 反查的 canonical path(header.cwd 兜底,取不到 NULL——解析绝不阻塞
|
|
846
|
+
// 写入),去重键扩展为 (type, title, agent_scope, workspace_scope, sensitivity)。
|
|
847
|
+
// 检索侧加权/过滤在 A2/A3 落地。
|
|
835
848
|
// 也走 feature_flags(FEATURE_FLAG_BOOLEANS 白名单),面板可启停=线上回滚开关。
|
|
836
849
|
scopeEnabled: z.boolean().default(false),
|
|
837
850
|
// strictScope(A3):硬过滤模式。关闭=A2 软隔离(他 scope 降权保留可见,
|
|
@@ -842,6 +855,19 @@ export const Config = z.object({
|
|
|
842
855
|
// 打开才有意义(没有写入标注就无可过滤维度),但独立成键:可先开标注积累
|
|
843
856
|
// 数据、观察 A2 加权质量后再切硬过滤。
|
|
844
857
|
strictScope: z.boolean().default(false),
|
|
858
|
+
|
|
859
|
+
// --- v0.9.0 M.O.(Modus Operandi · 手法,beta)--------------------------
|
|
860
|
+
// 命名:M.O. = Modus Operandi,拉丁语「一个人一贯的做事方式」,缩写 M.O.;
|
|
861
|
+
// 也正是 org 词根 modus(modusensus)。界面主显示 M.O.,设置项 hint 第一句
|
|
862
|
+
// 给全名——缩写不靠注释解释,靠用户第一眼能看见全名。
|
|
863
|
+
// 这是客户端只读视图(做法图谱 + 对话行结算卡)的门控。API 的 effective 取值只认
|
|
864
|
+
// config 顶层/嵌套路径,schema 里没有这个键 → effective 静默少键 → 客户端读
|
|
865
|
+
// 不到就一个座位都不注册(#254 漏登记 NESTED_FLAG_PATHS 是同一个坑)。
|
|
866
|
+
// 默认关:beta 功能必须 opt-in,老用户行为逐字节不变。不进 LIGHT_MODE_OFF——
|
|
867
|
+
// 它只是只读视图、不引入任何重型后台路径,轻量档用户显式勾选仍然赢(预设
|
|
868
|
+
// 该关的是重型能力,不该替用户关掉一个纯 UI 入口)。
|
|
869
|
+
// 也走 feature_flags(FEATURE_FLAG_BOOLEANS 白名单),面板可启停=线上回滚开关。
|
|
870
|
+
actionPipelineEnabled: z.boolean().default(false)
|
|
845
871
|
});
|
|
846
872
|
|
|
847
873
|
// Fields forced to false by the light-mode preset. Everything not listed here
|
package/lib/continuity.js
CHANGED
|
@@ -15,9 +15,11 @@ import { createUserMessage } from "@deepseek-ai/dsh-llm";
|
|
|
15
15
|
// 也只用本地字符串比较;门控成本为零。
|
|
16
16
|
// 4. **触发器就是宿主真的压缩**,不自定一套阈值。宿主在 `agent/pre-step`(压力)与
|
|
17
17
|
// `agent/request-error`(溢出)两处触发压缩,我们只订阅压缩事件
|
|
18
|
-
// (`compaction
|
|
18
|
+
// (`compaction/*` 一族,都在 @deepseek-ai/dsh-session 的事件白名单里
|
|
19
19
|
// `lib/types/known-event-types.js:31-34`)。自定阈值会与宿主口径漂移,还多一份没
|
|
20
20
|
// 人校准的参数;「压缩边缘」本来就有宿主自己下的定义。
|
|
21
|
+
// **但发过压缩事件不等于这段历史真的被折叠了** —— 压缩插件在每一步都会试一次,
|
|
22
|
+
// 失败时没有任何东西需要抢救。记账口径见 isCompactionEdge。
|
|
21
23
|
//
|
|
22
24
|
// 落点顺序(为什么 pre-step 里挂得住):宿主的压缩插件在它自己的 pre-step 监听器里先
|
|
23
25
|
// `await compactIfNeeded(...)` 再 `return next()`,所以我们的监听器拿到 `next()` 结果
|
|
@@ -38,7 +40,34 @@ export const CONTINUITY_NOTICE_MAX = 900;
|
|
|
38
40
|
/** 边缘表的兜底上限:只留序号、且有界——压缩完就没有下一次 pre-step 的会话不走消费路径。 */
|
|
39
41
|
const MAX_TRACKED_EDGES = 50;
|
|
40
42
|
|
|
41
|
-
|
|
43
|
+
/**
|
|
44
|
+
* 什么算「一个真的边缘」= 这段历史**真的被折叠了**。
|
|
45
|
+
*
|
|
46
|
+
* 这跟「有人发过压缩事件」不是同一个问题。压缩插件在**每一步**都会试一次压缩
|
|
47
|
+
* (`agent/pre-step` 里的 `compactIfNeeded`),所以 `compaction/start` 只在说
|
|
48
|
+
* 「有人试过」;尝试失败时没有任何东西需要抢救。按「试过就救」记账会得到一个
|
|
49
|
+
* **每步都触发**的触发器 —— 实测(2026-10-14,本机两个长会话、12,000+ 事件,
|
|
50
|
+
* 代理层把 dsh 原生压缩 100% 拒掉):
|
|
51
|
+
*
|
|
52
|
+
* · 按 `start|summary|end` 记账:1,287 次置位 / 359 次注入(4c28eaaa:2,895 / 625)
|
|
53
|
+
* · 按「真落地」记账: 39 次置位 / 32 次注入(4c28eaaa: 161 / 74)→ 降 91% / 88%
|
|
54
|
+
*
|
|
55
|
+
* 三种「真落地」:
|
|
56
|
+
* - `compaction/summary`:宿主压缩成功才落(dsh-compaction-basic 的
|
|
57
|
+
* `commitCompactionBody`,与成功路同步提交;失败路不落)。
|
|
58
|
+
* - `compaction/prune`:宿主把旧事件折叠出表面(dsh-compaction-tool-result-pruner),
|
|
59
|
+
* 是「表面真的变短了」最直接的证据。
|
|
60
|
+
* - `compaction/end`:**成败两条路都发**(同一份源码:失败路
|
|
61
|
+
* `append("compaction/end", { ...lifecycle, error: errorChain(error) })`),
|
|
62
|
+
* 所以只有**不带 error** 的那条才算落地。
|
|
63
|
+
*/
|
|
64
|
+
function isCompactionEdge(event) {
|
|
65
|
+
const type = event?.type;
|
|
66
|
+
if (type === "compaction/summary" || type === "compaction/prune") return true;
|
|
67
|
+
if (type !== "compaction/end") return false;
|
|
68
|
+
const error = event?.data?.error;
|
|
69
|
+
return error === undefined || error === null || error === "";
|
|
70
|
+
}
|
|
42
71
|
|
|
43
72
|
// 与 summarize.js 的 collectMessages 同一个口径:只取公开文本块,reasoning 私有推理
|
|
44
73
|
// 不进快照(快照可能被宿主重建进摘要)。刻意副本而不是导出——那边是会话蒸馏的内部
|
|
@@ -148,21 +177,21 @@ function noticeAlreadyInSurface(session, text) {
|
|
|
148
177
|
* 保持"挂上就是开"的单一口径。
|
|
149
178
|
*/
|
|
150
179
|
export function createContinuityRescue(ctx, store) {
|
|
151
|
-
// 待处理的边缘:sessionId →
|
|
180
|
+
// 待处理的边缘:sessionId → 压缩事件序号。**真落地**的压缩事件到达时置位,下一次 pre-step 消费后
|
|
152
181
|
// 删除(同一个边缘绝不注入第二次)。只留序号、不留 Session:插件在宿主进程里只 apply
|
|
153
182
|
// 一次、服务所有会话,握着 Session 就是握着它的全量日志不放——压缩完就收工、不再有
|
|
154
183
|
// pre-step 是常见路径,这些条目永远走不到消费路径上。
|
|
155
184
|
const edge = new Map();
|
|
156
185
|
|
|
157
186
|
const unsubscribe = ctx.on("session/event", (session, event) => {
|
|
158
|
-
if (!
|
|
187
|
+
if (!isCompactionEdge(event)) return;
|
|
159
188
|
if (!session?.id) return;
|
|
160
189
|
if (!edge.has(session.id) && edge.size >= MAX_TRACKED_EDGES) {
|
|
161
190
|
// 兜底淘汰最旧的(Map 是插入序):这里漏掉最坏也只少一次注入,不会无界增长。
|
|
162
191
|
edge.delete(edge.keys().next().value);
|
|
163
192
|
}
|
|
164
|
-
//
|
|
165
|
-
//
|
|
193
|
+
// 同一轮可能出现多条落地事件(prune 与 end 并存),后到的覆盖前一个:留着最后一个,
|
|
194
|
+
// 序号即触发证据。
|
|
166
195
|
edge.set(session.id, typeof event.seq === "number" ? event.seq : null);
|
|
167
196
|
});
|
|
168
197
|
|
package/lib/dream/decisions.js
CHANGED
|
@@ -395,6 +395,12 @@ function casGuard(service, snapshot, ids) {
|
|
|
395
395
|
* provided, every decision target is CAS-checked against it and a decision
|
|
396
396
|
* computed from stale state is skipped and reported as a conflict instead of
|
|
397
397
|
* overwriting concurrent writes (item ①).
|
|
398
|
+
* @param config - pipeline config; `config.mintSource` (optional string) is the
|
|
399
|
+
* provenance stamped on memories minted by `create` decisions. It is
|
|
400
|
+
* read from the CALLER, never from the decision object: the LLM authors
|
|
401
|
+
* `create` payloads and validateDecisions does not strip unknown fields, so
|
|
402
|
+
* trusting `d.source` would let a model label its own output as human-written.
|
|
403
|
+
* Absent/blank → saveWithDedupe's existing "manual" fallback (unchanged).
|
|
398
404
|
* @returns {{ applied: number, conflicts: Array, failures: Array, committed: Array }}
|
|
399
405
|
* applied - number of decisions/memories actually committed (archive counts
|
|
400
406
|
* each archived memory as one, merge/conflict/update count one).
|
|
@@ -484,7 +490,17 @@ function applyCreate(d, service, config = {}) {
|
|
|
484
490
|
const body = evidence.length > 0
|
|
485
491
|
? STR.evidenceSuffix[language](content, evidence)
|
|
486
492
|
: content;
|
|
487
|
-
|
|
493
|
+
// mint 来源由调用方(管道)给,不从决策对象取——LLM 能自己写 d.source,
|
|
494
|
+
// 信它就等于让模型给自造条目盖「人写的」戳(见 applyDecisions 的 @param config)。
|
|
495
|
+
// 取不到就一个字段都不传,落回 saveWithDedupe 原有的兜底,行为与改前一致。
|
|
496
|
+
const mintSource = typeof config?.mintSource === "string" ? config.mintSource.trim() : "";
|
|
497
|
+
const created = service.saveWithDedupe({
|
|
498
|
+
type,
|
|
499
|
+
title,
|
|
500
|
+
content: body,
|
|
501
|
+
importance,
|
|
502
|
+
...(mintSource ? { source: mintSource } : {})
|
|
503
|
+
});
|
|
488
504
|
const memory = created?.memory;
|
|
489
505
|
if (!memory) return "skipped"; // deduped/subsumed: nothing minted, clean no-op
|
|
490
506
|
return { applied: 1, committed: { action: "create", id: memory.id, type } };
|
package/lib/dream/sleep.js
CHANGED
|
@@ -381,7 +381,11 @@ async function phaseConflicts(ctx, service, config, logger, runId, semantic = nu
|
|
|
381
381
|
allowedActions: actionSet === "full" ? null : LEGACY_ACTIONS
|
|
382
382
|
});
|
|
383
383
|
if (!ok) return { status: "failed", error: `invalid decisions: ${errors.join("; ")}`, skipped };
|
|
384
|
-
|
|
384
|
+
// 同 phasePatterns——本相位的 create 也落 source=sleep(默认档的
|
|
385
|
+
// LEGACY_ACTIONS 含 create,理论上可 mint)。
|
|
386
|
+
const { applied, failures, conflicts } = applyDecisions(
|
|
387
|
+
decisions, service, logger, snapshot, { ...config, mintSource: "sleep" }
|
|
388
|
+
);
|
|
385
389
|
// Issue #126 review(Copilot):事务内 service.update 会跳过 scheduleEmbed(txDepth>0),
|
|
386
390
|
// 而 sleep 路径此前没有 dream 那样的 post-apply 索引维护——differentiate 的差异注记与
|
|
387
391
|
// supersede 的归档都反映不到缓存向量,"注记进 embedding 防再判重复"就是空承诺。
|
|
@@ -525,7 +529,11 @@ async function phasePatterns(ctx, service, config, logger, runId, signal = null)
|
|
|
525
529
|
maxCreatePerRun: maxPatterns
|
|
526
530
|
});
|
|
527
531
|
if (!ok) return { status: "failed", error: `invalid decisions: ${errors.join("; ")}` };
|
|
528
|
-
|
|
532
|
+
// 本相位的 create 落 source=sleep。来源从管道给而非从决策对象取——见
|
|
533
|
+
// applyDecisions 的 @param config。
|
|
534
|
+
const { applied, failures, conflicts } = applyDecisions(
|
|
535
|
+
decisions, service, logger, snapshot, { ...config, mintSource: "sleep" }
|
|
536
|
+
);
|
|
529
537
|
return {
|
|
530
538
|
status: applied > 0 ? "ok" : "noop",
|
|
531
539
|
scanned: memories.length,
|
package/lib/dream.js
CHANGED
|
@@ -1309,7 +1309,11 @@ export function createDreamScheduler({ onRun, thresholdCount = 10, thresholdChar
|
|
|
1309
1309
|
// a target changed during the LLM call is skipped and reported as a
|
|
1310
1310
|
// conflict instead of being overwritten (item ①). Frozen conflicts are
|
|
1311
1311
|
// excluded from this list (they are parked, not applied).
|
|
1312
|
-
|
|
1312
|
+
// mint 来源:主链路的 create 落 source=dream。来源从管道给而非从决策对象取——
|
|
1313
|
+
// 见 applyDecisions 的 @param config。
|
|
1314
|
+
const { applied, conflicts, failures, committed } = applyDecisions(
|
|
1315
|
+
applyList, service, logger, snapshot, { ...config, mintSource: "dream" }
|
|
1316
|
+
);
|
|
1313
1317
|
// Per-record receipt chain: one row per actually-committed merge/conflict/
|
|
1314
1318
|
// update verdict, stamped with the decision-basis digest + idempotency
|
|
1315
1319
|
// counters (count_before → count_after). Written here, before the run audit
|
|
@@ -163,6 +163,10 @@ export async function extractEntities(memory, { store, config, callLLM, logger }
|
|
|
163
163
|
if (config.entityExtractionModel) options.model = config.entityExtractionModel;
|
|
164
164
|
const reasoning = config.entityExtractionReasoning;
|
|
165
165
|
if (reasoning && reasoning !== "none") options.reasoningEffort = reasoning;
|
|
166
|
+
// #405: 审计行要能归因到具体记忆。extractor 只把 content 写进 prompt,id 这里顺
|
|
167
|
+
// options 交给 index.js 的 adapter(route 映射只认 provider/model,多余键不会进
|
|
168
|
+
// llm.stream)。拿不到 id 就不写这个键,adapter 侧照旧留空数组,不造虚账。
|
|
169
|
+
if (memory.id) options.memoryId = memory.id;
|
|
166
170
|
const llmResponse = await callLLM(messages, options);
|
|
167
171
|
|
|
168
172
|
if (!llmResponse) {
|
package/lib/index.js
CHANGED
|
@@ -14,7 +14,7 @@ import { createInjector } from "./inject.js";
|
|
|
14
14
|
import { createPreInjectGate } from "./pre-inject-gate.js";
|
|
15
15
|
import { createContinuityRescue } from "./continuity.js";
|
|
16
16
|
import { createSummarizer } from "./summarize.js";
|
|
17
|
-
import { createDreamScheduler } from "./dream.js";
|
|
17
|
+
import { createDreamScheduler, describeStreamFailure } from "./dream.js";
|
|
18
18
|
import { createSleepScheduler, runSleep } from "./dream/sleep.js";
|
|
19
19
|
import { createApi } from "./api.js";
|
|
20
20
|
import { createStandaloneApi } from "./api-standalone.js";
|
|
@@ -78,6 +78,11 @@ export function createEntityStreamAdapter({ llm, agentDefaultModel, logger, serv
|
|
|
78
78
|
} catch { /* fall through to whatever route we already have */ }
|
|
79
79
|
}
|
|
80
80
|
const effort = options.reasoningEffort;
|
|
81
|
+
// #405: 审计行的 related_memory_ids 此前恒空、error_message 又被压成一句常量,
|
|
82
|
+
// 于是「哪条记忆没抽到实体、为什么」在库里查不出来(主库 735 条 error 行全是
|
|
83
|
+
// 同一句话)。id 由 extractor 顺 options 传下来——它只认 memory.content,不认
|
|
84
|
+
// id。口径同 write-admission:350:拿不到 id 就留空,绝不挂不存在的 id 造虚账。
|
|
85
|
+
const relatedMemoryIds = options.memoryId ? [options.memoryId] : [];
|
|
81
86
|
// 没有解析出 provider/model 就没有可归属的模型——与 dream 的 resolveRoute
|
|
82
87
|
// 无路由早退同口径,那种情况不写审计行。
|
|
83
88
|
const modelId = route.provider && route.model ? `${route.provider}:${route.model}` : "";
|
|
@@ -89,7 +94,7 @@ export function createEntityStreamAdapter({ llm, agentDefaultModel, logger, serv
|
|
|
89
94
|
const timestamp = new Date(startedAt).toISOString();
|
|
90
95
|
// 记账是 best-effort:写审计行失败只 warn,绝不反噬抽取本身(CONTRIBUTING
|
|
91
96
|
// 的 fail-safe 硬约定)。
|
|
92
|
-
const writeAudit = (status, errorMessage) => {
|
|
97
|
+
const writeAudit = (status, errorMessage, relatedMemoryIds = []) => {
|
|
93
98
|
if (config?.llmAudit?.enabled === false || typeof service?.saveLlmAudit !== "function") return;
|
|
94
99
|
// #372: modelId 为空 = 路由解析失败——此前整个 writeAudit 静默 return,
|
|
95
100
|
// llm.stream 抛错也留不下任何行,正是 #108 要消灭的「零实体零日志」同款盲区。
|
|
@@ -111,7 +116,7 @@ export function createEntityStreamAdapter({ llm, agentDefaultModel, logger, serv
|
|
|
111
116
|
duration_ms: Date.now() - startedAt,
|
|
112
117
|
status,
|
|
113
118
|
error_message: errorMessage,
|
|
114
|
-
related_memory_ids:
|
|
119
|
+
related_memory_ids: relatedMemoryIds
|
|
115
120
|
});
|
|
116
121
|
} catch (auditError) {
|
|
117
122
|
logger?.warn?.(`dsh-mneme: entity extraction llm audit write failed: ${String(auditError)}`);
|
|
@@ -136,7 +141,7 @@ export function createEntityStreamAdapter({ llm, agentDefaultModel, logger, serv
|
|
|
136
141
|
if (Number.isFinite(o)) outputTokens = o;
|
|
137
142
|
}
|
|
138
143
|
if (chunk.type === "finish" && (chunk.reason?.kind === "error" || chunk.reason?.kind === "aborted")) {
|
|
139
|
-
writeAudit("error", "llm stream aborted or errored");
|
|
144
|
+
writeAudit("error", describeStreamFailure(chunk.reason) || "llm stream aborted or errored", relatedMemoryIds);
|
|
140
145
|
return undefined;
|
|
141
146
|
}
|
|
142
147
|
}
|
|
@@ -144,7 +149,7 @@ export function createEntityStreamAdapter({ llm, agentDefaultModel, logger, serv
|
|
|
144
149
|
return text;
|
|
145
150
|
})().catch((err) => {
|
|
146
151
|
logger?.warn?.(`[dsh-mneme] entity extraction llm stream failed: ${String(err)}`);
|
|
147
|
-
writeAudit("error", String(err?.message ?? err));
|
|
152
|
+
writeAudit("error", String(err?.message ?? err), relatedMemoryIds);
|
|
148
153
|
return undefined;
|
|
149
154
|
});
|
|
150
155
|
};
|
|
@@ -183,11 +188,6 @@ export const apply = (ctx, config) => {
|
|
|
183
188
|
}
|
|
184
189
|
|
|
185
190
|
const store = createStore(join(memoryDir, "memory.db"));
|
|
186
|
-
// Prune reflection failure rows older than 90 days on boot (best-effort, so
|
|
187
|
-
// the failure table never grows unbounded).
|
|
188
|
-
try {
|
|
189
|
-
store.deleteOldFailures(new Date(Date.now() - 90 * 86400000).toISOString());
|
|
190
|
-
} catch { /* non-fatal */ }
|
|
191
191
|
|
|
192
192
|
// User-configurable settings (profile, rules, panel mode, standalone API
|
|
193
193
|
// token) share the same SQLite file in dedicated tables, isolated from
|
|
@@ -226,6 +226,21 @@ export const apply = (ctx, config) => {
|
|
|
226
226
|
cfg[objKey] = { ...(cfg[objKey] ?? {}), ...sub };
|
|
227
227
|
}
|
|
228
228
|
|
|
229
|
+
// 失败表的启动期清理也放在装配之后(此前它在 createStore 之后、装配之前,写死
|
|
230
|
+
// 90 天)。保留期现在是显式配置键 reflectionFailureRetentionDays;等它进面板
|
|
231
|
+
// 白名单的那天,这里读的就是装配后的值,不用再改第二处。best-effort 语义不变:
|
|
232
|
+
// 清理失败不挡启动,否则一次锁表就能让插件起不来——但「不挡启动」不等于「可以
|
|
233
|
+
// 悄悄失败」:这一族的 recall_runs 清理当年就是静默蒸发,表涨到两万余行无人
|
|
234
|
+
// 察觉,所以本处与 llm_audit / distill_records / recall_runs 三处一样出声。
|
|
235
|
+
try {
|
|
236
|
+
const failureRetentionDays = Number.isInteger(cfg.reflectionFailureRetentionDays)
|
|
237
|
+
? cfg.reflectionFailureRetentionDays
|
|
238
|
+
: 90;
|
|
239
|
+
store.deleteOldFailures(new Date(Date.now() - failureRetentionDays * 86400000).toISOString());
|
|
240
|
+
} catch (err) {
|
|
241
|
+
ctx.logger?.warn?.(`[dsh-mneme] failure-table retention purge failed: ${String(err)}`);
|
|
242
|
+
}
|
|
243
|
+
|
|
229
244
|
// Bug8 的启动期清理放在装配之后:面板把 llmAudit.* 写进 kv、经 nestedFlags 合进
|
|
230
245
|
// cfg,而写入侧(dream / summarize / 写入准入)读的都是装配后的 cfg——清理若读
|
|
231
246
|
// rawCfg,面板改了保留期它不认,更糟的是「开不开审计」与写入侧可能取到不同的值
|
|
@@ -236,7 +251,9 @@ export const apply = (ctx, config) => {
|
|
|
236
251
|
const retentionMs = Number.isInteger(cfg.llmAudit?.retentionDays) ? cfg.llmAudit.retentionDays : 90;
|
|
237
252
|
store.deleteOldLlmAudits(new Date(Date.now() - retentionMs * 86400000).toISOString());
|
|
238
253
|
}
|
|
239
|
-
} catch {
|
|
254
|
+
} catch (err) {
|
|
255
|
+
ctx.logger?.warn?.(`[dsh-mneme] llm_audit retention purge failed: ${String(err)}`);
|
|
256
|
+
}
|
|
240
257
|
|
|
241
258
|
// #399 的启动期裁剪:与上面同款(读装配后的 cfg,理由同上——面板写的保留期必须
|
|
242
259
|
// 与写入侧取到同一个值)。不同的一点:蒸馏原文留存默认关,而「默认关」在写入侧
|
|
@@ -250,7 +267,27 @@ export const apply = (ctx, config) => {
|
|
|
250
267
|
: 90;
|
|
251
268
|
store.clearDistillRecords(new Date(Date.now() - retentionMs * 86400000).toISOString());
|
|
252
269
|
}
|
|
253
|
-
} catch {
|
|
270
|
+
} catch (err) {
|
|
271
|
+
ctx.logger?.warn?.(`[dsh-mneme] distill_records retention purge failed: ${String(err)}`);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// recall_runs 的滚动清理(v0.7.0 原设计,2026-10-14 找回):与 llm_audit 同策略,
|
|
275
|
+
// 按 cfg.recallRetentionDays(默认 90 天)启动时清一次。读装配后的 cfg,理由与上面
|
|
276
|
+
// 两处一致 —— 写入侧取的是 cfg,清理侧若取 rawCfg,面板改的保留期就不生效。
|
|
277
|
+
// 这一对曾在 dd0ed78(v0.7.12 发布提交)里被整块弄丢,配置键与文档却一直留着,
|
|
278
|
+
// 实测 2026-10-13 那天从 16,041 涨到 20,510 行(一天四千余行,只增不减;
|
|
279
|
+
// 单给一个静态行数会随表腐坏,故带日期的区间才是可核对的说法)。注入行开始写真实 query 之后,这条从「表膨胀」
|
|
280
|
+
// 升级为「原话无限期留存」,所以必须与那次改动同批交付。
|
|
281
|
+
// 保留期语义是「天」(函数内部乘 86400000),清理失败只 warn,绝不挡插件启动。
|
|
282
|
+
try {
|
|
283
|
+
const recallRetentionDays = Number.isInteger(cfg.recallRetentionDays) ? cfg.recallRetentionDays : 90;
|
|
284
|
+
store.purgeRecallRunsOlderThan(recallRetentionDays);
|
|
285
|
+
} catch (err) {
|
|
286
|
+
// 这里必须真的出声。这一对当初是**静默**蒸发的:函数与调用一起没了、没人发现,
|
|
287
|
+
// 表就这么涨到两万余行。清理再失败一次而仍然不出声,同样的回归照样看不见——
|
|
288
|
+
// 「只 warn」不是修辞,是这族清理唯一的事后线索。
|
|
289
|
+
ctx.logger?.warn?.(`[dsh-mneme] recall_runs retention purge failed: ${String(err)}`);
|
|
290
|
+
}
|
|
254
291
|
|
|
255
292
|
// 记忆语言(memory.language):本实例逐层传入 inject / summarize / dream /
|
|
256
293
|
// sleep / mirror,多实例(如 agent preset 内挂载)互不影响。
|
package/lib/inject.js
CHANGED
|
@@ -38,7 +38,12 @@ function visibleSeqsOf(session) {
|
|
|
38
38
|
// session event log (same event shape summarize.js consumes) and returns the
|
|
39
39
|
// most recent human message. Any failure degrades to "" — the injector then
|
|
40
40
|
// falls back to the legacy rule-based pick, never breaking the render.
|
|
41
|
-
|
|
41
|
+
//
|
|
42
|
+
// v0.9.0:导出给 tools.js 的 memory_update 用。失败记录表的 query 与注入
|
|
43
|
+
// 候选问的是同一件事——「当时到底想干什么」;这条取原话的路只该有一份实现。
|
|
44
|
+
// 复制一份的代价本仓库付过几次(src/lib 平价、snapshotEvents 兼容层),所以
|
|
45
|
+
// 这里只加 export,不改行为。
|
|
46
|
+
export function lastUserQuery(ctx) {
|
|
42
47
|
try {
|
|
43
48
|
const session = ctx?.agent?.session;
|
|
44
49
|
// DSH ≥0.1.2-rc only exposes events via snapshotEvents(); older builds
|