Repository navigation
[Bug] Conversation pane goes blank on all sessions — TypeError: Cannot read properties of null (reading 'kind') #9156
Replies: 5 comments 2 replies
|
补一条写入侧证据:
证据 坏记录原文(磁盘上,取自修复前备份): {"type":"turn/end","seq":537,"time":1791482813806,"data":{"turn":2,"reason":null}}共 8 条 / 5 个会话:
建议(三层一起修)
另:写出 null 的路径已知不止一条(#8832 怀疑 本报告由账号主指导 DeepSeek V4.1 Flash 在 Hermes 上进行整理创作(因为我的 DeepSeek Harness 刚才挂掉了)。 |
|
Three write-side facts, version-scoped — plus two producers I have to own. 1. The
|
@deepseek-ai/dsh-agent-loop |
turn catch | what a bad kind writes |
|---|---|---|
0.1.6-alpha.2 |
turnEnds = { kind:'aborted', reason: signal.reason }; abortedCancelCause does not exist in the build (0 hits) |
the cause verbatim |
0.1.7-rc.2, 0.2.0-rc.2, 0.2.1-alpha.2 |
const cause = abortedCancelCause(signal); if (cause !== void 0) { turnEnds = { kind:'aborted', reason: cause }; throw error } |
reason: null |
In the newer three, abortedCancelCause ends in default: return assertNever(cause) (in 0.2.0-rc.2: lib/index.js:731-745), under the comment "cancel accepts the closed AgentCancelCause union" — true of TypeScript, false at runtime for a JS-side caller. The throw happens inside the catch, so the assignment never runs, turnEnds is still null, and the finally appends turn/end with reason: null. That is exactly your record.
So the same misuse has two different fates: on 0.1.6-alpha.2 and earlier it lands as a malformed-but-tolerated reason: "thinking-loop" (the client reads ('thinking-loop').kind → undefined, no throw); from 0.1.7 on it lands as reason: null, which is fatal.
2. Two producers — both of them are mine
AgentCancelCause is a closed 4-member union (packages/core/session/src/types.ts:189): user | parent | disposed | hook(reason). hook is the member that exists for plugins — core uses it itself ({ kind:'hook', reason:'deepseek-account/signed-out' }). Two of my published packages hand a bare string instead:
// @argszero/cordis-plugin-thinking-loop-guard@0.1.11 (escalate:'cancel')
agent.cancel(config.cancelCause as unknown as AgentCancelCause) // cancelCause is z.string(), default 'thinking-loop'
// @argszero/cordis-plugin-turn-budget-guard@0.1.1
cancel(cause?: string, options?: { keepInbox?: boolean }): void // → agent.cancel(action.cause, { keepInbox: true })('thinking-loop').kind is undefined → the switch falls through to default → assertNever throws → reason: null. The as unknown as on the first line is the tell that the union was being lied to. Both will emit { kind:'hook', reason: <the configured string> } in their next releases; the config field stays a string, so nothing changes for a user.
3. The write boundary never inspected the field
Session.append snapshots the payload (snapshotJsonValue) and then calls validateSessionEventData (packages/core/session/src/surface.ts:166), which inspects surface message slots, request/header and tool/result only. turn/end.reason is a TypeScript-only invariant — nothing rejects null at the append site, which is why the record is durably on disk.
On the null-vs-undefined question
The append-time snapshot does not drop nulls: walkJsonValue (packages/util/values/src/index.ts) has an explicit if (current === null) { assign(task.destination, null); continue }. The log therefore preserves the explicit null; the undefined in your stack comes from a later stage, not from that projection.
Read side at HEAD
Still 11 unguarded reason.kind reads in dsh-client-ui-chat on origin/master: contract/turn-process.ts:72, chat/ChatGroupSeat.tsx:149, chat/TurnProcessNodeView.tsx:20, client/apply.ts:106, conversation-nodes/turn-error.ts:34,35,66,67, conversation-nodes/turn-max-tokens.ts:42,51, conversation-nodes/turn-tail.ts:82 — your count of 11 matches exactly.
Thanks for splitting the report into read and write halves; the version table is what made "which part is a regression" answerable at all.
|
The two producers are found, fixed, and published. Following up on this report: a source sweep for Each now hands over the Three details worth recording for anyone else hitting this thread:
One note for other plugin authors who need to abort a turn: the union is closed, but it has a member plugins own, and |
|
本机 profile 未安装 thinking-loop-guard / turn-budget-guard(node_modules 下没有 @argszero,全库也无 cancelCause 引用),但仍有一条同样的坏记录:会话 26375d09,seq 229,turn 1,reason 为 null,时间 2026-10-14T16:49:10.606Z(前一事件 step/end turn1 step22,后一事件 session/end-seed)。本机 bundles 只有 base、web-app、voice-input、pet bridge、dshmarket、computer-use。 另外 abortedCancelCause 是直接读 signal.reason.kind,所以「未带 reason 的裸 abort」(AbortError 不带 kind 字段)同样会落到 default 分支抛错——若扫描只匹配 agent.cancel(,这类调用方会被漏掉。建议在降级分支里把未知的 signal.reason 记一次日志/计数,否则事后无法定位调用方。 |
|
Your read of the two mechanisms is right, and I can now put exact lines under both. 1. Where the
So when 2. Your reason-less-abort observation — measured, not just reasoned.
So
3. On reachability from the shipped tree. The loop-owned controllers are created at Which means the record on your machine — session 4. Your suggestion. Agreed, and it is worth stating what the current shape assumes: the On the plugin side, If you can paste the raw line (whether it reads |
Uh oh!
There was an error while loading. Please reload this page.
DSH 对话区整体空白 —— turn 渲染路径空指针缺陷(附完整排查过程)
0. 摘要(TL;DR)
@deepseek-ai/dsh-client-ui-chat的 turn 渲染路径有 3 处未做空值保护的属性读取(
X.end?.data.reason.kind)。当某个 turn 节点的end存在、但end.data.reason为null时,会抛
TypeError: Cannot read properties of null (reading 'kind')。由于 DSH 的 slot 错误边界会把抛异常的
conversation.view整块渲染成空 div,表现为:对话区彻底空白(历史和新回复都看不到),所有会话包括新建会话都一样,且不重载无法恢复。
已确认:与用户数据无关、与第三方插件无关;在纯净内核(仅
dsh-base+dsh-web-app)配置下同样复现。加上可选链(
?.)后问题消失。1. 环境
0.2.0-rc.2(nightly 通道;更新源https://download.deepseek.com/dsh-desk/feeds/win-x64/nightly.yml当前最新即为此版本)@deepseek-ai/dsh-client-ui-chat@0.2.0-rc.2另经下载比对确认:更新版
@deepseek-ai/dsh-client-ui-chat@0.2.1-alpha.1中该行同样未修复。2. 症状与影响
3. 复现
0.2.0-rc.2。4. 崩溃日志证据
日志位置:
%APPDATA%\@deepseek-ai\dsh-desktop\logs\crash-<timestamp>-host.logDevTools(F12 → Console)中的堆栈:
5. 根因定位
在
@deepseek-ai/dsh-client-ui-chat/lib/client.js中,3 处同类缺陷:要点:
end(turn.end)确实可能不存在(未结束的 turn),所以作者加了end?.;end.data.reason也可能为null,而这里没有保护 → 直接抛reading 'kind'。const alwaysOpen = liveProcess || interleavedInput || turnProcessAlwaysOpen(routedNode);),一旦某个 turn 处于「有
end、无reason」的中间态就必崩。为什么会「整个对话区」空白:DSH 的 slot 错误边界在组件抛异常时,会把整个
conversation.view哨位渲染成一个空<div data-slot-error="conversation.view">,所以不是「某条消息没渲染」,而是整块对话区被清空;且该状态会保持到重载。
6. 已排除的可能(附证据)
排查过程中逐项排除,供官方参考:
session.v4.jsonl.zstd:多帧解压 0 失败、JSON 0 坏、尾部完整;所有turn/end事件均带reasondsh-base+dsh-web-app(≈纯净内核)后仍复现received more than one start Match,conversationContextKey已是防碰撞实现Cache/Code Cache/GPUCache后无效0.2.1-alpha.1中同一行同样未修关键结论:问题在 DSH 内核代码,与用户配置/插件/数据无关。
7. 建议修复
给三处补上可选链(一行改动):
已验证有效:本地对
app.asar内该文件按上述方式打补丁(等长替换 + 同步更新 asar 完整性哈希)后,对话区恢复正常,反复切换不再复现。
8. 临时绕过(给其他用户的建议)
app.asar(有完整性校验风险,需先备份)。English Summary
Title: Entire conversation pane goes blank — null dereference in the turn render path
Affected:
@deepseek-ai/dsh-client-ui-chatin DSH0.2.0-rc.2(desktop, nightly).Confirmed still present in
0.2.1-alpha.1.Symptom: The whole conversation pane renders blank (no history, no new replies) for all
sessions, including brand-new ones. Restarting does not help; only a reload (Ctrl+R) recovers it,
and switching again re-triggers it. Rest of the UI is fine.
Trigger: The first render is fine; the crash happens on re-assembly (switching
sessions/views).
Root cause: Three unguarded property reads in
dsh-client-ui-chat/lib/client.js(
turnProcessAlwaysOpen,ChatGroupSeat,TurnProcessNodeView):When a turn node has an
endbutend.data.reason === null, it throwsTypeError: Cannot read properties of null (reading 'kind'). DSH's slot error boundary then rendersthe whole
conversation.viewslot as an empty<div>, hence the fully blank pane.Not caused by: session data corruption (all session files validate), third-party plugins
(reproduces with a vanilla core-only profile), the known duplicate-callId defect (absent in this
build), the skin/wallpaper plugins, or client caches.
Fix: add optional chaining —
location.turn.end?.data?.reason?.kind.Verified locally: patching those three lines restores the conversation pane.
Workaround until fixed: reload (Ctrl+R) after a crash; avoid rapid session/view switching.
Logs:
%APPDATA%\@deepseek-ai\dsh-desktop\logs\crash-*-host.log(contains
slot entry crashed in 'conversation.view'and[session-controller] event feed subscriber failed: TypeError: Cannot read properties of null (reading 'kind')).All reactions