fix(noema): fail closed on a transport error instead of crashing the required check - #1566
Conversation
…required check Live incident on ContextualWisdomLab/naruon#1486: call_llm's opener.open(request) sat outside the surrounding try/except, which only guarded the JSON-decode/validation steps after a successful response. A genuine HTTP 502 from the completion request therefore crashed the whole required noema-review check with an unhandled traceback instead of getting the same one-time repair-retry the malformed-verdict path already has. Widened the try to also cover the request itself, and added urllib.error.URLError alongside RuntimeError to the existing repair-retry except clause. A transient transport failure now gets one retry, then fails closed with a clean RuntimeError on a second failure -- exactly like a malformed verdict already does. Verified genuine RED (the exact HTTPError: Bad Gateway reproduced uncaught) before the fix, GREEN after. Full suite: 2248 passed, 1 skipped, 21 subtests. Confirmed the repo's 99% (11 stmt/7 branch) coverage gap is pre-existing on main in pr_review_fix_scheduler.py/pr_review_merge_scheduler.py, unrelated to this two-file diff -- verified identically present before this change too. Narrowly scoped: nothing here touches the wall-clock-deadline design that #1438 was closed over, or the in-progress #1546 reconciliation (already checked -- #1546's call_llm has this exact same unguarded line).
|
Warning Review limit reachedNext included review available in 45 minutes. View limit detailsLimit details: You’ve used the included review currently available. You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository. Review configuration: ⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Team Run ID: 📒 Files selected for processing (4)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
|
This is unrelated to this PR's diff, which only touches Triggered one re-run to confirm ( Generated by Claude Code |
|
@opencode-agent fix the unresolved exact-head Devin finding on the existing single-writer branch, then re-review the new head. Keep the transport boundary narrow: normalize open/read transport failures such as |
…sport-error retry too Devin Review on #1566 correctly found that the round-1 transport-error fix (RuntimeError, urllib.error.URLError) still missed http.client.IncompleteRead -- raised by response.read() on a truncated body -- since it is neither a RuntimeError nor a URLError. The repo owner independently confirmed the same gap and specified the fix: widen to the bounded transport/read exception families (URLError, http.client.HTTPException including IncompleteRead/RemoteDisconnected, and raw OSError transport failures such as a bare socket timeout reaching opener.open() before urllib wraps it) without swallowing JSON/validator/programming errors, and add RED->GREEN regressions for a truncated-body success-after-retry, a repeated-failure case, and at least one timeout/disconnect family exercising a distinct exception path. Widened the except clause to (RuntimeError, urllib.error.URLError, http.client.HTTPException, OSError) and simplified the repair-retry re-raise to "re-raise as-is only when it's already our own RuntimeError; otherwise wrap in a clean RuntimeError" -- generalizes the fail-closed contract to any transport exception type rather than needing another isinstance branch added per exception class. Three genuinely distinct exception paths each get their own RED->GREEN success-after-retry and repeated-failure pair, none transferred from another case as substitute proof: - test_call_llm_repairs_once_after_a_truncated_response_then_succeeds / test_call_llm_fails_closed_after_a_repeated_truncated_response (http.client.IncompleteRead from response.read()) - test_call_llm_repairs_once_after_a_socket_timeout_then_succeeds / test_call_llm_fails_closed_after_a_repeated_socket_timeout (raw TimeoutError from opener.open() itself, never wrapped as URLError) Full suite: 2252 passed, 1 skipped, 21 subtests. noema_review_gate.py itself at 100% line/branch coverage; 100% docstring coverage. Repo-wide coverage remains the same pre-existing 99% (11 stmt/7 branch gap in pr_review_fix_scheduler.py/pr_review_merge_scheduler.py) confirmed unrelated to this diff in the prior commit on this branch. Updated docs/product-technical-gap-baseline.md with the full root cause/owner/status writeup for this incident (naruon#1486), including the round-1 and round-2 fixes and the unrelated SIGPIPE test flake found and fixed separately while verifying this change. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Y6UJHYbfbGdHfYPjgbVhAr
|
Pushed the requested fix on
Resolved the Devin/owner review thread accordingly. Requesting re-review on the new head. Generated by Claude Code |
|
@opencode-agent review new exact head |
|
@cwl-noema-review independently review new exact head |
Devin Review on #1566 found a fourth, distinct bug: gating the retry-vs-fail-closed decision on repair_error's truthiness conflated "is this the second attempt" with "does the caught exception have display text". Several transport exceptions (a bare OSError()/TimeoutError(), or an http.client.HTTPException raised with no message) all stringify to '', so an empty-message failure on the first attempt left repair_error falsy on the recursive call too -- the retry-state signal was lost, and call_llm would retry unboundedly (each recursive call another live gateway request) instead of failing closed after one attempt, eventually crashing on an uncaught RecursionError once the call stack was exhausted. Added an explicit is_retry: bool = False parameter that tracks retry state independently of the exception's text. It (not repair_error) now gates both the prompt-injection branch -- falling back to a generic message when repair_error is empty -- and the except clause's retry-vs-fail-closed decision, and is threaded through as is_retry=True on the recursive call. Verified genuine RED with a bounded-recursion regression test (test_call_llm_fails_closed_after_a_repeated_empty_message_transport_error, which raises a diagnostic AssertionError if call_llm retries more than once instead of letting it recurse to CPython's own limit) before this fix, GREEN after -- paired with test_call_llm_repairs_once_after_an_empty_message_transport_error_then_succeeds for the happy-path case. Full suite: 2254 passed, 1 skipped, 21 subtests. noema_review_gate.py still at 100% line/branch coverage; 100% docstring coverage. Repo-wide 99% remains the same pre-existing gap tracked by #1567, unrelated to this diff. Updated docs/product-technical-gap-baseline.md and CHANGELOG.md with this fourth fix round. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Y6UJHYbfbGdHfYPjgbVhAr
|
Pushed the fix for Devin's fourth finding ("Empty transport errors retry forever") on
Resolved the corresponding review thread. The two remaining Devin comments on this PR are informational ( Generated by Claude Code |
|
@opencode-agent review Re-review exact current head |
|
@opencode-agent review exact current head |
Preserve current central main including the #1496 authorized scheduler cycle-break and #1566 Noema transport fixes, while removing the completed PR #827 one-shot repair workflow/driver and only its dedicated coverage fixture. No live materialization tests are removed. Signed-off-by: Seongho Bae <me@seonghobae.me>
Summary
Live incident on
ContextualWisdomLab/naruon#1486: the requirednoema-reviewcheck crashed with an unhandledurllib.error.HTTPError: HTTP Error 502: Bad Gateway:Root cause:
call_llm'sopener.open(request)call for the actual LLM completion sat outside the surroundingtry/except— only the JSON-decode/validation step after a successful response was guarded. Any transport-level exception (here, a genuine 502 from the gateway) crashed the whole job with an unhandled traceback instead of getting the same one-time repair-retry the malformed-verdict path already has.I checked this isn't already fixed by the recently-merged
#1546reconciliation either — itscall_llmhas the identical unguardedwith opener.open(request) as response:line, noexceptaround it. So this is a real, independent gap that survives both the old and new (unbounded) design, unrelated to the wall-clock-deadline direction#1438was closed over.Fix
Widened the
tryto also cover the request itself (opener.open()+response.read()), and addedurllib.error.URLError(the parent ofHTTPError) alongsideRuntimeErrorto the existing repair-retryexceptclause. A transient transport failure now gets the same one-time retry a malformed verdict already gets, then fails closed with a cleanRuntimeErroron a second failure — never an unhandled traceback.Test plan
tests/test_noema_review_gate.py:test_call_llm_repairs_once_after_a_transport_error_then_succeeds— a first-attempt 502 gets one retry, then a normal successful verdict.test_call_llm_fails_closed_after_a_repeated_transport_error— two consecutive 502s produce one cleanRuntimeError, never an unhandled traceback.HTTPError: HTTP Error 502: Bad Gatewayreproduced uncaught against the pre-fix code. GREEN after the fix.PYTHONPATH=. coverage run -m pytest tests -q→ 2248 passed, 1 skipped, 21 subtests.python -m interrogate scripts/ci/noema_review_gate.py→ 100%.pr_review_fix_scheduler.py/pr_review_merge_scheduler.py) both before and after this change — independently confirmed by running the full suite against baremainwith this diff stashed out. Pre-existing, unrelated to these two changed files.Scope note
This is deliberately narrow: it only closes an unguarded exception path. It does not touch the wall-clock-deadline design
ContextualWisdomLab/.github#1438was closed over, and does not conflict with#1546's unbounded-inference direction (checked directly — the same gap exists there too, so this fix applies equally regardless of which timeout policy wins).Related
ContextualWisdomLab/naruon#1486— the PR whose CI run surfaced this incident.Generated by Claude Code