Replies: 3 comments
|
你的第 1 条(写入端存下了它自己的读取端认定为损坏的记录)是这份报告里最有价值的观察。补两件:那个"退化块"的来源可能不是模型,以及这个写读不对称属于哪个类。 一、
|
| 帖 | 存进去的东西 | 读出来时 |
|---|---|---|
| 本帖 #4704 | tool/result 的 source.callId 为空串 |
resume 时 assertMessageEventShape 判定损坏,会话打不开 |
| #2915 / #1627 / #1703 | 截断时 replay state 与持久内容块数不一致 | 每次重放都被拒,会话永久无法继续 |
| #740 | 中断留下带 tool_calls 却缺配套 tool 消息的历史 |
deriveMessages 忠实投影,每次请求都被 API 拒,会话报废 |
共同点:写入是宽松的、读取是严格的,而不一致的代价全部由用户承担,且总是在事后才显现。
建议把第 1 条提成一个独立条目,标题写成后果导向,例如 "Session writer accepts records its own loader rejects — corruption is only discovered at resume"。理由很实际:现在它挂在一个讲某个具体退化块的帖子里,很容易被修成"过滤掉空 callId"这一个特例,而下一个写读不对称照样会来。
(一个更根本的提法:写入路径应该用与加载路径同一套 schema 做校验,或者至少在写入时做一次同样的断言并 fail loud。这样坏数据在产生的那一刻就被发现,而不是几百个事件之后。)
三、你的第 2 条(级联 400)其实是第 1 条的下游
the empty
function.namepoisons every later request too
这一条和 #740 是同一个机制:一条不合规的历史进入持久层之后,之后每一次请求都会带上它。 所以它不需要单独修——只要第 1 条(别存进去)成立,级联就不会发生。值得在原帖里点明这个从属关系,免得被当成两个独立 bug 分头处理。
四、给现在被卡住的人
你提到"after manual data repair, user continues working"。补一句给搜到这帖的人:在修复合入前,唯一的自救就是手工编辑那条记录——但请先完整备份会话目录再动手,而且别只删那一条 tool/result:按你的描述,坏的是三件一套(assistant/message 里的块、tool/call 的 callId:""、以及那条 tool/result),只删其一可能触发另一个校验。
(更稳的做法是从坏掉那一轮之前分叉出一个新会话继续——这是 #1627 那边 @denial123789 对同类"会话被写坏"给出的建议,比手工编辑事件链安全。)
边界与利益相关
我们不修 DSH 自家组件——session persistence、assertMessageEventShape、流式累加都在 DSH 里。上面全是跨帖串联与提法建议;我没有复现过你这条,第一节的"可能是累加器"是一个待你用日志验证的假设,不是结论。引用的几帖都是别人的报告。
利益相关:我维护 pi2dsh(Pi 生态兼容层)。这条不推销——会话一旦被写坏,装任何第三方插件都不会让它重新通过校验。
|
I checked the reported writer/loader asymmetry against current
So filtering the known empty continuation overwrite is necessary, but it is not the whole durability fix. A regression should also prove that the same invalid result is rejected before live append/persistence and after restore. For operators, avoid editing the live I folded the source paths, correlated evidence set, safe containment, and regression gates into this independent runbook: https://github.com/sandbaseai/deepseek-harness-handbook/blob/main/docs/en/troubleshooting/streamed-tool-call-empty-identity.md Affiliation: I maintain this SandBase handbook. It is an independent community project, not official DeepSeek AI documentation. |
|
This is two separate defects that happen to arrive together, and separating them matters because fixing either one alone still leaves you broken. The empty
|
Uh oh!
There was an error while loading. Please reload this page.
Environment
@deepseek-ai/dsh@0.1.1-rc.2(npm global install; all inner@deepseek-ai/*packages also 0.1.1-rc.2)deepseek-v4-flash-vision-exp, reasoning effort highdsh web)Summary
Two coupled problems:
Writer/loader asymmetry bricks resume. When the streamed assistant response contains a tool-call block whose
idandnameare empty strings (arguments fully populated), the harness persists this as-is (assistant/messageblock,tool/callevent withcallId:"", plus a synthesizedtool/resulterror message whosesource.callIdis also""). On the next resume, loader validation rejects exactly this record:assertMessageEventShape(packages/core/session/src/index.ts) requires every storedtool/resultmessage to havesource.kind === 'tool'with a non-empty stringcallId. So the writer happily stores state its own reader considers corrupt. During live operation nothing re-validates, so users only discover this later, at resume time.Cascade 400 from the provider. Because history is replayed into subsequent requests, the empty
function.namepoisons every later request too — the following turns end with:This matches how it surfaced in the UI ("本轮运行失败").
Observed timeline (real session)
{ id:"", name:"", arguments:"{...search-pattern...}" }; harness logs trio of corrupt events; turn ends OKSessionPersistenceCorruptionErroron thattool/resultargumentspopulated,id/nameempty); same cascade 400 on the next requestThe recurrence (twice in one day, different tools: one search-like call, one file-write call) suggests the trigger sits in the stream path — possibly the relay's SSE translation of tool_call deltas under long context, but the harness recording raw empties unguarded is what turns a glitch into persistent corruption.
Evidence (trimmed)
Rejected by (
packages/core/session/src/index.ts,assertMessageEventShape):Note:
interruptedTurnClosers(packages/core/session/src/repair.ts) only repairs an open crash tail — it does not catch mid-log corruption like this, so there was no self-heal path.How I recovered (for anyone hitting the same thing)
Locate the log:
~/.dsh/sessions/<slug-of-workspace-cwd>/<session-id>/session.jsonl.zstd.Decompress (zstandard), rewrite the degenerate trio to self-consistent synthetic values,
e.g.
id/callId/toolCallId = "repaired-empty-call-<seq>",name = "__missing_tool__".All three must share one id: the assistant block
id, thetool/call.callId,and the
tool/result.message.source.callId+ innertoolCallId.Rebuild the artifact honoring the physical frame layout (this is important):
.zstdfile is a concatenation of independently compressed zstd frames.\n(enforced by
assertZstdHeaderFrame/readFirstZstdLine,packages/session/session-persistence-jsonl/src/index.ts).zlib.zstdCompress(..., { params: { ZSTD_c_checksumFlag: 1 } })or CLI equivalentzstd --check.Restart
dsh webbefore verifying — a running instance holds the prepared session in memoryand will write stale state back over your repair.
Gotcha worth flagging: my first attempt re-encoded the whole log as ONE zstd frame. Resume-time
full reads would likely pass, but the sidebar listing path (
readFirstZstdLine) hard-fails such afile ("corrupt Zstandard session log: first frame is not exactly one header line"), and because …
Secondary finding: session listing has no per-item error isolation
… the enumeration that feeds the web sidebar runs without isolating per-session failures.
With just one unreadable log present, every workspace's session history disappeared from the
sidebar at once, silently. Moving the single offending directory away instantly restored all lists.
It would help to skip+warn per broken log instead of failing the whole listing, and/or expose a
validation/repair command for stored sessions.
Suggested fixes
tool-call blocks / call events, synthesize deterministic placeholders when
idornameismissing (e.g.
call-missing-<seq>, name__missing_tool__) before anything is appended.This keeps the durable log validator-valid AND keeps serialized history provider-valid
(no more cascade
INVALID_REQUESTon later turns); the model still sees an explicitunknown tool "__missing_tool__"outcome and can recover.interruptedTurnClosers-style repair handle mid-logdegenerate trios, or provide an official
dsh session doctor/repaircommand.All reactions