### 现象
一次普通的技术调研中,工具抓取了一个第三方 GitHub raw 页面(约 8 KB 中文正文,含一对 U+1F1E6–U+1F1FF 区间的区域指示符,即显示为国旗图形的符号)。该正文进入对话历史后,**该会话的所有后续请求**都返回:
HTTP 400 {"message":"Content Exists Risk","code":"INVALID_REQUEST"}
### 关键判据:判定的是历史,不是新输入
- 同会话中把新消息缩短成两个字符(`test`)→ 仍然 400;
- 新建会话只发 `test` → 正常。
### 后果
1. 该会话不可恢复:报错不指明是哪条历史、哪个 token 区间、哪一类规则;也没有任何 API 层面的会话恢复出口,只能整段删除会话。
2. 排查过程本身会复现故障:为定位原因而读取旧会话转录 / 页面副本的会话,会立刻以同一原因失效——本次共 **6 个会话**依次熔断。
3. 一次无害的第三方文本抓取即让整段工作上下文作废,属单点不可逆故障。
### 复现(建议在隔离的调试环境)
1. 在全新会话中抓取该页面正文并写入会话(页面地址可私下提供;**请勿在生产会话里抓取**);
2. 在同一会话继续发送任意新消息(如 `test`)→ 此后全部 400;
3. 对照:新会话只发 `test` → 正常。
### 请求
1. `INVALID_REQUEST` 的响应体附带**历史定位元数据**(触发消息序号/时间戳、触发类别),不回显敏感原文;
2. 提供**会话级旁路**(按序号截断/删除历史后重试),而不是永久 400;
3. 文档补充该错误码的触发条件与恢复路径(当前官方错误码页只列 400 格式错误/401/402/422/429/500/503,未收录它)。
### 环境
DSH Desktop 0.6.4(内核 `@deepseek-ai/dsh` 0.1.5-rc.2),Windows,2026-09-19。
> 附:脱敏后的完整报告(已移除本机路径、用户名、账号标识与第三方页面地址,不含任何触发字符)。如需页面地址用于复现,请告知,我可私下提供。两点提醒
|
Replies: 3 comments
|
东南小岛flag会Content Exists Risk |
|
Thanks for the precise report — the flag-symbol observation and "a 2-character new message fails identically" are the two facts that pin this down. Here is the mechanism, and a plugin-shaped answer to both halves of your question (localization + a session-level bypass). Why the session, not the turn, is brokenNothing about your new message is being rejected. The request that fails contains the whole conversation: the loop re-derives it from the session log on every call, and Two consequences worth stating, because they rule out the obvious fixes:
The one lever that is left is what is sent, not what is stored. That is also the honest limit of the fix below: it does not remove the poison from your log, it stops the poison from being transmitted. A plugin that does exactly that
It uses your rule — insert a space between every pair of adjacent characters when neither is a space, newest messages first — but spends it reactively, so a healthy session pays nothing:
Mounting needs no configuration: - insert:
- id: content-exists-risk
name: '@argszero/cordis-plugin-content-exists-risk'On your first question — locating the history that triggers itThe ladder doubles as a localization tool, and this is free: the rung the provider finally accepts bounds the trigger's depth ("the newest 32 messages suffice" ⇒ it is within those 32, and the newest 4 are not enough). And if every rung is refused, the trigger is outside what the plugin can rewrite — assistant content ( Mechanics you may care aboutThe rewrite must be a re-dispatch, not an in-place edit: The transform itself iterates code points, so a surrogate pair is never split (your flag symbols are exactly this case), and never inserts ahead of a combining mark or a zero-width joiner, so an emoji sequence keeps rendering as one. Evidence47 tests against a real Known boundaries, honestly: assistant/system content is opt-in ( |
|
补一份客户端事件流侧的读数,机制那部分上面 argszero 已经讲清了,我不重复,只补「用户当时能知道什么」这一维。 口径:
有用的是那 2 个
也就是说,审核命中落到客户端事件流里,和「参数写错」共用一个 所以我想把「请求 1」往下压一小步,成本比历史定位低得多:
这两步不动 API 响应结构,只是在现有分类里加一支。有了它,「命中在哪条历史」可以作为第二步再谈;用户侧至少当场能判断该重试、该改输入、还是该弃会话。 我方在这条上的代价可以作参照:为了在日志里分清这个码,我们在自己插件侧补过一处分类,代价是每升一版要重打一次。止损那一半上面已经有现成插件覆盖了,我不重复;只是这件事放在发行件里做一次,比每人各打一处补丁划算。 最后一条排查提醒: |
Thanks for the precise report — the flag-symbol observation and "a 2-character new message fails identically" are the two facts that pin this down. Here is the mechanism, and a plugin-shaped answer to both halves of your question (localization + a session-level bypass).
Why the session, not the turn, is broken
Nothing about your new message is being rejected. The request that fails contains the whole conversation: the loop re-derives it from the session log on every call, and
GenerateOptions.messagesis documented as "ordered conversation messages, exactly as the provider sees them" (packages/llm/llm/src/types.ts:455-467). The rejection is a property of content, not of a turn — so once on…