Skip to content

fix(#651): drop oversized reasoning from closed compress turns - #667

Merged
ranxianglei merged 3 commits into
masterfrom
fix/compress-reasoning-floor
Sep 9, 2026
Merged

fix(#651): drop oversized reasoning from closed compress turns#667
ranxianglei merged 3 commits into
masterfrom
fix/compress-reasoning-floor

Conversation

@ranxianglei

Copy link
Copy Markdown
Owner

Fixes #651

Problem

Reasoning/thinking traces left on the wire before a compress call are a permanent uncompressible floor. The anchor of every fold is the compress call itself, so reasoning immediately before it survives every subsequent fold as protected prefix — it can never be re-summarized, only stripped. In the storm sessions this floor reached ~50% of visible context (75.8K tokens of a 151K view in one session).

This is the proxy-side twin of:

Change

New src/reasoning-drop.tsdropCompressReasoning(messages, cfg) strips the contiguous reasoning run immediately preceding a closed compress call when its summed length exceeds threshold chars. A turn is closed when the compress call already has its tool result AND a genuine user message follows it. Safety gates:

  • the active round (compress still in flight) is never touched
  • other tools' (read, bash, …) reasoning is preserved
  • runs are judged by summed length (2×1200 chars still trips a 2048 gate)
  • non-contiguous reasoning (text between fragments) is left alone
  • pure function: no input mutation; idempotent

Config — new nested field merged sub-field-wise across the three levels exactly like absorb/prompts:

compress: { reasoning: { drop: true, threshold: 2048 } }

drop: false restores the old wire verbatim. Invalid thresholds fall back to the default instead of throwing.

WiringresolveCompress() gains the third nested-object merge; all three wire prepares (anthropic / openai / responses) apply the drop after stripKernelSummaries, before the wire rebuild, with an ops log line when anything is dropped.

Tests

17 new tests in tests/reasoning-drop.test.ts (default gate, strictly-greater boundary, active-round protection, non-compress tools, summed runs, non-contiguous runs, kill-switch, purity, idempotence, resolver validation, three-level merge). Full suite: 1263/1263 pass; npm run typecheck clean.

Docs

CONFIGURATION.md + CONFIGURATION.zh-CN.md: new reasoning section with rationale, defaults, and safety gates.

Models that keep reasoning/thinking on the wire build a permanent
uncompressible floor: the fold anchor is a compress call, and any
reasoning sitting before that anchor survives every fold as protected
prefix. Storm sessions hit ~50% visible context lost this way.

Proxy-side twin of billion-context-pi #339 / opencode-acp #377:
- new src/reasoning-drop.ts: dropCompressReasoning() strips the
  contiguous reasoning run immediately preceding a CLOSED compress
  call (result present + genuine user message after) when the summed
  run exceeds a char threshold (default 2048). Active rounds, other
  tools' reasoning, and non-contiguous runs are never touched.
- resolveCompress() now merges a third nested-object field
  (reasoning) sub-field-wise like absorb/prompts.
- all three wire prepares (anthropic/openai/responses) apply the drop
  after stripKernelSummaries, before wire rebuild.
- new config: compress.reasoning = { drop = true, threshold = 2048 }.
- docs: CONFIGURATION.md + zh-CN; 17 new tests (1263 total green).
@github-actions

github-actions Bot commented Sep 9, 2026

Copy link
Copy Markdown

📦 Built Package Artifact

Branch: fix/compress-reasoning-floor (8471882)

Option A — Install from npm PR tag (recommended)

npm install -g billion-context@pr-667

Each push to this PR publishes a new version under the pr-667 npm tag.

Option B — Download artifact

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

This comment is automatically updated on each push.

@ranxianglei

Copy link
Copy Markdown
Owner Author

🤖 Powered by ework · qwen3.8-27b

[bot] 🏷 收到,正在 review PR #667(fix #651 reasoning floor)。已确认 diff 相对 origin/master 干净(7 个文件,全部与本 PR 目的相关)。接下来:① 审核 #651 问题定位是否准确(保护粒度 / prune 重发路径);② 逐行审 src/reasoning-drop.ts 与三处 wire 接线(含 plugin/proxy 双模式影响);③ 在 PR 分支上跑 typecheck + 全量测试。结果稍后回帖。

@ranxianglei

Copy link
Copy Markdown
Owner Author

🤖 Powered by ework · qwen3.8-27b

[bot] 🏷 Review of PR #667 — verified, one docs fix pushed, mergeable after the e2e gate

Duplicate screen: no duplicates. Source issue is #651 (this PR fixes it). Closest neighbor is #539/#540 (ACP-loop re-request dropping reasoning_content → DeepSeek 400, already fixed) — different bug, but it independently confirms the DeepSeek round-trip constraint discussed below. pi #339 / opencode-acp #377 are cross-repo twins, not duplicates.

Root-cause verification (triage step 1–2): confirmed in kernel 0.0.61 — filterProtectedToolMessages (dist/index.js:2531) hard-excludes protected tool calls from every compress selection, and compress is force-protected, so the reasoning attached to those turns is re-sent verbatim on every request. Your measurement (83.5% of never-covered residual, ~9 KB/round) matches the mechanism. Layer judgment: the protection itself cannot be removed (the compress call anchors the block summary), so stripping the attached reasoning at request time is the correct layer — the fix targets the symptom carrier without touching the load-bearing protection. This matches #651's suggestion and the owner-locked design from pi #339 (validated on the same storm session: 602K → ~7K chars).

What I verified on the branch (checked out fix/compress-reasoning-floor @ a464214):

  • npm run typecheck clean; npm test1263/1263 pass (matches your claim).
  • Diff is clean: 7 files, all on-topic, no unrelated churn.
  • src/reasoning-drop.ts: pure (returns a filtered copy, input untouched), idempotent, fail-safe (catch → input unchanged), strictly-greater gate, summed contiguous runs, tool-results don't count as genuine users, only toolName === "compress" selected, invalid thresholds fall back to 2048. All gates tested.
  • Wiring: applied after stripKernelSummaries, before the wire rebuild, on all three prepares. applyCompactionArchive/reapOrphanBlocks still run on the original msgs — persisted state and the ref map are never pruned by the drop; only the outbound view changes. Prefix-cache impact is one-shot per closed turn (coincides with the fold's own view change).
  • Both modes: plugin mode — the pass is effective (compress call+result live in the agent's re-sent history, i.e. the actual floor); proxy mode — the client wire carries no compress tool call (ephemeral server-side loop), so the pass is a correct no-op.
  • Config: third nested-object field, sub-field-wise 3-level merge like absorb/prompts, merge tested.

Provider round-trip analysis (the main risk of default-on):

  • Anthropic: dropping historical thinking blocks is API-legal — the signature check applies to blocks present, and Claude Code's own auto-compact replaces history (thinking included) without rejection.
  • Responses API: the kernel already ships ACP_REASONING_KEEP=none which drops all reasoning items at parse time, so dropping is a known-safe operation on this wire; codex e2e exercises it.
  • OpenAI chat thinking models: DeepSeek's docs are explicit — with tools in the request, all prior reasoning_content must be passed back or the API returns 400. So DeepSeek/GLM-thinking/QwQ users will 400 after the first closed compress turn over the threshold unless they set the kill-switch. This is exactly the pi fix: generate real changelog in GitHub Releases via generate_release_notes #339 pattern (default-on + per-provider opt-out), and the switch is wired correctly end-to-end (config → merge → resolveReasoningDrop → no-op) — but the docs didn't say when you need it. Fixed directly on the branch: commit bd8fd8a adds the use case + a per-provider example to both CONFIGURATION.md and CONFIGURATION.zh-CN.md.

Non-blocking finding (kernel-side, not for this PR): responsesToCore maps non-reasoning items — computer_call, file_search_call, web_search_call, image_generation_call, code_interpreter_call, mcp_call and their outputs — to contentType: "reasoning" (kernel wire/index.js:587). The contiguous-run walk can therefore sweep such items into a drop when they sit in the same run as an oversized reasoning run before a closed compress call. Consequence is bounded: call+output are both "reasoning"-typed so they drop together (no orphaned output, no 400), the content stays in the persisted history, and the compress summary covers the range — but it is technically "drop only reasoning" in name only on this wire. Suggest a kernel follow-up giving opaque tool items their own contentType (worth an issue in acp-kernel).

Before merge: per the repo spec, request-pipeline changes should run the 4-phase e2e core (ACP_TEST_E2E=1 node --import tsx --test tests/e2e/e2e-codex.test.ts) — I can't run it here (needs a live upstream). Unit suite + typecheck are green on the branch.

Verdict: mergeable once the e2e core passes.

中文摘要:验证了 #651 根因(保护是消息粒度,compress 强制受保护,附着 reasoning 每轮原样重发)与修复层次正确(剥离请求时视图而非动保护机制),typecheck 干净、1263/1263 全过、两种压缩模式与三家 provider 往返安全性均核过;已直接在 PR 分支补了 kill-switch 适用场景的文档(commit bd8fd8a),另发现 kernel 把 Responses 不透明工具项映射成 reasoning 类型的小问题(有界、不阻塞,建议 kernel 侧后续处理);跑完 e2e 四阶段核心后即可合并。

 twin)

The 'genuine user message after the compress call' closure gate is
unreachable in long agentic sessions — no user messages exist after the
opening prompt, so every compress round stayed 'active' forever and its
thinking survived as a permanent uncompressible floor (observed on the
pi side: 0 drops, 20.6K/8.4K/10.6K chars retained).

A round now closes when the compress call's tool-result (contentType
'tool-result', matching toolCallId) exists at a later index AND at least
one message follows it. In-flight rounds (result missing, or result
still the last message) are never touched; the per-provider
compress.providers.<name>.reasoning.drop=false escape hatch is preserved
for reasoning-replay providers (GLM).

Mirrors billion-context-pi #348 / PR #349. Tests: 1266 pass, gate suite
rewritten with round-evidence scenarios (no-user-message agentic chains,
pending result, result-before-call, mismatched id, distinct ids).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Protected-message exemption inflates the incompressible context floor: per-compression reasoning is never reclaimable (~83.5% of measured residual)

1 participant