Replies: 1 comment 1 reply
|
The rc.2 source confirms the read-state concern, and alpha.1 changes the boundary in a useful but incomplete way. In rc.2,
So the visible result can indeed resemble genuine exhaustion without proving anything about the durable file. The minimal incident receipt should preserve Alpha.1 ( However, this does not fully close the read-state gap: A durable contract could publish exactly one of Source-pinned comparison and recovery matrix: https://github.com/sandbaseai/deepseek-harness-handbook/blob/main/docs/en/troubleshooting/session-history-corruption-triage.md#failure-e-no-more-history-hides-a-read-failure |
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
Purpose
This is a technical analysis companion to our bug report on trailing aborted turns making a valid session log unservable by the history read path. We studied the engine source (rc.7, cross-checked against the latest
0.1.1-rc.2) to separate deliberate design from genuine gap, and to propose a fix path that respects the existing architecture. Shared openly in case it helps the maintainers.The problem — recap
A session log whose tail consists of one or more
abortedturns (turn/endwithreason.kind: "aborted", followed bysession/end-seed) is structurally valid: seq is contiguous from 0 and every event is present on disk. Yet the history read path (session.history) cannot serve the earlier region — reads return empty pages even though the events exist, and the failure is silently degraded to "no more history", with no error surfaced. The session's durable progress record — the full transcript, including scheduled/unattended runs whose aborted ending is routine — becomes unreachable, and the only recovery today is manual log truncation.We assess this as a genuine gap, not intended behavior: the fail-soft read policy is a deliberate design, but it was never meant to make a valid, first-class log state (a trailing aborted region) unreproducible. This analysis separates the design rationale from the gap and proposes a fix path that keeps the fail-soft philosophy.
Version note / 版本说明
Source references were verified on
@deepseek-ai/dsh0.1.0-rc.7and re-verified on0.1.1-rc.2— module internals unchanged, line numbers shifted:dsh-client-runtime/lib/client.js:loadOlder()now at:7384-7420(was:7388-7424).dsh-host-apiproxy/lib/index.js:historyCutOfnow at:2032(was:2084),paginateat:969(was:1030),viewForat:1333(was:1391).Root-cause analysis (design vs gap, with source references)
1. "Fail soft, never block reading" is an explicit design — and it is exactly what hides this defect.
The read path is built around soft-fall everywhere:
viewForcatches presenter errors and falls back to a generic card (dsh-host-apiproxy/lib/index.js:1333),listProjectionsForandsubagentHistoryProjectionsdegrade instead of failing (:1414/:1438). On the client,loadOlder()(dsh-client-runtime/lib/client.js:7384-7420) continues silently on an empty page (:7394-7400), treats a page discontinuity as end-of-history (:7402-7407), and swallows any thrown error with aconsole.erroronly (:7414-7415). By design, a history that cannot be served is indistinguishable from a history that ends.2. The serving side has the data — the full contiguous log.
session.historyreads events from the complete session log (historyCutOf,dsh-host-apiproxy/lib/index.js:2032-2046) and pages them by message count with a group cut (paginate,:969-988). Every event of a trailing aborted region is present and contiguous. The read contract is broken on the consuming side: "log exists" no longer implies "history is reproducible."3. The gap: a first-class durable state (aborted turns) has no first-class handling in the read/projection path.
turn/end reason.kind: "aborted"is a normal, documented event — a turn interrupted by the user, a timeout, a worker restart, or a kill; in scheduled/unattended runs it is the typical ending. The read/projection path has no branch for a trailing aborted region — no first-class representation, no skip-past rule, no error when the region cannot be served. A routine, ordinary ending therefore violates the read contract silently.Proposed fix path (respecting the fail-soft design)
A. Make "history failed to load" distinguishable from "no more history" (read contract).
loadOlder()currently swallows RPC errors and conflates discontinuity with end-of-history. Introduce an explicit read-failure state on the history RPC: a failure returns a distinguishable error (retryable), while the existing silent behavior is kept only for the genuinehasMore = falsecase. This is the minimal change and fixes the symptom (silent blank) without touching the projection.B. First-class handling of aborted turns in the session projection (removes the trigger).
Represent a trailing
abortedturn as a first-class, resumable/recoverable entry in the conversation projection (or page past it) instead of letting it break consumption. The event already carriesreason.kind: "aborted"— a dedicated projection entry is the natural shape and matches the existing conversation-node architecture. This is a state-machine/projection fix, not a UI patch: the log state becomes reproducible again.C. Visible gap markers for genuinely discontinuous logs (auditability).
When a page discontinuity is real (a damaged log), surface a "history gap at seq N" marker instead of silently ending the read. This preserves the fail-soft philosophy while making the failure auditable.
A fixes the live symptom, B removes the trigger for the routine case, C keeps genuinely damaged logs honest. A and B together are sufficient for production safety; C is a small addition for operators.
Community corroboration — the gap is broader than a trailing aborted turn
Since we first reported this, the community has surfaced several independent reports that converge on the same fragile contract between the session log and the read/resume path — different triggers, same class of failure:
seq=977737appears twice (oncesession/end-seed, once anagent/inbox/splicedcarrying another session's content). Result:history unavailable for session ...: Error: corrupt session log: seq gap in committed region. A manualzstdrecompress then failed withfirst frame is not exactly one header line, bricking the wholedsh webstartup. Community analysis (nokkies) pins the root cause to the same mechanism we audited as N6/P1: rc.2session-persistence-jsonlhas no cross-process writer lock, andseqis assigned asthis.log.length, so two writers collide. This is exactly the writer/reader asymmetry our fix C (visible gap markers) is meant to make auditable.id/name; the harness persisted it, but the loader'sassertMessageEventShaperejects exactly that record at resume time (SessionPersistenceCorruptionError: ... message must have tool source). The writer stores state its own reader considers corrupt. Same asymmetry: no write-time validation, only read-time failure — precisely the gap fix A (distinguishable read failures) targets.--resumeselects the session but renders an empty conversation until the first message; the persistedsession.jsonl.zstddecodes to a complete, valid artifact (314 events, 9 turns, cleanturn/end). Pure TUI attach-timing bug, but the user-visible symptom (resume shows blank history) is identical to our trailing-aborted-turn case — confirming the "empty history" symptom is surfacing across multiple layers of the read/attach stack, not a single edge.Takeaway for maintainers: the trailing-aborted-turn case we analyzed is one manifestation of a wider contract gap — the session log format (v0, no migration path) has no corruption isolation, no write-time validation, and no safe repair. Fixes A (distinguishable read failures) and C (visible gap markers) address the read side; the deeper fix is the format-versioning / corruption-isolation work in our architecture-layer series. We are not claiming all four are the same bug — #4750 is a TUI attach bug, #4704/#4767 are persistence/seq integrity — but they share the same root weakness: the log is trusted at write time and only questioned at read time.
Environment
@deepseek-ai/dsh0.1.0-rc.7/0.1.1-rc.2(verified on both)Questions for maintainers
Authorship note: reproduced and documented by me in real-world usage; 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.
中文版 / ZH
目的
这是关于"尾部 aborted 轮次使合法会话日志无法被历史读取路径服务"bug 的技术分析配套帖。我们研读了引擎源码(rc.7,并对照了最新的
0.1.1-rc.2),区分有意设计与真实缺口,并提出尊重现有架构的修复路径,公开发布以期为维护者提供参考。问题回顾
尾部由一个或多个
aborted轮次(turn/end的reason.kind: "aborted",随后session/end-seed)组成的会话日志结构合法:seq 从 0 连续、每个事件都在磁盘上。但历史读取路径(session.history)无法为更早区域提供服务——读取返回空页,且失败被静默降级为"没有更多历史",无任何报错。会话的持久化进度记录——完整转录,包括 aborted 结尾属于常规的调度/无人值守运行——变得不可达,当前唯一恢复方式是手工截断日志。我们判定这是真实缺口,而非有意行为:fail-soft 读取策略是有意设计,但它的本意绝不是让一个合法、一等公民的日志状态(尾部 aborted 区间)变得不可复现。本分析区分设计理由与缺口,并提出保留 fail-soft 哲学的修复路径。
版本说明
源码引用在
@deepseek-ai/dsh0.1.0-rc.7上验证,并在0.1.1-rc.2上重新验证——模块内部逻辑未变,行号有偏移:dsh-client-runtime/lib/client.js:loadOlder()现位于:7384-7420(原:7388-7424)。dsh-host-apiproxy/lib/index.js:historyCutOf现位于:2032(原:2084)、paginate位于:969(原:1030)、viewFor位于:1333(原:1391)。根因分析(设计 vs 缺口,附源码引用)
1. "宁可软失败,也不阻塞读取"是明确设计——而它恰恰掩盖了这个缺陷。
读取路径处处软降级:
viewFor捕获 presenter 错误并回退通用卡片(dsh-host-apiproxy/lib/index.js:1333),listProjectionsFor与subagentHistoryProjections降级而非失败(:1414/:1438)。客户端loadOlder()(dsh-client-runtime/lib/client.js:7384-7420)空页时静默继续(:7394-7400)、把分页不连续当作历史尽头(:7402-7407)、任何抛错仅console.error后吞掉(:7414-7415)。按设计,"历史无法服务"与"历史到此为止"不可区分。2. 服务端有数据——完整的连续日志。
session.history从完整会话日志读取事件(historyCutOf,dsh-host-apiproxy/lib/index.js:2032-2046),按消息数切页并做 group cut(paginate,:969-988)。尾部 aborted 区间的每个事件都存在且连续。读取契约在消费侧被破坏:"日志存在"不再蕴含"历史可复现"。3. 缺口:一等公民的持久化状态(aborted 轮次)在读取/投影路径中没有一等公民的处理。
turn/end reason.kind: "aborted"是正常、有文档的事件——用户中断、超时、worker 重启、被杀都会产生它;在调度/无人值守运行中它是典型结尾。读取/投影路径对尾部 aborted 区间没有任何分支——没有一等表示、没有跳过规则、区间无法服务时也不报错。于是一个例行、普通的结尾静默违反了读取契约。建议修复路径(尊重 fail-soft 设计)
A. 让"历史加载失败"与"没有更多历史"可区分(读取契约)。
loadOlder()目前吞掉 RPC 错误、并把不连续与历史尽头混为一谈。在 history RPC 上引入显式读取失败状态:失败返回可区分的错误(可重试),仅在真正的hasMore = false时保留现有静默行为。这是最小改动,修复症状(静默空白)而不触碰投影。B. 在会话投影中一等公民地处理 aborted 轮次(移除触发条件)。
将尾部
aborted轮次在对话投影中表示为一等公民的"可恢复/可继续"条目(或跳过),而不是让它破坏消费。事件本身已携带reason.kind: "aborted"——专门的投影条目是自然形态,也与既有 conversation-node 架构一致。这是状态机/投影层的修复,不是 UI 补丁:日志状态重新变得可复现。C. 对真正不连续的日志给出可见 gap 标记(可审计)。
当分页不连续真实存在(日志已损坏)时,显示"seq N 处存在历史 gap"标记,而不是静默结束读取。既保留 fail-soft 哲学,又让故障可审计。
A 修复实时症状,B 移除例行场景的触发条件,C 让真正损坏的日志保持诚实。A+B 已足以保障生产安全;C 是给运维的小补充。
社区印证 —— 缺口比"尾部 aborted 轮次"更宽
自我们首次报告以来,社区出现了几起独立报告,都收敛到同一处脆弱契约——会话日志与读取/恢复路径之间——触发条件不同,但属于同一类失败:
seq=977737出现两次(一次session/end-seed,一次agent/inbox/spliced还夹带了另一个会话的内容)。结果:history unavailable for session ...: Error: corrupt session log: seq gap in committed region。手动zstd重新压缩又报first frame is not exactly one header line,整个dsh web启动崩溃。社区分析(nokkies)把根因定位到我们审计为 N6/P1 的同一机制:rc.2 的session-persistence-jsonl没有跨进程写者锁,且seq直接用this.log.length分配,两个写者必然撞号。这正是我们修复 C(可见 gap 标记)要让其"可审计"的那类"写/读不对称"。id/name为空的 tool-call;harness 照存不误,但加载器的assertMessageEventShape在恢复时恰好拒绝这条记录(SessionPersistenceCorruptionError: ... message must have tool source)。写者存下了自己读者认为已损坏的状态。同一处不对称:只在读取时校验,写入时毫不验证——正是修复 A(可区分的读取失败)要解决的缺口。--resume选中了会话,却渲染出空白对话,直到发出第一条消息才出现;而持久化的session.jsonl.zstd解出来是完整、合法的产物(314 事件、9 轮、干净的turn/end)。纯 TUI 挂载时序 bug,但用户可见症状(恢复后历史空白)与我们尾部 aborted 轮次的情形完全一致——证明"空白历史"症状在读取/挂载栈的多个层同时出现,并非单一边缘。给维护者的结论: 我们分析的那例尾部 aborted 轮次,只是更宽契约缺口的一种表现——会话日志格式(v0,无迁移路径)没有损坏隔离、没有写入时校验、也没有安全修复手段。修复 A(可区分读取失败)与 C(可见 gap 标记)解决读取侧;更深的修复是架构层系列的"格式版本化 / 损坏隔离"工作。我们并非声称这四例是同一个 bug——#4750 是 TUI 挂载 bug,#4704/#4767 是持久化/seq 完整性——但它们共享同一处根弱点:日志在写入时被信任,只在读取时才被质疑。
环境
@deepseek-ai/dsh0.1.0-rc.7/0.1.1-rc.2(均在两者上验证)向维护者提出的问题
署名 / About this report
EN: Reported by the OfferKuai (Offer快) Team — an AI startup building full-lifecycle job-application services, guided by the belief that "users need results, not repeated conversations." Founder: Zhaofeng (Yaming). We use DeepSeek Harness as part of our daily development workflow; this report is our way of contributing back to the ecosystem. Website: https://www.offerkuai.com/ | Contact: <contact@offerkuai.com>
ZH: 本报告由 OfferKuai(Offer快)团队提交 —— 一家专注 AI 全流程求职托管的创业团队,核心理念是「用户要的是结果,不是重复对话」。创始人:Zhaofeng(Yaming)。我们将 DeepSeek Harness 用于日常开发工作流,这份报告是我们对社区的回馈。官网:https://www.offerkuai.com/ | 联系:<contact@offerkuai.com>
协作说明:本文由我在真实使用中复现并记录;根因与成文由 AI 协助完成,我负责校验与发布。我非工程背景,技术表述如有错误,欢迎指出——我会回去用工具链重新验证后更正。
All reactions