A proactive session-log completeness audit for the deepseek-harness (dsh) agent harness. It never fixes a log — it diagnoses the full corruption surface and prints a report before replay crashes or silently drops history.
The dsh harness persists every agent interaction to an append-only JSONL session log. Two in-tree mechanisms already exist, but neither is a full diagnostic:
repair.ts(interruptedTurnClosers) is narrow — it only synthesizes replay-time closers for a crash tail (open-turn truncation), one mechanism.validateStoredEvents(storage-contract) is a fail-closed load-time check — it refuses an unrecognized/structurally-invalid log outright, but tells you only "this log is bad", not which of the many corruption classes is present.
Upstream, the session-log corruption family has produced 14+ distinct bug reports (seq gaps, index-reuse, format-version drift, empty-id duplicates, orphan tool_calls, turn-reason loss). This plugin audits for all of them at once and classifies each finding as error (replay would fail / needs manual) vs warn (data may have silently dropped) vs info (recoverable — e.g. the open-turn tail the in-tree repair already handles).
| Code | Severity | What it means |
|---|---|---|
HEADER_MISSING |
error | First line is not a session header; log unusable |
FORMAT_VERSION_DRIFT |
error | Log format vN differs from the harness's SESSION_FORMAT_VERSION (written by newer/older harness) |
UNPARSABLE_LINE |
error | A committed line is not a decodeable session record |
SEQ_GAP |
error | Events are missing between seqs (contiguity break) |
SEQ_DUPLICATE |
warn | Same seq appears twice (index reuse) |
SEQ_REORDERED |
warn | An event appears below the contiguous watermark (out-of-order / reused seq) |
UNKNOWN_REQUIRED_TYPE |
error | A type outside the known vocabulary and not marked ignorable. The harness read path refuses a log containing an unmarked unknown type (SessionFormatUnsupportedError), so a reload of this session will hard-fail — this is the #5769 failure mode (a plugin wrote a custom event type that the log then rejects). Fix: mark the event ignorable, or the session may be unrecoverable. |
TOOL_CALL_NO_ID / TOOL_RESULT_NO_ID |
warn | tool/call or tool/result carries no usable call identity |
ORPHAN_TOOL_CALL |
info | A tool was requested (tool/call) but its outcome was never durably recorded (tool/result) |
OPEN_TURN |
info | A turn opened but never closed (crash interruption; the in-tree repair synthesizes closers for this) |
EMPTY_TEXT_BLOCK |
warn | A persisted assistant/message carries an empty/whitespace-only {type:'text', text:''} content block. Harmless under lenient models (GLM/Gemini), but a strict provider (Claude) refuses it and the session becomes permanently unusable after a model switch (#5773). Strip these blocks before replaying at a new provider. |
UNREADABLE_ARTIFACT |
error | The artifact is a Zstandard container whose structure is broken — a bad frame magic, a reserved frame-header bit, a reserved block type. This is the class that takes the whole application down: the harness's artifact listing aborts on it, so dsh cannot boot at all until the file is moved out of the sessions root (#7161). The finding names the byte offset, in the harness's own wording. |
UNDECODABLE_FRAME |
error | The container structure is intact, so the frames can be located, but a frame's content failed validation (checksum or entropy decode). The session cannot be read; unlike UNREADABLE_ARTIFACT this does not abort a boot, because the failure happens after the listing. |
TORN_FINAL_FRAME |
info | The final Zstandard frame is incomplete — a crash tail from a process killed mid-append. The harness tolerates this class while listing and repairs what it can, so it is reported as info and never as needs-manual. The tail's plaintext is recovered on a best-effort basis and reported separately; it is never folded into the audited content, which covers the complete frames only. |
SESSION_PATH_MISMATCH |
error | The artifact does not sit in the directory its own header names. The backend re-derives the path from the header on every read (assertStoredIdentity, session-persistence-jsonl) and refuses the read — corrupt session log "…": header id "…" and cwd identify "…" — and that refusal escapes the artifact listing, so it aborts the boot exactly like UNREADABLE_ARTIFACT (#7161, the renamed-directory row). It fires when a session directory was renamed or moved by hand. The session directory must be the header id under the backend's lossless path encoding; the check reports a path shape, so if both names resolve to one file (a symlink or hard link) the backend accepts the artifact and the finding is advisory. |
PROJECT_PATH_MISMATCH |
warn | The artifact sits under a project directory that the header's recorded cwd does not derive. The backend's project naming is deliberately lossy (separator runs collapse to one -, the slug is truncated to fit a filesystem component), so a disagreement is advisory — it usually means the layout predates a naming revision, not that the read is refused. A header with no cwd is compared against the _no-cwd bucket, which is where the backend puts such a session. |
EMPTY_ARTIFACT |
info | Zero bytes. The harness skips an empty artifact rather than refusing it, so this is not corruption. |
REPETITIVE_STREAM |
warn | A run of many identical consecutive assistant/chunk deltas within one stream — the signature of a degenerate decode loop (the model emits the same short token(s) repeatedly with no tool call between them, e.g. '\n' × 701). No tool-call repeat guard can see this because no tool call is made; the harness itself places no turn-length bound. |
Add it to a cordis.yml resolution manifest:
- insert:
- id: session-audit
name: '@argszero/cordis-plugin-session-audit'Then /session-audit audits every stored session artifact — .jsonl and the compressed .jsonl.zstd container — under the JSONL root, and /session-audit id=<substring> narrows to matching session ids. Configure the root via plugin config:
- insert:
- id: session-audit
name: '@argszero/cordis-plugin-session-audit'
config:
root: !!js dshHomePath('sessions')The package ships a session-audit bin that runs under plain Node and needs zero dsh runtime:
# audit a whole sessions root
npx @argszero/cordis-plugin-session-audit /path/to/sessions
# narrow to one session id
npx @argszero/cordis-plugin-session-audit /path/to/sessions --id <substring>
# default root from DSH_HOME
DSH_HOME=/path/to/home npx @argszero/cordis-plugin-session-auditExit code. The CLI exits 1 when any artifact needs manual attention, 0 otherwise — so it can gate a launch:
npx @argszero/cordis-plugin-session-audit "$DSH_HOME/sessions" && dsh web- Both containers are read.
.jsonland.jsonl.zstdare audited alike, so a machine with compression enabled gets the same findings as one without. The container is chosen by artifact name, which is how the harness chooses it — a.jsonl.zstdfile whose bytes are not Zstandard is reported as the structural defect it is, not as a malformed header. - Per-frame, deliberately. Node's
zstdDecompressSyncaccepts a concatenated container and decodes only the first frame, with no error. The harness's container is concatenated frames, one per durable batch, so a one-shot decode silently returns the first batch and drops the rest — which would make a badly truncated session read as clean. This tool scans the frame structure first and decodes each frame, which is also what lets it tell a torn crash tail apart from a structural defect. - Two-phase, so the important verdict survives a weak runtime. The structural scan is plain
Bufferarithmetic and needs nonode:zlib; decompression is the optional second phase. On a Node withoutzstdDecompressSyncthe audit still reports whether an artifact would abort a boot. - Non-mutating. This plugin never appends to, truncates, or rewrites a session log.
- It is a diagnostic companion, not a replacement for
repair.tsor a fix. Use it to understand what is wrong, then decide whether the in-tree repair or a manual migration is appropriate.
MIT