Repository navigation
Description
现象 / 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 截断后,整个会话就再也用不了了。触发条件是这三条同时成立:
- Agent回复里带 emoji(如🔴 ✅ 📌 )
- 这条回复在进入下一轮上下文时会被截短(mneme 默认只留前 800 个字符)
- 截短的位置正好落在这个 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
Merge pull request #335 from slow-stack/fix/inject-emoji-and-timeprefix
modusensus commentedon Sep 29, 2026
已合并(f56f5b6),#334 修复随 PR #335 落地,下个版本发布。要点:
safeSlice:切点落在代理对中间时丢弃高位代理项,宁少一个字符、不留半个。ASCII 文本零开销。感谢报告——4 处截断点的定位、0.8.5/0.8.9 双版本验证和「会话级持续 400」的机制分析(畸形文本永久写入会话历史)非常完整,对修复方案的收敛帮助很大。
fix(inject): emoji 代理对截断与会话时间注入恢复(issues slow-stack#334 slow-stack#333)
docs(changelog): v0.8.10 发版节承接本轮全部内容并删占位空节
chore: v0.8.10 发布准备 (#336)
modusensus commentedon Sep 29, 2026
v0.8.10 已发布,升级命令:
(DSH 桌面端用户重启 DSH 后插件即更新到 0.8.10。)
本版包含 #334(emoji 截断)与 #333(时间注入恢复)两个修复。@kklaozero 报告的会话级 400 在升级后即止——已损坏的历史会话无需处理:畸形文本是截断点留在注入消息里的,0.8.10 的注入不再产生新的畸形文本,旧会话下一轮注入按新逻辑渲染即可恢复正常。
@lqs50
injectTimePrefix升级后自动恢复生效:键名/格式不变,之前在 feature_flags 里开过的配置无需重设;每个新会话首轮注入一次[当前时间: YYYY-MM-DD 周X HH:MM],默认仍关。fix(inject): 热记忆与语义查询改走 surface 投影,撤回内容不再回流