typescript-parse-retry-storm-degenerate-output
The solution is written to ~/mafia-ai-benchmark/MAF-GAP-042-solution.md.
I diagnosed the problem class typescript-parse-retry-storm-degenerate-output by cloning the wojons/mafia-ai-benchmark repo (the fix commit 2e7821c is present in its history), reading the pre-fix and post-fix code across all four files, and running the test suite to verify.
Root cause — four compounding weaknesses when an LLM degenerates at late game:
1. getAIResponse recursively retried JSON-parse failures up to maxRetries (3) with a 1s delay — a guaranteed-failing loop = 3s × 10 players per turn with zero persisted events (~30s/turn stall).
2. The say-quality gate deduped per-player only (MAX_REPEAT=2), so the same canned mock phrase surfaced from 4+ different players (each with a fresh counter).
3. Engine console.log / pino JSON chatter wrote to stdout, polluting the adapter's JSON event stream.
4. handleBridgeMessage's switch default case spammed on typeless JSON lines.
Fix (commit 2e7821c):
- game-engine.js: added MAX_PARSE_RETRIES = 1, parse-retry condition now retryCount < Math.min(MAX_PARSE_RETRIES, this.config.maxRetries); network/response_format retries keep maxRetries.
- response-parser.js: added gamePhraseCounts + MAX_GAME_REPEAT = 3 (game-wide cap), with reset() clearing it.
- legacy-bridge.js: process.env.LOG_STRUCTURED='false' + console.log = console.error.bind(console) before loading the engine.
- legacy-game-adapter.ts: skip any message without a string .type.
Verification — all committed test suites pass and behavioral invariants hold:
- packages/shared: 410 passed (incl. new game-wide-cap and reset tests)
- legacy-engine-parser: 21 passed (parse retries capped at exactly 2 calls; network retries still 4)
- legacy-game-adapter: 49 passed (typeless lines ignored, no event count/status change; valid events still processed)
- 4 duplicate canned SAYS collapse to 3; degenerate player costs 2 calls + 1s, not 4 calls + 3s.
# Evidence - Problem class: typescript-parse-retry-storm-degenerate-output - Model: openrouter/deepseek/deepseek-v4-flash-0731 - Solved: 2026-08-17T17:18:58.641Z - Verification: solution produced by pi in sandbox; see signatures.json
{"description": "Legacy game-engine getAIResponse retried JSON-parse failures up to maxRetries (default 3) with a 1s delay per player per turn. When an LLM degenerates (empty SAYS, placeholder THINK) at late game stages, 10 players x 3 retries x 1s = multi-minute stall with zero persisted events and server log spam (JSON parse failed retrying 1/3..3/3). Fix: cap parse-failure retries at ONE (MAX_PARSE_RETRIES=1, min with config.maxRetries) while keeping network-error retries; add a game-wide repeat cap (MAX_GAME_REPEAT=3) to the say-quality gate so canned mock fallback phrases cannot surface 4x+ from different players (per-player dedupe was insufficient); redirect engine console.log to stderr in the bridge process so non-JSON chatter never pollutes the adapter JSON event stream, and skip parsed stdout lines without a string .type in the adapter.", "environment": "node-22-pnpm-monorepo", "language": "typescript", "model": "openrouter/deepseek/deepseek-v4-flash-0731", "problem_class": "typescript-parse-retry-storm-degenerate-output", "provider": "openrouter", "solved_at": "2026-08-17T17:18:58.641Z", "version": "mafia-ai-benchmark"}The solution is written to ~/mafia-ai-benchmark/MAF-GAP-042-solution.md.
I diagnosed the problem class typescript-parse-retry-storm-degenerate-output by cloning the wojons/mafia-ai-benchmark repo (the fix commit 2e7821c is present in its history), reading the pre-fix and post-fix code across all four files, and running the test suite to verify.
Root cause — four compounding weaknesses when an LLM degenerates at late game:
1. getAIResponse recursively retried JSON-parse failures up to maxRetries (3) with a 1s delay — a guaranteed-failing loop = 3s × 10 players per turn with zero persisted events (~30s/turn stall).
2. The say-quality gate deduped per-player only (MAX_REPEAT=2), so the same canned mock phrase surfaced from 4+ different players (each with a fresh counter).
3. Engine console.log / pino JSON chatter wrote to stdout, polluting the adapter's JSON event stream.
4. handleBridgeMessage's switch default case spammed on typeless JSON lines.
Fix (commit 2e7821c):
- game-engine.js: added MAX_PARSE_RETRIES = 1, parse-retry condition now retryCount < Math.min(MAX_PARSE_RETRIES, this.config.maxRetries); network/response_format retries keep maxRetries.
- response-parser.js: added gamePhraseCounts + MAX_GAME_REPEAT = 3 (game-wide cap), with reset() clearing it.
- legacy-bridge.js: process.env.LOG_STRUCTURED='false' + console.log = console.error.bind(console) before loading the engine.
- legacy-game-adapter.ts: skip any message without a string .type.
Verification — all committed test suites pass and behavioral invariants hold:
- packages/shared: 410 passed (incl. new game-wide-cap and reset tests)
- legacy-engine-parser: 21 passed (parse retries capped at exactly 2 calls; network retries still 4)
- legacy-game-adapter: 49 passed (typeless lines ignored, no event count/status change; valid events still processed)
- 4 duplicate canned SAYS collapse to 3; degenerate player costs 2 calls + 1s, not 4 calls + 3s.
# Evidence - Problem class: typescript-parse-retry-storm-degenerate-output - Model: openrouter/deepseek/deepseek-v4-flash-0731 - Solved: 2026-08-17T17:18:58.641Z - Verification: solution produced by pi in sandbox; see signatures.json
{"description": "Legacy game-engine getAIResponse retried JSON-parse failures up to maxRetries (default 3) with a 1s delay per player per turn. When an LLM degenerates (empty SAYS, placeholder THINK) at late game stages, 10 players x 3 retries x 1s = multi-minute stall with zero persisted events and server log spam (JSON parse failed retrying 1/3..3/3). Fix: cap parse-failure retries at ONE (MAX_PARSE_RETRIES=1, min with config.maxRetries) while keeping network-error retries; add a game-wide repeat cap (MAX_GAME_REPEAT=3) to the say-quality gate so canned mock fallback phrases cannot surface 4x+ from different players (per-player dedupe was insufficient); redirect engine console.log to stderr in the bridge process so non-JSON chatter never pollutes the adapter JSON event stream, and skip parsed stdout lines without a string .type in the adapter.", "environment": "node-22-pnpm-monorepo", "language": "typescript", "model": "openrouter/deepseek/deepseek-v4-flash-0731", "problem_class": "typescript-parse-retry-storm-degenerate-output", "provider": "openrouter", "solved_at": "2026-08-17T17:18:58.641Z", "version": "mafia-ai-benchmark"}