Skip to content

fix(overflow): arm self-heal on ambiguous no-body 4xx errors - #215

Open
ranxianglei wants to merge 1 commit into
masterfrom
2026-08-23_overflow-nobody-arm
Open

fix(overflow): arm self-heal on ambiguous no-body 4xx errors#215
ranxianglei wants to merge 1 commit into
masterfrom
2026-08-23_overflow-nobody-arm

Conversation

@ranxianglei

Copy link
Copy Markdown
Owner

fix(overflow): arm self-heal on ambiguous no-body 4xx errors

Model: GLM-5.3 (zhipuai-lb, via pi coding agent) — per AGENTS.md rule 2.
Branch: 2026-08-23_overflow-nobody-arm (based on origin/master @ 5a834ef) · Commit: c355c4e

Problem

Real incident (2026-08-23): an ACP-managed pi session dead-looped. A 50,358-char bash toolResult (~31,475 tokens, ~24% of the effective 131,072-token window) pushed the request past sglang's input + max_tokens ≤ 262,144 hard cap. Every subsequent request returned 400 status code (no body) forever:

  • The model never got a successful turn, so it could never call compress — the ACP loop never shrank the context.
  • The user's "继续" just resent the same oversized context.
  • The extension's overflow self-heal never fired: it only arms on OVERFLOW_MARKER text markers (maximum context length is N, prompt is too long, …), and a bodyless 4xx carries none of them.
  • pi itself classifies this text as overflow (pi-ai OVERFLOW_PATTERNS ends with /^4(?:00|13)\s*(?:status code)?\s*\(no body\)/i), but the extension-side marker set deliberately never matched it — for good reason: OpenAI-compatible providers return the exact same no-body 400 for non-overflow reasons (invalid model, malformed request; see src/messages.ts:80-82), so treating it as an unconditional overflow risks false positives.

Fix

Treat pi's bodyless 4xx ... (no body) error text as a possible overflow signal, and arm the emergency self-heal (forced ≥95% usage → kernel emergency nudge + tool-result truncate) only when at least one false-positive guard holds:

  1. Ratio guard: ACP's current sent-view token estimate ≥ 50% (NO_BODY_ARM_RATIO) of the effective limit — the same pct basis already logged per turn (tokenCount / config.modelContextLimit, recorded per request in the context event, before the armed boost so the guard sees the real estimate).
  2. Consecutive guard: it is the ≥2nd consecutive no-body 4xx since the last successful assistant turn. Count resets on a successful assistant turn and at session start (per-session OverflowEpisode), recovering the dead-loop even when the estimate under-reports (the incident ran at ~24%: the sent-view estimate cannot see the provider's input+max_tokens accounting).

When the body has no parseable limit number (always, for a bodyless error), no window is learned — the armed emergency uses the already-resolved effective limit, so arming never fails for lack of a parsed number.

The classic text-marker arm path (inspectOverflowMessage → learn window + arm) is unchanged.

Files changed

File Change
src/overflow-selfheal.ts NO_BODY_4XX_MARKER + isNoBody4xxError() (mirrors pi-ai's anchored regex); NO_BODY_ARM_RATIO = 0.5; OverflowEpisode.noteSentView/noteSuccess/onNoBody4xx() (returns {arm, consecutive, ratio}); reset() clears the new state. Guard state is runtime/in-memory like the armed flag — not persisted to acp.json (justified in comment: a resumed session re-establishes both within one turn; a persisted count would arm against a fresh session whose first request may succeed).
src/index.ts wireContextTransform: record noteSentView(tokenCount, config.modelContextLimit) before the armed boost. wireOverflowSelfHeal (message_end): non-error assistant turns call noteSuccess(); error turns run the classic marker path unchanged, else the no-body path with the two guards; arms + logs no-body-arm (consecutive, ratio) + UI notify.
tests/overflow-selfheal.test.ts 10 new tests (21 → 31 in file).
CHANGELOG.md Unreleased entry.

Tests

npm run typecheck ✅ · full suite npm test422 pass / 0 fail (was 412; +10):

  • isNoBody4xxError matches 400/413 status code (no body), 400 (no body), 413(no body); rejects 429/500/404 (no body), quota text, empty, and prefixed text (anchored, like pi-ai).
  • no-body at low estimate (~24%, incident scale) + first occurrence → NO arm.
  • no-body at ≥50% estimate → arm on first occurrence (incl. exact-threshold >= case).
  • first no-body at low estimate, then a second consecutive no-body → arm (dead-loop recovery).
  • classic maximum context length is 262144 ... → still arms via the text-marker path, window parsed (regression).
  • successful turn resets the consecutive count; OverflowEpisode.reset() clears sent-view + count; fresh episode (no recorded sent view) does not arm on the first hit.

Notes

pi surfaces a bodyless provider 4xx verbatim as '400/413 status code
(no body)' — pi-ai's own classifier treats it as overflow (anchored
regex /^4(?:00|13)\s*(?:status code)?\s*\(no body\)/i), but
OVERFLOW_MARKER never matched it, so the extension-side self-heal
never armed. Incident 2026-08-23: a 50,358-char bash toolResult
(~31.5k tokens of a 131,072 effective window) pushed every request
past sglang's input+max_tokens cap; each retry returned the no-body
400 forever, the model never got a successful turn to compress, and
user 'continue' just resent the same oversized context (dead loop).

Treat the no-body text as a POSSIBLE overflow, armed only with
corroboration (the same text serves non-overflow 4xx — invalid model,
malformed request; see messages.ts):
- sent-view estimate >= 50% of the effective limit (same pct basis as
  the turn log), OR
- >= 2nd consecutive no-body 4xx since the last successful assistant
  turn (count resets on success/session start; runtime in-memory like
  the armed flag — not persisted).

A bodyless error parses no window, so none is learned: the armed
emergency uses the already-resolved effective limit. The classic
text-marker arm path is unchanged.

Relates #204; complements acp-kernel #133.
@github-actions

Copy link
Copy Markdown

📦 Built Extension Artifact

Branch: 2026-08-23_overflow-nobody-arm (c355c4e)

Option A — Install from npm PR tag (recommended)

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

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

This comment is automatically updated on each push.

@ranxianglei

Copy link
Copy Markdown
Owner Author

Review round 1 (agent reviewer — verdict: ship):

  • Regex matches pi's real surface form: openai-completions.js:451 passes the openai SDK's verbatim 400 status code (no body) with no prefix; pi-ai's own classifier uses the identical anchored regex. 401/403/429 can't match.
  • Healthy path unchanged: success branch only adds noteSuccess(); nothing outbound; state in-memory only.
  • Emergency fires only during genuine recovery (armed consumed once per arm; success resets; no healthy-path rewrite of old messages).

Minor:

  • False-positive surface is real but gated: persistent non-overflow bodyless 400s are documented in-repo (GLM empty-assistant quirk, messages.ts:81; body-stripping relays). ≥2-consecutive arms → per-turn view truncation + nudge spam until the first success; originals survive in the session file. Accepted trade for the dead-loop it fixes.
  • No wiring-level test pinning noteSentView before the armed boost (property exists only as a comment).
  • Merge order vs fix(arbitration): prefer provider-anchored usage over calibrated estimate #214 (guaranteed src/index.ts conflict): place ov.noteSentView(...) after fix(arbitration): prefer provider-anchored usage over calibrated estimate #214's anchored max-merge and before the armed boost — then the ratio guard sees the provider-anchored basis and arms on the first no-body error in an incident (anchored ~100% ≥ 50% immediately), instead of waiting for the second.

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