Skip to content

fix(qa): de-flake behavioral A8 — recovered transient rejection is WARN, not RED (#897) - #1156

Merged
100yenadmin merged 1 commit into
mainfrom
feat/phase0-ws0b-behavioral-deflake
Jun 22, 2026
Merged

fix(qa): de-flake behavioral A8 — recovered transient rejection is WARN, not RED (#897)#1156
100yenadmin merged 1 commit into
mainfrom
feat/phase0-ws0b-behavioral-deflake

Conversation

@100yenadmin

Copy link
Copy Markdown
Member

WS0b — Behavioral-gate false-RED de-flake (closes #897)

The bug

The no_rejected_tool_calls (A8) gate in qa/assert_behavioral.py flipped RED on ANY single tool rejection carrying an extra_forbidden / validation error payload. Behavioral is computed from ONE stochastic duo, so a single recovered transient flub — the DM emits one malformed call, immediately retries the same tool correctly, the session completes cleanly (invisible to the player) — RED-capped EVERY lens to 2.5 and swung the headline RRI by ~1.0. Observed twice (the 27d8002 sweep read RRI 4.5 purely because the DM emitted one bare persist_beat() and recovered).

The fix (precision, not leniency)

Make A8 reflect a pattern, not a recovered blip — mirroring the #1030 discriminator-aware severity (WARN below threshold, FATAL only for the genuine defect). A schema rejection counts toward FATAL only when it is:

  • UNRECOVERED — the offending tool was never successfully called (is_error=False) anywhere in the run → the DM's intent silently never took effect → the real version-skew defect. (this is the existing corpus fixture's shape)
  • REPEATED — the same tool was rejected with a schema/validation error ≥2× → a systematic skew (the DM keeps re-using a stale/wrong signature). Stays FATAL even if a later call eventually succeeds (repetition is the skew signal).

A single rejection of a tool the DM then successfully retried → WARN, never RED (still surfaced, never silently dropped).

This does not weaken any true integrity gate: the unrecovered + repeated classes the gate was built for still flip RED. Benign non-schema engine-guard rejections (engine_guards_hit) are unchanged.

TDD

New qa/test_behavioral_gate_a8_deflake.py (7 cases), written failing-first:

scenario before after
single recovered transient (flub → retry OK) RED ❌ GREEN + WARN
two distinct tools each recovered RED ❌ GREEN + WARN
unrecovered single rejection RED RED (FAIL)
same tool rejected ≥2× (even if later OK) RED RED (FAIL)
mixed: 1 unrecovered + 1 recovered RED RED (FAIL)
benign engine-guard rejection (warn) unchanged WARN, no A8 ✅
clean run (none) no A8 check emitted ✅

Corpus regression lock

Verification (single-process, focused)

qa/test_behavioral_gate_a8_deflake.py          7 passed
qa/test_behavioral_gate_corpus.py             22 passed (19 RED + 2 GREEN + guards)
qa/test_qa_gate.py + test_root_cause_analyzer 45 passed
bash qa/fast_gate.sh                          241 passed — GREEN

Invariants

Additive + default-on for the new precision; reads only DM-stream tool results (engine-mutated ground truth, never fiction); QA-only (no engine/Eva/gateway touch). The behavioral corpus still REDs every genuine failure (player-seated, systematic rejected-tools, dice honesty, dm-output, SRD-correctness, xp).

…RN, not RED (#897)

The `no_rejected_tool_calls` (A8) gate flipped RED on ANY single tool rejection
carrying an extra_forbidden / validation-error payload. Behavioral is computed
from ONE stochastic duo, so a single RECOVERED transient flub (the DM emits one
malformed call, immediately retries the same tool correctly, the session
completes cleanly — invisible to the player) RED-capped EVERY lens to 2.5 and
swung the headline RRI by ~1.0. Observed twice (#897).

Make A8 reflect a PATTERN, not a recovered blip — mirroring the #1030
discriminator-aware severity (WARN below threshold, FATAL only for the genuine
defect). A schema rejection counts toward FATAL only when it is:
  - UNRECOVERED — the offending tool was NEVER successfully called anywhere in
    the run (the DM's intent silently never took effect — the real version-skew
    defect), OR
  - REPEATED — the same tool was rejected >=2x (systematic skew); stays FATAL
    even if a later call eventually succeeds.
A single rejection the DM then successfully retried -> WARN, never RED.

This is a PRECISION improvement, not a leniency hack: the unrecovered + repeated
classes the gate was built for still flip RED. The existing corpus fixture (an
unrecovered update_character rejection) still REDs; the new green corpus fixture
(case_no_rejected_tool_calls_recovered_warn) locks the FATAL->WARN scope guard so
a future re-promotion to FATAL is caught (#1036-style green-case discipline).

TDD: qa/test_behavioral_gate_a8_deflake.py (7 cases) — recovered/two-distinct
transients GREEN+WARN; unrecovered/repeated/mixed still RED+FAIL; benign
engine-guard rejection unchanged; clean run emits no A8 check.

Verified:
  - qa/test_behavioral_gate_a8_deflake.py        7 passed
  - qa/test_behavioral_gate_corpus.py           22 passed (incl. new green case)
  - qa/test_qa_gate.py + test_root_cause_analyzer 45 passed
  - bash qa/fast_gate.sh                        241 passed, GREEN
@coderabbitai

coderabbitai Bot commented Jun 22, 2026

Copy link
Copy Markdown

Warning

Review limit reached

@100yenadmin, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 39 minutes and 6 seconds. Learn how PR review limits work.

Your organization has used up its prepaid credits, and credit purchases are no longer available. Enable the review add-on in the billing tab to keep reviews running — you're only billed for reviews past your plan's rate limits ($0.25/file).

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based credits.

🚦 How do rate limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan refill rate.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, the refill rate gradually slows as usage increases. The highest same-day bursts are limited more strictly.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: a30ef09e-aa34-468c-8601-22cfdb68a992

📥 Commits

Reviewing files that changed from the base of the PR and between 7c78200 and 91566be.

📒 Files selected for processing (7)
  • qa/BEHAVIORAL_GATE_TAXONOMY.json
  • qa/assert_behavioral.py
  • qa/gate_corpus/builder.py
  • qa/gate_corpus/cases/no_rejected_tool_calls_recovered_warn/run.jsonl
  • qa/gate_corpus/cases/no_rejected_tool_calls_recovered_warn/state.json
  • qa/gate_corpus/manifest.json
  • qa/test_behavioral_gate_a8_deflake.py
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/phase0-ws0b-behavioral-deflake

Comment @coderabbitai help to get the list of available commands.

import sys
from pathlib import Path

import pytest
@100yenadmin
100yenadmin merged commit 0140cda into main Jun 22, 2026
21 checks passed
@100yenadmin
100yenadmin deleted the feat/phase0-ws0b-behavioral-deflake branch June 22, 2026 22:24
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.

Behavioral gate is single-duo + any-rejection → high-variance false-RED that false-caps the whole RRI

1 participant