DeepSeek Harness sessions won't open: null turn/end reason

TroubleshootingPublished 2026-10-17Author: DeepSeek Plugin Market
DeepSeek HarnessDSH session corruptionturn/endnull reasontoolCallIdSessionFormatErrorblank session
DeepSeek Harness sessions open blank or fail to load from three write/read mismatches: a null turn/end reason, a duplicate id, or a pruner outside a turn.

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:

  1. Write side: cancel(cause)'s entry point does not validate cause; the function that translates it into protocol fields only knows the four values user / parent / disposed / hook, and an unrecognized value throws inside the catch block, leaving the turnEnds variable that should have been assigned stuck at its initial null, and finally persists it unconditionally:
turn/end { "kind": "aborted", "reason": null }   <- violates the protocol
  1. Read side: the client reads end?.data.reason.kind / event.data.reason.kind directly in many places without a null guard; reading null throws Cannot 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:

  1. 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).
  2. 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:
powershell
Select-String -Path "<session file>" -Pattern '"reason":null'
#   expected: a few hits, i.e. that protocol-violating turn/end
  1. Confirm it is turn/end and not some other field. Check whether the event type near the hit is turn/end; only turn/end's reason being null crashes 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:

  1. Back up the session file first (copy it somewhere outside the workspace), then act.
  2. Change the hit "reason":null into the full structure the protocol requires:
json
{"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).

  1. 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".
  2. Reload the page; expect the conversation pane to recover; if it is still blank, check whether there is a second "reason":null.
  3. Add a prevention. The community plugin @argszero/dsh-cancel-cause-guard wraps cancel() at the agent/created stage and normalizes an out-of-range cause to {kind:'hook', reason}, stopping new nulls from landing at the source:
bash
npm i @argszero/dsh-cancel-cause-guard
yaml
- 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 null lines 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:

  1. The write side does not guarantee uniqueness: the dispatches counter in ptc.ts is scoped to a single execute() (restarting from 1 each time), while exec.callId is reused by the provider when resending a call — both ends can repeat;
  2. The read side enforces uniqueness: uniqueness is enforced only on the read side during folding (session format v4's subCallId must be unique), with no corresponding validation on the write side (#9677).

DSH plugin: numbered localization and fix

  1. Localize: count occurrences of sub-call ids in the target session file; any with a count greater than 1 is a conflict:
powershell
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)
  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.
  2. 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.
  3. 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:

  1. Localize: search the session file for surfaceOp, and see whether the tool/result carrying replace is preceded by an unclosed turn/start.
  2. 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).
  3. Verify: reopen the session; expect it to load normally; then trigger a long session to let it prune naturally and watch whether it recurs.
  4. 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:

  1. Look at the trajectory view first: the conversation pane blank but the trajectory normal strongly points to a turn/end reason problem; 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).
  2. Do not trust "the official validation passed so it must be fine": validateSessionEventData does not check the turn/end payload, and load validation does not check this item either (#9699).
  3. A supplied field must be complete: {"kind":"aborted"} is not a valid fix and triggers the next crash (#8832).
  4. 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.
  5. reason:null and "duplicate id" are two different fingerprints: the former searches "reason":null, the latter counts subCallId — do not mix them up (#9677).
  6. Prevention beats repair: plugins like cancel-cause-guard and ptc-subcall-guard stop new bad values, but existing ones still need manual handling.
  7. Always back up before changing anything: all three repairs edit the session file directly, and a backup is the only rollback.
  8. State the fix status honestly: turn/end's reason and the pruner's out-of-turn write are still unfixed as of 0.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".

Installed Plugins

Source: Discussion #8832, Discussion #9156, Discussion #9060, Discussion #9699, Discussion #9646, Discussion #9677, Discussion #8812.

FAQ

In DeepSeek Harness the conversation pane of all sessions suddenly goes blank while the trajectory view still works, and it recovers only on restart. Is the session file corrupt?

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"}}.

Why does a third-party plugin calling DeepSeek Harness's agent.cancel() corrupt the session log?

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 reports unable to load when opened. What feature can I see in the log?

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.

Opening a DeepSeek Harness session reports SessionFormatError: tool/result is outside an open turn. What wrote that?

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