# 失败日志 · Failure Journal **同一个错误犯五次,是一行记录 —— 在它滚出上下文之前就已经落盘。** 一个 DeepSeek Harness 宿主插件:把每一次异常退出的工具调用追加到磁盘上的 JSONL 日志,把近似相同的失败折叠成带计数的签名,并通过一个工具 `failure_journal` 交还给 agent。 [![License: MIT](https://img.shields.io/badge/license-MIT-3DA639.svg)](LICENSE) [![DeepSeek Harness plugin](https://img.shields.io/badge/DeepSeek%20Harness-tool%20plugin-4D6BFE.svg)](#install) [![version](https://img.shields.io/github/package-json/v/catsenior507/dsh-tool-failure-journal?color=4D6BFE)](package.json) [![node](https://img.shields.io/badge/node-%3E%3D20-3DA639.svg)](package.json) [![stars](https://img.shields.io/github/stars/catsenior507/dsh-tool-failure-journal?color=4D6BFE)](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