DeepSeek Harness 会话报 413 请求体过大:dsh_session_log 撑爆与永久自锁恢复
DSH 会话突然发不出任何消息、每轮都报 DeepSeek Messages request failed (413),哪怕只发两个字也不听使唤——这通常不是上下文超限,而是遥测字段 dsh_session_log 把整份会话日志塞进了请求体(实测 205.87 MB,占请求体 99.7%),撞上前置网关的体积上限;偏偏推进「水位」的那个回调只在请求成功后才执行,于是「太大→失败→水位不动→下次更大→还是太大」形成永久自锁。 本文按「自锁怎么形成 → 两条触发路径与版本归属 → 多种恢复方案」三段拆开,每段都给可粘贴的命令与配置。
413 自锁是怎么形成的:水位只在成功后推进
这个报错最反直觉的地方是「重试永远无效」,因为失败本身把唯一的修复路径也堵死了。
现场特征:三个一眼可辨的信号
compaction/start {turn:946}
compaction/end {turn:946, error:"DeepSeek Messages request failed (413)"}
turn/end {turn:946, reason:{kind:"error", code:"CONTEXT_WINDOW_EXCEEDED", status:413}}
- 界面上的 token 指标长期停滞却不报警:有用户看到表面 token 数停在 380,109、窗口上限 450,000,界面「看不出任何异常」,而真实请求体已经是 206 MB(#7699)。
实测证据:0.58 MB 的真实对话 vs 205.87 MB 的字段
用「消息条数变化 → 请求体变化」这一招就能把常量揪出来。 适配器侧抓到的一次真实 payload:
[DSH-BYTEGUARD] {"purpose":"compaction","ceiling":8388608,
"baseBytes":579474,
"fieldBytes":{"dsh_session_log":205872373,"dsh_plugin_packages":5916},
"dropped":["dsh_session_log"],"payloadBytes":585413}
| 量 | 值 |
|---|---|
dsh_session_log 字段 | 205,872,373 字节(205.87 MB) |
真实对话消息面 baseBytes | 579,474 字节(0.58 MB) |
| 被拒请求体(3 次采样) | 206,429,335 / 207,740,314 / 206,456,506 字节 |
| 主请求消息条数 | 863;压缩请求 208 |
最后一行是关键:消息条数差了 4 倍(208 vs 863),请求体却只差 1.3 MB——说明有一个约 206 MB 的常量跟着每个请求走,与对话内容无关(#7699)。另一位用户的会话是 29,290 事件、解压后 84.9 MB 的日志,抓到的分项是 dsh_session_log 94.99 MB、messages 1.48 MB、tools 28 KB(#7658)。
自锁循环:四步锁死,且自己锁自己
- 插件
dsh-session-log-deepseek向每个 DeepSeek 请求注入顶层字段dsh_session_log,内容是「上次被接受水位之后的全部会话事件」。 - 水位由
session-log-deepseek/delivery-accepted推进,而该事件由accept()写入,dsh-llm-deepseek只在 HTTP 2xx 之后才调用它。 - 字段越过网关上限 → 网关返回 413 →
accept()不执行 → 水位不动。 - 下次请求重发同一份(甚至因为新增事件而更大)→ 依旧 413 → 永久卡死(#7699)。
源码级确认把这三行钉死了:session-log-deepseek:111/113 的 if (acceptedFormatVersion !== session.header.version) continue 决定水位集合,:118-150 的迁移路径没有任何重新锚定,:185 的 accept 又被 2xx 卡住(社区逐行核对,#7658)。
怎么区分「413 是体积问题」还是「真的是上下文超限」
两套模型、两个数字,别看错指标。 服务端上限可以用「不消耗 token 的探测」量出来:用无效 body 请求 https://api.deepseek.com/anthropic/v1/messages,超限返回 413、未超限返回 422,两种都不消耗 token。实测 8 / 16 / 24 / 32 MB 通过,48 MB → 413(#7658)。
排查时按下面 1/2/3/4 步走:
- 看上下文指标是否健康:如果
messages只有 1~2 MB、contextPressure.surfaceTokens远低于contextWindow(例:279,257 / 1,000,000),就基本排除「上下文超限」(#7658)。 - 在请求路径上抓一次真实 payload:拿到分项字节数,确认是不是某个顶层字段一家独大。
- 做条数对照实验:对比主请求与压缩请求的消息条数,如果条数差几倍而体积几乎不变,就是常量字段在作祟(#7699)。
- 看一眼网关响应体:如果是 openresty 的 HTML 错误页
413 Request Entity Too Large,说明撞的是前置网关,不是模型接口:
HTTP/1.1 413
server: openresty
content-type: text/html
eo-cache-status: MISS
<html><head><title>413 Request Entity Too Large</title></head>
<body><center><h1>413 Request Entity Too Large</h1></center></body></html>
这也是适配器分类器失明的原因:响应不是 JSON 时,兜底文案 DeepSeek Messages request failed (413) 不匹配 isContextWindowExceededError() 的任何模式,于是被归类为 INVALID_REQUEST,自动压缩恢复根本不触发(#7699)。
两条触发路径与版本归属:为什么「突然大面积出现」
同为 afterSeq = -1,背后是两种状态:「水位被过滤掉」和「水位从未建立」。分清楚,才知道修复该放在哪一半。
路径 A:V3→V4 迁移后,旧代际水位被读侧等式过滤
V3→V4 会话格式迁移后,旧的 delivery-accepted 记录还在日志里、字段一字未改,只是被读侧的一个等式跳过了。 那条等式是:
const acceptedFormatVersion = event.data.sessionFormatVersion ?? 0; // session-log-deepseek:111
if (acceptedFormatVersion !== session.header.version) continue; // :113
按代际限定水位是刻意设计(只认「本 Session 格式代际内最高已确认序号」),但迁移路径缺少「重发不可交付」时的重新锚定,而 provider 侧的体积上限与「重发总是可交付」这个假设直接冲突(#7658)。
一位贡献者进一步指出:作废不是删除,而是读侧过滤;而且迁移阶段会重新分配序号(nextSeq = 0、mapping[源序号] = 目标序号),remapV3References 的变换表只覆盖六种类型,delivery-accepted 不在其中——所以迁移前标记里的 throughSeq 是一个旧坐标空间的号码,「数字是有的,缺的是它的含义」。这解释了为什么简单的「拿旧水位当新下界」并不安全(社区单方结论,#7658)。
路径 B:水位从未建立(默认值翻转是导火索)
另一条路根本不需要迁移:老会话从来没写过任何一条 delivery-accepted。 有用户实测其会话「活了 12 天、40,717 个事件,一个水位都没有」,其 14 条 accepted 事件全部是 sessionFormatVersion: 4(与表头相同,因此不属于路径 A)(#7658)。根因是默认值翻转:
| 0.1.5-rc.2 / rc.3 | 0.1.7-rc.1 / alpha.2 | |
|---|---|---|
dsh-session-log-deepseek 的 enabled 默认值 | false | true |
| README 措辞 | 「默认配置不注册请求字段」 | 「默认配置注册该请求字段」 |
机器 diff 两版 bundle 树,这个插件只变了一行:enabled: false -> true;另一台机器实测 lib/index.js:16 同样是 false → true。从未主动开启过它的用户,升级后也会中招——这些老会话没有水位可锚,第一次注入就直接 afterSeq = -1,把整份日志发出去(#7658)。
影响面:不是一条会话,是一批
同一 home 下的存量会话会集体踩线:
- 有用户统计:29 个会话代际里,16 个「零水位且事件数 > 500」,体积 2.0~89.9 MB,每一个都在等第一次官方 DeepSeek 请求就死锁(#7658)。
- 另一例:同 home 的 32,130 / 23,071 / 21,799 / 11,700 事件会话全部 0 条 acceptable(#7658)。
所以「我什么都没改,怎么突然坏了」是正常的——升级动作本身就把默认行为换了(#7699)。
为什么「退回旧版本」救不了这一类
只有「遥测默认开启」是 0.1.7-alpha.2 引入的,另外三条是长期存在的架构问题。 同机两套安装逐条对比(#7699):
| 0.1.7-alpha.2 | 0.1.5-rc.3 | |
|---|---|---|
| 遥测字段默认开启 | 是 | 否 |
retainTokens = 0 的恢复路径 | 有 | 有 |
| 空正文 413 分类缺陷 | 有 | 有 |
| 请求体总字节防线 | 无 | 无 |
三条长期缺陷值得单独记住:
- 恢复动作会原样重演失败:溢出恢复与手动
/compact都调用selectCompactableRange(session, measurement, 0),retainTokens = 0意为「一个字都不留、整段拿去摘要」,于是恢复请求和失败请求一样大,用失败的方式修复失败(#7699)。 - 错误分类器不认非 JSON 的 provider 报错(见上一节)。
- 请求路径上没有任何「总字节」防线:压力模型完全建立在 token 上,适配器对图片有 20 MB 上限(
maxInlineRequestImageBytes),对整体请求体一个都没有;而DEFAULT_CONTEXT_WINDOW = 1e6/DEFAULT_MAX_TOKENS = 256e3会把压缩触发线推到约 678k token,防线在墙后面(#7699)。
多种恢复方案:从「先救活会话」到「治本」
恢复的优先级是:先让会话能发消息,再决定要不要保留遥测功能。 下面四套方案按侵入性从小到大,方案一几乎所有平台都能立刻止痛。
方案一:关闭该注入字段(最快,覆盖所有平台)
这是社区验证过的即时解法,请求体立刻从 96.5 MB 降到 1.53 MB,同一会话正常作答。 关键细节:patch 要打在 home 层($DSH_HOME/cordis.patch.yml),只改 profiles/web/cordis.patch.yml 盖不住 tui / headless(#7658)。
- 关闭全部 DSH 进程。
- 编辑 home 层的 patch 文件,追加下面这段(
config.enabled !== true时插件在注册字段前就返回,因此不会注入):
- id: session-log-deepseek
config:
enabled: false
- 重启 DSH,打开原本卡死的会话。
- 验证:发一条最小消息(如「继续」),预期正常作答;若开了日志,应看到不再产生新的
delivery-accepted。
代价:官方 API 不再收到会话日志后缀、按需读取原始会话日志的能力与该 profile 的 /feedback 投递失效。回滚就是删掉这段配置再重启(#7658)。
方案二:用字节预算插件保留功能(推荐给仍想留日志的人)
如果不想牺牲服务端原始日志检索,可以在不改核心的前提下给这个字段加字节预算。 社区插件 @argszero/cordis-plugin-session-log-budget 包装注册表的 prepare,超预算时只发送最长的、可装载的前缀(afterSeq + 1 … throughSeq 仍是一句真话,尾部窗口做不到这一点),并自己补写一条只覆盖该前缀的接受记录(#7658)。
- 安装:
npm install @argszero/cordis-plugin-session-log-budget
- 在 profile 的
cordis.patch.yml挂载并按需调预算:
- insert:
- id: session-log-budget
name: '@argszero/cordis-plugin-session-log-budget'
config:
mode: enforce # 或 report:只测量并记录,不改行为
maxFieldBytes: 8000000
- 重启后打开卡死会话,连发几轮:待发积压会每轮排空一个预算大小,排完后自动回到正常增量路径。
- 想先观察再动手时,把
mode设为report,先看它测出的字段字节。
使用时要知道的边界:它救不了第一次请求的构建成本(插件看到时 90 MB 的值已经被构建出来);积压排空期间服务端先拿到的是最旧的一段;被缩短的请求会替换注册表的联合接受事务(今天 dsh_session_log 是产品里唯一的这类字段);超过整个预算的信封无法投递,字段会被丢弃并在宿主日志里点名(#7658)。
方案三:升级到带 maxBytes 的版本
0.1.7-rc.2 的产物里已经出现 maxBytes(默认 8 MiB)与「取放得下的最长待发前缀」行为。 社区据此判断升级既能止住新发生,也能让卡死会话按前缀逐轮排空(#7658)。但必须同时知道另一半:导致水位失效的那行等式(:113)没有变,「上限」与「作废」是两半,只有一半动了(#7658)。
- 升级到
0.1.7-rc.2或更新的版本。 - 打开卡死会话,连发多轮让积压排空;若单轮仍失败,说明第一条前缀也超限,需要配合方案一或方案二。
- 升级后仍要检查存量会话水位:用方案一关字段把老会话救活,是更稳的顺序。
方案四:自建「请求体总字节闸门」(给开发者)
治本方向是「任何注入请求的可选贡献都必须有字节上限,超限截断或跳过,绝不抛错」。 社区给出的实现参考(改 3 个文件)里,dsh-llm-deepseek 侧加了四件事:扩展字段字节闸门、整包字节阶梯、内联图片请求级上限、分类器的 isBodylessStatus;dsh-compaction-basic 侧把 retainTokens = 0 换成一个「既保证有东西可摘要、又把摘要输入封顶 131,072 token」的预算(#7699)。实测效果:
bodyBytes=585413 → 放行,水位正常推进
dsh_session_log: 205.87 MB → 93 KB
四步自检思路(不依赖具体实现,便于对齐你自己的补丁):
- 加字节闸门:在请求组装处对每个扩展字段与整体 body 设上限,超限时降级(弃字段 / 降级图片 / 截断长文本),绝不抛错。
- 保证恢复请求严格更小:排查所有
retainTokens = 0的恢复路径,它必然产出与失败请求等大的 body。 - 修分类器:让空正文 / HTML 正文的 400/413 被归类为尺寸或上下文溢出,并暴露原始响应体。
- 把
bodyBytes放进健康指标:只有 token 的指标在有超大扩展字段时是主动误导。
给非程序员的 3 步自救
不写代码也能先恢复,按顺序做(#7658):
- 完全退出 DSH(含后台进程)。
- 找到 DSH home 目录下的
cordis.patch.yml,在文件末尾追加方案一那段session-log-deepseek / enabled: false;改前先复制一份该文件作备份。 - 重新启动 DSH,打开卡死的会话发一条消息验证;确认恢复后,若以后想重新开启日志,把追加的配置删掉即可回滚。
排查注意事项
这个故障的样子很会骗人:界面显示「token 很宽裕」、报错有时是 transport failed、重装和换模型都无效。 九条要点:
- 先分清是「上下文超限」还是「请求体超限」:看
messages面的实际字节与surfaceTokens,两者是不同模型(#7658)。 - 413 不等于上下文超限:本场景里 413 是前置网关对请求体体积的拒绝,响应体是 HTML(#7699)。
- 满屏
transport failed也可能就是它:413 被归为INVALID_REQUEST、重试白名单不含它,且大请求体上传耗时会撞看门狗,最终以 TRANSPORT 暴露(#7658)。 - 别做无效动作:重装、换模型、换网络都不解决;污染源在会话记录与默认为
true的插件里(#7699)。 /compact不但没用、还会原样重演失败:恢复路径用retainTokens = 0,恢复请求和失败请求一样大(#7699)。- 补丁打 home 层:
$DSH_HOME/cordis.patch.yml才能覆盖tui/headless(#7658)。 - 升级不等于万事大吉:rc.2 只补了「上限」,没补「迁移后水位失效」,存量老会话仍可能卡(#7658)。
- 小会话也可能中招:有用户报告 132/58 事件、字段≈0 MB 的会话同样报 TRANSPORT,提示可能还有独立诱因,需要单独排查(#7658)。
--dump-config看不到它:它只打印显式配置、不展开 schema 默认值,所以一个默认开启、能产生 205 MB 行为的功能在任何配置导出里都不显示(#7699)。
这类故障多半伴随一连串「安装/更新/日志」动作,如果你平时把插件装卸、更新确认、系统日志和诊断集中在 DSH Plugin Hub 里管理,排查时至少能先在系统日志页确认是插件层异常还是请求层异常,再决定要不要去动配置文件。

来源:Discussion #7658、Discussion #7699。另可参考讨论中引用的补充帖 Discussion #7753 与归纳帖 Discussion #7737。
常见问题
413 在这里不是上下文超限,而是请求体体积超了服务端上限。实测中真实对话消息面只有 0.58 MB,但 dsh_session_log 这个遥测附加字段占了 205.87 MB,整个请求体约 206 MB 被网关以 413 拒绝。上下文压力指标和请求体字节数是两套模型,前者看起来「38 万/100 万、很宽裕」时后者可能早已撞墙。
这是 dsh_session_log 的注入在水位(watermark)缺失或失效时退化成「整份日志全发」。它有两种来路:V3→V4 会话格式迁移后旧代际的 delivery-accepted 记录被读侧等式过滤掉;或者该字段的默认值在 0.1.7-alpha.2/rc.1 从 false 翻成 true,老会话从未建立过水位。两种都让 afterSeq 回落到 -1。
不影响对话,它是可选的遥测附加字段,关掉后请求体立刻从 96.5 MB 降到约 1.53 MB、同一会话正常作答。代价是官方 API 不再收到会话日志后缀、按需读取原始会话日志的能力与服务端 /feedback 投递随之失效。新会话即使保持开启,通常也只在首次请求付出一次较大上传。
按社区对 0.1.7-rc.2 产物的核对,该版本给字段加上了 maxBytes(默认 8 MiB)与「取放得下的最长待发前缀」行为,因此升级后新发生会被截住、卡死的会话也能按前缀逐轮排空。但导致水位失效的那行等式没有改,上限与作废是两半,只有一半动了。
因为 413/400 会被归类为 INVALID_REQUEST,而重试白名单不含它;同时约 48 MB 的请求体上传耗时接近空闲看门狗,最终由兜底分支以 TRANSPORT 暴露。社区据此判断满屏的 transport failed 很可能就是本问题的表象,排查时不要只按网络方向找。
相关术语
- 水位(delivery-accepted watermark)
- 水位是 session-log-deepseek 用来记录「会话日志已被服务端接受到的最大序号」的标记,存放在 session-log-deepseek/delivery-accepted 事件里。每次请求只发送水位之后的增量;水位由 accept() 在请求成功(HTTP 2xx)后推进,这正是 413 能形成自锁的原因。— https://github.com/deepseek-ai/deepseek-harness/discussions/7658
- dsh_session_log
- dsh_session_log 是 dsh-llm-deepseek 请求上的一个顶层扩展字段,由 dsh-session-log-deepseek 插件注入,内容为「上次被接受水位之后的全部会话事件」。它是遥测/日志用途,不是模型输入,因此上下文压缩机制看不到它,也无法为它设限。— https://github.com/deepseek-ai/deepseek-harness/discussions/7699
- 413 Request Entity Too Large
- 413 是 HTTP 状态码,表示服务端/前置网关拒绝接收体积超限的请求体。在 DSH 这个场景里,它由 openresty 前置网关返回、响应体是 HTML 错误页,而不是 DeepSeek 模型接口的 JSON 错误,这也是适配器的错误分类器认不出它、误判为 INVALID_REQUEST 的原因。— https://github.com/deepseek-ai/deepseek-harness/discussions/7699
- 请求扩展字段(request extension field)
- 请求扩展字段是插件通过注册表往模型请求上附加的顶层字段(如 dsh_session_log、dsh_plugin_packages)。它们在请求组装阶段与 body 合并(JSON.stringify({...body, ...extensions.fields})),因此不受基于 token 的上下文压力模型约束,需要单独的字节上限。— https://github.com/deepseek-ai/deepseek-harness/discussions/7658
来源
- #7658 — 会话格式迁移后 dsh_session_log 水位失效:每次请求重发整份日志 → 413 且永久卡死· deepseek-ai(GitHub Discussions)
- #7699 — 默认开启的会话日志遥测字段把请求体撑到 205.87 MB,并永久锁死会话· deepseek-ai(GitHub Discussions)