Files
deepseek-harness/docs/defensive-patterns.zh.md
T

3.6 KiB

防御性模式

English | 中文

来之不易的缺陷类别规则:下面每条模式都是本项目实际发布或差点发布的一类缺陷,以防止其复发的规则形式陈述。在编写生命周期、并发、子进程或清理代码之前请先阅读本文。测试层面的对应规则(真实入口路径、验证实际结果、资源归属)见 testing.md

正交结果独立上报

一个结果可以同时具有多种性质:进程可能已经超时,却仍以退出码 0 结束,因为它捕获了终止信号。每个独立事实(timedOutsignalexitCode)都应单独上报;切勿把一个标志的上报嵌套在另一个标志的分支中,否则调用方可能把提前终止的运行误判为正常成功。

跨 seam 契约两侧都要遵守

当接口文档规定两种合法的信号方式时,消费方必须同时处理两条路径,而不能只处理第一个实现恰好使用的路径。例如,适配器既可以从 stream() 抛出异常来报告失败,也可以发送 finish {kind:'error'|'aborted'} 分片来结束流。基于依赖库实现的适配器可能无法在流中途抛出异常,只能使用带内路径;如果 agent loop(智能体循环)只捕获抛出的异常,就会把提供方的 401 错误误判为正常完成的轮次。请在类型定义处记录完整契约,并通过真实消费方测试每个分支。

异步状态不是同步状态

agent.followup() 不会在返回前改变状态;后台任务完成可能与轮次边界发生竞态;reader.close() 在 EOF 和 dispose(资源释放)时都会触发。不要根据刚刚请求的状态变化来控制流程,而应让实际触发的事件或 Promise(agent/statustask.done)驱动生命周期,并观察真实状态转换,例如先观察到 running,再观察到 idle。不要把状态当作每次 followup() 的结果:多个已排队的 followup() 可以在同一个 running 区间内连续执行多个轮次,而取消或资源释放可能丢弃尚未开始的项。反过来,如果等待的转换根本不会发生,例如 EOF 时从未提交工作,因而系统永远不会进入 running,等待就会无限期挂起;必须显式处理「无需等待」的分支。

Dispose 必须达到完全停稳,而不仅仅是请求停止

如果清理流程只发出终止或中止信号便返回,而不等待工作真正停止,就会留下孤儿进程。清理逻辑应采用异步流程,并等待子进程退出(发出终止信号后等待 done);还应在终止进程前关闭监听器和通知注册表,使迟到的完成事件保持静默。测试必须证明 dispose 确实等待了,例如 await fiber.dispose() 返回后进程 ID 已不存在,而不能只证明该进程最终会退出。

在边界处隔离回调异常

用户提供的监听器如果抛出异常,不得导致它所在的 promise 被 reject,也不得饿死排在它后面的监听器。请用 try/catch 包裹分发循环并记录日志;一个行为不当的订阅者绝不能破坏核心生命周期。

绝不将环境变量或可预测路径暴露给不可信输出

启动的命令应使用经过清理的环境变量,移除名称匹配 *KEY**SECRET**TOKEN**PASSWORD* 的项,防止 harness 凭证通过命令输出、env 或 spill 文件泄漏。临时文件和 spill 文件应放在权限为 0700 的私有目录中,使用随机文件名,并以独占且仅所有者可访问的方式打开('wx'0o600);可预测且全局可读的路径会引发符号链接竞态和信息泄露。