Bounded DeepSeek Session-log upload
有上限的 DeepSeek 会话日志上传
Without a watermark of the current Session format generation, a request carries the whole canonical log: the first request after upload becomes enabled, the first request after a format migration, and the first request of a fork all do. A long log can exceed the V8 string limit of 2^29 − 24 UTF-16 code units on 64-bit hosts. The adapter's merged `JSON.stringify` then throws `RangeError`, the adapter reports `TRANSPOR
English
Problem
Without a watermark of the current Session format generation, a request carries the whole canonical log: the first request after upload becomes enabled, the first request after a format migration, and the first request of a fork all do. A long log can exceed the V8 string limit of 2^29 − 24 UTF-16 code units on 64-bit hosts. The adapter's merged JSON.stringify then throws RangeError, the adapter reports TRANSPORT, the default retry policy repeats the same serialization, and no request reaches the 2xx that would advance the watermark, so every model request in that Session fails. Below the limit, one request can still carry hundreds of MiB: on Node v24.17.0 and macOS arm64, a 269 MiB first upload of a 96,341-event Session waited 34 s for its 2xx.
Decision
session-log-deepseek.Config.maxBytes, 8 MiB by default, bounds the complete serialized dsh_session_log field in UTF-8 bytes. Each request carries the longest contiguous run after the watermark whose field, including the header and numeric envelope fields, fits the limit. Acceptance advances the watermark to that run's throughSeq, and later requests continue from there. When the first pending event alone exceeds the limit, the request omits the field, the plugin logs a warning, and the watermark stays before that event until maxBytes admits it. An event whose JSON text exceeds the runtime's string limit counts as exceeding every limit, so it blocks upload without failing the request, and the plugin serializes it once per request.
The DeepSeek adapter treats a merged request body that fails to serialize as an extension failure rather than a transport failure. It sends the base body without any extension field, skips the joint acceptance transaction, and reports the omitted field names through the provider plugin's logger, so contributors resend unaccepted state on a later request. A base body that cannot serialize still fails the request.
This supersedes complete-suffix delivery and fail-closed serialization in the request-extension decision. That note still owns field ownership, cancellation, acceptance, and the remaining fail-closed failures.
Alternatives considered
Serialize the whole pending run first and bound it only on overflow. Measuring bytes requires serializing every candidate event either way. In a synthetic benchmark, a 14 KiB step took 14.3 µs whole versus 13.8 µs per event, while a 135 MiB backlog took 182 ms to serialize before any fallback versus 10 ms to fill 8 MiB. The whole-run attempt also still overflows the string limit for the logs that motivated this change.
Send an oversized first event alone. Exceeding the configured limit reintroduces the provider body-size rejections that still fail the model request. Omission keeps every request bounded, and the warning names the blocked event.
Omit only the field that fails to serialize. Acceptance is one joint transaction across fields, so per-field omission would split the registry's acceptance contract. Dropping every extension field on this path only leaves the small dsh_plugin_packages inventory out of one request.
Consequences
A backlog drains at up to 8 MiB per accepted request by default. At the end-to-end rate measured for the 269 MiB upload, about 8 MiB/s, an 8 MiB field adds about 1 s to each request while a backlog drains, and a 0.86 GB backlog drains in about 100 requests. In the same 96,341-event Session, the median per-step append is 11 KiB and the largest event is 414 KiB, so ordinary steps stay far below the limit and only an unusual event or a small configured limit reaches the oversized-event path. While a backlog drains, the endpoint receives a throughSeq behind the Session's current tip. Provider rejections still fail the model request; preparation and acceptance failures are logged and do not.
Plugin tests cover the exact byte boundary, UTF-8 counting, backlog draining across accepted requests, the oversized-event warning with its unchanged watermark, an event past the string limit, and the 8 MiB default through plugin configuration. Adapter tests cover the base-body fallback through the provider plugin's logger and through an adapter without a reporter.
中文
问题
没有当前会话格式代的水位时,请求会携带整份规范日志:开启上传后的第一次请求、格式迁移后的第一次请求,以及 fork 的第一次请求都是如此。长会话日志可能超过 64 位宿主上 V8 的字符串上限,即 2^29 − 24 个 UTF-16 码元。此时适配器合并正文的 JSON.stringify 会抛出 RangeError,适配器报告 TRANSPORT,默认重试策略重复同样的序列化,任何请求都无法拿到推进水位所需的 2xx,于是该会话的每次模型请求都会失败。即使低于上限,单次请求仍可能携带数百 MiB:在 Node v24.17.0 与 macOS arm64 上,一个 96,341 条事件的会话首次上传 269 MiB,等待 34 秒才收到 2xx。
决策
session-log-deepseek.Config.maxBytes 默认 8 MiB,按 UTF-8 字节限制完整的序列化 dsh_session_log 字段。每次请求携带水位之后字段能放进上限的最长连续事件段,字段大小包括 header 与数值型 envelope 字段。接受操作把水位推进到该段的 throughSeq,后续请求从那里继续。第一条待发事件本身就超过上限时,请求省略该字段,插件记录一条告警,水位停在该事件之前,直到 maxBytes 能容纳它。JSON 文本超过运行时字符串上限的事件视为超过任何上限,因此它会阻塞上传但不会使请求失败,插件每次请求只序列化它一次。
DeepSeek 适配器把合并后请求正文的序列化失败视为扩展失败,而非传输失败。它发送不含任何扩展字段的基础正文,跳过联合接受事务,并通过提供方插件的 logger 报告被省略的字段名,因此贡献方会在之后的请求中重发未接受的状态。基础正文本身无法序列化时,请求仍然失败。
本决策取代请求扩展决策中的完整后缀交付与 fail-closed 序列化。该记录仍负责字段所有权、取消、接受以及其余 fail-closed 失败。
考虑过的替代方案
先序列化整个待发事件段,只在超限时再按上限截取。 无论哪种方式,量字节都要序列化每个候选事件。合成基准中,14 KiB 的一步整体序列化耗时 14.3 µs,逐条累加耗时 13.8 µs;135 MiB 的积压在回退前整体序列化就要 182 ms,逐条装满 8 MiB 只需 10 ms。对促成本改动的日志,整体尝试仍会超过字符串上限。
单独发送超限的第一条事件。 超过配置上限会重新引入提供方请求体大小拒绝,而这类拒绝仍会使模型请求失败。省略字段能让每次请求都有上限,告警也会指出被阻塞的事件。
只省略序列化失败的那个字段。 接受是跨字段的一个联合事务,按字段省略会拆分注册表的接受契约。在这条路径上丢弃全部扩展字段,只会让体积很小的 dsh_plugin_packages 清单缺席一次请求。
后果
默认情况下,积压按每次已接受请求最多 8 MiB 补传。按那次 269 MiB 上传实测的端到端速度(约 8 MiB/s),补传期间 8 MiB 的字段让每次请求多约 1 秒,0.86 GB 的积压约 100 次请求补完。在同一个 96,341 条事件的会话中,每步追加量的中位数是 11 KiB,最大单条事件是 414 KiB,因此普通步骤远低于上限,只有异常事件或很小的配置上限才会走到超限事件路径。积压补传期间,端点收到的 throughSeq 会落后于会话当前末尾。提供方拒绝仍会使模型请求失败;准备失败与接受失败只记录告警,不会使请求失败。
插件测试覆盖精确字节边界、UTF-8 计数、跨已接受请求的积压补传、超限事件告警及其不变的水位、超过字符串上限的事件,以及经插件配置生效的 8 MiB 默认值。适配器测试覆盖经提供方插件 logger 报告的基础正文回退,以及没有 reporter 的适配器上的同一回退。