fix(llm): close the truncated-response silent-FN family (adapters + enhancer + verifier) - #207
Merged
Merged
Conversation
gadievron
requested review from
dgeyshis,
shahar-davidson and
sounil
as code owners
August 1, 2026 19:47
…t a verdict Stacked follow-up to the openai-adapter PR (#206). The adapter now honestly reports an abnormal/truncated termination as stop_reason="max_tokens" (BUG-2 responses path, BUG-7 chat path). But finding_verifier's block loop harvested a finish ToolUseBlock regardless of stop_reason (:438-476), so a max_tokens-truncated reply carrying a well-formed finish(agree=False, correct_finding="safe") was parsed as a COMPLETE verdict (incomplete=False) and silently downgraded a Stage-1 vulnerable to safe — the verify-stage tail of the same silent-false-negative family, and one that BUG-7 slightly widens (an unknown finish_reason now reaches this path instead of the end_turn fail-safe). C(b): gate finish-acceptance on stop_reason != "max_tokens"; a truncated finish is treated as verification-incomplete (agree=False, Stage-1 verdict preserved for triage), mirroring the enhancer's degenerate-exit handling. A malformed-mid-JSON finish was already caught (input_dict={} -> agree missing -> incomplete); this closes the narrower fully-formed-finish-under-truncation window (proxy / trailing-tokens / length). C(a): the end_turn+unparseable-text fail-safe (:425) returned without record_call, unlike its three sibling degenerate exits (finish/no-tool-calls/max-iterations) — a per-unit cost undercount. Added the missing record_call. Scoped to utilities/finding_verifier.py (+ tests); no core/parsers/reachability change (82/82 verifier-subsystem tests green; full module suite green in CI). Surfaced by an independent auditor + expert + judge review of the silent-FN family. Tests: +2 (truncated finish -> incomplete; normal finish still accepted). grep -c "def test_" tests/test_verifier_max_tokens_finish_incomplete.py -> 2 verifier-subsystem suite -> 82 passed, 2470 deselected, 1 warning in 0.67s Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ancer Round-2 re-hunt of the verifier fix surfaced three siblings of the same family (a truncated/abnormal response read as a clean/complete result), in code the verifier fix did not touch. The family closes iff two invariants hold everywhere: (1) every adapter emits stop_reason="max_tokens" for truncation, never masking it; (2) every consumer gates finish-acceptance on max_tokens. - R2-A (google.py): a MAX_TOKENS candidate carrying a function_call surfaced as stop_reason="tool_use" (has_tool_use checked before finish_reason), masking the truncation so the verifier/enhancer gate could never fire on Gemini. Now truncation wins over tool_use, mirroring the OpenAI responses path. - R2-B (agentic_enhancer/agent.py): the enhancer accepted a finish call regardless of stop_reason; a truncated finish defaulting security_classification to "neutral" silently drops a unit from the analysed set (a coverage/recall loss). Now a max_tokens finish is INCOMPLETE, mirroring the verifier + this agent's own degenerate-exit handling. - R2-C (google.py, anthropic.py): an unknown/abnormal finish/stop reason defaulted to "end_turn" (the un-ported BUG-7 sibling); now defaults to "max_tokens". Adapter/verify/enhance layers only — no core/parsers/reachability change; the enhancer's reachability fields (is_entry_point/reachable_from_entry/entry_point_path) are passed through unchanged. Capability balance: each trades a sliver of completeness for silent-FN safety; R2-B is coverage-POSITIVE (stops dropping units). Tests: +4 (google adapter family) + 2 (enhancer max_tokens-finish) + updated the anthropic unknown-stop test to the new contract. full module suite -> 2529 passed, 28 skipped (2 pre-existing local zig-subprocess reds) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…omplete survey Round-3 re-hunt exhaustion pass found the third and final invariant-2 consumer gap. explore_repository returned the finish tool payload the moment a finish block appeared, ignoring response.stop_reason (:285). A finish emitted on a turn truncated at the token cap (stop_reason=="max_tokens", partial arguments) was accepted as a complete survey and written into the application-context / threat-model document every later scan trusts — an under-scoped attack surface = silent coverage loss. Skip a max_tokens finish (getattr-guarded so mock/odd adapters without stop_reason are unaffected) so the loop nudges for a complete answer; if it never completes, the existing MAX_TURNS RuntimeError fires — a visible failure, not a silent partial. Mirrors the verifier (finding_verifier.py) and enhancer (agentic_enhancer/agent.py) max_tokens gates, completing invariant-2 across all three finish-consumers. context survey layer only — no core/parsers/reachability change (repo_explorer references zero reachability code). Capability: coverage-POSITIVE (stops silently trusting a partial threat model). Tests: +1 (truncated finish skipped, later complete finish used); 19/19 repo_explorer tests green; full module suite 2530 passed. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…sequence 400) Round-4 re-hunt found that the R3-A gate (30a1e62) introduced a Messages-API sequence violation: on a max_tokens-truncated finish it did `continue`, skipping the finish block WITHOUT emitting a tool_result — but the assistant turn (carrying that finish tool_use) is still appended, so the NEXT complete() sends an unanswered tool_use and real Anthropic/OpenAI reject it with 400. The intended graceful retry / MAX_TURNS exhaustion never happened against a real provider. Not a silent FN (it fails loud), but it defeated the fix's own purpose. Answer the truncated finish's tool_use with a retry-nudge tool_result before `continue`, so the sequence stays valid: the model is asked for a complete, more concise finish, and repeated truncation still exhausts MAX_TURNS -> RuntimeError. The R3-A test passed only because its fake adapter didn't validate tool_use/ tool_result pairing. Strengthened it with a _PairingFakeAdapter that enforces the real Messages-API rule (RED on 30a1e62 -> GREEN here, via /rebugz worktree). context survey layer only; no core/parsers/reachability change. full suite 2530 passed. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…nt-1 completeness) Round-5 dry-round confirmation noted a consistency gap in the R2-A/R2-C google fix: an UNKNOWN/abnormal finish_reason carrying a function_call hit `elif has_tool_use -> tool_use` BEFORE the R2-C `else -> max_tokens` default, so an unknown reason with a tool call would surface as a clean tool_use — a hole in invariant-1 (any abnormal/ unknown termination must be max_tokens). Future-value-only today (no currently-emitted Gemini truncation reason is unmapped-and-non-refusal), so speculative, not a live silent-FN — but it is an inconsistency in the fix's own invariant. Reorder: check `mapped is None` (unknown; refusals already raised above) BEFORE has_tool_use, so unknown -> max_tokens regardless of a tool call. Known non-truncation reasons still map to tool_use (with a tool) or their mapping. Re-derived table: STOP+tool->tool_use, STOP->end_turn, MAX_TOKENS(+/-tool)->max_tokens, UNKNOWN(+/-tool)->max_tokens. Only the unknown+tool cell changed. adapter layer only; no core/parsers/reachability change. full suite 2531 passed. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
gadievron
force-pushed
the
fix/verifier-stop-reason-gating
branch
from
August 2, 2026 01:55
29a5320 to
15d45aa
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Gate the Stage-2 verifier's
finish-acceptance onstop_reason, so a truncated (max_tokens) finish call is not read as a completed verdict. Stacked on #206.Why
#206 makes the OpenAI adapter honestly report abnormal/truncated terminations as
stop_reason="max_tokens"(BUG-2 responses path, BUG-7 chat path). Butfinding_verifierharvested afinishToolUseBlock regardless of stop_reason, so amax_tokens-truncatedfinish(agree=false, "safe")was parsed as a completed verdict and silently downgraded a Stage-1vulnerable— the verify-stage tail of the same silent-false-negative family. BUG-7 slightly widens this path (an unknown finish_reason now reaches it instead of theend_turnfail-safe), so this ships promptly alongside.How
stop_reason != "max_tokens"; a truncated finish → verification-incomplete (agree=False, Stage-1 verdict preserved for triage), mirroring the enhancer's degenerate-exit handling. A malformed-mid-JSON finish was already caught (input_dict={}→ agree missing → incomplete); this closes the narrower fully-formed-finish-under-truncation window (proxy / trailing-tokens /length).end_turn+unparseable-text fail-safe returned withoutrecord_callunlike its three sibling degenerate exits — added the missingrecord_call(per-unit cost undercount).Tests
+2 (truncated finish → incomplete; normal finish still accepted). 82/82 verifier-subsystem tests green; full module suite green. Each fix has a
/rebugzgit-history receipt (RED on base → GREEN on this commit).Compatibility
No breaking change; scoped to
utilities/finding_verifier.py(+ tests). No core/parsers/reachability change.Depends-on: #206
Surfaced by an independent auditor + expert + judge review of the silent-FN family.