Bundle
@dsh-external/dsh-tool-failure-journal
Failure journal: every tool call that exits abnormally is written to a durable JSONL journal on disk, clustered by error signature, and readable back by the agent through a `failure_journal` tool.
- Source
- catsenior507
- License
- MIT
- Updated
- Updated 13 hours ago
Readme
<div align="center">
# Failure Journal
**The same error, five times, is one row — written to disk before it scrolls away.**
A DeepSeek Harness host plugin that appends every tool call that exits abnormally
to a durable JSONL journal, folds near-identical failures into signatures with
counts, and exposes the whole thing to the agent as one tool: `failure_journal`.
[](LICENSE)
[](#install)
[](package.json)
[](package.json)
[](https://github.com/catsenior507/dsh-tool-failure-journal/stargazers)
[English](README.md) · [简体中文](README.zh.md)
</div>
---
## The problem
A coding agent does not only fail. It fails the *same way* over and over, and the
evidence leaves the context window before anyone can see the pattern: the tool
result is folded, compacted, or simply buried under the next four attempts.
The harness keeps a session log — but the session log **is** the model's context.
It is rewritten, folded, and pruned. It cannot be the place a failure history
lives.
So this plugin keeps a second record, outside the context window, in a format that
outlives the process.
## What it captures
One listener on `tools/result`, which is the harness's own final notification for
a call — emitted after pre-policy, guards, the body, post-policy, and output
validation. That single hook covers:
- a tool that threw
- a tool whose return value violated its declared output schema
- an unknown tool name
- a call denied before dispatch
- a call cancelled by the caller
**One hook instead of four**, and no polling.
Each record carries its own context, because a record has to stay useful after the
session it came from is gone:
```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; …"]}
```
## What makes it more than a log
**Signatures.** Every failure is hashed to a stable id built from the tool, the
error code, and a *normalized* message — paths, timestamps, UUIDs, and long
numbers replaced by placeholders. Forty near-identical failures become one row
with a count, which is the unit a reader can act on.
**`regression: true`.** A tool that already *succeeded* earlier in the session and
fails afterwards is marked. That is the strongest signal in the journal: something
that worked has stopped working, and it points at whatever changed in between.
**`aborted` is not `failure`.** A withdrawn call is tagged separately and never
counted as a defect. Counting cancellations as failures would poison the
recurrence numbers that make the journal worth reading.
**Shipped remediation.** For the failure classes whose fix is unambiguous —
`EDIT_NO_MATCH`, `EDIT_NOT_UNIQUE`, `COMMAND_NOT_FOUND`, `TIMEOUT`,
`PERMISSION_DENIED`, `SYNTAX`, `BAD_ARGS` — the record carries the cause and the
next action that is actually *different* from the one that just failed.
## The tool
| Action | What it answers |
| --- | --- |
| `stats` | What keeps breaking, folded by signature. **Start here.** |
| `list` | What failed most recently, newest first |
| `show` | Every occurrence of one signature, by prefix |
| `sessions` | Which journal files exist on disk, with sizes |
| `clear` | Archive the live page and start fresh |
| `selftest` | Prove the observer is attached and the directory is writable |
`stats` is the one that changes behaviour. "The same error five times" printed as
one row with `count=5, recurring=true` is a different instruction to a reader than
five separate stack traces.
<a id="install"></a>
## Install
The plugin is installed as a package into a dsh **profile**, the same way every
other dsh plugin is. `dsh plugin` forwards to `pnpm` inside the profile directory,
so any spec pnpm accepts works.
```bash
# from GitHub (the published form)
dsh plugin --profile web add github:catsenior507/dsh-tool-failure-journal
# a local checkout, while developing
dsh plugin --profile web add /absolute/path/to/dsh-tool-failure-journal
```
`web` is the shipped GUI profile; substitute `headless`, `sdk`, `acp`, or your own
profile name. On Windows, use forward slashes in a path.
Then **restart the host** so the profile recomposes, and confirm with:
```
failure_journal action=selftest
```
`selftest` reports whether the observer is attached and writes a probe file into
the journal directory to prove writability.
### What install does *not* do
- **No build step.** The published JavaScript *is* the source — there is no
`dist`, no bundler, and no `prepare` script, so nothing runs on install.
- **No dependencies.** `dependencies` and `peerDependencies` are both empty; the
plugin needs only the harness it is loaded into. Cordis is provided by the host
at runtime.
- **No native code, no compiler, no network at run time.**
Node.js 20 or newer, because the harness itself requires it.
## Where it writes
`<DSH_HOME>/failure-journal/sessions/<sessionId>.jsonl`, one file per session,
rotated by size (`maxBytes`, default 4 MiB, `maxRotated` generations kept).
Rotated generations are **kept, not deleted**: a session that loops on one error
fifty times is the case worth reading afterwards, and truncating the file at the
moment it becomes interesting would defeat the point.
## Configure
`dsh plugin add` already inserted the plugin row. To change the defaults, edit
that row's `config` in the profile's `cordis.patch.yml`:
```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` also journals successful calls — useful for "what did the
command actually do". Note that success *tracking* for the `regression` signal
runs regardless; the flag only controls what reaches the disk.
## Design constraints worth stating
- **The observer runs on every tool call in the process**, so it must never throw
and must never block. Every derived field reads through a total accessor, and
every write is a synchronous one-line append — microseconds, and it survives the
process dying mid-turn. An async queue would lose exactly the records this
plugin exists to keep.
- **A store failure is counted, not hidden.** `selftest` reports records written,
dropped, and rotated, so a journal that stopped working says so.
- **A tool call is never failed by this plugin.** Writing happens in a listener
the harness contains; the journal cannot break the tool it observes.
## Development
```bash
npm test # 31 tests, including a real cordis mount and a real event
```
`npm test` needs `@deepseek-ai/cordis` resolvable, which comes with a dsh
installation. Running from a checkout rather than an installed package, point
`node_modules` at the profile's copy — on Windows a directory junction works.
The mount test registers a fake `tools` service on a real cordis `Context`, emits
the genuine `tools/result` event, and reads the journal file back through the
tool — because "`inject` was declared wrong so the observer never attached" is
exactly the failure a hand-rolled fake context cannot catch.
| File | Role |
| --- | --- |
| `lib/index.js` | Cordis plugin: resolve config, attach, register |
| `lib/observer.js` | The `tools/result` and `session/event` listeners |
| `lib/store.js` | JSONL storage, rotation, archiving |
| `lib/signature.js` | Failure identity, normalization, remediation catalog |
| `lib/tool.js` | The `failure_journal` tool |
## License
MIT
Install
dsh plugin --profile web add github:catsenior507/dsh-tool-failure-journal
Profile: web
With the hub plugin installed, ask your agent to install it by name — it resolves the same plan shown here.
dsh plugin --profile web add github:stvlynn/dsh.fish#path:packages/dsh-plugin-hub
install dsh-external-dsh-tool-failure-journal from the hub
- This source has no pinned commit, so a later push upstream changes what installs. Prefer pinning a commit.