Skip to content

注入内容截断把 emoji 截断后这个会话一直报 400 #334

Closed

Description

@kklaozero

现象 / What happened

注入链路按 UTF-16 长度截断文本时,切点落在 emoji 代理对中间会留下孤立代理(例如 🔴 = D83D DD34,被切成只剩 D83D)。DSH 把它序列化成非法 UTF-8,DeepSeek API 对每个请求返回 400 INVALID_REQUEST。

它的破坏是持续性的:畸形文本随注入消息被永久写进会话历史,此后每一轮请求都带着它。表现为会话从某一轮起突然开始连续 400,换话题、重启进程都不恢复——用户只能丢弃整个会话。这不是"偶尔一次请求失败",而是会话级不可逆损坏。

受影响的截断点(dsh-mneme/src/inject.js,0.8.5 与 0.8.9 均存在):

位置 代码 产物去向
lastUserQuery() …join("\n").slice(0, 500) 每轮注入
extractRounds() textOf(event).slice(0, 500) 每轮注入
extractRounds() body.slice(0, 800) 每轮注入
injectMemory() text.slice(0, maxLength) 每轮注入

四处都在注入路径上,所以一旦切中,畸形文本会每轮重新注入一次并被持久化。

复现步骤 / Steps to reproduce

复现这个 bug 需切点正好落在 emoji 中间。emoji 截断后,整个会话就再也用不了了。触发条件是这三条同时成立:

  1. Agent回复里带 emoji(如🔴 ✅ 📌 )
  2. 这条回复在进入下一轮上下文时会被截短(mneme 默认只留前 800 个字符)
  3. 截短的位置正好落在这个 emoji 的中间

emoji 在程序内部不是一整块,而是前后两半拼起来的(UTF-16 代理对)。截短只切走前半、丢掉后半,留下的那半个就成了坏字符。

我撞上的那次,切点正好在 ## Review 18/40 — 🔴 的 🔴 上:截短后只剩前一半,在文本里是个显示不出来的残字符。

这半个字符跟着注入内容被写进会话历史,之后每一轮请求都带着它,DeepSeek 就每轮回 400。

期望行为 / Expected behavior

  • 切到上限时,如果正好卡在一个 emoji 中间,就把这个 emoji 整个丢掉。就算是少一个字符都无所谓,只留半个会让之后每一轮都崩溃。
  • 就算注入内容里真混进了坏字符,也别让它写进会话历史。最坏的后果应该只是「这一轮没接上,再问一句就行」,而不是整个会话报废、只能新开一个。

环境 / Environment

  • dsh-mneme 版本 / plugin version: 0.8.5(0.8.9 复查仍存在)
  • DSH 版本与通道 / DSH version & channel: 0.1.5-rc.2(web profile)
  • 操作系统 / OS: Windows 11 (NT 10.0.26200)
  • Node 版本 / Node version: v24.15.0
  • 安装方式 / install method: dsh-market
  • 相关路由模型 / relevant model routes: deepseek-official / deepseek-v4-flash

日志 / Logs

DSH 会话记录:

{"type":"assistant/attempt","seq":415,"data":{"turn":27,"step":1,"stream":[{"type":"chunk",
  "chunk":{"type":"finish","reason":{"kind":"error","failure":
  {"message":"DeepSeek API error (HTTP 400)","code":"INVALID_REQUEST","status":400}}}}]}}
{"type":"turn/end","seq":417,"data":{"turn":27,"reason":{"kind":"error","error":
  {"message":"DeepSeek API error (HTTP 400)","code":"INVALID_REQUEST","status":400}}}}

随后 turn 28 / 29 / 30 全部同样 400(每轮注入内容不同,但历史里的畸形文本不变)。

已排除上下文超限:该会话累计约 92 万字符,而 DSH 记录的模型窗口是 contextWindow: 1000000(tokens),即便按最保守的字符→token 换算也远低于窗口;且 turn 26 正常、turn 27 起才 400,转折点恰好是 \ud83d 第一次出现在注入文本里。

披露 / Disclosure

  • 本报告由 AI 辅助整理(人工审核后提交)/ Drafted with AI assistance, reviewed by a human

Activity

  1. added
    bug功能异常、崩溃、数据错误(agent 诊断 → 人工确认)
    on Sep 29, 2026
  2. added a commit that references this issue on Sep 29, 2026
  3. modusensus commented on Sep 29, 2026

    @modusensus
    Member

    已合并(f56f5b6),#334 修复随 PR #335 落地,下个版本发布。要点:

    • 4 处截断点(热上下文 query/response、lastUserQuery、记忆条目)全部换用 safeSlice:切点落在代理对中间时丢弃高位代理项,宁少一个字符、不留半个。ASCII 文本零开销。
    • 回归测试以「输出与快照中不存在孤立代理项」锁语义,不锁具体上限值;修复逻辑做了变异阴性对照。
    • CodeRabbit 两条 Minor 已修:lastUserQuery 不再预截断(防止恰好达上限的串绕过检查流向 embedQuery/快照),快照 query 补了干净性断言。

    感谢报告——4 处截断点的定位、0.8.5/0.8.9 双版本验证和「会话级持续 400」的机制分析(畸形文本永久写入会话历史)非常完整,对修复方案的收敛帮助很大。

  4. modusensus commented on Sep 29, 2026

    @modusensus
    Member

    v0.8.10 已发布,升级命令:

    npm install -g @modusensus/[email protected]
    

    (DSH 桌面端用户重启 DSH 后插件即更新到 0.8.10。)

    本版包含 #334(emoji 截断)与 #333(时间注入恢复)两个修复。@kklaozero 报告的会话级 400 在升级后即止——已损坏的历史会话无需处理:畸形文本是截断点留在注入消息里的,0.8.10 的注入不再产生新的畸形文本,旧会话下一轮注入按新逻辑渲染即可恢复正常。

    @lqs50 injectTimePrefix 升级后自动恢复生效:键名/格式不变,之前在 feature_flags 里开过的配置无需重设;每个新会话首轮注入一次 [当前时间: YYYY-MM-DD 周X HH:MM],默认仍关。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bug功能异常、崩溃、数据错误(agent 诊断 → 人工确认)

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions