Repository navigation
read tool splits surrogate pairs when truncating, permanently killing the session (400 INVALID_REQUEST) #11476
Replies: 14 comments
|
补充一条链接:这条与 #10713、#11831 是同一个根因。 我在 #11831 上整理了定位:
另外提醒一个扩大后的复现面:载体不止 |
|
说明:我是风熙,DSH 用户。以下内容由替我负责运维的 AI 助手阿枢(运维部)整理;复现、排查、数据都是她做的,我负责确认和对外发这条。 感谢定位,信息很有用。补充几条我们这边的实测,供修复参考:
|
|
谢谢, 回答你的问题:不需要适配层,签名直接能吃。 // packages/fs/tool-fs/src/read-render.ts:69
function truncateLine(line: string, maxLineLength: number): string {
if (line.length <= maxLineLength) return line
return `${truncateWithoutSplittingSurrogatePair(line, maxLineLength)}... (line truncated to ${maxLineLength} chars)`
}真正的成本不在接口,在依赖边:
所以是机械改动,不是设计问题,提 PR 完全可行。回归测试可以直接放在 关于「统一工具」:同意方向,而且那个统一工具已经存在( 但我想补一条更强的:如果只能改一处,我会选工具结果落盘前的 你们的 最后说清楚:我们是用户,不是维护者;PR 提了也由官方 review。 |
|
更正一条:我上一条说「提 PR 完全可行」,这个判断是错的,收回。 刚查了仓库的实际设置:
所以不用花这个时间,即使写出来也没有地方提交。 按 CONTRIBUTING 给的渠道走:
我们这边继续用监视器兜底,等上游修复。 |
|
感谢两条回复,信息很有用。 关于 PR:收到,既然 CONTRIBUTING 明确不收外部 PR、API 也返回 404,就不折腾了。我们继续走 Discussions 报告 + 生态插件这条路——两个插件仓库已经打上 关于你提的 三类载体我们这边也确认了:read 截断(老问题)、run_code 打印,以及 2026-10-29 那次的前缀 等上游修了 插件:https://github.com/fengxiforcy/dsh-surrogate-guard (npm 同名,0.1.10) |
|
Confirmed and fixed on current Patch — branch: https://github.com/aturzone/deepseek-harness/tree/fix/truncation-surrogate-pairs, commit
Evidence. Two regressions place an emoji boundary at the cut:
Audit status, honestly. I fixed the two carriers I could confirm in source: |
|
Audit follow-up on the same branch ( Additional carrier fixed: Audit results elsewhere (no change needed):
Still not reproduced: the |
|
先谢一句,plugin_market 这条抓得准——它正好印证了「载体不只在截断点」:300 码元 + 你们缺的那条复现,我们这边有:run_code 打印。 最小复现(注意:这一行会让会话报废,除非手头有日志修复工具): // 在 run_code 程序里运行
console.log("A" + String.fromCharCode(0xd800) + "B")现象:这条工具结果原样写进会话日志( 完整经过(两次事故、日志层面的证据、以及一个失败的自救尝试:在 关键点:这类载体不是截断。所以「把所有截断点换成安全函数」覆盖不到它,换多少个点都不行。 建议再收一道口:在 agent loop 写 两者不冲突:
这道口做了之后,那条 另外一个提醒(如果你们走内部通道就忽略):这个仓库在 GitHub 侧 PR 功能是关闭的( |
|
说明:我是风熙,DSH 用户。以下内容由替我负责运维的 AI 助手阿枢(运维部)整理——复现、排查、插件实现都是她做的,我负责确认和对外发这条。 两条都读了,合起来正好把问题讲全。 1. yingcai 的复现解释了阿枢排查失败的原因。 她一直假设 run_code 是"截断"造成的,试了两个方向都证伪: 2. 同意 toWellFormed 这条建议。 截断点用安全函数是"不产生",落盘前清洗是"产生了也不落地";两者不冲突,要一起上。aturzone 修的三个载体(read / web_fetch / plugin_market)属于前者;后者在 agent loop 一处就能覆盖全部,包括还没复现的取消路径。 3. 阿枢的插件做法与这个分层一致(生态侧兜底,不替代内核修复):
4. 关于 PR: 阿枢也踩过同一个坑(CONTRIBUTING 不收外部 PR、API 返回 404)。aturzone 的补丁要么由维护者 cherry-pick,要么把 diff 贴进讨论。如果维护者需要,她可以把插件侧"落盘前清洗"的实现作为参考贴出来。 |
|
这条复现和「一道口」的建议都成立,已按你的方案做了 durable 侧的收口: 分支:https://github.com/aturzone/deepseek-harness/tree/fix/tool-result-well-formed (commit 改动
证据
你提的 PR 通道问题我这边也确认过: 还差什么:取消路径我仍没有独立复现,但它现在和 run_code 打印走同一个 |
|
关于你问的「取消路径的事件类型」——我们把本机 110 个会话日志全量扫了一遍(逐 zstd 帧解压 → 逐条 1. 只有
|
| seq | data.message.content[0].text 开头 |
|---|---|
| 1159 | kill: "cancellation-requested" + new job: {"kind":"background","jobId":"pwsh-83"} |
| 16936 | Error: code run failed (timeout): execution deadline reached (120000ms) + Captured output: + cancellation-requested |
| 17327 | cancellation-requested + 进程退出码 |
data.error 的形态是 {"name":"CodeRunFailedError","code":"CODE_RUN_FAILED"}——枚举值,本身没有载体风险。你们清洗的是 error.reason,那个变体我们没采样到;如果它和 name/code 不是同一结构,值得确认是否还有别的自由文本字段。
4. 结论
取消路径大概不需要新增事件类型——它和 run_code 打印走同一个 tool/result.message.content[],你那三个写入点已经覆盖,前提是 kill: 那条也走同一个 append 路径(从日志形态看是的)。
如果阿枢那条的形态和我们不同,直接要原始记录比对,比推测快。
|
这份全量扫描太有用了,直接回答了我最不确定的那一题,也印证了收口位置: 1. 取消路径不需要新增事件类型。 你们 8 种事件里只有 2. 你的原始记录和补丁对得上。 3. 另外补充一下这个分支现在的证据面(因为仓库不收外部 PR,这些是给维护者 cherry-pick 时看的):
你们那边如果方便,可以在修复落地后把日志修复插件降级成纯保险;不过在上游合并之前,它仍然是唯一能救已经中毒的会话的手段。这正是仓库渠道关闭(CONTRIBUTING 明写不收外部 PR,API 404)下最现实的分工。 |
|
收到,补两条我们这边的数据:
上游合并前我们继续用监视器兜底,合并后按你说的降级成纯保险。辛苦。 |
|
三条污染记录全部是 我们这边保持现状:分支在 fork 上( 若你们之后在其他事件类型里再采样到载体,直接把 event type + 字段路径贴过来;否则这个补丁的覆盖范围就按现在的三处写入点定稿。 |
Uh oh!
There was an error while loading. Please reload this page.
Summary
readtruncates long lines with a raw UTF-16 code-unit cut (substring(0, 2000)). When the cut lands between the two halves of a surrogate pair, the result ends with a lone high surrogate. That character is written to the session log, and every later request fails with400 INVALID_REQUEST— the session is permanently dead.Where
@deepseek-ai/dsh-tool-fs/lib/index.js:substringcounts UTF-16 code units, so positionmaxLineLengthcan fall inside an emoji.Reproduction
1999 × 'a'+ 😀 +50 × 'b'(so the emoji's high surrogate sits at index 1999).readthat file with the read tool (not a shell command).DeepSeek Messages request failed (400) INVALID_REQUEST.Transcript evidence:
tool/result→data.meta.lines[0].textends withU+D83D(lone high surrogate).Why it kills the whole session
The lone surrogate is persisted as the JSON escape
\ud83d. Every turn resends the full history, so the API rejects every turn:Retry, restart, new window — none help. Only manually repairing the transcript or discarding the session works.
Suggested fix
The codebase already has a safe helper in
@deepseek-ai/dsh-output-retention:truncateLineshould reuse it (or do the same check).Also worth auditing
Any other character-budget truncation that produces model-facing text: grep output, bash output,
web_fetch, subagent results.readis one confirmed instance; the same raw cut may exist elsewhere.Environment
workspace-writeRelated: #8846, #1449
All reactions