# 失败日志 · Failure Journal
**同一个错误犯五次,是一行记录 —— 在它滚出上下文之前就已经落盘。**
一个 DeepSeek Harness 宿主插件:把每一次异常退出的工具调用追加到磁盘上的
JSONL 日志,把近似相同的失败折叠成带计数的签名,并通过一个工具
`failure_journal` 交还给 agent。
[](LICENSE)
[](#install)
[](package.json)
[](package.json)
[](https://github.com/catsenior507/dsh-tool-failure-journal/stargazers)
[English](README.md) · **简体中文**
---
## 要解决的问题
编程 agent 不只是会出错,它会**以同样的方式**反复出错,而证据在任何人能看出模式之前
就已经离开了上下文窗口:工具结果被折叠、被压缩,或者干脆被后面四次尝试埋掉。
宿主确实有一份会话日志 —— 但会话日志**就是**模型的上下文。它会被重写、折叠、裁剪。
它不可能是失败历史该待的地方。
所以这个插件维护了第二份记录,在上下文窗口之外,用一种比进程活得更久的格式。
## 它捕获什么
只挂一个 `tools/result` 监听器 —— 那是宿主对一次调用的最终通知,在
pre-policy、guards、工具本体、post-policy、输出校验**全部走完之后**才发出。
这一个钩子就覆盖了:
- 工具抛异常
- 工具返回值违反了它自己声明的 output schema
- 工具名不存在
- 分发前就被拒绝的调用
- 被调用方取消的调用
**一个钩子顶四个**,而且不需要轮询。
每条记录自带上下文,因为一条记录必须在它所属的会话消失之后依然有用:
```json
{"v":1,"at":"2026-09-11T00:31:07.412Z","sessionId":"session-c9a2…","callId":"call_00_…",
"turn":12,"step":3,"tool":"pwsh","aborted":false,"errorCode":"COMMAND_NOT_FOUND",
"message":"'lake' is not recognized as an error…","messageHead":"…","argsChars":41,
"args":"{\"command\":\"lake build\"}","content":"…","signature":"9f2c1ab73e04",
"tag":"failure","recurrence":3,"recurring":true,"firstSeenAt":"2026-09-11T00:28:51.003Z",
"remediation":["The binary is not on PATH for this shell; …"]}
```
## 它为什么不止是一份日志
**签名(signature)。** 每个失败都会被哈希成一个稳定 id,由工具名、错误码和一条
**归一化**后的消息组成 —— 路径、时间戳、UUID、长数字都被替换成占位符。
四十次近似相同的失败折叠成一行带计数,这才是人能据以行动的单位。
**`regression: true`。** 一个在本次会话中**先成功过、之后又失败**的工具会被标记。
这是日志里最强的信号:本来能用的东西坏了,它指向期间发生的任何改动。
**`aborted` 不算 `failure`。** 被撤回的调用单独打标,永远不计为缺陷。把取消算作失败
会污染复发统计 —— 而复发统计正是这份日志值得读的原因。
**内置修复提示。** 对成因明确的那几类 —— `EDIT_NO_MATCH`、`EDIT_NOT_UNIQUE`、
`COMMAND_NOT_FOUND`、`TIMEOUT`、`PERMISSION_DENIED`、`SYNTAX`、`BAD_ARGS` ——
记录里直接带上成因,以及**与刚才那次不同**的下一步动作。
## 工具
| action | 回答什么问题 |
| --- | --- |
| `stats` | 什么在反复坏,按签名折叠。**先看这个。** |
| `list` | 最近失败了什么,最新在前 |
| `show` | 某个签名的全部出现记录(支持前缀) |
| `sessions` | 磁盘上有哪些日志文件、多大 |
| `clear` | 归档当前页,重新开始 |
| `selftest` | 证明观察器已挂载、目录可写 |
`stats` 是真正改变行为的那个。"同一个错误犯了五次"以
`count=5, recurring=true` 一行呈现,和五段独立的堆栈,对读者来说是两条不同的指令。
## 安装
本插件和其他所有 dsh 插件一样,作为包安装进一个 dsh **profile**。
`dsh plugin` 会在 profile 目录里转发给 `pnpm`,所以 pnpm 接受的任何 spec 都可以用。
```bash
# 从 GitHub 安装(这是公开发布形式)
dsh plugin --profile web add github:catsenior507/dsh-tool-failure-journal
# 本地检出安装(开发时用)
dsh plugin --profile web add /absolute/path/to/dsh-tool-failure-journal
```
`web` 是自带 GUI 的 profile;可以换成 `headless`、`sdk`、`acp` 或你自己的 profile 名。
Windows 上路径请用正斜杠,或给路径加引号。
装完**重启宿主**让 profile 重新组装,然后用这条确认:
```
failure_journal action=selftest
```
`selftest` 会报告观察器是否已挂载,并往日志目录写一个探测文件以证明可写。
### 安装**不会**做的事
- **没有构建步骤。** 发布的 JavaScript **就是**源码 —— 没有 `dist`,没有打包器,
也没有 `prepare` 脚本,所以安装时不会执行任何东西。
- **没有依赖。** `dependencies` 和 `peerDependencies` 都是空的;插件只需要它被加载进的
那个宿主。cordis 由宿主在运行时提供。
- **没有原生代码、没有编译器、运行时不需要网络。**
需要 Node.js 20 或更新版本 —— 因为宿主本身就有这个要求。
## 写到哪里
`/failure-journal/sessions/.jsonl`,每个会话一个文件,
按大小轮转(`maxBytes` 默认 4 MiB,保留 `maxRotated` 代)。
轮转后的旧文件是**保留而不是删除**:一个会话把同一个错误循环五十次,
正是事后值得读的情形,而在这个文件刚变得有意思的时刻把它截断就本末倒置了。
## 配置
`dsh plugin add` 已经替你插入了插件行。要改默认值,编辑 profile 的
`cordis.patch.yml` 里那一行的 `config`:
```yaml
- insert:
- id: tool-failure-journal
name: '@dsh-external/dsh-tool-failure-journal'
config:
maxBytes: 4194304
clusterThreshold: 3
excludeTools: ['todo_write']
recordSuccesses: false
exposeTool: true
```
`recordSuccesses: true` 会把成功的调用也记下来 —— 用于回答"这条命令到底干了什么"。
注意:用于 `regression` 信号的**成功追踪**无论如何都在跑,这个开关只决定什么写进磁盘。
## 值得明说的设计约束
- **观察器会在进程里每一次工具调用上运行**,所以它绝不能抛异常、绝不能阻塞。
每个派生字段都通过全函数访问器读取,每次写入都是同步的单行 append —— 微秒级,
而且能在进程于回合中途被杀时保住记录。异步队列会恰好丢掉这个插件存在的理由所在的记录。
- **存储失败会被计数,而不是被隐藏。** `selftest` 报告写入、丢弃、轮转的条数,
所以一份停止工作的日志会自己说出来。
- **这个插件永远不会让一次工具调用失败。** 写入发生在宿主会兜住异常的监听器里;
日志无法破坏它所观察的工具。
## 开发
```bash
npm test # 31 个测试,含真实 cordis 挂载与真实事件
```
`npm test` 需要 `@deepseek-ai/cordis` 可解析 —— 它随 dsh 安装而来。如果你是从检出目录
而不是已安装的包运行,把 `node_modules` 指向 profile 里的那份;Windows 上目录 junction 即可。
挂载测试在**真实的 cordis `Context`** 上注册一个假的 `tools` 服务,发出**真实的
`tools/result` 事件**,再通过工具把日志文件读回来 —— 因为"`inject` 声明写错了导致
观察器根本没挂上"这一类故障,恰恰是手搓假 context 抓不到的。
| 文件 | 职责 |
| --- | --- |
| `lib/index.js` | cordis 插件:解析配置、挂载、注册 |
| `lib/observer.js` | `tools/result` 与 `session/event` 监听器 |
| `lib/store.js` | JSONL 存储、轮转、归档 |
| `lib/signature.js` | 失败身份、归一化、修复提示目录 |
| `lib/tool.js` | `failure_journal` 工具 |
## 许可
MIT