Skip to content

[PR] fix: stop the Codex/opencode WebSocket lanes from 413-ing long sessions before compression (#4804) - #4808

Closed
ranxianglei wants to merge 7 commits into
masterfrom
2026-10-26_issue4804-ws-admission-shape-pin
Closed

ranxianglei wants to merge 7 commits into
masterfrom
2026-10-26_issue4804-ws-admission-shape-pin

Conversation

@ranxianglei

Copy link
Copy Markdown
Owner

Part 1 (stopgap) of #4804 — see the in-thread plan; the root-cause fix (incremental trusted-prefix continuation path) follows as a second PR per the owner's pending decision and is what closes #4804 (acceptance criterion 1 requires it).

What was wrong

The Responses WebSocket lanes (codex-responses-ws, responses-ws) measured the reconstructed full history against a hardcoded 100 MiB cap before dispatch ran (src/responses-ws.ts admission). A long session whose processed outbound was far below the cap died with 413 request_too_large on the next turn even when the client sent only a few MiB of delta; checkpoint accounting shared the same cap and cleared checkpoints near it, stranding continuations with previous_response_not_found. #3202 made the HTTP budgets configurable but explicitly left the WS surface fixed at 100 MiB.

Fix

  • ws maxPayload, reconstructed-body admission, upstream frame/backpressure/output guards and checkpoint accounting all resolve limitsReceiveBytes() (BILI_RECEIVE_BODY_BYTES > network.receiveBodyBytes > 5 GiB) — zero new config fields (existing knob, same as the HTTP lane since feat: configurable request-body byte budgets under network.* (#3128) #3202)
  • one full serialization per turn: admission measures the same wire string the pipeline envelope pushes (was two)

Behavior changes (old → new, defaults) — disclosed per policy

Surface Old New Why
WS wire frame cap (maxPayload) 100 MiB 5 GiB (limitsReceiveBytes()) post-boundary reconnect full-replay frames must not die at the ws layer (acceptance #2/#3)
WS reconstructed-body admission 100 MiB 5 GiB the bug: the processed view is what should be sized, not retained originals
Checkpoint retention 100 MiB (cleared near cap) 5 GiB near-cap checkpoints no longer strand continuations
413 message text constant "100 MiB" names the live budget the error names the actual stage budget

All guards remain explicit and bounded (per-connection single-flight + the #2076 backpressure pause); nothing becomes unbounded. Restore the old value with BILI_RECEIVE_BODY_BYTES=104857600 (this also sets the HTTP receive budget, as that knob always did).

Tests

Wire/error codes unchanged (request_too_large 413, previous_response_not_found 400); no config schema change; no kernel changes.

ci-check added 2 commits October 26, 2026 13:45
- export ResponsesWsSession so the onMessage admission seam is testable
  directly (same seam as the issue's offline repro harness)
- tests/issue4804-ws-admission-boundary.test.ts expresses the acceptance
  behavior: full replay above the old 100 MiB cap reaches dispatch; small
  previous_response_id continuation over the reconstructed cap reaches
  dispatch; checkpoint accounting retains a completed turn whose combined
  size crosses the old cap. Verified to fail on unfixed code with the exact
  production signatures (request_too_large 413, previous_response_not_found).
  Controls (small turns pass, foreign id refused 400) stay green.
…4804)

The Responses WebSocket lanes (codex-responses-ws, responses-ws) measured
the RECONSTRUCTED full history against a hardcoded 100 MiB cap before
dispatch ran, so a long session whose processed outbound was far below the
cap died with 413 request_too_large on the next turn even when the client
sent only a few MiB of delta; checkpoint accounting shared the same cap and
cleared checkpoints near it, stranding continuations with
previous_response_not_found. #3202 made the HTTP budgets configurable but
explicitly left the WS surface fixed at 100 MiB.

- ws maxPayload, reconstructed-body admission, upstream frame/backpressure/
  output guards and checkpoint accounting all resolve limitsReceiveBytes()
  (BILI_RECEIVE_BODY_BYTES > network.receiveBodyBytes > 5 GiB) - zero new
  config fields
- one full serialization per turn: admission measures the same wire string
  the pipeline envelope pushes (was two)

Behavior changes (old -> new, defaults):
- WS wire frame cap 100 MiB -> 5 GiB
- WS reconstructed-body admission cap 100 MiB -> 5 GiB
- checkpoint retention cap 100 MiB -> 5 GiB (near-cap checkpoints survive)
- 413 message names the live budget instead of a constant "100 MiB"
Restore the old value with BILI_RECEIVE_BODY_BYTES=104857600 (this also sets
the HTTP receive budget, as it always did).
@ranxianglei ranxianglei changed the title fix: stop the Codex/opencode WebSocket lanes from 413-ing long sessions before compression (#4804) [PR] fix: stop the Codex/opencode WebSocket lanes from 413-ing long sessions before compression (#4804) Oct 26, 2026
…4804)

The codec comment claimed codex "never sends previous_response_id", but the
#4804 real-session capture shows another codex-family build sending
previous_response_id with a tiny input tail as its normal operating mode.
Scope the claim to the 0.147.0 capture it came from; both shapes are live
production traffic.
@ranxianglei

Copy link
Copy Markdown
Owner Author

🤖 Powered by ework · qwen3.8-27b

(ai: vllm-qwen/qwen3.8-27b)

查重结论:不重复。#3202(HTTP 侧预算可配置化)是直接前驱,当时明确把 WS 面留在固定 100 MiB;本 PR 是把同一旋钮延伸到 WS 面,属延续而非并列重复。#3128(抬 cap)已被 #3202 关闭,#4788/#4792 是内存/延迟与存储保留话题,机制不同。开放 PR 中引用 #4804 的只有本 PR 一个,无并行修复。

标签:bug / codex / opencode / scope/wiring / p1(底层缺陷 #4804 是 WS 协议接线面的 p1 可用性缺陷)。

审核结论(独立重读 diff + 本地红绿复现)

1. 是什么问题:Codex/opencode 的 WebSocket 车道在压缩还没跑之前,就拿「从 checkpoint 重建出的完整历史」去比一个写死的 100 MiB 上限——长会话下一轮直接 413 request_too_large,哪怕客户端只发了几 MB 的增量;checkpoint 记账共用同一上限,接近上限就把 checkpoint 清掉,后续轮次被卡在 previous_response_not_found,永久 413。

2. 什么原因导致的:三个独立字节预算(ws 帧 maxPayload、重建体准入、checkpoint 保留)全部硬编码 MAX_REQUEST_BYTES(100 MiB),且准入量的是 expand() 重建后的大小而不是客户端帧大小;#3202 只把 HTTP 侧做成可配置,明确把 WS 留在固定值。

3. 影响范围与严重程度:p1——codex-responses-ws / responses-ws 车道上累积历史超 100 MiB 的会话完全无法继续(用户只能本地打补丁,如 #4804 报告的 128 MiB 补丁);小会话不受影响。

4. 怎么修复的:三个预算全部走既有接收预算旋钮(BILI_RECEIVE_BODY_BYTES > network.receiveBodyBytes > 默认 5 GiB),零新增配置字段;每轮只做一次全量序列化,准入和推送共用同一份 wire 字符串(原来两次)。这是 #4804 线程里的方案 A(止血):恢复可用性、满足验收标准 2–4;验收标准 1(不物化无限增长的重建对象)由方案 B(增量可信前缀路径)作为第二个 PR 解决,B 吸收 A,合计 2 个 PR,符合止血例外纪律。B 等 owner 拍板,与本 PR 并行不阻塞。

5. 是否有回归问题:逐项审过,无意回归。

6. 是否破坏缓存:不涉及。

7. 是否改了设计方向:无转向——纯复用既有旋钮体系;B 是既定下一步。

8. 是否引入新配置:无(既有旋钮;要恢复旧行为设 BILI_RECEIVE_BODY_BYTES=104857600,注意这会同时改 HTTP 接收预算——自 #3202 起该旋钮一直如此)。

测试有效性:真缝测试(直接驱动 ResponsesWsSession.onMessage(),只 stub dispatch 计数器),双向断言(#3617 纪律)。我本地独立复现了红绿:换回修复前的 responses-ws.ts,三个验收用例失败且失败签名与生产完全一致(request_too_large ×2、"exceeds the 100 MiB" ×2、previous_response_not_found ×2),两个控制用例通过;换回修复版全绿。定向 5 套件(2 个新文件 + codex-responses-ws + request-body-budget + knobs-config-precedence)36/36 过,typecheck 干净。

CI 状态:head 2c8808225 各 lane 当前排队中(#4599 队列积压)。预告:master 本身目前有 2 个已知继承性红(tests/storage-unified.test.ts parity ×2,#4725 合入引入,见 #4599 floor 246/247;ci-fixes 哨兵已失效,merge 为空操作)——若队列恢复后 test lane 按该签名变红,属继承性失败,与本 PR 无关(diff 不碰 storage);届时会在 #4599 留一行说明。

判定:代码审核通过,止血红可合(CI 绿后由人合并——默认值变更类按 §7.4 本就 human-only);#4804 验收标准 1/5 待方案 B 第二 PR。

@github-actions

github-actions Bot commented Oct 26, 2026 •

Copy link
Copy Markdown
Contributor

📦 Built Package Artifact

Branch: 2026-10-26_issue4804-ws-admission-shape-pin (8a78879)

Latest build — install this exact version

npm install -g billion-context@0.0.6292-pr.4808.8d88ec1

This exact version is immutable and reproducible. The rolling pr tag tracks the newest test build across all PRs.

Recent builds (commit → version)

commit version build
8a78879 billion-context@0.0.6292-pr.4808.8d88ec1 build
6253c19 billion-context@0.0.6134-pr.4808.e70ba4e build
e8b7ae1 billion-context@0.0.6121-pr.4808.3a5b241 build

Download artifact

  1. Download the artifact from the Actions run
  2. Extract the tarball and install:
tar xzf billion-context-pr4808.tgz
npm install -g package

Install instructions are refreshed on each push.

yanhuojiyusheng pushed a commit to yanhuojiyusheng/billion-context that referenced this pull request Oct 26, 2026
… declaration for lockfile-only churn

kernel/package-lock.json is generated state, not kernel source. Agents' test
prep (npm install inside kernel/, sweep-bot merges of stale lockfiles) churns
it without any intent to change the kernel — 8 PRs (ranxianglei#4823 ranxianglei#4808 ranxianglei#4802 ranxianglei#4800
ranxianglei#4799 ranxianglei#4794 ranxianglei#4790 ranxianglei#4785) tripped the human gate on lockfile-only diffs, and
the previous stopgap (bots appending kernel-change: declarations to PR
bodies) was correctly rejected by the owner as bypassing the approval step.

The human gate exists to protect kernel SOURCE. Rule 1 now ignores
kernel/package-lock.json when deciding whether a PR 'touches kernel/'; real
source/build-config changes still require the declaration + owner approval,
and lockfile CONSISTENCY stays enforced by the separate lock-sync job (which
fails on genuine drift — the ranxianglei#4725 merge red being today's live proof).
@ranxianglei

Copy link
Copy Markdown
Owner Author

🤖 Powered by ework · qwen3.8-27b

(ai: vllm-qwen/qwen3.8-27b)

冲突已解,新 head 8a7887977(两次连续并入 master)。GitHub 侧确认与 master 无冲突(mergeable=true),当前 master 24e71f637 已是 PR head 的祖先、不再 behind;检查矩阵的 blocked 是推送后等 CI 的正常状态。

关键发现(比冲突本身重要):本分支历史里带着两份旧 ci-fixes 快照合并(e8b7ae1 / 6253c19,#4599 help-merge sweep 遗留)。由于分支↔master↔ci-fixes 互相合并形成 criss-cross 拓扑,直接 git merge origin/master 时 git 自动合并会静默保留旧内核状态——kernel 降级 0.0.167→0.0.166、#4797 的 refLedgerAligned 传播行被删、多个 golden 文件回退(正是 #4599 floor-302 复盘警告的隐患,#4724 差点被它带进 master)。若原样推送,本 PR 合入即回退 master 内核。处置:把 merge commit 的树重建为「当前 master + 仅本 PR 的 5 个文件」,并逐字节核对 kernel/ 与 master 一致。这是该机制第二次实证(#4724 为第一次);根治(sweep 改 rebase + 降级守卫)仍在 #4599 等 owner 拍板,按同机制同 issue 原则不再另立。

八问:

  1. 是什么问题:系统转发「PR 与基线冲突」;实测文本层无硬冲突,真问题是分支落后 master 28+ 提交,且历史里夹带旧 ci-fixes 快照,朴素合并会制造内核回退。
  2. 什么原因导致的:sweep 用 git merge origin/ci-fixes 把早于 master 后续改动的快照并进分支;criss-cross 下 merge-base 算法合成虚拟基线,把「旧内核状态」误判为本侧有意变更而保留,盖掉 master 新内容。
  3. 影响范围与严重程度:若不处理=本 PR 合入即回退 master 内核(0.0.167→0.0.166,丢 After the agent restarts, this session can no longer be compressed: the ref suggested by acp_status is flagged as "renum | 代理重启后该会话再也压不了缩:acp_status 建议的 ref 被 compress 判为「已重编号」,两面对 ref 的认知不一致 #4797 修复)+ golden 回退,属内核回归;处理后对 master 内容零影响,分支只留本 PR 自身改动。
  4. 怎么修复的:不信任自动合并产物——重建 merge commit 树 = 最新 master + 本 PR 5 个文件(src/knobs.ts、src/responses-ws.ts、src/ws-bridge.ts + 两个新测试文件),保留双亲拓扑;期间 master 又前进,做了第二遍(head 8a78879 直接以 master 24e71f6 为父)。
  5. 是否有回归问题:最终树相对 master 的差异逐文件核对过 = 恰好 PR 的 5 个文件,kernel/、lockfile、goldens 全部与 master 逐字节一致,本次操作本身不引入任何回归面;PR 自身的行为变更仍是原描述内容(WS 车道字节预算改走 receive budget knob),未被合并改动。
  6. 是否破坏缓存:不涉及(未触碰缓存/重试/增量游标逻辑)。
  7. 是否改了设计方向:没有。
  8. 是否引入新配置:没有。

本地验证:typecheck ✓、build ✓(dist-import-annotations / dist-external OK)、WS 车道定向 8 组 63/63 绿(issue4804×2、ws-bridge、responses-ws-http-fallback、codex-responses-ws、issue3701-ws-byte-budget、ws-upgrade、native-ws-intercept)。全量判定以推送后 CI 重跑为准。

结论:冲突解除,分支就绪,等 CI 绿灯即可,无需其他动作。

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

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug] Codex Responses WebSocket blocks long sessions at 100 MiB before ACP compression, including small incremental continuations

1 participant