Skip to content

Removing the implicit 90 s model timeout leaves review runner occupancy unbounded — needs an admission/continuation bound first #2139

Description

@seonghobae

The situation

contextual-orchestrator#1118 removed ModelClient.__init__'s implicit per-request wall-clock timeout: timeout: int = 90 at the currently vendored pin 414f2297, timeout: float | None = None at 012beaac. That removal is correct — an implicit 90-second cap on the inference path is exactly what ADR-0003's 2026-08-31 amendment forbids and what #1889/#1890/#1892 were reverted for reintroducing. It is not the problem. It is, however, load-bearing today in a way nobody planned for, and this issue is about what has to exist before it goes away.

What the 90 s cap is currently doing

Production evidence, #1884 head e85fc437, run 34732993973, sidecar stderr artifact 10310273053. The serving phase ran 02:41:17Z → 02:56:27Z (907.3 s) and ended HTTP 503. Inside that window the gateway made 15 provider attempts across two agents on two credential accounts:

10x  nvidia_nim     / deepseek-ai/deepseek-v4-flash-0731
 5x  nvidia_nim_sub / deepseek-ai/deepseek-v4-pro-0813

Measured attempt→failure durations:

  90.0s   90.0s   22.0s   90.0s   90.0s
  90.0s   90.0s   90.0s   90.0s    0.0s

Eight of ten land on exactly 90.0 s. That is not the providers answering — it is ModelClient's default timeout firing, over and over. Roughly 720 of the 907 seconds is the cap expiring against endpoints that never respond.

Circuit breaker in the same window: circuit_failure ×10, circuit_opened ×2, circuit_reset ×2, circuit_cleared ×1. The breaker opens at failures=3.0 threshold=3, resets 30 s later, and the ranking hands the same route straight back.

Why removing the cap makes this worse before it makes it better

At the target pin, nothing bounds a single attempt:

  • contextual_orchestrator_review_launcher.py:1240 builds the serving client as ModelClient(max_output_tokens=..., temperature=...)no timeout argument, so it takes the library default.
  • noema-review.yml:261 states "No job-level timeout-minutes here, deliberately."

So a socket that accepts a connection and then delivers nothing has no bound below GitHub's 6-hour job ceiling. Today's failure mode — 15 minutes, bounded, with a clear 503 and a full attempt trace — becomes an indefinite runner hold with one open attempt and no verdict. That is a worse operational outcome even though the change causing it is the correct one.

What must NOT be done

Re-adding a wall-clock timeout. docs/product-goal-directive.md §8 accepts that a model path may take more than two hours and states speed is not a core consideration; #1889, #1890, and #1892 each added a 900-second cap on genuine multi-hour-hang evidence and were all reverted (#1891, #1895). The directive names the alternative explicitly: fix runner occupancy at the admission/continuation boundary instead; never convert elapsed inference time into a model-failure verdict. This issue is not a request to reopen that.

What to build instead

The distinction the reverted PRs collapsed, and which this evidence makes concrete: a model streaming tokens for two hours is healthy; a socket that has delivered zero bytes for fifteen minutes is not inference at all. The bound belongs on progress, not on total elapsed time:

  1. Socket-idle bound, not a deadline. Reset on every byte received. A response actively streaming is never interrupted regardless of total duration; a connection that has produced nothing since the request was written is a black hole and can be abandoned without any claim about how long the model "should" take.
  2. Continuation admission. Once the breaker has opened on an agent, a reset should not immediately re-offer the same route while equally-ranked alternatives have not been tried. In this run, two ready routes (nvidia_nim/meta/llama-3.2-11b-vision-instruct and nvidia_nim_sub/meta/llama-3.2-11b-vision-instruct, both ready at preflight) were never attempted during serving while the two deepseek routes were cycled fifteen times.
  3. Occupancy ceiling at the job boundary, not the inference boundary. If a runner must be released, release it as an explicit occupancy decision with its own event — never as a model-failure verdict attributed to the provider.

Item 1 needs an ADR, since "is an idle-socket bound a wall-clock timeout?" is exactly the question the reverts turned on and it should be settled in writing before code.

Sequencing

#2137 advances the pin to 012beaac and therefore carries this exposure. I have flagged it there rather than merging past it. Options, for the owner to decide:

  • land the occupancy bound first, then #2137;
  • land #2137 and accept a window where a hung provider can hold a review runner for up to 6 hours;
  • land #2137 with an explicit timeout= at the launcher's serving call site — which would just reinstate the reverted cap under a different name, and I am not proposing it.

Found while root-causing #1884's noema-review failure. Refs #1915, #2000, #2137, ContextualWisdomLab/contextual-orchestrator#1118.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions