The seam README states the JSON-shaped write boundary, watch-disposer quiescence, async listener containment, and the drained teardown; the provider README rewrites Behavior around the operation chain, read-modify-write, writer lock, ready reconcile, and leaf-level YAML diffs, and updates Known Limitations to the residual guarantees. A new Agent Note records the round's decisions and supersedes the original note's deferred-lockfile alternative (cross-linked in place). Chinese counterparts updated pair-by-pair (three briefed minimal updates, one whole-document translation); type-equiv, config, cordis, and module-graph catalogs re-recorded.
5.0 KiB
Agent Note:用户设置 seam(ctx.settings)与文件 provider
Status: implemented
English | 中文
范围:
packages/settings/能力族——抽象 seam、文件 provider,以及用户设置与cordis.yml的组合边界。web config-tree note 曾把"profile 写路径"记为延后项;本 seam 就是该写路径的归属。消费者迁移(主题、语言、默认模型路由)与 websettings.*RPC 面是后续工作,不在本 note 已交付范围内。
问题
用户可编辑配置没有归属:dsh web 经静态白名单读 cwd 锚定的 profile json 且无写路径,TUI 读 $DSH_HOME/config.yaml 裸 loader patch,两者都在启动时冻结。个人设置页(web GUI)需要一个跨 surface 的用户层,带 schema 校验、写路径与热传导——同类产品(Codex、Claude Code、Kimi、OpenCode、Pi)也全部收敛于"用户偏好与扩展组合分离"。Loader 的 reactive 配置更新承载不了这件事:fiber.update 原地替换 entry config,构造期读过配置的插件毫无感知,也没有任何回调通知它。
决策
两个面,一条判定。cordis.yml(+ Include patches)仍是组合面:有哪些插件、接线、部署配置,归 orchestrator 所有并随产品升级。settings namespace 只承载用户可编辑子集;判定是"个人配置页应该能改它吗?"值可同时存在于两个面而不歧义,因为分层就是契约:schema 默认值,然后注册方的组合 base(其 entry 配置子集),最后用户文档分节。
镜像 session-persistence/ 的三包 seam。dsh-settings 拥有抽象 Settings 服务:namespace 注册表、分层解析、schema 校验、按 namespace 深相等变更检测,以及 settings/updated 提交事件。provider 只实现 writable/load()/persist(ns, section),并通过受保护的 publish(doc) 推入外部观察到的文档——因此热更新语义对所有 provider 一致,网络配置中心后端(nacos 类,可能只读)只是一个平级包的距离。dsh-settings-local 是文件 provider:resolveSpec 显式默认到 <DSH_HOME>/settings.yaml 的 YAML/JSON、chokidar 监听、跨进程写锁下以 0600 tmp+rename 原子提交的读-改-写 persist、对被写 namespace 的叶子级 diff 修补(未触碰节点的注释得以保留)、按内容相等抑制自写(write-path integrity note)。
注册是调用方 fiber 上的 effect。register() 经服务代理调用,this.ctx 即注册方 context,注册挂在 ctx.effect 上:dispose 注册方即移除 namespace 及其观察者(HMR disposal 测试证明),而用户的分节继续留在存储中等待下一任 owner。
**静止时响亮报错,运行中保留最后可用值。**启动期与注册期校验直接抛错(非法存量分节使注册插件加载失败;存在但不可解析的文档使 provider 加载失败)。运行中坏的外部编辑只告警并按 namespace 保留最后可用状态——热重载绝不拖垮进程。该不对称镜像 Include.refresh() 与 Kimi 的安全运行时重载。
**消费者天然可选。**消费者在 ctx.inject(['settings'], …) 内注册;不挂 provider 时仍只按 entry 配置解析,因此所有既有组合、demo、snapshot 原样工作,迁移按插件渐进。
Alternatives considered
- 以 Include 写回为用户层(cordis-webui 式的按插件配置页写 loader entry 文件):写回目标是按组合的文件,会把用户偏好绑死在某个
cordis.yml上;用户层必须在模板升级中存活,并以同一文档服务 TUI 与 web。 - 以 Loader reactive
fiber.update为传导通道:构造期读取毫无感知;seam 的显式watch()把热更新变成消费者契约而非框架魔法。 - 领域化的 settings 服务(按产品域的 getter):设计评审中的耦合反对成立;服务只做存储、校验、发布——领域含义留给拥有 schema 的注册方。
- 现在就做多层优先级(Codex/Claude Code 式 system/managed/project 层级):延后到真实第二层出现;resolve 步骤是分层未来唯一的扩展点。
- 现在就上跨进程锁(Pi 的 proper-lockfile):最初以"原子替换加 watcher 收敛,真实冲突出现再说"为由延后——评审发现收敛会丢失未观察到的同级 namespace,因此该延后已被 write-path integrity note 的手写写锁取代。
后果
按依赖顺序延后:web settings.raw/settings.describe/settings.update RPC 面(暴露前必须对 role('secret') 字段脱敏);首批消费者迁移(ui-theme、语言、api-gateway 默认路由)并退役 PROFILE_MAPPINGS 与 profile json;面向密钥的 ${env:VAR} 值间接引用;provider 侧分层。keyless snapshot 义务随第一个模型或产品用户可见的消费者落地,而非本基础设施步骤。