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:
- 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.
- 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.
- 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.
The situation
contextual-orchestrator#1118removedModelClient.__init__'s implicit per-request wall-clock timeout:timeout: int = 90at the currently vendored pin414f2297,timeout: float | None = Noneat012beaac. 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/#1892were 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,
#1884heade85fc437, run 34732993973, sidecar stderr artifact10310273053. The serving phase ran 02:41:17Z → 02:56:27Z (907.3 s) and endedHTTP 503. Inside that window the gateway made 15 provider attempts across two agents on two credential accounts:Measured attempt→failure durations:
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 atfailures=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:1240builds the serving client asModelClient(max_output_tokens=..., temperature=...)— notimeoutargument, so it takes the library default.noema-review.yml:261states "No job-leveltimeout-minuteshere, 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#1892each 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:
nvidia_nim/meta/llama-3.2-11b-vision-instructandnvidia_nim_sub/meta/llama-3.2-11b-vision-instruct, bothreadyat preflight) were never attempted during serving while the two deepseek routes were cycled fifteen times.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
#2137advances the pin to012beaacand therefore carries this exposure. I have flagged it there rather than merging past it. Options, for the owner to decide:#2137;#2137and accept a window where a hung provider can hold a review runner for up to 6 hours;#2137with an explicittimeout=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'snoema-reviewfailure. Refs #1915, #2000, #2137, ContextualWisdomLab/contextual-orchestrator#1118.