Skip to content

[Bug] autoDream 长期不可用:两处独立缺陷 ——「模型回填 8 位 id 前缀 vs 校验器 36 位整串匹配」与「默认档位+预算让思考型模型返回空体」 #135

Description

@aizhimoran

摘要:181 轮 autoDream 里 169 轮失败no json array in llm output 146 轮、invalid decisions: 1 errors 22 轮),只有 5 轮真正合并过任何东西。

本机定位到两处互相独立的缺陷,并各自验证了修复:

  1. 主缺陷(本轮新发现):模型把 36 位 UUID 回填成前 8 位前缀,而校验器用 snapshot.get(id)整串精确匹配 → 逐条 unknown idclaimed=0 → 覆盖率闸整单拒绝。模型的语义判断其实是对的(实测 78/78 个前缀都能精确对应到窗口里的真实记忆)。
  2. 次缺陷(承接 [Bug] autoDream 在记忆量较大且路由为思考型模型时 100% 失败:no json array in llm output #9dreamReasoningEffort 默认 none 会让 harness 补上模型的 defaultEffort(v4-flash 系 = high),叠加 dreamMaxTokens 的 schema 默认值 32768,思考型模型把预算烧在推理上 → 正文为空。

关键放大器:这两类失败既不落库也不落日志(见「观测缺口」),所以问题被藏了 140+ 轮。

修复后首轮即成功:status=degraded, applied=34, merge-archived=62

环境

DSH 0.1.5-rc.1(win32,web profile)
@modusensus/dsh-mneme 0.7.31(npm latest,GET /api/dsh-mneme/info 确认)
agent 默认模型 deepseek-official / deepseek-flash~/.dsh/settings.yamlagent-default-model
dream 路由(复现期) deepseek-official / deepseek-v4-flash(思考型,声明 off/low/high/maxdefault = high),后落到 agent 默认 deepseek-flash
dream 生效预算 dreamMaxTokens = 32768schema 默认值,未配置
候选窗口 dreamMaxSnapshotSize = 200(默认)
记忆库 总 1010 / 活跃 376 / 归档 634

现象

dream_runs 全量(2026-08-28 → 2026-09-12 01:41,共 181 轮):

status 轮数 applied 合计 说明
failed 169 (93.4%) 0 全部无产出
ok 8 6 其中 7 轮是「模型零有效决策」被隐式 keep 补齐(见附录 B)
reconcile 3 100 决策部分未提交(CAS/回滚)
degraded 1 34 修复补丁落地后的首轮

失败原因:

error 轮数
no json array in llm output 146
invalid decisions: 1 errors 22
llm failed 1

真正合并过的轮次全库只有 5 轮,累计 merge-archived 295 条。

缺陷 1 的实证:8 位前缀 vs 36 位整串

在失败路径落库的校验明细里(见「观测缺口」,这是本机打补丁后才拿到的):

errors  = ['explicit decision coverage 0% < minimum 1%']
skipped = 27 条
  decision[0] merge ids=5 → unknown id "f77a0d17"; …(5 个全 unknown)
  decision[1] merge ids=4 → unknown id "23178b3b"; …
  …

三向归类(window_ids 取该轮 dream_runs.input 的 200 个 id;all_idsmemories 全表 1002 个 id):

分类 条数
在窗口内 0
不在窗口但在库中(活跃/归档) 0
库中根本不存在 77(去重后)= 全部

被拒 id 的长度分布是 {8: 78},而窗口 id 与库中 id 的长度分布都是 {36: 200} / {36: 1002}。把它们按「窗口 id 的前 8 位」匹配:

被拒 id 共 78 个,能在窗口前 8 位中找到的:78 / 78
  f77a0d17 → f77a0d17-2586-41fe-a789-d69f4d1cee83
  bc17c9ed → bc17c9ed-e84c-4b9f-bc36-48f8e9a6b3e0
  fe6199e6 → fe6199e6-1933-4520-a028-9ceb2cf79011

而提示词给模型看的是完整 36 位src/dream.js:693 的聚类分支与 :706-708 的兜底分支都是 id=${m.id})。

模型认对了要合并哪几条记忆,只是把 id 缩写成了前 8 位;校验器按整串查表,于是整轮成果被判无效。

缺陷 2 的实证:no json array in llm output

  • 失败点是 src/dream.js:762-765extractJsonArray(text) 返回 null → 整轮丢弃。
  • streamText 只收集 text-delta;思考型模型把 maxTokens 全花在 reasoning-delta 时正文为 0 —— 该机制已由 [Bug] autoDream 在记忆量较大且路由为思考型模型时 100% 失败:no json array in llm output #9 用 chunk 分布实测({reasoning-delta: 32768, text-delta: 0})并获维护者认可。
  • 本机命中该模式的两个条件都成立:
    • (a) dreamMaxTokens 停在 schema 默认 32768src/config.js:67.default(32768)[Bug] autoDream 在记忆量较大且路由为思考型模型时 100% 失败:no json array in llm output #9 只把 max 抬到 131072,默认值没抬)。
    • (b) dreamReasoningEffort 默认 "none"config.js:81),而 resolveDreamEffortsrc/dream.js:392 直接短路 if (!configuredEffort || configuredEffort === "none") return null;不发送 reasoningEffort 字段 → harness 补上模型声明的 defaultEffort(v4-flash 系 = high)。即开箱默认配置恰好把思考预算拉满,也让 v0.7.26 声称的「defaultEffort 陷阱根治」默认不生效。
  • 换非思考模型只压低、未消除:把路由换到 deepseek-flash 后,失败率从 98.0% 降到约 50%(重启后 6 轮里仍有 3 轮 no json array),输入 8 万字符量级。所以预算/档位仍是活的隐患。

观测缺口(本机为了让根因可见,不得不先打补丁)

#104 的关系:该 issue 已包含「失败审计缺明细」这一项,本 issue 不主张它是新问题;这里提供的是可复现的量化证据(168 轮失败零现场、明细 warn 因写在 return 之后而不可达),可直接并入 #104 处理。

失败轮次不落任何可诊断信息,且日志路径不可达:

// src/dream.js:779-786
if (!ok) {
  logger?.warn?.(`dsh-mneme dream: invalid decisions: ${errors.join("; ")}`);
  return finish({ ok: false, error: `invalid decisions: ${errors.length} errors`, summary: false }); // ← 不传 decisions
}
const skippedInvalid = skipped.length > 0;
if (skippedInvalid) {
  logger?.warn?.(`dsh-mneme dream: ${skipped.length} invalid decision(s) skipped …`); // ← 在 return 之后,永远执行不到
}
  • finish 在失败分支不传 decisions → 落库为 NULL;而 validateDecisions 明明已经把逐条明细 skipped: [{index, action, ids, error}] 返回给了调用方,却被丢弃
  • 唯一的明细日志写在 return 之后 → 不可达
  • 本机实测该插件的 ctx.logger.warn 也不上 stdout(终端里看不到 mneme 的任何日志)。
  • 结果:146 + 22 = 168 轮失败,没有一轮留下「模型到底输出了什么」的痕迹。

根因

主因:校验器不接受 id 前缀

src/dream/decisions.jsvalidateDecisions 用整串匹配判定 id 合法性,任何未精确命中的 id 都记 unknown id 并把该条决策跳过(dreamSkipInvalid 默认 true)。当模型把所有 id 都缩写成 8 位前缀时,全部决策被跳过claimed.size = 0 → 覆盖率闸(explicit decision coverage 0% < minimum …)整单拒绝 → 整轮丢弃、applied=0

这是「校验器过严 + 模型习惯」的组合缺陷,而且因为上文的观测缺口,运维侧完全看不出是 id 格式问题(错误串只有一句覆盖率,明细被丢)。

次因:预算与默认档位(承接 #9

见「缺陷 2 的实证」。

第三层:候选窗口(已由 #125 覆盖)

dreamMaxSnapshotSize(默认 200)按 updated_at DESC 截断候选,窗外旧记忆永不进视野。本机候选池 375 条、窗口覆盖约 53%,且写入速率可达单小时 140 条。此处不复述、不重复提交。

附属发现:memoryQualityFilter 的两个问题(与 dream 失败无关,但同属「静默归档 + 审计线索被销毁」)

机制src/service.js:960-978):每次写入由 LLM 打分(0–100,本机全库均分 79.2)并给出理由标签(low_quality / duplicate / meta / repetitive / short_content);score < archiveThreshold(默认 30)即无条件归档,标签以并集写入,作为「为什么被归档」的审计痕迹。

本机实测(2026-09-12):

口径 条数
quality_score < 30 150
其中带 low_quality 标签 148
已归档 149
其中 importance ≥ 4 128(126 条带标签)

问题 1:importance 完全不参与归档决策

150 条低分条目 100% 被归档,其中 51 条 importance=5、77 条 importance=4 —— 一条被标为最重要的记忆,会被一个 LLM 分数静默归档:无感知、无通知、无豁免。建议 importance ≥ 4(或 ≥5)时只降权不归档,或至少归档时给出可见通知。

但这些低分条目并非误伤:判断器自己给出的理由标签里 duplicate 占 79%(101/128)meta 37%、repetitive 34%,且无一条 short_content(说明不是按长度误杀)。它归档的确实多是重复/元信息。

真正的上游问题在 #127:本机这 150 条低分条目的 source 几乎全是 session:*,即 autoSummarize 的蒸馏产物——每轮都在铸造注定被立即归档的条目,白烧 LLM 配额

问题 2:信号标签会被后续 update 覆盖

applyQualityDisposition并集写标签(src/service.js:967),但更新路径是整组替换

memory_update(args)               lib/tools.js:233
  → service.update(id, {…, tags})  src/service.js:1485
    → store.update(id, p)          (p.tags 直接写入,不再并集)

于是任何带 tags 的更新都会冲掉判断器留下的 low_quality/duplicate/meta 等标记。实证对账:150 条低分条目中 148 条带 low_quality缺的 2 条恰好都是被 memory_update 改过的;而另一条创建后未再更新的同类条目标签完好。

后果:「这条记忆为什么被归档」变得不可追溯 —— 无法区分「质量门自动归档」「用户手动去重归档」「dream 合并归档」。本机就因此一度误判为"是被 dream 合并归档的",不得不额外排查全部 dream 轮次的 outcome/decisions 才排除。建议更新时把系统信号标签并集保留(用户自传的标签照常生效)。

附属发现 2:向量层从未配置 —— 五个功能静默失效,而 /semanticready: true

机制src/embedding.js:85-86:111-112local-embedder.js 的各后端同理):

const cfg = settings.getVectorConfig();
if (!cfg?.enabled || !cfg.baseUrl || !cfg.apiKey || !cfg.model) return;   // ← 静默返回

本机实测(2026-09-12)

GET /api/dsh-mneme/vector-config → {"enabled": false, "baseUrl": "", "apiKey": "", "model": ""}
kv(user_settings)              → 只有 external_api 与 feature_flags,没有任何向量配置
memories                         → 总 1035 / 无向量 1035(100%)
vector_meta                      → 空表(0 行)
llm_audit_logs                   → 435 行里没有一种与嵌入沾边的 operation_type

一次嵌入请求都没有发出去过(在第 86 行就 return 了),因此不产生任何错误日志 —— 连"未配置"这个事实都无处可查。

静默失效的五个功能

功能 表现
语义召回(向量检索) 无向量可搜,只能靠关键词 / BM25 兜底
searchSemanticDedup 开关为 true 但不生效
rerankEnabled reranker: null
sleep 的冲突检测 被跳过(报告里写 "reason": "no usable vectors"
dream 的语义聚类预分组 退化到全量窗口列表兜底(dream.js:705-708

对照:不依赖向量的功能是正常的 —— entities 121 行、recall_runs 2177 行、llm_audit_logs 435 行。所以问题精确定位在向量这一层。

问题:GET /api/dsh-mneme/semanticready: true,但同一响应里 index.embeddedCount = 0 / totalCount = 346modelHash / dimension / reranker 全为 nullready 只反映"嵌入器对象已构造",不反映索引里有没有数据。建议:ready 结合 embeddedCount / totalCount 计算;并在 getVectorConfig() 为空时至少记一条 info/warn

补充:本机原先 embedProvideropenai,而该分支必须依赖上表那四个字段(index.js:210-213);把 embedProvider 改成 local/ollama 则走 local-embedder.js(本机已装 @huggingface/transformers + onnxruntime-node,但 HF 模型缓存为空,首次使用需下载)。

本 issue 涉及的「绿色假阳性」共性清单

这一类问题在本仓库已经出现 5 次,模式完全一致:状态显示正常,功能实际是死的。

# 表面状态 实际情况
1 /semanticready: true 向量索引 0/346,嵌入器从未被配置
2 面板「测试连通性」→ 通过 maxTokens: 1024 + 一句提示词;空 reply 也算通过;与 dream 路径无关
3 dream_runs.status 静默记 ok 模型零有效决策被隐式 keep 补成 200 条(dreamMinExplicitCoverage: 0 时)
4 llm_audit_logs 有 435 行 input_tokens / output_tokens / cost_usd 全部为 0
5 记忆带 low_quality 标签、看似可审计 一次 memory_update 就把信号标签冲掉

建议给这五处各加一条"真实状态"断言或启动自检(例如启动时校验 embeddedCount > 0,否则明确警告),比逐个修更省事。

修复与验证

本机通过两个本地补丁验证(pnpm patch,合并进同一份 @modusensus__dsh-mneme@0.7.31.patch):

  1. 让失败可观测!ok 分支改为 finish({ …, decisions: [{ _validationFailed: true, errors, skipped }] }),并把 skipped 明细的 warn 提到 return 之前。
  2. 让前缀可解析:在 validateDecisions 入口把「唯一前缀」解析回完整 id(git 短哈希式)——仅在 snapshot 内解析,前缀有歧义或匹配不到则原样保留、仍由后续校验如实报 unknown id;就地改写 ids / keepSource / winner / loser,使下游 applyDecisions 也拿到完整 id。

修复后首轮(补丁需重启 dsh 生效;cordis.patch.yml 那类配置是 live 重载,插件源码不是):

status   = degraded        (skipInvalid 语义:单条非法只跳过、合法子集照常应用)
applied  = 34
decisions= keep=103  merge=31  archive=2  conflict=1
outcome  = merge-archived: 62 · merge-keep: 31 · conflict-winner: 1 · conflict-archived: 1
库况      = 活跃 436 → 376(−60)/归档 566 → 634(+68)   ← 单轮

全库成功合并轮次复盘:

# 时间(北京) status applied merge-archived
1 08-28 21:48 ok 6 6
2 09-10 20:39 reconcile 31 90
3 09-10 20:43 reconcile 35 65
4 09-12 00:52 reconcile 34 72
5 09-12 01:41 degraded 34 62

前 4 轮都发生在 deepseek-v4-flash 时代(那时模型回填完整 id),第 5 轮是首次在 deepseek-flash 上成功、也是首次由前缀解析解开的。

复现

  1. dream 路由指向一个会缩写 id 的模型(本机 deepseek-flash);
  2. 记忆库涨到候选窗口 200 条、单轮输入 8 万字符量级;
  3. 等 autoDream 触发,查 dream_runsstatus='failed'error='invalid decisions: 1 errors'applied=0decisions IS NULL
  4. 想看到真正的原因,需要先让失败路径落库(见上文补丁 1)——否则 skipped 明细不可得。

窗口口径(与 src/dream.js:600-612 一致):活跃非 summary、按 updated_at 倒序取前 dreamMaxSnapshotSize 条。

建议(按优先级)

  1. 校验器接受「唯一前缀」:在 validateDecisions 入口把短 id 解析回完整 id(git 短哈希式),歧义/无匹配才判 unknown id。这是本机解锁合并的决定性一步,且不改变任何用户可见语义
    (替代方案:在 CONSOLIDATION_PROMPT 里明确要求「原样复制 36 位完整 id」,并把 few-shot 示例里的 "m1"/"m2" 换成真实 UUID —— 当前示例的短占位符很可能在诱导模型缩写。)
  2. 失败路径必须落 decisions / skipped:把 validateDecisions 已经返回的逐条 {index, action, ids, error} 写进 dream_runs.decisions,并把那行 skipped warn 提到 return 之前。否则上百轮失败级的故障完全没有现场。
  3. dreamMaxTokens 的 schema 默认值从 32768 抬到 131072(或按输入长度自算预算)。[Bug] autoDream 在记忆量较大且路由为思考型模型时 100% 失败:no json array in llm output #9 只放开了 maxdefault 未动,等于把已修好的问题留给了默认配置。
  4. dreamReasoningEffort 默认不应是 none:对"必须产出 JSON"的后台调用,默认发送该模型支持的最低档,而不是省略字段让 defaultEffort 顶上。
  5. resolveDreamEffortnone 的短路(dream.js:392)应区分"用户显式选了 none"与"默认未配置":前者尊重省略语义,后者取最低支持档。这是 v0.7.26 那处"defaultEffort 陷阱根治"真正生效的前提。
  6. 修复 llm_audit_logs 的 token/cost 恒为 0(见附录 B)。这是定位本类问题唯一的量化面。
  7. 可选:失败轮持久化原始 LLM 文本(或至少 raw length + 前 300 字节),现在只进不可达的日志。
  8. memoryQualityFilter:让 importance 参与归档决策(≥4/≥5 只降权不归档,或归档时给出可见通知);并让系统信号标签不被 update 覆盖。详见「附属发现」。
  9. 向量层:让 /semanticready 反映索引覆盖度embeddedCount / totalCount),并在嵌入器未配置(getVectorConfig() 为空)时至少记一条 info/warn。否则"从未配置"会让五个依赖向量的功能长期静默失效,而状态页一直是绿的。详见「附属发现 2」。
  10. 可选(更强):把「附属发现 2」里那张绿色假阳性清单做成启动自检 —— 状态页只在功能真能工作时才显示绿。

与既有 issue 的关系

附录 A:生效配置

GET /api/dsh-mneme/featureseffective(2026-09-11 实测):

{
  "autoDream": true, "autoInject": true, "autoSummarize": true, "hotMemoryEnabled": true,
  "sleepModeEnabled": true, "heatEnabled": false, "conflictFreezeEnabled": false,
  "allowCrossTypeMerge": false, "dreamSkipInvalid": true, "llmAudit.enabled": true,
  "memoryQualityFilter.enabled": true,
  "dreamMinIntervalMinutes": 0, "dreamMaxTokens": 32768,
  "dreamProvider": "", "dreamModel": "",
  "distillMaxChars": 24000, "rerankEnabled": true, "searchSemanticDedup": true
}

另注:面板的「测试连通性」按钮不能代表巩固可用——它 maxTokens: 1024、提示词仅 Reply with exactly one word: ok,且只在流报错时返回 ok:false(空 reply 也算通过)。作者注释已点破机制(16 会被 reasoning 全部吃掉,reply 恒为空):他们把测试按钮的预算 16→1024 修了,却没动 dream 那条路径。

附录 B:审计方法与库况

  • 方法:把 memory.db + -wal + -shm 复制到临时目录后以只读方式打开,不触碰运行中的库,用 Python sqlite3 分析 dream_runs / llm_audit_logs / memories
  • 判定 keep 是模型输出还是代码补齐:取 dream_runs.decisions 的 id 序列与 dream_runs.input 快照的 id 序列(updated_at DESC)做尾对齐 —— 代码 decisions.js:183 补的 keep 严格按快照顺序、落在数组末尾。实测:真实合并轮只有尾部对齐(补 1/6/1/3 条),空转轮从头就对齐(整条 200 条都是补的)。另:200 × 64 + 199(逗号) + 2(括号) = 13,001,即那批 keep=200decisions 长度是确定性代码产物,不能当作模型行为指标。
  • 判定 unknown id 的性质:把被拒 id 分别与「窗口 id 集」「库中全表 id 集」比对,并额外做「窗口 id 前 8 位」匹配 —— 本机正是靠这一步把根因锁定到"8 位前缀"。
  • 库况(2026-09-12 01:42):总 1010 / 活跃 376 / 归档 634。
  • llm_audit_logs 全部 435 行里(dream_consolidate 183 + summarize_compress 238 + dream_summarize 14),input_tokens / output_tokens / cost_usd 三项为 0 —— runAuditedLlmreportUsage 从未拿到 usage,token 计量与成本核算完全失效(见建议 6)。

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

bug功能异常、崩溃、数据错误(agent 诊断 → 人工确认)

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions