Back to home@catsenior507

dsh-tool-failure-journal

Durable failure journal for dsh tool calls: every abnormal exit appended to JSONL on disk, folded by error signature, readable back by the agent.

Stars
0
Language
JavaScript
Created
Sep 10, 2026
Updated
Sep 10, 2026
GitHub repo

Introduction

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: MIT DeepSeek Harness plugin version node stars

English · 简体中文


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:

{"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

ActionWhat it answers
statsWhat keeps breaking, folded by signature. Start here.
listWhat failed most recently, newest first
showEvery occurrence of one signature, by prefix
sessionsWhich journal files exist on disk, with sizes
clearArchive the live page and start fresh
selftestProve 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.

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.

# 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:

- 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

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.

FileRole
lib/index.jsCordis plugin: resolve config, attach, register
lib/observer.jsThe tools/result and session/event listeners
lib/store.jsJSONL storage, rotation, archiving
lib/signature.jsFailure identity, normalization, remediation catalog
lib/tool.jsThe failure_journal tool

License

MIT