Replies: 3 comments
EN / EnglishEvidence1 · The data layer is healthy. Before touching anything, we checked the whole log with read-only tooling:
2 · Two other sessions of comparable size load fine. One at ~690k events and one at ~360k render normally; the affected one (~330k events, 724 messages) does not. So size alone does not decide it. 3 · The only anomaly in the log is one These three are the edit marker written when a message was edited — the marker records the edited node but leaves 4 · Setting the missing turn restores the session. We rewrote those three events from
That is the whole evidence chain: healthy data + exactly one unattributable block + fixing that block restores the client. The block is the cause; the client's handling of it is the defect (see the post's Proposed Fix). Authorship note: reproduced and measured by me against my own store; root-cause tracing and drafting assisted by AI; verification and publication by me. I have no engineering background — if any technical claim reads wrong, please call it out; I will re-verify against the toolchain and correct. Reported by the OfferKuai Team — Founder: Zhaofeng (Yaming). Website: https://www.offerkuai.com/ | Contact: contact@offerkuai.com 中文版 / ZH证据1 · 数据层是健康的。 动任何东西之前,我们用只读工具把整份日志查了一遍:
2 · 另外两个体量相当的会话能正常打开。 一个约 69 万事件、一个约 36 万事件都正常渲染;出问题的那个(约 33 万事件、724 条消息)不行 ⇒ 单看体量决定不了。 3 · 日志里唯一的异常是一个 这三条是消息被编辑时写入的编辑 marker —— marker 记下了被编辑的节点,却没有填 4 · 把缺失的 turn 补上,会话就恢复。 我们把这 3 个事件从
这就是完整的证据链:数据健康 + 恰好一个无法归属的块 + 修掉那个块客户端就恢复。病根是那个块;客户端对它的处理方式是缺陷(见主帖「建议修复」)。 声明:本次实测与统计由我针对自己的数据完成;根因追查与文稿撰写由 AI 辅助;核验与发布由我本人负责。我没有工程背景 —— 若任何技术表述有误,请直接指出,我会对照工具链重新核实并更正。 本报告由 OfferKuai(Offer快)团队提交 —— 创始人:Zhaofeng(Yaming)。官网:https://www.offerkuai.com/ | 联系:contact@offerkuai.com |
EN / EnglishProposed fix (directions, not a patch)1 · Give the open state machine an exit for every failure, not only remote ones. In :1994 await events.open({ maxMessages: 50 });
:1996 this.openState = "open"; // exit 1
:1999 if (!isRemoteFailure(error)) throw error; // non-remote error → re-thrown, state stays "loading"
:2001 this.openState = "error"; // exit 2 (remote failures only)So a non-remote failure during open leaves the UI with no state to move to. One direction: set 2 · Make an unattributable step non-fatal at the render layer. The render state machine waits for something that can never arrive when a The honest cost: skipping hides content. The edited message's replacement marker would silently not appear, and a reader has no way to tell "nothing happened" from "something was skipped". If this direction is taken, we would argue for a visible single notice (e.g. one line in the conversation: "1 event could not be attributed and was skipped") rather than a silent skip — otherwise the bug is traded for a quieter one. 3 · Catch these blocks offline, before a user opens the session. A structural scan for Where we would look firstIf we had to pick one: direction 1, because it is the difference between "a failure" and "a hang". Direction 2 is what makes the failure non-fatal for this particular shape, and direction 3 only surfaces it earlier. Our log shows the hang is reachable from a single malformed block, so the state machine having no exit is the general case the two others specialise. Questions for maintainers
Authorship note: reproduced and measured by me against my own store; root-cause tracing and drafting assisted by AI; verification and publication by me. I have no engineering background — if any technical claim reads wrong, please call it out; I will re-verify against the toolchain and correct. Reported by the OfferKuai Team — Founder: Zhaofeng (Yaming). Website: https://www.offerkuai.com/ | Contact: contact@offerkuai.com 中文版 / ZH建议修复(方向,不是补丁)1 · 给 open 状态机「每个失败」都留出口,而不只是远端失败。 在 :1994 await events.open({ maxMessages: 50 });
:1996 this.openState = "open"; // 出口 1
:1999 if (!isRemoteFailure(error)) throw error; // 非远端错误 → 重新抛出,状态仍是 "loading"
:2001 this.openState = "error"; // 出口 2(仅远端失败)⇒ 打开过程中的非远端失败会让 UI 没有任何状态可去。一个方向:对每个失败都置 2 · 让「无法归属的 step」在渲染层不致命。 当一个 代价要说清:跳过会藏起内容。被编辑消息的替换 marker 会静默不出现,读者分不清"什么都没发生"和"有东西被跳过了"。若取这个方向,我们认为应给一个可见的提示(例如对话里一行:"有 1 个事件无法归属,已跳过"),而不是静默跳过——否则只是把一个 bug 换成更安静的一个。 3 · 在用户打开会话之前,离线把这些块抓出来。 对 我们会先看哪里若只能选一个:方向 1 —— 它是"一次失败"与"一次挂死"之间的差别。方向 2 让这个特定的形状不致命,方向 3 只是更早暴露。我方日志显示一个畸形块就足以构成挂死,所以"状态机没有出口"是那两个方向共同特化的一般情形。 给维护者的问题
声明:本次实测与统计由我针对自己的数据完成;根因追查与文稿撰写由 AI 辅助;核验与发布由我本人负责。我没有工程背景 —— 若任何技术表述有误,请直接指出,我会对照工具链重新核实并更正。 本报告由 OfferKuai(Offer快)团队提交 —— 创始人:Zhaofeng(Yaming)。官网:https://www.offerkuai.com/ | 联系:contact@offerkuai.com |
EN / EnglishA note on the shape of this: over four hours today, five other reports landed on the same class of failure as this one — a render or load path that met unexpected input, threw, and left the user with a blank surface and no message. Each has its own root cause, and each was located independently and precisely by its author; I am listing them together because the structure is shared, not the cause. From oldest to newest:
The common shape, in three steps:
One place where two of these meet in the code. In #6954, @kevinchiha traces the failure to a That is the extent of our contribution here, and it is a narrow one: one function's exit conditions, read on the current line. The If the maintainers want a single place to start: the shared question is "what should a consumer do when persisted or transmitted data is structurally valid but unexpected?" — the answers in these five plus ours all currently resolve to throw, and let a boundary hide it. Authorship note: reproduced and measured by me against my own store; root-cause tracing and drafting assisted by AI; verification and publication by me. I have no engineering background — if any technical claim reads wrong, please call it out; I will re-verify against the toolchain and correct. Reported by the OfferKuai Team — Founder: Zhaofeng (Yaming). Website: https://www.offerkuai.com/ | Contact: contact@offerkuai.com 中文版 / ZH先说清这条的性质:今天四小时内,另有五篇报告落到了与本帖同一类失败上 —— 某条渲染或加载路径遇到意料之外的输入、抛错,最后留给用户一片空白、且没有任何提示。 它们各有各的根因,每一篇都是作者独立且精确定位的;我把它们列在一起,是因为结构相同,而不是原因相同。 按时间从早到晚:
共同形状,三步:
其中有两篇在代码里交汇于一处。 #6954 里,@kevinchiha 把失败追到一个 open 路径不认为可恢复的 这就是我方在这条里的贡献边界,而且很窄:一个函数的出口条件,且是在当前线上读到的。产生我们那片白屏的 如果维护者想找个起点:共同的问题是「当落盘或传输来的数据"结构合法但出人意料"时,消费者该怎么办?」—— 这五篇加我们这一篇,当前的答案都归结为 抛出去,然后让某个边界把它藏起来。 声明:本次实测与统计由我针对自己的数据完成;根因追查与文稿撰写由 AI 辅助;核验与发布由我本人负责。我没有工程背景 —— 若任何技术表述有误,请直接指出,我会对照工具链重新核实并更正。 本报告由 OfferKuai(Offer快)团队提交 —— 创始人:Zhaofeng(Yaming)。官网:https://www.offerkuai.com/ | 联系:contact@offerkuai.com |
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
EN / English
Summary
Opening a session causes a blank screen stuck on "Loading history…" — the client render loop never advances (
openState=loading), with the renderer at elevated CPU. The data layer is 100% healthy:our offline contract checkerpasses, the officialSessionfold +deriveMessagessucceeds, pagination is fast. The one anomaly in the session log is astep/start → assistant/message → step/endblock whosedata.turnisnull(a surface-replace marker written by a third-party edit operation in the gap between an open turn's steps). The client's render state machine cannot attribute the null-turn step to any turn → it waits forever → render loop → blank screen.This is a client-side robustness gap: the renderer should degrade (skip/ignore the unattributable block) instead of looping indefinitely on one malformed-but-persisted event. We hit this during production testing of DSH with a large session (~330k events, 724 messages); restarting DSH, clearing projcache, and rebuilding frames did not help — only rewriting the null-turn block to its owning turn (turn 95) restored the session.
Status as of
0.1.5-rc.2(re-read 2026-09-17): the open state machine is unchanged in shape. In@deepseek-ai/dsh-api-session-controller@0.1.5-rc.2(lib/client.js:1979-2006),Session.doOpensetsopenState = "loading"and has exactly two exits —"open"afterawait events.open({ maxMessages: 50 })resolves, and"error"only whenisRemoteFailure(error); any other error is re-thrown out ofdoOpenwithout setting"error", leaving the UI on"loading". An earlier note (0.1.3-alpha.1) also stands: the v2 migration may normalize some legacy null-turn blocks during upgrade, narrowing the trigger surface for newly migrated sessions — while this state machine still has no degradation path for an unattributable step that survives into the log.Honest boundaries: (1) the blank-screen loop was not re-tested end-to-end on 0.1.5-rc.2 — what was re-read is the state machine; (2) we did not trace which of the two routes (hang vs. non-remote throw) a
turn=nullblock actually takes throughSessionEventStream.open.Trigger Condition
step/start+assistant/message+step/endblock withdata.turn === null(any plugin/operation that writes an edit marker outside an open step — including while its turn is still open — can produce this shape)Proposed Fix
turn=nullstep/marker as non-renderable (skip/ignore with a warning) instead of entering a wait-for-start state that never resolves — the renderer must not loop on persisted data it cannot attributeour offline contract checkergains a rule flaggingturn=nullstep/marker blocks so affected sessions are caught offline before openingRelated
turn=nullstep block loops the render state machine at any session size. Cross-linked so the two are not read as one defect.Environment
Authorship note: reproduced and measured by me against my own store; root-cause tracing and drafting assisted by AI; verification and publication by me. I have no engineering background — if any technical claim reads wrong, please call it out; I will re-verify against the toolchain and correct.
Reported by the OfferKuai Team — Founder: Zhaofeng (Yaming). Website: https://www.offerkuai.com/ | Contact: contact@offerkuai.com
中文版 / ZH
摘要
打开会话 → 白屏卡在「载入历史…」——客户端渲染循环永不前进(
openState=loading),渲染进程 CPU 升高。数据层 100% 健康:our offline contract checker通过、官方Session折叠 +deriveMessages成功、分页很快。会话日志中唯一异常是一个data.turn为null的step/start → assistant/message → step/end块(第三方编辑操作在某个已打开 turn 的 step 间隙写入的 surface-replace marker)。客户端渲染状态机无法将该 null-turn step 归属到任何 turn → 永远等待 → 渲染死循环 → 白屏。这是客户端健壮性缺口:渲染器应对无法归属的持久化事件降级(跳过/忽略),而不是无限死循环。我们在生产环境测试 DSH 时命中(约 33 万事件、724 条消息的大会话);重启 DSH、清 projcache、重建帧均无效——只有把 null-turn 块改写为其所属 turn(turn 95)后会话才恢复。
版本现状(@
0.1.5-rc.2实读,2026-09-17):open 状态机结构未变。在@deepseek-ai/dsh-api-session-controller@0.1.5-rc.2(lib/client.js:1979-2006)里,Session.doOpen置openState = "loading"后只有两个出口——await events.open({ maxMessages: 50 })成功后的"open";以及仅当isRemoteFailure(error)时的"error";其它异常会被重新抛出doOpen而不设"error",于是 UI 没有任何状态可去,只能停在"loading"。此前的 0.1.3-alpha.1 注也仍成立:v2 迁移可能在升级时规范化部分旧数据的 null-turn 块(新迁移会话触发面收窄),而该状态机对存活进日志的无法归属 step 仍无降级路径。诚实边界:(1)白屏死循环未在 0.1.5-rc.2 上端到端重测——本次实读的是状态机;(2)我们未追查
turn=null块经SessionEventStream.open实际走的是「挂住」还是「抛非远端错误」哪条路。触发条件
data.turn === null的step/start+assistant/message+step/end块(任何在打开的 step 之外写编辑 marker 的插件/操作都可能产生此形态——包括其 turn 仍打开时)建议修复
turn=null的 step/marker 视为不可渲染(跳过/忽略 + 警告),而不是进入永不 resolved 的 wait-for-start 状态——渲染器不得对无法归属的持久化数据死循环our offline contract checker增加规则标记turn=null的 step/marker 块,让受影响会话在打开前就被离线发现相关
turn=null的 step 块在任何规模下都会让渲染状态机打转。做交叉引用,是为了别把两者读成同一个缺陷。环境
声明:本次实测与统计由我针对自己的数据完成;根因追查与文稿撰写由 AI 辅助;核验与发布由我本人负责。我没有工程背景 —— 若任何技术表述有误,请直接指出,我会对照工具链重新核实并更正。
本报告由 OfferKuai(Offer快)团队提交 —— 创始人:Zhaofeng(Yaming)。官网:https://www.offerkuai.com/ | 联系:contact@offerkuai.com
All reactions