# 防御性模式

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

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

## 正交结果独立上报

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

## 公共约定两侧都要遵守

当一个实现收到同一结果的多种表示时，应在通过公共 API 返回前将其规范化。`LlmAdapter.stream()` 的实现可以抛出异常或发出 `finish {kind:'error'|'aborted'}`，但 `LlmRuntime.stream()` 只会通过终止型 finish 分片暴露模型请求失败；middleware 缺陷与消费方缺陷仍会以异常形式抛出。这使消费方不必猜测捕获的异常究竟来自提供方、包装层、分片日志记录还是自身组装逻辑。请在类型定义处记录规范化后的约定；通过真实消费方覆盖每种来源形式。

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

`agent.followup()` 没有逐消息的完成状态或结果；后台任务的完成与轮次边界存在竞争；`reader.close()` 在 EOF 和 dispose（资源释放）两种情况下都会触发。切勿把 `agent/status` 或 `whenIdle()` 当作某次 `followup()` 的结果：多条已排队的后续消息、steering（中途引导）和注入工作可能共用同一个 `running` 区间，而取消或资源释放可能丢弃尚未启动的项。真正拥有一次运行的自动化调用方必须显式定义其区间——例如从消息的持久 inbox 回执到整个 agent（智能体）下一次进入 `idle`——并将选取的任何输出描述为整个区间的输出，而不是把因果关系归于该消息。这条守则是双向的：如果等待的转换永远不会发生，等待就会挂起，因此应显式处理「无需等待」的分支。

<a id="dispose-must-reach-quiescence-not-just-request-it"></a>

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

如果清理流程只发出终止或中止信号便返回，而不等待工作真正停止，就会留下孤儿进程。清理逻辑应采用异步流程，并等待子进程退出（发出终止信号后等待 `done`）；还应在终止进程前关闭监听器注册表和通知注册表，使迟到的完成事件保持静默。

## 在分发器中隔离回调异常

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

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

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

## 用 unlink 删除链接形态的路径

可能是符号链接或 Windows junction 的路径，应先用 `lstatSync().isSymbolicLink()` 判断，再用 `unlinkSync` 删除：unlink 只删除链接本身并拒绝真实目录，因此绝不会跟随链接进入其目标。Windows 上对 junction 调用 `rmSync(link)` 会抛 `ERR_FS_EISDIR`；递归删除可能穿过 junction 进入其目标。真实目录才使用带 `recursive` 的 `rmSync`。
