# 防御性模式

[English](defensive-patterns.md) | 中文

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

## 正交结果独立上报

一个结果可以同时具有多重性质：进程可能既超时又以 exit 0 退出，因为它捕获了信号。每个独立事实（`timedOut`、`signal`、`exitCode`）都应独立暴露；切勿将某个 flag 的上报嵌套在另一个 flag 的分支内，否则调用方会把一次被截断的运行误读为正常成功。

## 跨 seam 契约两侧都要遵守

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

## 异步状态不是同步状态

`agent.followup()` 不会在返回前翻转状态；后台任务的完成与轮次边界存在竞争；`reader.close()` 在 EOF 和 dispose（资源释放）两种情况下都会触发。切勿基于一个刚刚请求的状态来控制流程——应以实际触发的事件/promise（`agent/status`、`task.done`）驱动生命周期，并观察状态转换（先看到 `running` 再看到 `idle`），而不是把状态当作逐次 `followup()` 的结果：多次排队的 `followup()` 会在同一个 `running` 区间内连续运行多个轮次，而取消或资源释放可能丢弃尚未启动的项。这条守则是双向的：如果等待的转换永远不会发生（EOF 时没有提交过任何工作 → 永远不会进入 `running`），等待就会挂起——请显式处理「无需等待」的分支。

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

一个清理流程如果发出 kill/abort 后就返回、而不等待工作实际停止，就会留下孤儿进程。请让清理逻辑异步化并 await 子进程退出（kill → await `done`），并在 kill 之前关闭监听器/通知注册表，使迟到的完成事件保持静默。测试应证明 dispose 确实等待了（`await fiber.dispose()` 之后 pid 已不存在），而不仅仅是进程最终会死。

## 在边界处包容回调异常

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

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

spawn 的命令应获得一份经过清洗的 env（去除 `*KEY*`/`*SECRET*`/`*TOKEN*`/`*PASSWORD*`），使 harness 凭证无法泄漏到输出、`env` 或溢出文件中。临时/溢出文件应使用私有（0700）目录、随机文件名和排他的仅所有者可访问打开方式（`'wx'`、`0o600`）——可预测的全局可读路径会招致符号链接竞争和信息泄露。
