Fix: abort degenerate/repeating generation instead of streaming it to the output cap (#1029) — port of @r0h1tb's #1099 #1797
kaluli123123
started this conversation in
Bug reports
Replies: 0 comments
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Uh oh!
There was an error while loading. Please reload this page.
Credit upfront
This fix's detection algorithm, calibration data, and full test suite (including the end-to-end SSE-replay test) are @r0h1tb's, from #1099 (closed, not merged, via the maintainer's standard backlog-sweep note citing #1165 as "covering" this — the underlying gap is still live on current
main, confirmed below by porting #1099's own tests and verifying they fail on pre-fixmain). This is a port of that PR's diff against currentmain, not independent (re)work. If a maintainer picks this up, #1099 and @r0h1tb should be the credited source.Summary
A thinking stream can settle into emitting the same short phrase back to back thousands of times and never stop on its own: one incident measured 79,222 characters of
"The the the the the the the "before the user aborted by hand. No layer owns this failure class: provider streamers are faithful assemblers that never inspect content, request construction attaches no repetition penalties, and the agent loop treats a completed stream as progress. A runaway response can burn the whole 32k output cap before anything notices. This is #1029.Fix
A new
RepetitionGuard(packages/ai/src/utils/repetition-guard.ts) runs incrementally over the reasoning channel ofopenai-completions(the affected provider in the original incident; the text channel is deliberately left unguarded since a long legitimate answer can contain generated tables/code that look repetitive, and a false abort there destroys real work). Two independent detectors, because a loop rarely stays verbatim for long:0.20novelty floor is calibrated against a measured corpus (verbatim/drifting loops score 0.001/0.136; enumerated analysis, markdown tables, JSON, source code, numbered lists, and prose all score 0.28–1.0), not guessed — the calibration table is reproduced in the module docstring. Word-trigram Jaccard between paragraphs was tried first and rejected: it could not separate drifting loops from legitimate structurally-similar paragraphs at any threshold.On a hit,
streamOpenAICompletionsthrows aStreamFailureErrorclassified as a new"degenerate_output"StreamFailureKind, which the session's existing auto-retry path treats as retryable — the session re-samples instead of committing the garbage to the transcript. Kill switch:PRIME_AGENT_NO_REPETITION_GUARD=1.A second, separate gap closed along the way (also part of #1099's diff):
openai-completions.tswas the only major provider streamer that never calledrecordStreamFailure— every other provider already does. Failures on this path (OpenRouter, llama.cpp, any OpenAI-compatible endpoint) reached the session with no classification at all; the newdegenerate_outputkind depends on this wiring anyway, and fixing it also fixes classification for every existing failure kind on that provider.Tests (ported verbatim from #1099)
repetition-guard.test.ts(21 tests): unit coverage for both detectors, plus an extensive false-positive corpus (source code, markdown tables, numbered lists, JSON, prose, enumerated analysis, whitespace padding, short sub-threshold stutters) that the guard must NOT fire on.openai-completions-repetition-guard.test.ts(4 tests): end-to-end against the realstreamOpenAICompletions, driven by a local SSE server replaying the actual incident (2,366 reasoning deltas of the loop unit) — confirms the stream terminates as an error, classifies asdegenerate_output, and stops after ~2,072 characters against the 79,222 the incident produced; and that the kill switch restores full pass-through.Verified both test files fail against pre-fix
main(repetition-guard.tsdoesn't exist; the e2e test fails to even import) and pass after the port.Validation
npx tsgo -p tsconfig.json --noEmitand the rootnpm run check(biome, tsgo, installer render, browser smoke) both pass. The only manual merge needed against currentmainwas one relocated hunk inopenai-completions.ts(context drift, no logic conflict). Added apackages/ai/.changes/fragment, credited to #1099/@r0h1tb, per the changelog-fragment CI check.Patch
Branch: https://github.com/kaluli123123/prime-agent/tree/fix/repetition-guard-degenerate-output
Diff: main...kaluli123123:prime-agent:fix/repetition-guard-degenerate-output
All reactions