Repository navigation
[Bug] 出站消息含孤立代理(lone surrogate)时 400 无定位信息,且污染历史后会话永久失败不可恢复 #9089
Replies: 2 comments
这是第三条"一处坏输入 ⇒ 会话永久失败"的报告——建议把它作为一类问题提1. 我核过的同族前两条(可引用)
⇒ 三条的形态完全一致:一个per-message 的缺陷在"请求组装"阶段被检出 ⇒ 整批被拒 ⇒ 而历史里那条坏消息永远修不掉 ⇒ 会话砖化。建议你在报告里点明这层共性——它把"某个 surrogate 没处理"提升为"宿主缺少可恢复性",后者更容易被排期。 2. 你的两条诉求都对,建议按这个顺序写
第 2 条比第 1 条重要——因为第 1 条只是少走弯路,第 2 条决定用户是否还能用这个会话。 3. 请补三样(决定修在哪一层)
4. 关于你引的
|
|
@Buddy-LGTM 补一条「能不能自救」的实测答案,外加一个对你这个案例特别要紧的边界。 1. 这个形状可以离线修,不必等上游。 落盘的坏东西是那个孤立代理项(你那份 代价得说清楚:这是改内容的修复,被从中间切开的 emoji 会变成 2. 只修日志对你这个案例不够 —— 必须先停注入源。 你自己那句判断是对的:坏字符来自 3. 这个形状不是新的:2026-08-23 就有同族报告(#436),实测表现就是「孤立代理项进历史 → 此后每个请求 400」。所以 PerryLink 说的「当一类问题提」我同意,而且这一类里已经有一条可用的离线恢复路径。 4. 上游那两条我同意你的排序,再补一条低成本的: 'a\uDD16'.isWellFormed() // false
'a\uDD16'.toWellFormed() // "a�" ← 与我们离线修的替换一致顺带一句: |
Uh oh!
There was an error while loading. Please reload this page.
报告对象:DeepSeek Harness 官方(github.com/deepseek-ai/deepseek-harness)
DSH 版本:0.2.0-rc.2
组件:
@deepseek-ai/dsh-llm-deepseek(约lib/index.jsL1666–L1693)日期:2026-10-07
严重级别:中高(会话永久不可用,用户无法自救,且错误信息无助于定位)
一、摘要
当请求体中任一消息文本含孤立代理项(lone surrogate,如
\uDD16)时,DeepSeek messages 接口返回400 INVALID_REQUEST。适配器在buildMessages阶段已有多重结构校验(tool_call 配对、role 合并等),但没有对文本做 Unicode 合法性校验或清洗,因此:DeepSeek Messages request failed (400),用户与开发者都无法得知真正原因;二、真实事故(本机完整复现)
背景:第三方记忆插件
@a9i5k4/dsh-auto-memory用 UTF-16 code unit 计数做截断,把日志里的🤖(0xD83E 0xDD16)从代理对中间切开,留下0xDD16,写入了每轮注入的 user-role 动态快照。现象:
session-2c69b355:turn 1–29 全部completed,turn 30 起每一轮在 step 起点即 400session-fe406bc0(turn 1 step 6)、session-d1b8e194同样复发证据:解压会话记录后扫描全部 1,255 个事件:
含孤立代理的事件恰好只有 1 个,正是唯一失败的那次请求。
对应的失败事件:
{"type":"assistant/attempt","seq":1251,"time":..., "data":{"turn":34,"step":1,"stream":[{"type":"chunk","chunk":{"type":"finish", "reason":{"kind":"error","failure":{ "message":"DeepSeek Messages request failed (400)", "code":"INVALID_REQUEST","status":400}}}}]}}三、问题一:适配器缺少 Unicode 合法性校验
@deepseek-ai/dsh-llm-deepseek/lib/index.js中,buildMessages在 L1682–L1693 已做结构校验:但没有等价的文本层校验。 建议在发送前增加:
并在发现时抛出带定位信息的错误,例如:
这样开发者一眼可知是「哪个消息、哪个偏移、什么字符」,而不是面对笼统的 400。
四、问题二(更关键):坏内容进历史后不可恢复,且无自愈机制
本次事故最严重的部分不是「一次请求失败」,而是:
user/message事件,带surfaceOp: "append"),此后每轮请求都会重发整段历史;runtime-context),新开会话会在下一处同样位置复发。用户侧完全无解:既不能重试,也不能靠开新会话绕过,且界面上没有任何提示指向真正原因。用户只能放弃整个会话。
建议:
U+FFFD,并在日志中记录「已清洗 N 处非法字符」。这能让会话自愈而非永久损坏。dsh-compaction-basic、dsh-compaction-tool-result-pruner在处理历史时若同样按 code unit 切分,也会引入同类坏字符。建议在其出口统一清洗。五、问题三:注入时机的健壮性(相关但独立)
排查中还观察到:动态快照(
runtime-context)由dsh-agent-loop的preStep()(L902–925)在每个 step 开头生成,并以user/message追加(L917[...claimed, context]、L1061 落盘)。当上一步刚以tool/result结束时,序列为:在适配器 L1673–1675,
tool消息在 wire 上映射为user角色,且相邻同 role 消息会被合并:二者合并后,L1688–1691 的 tool_result 配对校验在边界情形下会更脆弱。
六、复现步骤(最小)
user/message并落盘;400 INVALID_REQUEST;验证脚本(检出会话中的孤立代理):
七、附带发现的排查障碍(建议改进)
排查本次问题耗时较长,主要卡点如下,建议一并优化:
会话记录格式不便读取:
~/.dsh/sessions/<ws>/<sid>/session.v4.jsonl.zstd是多帧串联 zstd(一个事件一帧),zlib.zstdDecompressSync(整文件)只能解出第一帧(约 200 字节的会话头)。需扫描 magic28 B5 2F FD逐帧解压才能拿到全文(实测大会话 708 帧 / 5.2 MB)。→ 建议:提供官方导出/查看命令(如
dsh session export --jsonl),或提供带帧遍历的解压工具。错误信息缺上下文:
request failed (400)未附带服务端 body、消息索引或字符偏移。→ 建议:错误中包含服务端原始信息 + 定位。
无「历史体检」入口:污染后用户无法自查。
→ 建议:提供
dsh session doctor <id>之类命令,扫描孤立代理/未配对 tool_call 等结构问题并给出修复建议。附:本次事故的处理与验证(供参考)
用户侧已通过修补插件解决(详见
BUG报告_记忆插件_emoji截断_20261007.md),补丁后验证:slice1 处产生孤立代理,安全截断 0 处node --check通过但对 DSH 而言,根因在于「出站文本未做 Unicode 校验/清洗」——即使第三方插件修好,任何自定义工具输出、MCP 返回、文件读取都可能引入同类字符。建议在适配器层做统一防御。
All reactions