Repository navigation
Replies: 2 comments
|
Bump, 已遇到同款问题, 尝试直接读取一个程序的错误 minidumps 时可以稳定触发, 必须再发起1次新对话, 并在新对话中告知原因 (或告知其阅读此 Discussion) 让其进行修复才可解决. const fs=await import("node:fs");const NL=String.fromCharCode(10);
const fp="G:/Users/13286/AppData/Local/CrashDumps/cstrike-online.exe.23860.dmp";
const fd=fs.openSync(fp,"r");
const head=Buffer.alloc(12*1024*1024);
const got=fs.readSync(fd,head,0,head.length,0);
fs.closeSync(fd);
const b=head;
console.log("read "+got+" bytes header");
const n=b.readUInt32LE(8),r0=b.readUInt32LE(12);const st=[];for(let i=0;i<n;i++){const o=r0+i*12;st.push({type:b.readUInt32LE(o),size:b.readUInt32LE(o+4),rva:b.readUInt32LE(o+8)});}
const mods=st.find(x=>x.type===4);
console.log("module stream: size="+mods.size+" rva=0x"+mods.rva.toString(16));
const cnt=b.readUInt32LE(mods.rva);
console.log("count="+cnt);
const rows=[];
for(let i=0;i<cnt;i++){const o=mods.rva+4+i*108;
const base=b.readBigUInt64LE(o), size=b.readUInt32LE(o+8);
const nrva=b.readUInt32LE(o+32);
let nm="?"; try{const nlen=Math.min(b.readUInt32LE(nrva),520); nm=b.toString("utf16le",nrva+4,nrva+4+Math.max(0,nlen-2));}catch(e){nm="ERR";}
rows.push(base.toString(16).padStart(8,"0")+" "+size.toString(16).padStart(7,"0")+" "+nm.split("\\").pop());}
console.log(rows.join(NL));``` |
|
这个形状我们一直在覆盖(#436 那一族),正好给你实测结论和一条恢复路径。 为什么「看不到原因」——我复现了「文件能打开」这一半 拿一份真实的 184 事件 v4 日志,按你的描述污染(在一条
恢复路径:用 dsh-session-surgeon 离线改
修复把每个孤立代理替换成 顺手补的两处:面板以前对健康码 一处边界:如果代理落在 header(Windows 上目录名可以带不成对代理,而 header 记着 安装/更新:桌面端「插件」页 → dsh-session-surgeon → 更新(等价 |
Uh oh!
There was an error while loading. Please reload this page.
Summary
任何进入会话历史的非 well-formed 文本(典型来源:把二进制数据按 UTF-16 解码,得到的孤立代理项 U+D800–U+DFFF)都会随历史进入之后的每一次模型请求。
JSON.stringify把孤立代理项输出为\udXXX转义:JSON 文本本身合法,但它不是合法的 Unicode 标量值,服务端会直接拒绝整个请求体的解析并返回 400。由于会话日志只追加、该事件永久留在历史里,这个会话之后的每一轮请求都必然失败,且产品内没有任何恢复手段,任务就此报废。
用户看到的只有一句
DeepSeek Messages request failed (400)。真实原因被丢掉了:服务端对这类失败返回的是text/plain而不是 JSON 错误信封,providerError()因此取不到error.message,回退成通用文案。Reproduction
A. 产品内最小复现
真实场景同样容易触发:读取 UTF-16 编码的二进制日志/文件、以错误编码抓取子进程输出等。本次事故的真实触发就是:
tool/resultseq 2923,工具run_code,callIdcall_00_vcdTPpSaByYwBlFnBSsm8826)。B. 供应商侧最小复现(不经过 harness)
实测结果(响应正文逐字):
把
\udc3e换成\ufffd(U+FFFD)或普通文本,同一个请求返回 200。变量已隔离。Current behavior
会话
session-b48acc80(会话格式 v4)中的实际序列:tool/callrun_code:把二进制日志按utf16le解码tool/result:正文含 54 个孤立代理项(U+DC3E、U+DBE8、U+DF76、U+D8D1 …)写入历史step/start→assistant/attemptfinish.reason = error,INVALID_REQUEST/ 400turn/endturn 19请求在解析请求体阶段即被拒绝,未到达模型;失败是亚秒级的(本次三轮分别为约 0.4 s / 0.4 s / 0.7 s),与重试、限流、上下文长度都无关。
对会话日志逐字节扫描后确认:整个会话中只有 seq 2923 这一条记录含孤立代理项,共 54 个码位单元;它出现的位置正好在首次 400 的那一步之前,而该步之前的请求全部成功。
Expected behavior
text/plain的响应体里带着唯一可诊断的信息,现在被回退文案吞掉了;用户只看到(400),无法据此做任何判断或自救。.jsonl.zstd会话日志再写回(本次事故就是这样才救回来的)。对终端用户这条路径不存在,等于数据报废。Environment
0.2.0-rc.2,ProductVersion0.2.0.0)deepseek-ai/deepseek-harness@639ed01(release-dsh-0.2.0-rc.2)desktop;工作目录E:\Agent测试;会话格式 v4deepseek-account,modeldeepseek-flash,endpointhttps://api.deepseek.com/anthropic/v1/messages,anthropic-version: 2023-06-01影响面
(400),用户无从知道是内容问题。根因定位
请求构造处没有做 well-formed 归一化,文本原样进入 wire:
packages/llm/llm-deepseek/src/serialize.ts:30— assistant 的 text 块:return { type: 'text', text: block.text }packages/llm/llm-deepseek/src/serialize.ts:65— 用户 / 工具结果的 text 块:return block.text ? [{ type: 'text', text: block.text }] : []注意
llm-deepseek-account(providerdeepseek-account)与llm-deepseek-api-key(providerdeepseek-official)都只负责鉴权与配置,各自调用@deepseek-ai/dsh-llm-deepseek的registerDeepSeekProvider,共用这一份serialize.ts;因此两条 DeepSeek 路由同时受影响,修一处即可覆盖两条。错误信息被吞掉的位置:
packages/llm/llm-deepseek/src/transport.ts:25—const message = typeof error.message === 'string' ? error.message : 'DeepSeek Messages request failed (' + status + ')'packages/llm/llm-deepseek/src/transport.ts:33—status === 400映射为INVALID_REQUEST响应体是
text/plain; charset=utf-8、JSON.parse失败,envelope.error为undefined,于是回退文案生效,服务端原文被丢弃。仓库内其实已经识别过这一类危险:
packages/core/tools/src/py-types.ts:191附近明确注释了「Python 源码必须 UTF-8 可编码,而落单代理项不是,compile()会抛UnicodeEncodeError: surrogates not allowed」,并对不可打印字符与U+D800–U+DFFF落单半区做了转义处理。同样的规则没有覆盖模型 wire 这条路。建议修复
1)请求构造处做 well-formed 归一化(必需)
在
serialize.ts里对每个出站字符串统一归一化,例如在构造WireBlock/WireInput的唯一入口处调用String.prototype.toWellFormed()(engines 为^22.19 || >=24,可用),或按py-types.ts的既有做法显式替换落单半区为 U+FFFD。放在发送时而不是写入会话事件时有一个额外好处:已经中毒的既有会话在恢复时会自动痊愈,不需要人工改写日志。代价是日志本身仍保留不可编码文本,与「Model-visible ⟺ logged」不冲突(模型看到的是 U+FFFD,归一化是确定性的)。若更希望日志本身不含不可编码文本,则应在工具结果进入会话事件时归一化,但那样无法自动修复既有会话;两者可以都做。
2)非 JSON 错误体回退到原文(必需)
transport.ts在缺少 JSON 信封时,应把响应体原文(截断到合理长度)并入错误消息,而不是只回(400)。本次问题之所以对用户完全不可诊断,就是这一步。3)提供产品内恢复路径(建议)
一旦历史被污染,用户目前无路可走。任何形式的回退 / 剪枝 / 修复入口都能把「任务报废」降级为「损失一轮」。
4)无密钥回归覆盖(建议)
在 snapshot 或 e2e 中固定一条「工具结果含孤立代理项」的用例,断言出站请求体不含任何不可编码码位且不被服务端拒绝。
说明:本报告只包含最小化技术证据;会话正文与业务内容未附。会话 id 仅用于可追溯,可删除。
All reactions