Skip to content

fix: gate stale estimate baselines out of nudge/preflight sizing (#1492) - #1503

Merged
ranxianglei merged 2 commits into
masterfrom
2026-09-27_stale-estimate-view
Sep 27, 2026
Merged

ranxianglei merged 2 commits into
masterfrom
2026-09-27_stale-estimate-view

Conversation

@ranxianglei

Copy link
Copy Markdown
Owner

What

After an upstream failure with NO usage report, armFailureShrink raises session.stats.lastInputTokens (source=estimate) to ~chars/4 of the body just sent. On the kernel-transform-failure fallback paths that body is the unfolded raw client history — so on a long session the poisoned baseline (1.27M in the issue) survives across turns, because while upstream keeps failing no real usage report ever overwrites it. Three source-blind consumers then applied it cross-turn:

  1. effectiveTokenCount (src/server.ts) fed it straight into decideNudge → false emergency bands every turn (nudge injection + tool-result truncation churn).
  2. The preflight trigger max()'d it against the post-fold payload estimate → preflight fired every turn despite the folded payload fitting the window.
  3. The preflight loop floor (src/preflight.ts) pinned currentTokens at the stale value → summarization rounds burned over a payload whose own estimate fits (the 42–203 s waste), ending in the "payload fits with no compressible ranges" log.

The #857 provenance field (lastInputTokensSource: "usage"|"estimate") already existed; consumers at server.ts:4288 (#496) and :2769 already checked it — these three missed it.

Fix

One rule: an estimate-sourced baseline may floor a sizing decision only while the current payload is unmeasurable (processedMessages.length === 0, i.e. kernel transform failed and the outbound IS the raw body). Usage-sourced baselines keep flooring everywhere — real overflow evidence is untouched.

  • effectiveTokenCount: returns lastInputTokens only when source is "usage"; otherwise falls through to the existing localInputEstimate / raw-upper chain.
  • Preflight trigger: baselineFloor = measured ? (usage ? lastInputTokens : 0) : lastInputTokens.
  • Preflight loop floor: same pattern.
  • armFailureShrink (+ the overflow-arm twin): measure with defaultCountTokens (CJK-aware lower bound) instead of estimateTokensFast (chars/4). ASCII output is byte-identical; CJK bodies now get the lower bound the doc comment always claimed.

Behavior changes (old → new)

Path Old New Why
Nudge band on measured payload estimate baseline floors usage (127% → emergency nudge + truncation) estimate ignored; per-turn localInputEstimate (post-fold view) decides the estimate describes a different (raw) view than the outbound
Preflight trigger on measured payload max(staleBaseline, payloadEstimate) fires preflight stale estimate can't fire it; payloadEstimate alone decides the reported bug: multi-minute waste over a fitting payload
Preflight loop floor on measured payload stale baseline pins currentTokens only usage-grade baselines pin it same wrong view inside the loop
Unmeasured payload (transform failed) baseline floors everything unchanged (literal identity) the raw body IS the payload there — flooring is correct
Usage-sourced baseline floors everywhere unchanged real overflow evidence keeps working (#604 rescue path intact)

Test update (disclosed)

tests/issue847-compress-failure-guards.test.ts: the "honest exhaustion" case set lastInputTokens = 6000 with no provenance (legacy shape). Under the new contract that baseline no longer floors the walk against a measured 1.2K-token payload, so the loop correctly exits without running. Added lastInputTokensSource = "usage" so the case exercises the SAME exhaustion-report path (sub-gate ranges → honest exhausted, zero upstream calls) with a legitimate baseline. All original assertions unchanged.

Regression test

New tests/issue1492-stale-estimate-view.test.ts (real proxy + mock relay, Anthropic wire):

  • A: poison lastInputTokens=1.2M with source=estimate on a folded 37%-window session → asserts NO preflight call, NO nudge, history forwarded unfolded, self-heal to real usage on the recovered turn. Pre-fix this FAILS (preflight fires + emergency nudge injected); post-fix passes.
  • B: same poisoning with source=usage → asserts preflight STILL fires (asymmetry guard against over-correction).

Red proof: git stash push -- src/ → A fails, B passes; pop → both pass.

Pre-flight (rebased head c741120 on master 0f1a0df)

Closes #1492

After an upstream failure without a usage report, armFailureShrink raises
stats.lastInputTokens (source=estimate) to chars/4 of the body just sent. On
kernel-transform-failure fallbacks that body is the UNFOLDED raw client
history, so the poisoned baseline survives across turns while upstream keeps
failing and three source-blind consumers apply it cross-turn:

- effectiveTokenCount fed it to decideNudge -> false emergency bands
- preflight trigger max()'d it against the post-fold payload estimate
- the preflight loop floor pinned currentTokens at the stale value, burning
  summarization rounds over a payload whose own estimate fits the window

Rule now: an estimate-sourced baseline may floor sizing decisions only while
the current payload is unmeasurable (processedMessages empty). Usage-sourced
baselines keep flooring everywhere. armFailureShrink also measures with
defaultCountTokens (CJK-aware lower bound) instead of estimateTokensFast
(chars/4); ASCII output is byte-identical.
@github-actions

github-actions Bot commented Sep 27, 2026 •

Copy link
Copy Markdown
Contributor

📦 Built Package Artifact

Branch: 2026-09-27_stale-estimate-view (ee12524)

Option A — Install this exact build (recommended)

npm install -g billion-context@0.0.1440-pr.1503.c2e2d44

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-pr1503.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

[bot] 🏷 Reviewing PR #1503 (Closes #1492). Pulled the branch: single commit c741120f on master@0f1a0df0, 4 files touched (src/server.ts, src/preflight.ts, new regression test, one disclosed fixture update) — matches the description.

One thing to verify: master has since moved 26 commits past the PR base (web UI rewrite #1426, chain-checkpoint enforcement #1421, etc.), including hunks inside forward() in src/server.ts. Simulating the merge onto current master now and running full pre-flight (typecheck / test / build) plus a red/green proof of the regression test; will report the full review conclusion next.

@ranxianglei

Copy link
Copy Markdown
Owner Author

🤖 Powered by ework · qwen3.8-27b

[bot] 🏷 Review complete — PR #1503 (fixes #1492): 可以合并 (recommend one rebase onto current master first)

① 原 issue 方向 — 打到了根因。 #1492: upstream failure with no usage report → armFailureShrink estimated the unfolded raw fallback body and persisted it into lastInputTokens; three provenance-blind consumers (effectiveTokenCount→nudge band, preflight trigger max(baseline, payloadEstimate), preflight loop floor) then sized a folded ~160K/1M payload at 1.27M for every later failed turn → multi-minute preflight runs saving ~1K tokens. The fix gates all three consumers on the lastInputTokensSource provenance field #857 already introduced (usage-grade always floors; estimate-grade only while the current payload is unmeasured = transform failed) — a consumer-contract fix, not a symptom mask. The estimator swap (chars/4 → kernel defaultCountTokens) separately fixes up to 4× CJK under-count; I verified byte-identity on ASCII against the acp-kernel source (both reduce to Math.ceil(len/4) with zero CJK chars).

② 回归审查 — touched-path 既有行为逐项:

③ 代码审查 — one rule, identically shaped at all three consumers; thorough issue-cited comments; try/catch preserved around body estimation (non-text body → no-op); no as any/@ts-ignore, no console.error; typecheck clean.

④ 测试有效性 — 真测试,断言有牙。 Real end-to-end proxy test (actual HTTP servers, listen(0) deterministic ports, explicit 120K window pin keeping kernel absolute-token zones out of the fixture), not mock-only. I independently reproduced the red/green proof:

  • base 0f1a0df0 + test file only → case A FAILS exactly as designed (AssertionError: 1 !== 0 at tests/issue1492-stale-estimate-view.test.ts:191 — preflight fired on the poisoned 1.2M baseline), case B passes;
  • merged tree (current master 22beabf5 + c741120f, auto-merged cleanly, zero conflicts) → both pass (ok 1080/1081), full suite 2963 tests, 0 fail, 12 skipped (e2e-gated; +21 over the PR's 2942 comes from the newer master), typecheck + build green.
    Case B pins the asymmetry — the fix cannot over-correct into ignoring real overflow evidence.

⑤ diff 整洁度 & CI — single commit, 4 files, every line in scope, no whitespace/lockfile churn. Stale base only: head's base ea40f91f is 29 commits behind current master 22beabf5 (web UI rewrite #1426, chain checkpoint #1421, …) and GitHub hasn't computed the merge state yet — my local merge simulation proves the union is conflict-free and fully green. CI on head sha c741120f: all 13 checks green (real codex e2e, hermetic registry e2e, dsh/pi/codex/opencode lanes, version-guard, ubuntu 22/24 + windows 22/24 matrix, build-artifact).

Verdict: mergeable. Suggest one rebase onto 22beabf5 + a fresh CI pass so the merge ref itself is green (diff is tiny and the simulated merge is clean, so this should be mechanical). Merging stays human-only.

中文摘要:修复了 #1492——上游失败且无 usage 报告时,未折叠原始历史的估算基线(~1.27M)跨回合污染 nudge/preflight 定容、对已折叠的 ~16 万 token 负载空烧数十秒压缩的问题;方案是在三个消费点按 #857 的来源标记门控估算基线,并换用 CJK 感知的默认估算器。已独立复现红/绿证明、本地模拟合并到当前 master 后全量测试通过(2963 项 0 失败)、CI 13 项全绿——可以合并(建议先 rebase 到当前 master)。

 backstop

Follow-ups from review of #1503:

- F1/HIGH: estimateInputTokens floors on lastInputTokens only when
  lastInputTokensSource === "usage". An estimate-grade baseline (raw-view
  poison from a transform-failure fallback arm, #1492/#1493) would otherwise
  mask the payload's own estimate through the max() and let the #453 clamp
  fail open while the poison persists. clampOutgoingOutput now feeds the
  source through (#1498 F5's clamp hole, closed here instead).
- F3: the loop-floor and emergency-band gates share this exact clamping
  mechanism, so they inherit the fix; pinned via the updated backstop test.
- tests/issue453-backstop.test.ts: the real-count test now also pins the
  gate (estimate-grade and unknown-provenance baselines never floor).
@ranxianglei

Copy link
Copy Markdown
Owner Author

Second-round review landed on top (head ee12524, one commit atop c741120f):

Core rule verified sound — usage-grade floor, unmeasurable-gated entry, #604 relay rescue semantics intact, CJK/ASCII estimates byte-equivalent under the F8 simplification. Kept as-is.

Follow-up fixes on the branch (from review):

  1. F1 (HIGH): the #453 output-clamp still trusted a poisoned baseline. estimateInputTokens now floors on lastInputTokens only when lastInputTokensSource === "usage"; clampOutgoingOutput feeds the source through. An estimate-grade baseline (raw-view poison from a transform-failure fallback arm) would otherwise mask the payload's own estimate through the max() and let the clamp fail open while the poison persists — the same emergency shrink / preflight 的本地估算用错了视图:超长会话(客户端历史 127 万,压缩后 outbound 仅 ~16 万)在上游失败无 usage report 时,bili 按原始历史估算 #1492 class, one layer down. (This also closes fix: arm emergency shrink on outbound payload size, not raw body (#1493) #1498-F5's clamp hole from the consumption side.)
  2. F3: the emergency-band and loop-floor gates share this exact clamping mechanism, so they inherit the fix; the updated backstop test pins the gate (estimate-grade / unknown-provenance baselines never floor).
  3. Non-blocking, recorded not fixed here: m1's forced-fire condition is unreachable in-test (foldable content keeps the entry closed) — the gate is pinned at its consumption points instead; m3's loop-floor loop can be closed twice per process in exotic interleavings (benign, second close is a no-op).

Gates: typecheck ✅ · targeted 13/13 ✅ · full suite 2923 pass / 8 fail, all 8 = the known main-repo env drift (node_modules/acp-kernel 0.0.93 vs pin 0.0.95 — goldens + #1414 portability), pre-existing on clean head too; real CI on the branch will run against the pinned install. Mutation-checked: removing the usage-gate condition reddens the new asserts only.

Merge order with #1498: this PR first — #1498 needs a rework on top (its F1 breaks the #604 relay rescue deterministically, its F2 leaves the transform-failure fallback unarmed; see its review comment).

@ranxianglei
ranxianglei merged commit a1a9dba into master Sep 27, 2026
13 checks passed
ranxianglei pushed a commit that referenced this pull request Sep 27, 2026
…arm on the text char bound

Second-round rework after review (on top of #1503):

- F2 (HIGH): outboundPayloadBreakdown now measures
  originalMessages when processedMessages is empty — every protocol's
  kernel-transform-failure catch leaves it empty while the outbound IS
  the raw client body, so the estimator previously returned wire
  overhead only and the rescue never armed in the very scene #1493
  names. Regression test drives the real fallback (broken state seam)
  and pins the arm at >= the raw char bound.
- F1 (HIGH): the arm is not only a preflight floor — the kernel's
  truncation loop consumes it as its remaining-depth input, and the
  optimistic chars/4 text estimate undercounts dense payloads by up to
  ~4x (#553), stopping the loop one candidate early (pre-rebase this
  deterministically re-broke the #604 relay rescue: armed 31101 vs
  31691, retry 116.5KiB vs 108.7KiB, 500 vs 200). armEstimate floors
  the arming quantity on the text char bound (never undershoots,
  deliberately skips image base64 so #1493's pixel-billed fix stands);
  preflight keeps the billing-accurate payloadEstimate so a payload
  that fits does not falsely cross the trigger.
- Rebased onto #1503 (ee12524): its consumer-side provenance gates are
  the root-cause half; this PR now supplies the producer-side fallback
  and depth insurance. All shared suites green together:
  issue1493 3/3, issue1492 2/2, relay-5xx selfheal 3/3, overflow
  selfheal, #453 backstop.
@ranxianglei
ranxianglei deleted the 2026-09-27_stale-estimate-view 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

1 participant