Bug Fix #4240
yun-petrel
started this conversation in
General
Bug Fix
#4240
Replies: 1 comment
|
这是官方 dsh-session-surgeon 现在不会给空 callId 编一个假 id(那会把后续 tool 对账修得更假)。它可以 dsh plugin --profile web add "github:xiaoshenming/dsh-session-surgeon#main"空 callId 这条还得官方在 |
0 replies
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Uh oh!
There was an error while loading. Please reload this page.
DeepSeek Harness 会话持久化损坏(SessionPersistenceCorruptionError)现象与解决办法
1. 现象(Symptom)
启动 Web UI 后(
pnpm dsh web或桌面DeepSeek-Harness.bat),打开部分历史会话时加载失败,控制台/前端报错:受影响的会话(5 个中 3 个损坏):
session-b6183a9e-...tool/result空callIdsession-e4603dce-...session-6ae16454-...session-f3f156d8-...、session-2c957aeb-...会话文件实为目录:
C:\Users\zou_c\.dsh\sessions\--E-Project--\<sessionid>\session.jsonl.zstdnode:zlib的zstdCompressSync,每帧带ZSTD_c_checksumFlag)。assertZstdHeaderFrame校验),其余帧为事件行。2. 根因(Root Cause)
完整调用链:
local/1m发出一个工具调用块id为空的调用:{ name: "", callId: "" }。packages/core/tools/src/index.ts的toolErrorResult()(L1870)将该失败转为错误结果(
ToolNotFoundError,调用点 L1439 / L1547 / L1769 / L1838)。packages/core/agent-loop/src/tool-calls.ts:276调用createToolResultMessage({ callId: block.id, ... }),把空""直接传入。packages/llm/llm/src/message.ts的createToolResultMessage用该callId同时填充source.callId和content[0].toolCallId→ 二者都为空串。tool/result落盘后,重载时被assertMessageEventShape(
packages/core/session/src/index.ts:337-340,要求sr.callId为非空字符串)拒绝 → 报message must have tool source(internal)→ 整个会话损坏。一句话:Harness 把模型产出的空
callId原样写进了tool/result,而它自己的重载校验不允许空callId—— 即 Harness 写出了连它自己都校验不过的日志。3. 解决办法
分两层:数据层(修复已损坏的历史)+ 源码层(根治复发)。
3.1 数据层修复(已损坏会话)
对损坏的
tool/result与其关联的tool/call,写入一致的非空合成callId(repair-<seq>),并保留原始多帧 ZSTD 结构(仅重压缩含损坏事件的帧,其余帧字节原样保留),以兼容首帧 header 约束。
C:\Users\zou_c\AppData\Local\Temp\opencode\repair-v5.js(通用多会话修复)。verify-all.js(独立只读校验,遍历全部会话统计failures)。session.jsonl.zstd.repair-bak(原始未修复多帧文件),可秒级回滚。校验结果(全部 5 个会话):
3.2 源码层根治(防止复发)
只改唯一的生产构造点
packages/llm/llm/src/message.ts的createToolResultMessage,对空callId做兜底。所有成功/失败结果(以及迁移、修复路径)都经此函数,一处修改即全覆盖。
改动 1 — import(L3):把
CallId由 type-only 改为 value 导入:改动 2 — 函数体(L231 起):空
callId回退为合成 UUID,且source.callId与content[0].toolCallId保持一致:export function createToolResultMessage(input: ToolResultMessageInput): ToolResultMessage { + // A model may emit a tool call whose id is empty (e.g. behind a local + // proxy). Persisting a tool/result whose source.callId is empty fails the + // harness's own reload validation and corrupts the session, so fall back to + // a synthesized id that keeps the result self-consistent and loadable. + const callId = input.callId === '' ? CallId(globalThis.crypto.randomUUID()) : input.callId return createUserMessage({ - source: { kind: 'tool', callId: input.callId }, + source: { kind: 'tool', callId }, content: [{ type: 'tool-result', - toolCallId: input.callId, + toolCallId: callId, content: input.content, isError: input.isError, }], }) }重建(本机
git/pnpm不在默认 PATH,需先补齐):4. 验证(Verification)
构建:
pnpm run build全量通过(web-frontend 末步built in 4.78s,pipeline 无报错)。编译产物功能验证(针对
lib/types/message.js跑脚本):callIdsource.callIdcontent[0].toolCallId"""real-call-1""real-call-1"(原样透传)"real-call-1"→ 空 id 不再产生空
callId的tool/result,且二者始终一致。历史加载:重启后全部 5 个会话 0 failures(见 3.1)。
5. 重启步骤
历史会全部正常加载;今后即便
local/1m再吐空 id 的工具调用,也不会再损坏会话。6. 注意事项
D:\deepseek-harness源码内。pnpm update/ 从 GitCode 镜像重新git clone会覆盖改动,需按第 3.2 节重新打(仅 2 处,约 10 行)。session.jsonl.zstd.repair-bak;若需撤销,删除修复后的session.jsonl.zstd、把.repair-bak改名回session.jsonl.zstd即可。assertMessageEventShape的空callId校验,而是从来源保证callId永远非空,保持了会话完整性约束。
All reactions