Skip to content

[audit] 冲突自动裁决档(conflictAutoResolveEnabled)开启前的三条硬边界:阈值数字过期、freeze 父子绑定、NULL scope 被判为同 scope #616

Closed

Description

@modusensus

背景

conflictAutoResolveEnabled(#603 第 4 层,提交 7fbd60f / 9b2b2ef)是给人工裁决减负的自动档:相似度 ≥ 阈值的同 scope 对直接保留信息量更大的一条、归档另一条,不进人工队列。

在把这档真正打开之前做了一次审计,结论是现在不该开。三条边界如下,都需要落到代码注释与文档里,否则下一次调整会把它们重新踩掉。

一、阈值论证的数字是早期快照,当前库里这档等于不生效

提交信息与 src/dream/sleep.js 的注释都写着:

live 库 345 条有相似度的历史行里,>=0.95 只有 20 条(5.8%)、>=0.94 是 28 条(8.1%)

2026-11-09 在 live 库 D:\DSH\.dsh\memory\memory.db 重跑相同口径:

项 实测
conflict_pending 全表 748 行
similarity 非 NULL 11 行
其中 ≥0.95 0 行
最高一对 0.914
分档 ≥0.95=0,0.94–0.95=0,0.90–0.94=1,<0.90=10

也就是说按默认阈值 0.95 打开开关,今晚一对都不会被自动裁决——用户看到的会是「什么都没变」,然后怀疑功能坏了。这是最该避免的「假成功」。

另:748 行里 737 行 similarity 为 NULL(62606bc 之前入库的老行),这批在队列排序里沉底,相似度条只能回落到解析 reason 字符串。

二、自动档嵌在 freeze 分支里,不是独立开关

src/dream/sleep.js:432 的 if (config.conflictFreezeEnabled === true) 一直开到 :566,自动裁决(autoEnabled/autoPool/autoResolved)全在这段里。:568 往后的非 freeze 路径只有「跨 scope 停车 + LLM 裁决」,没有自动档。

面板上 conflictFreezeEnabled 与 conflictAutoResolveEnabled 看着是平行开关,实际是父子关系。哪天关掉 freeze,自动档会跟着静默消失,且没有任何提示。

三、自动档零可见,台账记不出赢家

  • src/dream/sleep.js:555 返回的 autoResolved / autoFailed 在全仓除 sleep.js 自己与 test/scope-candidates.test.js 外没有任何消费者;src/dream.js 的两处 return(:1675、:1741)只取 frozen。
  • conflict_reviewed 表有 saveConflictReviewed / loadConflictReviewed,但没有任何 API 路由暴露。
  • 该表 schema 为 (memory_a, memory_b, judged_at, outcome, run_id),不记哪一方赢。而 memory_a/b 是排序过的,事后想回答「这条为什么被归档」只能去读 loser 正文里的「(已被取代:…)」后缀反推。

四、未标注 scope 被当成同 scope(最重的一条)

isCrossScopePair(src/dream/sleep.js:359):

const isCrossScopePair = (p) =>
  scopeKeyOf(p.a.agent_scope) !== scopeKeyOf(p.b.agent_scope)
  || scopeKeyOf(p.a.workspace_scope) !== scopeKeyOf(p.b.workspace_scope);

scopeKeyOf(src/scope.js:185)把 NULL / 空串归一成 null,于是 null !== null 为 false —— 「未标注」被判定为「同 scope」。

实测活跃库(archived=0):

项 实测
活跃记忆总数 8259
两个 scope 全 NULL 2710(32.8%)
agent_scope NULL 2710
workspace_scope NULL 2714

这批的归属是未知,不是相同。按「归属是用户的决定、永不自动裁决」(v0.8.1 #170)这条守卫,它们本来该落在不可自动的范围里,现在却直接进自动池。

一个利好:自动归档是可逆的

  • src/api.js:1055 的 POST /update 接 archived 布尔
  • lib/client.js:6961 有 {id, archived:false} 恢复按钮
  • src/tools.js:626 的 memory_update 支持 archived=false
  • lib/client.js:8223 有「只看已归档」过滤器

所以不是删数据,风险等级比听上去低。失败也会退回人工队列(src/dream/sleep.js:530-545),不会静默吞。

建议的处理顺序

  1. 先补可见性:面板能报「本轮自动裁决 N 对」与「自动档失败退回 M 对」,autoResolved 从 sleep.js 透出到 dream.js 的 phases 与面板。
  2. 修 NULL scope 判据:isCrossScopePair 里把「任一未标注」显式判为不可自动(而不是当同 scope),并为这批行给出归属提示。
  3. 台账补 winner 列:conflict_reviewed 加 winner/loser,或至少记 winner_id,让事后归因不必反推正文。
  4. 解除父子绑定:自动档不该依赖 conflictFreezeEnabled,或至少在 freeze 关闭时明确告警。
  5. 阈值按当前分布重定:开放后再按实测分布调,不要沿用注释里的 345/20。若坚持沿用 0.95,需在注释里注明该数字的来源快照与日期。

真正在减负的其实是 #603 已生效的第 1 层(每轮 ≤60、队列容量 conflictFreezeMaxPending=100),加上 748 条存量已全部清空(未解决 0 行)。自动档是「减峰值」,不是「清队列」。


审计来源:Ephor(dsh-mneme 代码审计)+ Amarre 在 live 库的独立复算,2026-11-09。

Activity

  1. modusensus commented on Nov 9, 2026

    @modusensus
    MemberAuthor

    边界已写进代码注释与配置说明,提交 6b7c497(comments only,无行为变化),已随 #614 合入 main(514d238)。

    #616 的三个待办仍开着,留给真正打开这一档之前处理:

    1. autoResolved / autoFailed 透出到 dream phases 与面板
    2. isCrossScopePair 把「任一 scope 未标注」显式判为不可自动
    3. conflict_reviewed 补 winner 列

    自动档默认仍为关(conflictAutoResolveEnabled = false),本次不动开关。

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions