Skip to content

compress: break identical-rejection retry death-loops - #103

Open
ranxianglei wants to merge 1 commit into
masterfrom
2026-08-22_compress-rejection-loop-guard
Open

compress: break identical-rejection retry death-loops#103
ranxianglei wants to merge 1 commit into
masterfrom
2026-08-22_compress-rejection-loop-guard

Conversation

@ranxianglei

@ranxianglei ranxianglei commented Aug 21, 2026

Copy link
Copy Markdown
Owner

Incident

pi session 01a02542 (qwen3.8-27b via local vLLM/SGLang, billion-context-pi 0.1.43 + acp-kernel 0.0.30) entered a compress retry death-loop:

  • After 5 successful compressions nothing compressible remained (22.6K context, 11 active blocks, remaining compressible content 0 chars < min 5000).
  • The min-gate rejection ended with: "Nothing to do. Current active blocks span b1..b11 — retry with startId/endId set to active block IDs in that span."
  • The model re-submitted the identical rejected call () 3853 times over 5h11m (~4.9s/call, ~800 GPU-minutes burned) — it followed the literal "retry" instruction and ignored the "Nothing to do" half. No dedup or circuit breaker existed anywhere.

Fix

  1. Rejection trackingCompressionState.rejections (new optional field; bounded 8-entry FIFO of { spec, count }; keyed on the range signature startRef..endRef list; backward compatible — old persisted states start counting from 1, cloned by cloneState).
  2. Terminal message from the 2nd identical rejection — the gate responds: identical range(s) rejected N times, re-submitting will keep failing, stop calling compress with these refs and answer the user; new content becomes compressible once it exceeds min chars. No retry guidance of any kind.
  3. First-rejection advice kept, de-weaponized — the liveHint now reads "only a block-to-block distillation (startId/endId set to those block IDs) can reclaim more; otherwise nothing remains to compress" — the word "retry" is gone, the genuinely useful tier-2 action is still offered.

Tests

  • New tests/compress-rejection-loop.test.ts (4 cases): first rejection keeps guidance + records count; identical 2nd/3rd rejections flip to terminal (asserts the message contains "rejected N times" and contains NO retry/re-issue/acp_status guidance); different range gets its own counter; caller state not mutated; legacy state without the field counts from 1.
  • Updated the one assertion that pinned the old "retry with startId/endId" wording.

Full pre-flight: tsc --noEmit clean, 395 tests pass, build OK.

Blast radius

Pure kernel gate change — both adapters (billion-context-pi, billion-context-omp) pick it up on their next kernel pin bump. The omp adapter's compress tool needs no change (it forwards kernel errors verbatim).

Session 01a02542 (pi host, qwen3.8-27b via local vLLM/SGLang) re-submitted
the SAME rejected compress call 3853 times over 5h11m (~4.9s/call) after
every compressible range was consumed. Driver: the min-gate error ended
with 'retry with startId/endId set to active block IDs in that span' —
a literal retry instruction that weak models follow verbatim while the
'Nothing to do' half is ignored, with no dedup guard anywhere.

Track identical consecutive gate rejections per range signature in
CompressionState.rejections (optional field, bounded 8-entry FIFO,
backward compatible with old persisted states). From the 2nd identical
rejection the gate message becomes terminal: states the count, says
re-submitting will keep failing, and tells the model to answer the user
instead — no retry guidance of any kind. First rejections keep their
actionable advice, with the liveHint rephrased to drop the word 'retry'
(block-to-block distillation is still offered as the one useful action).
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