Skip to content

feat: compress.reasoning — drop oversized thinking from closed-turn compress calls (#336) - #339

Merged
ranxianglei merged 1 commit into
masterfrom
2026-09-09_compress-reasoning-drop
Sep 9, 2026
Merged

feat: compress.reasoning — drop oversized thinking from closed-turn compress calls (#336)#339
ranxianglei merged 1 commit into
masterfrom
2026-09-09_compress-reasoning-drop

Conversation

@ranxianglei

Copy link
Copy Markdown
Owner

Fixes #336. Supersedes the closed #338 (kernel reasoningReplay approach) — reimplemented cleanly on master with exact semantic alignment to opencode-acp #377 (owner-locked design).

Problem

compress tool-call messages are hard-exempt from every compression selection (their tool results anchor the block summaries), so their thinking parts ride along every request — a monotonically growing, unreclaimable context floor (in the #336 storm session, ~53K tokens of anchor thinking in the outgoing view).

Design (mirrors opencode-acp #377)

Request-time pass (src/reasoning-drop.ts) that removes thinking parts from a message only when ALL gates hold:

  1. Closed turn — strictly before the last genuine user message; the active round is never touched (some providers require replaying the active round's thinking).
  2. Selector — the message carries a toolCall part with name compress (only compress; other protected tools would need their own explicit config).
  3. Size — the message's total reasoning length (chars, summed across parts of that message, never across messages) strictly exceeds threshold. 0 drops any non-empty.

Config — nested under the existing compress three-level cascade, merged field-wise (deepest set field wins):

{
  "compress": {
    "reasoning": { "drop": true, "threshold": 2048 },
    "providers": {
      "openai": { "reasoning": { "drop": false } },
      "anthropic": { "models": { "claude-sonnet-4-5": { "reasoning": { "threshold": 8000 } } } }
    }
  }
}

Opaque-reasoning providers (OpenAI encrypted reasoning) opt out per-provider as shown. Persisted history is never modified — only the outgoing view (rebuilt from the session log each request) changes. Pure, idempotent, fail-safe.

Wired in the context transform right after view rebuild and before the nudge push, so a synthetic user-role nudge can never become the boundary.

Tests

638 total, 0 fail. 12 new (tests/reasoning-drop.test.ts): defaults, invalid-value fallbacks, all three gates (boundary / selector / size incl. 2048==kept, 2049 boundary, per-message sum, no cross-message accumulation), threshold-0 semantics, purity + idempotence, fail-safe, kill-switch, no-user case, toolCall/thoughtSignature preservation, field-wise three-level merge.

Real-session validation

Replayed the #336 storm session (01a07b3c, 998 messages): 602K chars of thinking on compress-call messages → ~7K after the pass (48 messages hit), idempotent.

Docs: CONFIGURATION.md + CONFIGURATION.zh-CN.md (compress.reasoning section, field table, sample config).

…ompress calls (#336)

Exact semantic alignment with opencode-acp #377 (owner-locked design):
compress calls are hard-exempt from compression, so their thinking rides
along every request as an unreclaimable context floor. A request-time
pass (src/reasoning-drop.ts) removes thinking parts only when ALL gates
hold: closed turn (strictly before the last genuine user message),
compress toolCall selector, and total reasoning length strictly exceeds
threshold chars (2048 default, summed per-message; 0 drops any
non-empty). The active round is never touched; persisted history is
never modified; pure/idempotent/fail-safe.

Config: compress.reasoning { drop, threshold } merged field-wise across
the existing three levels (model > provider > global). OpenAI-style
opaque reasoning opts out per-provider:

  { "compress": { "providers": { "openai": { "reasoning": { "drop": false } } } } }

Applied to the rebuilt outgoing view BEFORE the nudge push so a
synthetic user-role nudge can never become the boundary.

Fixes #336
@github-actions

github-actions Bot commented Sep 9, 2026

Copy link
Copy Markdown

📦 Built Extension Artifact

Branch: 2026-09-09_compress-reasoning-drop (52567ac)

Option A — Install from npm PR tag (recommended)

pi install npm:billion-context-pi@pr-339

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

Option B — Download artifact

  1. Download the artifact from the Actions run
  2. Extract the tarball and install:
tar xzf billion-context-pi-pr339.tgz
pi install ./package

This comment is automatically updated on each push.

@ranxianglei

Copy link
Copy Markdown
Owner Author

🤖 Powered by ework · qwen3.8-27b

[bot] 🏷 PR #339 review 完成 — LGTM(一个非阻塞 nit)

查重结论:非重复。#339 正是 #338 floor 9 承诺的 clean reimplementation(owner-locked:"exact alignment with opencode-acp #377 … Clean reimplementation coming on a fresh branch; kernel stays untouched")。#338 已按 superseded 关闭;仓库内无其他同主题 open 项。

#336 审核:问题真实存在——compress 调用被硬排除在压缩之外(FORCE_COMPRESS_PROTECTED),其 thinking 部分每轮原样重发,构成单调上涨的不可回收地板。机制与代码路径已核实。

本地验证(PR commit 52567ac,base f2c5924,npm ci + 全套检查):

  • typecheck ✅ / test ✅ 638 total, 635 pass, 0 fail, 3 skipped(与 PR 声称一致)/ build ✅ dist/index.js 730.76 KB
  • 新增 tests/reasoning-drop.test.ts 12/12 pass(与"12 new"一致)
  • git merge-tree 对当前 master(6d3ee9d,v0.1.60):干净,无冲突

代码 review(逐点核实):

  1. src/reasoning-drop.ts — 三道门控与设计一致:closed turn(严格位于最后一条 user 消息之前)、selector(toolCallname === "compress")、size(单消息内各 thinking 部分求和严格大于 threshold;0 丢弃任何非空)。纯函数(slice + spread,不改输入)、幂等(无变化时返回同一引用)、fail-safe(try/catch 原样返回)。
    • pi-ai part 形状已核实:{ type: "thinking", thinking: string, thinkingSignature? },isThinking 匹配;整块移除(含 signature)语义自洽(signature 只随块回传才有意义)。
    • pi 消息角色已核实:user | assistant | "toolResult"——工具结果有独立角色,"所有 user 消息都是 genuine"成立。
    • content 不会变空:selector 要求存在 compress toolCall 部分,该部分必然存活。
    • 前缀缓存:pass 对同一 log 状态确定;边界只前移;一旦丢弃永不恢复(单调),缓存友好。
  2. src/config.ts:356-371 mergeCompress — 逐字段 model?.reasoning?.drop ?? provider?.reasoning?.drop ?? global?.reasoning?.drop,最深 set 字段赢 ✅。
  3. src/runtime.ts reasoningDropForresolveCompress(adapterRef.compress, provider, modelId) 参数顺序已核对;走 live adapterRef,config reload 即时生效。
  4. src/index.ts:399-412 — drop 在 view rebuild 之后、nudge push(第 496 行)之前 ✅;nudge 不落盘(每次事件从 session log 重建),边界永远由 genuine log 消息决定。coreOutToAgentMessages 仅一处构建出口视图,无绕过路径。
  5. 文档:CONFIGURATION.md + zh-CN 均有 compress.reasoning 章节(字段表 + 示例配置)✅。

Nit(非阻塞):src/index.ts:410debug.event("reasoning-drop", { dropped: droppedThinking.length, ... })droppedThinking 是返回的完整数组,.length整个视图的消息总数,不是实际被剥离 thinking 的消息数,字段名有误导(仅 debug 事件,无功能影响)。建议让 dropCompressReasoning 额外返回改写计数,或改日志字段。

Consideration(owner 拍板项,无需动作):默认 drop: true 对全部 provider(含 OpenAI 加密 reasoning)生效,文档只提示按 provider 退出;被取代的 #338 里 GPT 系是默认关。新设计 owner-locked 且与已合入的 opencode-acp #377 一致,属有意为之——仅提示 OpenAI 用户需自行设置 providers.openai.reasoning.drop: false

结论:实现与 owner-locked 设计及已合入的 opencode-acp #377 语义对齐,全套 CI 本地通过,与 master 无冲突,可人工合并(按仓库规则 PR 合并仅人工)。"Real-session validation"数字(602K→~7K chars,48 messages)合理,但沙箱内无风暴会话日志,无法独立复现。

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