DeepSeek Harness 升级后设置静默丢失:settings.yaml 迁移与模块双实例

故障排查发布于 2026-10-03作者: DeepSeek Plugin 插件市场
DeepSeek HarnessDSHsettings.yaml配置迁移设置丢失配置回退dsh-app-boot
DSH 从旧版升级后设置页突然变成默认值、模型与 provider 消失,或在设置面板里改任何选项都立刻弹回——两种症状都不报错。前者是 settings.yaml 一次性迁移「改名先于写入」,失败即永不重试;后者是模块级状态被拆成两个实例。本文给出三种迁移失败模式、恢复步骤与上游补丁。

DSH 升到 0.1.7 线之后,会出现两种「设置悄悄没了」的症状:一种是设置页整片变成默认值、LLM provider 与模型选择全部消失;另一种是你在设置面板里改任何选项都立刻弹回原值,cordis.patch.yml 的修改时间纹丝不动——两种都不给任何错误提示。 前者来自 0.1.7 新增的一次性迁移:importLegacyDocument() 把 settings.yaml 改名成 .imported 之后才逐段写入,只要写入通道那一刻不可用,整份文档就被永久标记为已消费、永不重试(#7534、#7814)。后者来自 dsh-app-boot 在同一个进程里被加载成了两个模块实例,根 Include 条目写在实例 A 的模块级 WeakMap 里、却在实例 B 被读取,于是写入被拒(#7675)。好消息是数据几乎都没丢——旧值都在 .imported 里,整篇文章按「先分诊 → 两条机制 → 恢复与修复」展开,每步都给可粘贴的命令与文件位置。

先分诊:两种症状,别当成同一件事修

动手之前先确认到底中了哪一条——它们的数据位置、日志特征、绕过方式都不一样。

判据迁移一次性丢失设置改动被静默回退
设置页表现整片空白 / 回到默认值选项立刻弹回已存值
是否有 ~/.dsh/settings.yaml.imported有可能没有
cordis.patch.yml 修改时间不变不变(写入被拒)
日志特征settings: section … was not imported + cannot get required service "configEditor" in inactive contextsettings/rejected: dsh: profile reload requires the root Include entry
影响面升级后一次性每一次设置写入(还有插件启停、热重载)
根因位置dsh-settings 的导入顺序dsh-app-boot 的模块级 WeakMap

两条链会在同一份坏布局里交汇:模块双实例正是迁移失败模式 A 的成因(见下一节),所以有人会同时看到两种症状。

机制一:一次性迁移的「提交点倒置」——改名先于写入

一句话:迁移先 rename 再写,而「写入通道不可用」和「某段被拒绝」在代码里长得一模一样,于是任何一次失败都被当成「已处理」,文档从此无人消费。

顺序就是缺陷本体

0.1.7 引入的导入路径(提交 601d6761e4,feat(settings): project volatile Config through profile-backed forms (#4587),含 dsh-v0.1.7-alpha.1)里,importLegacyDocument() 的顺序是(#7814):

js
async importLegacyDocument() {
    const path = join(profile.home, "settings.yaml");
    if (!existsSync(path)) return;
    const imported = `${path}.imported`;
    await rename(path, imported);               // ← 提交点在所有写入之前
    const sections = parse(await readFile(imported, "utf8"));
    for (const [section, values] of Object.entries(sections ?? {})) {
        const ns = LEGACY_SECTION_ENTRIES[section] ?? section;
        try {
            await this.update(ns, values);      // ← configEditor 不可用时每段都抛
        } catch (error) {
            this.ownerContext.logger.warn("settings: section %s of %s was not imported into entry %s", section, imported, ns);
            this.ownerContext.logger.warn(error); // ← 失败只 warn,无重试、无 UI 提示
        }
    }
}

JSDoc 自述这是刻意的:"The document is renamed before the first write, so a partial import never repeats"——防重复的代价,就是失败后永不重试(#7814)。而这个导入是挂在 ctx.root.loader.await().then(…) 上的一次性延续(index.ts:235),「await settle」并不等于「服务可用」。

触发路径一:启动审计失败,导入写进已释放的 context

社区在 0.1.6-alpha.2 → 0.1.7-alpha.1 的升级上复现了最完整的一条(#7534):

  1. @deepseek-ai/dsh-app-boot 的启动审计发现有必需条目未激活(本例是 stale 的 web-app 前端 dist 与 client-modules bundle),抛 StartupError 并 await ctx.fiber.dispose() 释放整棵树;
  2. 而一次性导入与那次释放是同一个 loader settled 时刻的延续(app-boot/src/index.ts:971-977 先 await ctx.get('loader')?.await(),再审计、再 dispose);
  3. 于是 10 个 section 全部被写进了已经 dispose 的 context:每次写入都要解析 this.ownerContext.configEditor(index.ts:382),此时得到 cannot get required service "configEditor" in inactive context;
  4. 每段的 try/catch 把致命错误降级成一条 warning 并继续循环——「服务已死」和「该段被当前组合拒绝」在这里不可区分(index.ts:248-256);
  5. 文档在第一次写入前就改名为 .imported(index.ts:245-246),而后续启动因为 settings.yaml 不存在直接 return(index.ts:244);this.closed 早已存在(index.ts:227)却从未被 importLegacyDocument 检查;循环结束后 index.ts:257 还无条件打一行 settings: imported …——一次什么都没导入的启动,也可能报告成功。

实测的时间线(同一 10 毫秒窗口内):

text
ts …25.630Z  launch: 2 required plugins did not activate(stale dist,加载器需约 10 s 才 settle)
ts …35.278Z  hmr: 'config reload at %C failed' / HMR is disposed
ts …35.288Z  settings-forms: 'section ui-onboarding … was not imported'
ts …35.288Z  settings-forms: Error: cannot get required service "configEditor" in inactive context
… 10 个 section,全部在同一毫秒

那位报告者的 10 个 section 是 ui-onboarding、ui-theme、agent-presets、permission、agent-default-model、llm-pi-ai、provider-quotas、dsh-better-sidebar、glance-theme、subagent-model-selection——LLM providers、主题、权限预设、默认模型全部回退。

触发路径二:端口占用 + HMR reload 的启动竞态

另一份报告在 0.1.7-rc.1 上撞到的是同类竞态的另一入口(#7814):宿主启动时端口被占用,叠加 HMR reload,导致 configEditor 所在的 context 在导入执行时处于 inactive,4 个 section(ui-onboarding、ui-theme、llm-pi-ai、agent-default-model)全部抛同一个错误——注意这不是「部分失败」,而是写入通道整体不可用;作者把它称作「原子性倒置」。并且他复核了 0.1.7-rc.2:该函数与 rc.1 逐字相同,缺陷仍在。

三条互相独立的失败模式:A / B / C

社区对 .imported 里的 section 逐节核对后发现,任一条单独成立就足以让某一节静默丢失(#7814):

模式抛错位置触发条件对应修法
Aprofile reload requires the root Include entry(dsh-app-boot)同一个包存在两份实例/副本,模块私有 WeakMap 读不到写入消除双实例(见机制二)
BNo configurable plugin entry "<ns>"(dsh-settings)旧节名 ≠ 真实 entry id把旧节名映射到真实 entry id
Chas no volatile fields(dsh-settings)该条目没有声明任何 volatile() 字段目前无兜底通道

B 的关键细节:代码里的 legacy 映射表只有三项(dsh-settings 的 LEGACY_SECTION_ENTRIES):

js
const LEGACY_SECTION_ENTRIES = {
	"ui-developer-tools": "ui-settings",
	"ui-onboarding": "ui-settings-general",
	shell: process.platform === "win32" ? "pwsh-sandbox" : "bash-sandbox"
};

导入时是逐字兜底的(const ns = LEGACY_SECTION_ENTRIES[section] ?? section;),所以表外节名(例如 agent-presets、subagent-model-selection、dsh-better-sidebar)会以原样去查并抛错。代码并不做名称到 id 的转换——这恰恰是 B 类失败成立的原因。报告者特别指出:这也意味着产品自身改个 entry id,就会让上一版导入成功的节在下一版不再匹配,B 是自我累积的(#7814)。

C 的关键细节:SettingsForms.write() 在调用 edit() 之前就要求「该条目至少声明一个 volatile() 字段」,而 volatile() 只有 @deepseek-ai/schemastery 的 3.18.4 及以后版本才有(原版 schemastery@3.18.0 里这个词出现 0 次)。于是任何使用原版 schemastery 的第三方插件,它的配置节永远无法被迁移——静默跳过、无任何提示、也没有替代入口,卡在「看得见、迁不动」(#7814)。

机制二:模块级 bootstrapIncludes 被拆成两个实例

一句话:app-boot 把「根 Include 条目」存在模块作用域的 WeakMap 里,而 ESM 按解析后的 URL 字符串缓存模块;只要同一个文件出现两种路径拼写,写入方和读取方就活在不同的实例里。

盘符大小写如何造出两个实例

Windows 上,如果打包 CLI 的启动路径盘符是小写(PATH 项 d:\Users\<user>\Application Data\npm,由 npm 生成的 dsh.ps1 启动 dsh web),设置面板里的任何改动都会被 Host 拒绝并静默回退(#7675):

  1. mountRootInclude() 把根 Include 条目通过模块级状态(bootstrapIncludes,一个模块级 WeakMap)交给进程内其它消费者,因此这次交接只在运行它的那个模块实例内可见:
    ts
    const bootstrapIncludes = new WeakMap<Context, Entry>()                       // :255
    const entry = bootstrapIncludes.get(ctx)                                      // :274  ← 读取方 ①
    if (entry === undefined) throw new Error(`${binName}: profile reload requires the root Include entry`)   // :275
    bootstrapIncludes.set(ctx, entry)                                             // :581
    
  2. 运行时解析表用 realpath 规范化了每个包目录(realModuleDirectory()),installRuntimeInterception() 让每一次插件导入都从这些规范化路径解析;
  3. Windows 上 realpath 返回的盘符是大写,而启动器自身的模块 URL 保留进程启动时的拼写——npm 生成的 shim 执行 node "$basedir/node_modules/@deepseek-ai/dsh/lib/bin.js",其中 $basedir 直接来自 PATH 项;
  4. ESM 按解析后的 URL 字符串做模块缓存,于是 file:///d:/…/dsh-app-boot/lib/index.js 与 file:///D:/…/dsh-app-boot/lib/index.js 是同一个文件的两个模块实例。启动器把 Include 记在小写实例里;而 dsh-config-editor、dsh-hmr、dsh-plugin-manager 经拦截(大写)导入,从大写实例调用 reconcileProfilePatches(ctx.root, …),那张表是空的,于是命中 :274-275 的抛错(#7675)。

只有打包安装形态能复现:同一份 profile 用源码方式启动(node --import tsx/esm apps/cli/src/bin.ts web)完全正常,因为那时所有模块 URL 拼写一致——「源码正常、打包异常」正是两个模块实例的指纹(#7675)。

不止盘符:双份实体目录同样中招

同一根因在 Linux/Docker 上有另一条触发路径(#7675):CLI 树在 /npm-global/...(全局 npm),profile 树在 /dsh-home/profiles/web/node_modules(pnpm,nodeLinker: hoisted),两边各有一份 @deepseek-ai/dsh-app-boot 的实体目录——不是软链,inode 不同,但内容字节完全相同。

⚠️ 这一点很坑:因为字节相同,按 md5 或版本号比对会把两份误判成「同一份」。模块身份不能靠「路径拼写」或「内容相等」来判断。

影响面:两条读取路径,两种症状

bootstrapIncludes 有两处读取方,这才是症状不一致的原因(#7675):

ts
const entry = bootstrapIncludes.get(ctx)                     // :274  读取方 ①(reconcile)
...
const required = new Set(failures.filter(({ entry }) => entry === bootstrapIncludes.get(ctx)   // :929  读取方 ②(audit)
  || requiredStartupEntryIds.has(entry.options.id)).map(({ entry }) => entry))
if (required.size > 0) throw new StartupError(...)           // :932  致命
  • 读取方 ① 抛的错若被上层吞掉/降级 ⇒ 表现为设置静默回退(原帖症状);
  • 读取方 ② 把「bootstrap Include 未激活」算进必需项 ⇒ 启动直接致命。

也就是说:同一份坏布局,有人看到「改了没反应」,有人看到「起不来」。 写入侧则覆盖 6 处调用 / 4 个模块(#7675):

#位置场景
1-3dsh-config-editor/lib/index.js:69 / :119 / :122设置写入、以及写失败回滚时的 reconcile
4dsh-plugin-manager/lib/index.js:2033插件安装/卸载后的 reconcile
5dsh-plugin-manager/lib/types/index.js:812同上(随包发布)
6dsh-hmr/lib/index.js:370热重载

判别与绕过

判别(不用起服务、不用 profile):把同一个 dsh-app-boot/lib/index.js 分别以大写盘符与小写盘符两种 URL 导入,会得到两个互不相等的模块实例,导出的 reconcileProfilePatches 也不是同一个函数;用一个实例 boot 出最小 include 树、再用另一个实例对该 ctx 调用 reconcileProfilePatches,即可复现同一句报错(#7675)。

就地绕过(任选其一)(#7675):

  1. 用大写盘符的完整路径直接启动 lib/bin.js(绕开 shim 的拼写):
    powershell
    node "D:\Users\<user>\AppData\Roaming\npm\node_modules\@deepseek-ai\dsh\lib\bin.js" web
    
  2. 或把 PATH 项的大小写改对:d:\...\npm → D:\...\npm,让 shim 传出大写路径。
  3. 或直接编辑 ~/.dsh/profiles/<profile>/cordis.patch.yml 并重启,让某个偏好生效。

上游修法方向:把这次交接移出模块作用域——在 boot 时把根 Include 条目挂到 boot context 上(或挂到已经共享的 profileContext 服务上),reconcileProfilePatches() 与 auditStartupEntries() 先读它、再回落到现有 WeakMap;或者用 Symbol.for("…bootstrapIncludes") 做进程级注册表。「把状态放在模块作用域上」,本来就会在「同一模块被解析成两个 URL」时失效,而 Windows 的盘符大小写恰好能造出两个 URL。(同一文件 :607 的 assembledActivationRejections 是同类跨实例模块状态,建议一并迁移。)

恢复与修复:先救数据,再选改法

记住第一原则:数据几乎都在 .imported 里,先别急着重装或删 .dsh。

第一步(通用):手工恢复 .imported

  1. 找到文件:~/.dsh/settings.yaml.imported(Windows 为 %USERPROFILE%\.dsh\settings.yaml.imported)。
  2. 打开它,看清有哪些 section 与值。
  3. 按当前 profile 的条目形态,把每个可恢复的 section 写成 cordis.patch.yml 的覆盖行(- id: <entry id> 加 config: …),并对照 0.1.7 的 Config schema 校验:
yaml
# ~/.dsh/profiles/<profile>/cordis.patch.yml
- id: llm-pi-ai
  config:
    # 从 settings.yaml.imported 里对应 section 的字段逐项搬过来
- id: ui-theme
  config:
    preference: dark
  1. 重启 DSH,逐项确认设置页已恢复。

真实的恢复经验(#7534):10 个 section 里大部分都能这样恢复;provider-quotas 无需恢复,因为该插件现在自持存储;而 glance-theme(插件仍在调用已被移除的 ctx.settings.register)与 dsh-better-sidebar(已不再挂载)无法用配置恢复,属于第三方插件层面的工作。另一份报告(#7814)也确认:.imported 里原样保留,直接按目标形态写回 cordis.patch.yml 即可一次恢复成功。

第二步:按失败模式对症下药

模式你的动作
A(双实例)先按上一节的「判别与绕过」消除双实例:改 PATH 盘符大小写、用大写完整路径启动,或在 Docker/pnpm 双树下把 profile 那份实体目录换成指向 CLI 副本的符号链接(两处 realpath 收敛到同一 URL,Node 按 URL 缓存、只剩一个实例)。
B(旧节名 ≠ entry id)手工把节名换成真实 entry id 再写进 cordis.patch.yml(例如 agent-presets 的真实 id 是 agent-preset-registry、subagent-model-selection 对应 subagent-model-selection-settings、dsh-better-sidebar 对应 better-sidebar)。
C(无 volatile())目前没有兜底通道。若你是第三方插件作者,请把配置项标注为 volatile()(需要 @deepseek-ai/schemastery>=3.18.4);否则只能等上游提供「不经 volatile 直接写 patch 文档」的通道或 CLI 子命令。

第三步:上游补丁(社区,非官方)

社区针对 importLegacyDocument 给出了补丁,思路是「就绪门控 + 不消耗写不进的文档」(#7534):

  1. 就绪门控:一次性导入等待启动器提交的 appReady——其契约已写明「失败或被外部终止的启动绝不会调用它」(cmdline/src/index.ts:45);appReady.commit() 位于 apps/cli/src/profile-boot.ts:311-315,在 boot() 返回之后,所以审计拒绝时会被跳过,文档因此留给下一次启动。
  2. 读前不改名:先读取文档、并要求所属 fiber 处于 active 才消耗它——「读取在途时服务被 dispose」的启动什么都不导入,而不是把唯一副本改名。
  3. 诚实的收尾日志:报告恢复了几段、拒绝了几段,而不是在一次什么都没写的流程后打一行成功。

补丁作者的验证:pnpm exec vitest run packages/settings/settings packages/api/settings-controller 62 passed、src/index.ts 语句/分支/函数/行 100% 覆盖;两条就绪测试在未打补丁的源码上都失败(证明文档确实被消耗);tsc -b、oxlint、文档预算/导出 JSDoc/翻译配对检查均干净。

社区复核指出的两处残余(合并前值得一并解决,#7534):

  1. fallback 分支仍会丢数据:补丁在「宿主不提供 appReady」时回落到 await ctx.root.loader.await(),也就是原样的危险时序。这在树内有实例——webworker-runtime/src/worker-host.ts:242-245 调用 provideCmdline(hostCtx, { args, exit }) 时不传 ready;而 boot/hmr/src/index.ts:208-209 对同一问题选择了相反做法:if (ready === undefined) throw new Error('Profile HMR requires application readiness')——拒绝运行,而不是悄悄退回不安全时序。要么对齐 HMR(拒绝或至少警告一次),要么在 worker-host.ts 补上 ready。
  2. rename 仍不是后置条件:补丁里活体检查(assertActive())在 rename 之前只跑一次,循环中不再复查;一次恰好落在「rename 之后、最后一次写入之前」的 dispose,仍会让每段以 rejection 被吞、文档已被消耗。建议把 rename 移到循环之后(flush-to-imported,而不是 rename-then-import):因为 update() 走的是 mergeLayers(:347-350),重复应用同一批 section 是幂等的,于是失败方向从「丢失」变成「重试」,代价只是崩溃时两份文件同时存在到下一次对账。另外 restored === 0 && rejected > 0 目前仍打 info,这是唯一需要操作者介入的情形,建议改为 warn。

预防在 core,恢复可挂载(#7534):导入调度写在 SettingsForms 构造函数里,插件无法拦截,所以预防必须上游做;但恢复是可挂载的——ctx.settings 是公开服务,其写入路径(update(ns, patch) / replace / mutate)正是失败导入调用的同一条,配合文档路径 <profile.home>/settings.yaml.imported,插件可以按受支持 API 重新应用被吞掉的文档(唯一设计问题是何时重应用——显式调用才诚实,否则会覆盖用户后续的编辑)。唯一小障碍是 LEGACY_SECTION_ENTRIES 是模块私有的,树外恢复插件需要复制那三个已文档化的别名。

排查注意事项

这类问题的第一原则是「先看文件在不在,再决定动不动代码」——数据绝大多数时候没丢。 八条要点:

  1. 先查 .imported:~/.dsh/settings.yaml.imported 存在,就说明一次性迁移来过且可能失败;里面就是你的旧值(#7814)。
  2. 看启动日志目录:~/.dsh/logs/startup-*.log,找 was not imported、cannot get required service "configEditor" in inactive context、profile reload requires the root Include entry 三类串,分别对应迁移失败、context 已释放、模块双实例(#7534)。
  3. 分清「读坏了」还是「写不进」:直接调 Host 的 settings RPC,若 describe 的存储层 user 已是新值、运行时 value 仍是旧值、mutate 返回 settings/rejected,则失败在「把补丁应用进 Loader」,即双实例;文件层与运行时层的这种长期不一致就是指纹(#7675)。
  4. 「源码正常、打包异常」直接指向双实例:单实例不可能有这种差异(#7675)。
  5. 别用 md5 / 版本号判断是不是同一个模块:Docker/pnpm 双树下两份实体目录字节完全相同、inode 不同,比对内容会误判成同一份(#7675)。
  6. 同一坏布局会呈现两种症状:读取方 ①(reconcile)被降级 ⇒ 静默回退;读取方 ②(audit)计入必需项 ⇒ 启动致命。问「你是改了没反应还是起不来」能快速分类(#7675)。
  7. 迁移失败不是环境特有的:macOS(global npm)、Windows(源码启动)、Linux 均有独立复现,0.1.7-rc.2 与 rc.1 该函数逐字相同(#7814)。
  8. 跨版本抄配置时警惕 YAML 1.1 的 off:YAML 1.1 会把裸键 off 解析成布尔 false,再序列化就写成 false:;某些插件(如 llm-pi-ai 的 resolveModelReasoning)只遍历合法档位取值,多出来的 "false" 键既不报错也不告警、直接被忽略,于是模型的「关闭思考」档凭空消失。用 dump-config 验证:键正确时输出带引号显示 'off': none,被污染时显示 false: none(#7534)。

排查这类问题时,用 DSH Plugin Hub 的已安装列表和设置页先确认 DSH 侧版本与插件状态、在系统日志页把诊断导出,能先把「插件不兼容」这一层排除,再决定是恢复 .imported 还是处理模块双实例。

DSH Plugin Hub · 设置

来源:Discussion #7534、Discussion #7675、Discussion #7814。

常见问题

DSH 升级后设置页变成默认值、模型和 provider 全没了,配置是彻底丢了吗?

基本都没丢。旧值仍在 ~/.dsh/settings.yaml.imported 里——迁移逻辑是先把 settings.yaml 改名成 .imported,再逐段写入 profile,写入失败时文件已经被改名、且没有任何代码路径再去读它,所以你只是看到「没人消费它」。可以把 .imported 里的段按当前 profile 的条目形态写回 cordis.patch.yml(每个段一行 - id: <entry id> 加 config: …),一次即可恢复。

为什么我在设置面板里改任何选项都立刻弹回,而且完全没有报错?

这是另一条根因:dsh-app-boot 在同一个进程里被加载成了两个模块实例。根 Include 条目存在模块级 WeakMap 里,写入发生在实例 A、读取发生在实例 B,于是 reconcileProfilePatches() 抛 dsh: profile reload requires the root Include entry,Host 拒绝写入、浏览器把拒绝当成可恢复的重读、回退到已存值。触发方式在 Windows 上最常见的是 PATH 项盘符小写(d:\...\npm),任何路径拼写差异(junction、软链、双份实体目录)都会造成同类分裂。

怎么判断是「迁移一次性丢失」还是「设置改动被静默回退」?

看两处。第一,检查 ~/.dsh/settings.yaml.imported 是否存在——存在说明一次性迁移来过。第二,看失败发生在读取还是写入:直接调用 Host 自己的 settings RPC,如果 describe 返回的存储层 user 已是新值、而运行时 value 仍是旧值,mutate 返回 settings/rejected,说明补丁文件被读到了,坏在「把补丁应用进 Loader」这一步,也就是模块双实例;如果设置页整片空白、日志里是 was not imported,那就是迁移失败。

同一份坏布局,为什么有人看到「改了没反应」,有人看到「直接起不来」?

因为读那个模块级 WeakMap 的地方有两处:reconcileProfilePatches()(读不到就抛,通常被上层降级 ⇒ 表现为设置静默回退)和 auditStartupEntries()(把「bootstrap Include 未激活」算进必需项 ⇒ 直接抛 StartupError,启动致命)。同一根因因此呈现两种完全不同的症状。

官方修了吗?我该打补丁还是手工恢复?

截至本文所引报告,两条修复都还在讨论阶段。社区已给出可用的补丁(把一次性导入挂到启动就绪信号 appReady 而非 loader.await(),并让「读到了但写不进」不消耗文档),也有人指出补丁的 fallback 分支与「rename 仍是单瞬间检查」两处残余。在你这条版本线上,最稳的当下做法是手工恢复 .imported 并直接编辑 cordis.patch.yml;若是双实例导致的写入被拒,用大写盘符的完整路径启动、或修正 PATH 项大小写即可立刻绕过。

相关术语

settings.yaml.imported
0.1.7 起,旧版留在 harness home 的 `settings.yaml` 会在一次性导入时被改名成 `settings.yaml.imported`。改名发生在第一次写入**之前**,用于防止部分导入被重复执行;代价是一旦写入失败,文档既不会重试、也没有代码路径再消费它,旧值就只剩在这个文件里。— https://github.com/deepseek-ai/deepseek-harness/discussions/7534
提交点倒置(原子性倒置)
指一次性迁移把「提交点」放在所有写入之前:先 rename 再逐段 update。正常语义应是「全部写入成功后才改名」,失败时可重试;倒置后,任何一段写入失败都会让整份文档被标记为已消费,因此无法续传、也无法重试。— https://github.com/deepseek-ai/deepseek-harness/discussions/7814
bootstrapIncludes
`dsh-app-boot` 里保存「根 Include 条目」的模块级 `WeakMap`(键是 Context、值是 Entry)。因为它在模块作用域,同一个包被解析成两个模块实例时,写入方与读取方各持一份,读取方永远拿到 `undefined`,于是 `reconcileProfilePatches()` 抛出 `profile reload requires the root Include entry`。— https://github.com/deepseek-ai/deepseek-harness/discussions/7675
appReady(启动就绪信号)
启动器在启动审计通过后提交的就绪信号,其契约明确「失败或被外部终止的启动绝不会调用它」。社区补丁用它替代 `loader.await()` 作为一次性迁移的触发点:未提交就绪的启动因此不会消耗 `settings.yaml`,把它留给下一次启动。— https://github.com/deepseek-ai/deepseek-harness/discussions/7534

来源