Skip to content

fix(noema): raise call_llm HTTP timeout from 120s to org policy - #1509

Closed
seonghobae wants to merge 5 commits into
mainfrom
fix/noema-review-gate-http-timeout-too-short
Closed

fix(noema): raise call_llm HTTP timeout from 120s to org policy#1509
seonghobae wants to merge 5 commits into
mainfrom
fix/noema-review-gate-http-timeout-too-short

Conversation

@seonghobae

@seonghobae seonghobae commented Aug 31, 2026

Copy link
Copy Markdown
Contributor

Summary

noema-review's required check failed with an identical TimeoutError at call_llm (scripts/ci/noema_review_gate.py:656) across at least 4-5 separate check runs on 3+ PRs (ContextualWisdomLab/contextual-orchestrator#965, #958 twice, #960) inside roughly two hours. Investigated whether this was transient infra flakiness or a genuine policy/timeout mismatch.

Confirmed: this is a real policy/timeout mismatch bug, not infra flakiness.

  • call_llm's HTTP request timeout was a hardcoded literal timeout=120 (seconds) — three orders of magnitude short of this org's own recorded policy (docs/product-goal-directive.md line 65): "중앙 OpenCode, Strix, Noema는 모델당 두 시간 이상 걸릴 수 있음을 수용한다" — central OpenCode, Strix, and Noema may legitimately take over two hours per model, and the org explicitly accepts this.
  • The failing call is call_llm's real review-completion request — carrying up to MAX_DIFF_CHARS (60000) + MAX_REVIEW_CONTEXT_CHARS (24000) chars of prompt and requesting a structured multi-part JSON verdict — not the sidecar's own tiny "reply with just 'OK'" preflight smoke test. For the job to even reach call_llm, the sidecar's own /healthz wait and virtual-pool smoke request must already have succeeded, so a TimeoutError here means the gateway was already proven reachable; the much larger real request is what ran past the bound.
  • docs/adr/0005-sidecar-preflight-token-budget.md already reasoned through this exact bug class once, for the sidecar's own smoke-test call (raised 30s → 120s after live reproduction, citing this same policy) — but that reasoning was never extended to call_llm's much larger real review request, which apparently inherited the same 120 literal by copy rather than by its own sizing decision.
  • noema-review.yml's job carries no timeout-minutes at all (confirmed via git log -p — never set), so the effective outer bound is GitHub Actions' 360-minute default. There was no outer-bound reason to keep the inner timeout short.

Fix

Replaced the hardcoded 120 with a named module-level constant, LLM_REQUEST_TIMEOUT_SECONDS = 3600, reusing this org's own already-codified precedent for one model-call attempt rather than inventing a new number: OPENCODE_RUN_TIMEOUT_SECONDS's default of 3600 in scripts/ci/run_opencode_review_model_pool.sh (OpenCode's own per-model-attempt timeout, under the same policy sentence that explicitly names OpenCode too).

call_llm may recurse exactly once (one repair attempt when validate_substantive_verdict rejects the first verdict), so one review's worst case is two attempts at this bound: 3600 × 2 = 7200s (2 hours) — matching the org's stated per-model policy exactly, and matching OpenCode's own analogous OPENCODE_LARGE_CHANGE_TOTAL_BUDGET_SECONDS default of 7200 for the same reason. This worst case still leaves generous headroom under the job's 360-minute default ceiling, so no change to noema-review.yml's timeout-minutes was needed or made.

Developer experience

  • Updated the two existing tests that pinned the old 120 literal (test_noema_review_gate.py::test_call_llm_repairs_one_rejected_changed_line_verdict, test_repository_branch_coverage_review_schedulers.py::test_noema_public_dns_result_reaches_valid_model_response) to assert against noema.LLM_REQUEST_TIMEOUT_SECONDS.
  • Added a dedicated regression test (test_noema_review_gate.py::test_llm_request_timeout_matches_org_two_hour_per_model_policy) pinning both the constant's value and the two-attempt worst-case arithmetic against the org's stated policy.
  • Added a dated docs/product-technical-gap-baseline.md entry (2026-08-31) recording the investigation, evidence, and fix.
  • coverage run -m pytest tests -q && coverage report --show-missing: 2127 passed, 1 skipped, 21 subtests passed; 100% coverage on scripts/ci/.
  • interrogate: 100% docstring coverage.

User experience

No user-facing behavior change for a healthy review — this only widens how long a legitimately slow model completion is allowed to take before noema-review gives up and fails the check, consistent with the org's own stated "accuracy over speed" policy for this exact reviewer.

Test plan

  • coverage run -m pytest tests -q && coverage report --show-missing — 2127 passed, 1 skipped, 100% coverage on scripts/ci/
  • interrogate — 100% docstring coverage
  • CI (required workflows) green on this PR

🤖 Generated with Claude Code

https://claude.ai/code/session_015Gs7KmNvH75nxz1sL8mKjw


Generated by Claude Code

Summary by CodeRabbit

  • 개선 사항

    • LLM 리뷰 처리 시간을 늘려 장시간 리뷰의 안정성을 향상했습니다.
    • 리뷰 결과 생성과 제출을 분리해, 제출 시 최신 인증 정보를 사용하도록 개선했습니다.
    • 리뷰 상태를 저장하고 재개할 수 있는 2단계 처리 흐름을 지원합니다.
    • 제출 전 리뷰 작성자 검증을 강화했습니다.
  • 문서

    • 리뷰 타임아웃 및 제출 안정성 개선 사항을 문서화했습니다.

noema-review's required check failed with an identical TimeoutError at
call_llm (scripts/ci/noema_review_gate.py:656) across 4-5 check runs on
3+ PRs (contextual-orchestrator#965, #958 twice, #960) inside ~2 hours.

The 120-second literal was three orders of magnitude short of this
org's own recorded policy (docs/product-goal-directive.md: "central
OpenCode, Strix, and Noema may take over two hours per model, and the
org accepts this"). The sidecar's own preflight smoke test already
went through this exact bug class once for its tiny "reply OK" probe
(docs/adr/0005-sidecar-preflight-token-budget.md, 30s->120s), but that
reasoning was never extended to call_llm's much larger real review
request (up to MAX_DIFF_CHARS + MAX_REVIEW_CONTEXT_CHARS of prompt),
which inherited the same 120s literal by copy, not by sizing decision.

noema-review.yml's job has no timeout-minutes (GitHub's 360-minute
default applies), so there was no outer-bound reason to keep the
inner timeout short.

Replace the hardcoded 120 with LLM_REQUEST_TIMEOUT_SECONDS = 3600,
reusing this org's own existing precedent for one model-call attempt
(OPENCODE_RUN_TIMEOUT_SECONDS's default in
run_opencode_review_model_pool.sh) rather than inventing a new number.
call_llm recurses at most once (one repair retry), so one review's
worst case is 3600*2 = 7200s (2 hours), matching the org's stated
per-model policy and OpenCode's own
OPENCODE_LARGE_CHANGE_TOTAL_BUDGET_SECONDS=7200 precedent for the same
reason -- well within the job's 360-minute default ceiling.

Update the two tests that pinned the old literal and add a dedicated
regression test pinning the constant and the two-attempt worst-case
arithmetic against the policy, plus a dated gap-baseline entry
recording the investigation and evidence trail.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015Gs7KmNvH75nxz1sL8mKjw
@coderabbitai

coderabbitai Bot commented Aug 31, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

Next included review available in 45 minutes.

View limit details

Limit 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.

Learn how review limits work.

Review configuration:

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 4f07efd0-802d-402d-a654-6b0c78a92ec0

📥 Commits

Reviewing files that changed from the base of the PR and between 0563659 and 3d2b6a8.

📒 Files selected for processing (4)
  • .github/workflows/noema-review.yml
  • docs/product-technical-gap-baseline.md
  • scripts/ci/noema_review_gate.py
  • tests/test_noema_review_gate.py
📝 Walkthrough

Walkthrough

Noema 리뷰 게이트가 LLM 리뷰와 판정 제출을 분리합니다. 리뷰 결과를 상태 파일에 저장합니다. 제출 전에 GitHub App 또는 OIDC 자격 증명을 새로 발급합니다. 제출자는 리뷰 생성자와 일치해야 합니다.

Changes

Noema 리뷰 흐름

Layer / File(s) Summary
LLM 타임아웃 정책과 회귀 검증
scripts/ci/noema_review_gate.py, tests/test_noema_review_gate.py, tests/test_repository_branch_coverage_review_schedulers.py, docs/product-technical-gap-baseline.md
LLM 호출 타임아웃을 7200초로 설정하고 총 예산을 14400초로 정의했습니다. 관련 테스트와 기술 기준 문서를 갱신했습니다.
상태 기반 리뷰 게이트
scripts/ci/noema_review_gate.py, tests/test_noema_review_gate.py, docs/product-technical-gap-baseline.md
리뷰와 제출을 `--phase review
워크플로 인증 갱신 및 제출 분리
.github/workflows/noema-review.yml, tests/test_noema_orchestrator_workflow_contract.py, tests/test_required_workflow_queue_contract.py
리뷰 단계 후 GitHub App 또는 OIDC 제출 토큰을 새로 발급합니다. 별도 제출 단계가 저장된 판정을 제출합니다. 작업 타임아웃을 300분으로 설정하고 워크플로 계약 테스트의 단계 이름을 갱신했습니다.

Estimated code review effort: 4 (Complex) | ~45 minutes

Merge Risk: 🟡 Moderate · up to 05636

The PR allows slower model reviews and uses a split review-submission flow, but it can submit results for an outdated commit, exceed its intended request budget, or expose OIDC credentials through an insufficiently restricted exchange endpoint. These risks can cause stale reviews, failed or overlong checks, or credential exposure, so the PR is not merge-ready until they are fixed or explicitly accepted.

Sequence Diagram(s)

sequenceDiagram
  participant Workflow as noema-review.yml
  participant ReviewGate as noema_review_gate.py
  participant State as review-state.json
  participant Credentials as GitHub App/OIDC
  participant GitHub as GitHub API
  Workflow->>ReviewGate: --phase review
  ReviewGate->>GitHub: LLM 리뷰 및 PR 정보 조회
  ReviewGate->>State: 리뷰 결과 저장
  Workflow->>Credentials: 제출용 자격 증명 재발급
  Workflow->>ReviewGate: --phase submit
  ReviewGate->>State: 리뷰 결과 로드
  ReviewGate->>GitHub: current_actor() 신원 검증
  ReviewGate->>GitHub: 판정 제출
Loading
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 71.43% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 28 functions across 5 files. (2 skipped: … Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed 제목은 call_llm HTTP 타임아웃을 120초에서 조직 정책에 맞게 늘리는 핵심 변경을 정확히 설명합니다. 장시간 모델 요청 지원과 관련된 주요 변경을 간결하게 요약합니다.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Full details: Docstring Coverage

Explanation

Docstring coverage is 71.43% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 28 functions across 5 files. (2 skipped: 2 unsupported.)

✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/noema-review-gate-http-timeout-too-short

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.

❤️ Share

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

@seonghobae
seonghobae marked this pull request as ready for review August 31, 2026 10:54
devin-ai-integration[bot]

This comment was marked as resolved.

…on token

Devin Review found two real bugs in the prior LLM_REQUEST_TIMEOUT_SECONDS=3600
fix on this same PR:

1. The org's "over two hours per model call" policy applies per attempt, not
   split across the at-most-one repair retry. The repair retry only fires on a
   content-validation failure, never because the HTTP call ran long, and it
   resends the same full diff/context as the original attempt (verified by
   reading call_llm's repair-retry prompt construction), so it deserves the
   same budget, not half of it. Raised LLM_REQUEST_TIMEOUT_SECONDS to 7200
   (the full policy bound) and added LLM_REQUEST_TOTAL_BUDGET_SECONDS=14400 to
   document the two-attempt worst case explicitly. Added an explicit
   timeout-minutes: 300 to noema-review.yml's job (previously relying on
   GitHub Actions' implicit 360-minute default), sized above the recomputed
   ~252-minute worst case while staying under the 360-minute hosted-runner
   hard ceiling.

2. noema-review.yml minted its GitHub App/OIDC submission credential once at
   job start and reused it, unchanged, through call_llm and the final
   submit_review POST -- a credential with an ~hour lifetime reused after a
   call that can now legitimately run up to four hours. Split
   noema_review_gate.py's inspect_and_review into run_review_phase (through
   call_llm, persists JSON state) and submit_pending_verdict (submits under
   the current credential, re-verifying the reviewer identity against a fresh
   credential rather than trusting the identity recorded when the verdict was
   computed). noema-review.yml now mints a fresh token between the review and
   submit steps, gated on whether a verdict was actually produced.

Added six new tests for the phase-split and identity re-verification, plus
evidence assertions for the repair-retry sizing decision. Updated two
workflow-contract tests for the renamed step. 2134 tests pass (2133 + 1
pre-existing skip); 100% coverage and 100% docstring coverage on scripts/ci/.

Documented in docs/product-technical-gap-baseline.md (2026-08-31 entry).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015Gs7KmNvH75nxz1sL8mKjw

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note

This report is out of date. Scroll down for Devin Review's latest report on this PR.

Devin Review found 3 new potential issues.

Devin Review

Comment thread scripts/ci/noema_review_gate.py
Comment thread .github/workflows/noema-review.yml
Comment thread scripts/ci/noema_review_gate.py
coderabbitai[bot]

This comment was marked as resolved.

…mit; bound call_llm's total read time

CodeRabbit reviewed this PR and found three real issues, all confirmed
against the current code before fixing:

1. Neither OIDC app-token exchange step in noema-review.yml validated that
   TOKEN_EXCHANGE_URL (a repo variable) actually used https:// before POSTing
   the freshly-minted OIDC identity token to it. A misconfigured http:// value
   would have sent that token in cleartext. Added a `case ... https://*` guard
   using the step's existing fail_unavailable() helper, before either curl
   request, in both the initial and the post-call_llm submission exchange
   steps.

2. submit_pending_verdict re-verified reviewer identity against the fresh
   submission credential (the prior fix on this PR) but never re-fetched the
   PR, so it always submitted against state["pr"]'s now-possibly-stale
   headRefOid. A commit landing during call_llm's now-multi-hour window would
   attach the review to an outdated commit_id, undermining this org's
   exact-head evidence model. submit_pending_verdict now calls fetch_pr again
   immediately before submit_review and aborts (bounded RuntimeError) on a
   head mismatch.

3. call_llm's `opener.open(..., timeout=LLM_REQUEST_TIMEOUT_SECONDS)` only
   bounds the connection phase and each individual socket read, not the total
   time response.read() can spend looping over many such reads to reach EOF
   (confirmed empirically with a real trickling local HTTP server). A slow or
   pathological server could keep one attempt's read phase alive indefinitely,
   blowing past LLM_REQUEST_TOTAL_BUDGET_SECONDS even with a "healthy"
   connection. call_llm now threads a shared time.monotonic() deadline across
   the original attempt and its repair retry; the response body is read
   through a new _read_response_body_within_deadline helper that arms a
   watchdog timer to force-close the socket's read side once the deadline
   passes, converting a still-blocked or interrupted read into the same
   bounded, fail-closed RuntimeError this file already uses elsewhere.

Regression coverage: a real local HTTP server trickling chunks under a tiny
monkeypatched budget (proving the deadline fires mid-read, not just once per
attempt), four focused unit tests on the new read helper's edge branches, one
on call_llm's own pre-attempt budget check, and one proving submit_review is
never called when the head changed between phases. 2141 tests pass (2140 plus
one pre-existing skip); 100% coverage and 100% docstring coverage on
scripts/ci/. noema-review.yml still parses under PyYAML with its original 12
steps and timeout-minutes: 300; all `run: |` blocks pass bash -n.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015Gs7KmNvH75nxz1sL8mKjw

Copy link
Copy Markdown
Contributor Author

Fresh unchanged downstream acceptance canary from ContextualWisdomLab/Orgmetra#40 confirms this timeout mismatch on an independent consumer and gives a protected-main verification target.

  • consumer PR: ContextualWisdomLab/Orgmetra#40
  • exact head: 6917e41f9053fab6f7e99f8185f2137e8fc5fca5
  • independently resolved live base: develop@9e3e4847510e1e612b48474ba42b177b8ed824df
  • Required Noema run: 33294991587, attempt 8
  • failing replacement job: 99436298297 (noema-review)
  • the repository-scoped cwl-noema-review identity/target visibility, trusted sidecar materialization, /healthz, provider-route preflight, and gateway chat/completions preflight all succeed before the substantive call
  • first causal boundary: the real review transaction reaches scripts/ci/noema_review_gate.py::call_llm and then expires at the hard opener.open(..., timeout=120) bound with TimeoutError; no authenticated formal Noema verdict is published for the exact consumer head

This is not an Orgmetra source/test failure and there is no correct consumer-side shim: widening/removing the trusted reviewer transport bound belongs here in the central owner.

Post-integration acceptance: rerun this same unchanged consumer head against the protected central source. The review must no longer terminate at the 120-second inner timeout; it must either complete and publish a valid exact-head Reviews API verdict, or fail closed for a different, explicitly typed substantive cause. A status-only success, predecessor verdict, or timeout hidden behind a green wrapper is non-passing.

#1508 currently overlaps the same timeout=120 owner line with a different timeout policy. Please converge that same-scope overlap in the central writer lane rather than requiring a consumer workaround. No .github source/ref/workflow/settings mutation was made from the Orgmetra writer.

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note

This report is out of date. Scroll down for Devin Review's latest report on this PR.

Devin Review found 2 new potential issues.

Devin Review

Comment thread scripts/ci/noema_review_gate.py Outdated
Comment thread scripts/ci/noema_review_gate.py
Devin Review (#1509) found that call_llm's
shared LLM_REQUEST_TOTAL_BUDGET_SECONDS deadline was being passed
directly to _read_response_body_within_deadline as the read watchdog
for BOTH the original attempt and the repair retry, instead of each
attempt getting its own fresh attempt_start_time + LLM_REQUEST_TIMEOUT_SECONDS
bound. A slow-but-healthy original attempt could therefore run for up
to the full 4h shared budget before being cut off, starving a
subsequent repair retry of its fair 2h share under the org's
per-model-call policy.

call_llm now captures attempt_start_time fresh on every call (original
and repair-retry recursion alike) and derives
effective_deadline = min(attempt_start_time + LLM_REQUEST_TIMEOUT_SECONDS, deadline),
using it for both the connection-level attempt_timeout and the
response-read watchdog. Neither attempt can now individually exceed
its two-hour allowance, while the pair still respects the four-hour
outer backstop threaded through the deadline parameter.

Adds a regression test mirroring the existing trickling-response test
but with the two constants swapped (tiny per-attempt bound, generous
total budget), proving the original attempt is cut off at its own
bound rather than riding out the shared budget. Leaves the informational
write_review_state atomicity note from the same review round untouched
per this PR's scope discipline.

Validation: coverage run -m pytest tests (2141 passed, 1 skipped) +
coverage report --show-missing (100% on scripts/ci/) + interrogate
(100% docstring coverage).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015Gs7KmNvH75nxz1sL8mKjw
devin-ai-integration[bot]

This comment was marked as resolved.

…s can't bypass the deadline

Devin Review found that call_llm's per-attempt deadline enforcement only
protected response.read() -- opener.open() itself (connect, TLS handshake,
request transmission, and status-line/header receipt) had no watchdog, so a
provider trickling response HEADER bytes slowly could keep it blocked well
past effective_deadline (#1509).

Extracts the existing body-read watchdog's arm/expire logic into a shared
_arm_deadline_watchdog helper, then reuses it one phase earlier: a new
_deadline_guarded_connection builds an HTTPConnection/HTTPSConnection
subclass that arms the same watchdog on its own socket the instant
connect() returns, swapped in via two thin handler classes
(_DeadlineHTTPHandler/_DeadlineHTTPSHandler) so build_opener's usual
HTTPHandler/HTTPSHandler defaults are replaced rather than duplicated.
_open_response_within_deadline wraps opener.open() itself, mirroring the
body-read watchdog's three outcomes for the header phase.

Assessed and documented the residual gap this does not close: the TLS
handshake performed inside connect() for https:// targets, which neither
watchdog can reach because ssl's SSLContext.wrap_socket() detaches the
pre-wrap socket's file descriptor before the handshake I/O actually runs.

Adds a real local-http.server byte-at-a-time header-trickle regression test
mirroring the existing body-trickle test, plus 8 isolated unit tests for the
new helpers following the existing fake-double pattern. 2150 tests pass (up
from 2141); 100% coverage and 100% docstring coverage on scripts/ci/.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015Gs7KmNvH75nxz1sL8mKjw

Copy link
Copy Markdown
Contributor Author

Standing down on further timeout/deadline hardening in this PR after this round (commit 3d2b6a8).

This PR has now gone through several rounds addressing real, distinct gaps in call_llm's deadline enforcement, each genuinely earlier in the HTTP request lifecycle than the last: the base 120s→7200s policy fix, a monotonic total-budget deadline on response.read(), a per-attempt deadline so a slow-but-healthy first attempt can't starve the repair retry's fair share, and (this round) extending that same deadline enforcement to opener.open() itself so a peer trickling response headers can't evade it either.

The latest fix's own investigation found one more residual gap, documented in code (_deadline_guarded_connection's and call_llm's docstrings) and in the dated docs/product-technical-gap-baseline.md entry rather than fixed: the TLS handshake performed inside HTTPSConnection.connect() for https:// targets isn't covered by either watchdog, because ssl.SSLContext.wrap_socket() detaches the plain socket early (invalidating any watchdog armed on it) and doesn't return until its own synchronous handshake completes (so nothing can arm a watchdog on the post-wrap SSLSocket earlier either).

I'm treating this as the stopping point rather than dispatching another round, for two reasons:

  1. Different threat model. Every gap fixed so far protects the scenario this org's own policy explicitly anticipates and wants supported — a legitimately slow (but honest) reasoning-model completion taking up to ~2 hours. TLS handshakes complete in milliseconds under normal operation; a trickled handshake requires an actively adversarial or compromised endpoint deliberately stalling at the transport layer, not a slow model. NOEMA_LLM_API_URL targets this org's own vendored contextual-orchestrator gateway, not an arbitrary caller-supplied URL, which narrows this further.
  2. Already has a backstop, and the real fix is more invasive. .github/workflows/noema-review.yml's timeout-minutes: 300 job bound still applies even to a stalled handshake — the failure mode without this gap's fix is "waits out the job timeout instead of failing closed with a clean diagnostic," not "hangs forever." Closing it properly needs manual handshake control (do_handshake_on_connect=False plus a deadline mechanism above the socket layer) — a more invasive change to this module's TLS handling than the incremental watchdog extensions so far, and worth its own focused PR with its own threat-model writeup rather than folding into this one's scope.

Recommend a separate follow-up if the org wants defense-in-depth against a compromised/adversarial TLS endpoint specifically; not blocking this PR.


Generated by Claude Code

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 1 new potential issue.

Devin Review

Comment thread scripts/ci/noema_review_gate.py

Copy link
Copy Markdown
Contributor Author

Flagging cross-PR overlap so we don't duplicate more work on this: #1415, #1511, and this PR are all independently fixing the same contextual-orchestrator#946 call_llm timeout root cause, opened within the same short window.

#1415 is currently the most mature of the three (many additional, unrelated fixes already reviewed across dozens of Devin/CodeRabbit rounds: batched route preflight, family/account cap fixes, startup watchdog timing) and just landed its own fix for this exact bug class: REVIEW_SERVING_MAX_CANDIDATES=10 caps how many preflight-verified candidates the serving orchestrator draws from (separate from preflight's own 24-route admission depth), so CALL_LLM_TIMEOUT_SECONDS=9600 now has an honest worst case that actually fits inside noema-review.yml's timeout-minutes: 360 job ceiling.

Your 7200s-per-call/14400s-total-budget derivation and — more importantly — the credential-expiry-mid-review finding (a ~1-hour GitHub App installation token can outlive a now-multi-hour review, failing the submission step with an auth error even though the review itself succeeded) are genuinely valuable and independent of #1415's fix; #1415 does not address the credential-expiry gap at all. I intend to port the two-phase review/mint-fresh-token/submit flow from this PR into #1415 as a fast-follow.

Before that port, though: CodeRabbit's pre-merge check on this PR flags real, unresolved risk here — submitting results for an outdated commit, exceeding the intended request budget, and a possibly-insufficiently-restricted OIDC exchange endpoint. Those need to be resolved (here or in whatever absorbs this work) before the credential-refresh mechanism is safe to carry over — I'm not treating this PR as ready to merge as-is.

Given the overlap, I'd like to close #1511 as superseded by whichever of #1415/this PR ends up serving as the timeout fix (its timeout=None idea is the same shape as this PR's now-superseded 3600s-then-7200s evolution, just without the deadline/credential work). I'm leaving this PR open rather than closing it now, since its credential-refresh and deadline-watchdog subsystems haven't been ported anywhere yet.


Generated by Claude Code

Copy link
Copy Markdown
Contributor Author

Consumer canary evidence from BandScope confirms this exact causal boundary on the current central protected workflow, without a BandScope-local source failure.

  • consumer PR: ContextualWisdomLab/bandscope#1115
  • consumer exact head: 223d53f6c63dc17f7ce219faa52e5eafbd7719ed
  • consumer protected base: develop@749511c3ad4000090048718f685c6bee6b3d2c25
  • required Noema run/job: 33386655858 / 99470699909
  • trusted central workflow source: .github@1cbb6aaf0a24c3628d24c3dd6d9dcaa8a7eec0c5
  • vendored contextual-orchestrator sidecar: 8cd99f139915131ba0239bce12a5d6a5fd85394e

Exact job evidence: credential mint, trusted-source materialization, repository visibility, sidecar health/provider-route preflight, and gateway chat/completions preflight all succeeded. The real review then reached scripts/ci/noema_review_gate.py::call_llm; urllib.request used the protected-main timeout=120 path and terminated with TimeoutError: timed out after the substantive review request. The run produced no Noema verdict and correctly failed closed. The same consumer head has terminal-success deterministic ci / build-and-test, release-preflight, security-audit, Semgrep/Bandit, dependency review, Trivy filesystem, SBOM/supply-chain, CodeQL and coverage evidence, so shrinking/suppressing the BandScope change would not be a causal repair.

I triggered one bounded exact-job rerun on unchanged bandscope#1115@223d53f... to distinguish a transient from the already-reproduced policy mismatch; do not treat that rerun as acceptance until it reaches a terminal substantive verdict. Please use this unchanged consumer as an additional post-fix canary for #1509: acceptance is a materialized Noema review on exact 223d53f... that is allowed past 120s when necessary and finishes with an authenticated substantive verdict, while malformed/provider failures continue to fail closed.

@seonghobae
seonghobae enabled auto-merge (squash) August 31, 2026 16:43

Copy link
Copy Markdown
Contributor Author

Closing as superseded by #1507.

Both PRs fix the same root cause (call_llm's hardcoded timeout=120 in scripts/ci/noema_review_gate.py, discovered independently via failures on contextual-orchestrator#960/#958/#965). #1507's branch has since absorbed and gone well beyond this fix: it replaces the raw socket timeout with a named NOEMA_LLM_TIMEOUT_SECONDS = 4*60*60 constant plus a proper per-attempt deadline (effective_deadline) computed fresh on every call_llm invocation — including the one-time repair retry — and a header/body-phase watchdog that fails closed instead of hanging, in addition to several other stale-trigger and malformed-response hardening fixes this PR doesn't include.

This PR's opencode-review check has also been stuck on the async dispatch poller (no verdict posted within the 90-minute window) for over an hour on a now-stale head, while #1507 has continued iterating and is close to merge-ready. Rather than run a second, redundant OpenCode review cycle for a strict subset of #1507's change, closing this one and letting #1507 carry the fix to main.


Generated by Claude Code

@seonghobae seonghobae closed this Aug 31, 2026
auto-merge was automatically disabled August 31, 2026 17:09

Pull request was closed

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.

2 participants