Files
deepseek-harness/packages/settings/settings/README.zh.md
Yichen Jiang f44b4db1f2 fix(settings): harden seam and provider per review findings
Confirmed and fixed, each with a regression test that failed first:

- Concurrent update() lost patches (merge over one stale snapshot):
  per-namespace serialized write queues; a failed write cannot poison
  the queue for later writers.
- Fixed-name .tmp write followed planted symlinks and kept stale modes:
  random-suffix sibling, exclusive-create (wx), 0600, cleanup on
  failure, then rename.
- A throwing settings/updated listener escaped commit and permanently
  wedged the provider reload chain (rejected refreshTask): commit now
  contains listener failures (INVARIANT-coded errors still propagate),
  async watcher rejections are adopted and contained
  (watch callbacks are officially void | Promise<void>), and the
  provider chains refreshes on a settled tail with an error log.
- No way to remove a user override: scope/service replace(section)
  sets the user section wholesale; replace({}) re-inherits base and
  schema defaults.
- The three-primitive provider contract did not hold (base never
  called load()): the base Service.init loads and publishes once;
  settings-local delegates via yield* super[Service.init]().
- Dispose did not quiesce: teardown flags closed, closes the watcher,
  then awaits queued/in-flight reloads; closed is re-checked across
  await points.
- Invariant now checks the authoritative relation with the seam's own
  deepEqualJson: emitted next must equal settings.get(ns), and
  next/prev must differ structurally (cosmokit dependency dropped).
- New docs/core-data-structures/settings.{md,zh.md} with type-equiv
  blocks + manifest entries; catalog types moved from exemptions to
  LINK_MAP; website page registered.

Both packages stay at per-file 100% coverage.
2026-07-29 10:19:32 +08:00

3.0 KiB
Raw Blame History

@deepseek-ai/dsh-settings

English | 中文

抽象用户设置 seam(ctx.settings)。一个 provider 持有按 namespace 分节的原始文档;插件注册 namespace schema 并读取分层解析值:schema 默认值,然后注册方的组合 base(其 cordis.yml entry 配置子集),最后用户文档分节。不挂载 provider 时消费者行为不变:仍只按 entry 配置解析,因此任何组合有无 settings 都能工作。

服务 API

  • register(ns, schema, { base?, applies? }) — 返回 owner 的 SettingsScope(get/watch/update)。注册是调用方插件 fiber 上的 effect:dispose 该 fiber 即移除 namespace 及其观察者。schema 拒绝的存量分节会使注册本身失败;重复 namespace 立即报错。
  • describe() — 每个 namespace 一条描述(schema.toJSON() 信封、解析值、applies),供配置界面使用。
  • get(ns) — 解析值;未注册时为 undefined。
  • update(ns, patch) — 把普通对象 patch 深合并进用户分节(绝不合并进 base),校验解析候选值,经 provider 持久化后提交。校验失败在持久化前拒绝;只读 provider(writable: false)拒绝一切写入。同一 namespace 的写入按调用顺序串行。
  • replace(ns, section) — 整体替换用户分节:merge 表达不了的删除/重置路径(replace({}) 重新继承 base 与 schema 默认值)。
  • 解析值是深冻结快照;每次提交后观察者收到 (next, prev);观察者异常——同步抛出与异步拒绝——均被隔离。

Provider 契约

子类实现 writable、load()、persist(ns, section),并通过受保护的 publish(doc) 推入外部观察到的文档。基类 service init 在服务可注入前加载并发布一次文档;自有 init(watcher、连接)的 provider 先经 yield* super[Service.init]() 委托。publish 时每个已注册 namespace 独立重解析:非法分节保留该 namespace 的最后可用值并告警——热重载绝不拖垮进程;启动期与注册期校验则立即报错。

事件

settings/updated (ns, next, prev, source) 在每次提交后触发;source 为 update(进程内写入)或 provider(外部变更)。解析值深相等时绝不触发。

Model Experience

间接生效:消费插件从各自 namespace 解析影响模型的值(例如默认模型路由);效果由各消费者自己的文档描述。

KV Cache effect

无直接失效;把设置值折叠进请求前缀的消费者拥有该变更。

Known Limitations and Deferred Work

  • 单一用户层 — 解析只认识 schema 默认值、一个组合 base 与一个用户文档;尚无 project/managed 分层或按值溯源。
  • 跨进程并发由 provider 定义 — seam 仅在进程内按 namespace 串行化写入;跨进程并发按 provider 行为收敛(本地文件 provider 为后写胜出)。
  • 无 secret 字段脱敏 — describe() 原样返回解析值;wire 面(RPC/UI)在暴露前必须对 role('secret') 字段脱敏。