Skip to content

fix(arbitration): prefer provider-anchored usage over calibrated estimate - #214

Open
ranxianglei wants to merge 1 commit into
masterfrom
2026-08-23_arbitration-real-usage
Open

fix(arbitration): prefer provider-anchored usage over calibrated estimate#214
ranxianglei wants to merge 1 commit into
masterfrom
2026-08-23_arbitration-real-usage

Conversation

@ranxianglei

Copy link
Copy Markdown
Owner

fix(arbitration): prefer provider-anchored usage over calibrated estimate

Model: GLM-5.3 (zhipuai-lb, via pi coding agent) — per AGENTS.md rule 2.
Branch: 2026-08-23_arbitration-real-usage (based on origin/master) · Commit: 26d83f2

Problem

Same incident family as #204, but the deepest layer: the arbitration number itself was fiction.

Session 01a02d90 (2026-08-23) dead-looped on 400 (no body) from sglang. Autopsy of the session jsonl:

  • 953/953 assistant turns carried real provider usage (pi requests stream_options.include_usage, parses it, persists it). The final pre-crash turn reported {input: 169, cacheRead: 134400} = 134,569 real context tokens — already past the effective input limit (262,144 − 131,072 maxTokens reserve).
  • Yet the per-turn log arbitrated on tokens=57463 pct=51.8 — a chars/4×density estimate of the sent view, 4.5× below reality.
  • The DensityEstimator was doing its job — acp.log shows density=2.5 on 831 turns, i.e. pinned at its ceiling — but the true ratio was ~4.5, so calibration could never catch up and every threshold (75% nudge, 95% emergency) stayed closed while the provider rejected every request.

The correct number was already in the host: pi's getContextUsage().tokens anchors on the last assistant's provider-reported usage (plus estimated trailing tokens; calculateContextTokens includes cacheRead). On the very turn the loop started it read >100%. The extension fed it only to density.update(), never to arbitration.

Fix

Arbitrate on max(calibrated estimate, provider anchor) — new src/arbitration.ts:

tokenCount = calibrateTokens(sentTokens, densityFor(modelId));
const anchored = providerAnchoredTokens(realUsage, entries, postCompression);
if (anchored !== null && anchored > tokenCount) tokenCount = anchored;

The anchor is consumed only when trustworthy (providerAnchoredTokens, all three guards required):

  1. realUsage.tokens > 0 — pi yields null when it cannot anchor (e.g. right after a pi-side compaction with no post-compaction usage).
  2. Not a post-compression transient turn — the anchor assistant predates an ACP compress; consuming it would false-EMERGENCY right after a successful shrink (omp fix: inline typebox into dist (ACP tags/nudge silently disappeared) #18 family; same guard as density's postCompressionSkip). To enable this, noteActiveBlocks() is hoisted above arbitration (was below the self-heal block; called once per context event, unchanged semantics).
  3. Provider-usage regime confirmed — at least one assistant entry in the merged session view carries a non-zero usage record (same rule pi's compaction getAssistantUsage uses). Without it, pi's number is a whole-tree sum that never shrinks — the permanent false-EMERGENCY regime of omp issue fix: inline typebox into dist (ACP tags/nudge silently disappeared) #18.

Taking max() means a stale-low estimate can never mask a real overflow; the anchored number already includes pi's estimated trailing tail, so it never under-counts what is about to be sent.

Overflow self-heal's armed ≥95% floor still applies after arbitration (unchanged).

Files changed

File Change
src/arbitration.ts (new) providerAnchoredTokens(realUsage, entries, postCompression) + the three-guard rationale.
src/index.ts import; hoist noteActiveBlocks above arbitration; anchoredTokens max-merge; anchoredTokens added to the context-in debug event.
tests/arbitration.test.ts (new) 9 tests.

Tests

npm run typecheck ✅ · npm run build ✅ · npm test421 pass / 0 fail:

  • anchored tokens returned when last assistant carries usage; back-scan skips user/tool entries
  • usage-less assistant tail (aborted/error) falls through to an earlier provider-backed assistant
  • omp fix: inline typebox into dist (ACP tags/nudge silently disappeared) #18: no assistant ever reported usage → null (tree-sum regime distrusted)
  • post-compression transient → null; empty entries → null
  • null/0/NaN tokens → null; totalTokens-only usage counts; all-zero record does not

Relation to other work

…mate

Incident 01a02d90: nudge/emergency arbitration ran on a chars/4×density
estimate of 57K (51.8% of the 131,072 effective window) while the
provider was already rejecting a 134.5K prompt — the density estimator
was pinned at its 2.5 ceiling and could never close a 4.5× gap, so the
75%/95% bands never fired and the session dead-looped on 400s.

pi's getContextUsage().tokens anchors on the provider-reported usage of
the last assistant (cacheRead included via calculateContextTokens), which
was >100% of the window on the very turn the loop started. Arbitrate on
max(calibrated estimate, provider anchor) whenever the anchor is
trustworthy:

- null tokens (pi cannot anchor, e.g. right after pi-side compaction)
  → keep the calibrated estimate
- post-compression transient turns: the anchor assistant predates the
  shrink; consuming it would false-EMERGENCY right after a successful
  compress (omp #18 family) → keep the calibrated estimate
- provider-never-reports-usage regime: pi falls back to summing the
  whole session tree (never shrinks; omp issue #18) → detect via the
  same rule pi's compaction uses (assistant entry with non-zero usage)
  and keep the calibrated estimate

The overflow self-heal 95% floor is unchanged and still applies after
arbitration.
@github-actions

Copy link
Copy Markdown

📦 Built Extension Artifact

Branch: 2026-08-23_arbitration-real-usage (26d83f2)

Option A — Install from npm PR tag (recommended)

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

Each push to this PR publishes a new version under the pr-214 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-pr214.tgz
pi install ./package

This comment is automatically updated on each push.

@ranxianglei

Copy link
Copy Markdown
Owner Author

Review round 1 (agent reviewer, verified against pi-stable internals — verdict: ship):

Healthy path verified safe for prefix cache: the max-merge only raises tokenCount feeding nudge/truncate decisions; nudges are tail-appended (rebuilt.push), previously-sent bytes never change. Tree-sum regime (omp #18) correctly blocked — guard (c) is a superset of pi's validity condition; ~100%-cached sessions anchor via totalTokens (sglang input = max(0, prompt − cacheRead − cacheWrite)).

Minor findings (follow-ups, not blockers):

  • Stale-anchor window: postCompression guard covers exactly one round. If the LLM call right after a successful compress fails (throttle — the realistic case), the next round consumes a pre-compress anchor (≥95%) with no guard → one false emergency (view-only truncation + nudge; originals safe). Same at session resume (noteActiveBlocks returns false when prev === undefined). Hardening: suppress the anchor until an assistant with usage appears after the last compress boundary.
  • Guard (c) vs pi's predicate divergences (stopReason, usage shape) are conservative-only — document-only.
  • max-merge + stale-anchor sequence untested at the index.ts layer.

Suites: 421/421 (#214), 422/422 (#215), typecheck clean.

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.

1 participant