DeepSeek Harness 升级后设置静默丢失: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 context | settings/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):
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):
@deepseek-ai/dsh-app-boot的启动审计发现有必需条目未激活(本例是 stale 的web-app前端 dist 与client-modulesbundle),抛StartupError并await ctx.fiber.dispose()释放整棵树;- 而一次性导入与那次释放是同一个 loader settled 时刻的延续(
app-boot/src/index.ts:971-977先await ctx.get('loader')?.await(),再审计、再 dispose); - 于是 10 个 section 全部被写进了已经 dispose 的 context:每次写入都要解析
this.ownerContext.configEditor(index.ts:382),此时得到cannot get required service "configEditor" in inactive context; - 每段的
try/catch把致命错误降级成一条 warning 并继续循环——「服务已死」和「该段被当前组合拒绝」在这里不可区分(index.ts:248-256); - 文档在第一次写入前就改名为
.imported(index.ts:245-246),而后续启动因为settings.yaml不存在直接 return(index.ts:244);this.closed早已存在(index.ts:227)却从未被importLegacyDocument检查;循环结束后index.ts:257还无条件打一行settings: imported …——一次什么都没导入的启动,也可能报告成功。
实测的时间线(同一 10 毫秒窗口内):
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):
| 模式 | 抛错位置 | 触发条件 | 对应修法 |
|---|---|---|---|
| A | profile reload requires the root Include entry(dsh-app-boot) | 同一个包存在两份实例/副本,模块私有 WeakMap 读不到写入 | 消除双实例(见机制二) |
| B | No configurable plugin entry "<ns>"(dsh-settings) | 旧节名 ≠ 真实 entry id | 把旧节名映射到真实 entry id |
| C | has no volatile fields(dsh-settings) | 该条目没有声明任何 volatile() 字段 | 目前无兜底通道 |
B 的关键细节:代码里的 legacy 映射表只有三项(dsh-settings 的 LEGACY_SECTION_ENTRIES):
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):
mountRootInclude()把根 Include 条目通过模块级状态(bootstrapIncludes,一个模块级WeakMap)交给进程内其它消费者,因此这次交接只在运行它的那个模块实例内可见:tsconst 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- 运行时解析表用 realpath 规范化了每个包目录(
realModuleDirectory()),installRuntimeInterception()让每一次插件导入都从这些规范化路径解析; - Windows 上 realpath 返回的盘符是大写,而启动器自身的模块 URL 保留进程启动时的拼写——npm 生成的 shim 执行
node "$basedir/node_modules/@deepseek-ai/dsh/lib/bin.js",其中$basedir直接来自 PATH 项; - 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):
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-3 | dsh-config-editor/lib/index.js:69 / :119 / :122 | 设置写入、以及写失败回滚时的 reconcile |
| 4 | dsh-plugin-manager/lib/index.js:2033 | 插件安装/卸载后的 reconcile |
| 5 | dsh-plugin-manager/lib/types/index.js:812 | 同上(随包发布) |
| 6 | dsh-hmr/lib/index.js:370 | 热重载 |
判别与绕过
判别(不用起服务、不用 profile):把同一个 dsh-app-boot/lib/index.js 分别以大写盘符与小写盘符两种 URL 导入,会得到两个互不相等的模块实例,导出的 reconcileProfilePatches 也不是同一个函数;用一个实例 boot 出最小 include 树、再用另一个实例对该 ctx 调用 reconcileProfilePatches,即可复现同一句报错(#7675)。
就地绕过(任选其一)(#7675):
- 用大写盘符的完整路径直接启动
lib/bin.js(绕开 shim 的拼写):powershellnode "D:\Users\<user>\AppData\Roaming\npm\node_modules\@deepseek-ai\dsh\lib\bin.js" web - 或把 PATH 项的大小写改对:
d:\...\npm→D:\...\npm,让 shim 传出大写路径。 - 或直接编辑
~/.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
- 找到文件:
~/.dsh/settings.yaml.imported(Windows 为%USERPROFILE%\.dsh\settings.yaml.imported)。 - 打开它,看清有哪些 section 与值。
- 按当前 profile 的条目形态,把每个可恢复的 section 写成
cordis.patch.yml的覆盖行(- id: <entry id>加config: …),并对照 0.1.7 的 Config schema 校验:
# ~/.dsh/profiles/<profile>/cordis.patch.yml
- id: llm-pi-ai
config:
# 从 settings.yaml.imported 里对应 section 的字段逐项搬过来
- id: ui-theme
config:
preference: dark
- 重启 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):
- 就绪门控:一次性导入等待启动器提交的
appReady——其契约已写明「失败或被外部终止的启动绝不会调用它」(cmdline/src/index.ts:45);appReady.commit()位于apps/cli/src/profile-boot.ts:311-315,在boot()返回之后,所以审计拒绝时会被跳过,文档因此留给下一次启动。 - 读前不改名:先读取文档、并要求所属 fiber 处于 active 才消耗它——「读取在途时服务被 dispose」的启动什么都不导入,而不是把唯一副本改名。
- 诚实的收尾日志:报告恢复了几段、拒绝了几段,而不是在一次什么都没写的流程后打一行成功。
补丁作者的验证:pnpm exec vitest run packages/settings/settings packages/api/settings-controller 62 passed、src/index.ts 语句/分支/函数/行 100% 覆盖;两条就绪测试在未打补丁的源码上都失败(证明文档确实被消耗);tsc -b、oxlint、文档预算/导出 JSDoc/翻译配对检查均干净。
社区复核指出的两处残余(合并前值得一并解决,#7534):
- 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。 - 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 是模块私有的,树外恢复插件需要复制那三个已文档化的别名。
排查注意事项
这类问题的第一原则是「先看文件在不在,再决定动不动代码」——数据绝大多数时候没丢。 八条要点:
- 先查
.imported:~/.dsh/settings.yaml.imported存在,就说明一次性迁移来过且可能失败;里面就是你的旧值(#7814)。 - 看启动日志目录:
~/.dsh/logs/startup-*.log,找was not imported、cannot get required service "configEditor" in inactive context、profile reload requires the root Include entry三类串,分别对应迁移失败、context 已释放、模块双实例(#7534)。 - 分清「读坏了」还是「写不进」:直接调 Host 的 settings RPC,若
describe的存储层user已是新值、运行时value仍是旧值、mutate返回settings/rejected,则失败在「把补丁应用进 Loader」,即双实例;文件层与运行时层的这种长期不一致就是指纹(#7675)。 - 「源码正常、打包异常」直接指向双实例:单实例不可能有这种差异(#7675)。
- 别用 md5 / 版本号判断是不是同一个模块:Docker/pnpm 双树下两份实体目录字节完全相同、inode 不同,比对内容会误判成同一份(#7675)。
- 同一坏布局会呈现两种症状:读取方 ①(reconcile)被降级 ⇒ 静默回退;读取方 ②(audit)计入必需项 ⇒ 启动致命。问「你是改了没反应还是起不来」能快速分类(#7675)。
- 迁移失败不是环境特有的:macOS(global npm)、Windows(源码启动)、Linux 均有独立复现,
0.1.7-rc.2与 rc.1 该函数逐字相同(#7814)。 - 跨版本抄配置时警惕 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/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
来源
- #7534 — [Bug] 0.1.7-alpha.1 and alpha.2: a failed startup still consumes settings.yaml — legacy sections import into a disposed context and are lost permanently· deepseek-ai(GitHub Discussions)
- #7675 — 【Bug】[Windows] 设置改动被静默回退 —— 启动路径的盘符大小写把 dsh-app-boot 加载成了两个模块实例· deepseek-ai(GitHub Discussions)
- #7814 — settings.yaml 一次性迁移在启动竞态下全数失败且永不重试:4 个配置段静默丢失(0.1.7-rc.1,rc.2 仍在)· deepseek-ai(GitHub Discussions)