Repository navigation
[0.9.3] 解析失败后确定性无限重试同一窗口:转录裸放 user 消息会被模型当成「该我回话了」 #493
Description
现象
@modusensus/dsh-mneme@0.9.3 的会话蒸馏(summarize_compress)在同一个会话窗口上连续失败 20 次,面板「工作动态」刷出一整屏相同的 invalid summary JSON(间隔 2–5 分钟,持续约 3.5 小时)。用户侧看起来像「记忆一直在提炼失败」。
实际上是同一个窗口被确定性重试了 20 次,不是 20 次不同的失败。
观测数据(本机 llm_audit_logs 只读查询)
- 近 7 天:
summarize_compress成功 469 次、dream_consolidate成功 187 次、dream_summarize成功 178 次 —— 蒸馏主路径平时是好的。 invalid summary JSON共 20 条,全部落在2026-10-20T15:40Z — 19:21Z(+08:00 的 23:40 — 03:21),模型均为cn:deepseek-v4.1-flash(openai-completions via 自建 relay)。- 这 20 条的输入 tokens 单调递增 8194 → 11118(同一窗口越攒越大),输出 225 — 4859,单次耗时 2.3 — 27.8s,
finish_reason = stop(不是截断、不是超时、不是 429)。 distill_cursors:该会话last_seq = 2229、updated = 2026-10-20T15:19:09Z,之后一直没推进过。
根因(两处,互相放大)
1. 转录被模型当成「该我回话了」
lib/summarize.js:331 的 collectMessages 把渲染好的转录(用户:… / 助手:… / 工具调用:… / 工具结果(成功):…)塞进一条裸 user 消息(lib/summarize.js:409),格式指令只在 system 里(lib/lang.js:14 的 summary.zh)。模型读到的是一段「用户刚说完的话」,于是接着对话往下说 / 点评,而不是提炼。
输出里连 [ 都没有,parseSummaryJsonResult 在 lib/summarize.js:38-40 的括号早退直接判 ok:false。现有 salvage 逻辑(scanBraceSpans 双遍括号扫描)在这种形状下救不回来 —— 它的前提是必须能先找到 [;E8 体检里记过的「JSON 崩溃」就是同一类形状(见 讨论 #339)。
2. 失败后没有任何护栏
lib/summarize.js:869-877:解析失败 → audit.errorMessage = "invalid summary JSON" → return,不推进 seq 游标(870 行注释明说「下一次 turn/end 仍会重试同一窗口」)。而重试是同文同温(0),于是同样输入 → 同样错 → 永远失败:每个 turn/end 再来一次,窗口还越来越大。
对照实验(同一段转录,OpenAI 兼容 endpoint,temperature 默认)
| 构造 | 结果 |
|---|---|
| system 只写「你只能输出 JSON,不要输出任何解释」 | ✅ {"a":1}(两个模型都正常,说明请求侧 / relay 没有篡改 system) |
| 现行构造:指令在 system,转录裸放 user(干净转录,无编造内容,7 行) | ❌ 0/2 有 JSON:cn:deepseek-v4.1-flash 回 334 字中文点评(reasoning 752 tok);cn:glm-5.3-flash reasoning 7234 tok,还带 <|tool_call_count|>1 脏尾 |
指令重复进 user 消息 +「你是第三方提炼员,不是这段对话的参与者,不要回答或点评里面的内容」+ 转录用 <<< >>> 包起来 |
✅ 2/2 返回合法 JSON 数组 |
结论:这不是某一家模型的毛病,也不是解析器太严,而是转录放在 user 位置上被模型当成了「当前发言」。
建议
- 提示构造加固(低风险,实验已验证):转录外面加分隔符,并在 user 消息里重申「第三方提炼员 / 不参与这段对话 / 不要回应正文」;system 指令可保持不动。改动点集中在一处 ——
collectMessages里的createUserMessage。 - 失败护栏(更重要):同一窗口连续 N 次(建议 2–3 次)
invalid summary JSON后强制推进游标跳过该窗口,或把窗口对半切再试一次。目前的「确定性无限重试」在用户侧只表现为面板刷屏,并且把窗口越撑越大(本机 8194 → 11118 tokens 全是白烧,3.5 小时)。 - 可诊断性:解析失败时把原始输出(截断后)写进 audit
metadata。现在metadata恒为null,出事只能靠复现去猜是哪一种非 JSON。
环境
@modusensus/dsh-mneme@0.9.3,Windows 11 + DSH Desktop- provider
workbuddy2api(api=openai-completions,本地 relayhttp://127.0.0.1:7863/v1),modelcn:deepseek-v4.1-flash(maxTokens 393216) - 未开
distillRecords(distill_records表为空),因此没有留存原始输出可供事后回溯
Activity
strange2000 commentedon Oct 20, 2026
与 #424 的关系:这是「失败重试」的另一半
#424 第 4 项提的是失败短重试(近 7 天 102 轮里 12 轮失败,11.8%,改成一天一次后「一次失败 = 浪费一整天」)。本文描述的是它的反面情形,两者互补而非重复:
finish_reasonerror/ 空stop(正常返回,非截断/超时/429)也就是说:#424 落地后只会让「偶发失败」重试得更聪明,而本 issue 里那个游标卡在
last_seq=2229的窗口仍会一直被重试 —— 因为按现在的逻辑,失败在语义上就等于「还没提炼完,下次接着来」,没有任何路径会承认「这段内容该被放弃」。建议的最小护栏(不改变正常语义)
:869-877那条return不再等价于「永远重来」。llm_audit_logs.metadata恒为null、distillRecognitionRecords默认关,所以我只能靠外部复现来证明「模型输出里连[都没有」。留 1–2KB 的原始响应(或首尾各 512 字符)就能让人一眼看出是「模型没照格式」还是「格式变了解析器没跟上」。补充:复现方法(无需改代码)
把
lib/summarize.js里buildTranscript的产物直接 POST 到任意 OpenAI 兼容端点(system 用STR.prompts.summary,user 用转录原文),即可稳定复现:复现脚本与原始审计数据(20 条
invalid summary JSON的完整字段、distill_cursors快照)我可以直接贴上来,需要的话说一声。strange2000 commentedon Oct 20, 2026
复现材料开源了:https://github.com/strange2000/dsh-repro →
repro/mneme-0001/两个脚本都可以直接跑,对应 issue 里的两张表:
scan-audit.cjs—— 只读打开 mneme 的 SQLite(MNEME_DB,默认E:/mneme/memory.db),按--days/--op打印:各操作成败分布、失败明细、同 op+model+error 的失败串、distill_cursors、metadata 留存。今天在本机重跑,这条串还在:summarize_compress / workbuddy2api:cn:deepseek-v4.1-flash / "invalid summary JSON"28 次,input_tokens 2217 → 11118。repro-prompt.mjs—— 三臂对照:A 只有「输出 JSON」的 system,B 用蒸馏 system + 转录裸放 user,C 加<<<TRANSCRIPT>>>分隔符并重申「你是第三方提炼员」。密钥走MNEME_API_KEY环境变量,解析口径照抄lib/summarize.js的indexOf("[")/lastIndexOf("]")。刚才 B 臂又跑了一次:
finish=stop,输出 58 字「已按你的要求将值班轮单日上限改为 5,其余配置保持不变…」,解析no-brackets—— 和 issue 里的判断一致,模型仍在把转录当成自己该回话的对话。顺带一个取证时踩到的细节,对排障有用:
llm_audit_logs.session_key对summarize_compress/dream_consolidate/dream_summarize/entity_extract全为 NULL(只有write_admission有值),所以「哪次失败属于哪个会话」在库里是查不到的;失败行 metadata 也是 100% NULL。这两点是「必须做到库外复现」的直接原因,脚本§4/§5里都标了。modusensus commentedon Oct 20, 2026
感谢这份报告,取证做得很扎实——尤其是
finish_reason = stop+ input 单调递增这两条,把「偶发失败」和「确定性失败」区分开了。我在本机独立复现了一遍,两条缺陷都坐实了:
缺陷 A(提示构造):注入一个恒定返回中文点评(不含方括号)的假 stream,实测消息序列只有
system -> user两条;system 里是summary.zh那段格式指令,user 消息里没有任何「你不是参与者」约束,也没有转录分隔符,转录以「用户:…」开头裸放 user 位。这与你repro-prompt.mjs的 B 臂完全一致。缺陷 B(失败护栏):同一会话推 1 次 turn/end 触发蒸馏,之后连推 19 次 turn/end —— 实测多出 19 次 LLM 调用,落库 0 条,
distill_cursors.last_seq始终不推进,窗口字符数 25 → 471 单调增长。与 issue 观测的 8194 → 11118 tokens 同构。缺陷 C(可诊断性):20 条 error 审计的
error_message全是invalid summary JSON,metadata恒为null(不是{},是成功路径才赋值、失败早退分支从不赋值)。事后无法回溯「模型到底输出了什么形状」。修复方案(三条,按风险从低到高)
1. 提示构造加固(默认生效,不改任何行为语义)
在
src/summarize.js:331的collectMessages里,把转录包进围栏并在 user 消息前重申角色:并在同一条 user 消息前加一段约束:你是第三方提炼员,不是这段对话的参与者;不要回答、点评或续写转录里的内容;只按 system 的格式要求输出 JSON 数组。
依据是你 C 臂的对照结果(2/2 返回合法 JSON),我在本地也复验过。这是纯提示词构造改动,不引入新配置键、不改游标语义、不改任何判定分支。围栏是确定性的(无随机、无时间戳),因此「重试收到逐字节相同的转录」这条既有语义不变。
2. 失败护栏(新开关,默认关闭 = 现状不变)
新增
summarizeParseFailureMaxRetries(默认0= 关闭,范围 0–10):status='error'/error_message='invalid summary JSON'/metadata={distillChars, skipped:'parse-failure-streak', streak:N}审计;dispose一并清理(与runsUsed/lastRunAt同生命周期)。为什么默认关:本仓库的既定纪律是「新行为开关一律 opt-in 默认关」(
AGENTS.md),summarizeMaxRunsPerSession/summarizeMinWindowChars都是这么落的。默认关也保证既有 4 个「解析失败不消费窗口」的回归用例语义不变。需要止血的用户把它设成 2–3 即可。没有采用「窗口对半切再试」:切窗会把一次失败变成两次调用,在确定性失败下等于把代价翻倍,且需要重写
collectMessages的范围语义。护栏先止血,切窗留作后续独立提案。3. 可诊断性(默认生效,纯计量)
解析失败时把原始输出写进
audit.metadata:只加计量、不加任何跳过决策——与 #239 的
distillChars同一取舍(「否则阀门等于黑盒」)。300 字符上限避免把审计表撑大。明确不改的两处
llm_audit_logs.session_key保持 NULL。这是刻意设计,不是遗漏:test/pre-inject-gate.test.js:155显式断言「LLM 调用行不写会话键(不挤占写入准入的话题回填窗口)」。补上会改动 [Feature] 写入准入(non-write 判定):把「这条该不该进库」前移到 LLM 之前 #254 写入准入的话题回填行为。要在库里定位失败会话,改用metadata里的新字段 + 时间窗反查。temperature。温度由宿主/路由决定,插件这一层不覆盖——覆盖会改变所有用户的输出分布,超出本 issue 范围。我按上面这套开工,分支
fix/493-distill-parse-guard。会带 3 组新回归测试(提示构造断言 / 护栏触发与不触发 / 失败路径 metadata),并同步npm run sync与docs/CONFIGURATION.md、README.md、CHANGELOG.md的配置表。落地后在本帖回报。strange2000 commentedon Oct 20, 2026
方案我完全同意——尤其「护栏默认关」「围栏确定性」这两处取舍的边界划得对。开工前补四条,都属于落地细节,不影响你按现方案推进:
① 跳过行的
error_message建议与真失败分开error_message = 'invalid summary JSON'与真失败同串,按error_message聚类的统计(比如我脚本 §2/§3 那种写法)会把「已放弃、游标已推进」的行算成「又失败了一次」——护栏上线后失败率曲线反而更难看懂。建议写成invalid summary JSON (skipped after N),或直接独立一个status='skipped'。② streak 建议持久化
「与
runsUsed/lastRunAt同生命周期」意味着进程重启清零。但这个失败恰恰是跨重启复现的:我库里那条游标last_seq=2229从2026-10-20T15:19:09Z卡到现在(期间机器重启过多次),每次重启白送 N 次重试,护栏「有上界」就不严格了。哪怕只在distill_cursors旁加一列parse_fail_streak也够用。③ 围栏要防转录自己撞上 END 标记
转录是用户 / 模型原文,里面出现
<<<END TRANSCRIPT>>>是完全可能的,一撞就提前闭合、等于退回裸放。在确定性前提下有两种做法:生成时检查转录是否含该串(含则把围栏加长或做转义),或写成带长度前缀的<<<TRANSCRIPT len=8000>>>。④ 失败 metadata 建议补一个
raw_tailraw_head前 300 字符有个盲区:有些模型先铺垫一段再吐 JSON,头部恰好看不到关键形状。尾部再截 300 字,成本几乎为零。另外一条供参考:围栏加固默认生效后,我这边那个卡住的窗口(
last_seq=2229,窗口内容也已冻结)是天然的验收样本——理论上它下次就该一次解析通过、游标自己往前走。分支落地后我用
scan-audit.cjs重跑,把游标前后值贴回来,顺带把脚本 §5 扩成能读raw_head/raw_tail的新字段。不急,你按自己的节奏来。fix(distill): only skip a window when the cursor is actually committa…
Merge pull request #494 from slow-stack/fix/493-distill-parse-guard
modusensus commentedon Oct 20, 2026
已在 #494 修复合入
main(4abb257),本条随 PR 关闭。谢谢你这份报告,它比一般的 bug report 好用一个量级。 具体是这几点让修复能一次到位:
finish_reason = stop这一条,把问题从「解析器太严」直接钉成「模型压根没按格式写」——没有它,很容易走错方向去放宽解析。repro-prompt.mjs的三臂对照(A 只有 system / B 现行构造 / C 加分隔符 + 角色重申)基本就是修复方案的实验依据。我们复跑后结论一致,C臂 2/2 出合法 JSON。session_key全为 NULL、metadata 100% NULL 这两条,直接决定了「必须库外复现」——也成了这次改动的第 3 项(失败行 metadata 现在恒带raw_head/raw_has_bracket/streak)。实际改动(细节见 #494):
<<<会话转录开始>>>/<<<会话转录结束>>>,user 侧重申「你是第三方提炼员,不是这段会话的参与者」。system 保持原样——我们实测只加 system 约束仍 0/2 出 JSON,判断「这段文本是什么」的是它所在的那条 user 消息。围栏是确定性的,重试收到的转录与首次逐字节相同。summarizeParseFailureMaxRetries(默认0= 关):连续失败到阈值后强制消费游标跳过该窗口。默认关是因为放弃内容有代价——你提到的 [Feature] 巩固/蒸馏的维护窗口:正向定点 + 债务上限 + 手动触发入口 #424 那一侧(429/5xx/超时)短窗口仍值得重试,两者确实互补。有一处和你的建议不同,说明一下:你建议 2–3 次,我们把它做成可配且默认关,而不是默认 2。原因是本仓的新行为一律 opt-in(见
AGENTS.md)——静默丢弃内容对已有用户是行为变更。但你这种场景设成2就是你要的效果,README / CONFIGURATION 里都写了。升级后你那条卡住的会话(
last_seq = 2229)仍会重试一次:第一次失败把 streak 推到 1,第二次即达阈值被跳过,之后新窗口正常蒸馏。另外确认一下你说的两处「刻意不改」:
session_key仍为 NULL(test/pre-inject-gate.test.js:155显式锁着这条,补上会牵动 #254 的写入准入),temperature也不显式传(温度由宿主/路由决定,插件覆盖会改变所有用户的输出分布)。feat(profile): draft the user profile from memory on demand (#497)
fix(persona)+feat(profile): 收回 default 卡兜底、生效文本单源渲染,并新增画像提取草稿(#497) (#…
strange2000 commentedon Oct 21, 2026
先报告:0.9.4 已装上(
@modusensus/dsh-mneme0.9.4,本机 2026-10-22 02:26 安装)。围栏确实落在lib/summarize.js:418,transcriptFenceOpen/Close与角色声明在lib/lang.js:173-180,raw_head/parse-failure-streak也在。这条路径我不再重复。但同一缺陷在 dream 侧还有 5 处,0.9.4 一处都没覆盖。 这就是我这两天面板上「不止总结提炼」的那一半。
一、现场:48 小时审计(本机
llm_audit_logs)summarize_compressinvalid summary JSON+ 2× stream errordream_consolidateno json array in llm output+ 1× 503no_healthy_accountdream_summarizeNO_ADAPTER(重启期偶发)no json array in llm output那 8 次,输入是 47,373 → 54,905 tokens,输出只有 212–907 tokens —— 显然不是在输出决策数组,而是在说话。二、时间线判据:这不是抖动,是一次性变化
三类操作最后一次成功全部落在
2026-10-20 15:19–15:20Z,此后 27 小时零成功:也就是说:总结提炼与 dream 侧是同时开始全崩的。这与「提示构造在两处都脆弱、而端点行为在那一刻变了」吻合,而不是两件互不相关的偶发。
三、代码位置(0.9.4,逐处核对)
dream 侧五处
user消息,全部是把列表/转录裸放,system之后没有任何分隔符与「你不是参与者」约束:lib/dream.js:1218dream_consolidatelistText(聚类后的记忆清单)lib/dream.js:514listinglib/dream.js:1461dream_summarizesummaryInputs.map(...)lib/dream/sleep.js:354listTextlib/dream/sleep.js:530listTextgrep transcriptFence lib/只命中lang.js(定义)与summarize.js(使用),dream 一处未命中。四、日志原文(
~/.dsh/logs/desktop-next.log)它把 46K tokens 的记忆清单读成「有人丢给我一包检索结果但没下指令」,于是开始寒暄。这与 #493 里 B 臂的形状完全同构,只是 prompt 换了一套。
五、端点侧的旁证(今天重跑的三臂)
<<<TRANSCRIPT>>>围栏 + 第三方提炼员含义:端点现在对「弱指令」的服从性明显变差,强约束仍然可靠 —— 所以围栏方向是对的,而 dream 侧必须同样处理,否则永远停在「弱指令」。
六、成本
8 次 × 约 54K 输入 ≈ 43 万 token/天,全部落库 0 条。
七、建议(都不急,按你的节奏)
lang.js已有的transcriptFenceOpen/Close+distillRoleReminder复用到上面 5 处;dream 的清单本身就是模型可读文本,同样存在「提前闭合」风险,第 ③ 条建议(fence 内容自检或len=前缀)在这里更必要。lib/dream.js:1206-1208与sleep.js:345/521的auditError目前只返回error 文案,metadata同样是 null;raw_head那套可以直接复用。last_seq无关,是定时触发,失败一次就是一次浪费。不需要你现在回。我这边先观察 0.9.4 之后那个
last_seq=2229的窗口是否解冻,有结果再贴。strange2000 commentedon Oct 21, 2026
补一条刚发生的对照,在同一分钟内(本机 18:32,0.9.4 已生效):
19 秒之隔,前者解冻、后者照旧 —— 输入规模同级(10.7K vs 54.9K),差别只在 prompt 是否围栏。这条不用回,当作上面第三节的实测注脚。
strange2000 commentedon Oct 21, 2026
再补一条 0.9.4 之后才显形的第二形态。它和围栏无关,也不是解析器太严——JSON 是合法的,条目是被
type枚举滤空的。一、现场
0.9.4 生效后 28 分钟,本机
llm_audit_logs:两条失败行的
metadata:raw_has_bracket=true说明模型确实输出了带方括号的数组,JSON.parse也过了。但raw_head长这样:[ { "id": 1, "type": "环境/版本", "content": "本地安装的 @modusensus/dsh-mneme 版本为 0.9.4…", "source": "工具结果:版本确认" }, …另一条窗口里:
[ { "id": "user_intent", "type": "用户需求", … }, { "type": "项目能力", … } ]模型自造了中文分类,还多给
id/source/依据这类键。于是:entries变成空数组,而lib/summarize.js:81的判据是:arr.length > 0但entries.length === 0→ok:false→summarize.js:921记invalid summary JSON,游标不推进,整窗重试。所以一个只错在分类词上的窗口,会被当成「模型没按格式写」反复重放。二、为什么会自造分类
lib/lang.js:14的中文蒸馏 prompt 里,类型是这么写的:竖线连写、只列 4 个、没有「只能用这些值」的约束。而同一文件
lang.js:18的 codingSummary 版把 7 个枚举写全了。两套 prompt 对同一件事的严格程度不一致,模型自然按「最像中文标签」的方式发挥。顺带:这条路径下失败行没有
raw_tail(keys里没有),而有些模型会先写几条合法条目、再在后面跑偏,头 300 字看不出来。三、三条可选修法(按侵入性从小到大)
entries.length === 0 && arr.length > 0时,error_message写成no valid summary entries (arr=12, valid=0),metadata补invalid_entries(被滤掉的条数)与sample_types(前 5 个出现的type原文)。这一步不改行为,只让下次能一眼看出是「没写 JSON」还是「分类词不对」。type走一次映射表(中文/近义词 → 枚举,未知落project或history),映射不中再滤镜。收益是内容能落库;代价是分类可能失准,所以我更倾向先做 1,再看 1 的统计决定要不要做 2。lang.js:14列全 7 个枚举、写明「不要输出枚举外的值,不要输出 id/source 等额外键」,与lang.js:18对齐。我的偏好排序:1 → 3 → 2。1 是纯诊断、零风险;3 是纯提示词;2 会改变入库分布,需要你判断值不值。
四、和上一条的关系
上一条报的 dream 侧 5 处裸放,与你重开 #493 的动作我理解为同一件事,就不再另开 issue。若你要分开发版,我可以把 dream 侧那节单独提成新 issue,说一声即可。
另外提醒一句:
summarizeParseFailureMaxRetries我这边保持默认0(关),因为想先看清失败形态再决定放弃阈值——上面这两条正好是「不该被放弃、该被修」的类型,设置成 2 会把它们连同窗口一起丢掉。等 1 或 3 落地后再开护栏更合适。modusensus commentedon Oct 21, 2026
@strange2000 你 18:34 与 19:12 两条都收到,全部按代码核过,结论与你写的一致。拆成两条走,理由是你自己也提的那点——两者不是同一个失效路径,混在一条里以后没人能按它发版:
dream.js叙述条/巩固/摘要 +sleep.js冲突/模式)。围栏与角色声明另起一对名字(memoryFenceOpen/Close),没复用「会话转录」——喂的是清单,标错名字会误导以后读审计的人。角色声明落在 user 段开头,正是你那组 A/C 臂对照给出的结论:只写 system 救不回来。raw_head复用,我按实情做了一处修正:dream 的 warn 日志本来就带raw length+ 前 300 字(dream.js的 finish 分支),所以缺的不是观测,是可查询的审计读数。现在两个 audited runner 支持spec.metadataFrom(result),失败行落parse_failure/raw_len/raw_head/raw_has_bracket。VALID滤空。我把不对称坐实了——summarize.js的VALID吃 7 个类型,lang.js里普通summary的中英两版只列 4 个,codingSummary才列全 7 个,且两个 prompt 都没有「只能用这些值 / 不要额外键」这类约束(grep 零命中)。按你排的 1 → 3 → 2 做:先诊断分叉(arr=N, valid=0+invalid_entries+sample_types,另补你指出的raw_tail)→ prompt 列全并收紧 →type归一化暂缓,因为它改入库分布,要等 1 的统计出来再判断值不值。summarizeParseFailureMaxRetries保持0」的判断我完全同意,而且这正是护栏默认关的理由:这两种形态是该被修的,设成 2 会把它们连着窗口一起丢掉。等 [蒸馏] type 枚举不对称:prompt 给 4 值、VALID 吃 7 值,滤空窗口被报成「没按格式写」并反复重放(#493 第二形态) #502 的 1 或 3 落地后再开阈值更合适。两条你都不用再补证据。#503 合开发版后我回这里报版本号,届时可观测判据是:
dream_consolidate失败行的metadata不再是null,且该 operation_type 的失败次数应显著下降;如果你那边重跑三臂,也欢迎把新的 A/C 读数贴上来——你这两组对照实验是这次定位里最有用的部分。modusensus commentedon Oct 21, 2026
补一条进度:你 19:12 报的第二形态已经单独立项并开了 PR——#502 / PR #504。
按你排的 1 → 3 做了,两条都零行为变更:
describeParseFailure把no-array/syntax/filtered-empty分开,第三种的错误消息是no valid summary entries (arr=N, valid=0),metadata带failure_kind/arr_count/invalid_entries/sample_types(模型自造的分类词原样回显)+你要的raw_tail;summary的中英两版枚举列全 7 个并写明「不要自造分类、不要附加id/source」。归一化(你的 2)暂缓,理由与你一致——它会改变入库分类分布,等
sample_types跑出统计再判断。summarizeParseFailureMaxRetries也继续默认0:这两种形态是该修的,不该被阈值丢掉。两条 PR(#503 dream 围栏、#504 本条)都过了 CI 与复审、无未解决线程。合完发版后我在 #503 那边回版本号;你那边可观测判据是
dream_consolidate失败行的metadata不再是null,以及summarize_compress失败行开始区分filtered-empty与「没写格式」。