Skip to content

fix(#1712): blank decompress range fields = omitted; stop advertising range args where CCR cannot arm - #1713

Merged
ranxianglei merged 1 commit into
masterfrom
2026-09-30_decompress-empty-range
Sep 30, 2026
Merged

ranxianglei merged 1 commit into
masterfrom
2026-09-30_decompress-empty-range

Conversation

@ranxianglei

Copy link
Copy Markdown
Owner

Closes #1712

Problem

In OMP native plugin mode, decompress with blank startId/endId ("" or whitespace) entered the range-restore branch (typeof === "string" check only) and failed with startId and endId must be given together. Following that hint, filling real ranges then hit range restore … is proxy-mode only — plugin mode owns its folds natively — a dead end that sent the model into a fill-the-params / unsupported retry loop (16 observed failures vs 10 successes when the fields were simply omitted).

Root cause is two-fold:

  1. Blank optional params are treated as "present but empty" instead of "unspecified".
  2. The decompress schema advertises startId/endId unconditionally on every wire and in the plugin manifest, while execution requires CCR to be armed ([Feature] CCR v2: message-ID coverage in block summaries, range-level decompress, ID-returning search_context #1179). Plugin mode arms CCR only on anthropic/openai with base-config enabled ([Feature] CCR enablement switch on every host lane: acp_retrieve tool + config surface, one aligned schema across three repos #1271/bug(ccr): plugin manifest tool name can diverge from the route/model-resolved CCR tool name #1345), so for most plugin sessions — including the reporter's OMP-on-Responses case, where CCR never arms at all — the advertised params can never work. The old error text ("proxy-mode only") was also stale since [Feature] CCR enablement switch on every host lane: acp_retrieve tool + config surface, one aligned schema across three repos #1271.

Changes

Execution semantics (src/decompress-shared.ts):

  • New hasRangeArgs(): a call asks for a range restore only when at least one field carries a non-blank value. Both blank/whitespace/missing → whole-block restore (the bug's repro now succeeds).
  • One-sided non-blank still fails, message extended: …must be given together — pass both mNNNNN refs, or omit both to restore the whole block.
  • Plugin-mode CCR-off error replaced with copy-pasteable working guidance: [decompress FAILED: range restore (startId/endId) requires CCR, which this plugin-mode session does not have — omit startId/endId and restore the whole block: {"blockId":"<id>"}] (replaces the stale "proxy-mode only" text).
  • Same blank-as-omitted rule applied to the derived-parent path ([pi plugin] Prime RLM inline 派生子会话在代理模式下零继承:搜不到也解压不了父会话压缩块(billion-context-pi#531 同场景) #1333).

Schema surface — #1192 conservative rule applied to decompress (src/compress-tool.ts, src/plugin.ts, src/server.ts):

  • New BILI_ACP_TOOLS_*_NO_RANGE arrays: identical except decompress lacks startId/endId.
  • Proxy wire injection serves the NO_RANGE variant while the session is not CCR-armed (same predicate already used for acp_retrieve on each wire); CCR-armed sessions get the full schema as before.
  • Plugin manifest advertises range args only on anthropic/openai when base config enables CCR; never on responses (plugin mode structurally disarms CCR there — PLUGIN_CCR_WIRES excludes it, mirroring how ccrTools is never spread into the responses array).
  • Param descriptions document omit-both semantics and that empty values count as omitted.

Behavior changes (old → new)

Surface Old New Why
decompress exec, both fields blank/whitespace [decompress FAILED: startId and endId must be given together] whole-block restore succeeds optional params semantically mean "unspecified" when blank; kills the retry loop
decompress exec, one-sided non-blank …must be given together same failure + — pass both mNNNNN refs, or omit both to restore the whole block steers to the working call shape
decompress exec, plugin mode w/o CCR range restore (startId/endId) is proxy-mode only — plugin mode owns its folds natively requires CCR, which this plugin-mode session does not have — omit startId/endId … {"blockId":"<id>"} old text factually stale since #1271 and pointed into a dead end
Served schema, proxy wire, CCR-off session decompress carries startId/endId NO_RANGE variant (no startId/endId) params can never execute there; advertising guaranteed a rejected call
Served schema, plugin manifest, CCR off (default) range args advertised on all 3 wires no range args anywhere same reason; default config has CCR off
Served schema, plugin manifest, base CCR on range args on all 3 wires anthropic/openai only responses never arms CCR in plugin mode (#1271)

No new config fields/env vars/CLI flags — the config surface is untouched. No change to what CCR-armed sessions can do: range restore behaves exactly as before wherever it could already run.

Golden regeneration

tests/golden/wire-contract/{bili,manifest-default,manifest-optin}.json regenerated via node --import tsx scripts/update-wire-contract-goldens.ts. Justification: the diff IS the intended behavior change above — manifest-default loses the two props from all three wires' decompress; manifest-optin keeps them on anthropic/openai only (responses loses them); bili.json carries only the updated param description text. kernel.json unchanged.

Tests

New tests/decompress-empty-range.test.ts (9 tests): blank/whitespace/one-missing → whole-block restore; one-sided → paired-error with omit guidance; plugin CCR-off → copy-pasteable guidance (and no more "proxy-mode only"); proxy CCR-off → enable-CCR error; with-range vs NO_RANGE array parity (drop exactly startId/endId, tool set identical, search_context keeps conversation_id); manifest default/optin per-wire assertions.

Pre-flight (all green on this head):

  • npm run typecheck ✅
  • npm test — 3241 pass / 0 fail / 12 skipped ✅
  • npm run build ✅

E2E: local upstream (127.0.0.1:8199) unreachable from this sandbox, so the codex e2e suite runs via CI (ci-e2e.yml auto-triggers on this diff — src/server.ts is a hot file).

中文摘要:修复了 OMP 插件模式下 decompress 传空字符串范围参数导致的反复失败重试——空白参数现在按"未指定"处理直接整块恢复,且不再向无法启用 CCR 的会话/清单暴露 startId/endId(错误提示改为给出可直接复制的成功调用形式)。

@github-actions

github-actions Bot commented Sep 29, 2026 •

Copy link
Copy Markdown
Contributor

📦 Built Package Artifact

Branch: 2026-09-30_decompress-empty-range (75e23c1)

Option A — Install this exact build (recommended)

npm install -g billion-context@0.0.1668-pr.1713.6a644b3

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

Option B — Download artifact

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

Install instructions are refreshed on each push.

@ranxianglei

Copy link
Copy Markdown
Owner Author

🤖 Powered by ework · qwen3.8-27b

收到,开始 review。流程:① 审原 issue #1712(复现路径已在 master 代码上核对)→ ② 回归审查(diff 触碰的既有行为逐项清点)→ ③ 代码审查 → ④ 测试有效性 → ⑤ CI 状态与 base 新鲜度。结论随后补在此楼。

…rtising range args where CCR cannot arm

- resolveDecompress/resolveDerivedDecompress: blank or whitespace-only startId/endId now mean unspecified (whole-block restore) instead of entering the range path and failing 'given together'; one-sided non-blank still fails, with guidance to omit both.
- plugin-mode CCR-off error replaced (the old 'proxy-mode only' text was stale since #1271/#1345 and steered callers into filling unsupported params): now points at the working call shape {"blockId":"<id>"}.
- decompress schema surface (#1192 conservative rule): new *_NO_RANGE tool arrays without startId/endId. Proxy wire injection serves them while the session is not CCR-armed; the plugin manifest advertises range args only on anthropic/openai when base config enables CCR — never on responses (plugin mode structurally disarms CCR there, #1271).
- param descriptions document omit-both / empty-counts-as-omitted.
- goldens regenerated via scripts/update-wire-contract-goldens.ts (manifest-default loses the two props; manifest-optin keeps them on anthropic/openai only; bili.json description text).
- tests/decompress-empty-range.test.ts: 9 regression tests (execution semantics + schema surface + manifest).
@ranxianglei
ranxianglei force-pushed the 2026-09-30_decompress-empty-range branch from f30259f to 75e23c1 Compare September 29, 2026 22:09
@ranxianglei

Copy link
Copy Markdown
Owner Author

🤖 Powered by ework · qwen3.8-27b

[bot] 🏷 Review complete — mergeable. All 13 CI checks green on the rebased head 75e23c17. Details below.

① Direction (issue audit)

Root-cause analysis in the PR holds up, verified against the code: two independent defects, both fixed at their source (the decision point and the producer), not masked:

  1. src/decompress-shared.ts gated the range branch on typeof === "string" — blank fields were "present but empty" instead of "unspecified". New hasRangeArgs() fixes this at the decision point.
  2. Range params were advertised unconditionally while execution requires CCR arming — the schema lied about capability. Now conditional on the same predicate execution uses.
    The dead-end loop in [Bug] OMP plugin: empty optional decompress range fields cause failures and unsupported-range retries #1712 (fill-params → "proxy-mode only" → retry) is broken at both ends: blank fields now succeed, and the remaining failure paths hand back copy-pasteable working calls.

② Regression inventory (existing behaviors touched)

Behavior Old → New Verdict
Both range fields blank/whitespace must be given together failure → whole-block restore. Intended (the fix)
One-sided non-blank paired error unchanged; message extended with omit-both steering. Intended
Plugin-mode range call, CCR off stale "is proxy-mode only" (stale since #1271) → actionable guidance with working call shape. Intended, disclosed
Proxy-mode range call, CCR off enable-CCR error unchanged (pinned by test #615)
Derived-parent block (#1333), blank range "not supported" failure → whole-block derived restore (src/decompress-shared.ts:460-469, implemented & sound). Intended alignment
Advertised schema, CCR-off sessions / default manifests startId/endId present → dropped on all wires. Intended (core of fix). Prefix-cache: tools-array bytes flip only when CCR arming flips — same flip class as the existing retrieve-tool add/remove; static consts are byte-stable within a state. No new cache-break class
CCR-armed sessions full schema + range restore zero change (parity test asserts NO_RANGE drops exactly startId/endId, tool set otherwise identical)
Wire-contract ledger (#1304) goldens regenerated via sanctioned script with stated justification; WIRE_RULES/fakes untouched — ledger not shrunk compliant

③ Code review

④ Test validity

The 9 new tests exercise the real kernel core + real applyRanges fold + real handlePluginManifest (no mocks of the unit under test), and the assertions have teeth: exact output-prefix matches, deepEqual on tool-name sets, doesNotMatch pinning the stale "proxy-mode only" wording away. Tests 610–612 reproduce the issue's exact repro (blank/whitespace/one-missing → must succeed) and would fail on pre-fix code. Minor gap, acceptable: no explicit assertion that whole-block restore succeeds in plugin-CR-off mode — but that equivalence is structural (blank now takes the identical code path as omission) and the reporter confirmed omission works in production.

⑤ Base freshness, diff, CI

  • Base was 42 commits stale (mergeable_state: behind). I rebased onto master tip 75c1e75c — clean, zero conflicts — and force-pushed the rebased head 75e23c17 to the PR branch. That is the only change I made to the branch: history rewrite only, no content edits.
  • Local on rebased head: typecheck ✅ · npm test 3307 run / 3289 pass / 17 skipped ✅ · build ✅. The single local failure was tests/fix-437-idle-timeout.test.ts (在0.1.68版本接入dsh时遇到的问题 #437) — a pre-existing timing flake: it passes 2 of 3 isolated reruns, and neither this PR nor any of the 42 master commits since the merge-base touch src/fetch-util.ts or that test file. It did not reproduce in CI.
  • CI on 75e23c17: all 13 checks green — test matrix (ubuntu 22/24, windows 22/24), real codex e2e, hermetic registry e2e, build-artifact, version-guard, all zero-token lane checks.
  • Diff: 8 files, every line on-purpose (fix + regression tests + justified golden regeneration). No unrelated churn, no lockfile changes.
  • §7.4 auto-merge gate: this touches the wire/tool-shape surface (schema advertisement), which stays human-decided by rule — quality bar met, merge is yours.

Chinese summary: 修复了 decompress 空 range 参数被当成"已提供"而报 "given together"、填了真值又撞过时的 "proxy-mode only" 死循环的缺陷——根因在参数判定(空白=未指定)与工具面无条件广告(改为按 CCR 实际可武装性收敛)两处,均已从源头修复;分支已 rebase 到最新 master(无冲突)、本地与 CI 全绿,可以合并。

@ranxianglei
ranxianglei merged commit 78a8db1 into master Sep 30, 2026
13 checks passed
ranxianglei pushed a commit that referenced this pull request Sep 30, 2026
Rebased onto master (post #1713 NO_RANGE manifest locals, post #1745):
goldens now reflect zero-injection manifests — every conversation_id
param block removed (routing param, search_context cross-session param,
one tool left with empty properties). Regenerated via
scripts/update-wire-contract-goldens.ts; manifest arrays keep the
NO_RANGE-aware locals instead of the pre-rebase plain BILI_ACP_TOOLS_*
arrays.
ranxianglei pushed a commit that referenced this pull request Sep 30, 2026
)

decompress-empty-range.test.ts pinned 'search_context keeps
conversation_id' from master (post-#1713). Under #1692 zero-injection
the param is intentionally gone from every served array — the
rebase surfaced the stale pin; flipped to assert absence.
ranxianglei pushed a commit that referenced this pull request Sep 30, 2026
Rebased onto master (post #1713 NO_RANGE manifest locals, post #1745):
goldens now reflect zero-injection manifests — every conversation_id
param block removed (routing param, search_context cross-session param,
one tool left with empty properties). Regenerated via
scripts/update-wire-contract-goldens.ts; manifest arrays keep the
NO_RANGE-aware locals instead of the pre-rebase plain BILI_ACP_TOOLS_*
arrays.
ranxianglei pushed a commit that referenced this pull request Sep 30, 2026
)

decompress-empty-range.test.ts pinned 'search_context keeps
conversation_id' from master (post-#1713). Under #1692 zero-injection
the param is intentionally gone from every served array — the
rebase surfaced the stale pin; flipped to assert absence.
@ranxianglei
ranxianglei deleted the 2026-09-30_decompress-empty-range branch October 15, 2026 05:32
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.

[Bug] OMP plugin: empty optional decompress range fields cause failures and unsupported-range retries

1 participant