DeepSeek Harness sessions won't open: null turn/end reason
One especially deceptive form of "the session will not open" in DeepSeek Harness (DSH) is this: the file is not actually corrupt and validation passes, yet it opens blank or directly reports unable to load. In this batch of threads this class is split into three independent mechanisms, all sharing the trait that the write side and the read side disagree about the same log: cancel() writes turn/end's reason as null (the reader reading reason.kind throws, the error boundary renders the whole conversation view as an empty div, so all sessions look blank); run_code's sub-call id repeats within the same session (the writer does not validate, the reader enforces uniqueness, so the whole session is rejected); the tool-result pruner writes a replacement tool/result outside a turn (the reader requires non-append writes to be inside an open turn, so the reader rejects what its own writer produced). None of the three needs "guessing" — the log has a searchable distinguishing feature, and this article gives localization commands and field-level fixes for "one bad event → duplicate id → out-of-turn write" one by one.
DeepSeek Harness: a null turn/end reason blanks the whole session list
In one line: after turn/end's reason lands as null, the client throws while reading reason.kind and the error boundary renders the whole conversation.view as an empty div, so "one session is broken" shows up as "all sessions are blank". The symptom has two degrees: at best the conversation pane is blank but the trajectory view is normal, at worst the whole session list goes blank (#8832, #9156).
DSH plugin: mechanism, the writer throws and the reader reads bare
According to several community members' line-by-line source review, there are two gaps on the chain, and either one filled in would avoid the crash, but both are missing:
- Write side:
cancel(cause)'s entry point does not validatecause; the function that translates it into protocol fields only knows the four valuesuser/parent/disposed/hook, and an unrecognized value throws inside the catch block, leaving theturnEndsvariable that should have been assigned stuck at its initialnull, andfinallypersists it unconditionally:
turn/end { "kind": "aborted", "reason": null } <- violates the protocol
- Read side: the client reads
end?.data.reason.kind/event.data.reason.kinddirectly in many places without a null guard; readingnullthrowsCannot read properties of null (reading 'kind'). According to the localization in the thread, there are about 11 such bare reads on the read side (#9156).
There is also an often-overlooked fact: validateSessionEventData does not validate turn/end's payload, so null lands on disk without trouble; and the session's load validation does not check this item either, so "the file is valid, reading crashes" holds (#9699).
DeepSeek Harness: numbered localization to find that bad event
The key test is: the session log contains "reason":null, while the session's own load validation passes normally. Three steps:
- Look at the symptom combination. Expect all at once: the conversation pane blank / the session list blank, the trajectory view still renders normally, and reloading or restarting recovers it for a while before it recurs. This combination basically orients it to this case (#9156).
- Search the session log for the bad value. Session logs live in
~/.dsh/storages/sessions/(on Windows%USERPROFILE%\.dsh\storages\sessions\). Run this against the target session file:
Select-String -Path "<session file>" -Pattern '"reason":null'
# expected: a few hits, i.e. that protocol-violating turn/end
- Confirm it is
turn/endand not some other field. Check whether the event type near the hit isturn/end; onlyturn/end'sreasonbeingnullcrashes the whole view (#8832).
DSH plugin: fix, complete the reason into a full structure
Manual repair has only one key point — the shape you supply must be complete; writing it short just trades one crash for another. Steps:
- Back up the session file first (copy it somewhere outside the workspace), then act.
- Change the hit
"reason":nullinto the full structure the protocol requires:
{"kind":"aborted","reason":{"kind":"hook","reason":"unknown"}}
Note that it cannot be shortened to {"kind":"aborted"} — the thread measured that the reader next reads reason.reason.kind, so the short form directly triggers another crash (#8832).
- Change only that field, keeping seq and the frame structure unchanged: do not reorder events, do not renumber, do not touch other fields, or you will turn a "fixable dirty value" into an "unreadable log".
- Reload the page; expect the conversation pane to recover; if it is still blank, check whether there is a second
"reason":null. - Add a prevention. The community plugin
@argszero/dsh-cancel-cause-guardwrapscancel()at theagent/createdstage and normalizes an out-of-range cause to{kind:'hook', reason}, stopping newnulls from landing at the source:
npm i @argszero/dsh-cancel-cause-guard
- insert:
- id: cancel-cause-guard
name: '@argszero/dsh-cancel-cause-guard'
State the boundary clearly: the prevention plugin can only stop "newly produced" bad values, not fix
nulllines already on disk. So existing sessions still have to go through the manual repair in step 2 (#9699).
DeepSeek Harness: who is most likely to write a null reason
The most frequently named is "a third-party plugin calling agent.cancel() with a bare string or an unrecognized cause". According to the community's review of source consistent with master: ReactLoopAgent.cancel()'s entry point does not validate cause, and when abortedCancelCause(signal) throws assertNever, turnEnds is still null, and finally appends unconditionally (#9699). So when troubleshooting, list "which plugins that cancel turns were installed before the incident"; you can often find the one that corrupted the log directly.
DSH plugin: duplicate sub-call ids, no error on write but rejected on read
In one line: as long as a run_code (PTC) sub-call id repeats once within a Session, writing reports no error at all, but the next read rejects the whole thing and the entire session is permanently unopenable — one duplicate costs you the whole session. The hit session mentioned in the thread reached 3,569 events (#9677).
DeepSeek Harness: mechanism, a mis-scoped counter and write/read asymmetry
According to the reporter and the community's source-level confirmation, the two halves are:
- The write side does not guarantee uniqueness: the
dispatchescounter inptc.tsis scoped to a singleexecute()(restarting from 1 each time), whileexec.callIdis reused by the provider when resending a call — both ends can repeat; - The read side enforces uniqueness: uniqueness is enforced only on the read side during folding (session format v4's
subCallIdmust be unique), with no corresponding validation on the write side (#9677).
DSH plugin: numbered localization and fix
- Localize: count occurrences of sub-call ids in the target session file; any with a count greater than 1 is a conflict:
Select-String -Path "<session file>" -Pattern '"subCallId":"([^"]+)"' -AllMatches |
ForEach-Object { $_.Matches } | ForEach-Object { $_.Groups[1].Value } |
Group-Object | Where-Object Count -gt 1 | Select-Object Count, Name
# expected: list the duplicated subCallIds (Count > 1)
- Prevention (recommended, lowest cost): install the community plugin
@argszero/dsh-ptc-subcall-guard; it renames on the write path and avoids duplicate ids in new sessions. - Fix an already-corrupted log: this requires renumbering the artifacts already on disk (append a suffix to the later duplicate ids and sync the four representations), offline surgery — back up first; a third-party session-repair tool in the community can do this renumbering.
- Verify: after the fix, reopen the session; expect it to no longer be rejected wholesale; if there are other shape problems, a different validation error will appear — continue per the error.
DeepSeek Harness: the pruner writes a replacement outside a turn
In one line: when a replacement write of tool/result does not fall inside an open turn, the reader judges it illegal — this is not "the file is broken", but components disagreeing about an invariant. The error shape:
SessionFormatError: tool/result is outside an open turn
According to the reporter's localization and a synthetic minimal repro provided by the community: the tool-result pruner appends, while the session is idle (no open turn), a replacement tool/result carrying surfaceOp.replace, and the read side requires a non-append tool/result to have an open turn, so the read side rejects what its own writer wrote (#8812).
Handling steps:
- Localize: search the session file for
surfaceOp, and see whether thetool/resultcarryingreplaceis preceded by an unclosedturn/start. - Emergency fix: truncate all prune lines after the last
turn/end(those lines are replacement writes; the original content is still in the log, so no messages are lost), and delete that session's projection cache so it rebuilds (#8812). - Verify: reopen the session; expect it to load normally; then trigger a long session to let it prune naturally and watch whether it recurs.
- Long-term dependency: this behavior is an inconsistency between the core-side pruner and the read-side invariant; the plugin layer cannot fix the root cause.
DSH plugin troubleshooting notes
All three are "write/read inconsistency", and the common principle is: first judge "the file is valid but reading crashes" versus "reading validation rejects", then act. Eight points:
- Look at the trajectory view first: the conversation pane blank but the trajectory normal strongly points to a
turn/endreasonproblem; if the trajectory will not open either, the whole thing is rejected (the duplicate-id / out-of-turn-write class); if the log holds no bad value and it merely shows nothing after opening, that is the other, UI-side family, covered in DeepSeek Harness blank sessions (#9156). - Do not trust "the official validation passed so it must be fine":
validateSessionEventDatadoes not check theturn/endpayload, and load validation does not check this item either (#9699). - A supplied field must be complete:
{"kind":"aborted"}is not a valid fix and triggers the next crash (#8832). - Change only that field in the log: keep seq, the frame structure and the event order; any reordering turns a fixable dirty value into an unreadable log.
reason:nulland "duplicate id" are two different fingerprints: the former searches"reason":null, the latter countssubCallId— do not mix them up (#9677).- Prevention beats repair: plugins like
cancel-cause-guardandptc-subcall-guardstop new bad values, but existing ones still need manual handling. - Always back up before changing anything: all three repairs edit the session file directly, and a backup is the only rollback.
- State the fix status honestly:
turn/end'sreasonand the pruner's out-of-turn write are still unfixed as of0.2.1-alpha.*; duplicate ids have a community plugin to work around, but the write-side validation in the core is still missing.
When troubleshooting session problems, you often have to rule out the plugin variables first — after all, the most common source of corrupted logs is a third-party plugin's malformed call. If you want to see at a glance "which plugins are installed and whether there have been recent install/uninstall changes", use a visual plugin marketplace such as DSH Plugin Hub instead of hand-checking commands everywhere: plugin list, install/uninstall history and system logs are gathered in one place, making it much faster to locate "who wrote this bad event".

Source: Discussion #8832, Discussion #9156, Discussion #9060, Discussion #9699, Discussion #9646, Discussion #9677, Discussion #8812.
FAQ
In DeepSeek Harness, all sessions' conversation pane suddenly going blank while the trajectory view still works usually means the files are valid and the read side is crashing on valid input. The cause is that one turn/end has its reason written as null, and the client reads reason.kind without a guard, so the error boundary renders the whole conversation view as an empty div and all sessions look blank. To localize it, search the session log for "reason":null, and to fix it, complete that entry into a fully shaped structure such as {"kind":"aborted","reason":{"kind":"hook","reason":"unknown"}}.
A third-party plugin calling DeepSeek Harness's agent.cancel() corrupts the session log because cancel's entry point does not validate cause, and the function that translates cause into protocol fields only knows the four values user/parent/disposed/hook; an unrecognized value throws in the catch block, leaving the turn-end variable that should have been assigned stuck at its initial null, and finally then persists it unconditionally as reason:null. The protocol requires the shape {"kind":"aborted","reason":{...}} here, so once it is written as null the read side crashes.
A DeepSeek Harness run_code-related session failing to load shows a repeated sub-call id within the same session. run_code's dispatches counter is scoped to a single execute() and restarts from 1 each time, while the call id is reused by the upstream on resend; uniqueness is enforced only on the read side during folding, not validated on the write side, so writing reports no error while reading rejects the whole session. To localize it, count occurrences of subCallId in the session; any with a count greater than 1 is a conflict.
A DeepSeek Harness session reporting SessionFormatError: tool/result is outside an open turn was written by the tool-result pruner. While the session is idle with no open turn, it appended a replacement tool/result carrying surfaceOp.replace, and the read side requires a non-append tool/result to be inside an open turn, so the read side rejects what its own writer wrote. The emergency handling is to truncate the prune lines after the last turn/end and delete the projection cache; this way no messages are lost.
Related Terms
- turn/end event
- turn/end is the event in a DeepSeek Harness session log that marks the end of a turn; by protocol it must carry a reason field explaining why the turn ended (such as completed, aborted), and when aborted the reason must itself contain a specific cause. A null reason violates that protocol and makes a client reading the event throw.— https://github.com/deepseek-ai/deepseek-harness/discussions/9060
- surfaceOp.replace
- surfaceOp.replace is the marker in a DeepSeek Harness session log indicating that 'this event replaces an earlier record', a non-append write. The read side has an extra requirement for a tool/result carrying this marker (it must be inside an open turn), so writing it outside a turn is judged illegal by the reader.— https://github.com/deepseek-ai/deepseek-harness/discussions/8812
- subCallId
- subCallId is the identifier run_code (PTC) assigns to each sub-call within one execution, and the session format requires it to be unique within the same Session. The write side not validating uniqueness while the read side enforces it is the root of the family 'a duplicate id makes the whole session unreadable'.— https://github.com/deepseek-ai/deepseek-harness/discussions/9677
- validateSessionEventData
- validateSessionEventData is the shape-validation function DeepSeek Harness runs when writing or loading session events, used to intercept events with missing fields or wrong types. It currently does not check turn/end's payload, so an illegal value such as reason:null can land on disk without trouble.— https://github.com/deepseek-ai/deepseek-harness/discussions/9699
Sources
- #8832 — A single turn/end with a null reason blanks the whole session list (with localization and fix suggestions)· deepseek-ai (GitHub Discussions)
- #9156 — Conversation pane goes blank on all sessions — TypeError: Cannot read properties of null (reading 'kind')· deepseek-ai (GitHub Discussions)
- #9060 — cancel() writes turn/end with reason:null (violating the TurnEndReasonMap protocol)· deepseek-ai (GitHub Discussions)
- #9699 — ReactLoopAgent.cancel() does not validate cause, writes {"reason":null} and permanently crashes the Chat UI· deepseek-ai (GitHub Discussions)
- #9646 — DSH bug report: a single turn/end with reason: null permanently crashes the whole Chat view and drags down all sessions· deepseek-ai (GitHub Discussions)
- #9677 — One repeated PTC sub-call id makes an entire Session unloadable (data loss)· deepseek-ai (GitHub Discussions)
- #8812 — tool-result pruner appends replacement tool/result outside any open turn· deepseek-ai (GitHub Discussions)