Skip to content

[Bug] 「从远端合并到本机」把 patch 的 - insert: 块整份摊平成顶层行:所有纯新增条目(MCP server 等)在 DSH 侧 not found 失效 #133

Closed

Description

@LingYingNX

受影响的功能

远程同步 → 「从远端合并到本机」(sync-merge-apply)写回 profile patch(plugins / mcp 适配器)

插件版本

0.8.0

DSH 版本

桌面版 2.0.22-next(内核 dsh 0.2.2-rc.1 / Electron 44.7.0 / Node v24.21.0)

操作系统

Windows 11 专业版(build 28000)/ AMD64

DSH 的启动方式

桌面客户端(D:\DSH NEXT),非 WSL、非 CLI

问题描述

执行一次「从远端合并到本机」之后,$DSH_HOME/profiles/desktop/cordis.patch.yml 中所有 - insert: 块被整份摊平成顶层 - id: xxx 行(文件首行就是插件自己写的 # rewritten by dsh-config-manager import (original comments not preserved))。

DSH 的 patch 语义是:

  • 顶层 - id: X → 覆盖 / 修改已存在的条目;
  • - insert: 块成员 → 新增条目。

于是所有「bundle 层里本来不存在、只能靠 - insert: 新增」的条目,全部变成「修改一个不存在的条目」,DSH 每次加载都打 warn 并丢弃:

patch: entry "mcp-blender" not found
patch: entry "mcp-rea" not found
patch: entry "magpie-mcp-magpie-image" not found
patch: entry "magpie-mcp-minimax" not found

实际后果:4 个 MCP server 全部从 DSH 面板消失(设置 → Skill & MCP 列表为空),blender / rea / magpie-image / minimax 四条通道全断。

同一个文件里的 ui-*、llm-*、permission、imagegen 等条目毫发无伤 —— 因为它们本来就在 bundle 层(package.json 的 dsh.profile.bundles)里存在,被写成顶层行恰好是合法的 update。「只有纯新增条目全灭、其余全部存活」这个规律就是本 bug 的指纹,也是它不容易被发现的原因。

根因(已定位到行)

lib/index.js 的 PatchFileFacade.applyPatchChanges(file, changes)(第 1488 行起):

  1. 读现有文档,把每个 - insert: 块的成员摊平塞进 rows / order(1471-1541 行);
  2. 然后统一按顶层行输出(1557-1568 行):
// lib/index.js:1557
// 3. Rebuild: every id row is emitted as a top-level row. The loader treats
//    a top-level { id, name } row exactly like an `- insert:` block member
//    (dsh-base patch precedent), so the document stays semantically equal.
const out = [];
for (const id of order) {
    const raw = rows.get(id);
    if (raw !== undefined)
        out.push(raw);
}
const text = '# rewritten by dsh-config-manager import (original comments not preserved)\n'
    + dumpPatchDocument(out);
await atomicWriteFile(p, text);

第 1557-1559 行注释里的前提不成立。dsh-base 的「顶层 {id, name} 行能生效」只在该 id 已存在于某个 bundle 层时成立(顶层行覆盖它);对 profile 里不存在的 id,loader 会判成「找不到条目」并丢弃 —— 也就是上面那串 not found。新增语义只有 - insert: 有,两者并不等价。

顺带,同一条路径还会:

  • 整份重写文件 → 原有注释全丢、引号风格统一;
  • 值层面出现空值归一化(实测 7 处 "" → null:agency-agents.config.customTeams[0].constraints / .coordinatorPrompt、imagegen.config.channels[0/1].preset / .subscription、bili-scroll.config.personaPrompt);
  • 事务自检全绿:state: COMMITTED、commit.validated: true、validationWarnings: [],每步消息还写着「MCP server X 已写入 patch,重启后生效」。用户完全看不出已经写坏。

复现步骤

  1. DSH 桌面版,profile desktop;profiles/desktop/cordis.yml 是空列表,插件条目全部来自 package.json 的 dsh.profile.bundles;
  2. 在 profile patch 里放几个 - insert: 块(本例是 4 个 MCP server),确认 DSH 正常加载、面板能看到它们;
  3. 面板点「从远端合并到本机」,勾上 MCP 连接器 / 插件分区,执行合并;
  4. 看 profiles/desktop/cordis.patch.yml:- insert: 数量变成 0,MCP 条目变成顶层行;
  5. 文件写完后数秒内,DSH 日志出现上述 4 条 not found warn,MCP 从面板消失。

本机实测时间线(UTC;北京时间 = UTC+8):

时间 事件
2026-11-11T01:35:11Z DSH 自己的 recovery 快照里,该文件仍是 3 个 - insert: 块(干净态)
2026-11-11T03:22:12Z 事务 e186fe1b 开始,operationType: sync-merge-apply,plannedSteps: ["patch:imagegen","mcp:blender","mcp:rea","mcp:magpie-image","mcp:minimax"]
2026-11-11T03:22:19.402Z 该事务 COMMITTED,文件被重写
2026-11-11T03:22:24.075Z DSH 打出第一条 patch: entry "mcp-blender" not found(提交后 4.7 秒)

同一天又合并两次(03:24:04、03:24:40),每次都重新摊平一遍 —— 100% 可复现,不是时序巧合。另外 2026-11-04T06:04:26Z 的 recovery 快照里也出现过同样的 insert=0 + - id: mcp-blender 被摊到顶层第 64 行,说明这不是一次性事故。

建议修法

applyPatchChanges 重建文档时,对每个 id 先判断它是否已存在于生效的 patch 链(bundle 层 + 本文件之外的层)中:

  • 已存在 → 照旧输出顶层行(覆盖语义);
  • 不存在 → 重新包成 - insert: [ { id, name, ... } ](或保留它原先所在的 insert 块),绝不能把纯新增条目提升到顶层。

插件里已有现成的判断工具可以复用:patchRowActivates(raw, name)(lib/index.js:410-417,它已经兼容「单行 + insert 块成员」两种形态),以及 readPatchLines() / loadPatchDocument() 读取各层 patch 的能力。

另外建议把「原本在 insert 块里、这次被提升到顶层」的 id 收集起来写进 commit.validationWarnings(或直接拒绝这次写入),这样至少用户能看见,而不是一路绿勾。

影响面

只要用户的 profile 里有靠 - insert: 新增的条目(MCP server 是最常见的一类),一次「从远端合并到本机」就会把它们全部打成失效。本机这一次的受害者:4 个 MCP server + 4 个 dsh-ops-* 激活行 + 1 个本地插件激活行。


需要日志原文、patch 文件前后对照或事务 JSON 的话我可以在评论里补。


分诊状态(2026-11-11,AI triage)

结论:缺陷成立,已复现 —— 不是推测。 三条论断逐条复核通过:① 宿主 patch 合并循环里,只有不带 id 的 insert 块才走新增(data.push(...clone)),顶层行只按 id 覆盖已存在条目、查不到就打 patch: entry %C not found 丢弃;② DshPatchFileFacade.applyPatchChanges 确实把 insert 成员摊平进顶层行,其注释「顶层 {id,name} 行 ≡ insert 成员」与宿主源码冲突;③ 一条 update 即触发,不需要多次合并(用已发布 lib/index.js 做的单测级复现:insert 归零、注释丢失、name 变裸名、transport 消失)。

同时确认的第二个独立缺陷:buildMcpPatchLine() 按白名单重建 patch 行,不写 transport、name 不带 @deepseek-ai/ scope —— 摊平修好后这一条仍然存在。

事务为什么全绿:patch 行项无文件 rel ⇒ external: rel === null ⇒ beforeFp/afterFp 恒 null,校验只能空过(既有可证限制)。这条写路径缺的是写后语义自检。

实施状态:无。 工作树无相关改动、git log --all 无 #133 提交、CHANGELOG 无条目、.dsh/issue-ledger.md 无 133 行、版本仍为 0.8.0(= 报告版本)。本次分诊未改任何业务代码。

证据笔记:docs/evidence/profiles-and-instances.md → 「profile patch 的 insert 与顶层行不等价(#133,2026-11-11 源码核实)」(宿主语义原码引文、复现、修复判据、存疑项)。

维护者拍板(2026-11-11):Q1–Q3 全按推荐

  • Q1:摊平与 MCP 字段损失(transport + scoped name)一起修,分两个提交、两条验收;同轮更新 docs/contracts/bundle-format-v1.md 的 mcp 白名单。
  • Q2:写前拒绝——某次写入若会把原本的 insert 成员提升为顶层行,整笔拒绝、文件逐字节不变;不自动改用户文件,存量坏文件走插件自拍快照恢复。
  • Q3:补写后语义自检(重读断言 insert 成员仍在 insert 块内),覆盖 sync-merge-apply / import-apply / mcp / plugins / prompts 全部写入口。

下一步:标签转 bug + ready-for-agent,实施单(Agent Brief)见本票评论。

未验证:宿主是否接受裸名 dsh-mcp-client 解析到 @deepseek-ai/dsh-mcp-client;报告者观察到的 7 处 "" → null 归一化本机未复现。

Activity

  1. github-actions commented on Nov 11, 2026

    @github-actions
    Contributor

    感谢反馈!🙏 我通常会在 48 小时内回复;这条已加上 needs-triage,进度会体现在标签上。

    Thanks for the report! I usually reply within 48h. This issue is labeled needs-triage; progress shows up in the labels.

    如果还没写,补上这几项能少好几轮来回 / Adding these saves round trips:

    • 截图 / 录屏 Screenshot —— 直接把图片拖进编辑框,或 Ctrl+V 粘贴
    • 控制台报错 Console —— F12 → Console,复制文本或截图
    • 环境 Environment —— 插件版本(设置 → 备份与迁移 → 关于,可一键复制)、DSH 版本、系统、浏览器版本、显示缩放与浏览器缩放、深色/浅色

    ⚠️ 请务必抹掉 API Key / Token / 密码等敏感信息(包括截图里的)。
    Never paste real secrets — including inside screenshots.

  2. added
    needs-triage等待维护者分类与确认(awaiting maintainer triage)
    on Nov 11, 2026
  3. LingYingNX commented on Nov 11, 2026

    @LingYingNX
    Author

    补充:可复核的原始证据

    只补证据,不改正文结论。以下路径都在本机 DSH 数据目录下,全部可以自己复现。

    1. 日志:这 4 条 MCP 的 not found 是本次合并"引入"的

    统计 ~/.dsh/logs/desktop-next-2026-11-11.0.jsonl 里 event = "log" 且 message 以 not found 结尾的记录:

    2026-11-11T01:33:09.465Z  patch: entry "dsh-ops-shell" not found          (53 次)
    2026-11-11T01:33:09.465Z  patch: entry "dsh-ops-file" not found           (53 次)
    2026-11-11T01:33:09.467Z  patch: entry "dsh-ops-ptc" not found            (53 次)
    2026-11-11T01:33:09.467Z  patch: entry "dsh-ops-discovery" not found      (53 次)
    2026-11-11T01:33:09.467Z  patch: entry "pixiv-duty" not found             (54 次)
    2026-11-11T01:33:09.467Z  patch: entry "agents-anywhere-bridge-next" ...  (55 次)
    --- 以上 6 条在 01:33 那次启动就存在,与本次合并无关 ---
    2026-11-11T03:22:24.075Z  patch: entry "mcp-blender" not found            (31 次)  <- 首次
    2026-11-11T03:22:24.075Z  patch: entry "mcp-rea" not found                (31 次)  <- 首次
    2026-11-11T03:22:24.075Z  patch: entry "magpie-mcp-magpie-image" not ...  (31 次)  <- 首次
    2026-11-11T03:22:24.076Z  patch: entry "magpie-mcp-minimax" not found     (31 次)  <- 首次
    

    4 条 MCP 在整个日志里第一次出现就是 2026-11-11T03:22:24.075Z,也就是事务提交(03:22:19.402Z)后 4.7 秒、落在同一个 run(e25e8d99-6008-41ca-b8e7-c60b98ad11c0,01:34:56 启动、05:41:53 结束)里;而上面 6 条来自更早的 run(788fb7dc),两者互不相干 —— MCP 不是"一直就坏",是这一刻才坏的。

    提交后第一屏原文(注意 sequence 122 是这次写入之后 patch 的第一次重新加载):

    {"time":"2026-11-11T03:22:24.075Z","runId":"e25e8d99-6008-41ca-b8e7-c60b98ad11c0","sequence":122,"source":"host.kernel.root","event":"log","level":"warn","message":"patch: entry \"mcp-blender\" not found"}
    {"time":"2026-11-11T03:22:24.075Z","sequence":123,"level":"warn","message":"patch: entry \"mcp-rea\" not found"}
    {"time":"2026-11-11T03:22:24.075Z","sequence":124,"level":"warn","message":"patch: entry \"dsh-ops-shell\" not found"}
    {"time":"2026-11-11T03:22:24.075Z","sequence":127,"level":"warn","message":"patch: entry \"dsh-ops-discovery\" not found"}
    {"time":"2026-11-11T03:22:24.075Z","sequence":128,"level":"warn","message":"patch: entry \"magpie-mcp-magpie-image\" not found"}
    {"time":"2026-11-11T03:22:24.076Z","sequence":129,"level":"warn","message":"patch: entry \"magpie-mcp-minimax\" not found"}
    {"time":"2026-11-11T03:22:24.076Z","sequence":130,"level":"warn","message":"patch: entry \"pixiv-duty\" not found"}

    2. 事务 JSON:它自己认为一切正常

    ~/.dsh/dsh-config-manager/transactions/completed/e186fe1b-2f25-4df5-9269-33ad38f663f7.json
    (先把复盘用到的字段摘出来,字段名与原文件一致):

    {
      "operationType": "sync-merge-apply",
      "packageVersion": "0.8.0",
      "state": "COMMITTED",
      "createdAt": "2026-11-11T03:22:12.353Z",
      "updatedAt": "2026-11-11T03:22:19.403Z",
      "snapshotId": "c80f89f7-f213-4f53-86c5-96220a293fab",
      "plannedSteps": ["patch:imagegen", "mcp:blender", "mcp:rea", "mcp:magpie-image", "mcp:minimax"],
      "commit": { "at": "2026-11-11T03:22:19.402Z", "validated": true, "validationWarnings": [] },
      "rollback": { "attemptedAt": null, "full": false, "failed": [], "entryDone": {} },
      "recovery": { "attemptedAt": null, "outcome": null, "reason": "", "attempts": 0 }
    }

    steps 里 5 步全是 status: "done",文案还是"MCP server X 已写入 patch,重启后生效",而且 beforeFp 和 afterFp 都是 null:

    "mcp:blender": { "adapter": "mcp", "kind": "Update", "external": true,
      "beforeFp": null, "afterFp": null, "status": "done",
      "appliedAt": "2026-11-11T03:22:19.099Z",
      "message": "MCP server blender 已写入 patch,重启后生效" }

    即:事务层手里没有任何"内容真的变了 / 没变"的指纹,却给了 validated: true + 空 validationWarnings。这是"用户完全看不出已经写坏"的直接来源。

    3. 插件自己在写入前 0.9 秒拍下的快照

    同一个 snapshotId 的目录 ~/.dsh/dsh-config-manager/snapshots/c80f89f7-f213-4f53-86c5-96220a293fab/snapshot.json:

    • "createdAt": "2026-11-11T03:22:19.049Z"(比写入早 0.9 秒)
    • hostFileBackups 中有 profiles/desktop/cordis.patch.yml 的写入前完整副本(blob,9016 字节)。我把它和 DSH 自己在 01:35:11Z 存的 recovery 副本逐字节比对:完全相同 → 可以当作写前状态
    • 这份写前副本:- insert: 3 个、顶层 - id: 27 个;mcp-blender 在第 61 行、缩进 4(insert 成员):
    - insert:
        - id: mcp-blender
          name: '@deepseek-ai/dsh-mcp-client'
          config:
            serverName: blender
            transport: stdio
            command: 'C:\Users\LYNX\.local\bin\uv.exe'
            args:
              - tool
              - run
              - --from
    • 而且 entries 里 5 条全部 "existed": true,before 保存的就是这份完整值(含 transport 和带 scope 的 name)—— 说明插件写之前手里就有正确数据:
    {
      "kind": "patchLine",
      "adapter": "mcp",
      "ref": "mcp-blender",
      "file": "profiles/desktop/cordis.patch.yml",
      "before": {
        "id": "mcp-blender",
        "name": "@deepseek-ai/dsh-mcp-client",
        "config": {
          "serverName": "blender",
          "transport": "stdio",
          "command": "C:\\Users\\LYNX\\.local\\bin\\uv.exe",
          "args": [
            "tool",
            "run",
            "--from",
            "mcp-for-blender",
            "mcp-for-blender"
          ]
        }
      },
      "existed": true
    }

    4. 写入之后(DSH 自己在 05:42:01Z 存的 recovery 副本)

    - insert: 0 个、顶层 - id: 31 个 —— 27 + 4,算术正好等于被提升的 4 条。同一条 mcp-blender 变成了缩进 0 的顶层行:

    - id: mcp-blender
      name: dsh-mcp-client
      config:
        serverName: blender
        command: C:\Users\LYNX\.local\bin\uv.exe
        args:
          - tool
          - run
          - --from

    四条前后对照:

    id                          写前                                          写后
    mcp-blender                 第61行 缩进4 在 insert 块内                    第61行 缩进0(顶层)
                                name: '@deepseek-ai/dsh-mcp-client'           name: dsh-mcp-client
                                transport: stdio                              transport 整行消失
    mcp-rea                     第73行 缩进4(另有 disabled: true)            第72行 顶层
    magpie-mcp-magpie-image     第164行 缩进4                                 第158行 顶层
    magpie-mcp-minimax          第212行 缩进4 transport: streamable-http       第205行 顶层 transport 消失
    

    顺带两个同一次写入里的差异(同样的 4 条才有,未被提升的条目没有):

    1. name 的 @deepseek-ai/ scope 丢了:'@deepseek-ai/dsh-mcp-client' → dsh-mcp-client;而同一份文件里其它补丁行(例如 - id: ui-settings-general 的 name: '@deepseek-ai/dsh-client-ui-settings-general')scope 完好。
    2. transport 字段被整个丢掉(stdio / streamable-http)。magpie-mcp-minimax 是 transport: streamable-http + url,少了 transport 之后靠什么判定传输方式我不确定,但这不像是"语义等价"的重写。

    这两点是从"写前 blob vs 写后 recovery 副本"的 diff 里读出来的;如果 transport / scope 是别处有意归一化,请忽略,但它们确实都发生在 03:22 这一次写入之后。

    5. 手动复核(大约 5 分钟)

    1. 在 ~/.dsh/dsh-config-manager/transactions/completed/ 里找 operationType: "sync-merge-apply" 的那个 JSON,记下 snapshotId;
    2. 打开 ~/.dsh/dsh-config-manager/snapshots/<snapshotId>/snapshot.json,在 hostFileBackups 里找 profiles/<profile>/cordis.patch.yml,读它的 blobPath —— 那个 blob 就是写入前的原文件;
    3. 和当前 profiles/<profile>/cordis.patch.yml 比 - insert: 的条数:写前 3、写后 0。

    一行统计(PowerShell):

    $f = Join-Path $env:USERPROFILE '.dsh/profiles/desktop/cordis.patch.yml'
    "inserts = " + (Select-String -Path $f -Pattern '^\s*- insert:').Count
    "topIds  = " + (Select-String -Path $f -Pattern '^- id:').Count

    上面涉及的三个文件(事务 JSON、写前快照、以及写前/写后两份 patch 副本)如果需要我打包贴出来,或者要我补其它片段(例如 exportAppliedSections 之后的文件内容、连续多次合并的对比),说一声就行。

  4. LingYingNX commented on Nov 11, 2026

    @LingYingNX
    Author

    补充:transport / scoped name 丢失不是 YAML 归一化,而是写回函数的固定输出

    补一条代码级定位,方便把“摊平”和“字段丢失”一起修。以下基于 0.8.0 的 lib/adapters/mcp.js:

    • buildMcpPatchLine(lineId, server)(106-123 行)构造 config 时只写入 serverName;streamable-http 分支只写 url / headers,stdio 分支只写 command / args / env / cwd,没有任何地方写 transport。
    • 该函数返回 { id: lineId, name: 'dsh-mcp-client', config }(122 行),没有保留 @deepseek-ai/ scope。
    • McpAdapter.applyItem(404-425 行)对 Create 和 Update 都会调用 buildMcpPatchLine,再执行 applyPatchChanges;所以即使 before 中的原行已有 transport: stdio|streamable-http 和带 scope 的 name,一次 Update 也会按上面的模板重写,而不是保留原值。

    这正好对应补充评论里看到的两个差异:mcp-blender 的 transport 消失、name 变成 dsh-mcp-client。它们是 buildMcpPatchLine 的固定输出,不是 diff / YAML 层的偶发归一化。

    另外,@max-null/dsh-skill-mcp-center/dist/service.js:421 只接受 entry.options.name === '@deepseek-ai/dsh-mcp-client';如果只修 applyPatchChanges 的顶层摊平、仍让 buildMcpPatchLine 写短名,MCP 面板可能还是空。

    建议回归测试至少包含两条:mcp-blender 写回后保留 transport: stdio + scoped name;magpie-mcp-minimax 保留 transport: streamable-http + url + scoped name。若短名是有意设计,也请在代码注释里说明下游如何映射。

  5. xiajiajun516 commented on Nov 11, 2026

    @xiajiajun516
    Owner

    This was generated by AI during triage.

    分诊笔记(2026-11-11)

    已查阅范围:本票正文与 3 条评论;仓内既有笔记 docs/evidence/profiles-and-instances.md(cordis.patch.yml 方言与拒覆盖)、docs/evidence/recovery-console.md(external step 无指纹)、docs/contracts/bundle-format-v1.md 的 mcp 白名单、docs/contracts/compat-matrix.md R15、docs/known-issues/known-limits.md §3、.out-of-scope/(仅 README)、.dsh/issue-ledger.md、git log --all;以及本机 D:\Apps\DSH\resources\app.asar 里内嵌的 patch 合并循环源码。既有笔记没有一份写到 insert 与顶层行的语义差别,所以最后这项是新查的。

    核实:成立,并且复现了。

    1. 宿主语义(asar 内嵌合并循环,三处同段):insert 不带 id 时 data.push(...clone) + buildMap(clone),这是唯一的新增路径;带 id 的 insert 只往已存在 group 的 config 里 push。非 insert 行一律 entryMap.get(id):查不到 → patch: entry %C not found 丢弃;name 与目标不一致 → patch: name mismatch … skipping。⇒ 顶层行只覆盖、绝不新增,src/index.ts 里那句 “dsh-base patch precedent” 的注释前提是反的。
    2. 摊平发生在哪:DshPatchFileFacade.applyPatchChanges 载入时把 insert 成员塞进 rows/order,重建时统一输出顶层行(与你读到的 lib/index.js 同一处,源码在 src/index.ts)。
    3. 一次就触发:.tmp/repro-133.cjs 用已发布的 lib/index.js(DshPatchFileFacade + buildMcpPatchLine),在临时 home 造「1 个顶层行 + 1 个 insert 块(mcp-blender,带 scoped name 与 transport: stdio)+ 1 行注释」,跑一次 update → insert=0、顶层 2 行、注释丢失、name 变 dsh-mcp-client、transport 整行消失。不需要多次合并,这也解释了你同一天三次合并为什么次次复现。

    顺带确认的第二个缺陷(独立于摊平):buildMcpPatchLine() 按白名单重建 patch 行,只输出 serverName + url/headers 或 command/args/env/cwd;不写 transport,name 也不带 @deepseek-ai/ scope。这条界线写在 docs/contracts/bundle-format-v1.md 的 mcp 一节里,属于「按现有契约也不会保留」的字段损失 —— 要修得连契约一起改。

    「事务全绿」不是本 bug 独有:patch 行项没有文件 rel ⇒ external: rel === null ⇒ beforeFp/afterFp 恒 null,校验只能空过(docs/evidence/recovery-console.md 已记录这条可证限制)。要挡住这类写入,靠指纹不行,只能加写后语义自检。

    实施情况:无。 工作树无相关改动、git log --all 无 #133 提交、CHANGELOG 无条目、.dsh/issue-ledger.md 无 133 行、版本 0.8.0 = 你报告的版本。本次分诊没有改任何业务代码。

    重复实现检查:查过 patch 写路径(applyPatchChanges / patchRowActivates / patch-layers.ts / boot-rescue.ts)。「按层身份决定顶层还是 insert」这套判断只有 boot-rescue.ts 在写救援挂载时做对过(新增只写 - insert: 块;包已在 bundles 时一行都不写),没有可直接复用的完整实现。

    证据笔记:docs/evidence/profiles-and-instances.md,新节「profile patch 的 insert 与顶层行不等价(#133,2026-11-11 源码核实)」(宿主原码引文、复现、修复判据、存疑项)。票正文也补了「分诊状态」一节。

    待你拍板

    ❓ Q1 修复范围:只修「insert 被摊平」,还是把 buildMcpPatchLine 的字段损失(transport + scoped name)一起修?
    ➡️ 建议一起修、但分成两个提交两条验收:两个缺陷在同一次写入里打坏同一个文件,分两次发版会让中间版本的用户照样拿不到完整修复;字段那条要同步改 docs/contracts/bundle-format-v1.md。

    ❓ Q2 已写坏的 profile 怎么兜底:自动改回去、写前拒绝、还是只告警?
    ➡️ 建议不自动改用户文件:写前若能判定「原本在 insert 里的 id 会被提升到顶层」,就直接拒绝这次写入并给出可操作错误(插件自拍快照里就有写前副本,回滚路径清楚);存量的坏文件靠快照恢复 + 文档说明。自动重写别人手写的 patch 文件,风险大于收益。

    ❓ Q3 要不要连「写后语义自检」一起做:写完重读并断言「原本在 insert 块里的 id 仍在 insert 块里」,不满足就 warning 或回滚;覆盖 sync-merge-apply / import-apply / mcp / plugins / prompts 全部写入口。
    ➡️ 建议要。patch 行项天然没有指纹,这是这条写路径唯一能自我证明的防线,也是这次「一路绿勾」直接的成因。

    回复之后我会做:Q1–Q3 都按推荐 ⇒ 打 ready-for-agent 并贴 Agent Brief(验收判据 + 范围边界);只修摊平、或你要自己实现 ⇒ ready-for-human;需要我再补宿主侧证据(裸名能否解析)⇒ 先 needs-info。当前标签保持 bug + needs-triage,不替你拍板。

  6. xiajiajun516 commented on Nov 11, 2026

    @xiajiajun516
    Owner

    This was generated by AI during triage.

    Agent Brief

    类别: bug
    一句话: patch 文档重建必须保留「顶层行 = 覆盖、- insert: 块 = 新增」的语义,且 MCP 写回不得丢 transport 与 scoped name。

    当前行为:

    • 任何一次 patch 写入(sync-merge-apply、import-apply、MCP/插件/提示词的 Create 与 Update 全部共用同一个门面)在重建文档时把所有 - insert: 块的成员提升为顶层 - id: 行。
    • DSH 的合并语义是:顶层行只按 id 覆盖已存在条目(查不到就 patch: entry "…" not found 丢弃);只有不带 id 的 insert 块才新增。于是所有纯新增条目(典型是 MCP server)在写入后的下一次加载里全部失效,而 bundle 层本来就有的条目因为顶层行恰好合法而毫发无伤。一条 update 就够触发,不需要多次合并。
    • 同一写路径上还有第二个独立缺陷:MCP 行按白名单重建时只写 serverName 与按 type 分支的 url/headers 或 command/args/env/cwd,丢掉 transport,name 也不带 @deepseek-ai/ scope。
    • 写入过程整份重写文件、丢掉原有注释,且事务自检恒绿(patch 行项没有文件 rel,beforeFp/afterFp 恒 null),用户看不到写坏。

    期望行为:

    1. 重建时按「id 是否已存在于生效 patch 链(bundle 层 + 本文件之外的层)」分流:已存在 ⇒ 顶层覆盖行;不存在 ⇒ 必须留在 - insert: 块里。原本是 insert 成员的 id,写后必须仍是 insert 成员。
    2. 若某次写入会不可避免地提升这类 id(例如实现拿不准层身份),整笔拒绝并返回可操作错误,文件逐字节不变;不自动改写用户手写的 patch 文件。
    3. 新增语义要真的成立:通过任一入口新建一个此前不存在的条目时,写回结果为 insert 块成员,DSH 加载后不再出现 not found。
    4. MCP 写回保留 config.transport(stdio / streamable-http 原值)与 name: '@deepseek-ai/dsh-mcp-client';对已有行做 Update 时也不得把它们抹掉。
    5. 写后自检:重读文档并断言「写前是 insert 成员的 id 写后仍在 insert 块内」,不满足就进 commit.validationWarnings 或回滚,不再出现「全绿但写坏」。
    6. 契约文档同步:mcp 白名单与 patch 行章节、兼容性矩阵里对应的风险条目。

    关键接口:

    • DshPatchFileFacade.applyPatchChanges(file, changes):现签名保留;changes 的 action 已经区分 insert / update / remove,但三种都按顶层行输出 —— 需要让 insert(以及任何目标 id 不在生效链里的写入)真正落到 insert 块,update 才用顶层行。
    • patch 层身份解析:已有 readEffectivePatchLines / resolveWriteLayer / locatePatchLineLayer 提供层身份,patchRowActivates 已兼容「单行 + insert 成员」两种形态 —— 分流判断必须复用它,不要再写第二份形态识别。
    • buildMcpPatchLine(lineId, server):返回的 patch 行需要携带 transport 与完整 name;同时确认读侧(patch 行 → MCP 条目)能读回这两项,避免写读不一致。
    • 救援路径 rescuePatchText 已经示范正确形状(新增只写 insert 块;包已在 bundles 时一行都不写,因为同 id 重复会让 loader 抛错)—— 分流逻辑要与它同口径。

    验收判据:

    • 给定一份含多个 insert 块(每块 1..n 个成员)的 patch 文档,做一次任意 update / merge 后,块仍在、成员 id 集合与写前一致(顺序可变)。
    • 写后文档中不存在「写前是 insert 成员、写后变成顶层行」的 id;人为构造这种情形时写入被拒绝、文件逐字节不变、事务给出可操作错误。
    • 从零新建一个此前不存在的 MCP server:写回落进 insert 块,重启 DSH 后无 patch: entry "…" not found,面板可见该 server。
    • MCP Update 回归:stdio 型保留 transport: stdio;streamable-http 型保留 transport: streamable-http + url;两者 name 均为 @deepseek-ai/dsh-mcp-client。
    • 写后自检失败时,事务不再出现「validated: true + validationWarnings: []」却已写坏的结果(要么 warning 可见,要么回滚)。
    • 四条入口都有覆盖:sync-merge-apply、import-apply、MCP 适配器、插件/提示词适配器(同一门面,用真实文档往返断言)。
    • 契约文档(mcp 白名单、patch 行章节、兼容性矩阵 R15)与实际输出一致,conformance / roundtrip 测试通过。

    范围之外:

    • 不自动修复用户已经被写坏的 patch 文件;存量恢复走插件自拍快照与文档说明。
    • 不保留 patch 文件原有的注释与排版(首行「rewritten by …」说明保留即可)。
    • 不动 !!js 方言读写与「存在但解析不了就拒覆盖」的现有逻辑。
    • 不改 DSH 宿主侧行为,以上下游现有语义为准。
    • 报告里那 7 处 "" → null 的归一化本机未复现,不在本票范围内处理。

    承接: 本票正文的事故时间线与评论里的日志/事务 JSON 证据仍然有效,作为回归夹具的取材来源。

  7. added
    ready-for-agent完全描述清楚、可直接交给 AFK agent(fully specified, ready for an AFK agent)
    and removed
    needs-triage等待维护者分类与确认(awaiting maintainer triage)
    on Nov 11, 2026
  8. xiajiajun516 commented on Nov 11, 2026

    @xiajiajun516
    Owner

    这是实施回执(AI 在本地完成并验证),不是 triage 评论;发版后再回版本号。

    已修复(本地主干,待发版)

    两个提交,按 Brief 的验收判据逐条落地:

    • 74539bd — insert 块不再被摊平:写回改为「按原文档的归属重排」,insert 成员留在块里、顶层行仍是顶层行,update 不改归属、新增按动作进块;落盘之前做两道闸:结构自检(重建后 id 身份必须与计划一致)与跨层提升守卫(另一层 insert 里已有的 id 不许被写成顶层行)——任一不过即拒写、文件逐字节不变,报 host.patchRefusePromote / host.patchSemanticDrift。
    • 06daa26 — MCP 写回补齐字段:每行显式写 config.transport(= 条目 type)与 scoped 行名 @deepseek-ai/dsh-mcp-client;读侧先认 transport,缺该字段的旧行才回落「有没有 url」的猜测,存量行不受影响。

    验证

    • npm run typecheck 通过(提交 1、2 各一次)。
    • npm test:tests 5574 / pass 5572 / fail 0 / skipped 2;其中 src/utils/workdir-lock.test.ts 的「不同工作副本互不阻塞」在负载下抖动过一次,单文件重跑 10/10 绿(与本改动无关的时序用例)。
    • 新增:src/core/patch-document.test.ts 9 例(纯函数)、src/index.facade.test.ts 3 例真机(含「跨层提升拒写且文件不变」)、src/adapters/mcp.test.ts 补 http 分支与读侧优先级。

    行为变化(发版说明会写进 CHANGELOG)

    • 从零新建的 MCP 服务器行现在落在 - insert: 块里,重启不再 patch: entry not found。
    • 已经写坏的 patch 文件不会被自动修(按 Brief 的决定):下一次写入会先拒,由用户从快照恢复。
    • 注释与排版仍不保留(与之前一致,未在本票范围内)。

    文档同轮更新:docs/contracts/bundle-format-v1.md 的 mcp 写回形状、CHANGELOG.md 的 ## [Unreleased] 两条、docs/evidence/profiles-and-instances.md 的 #133 节补「修复落点」。

    标签:bug + confirmed(已核实、修复待发版);发版后回版本号并换 fixed。

  9. added
    confirmed已确认,会修复(confirmed, will be fixed)
    and removed
    ready-for-agent完全描述清楚、可直接交给 AFK agent(fully specified, ready for an AFK agent)
    on Nov 11, 2026
  10. added a commit that references this issue on Nov 11, 2026
  11. xiajiajun516 commented on Nov 11, 2026

    @xiajiajun516
    Owner

    This was generated by AI during triage.

    已在 v0.8.1 修复并发布。

    现在从远端合并或导入配置后,profile patch 里 - insert: 块中的纯新增条目(MCP server 等)不会再被摊平成顶层行,重启后不会再整条 not found 消失;同时 MCP 写回行会带上连接类型与完整包名(@deepseek-ai/dsh-mcp-client),升级后不用手动改配置文件。

    升级:

    dsh plugin --profile <你的档案> add dsh-config-manager@0.8.1
    

    自证:升级并重启后,把之前读不到的 MCP server 重新合并一次,然后重启 DSH —— 服务器仍在列表里、能连上,即为修复生效。

  12. added
    fixed已在某个版本中修复(fixed in a release)
    and removed
    confirmed已确认,会修复(confirmed, will be fixed)
    on Nov 11, 2026
  13. added a commit that references this issue on Nov 11, 2026
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

    bugSomething isn't workingfixed已在某个版本中修复(fixed in a release)

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions