docs(mt#4996): Settle the 120s toolloop timeout: accept the cadence, record it at the constant - #3650
Conversation
… at the constant mt#1897 measured the failure; this chooses the remedy and records it where a reader lands. No behaviour changes: a decision-record comment on DEFAULT_MODEL_TIMEOUT_MS and a README section, nothing else. Measured over 53,038 completing rounds (all reviewer history): p50 7.7s, p99.9 105.0s, max 118.0s — the cap sits above p99.9, so lowering it would start truncating real work. And raising it buys nothing: a round at the cap returned nothing at all (every failure at round=0; PR #3625 burned 4x120s across two retry layers and completed no round). The retry stack is the remedy and it works — 133 of 134 timeout events recovered, 1 unrecovered in 103 days. Also corrects two things in the README that were wrong rather than merely stale: the tuning advice told operators to lower reasoning_effort when model timeouts fire, which would cost review quality without reducing timeouts since the failing rounds are not running long; and the 2026-05-24 latency figures. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YT3CvxJhrcGeVCv8P3DGPD
Minsky Reviewer StatusVerdict: APPROVED — no blocking findings Commands
|
There was a problem hiding this comment.
Independent adversarial review (Chinese-wall)
Reviewer: minsky-reviewer[bot] via openai:gpt-5
Tier: 2
Reviewed the documentation-only PR that records a decision to keep the 120s toolloop timeout and corrects prior operator guidance. No executable behavior changed; constants and retry logic remain untouched. The additions are well-argued and align with the task spec’s success criteria. I found two non-blocking issues: embedding point-in-time metrics in a code comment is brittle and will stale; consider summarizing invariants and linking to the canonical query, and clarify in README that while the env knob exists, production policy is not to tune it absent the named triggers. With those nits noted, the PR meets the spec and can merge.
Findings
- [NON-BLOCKING] services/reviewer/src/providers.ts:46 — Hard-coded live metrics in a source comment are brittle and will stale quickly
The new docblock aboveDEFAULT_MODEL_TIMEOUT_MSembeds specific counts and distribution figures (e.g., “53,038 completing rounds… p99.9 105.0s… 133 of 134 recovered… 1 unrecovered in 103 days”). These will drift as soon as more data lands and risk becoming misleading over time. Consider summarizing the invariants (e.g., “cap > p99.9 over completing rounds; retries recover ≈99%+ of events; do not lower/raise without X triggers”) and linking to the canonical query/spec section instead of freezing point-in-time numerics in code. If you keep the figures, include an explicit as-of date (the README does; mirror that here) and note they are illustrative, with the durable reopen triggers listed separately. - [NON-BLOCKING] services/reviewer/README.md:574 — Operator guidance could clarify that the timeout is configurable but should not be tuned in normal ops
The new subsection strongly says “do not tune it,” which is fine as a policy, but the environment knob (REVIEWER_MODEL_TIMEOUT_MS) remains present and documented. To reduce confusion for operators, consider adding a one-line clarification that the env var exists for parity/testing but production policy is to leave it at 120s unless the named reopen triggers fire. This avoids a perceived contradiction between a visible knob and a “never change it” stance.
Spec verification
| Criterion | Status | Evidence |
|---|---|---|
| One option above is chosen, with the choice and its basis recorded — citing the measurement, not re-deriving it. | Met | README adds “The 120s model timeout is settled — do not tune it (mt#4996)” selecting option (3) and cites concrete measurements; providers.ts docblock records the same. See services/reviewer/README.md:“### The 120s model timeout is settled …” and services/reviewer/src/providers.ts:46-81. |
| SC5 of mt#1897 is honored: no change to MAX_TOOL_ROUNDS or the retry policy ships without coordinating with mt#2718 and mt#3526. Record the coordination outcome, including “no change needed” if that is the answer. | Met | README subsection explicitly states no code changes and records coordination outcome; PR body likewise. No edits to MAX_TOOL_ROUNDS, retry policy, or timeouts appear in the diff (providers.ts only adds comments). Evidence: services/reviewer/src/providers.ts shows constants unchanged; README records no-change coordination. |
| If a timeout value or budget changes, a regression guard exists (mt#1897 AT4) keyed to the in-regime, completing-rounds percentiles — never to a population containing capped rows. | N/A | No timeout or budget changed in this PR (diff shows comments/README only), so AT4/SC3 does not apply. |
| If option (3) is chosen, the justification is written into this spec and mt#1897 is cross-referenced, so a future burst does not reopen the question from scratch. | Met | README subsection cites mt#4996 and mem#1373, describes measurement, justifications, and reopen triggers; providers.ts docblock cross-references mt#1897/mem#1373. See services/reviewer/README.md section and services/reviewer/src/providers.ts comment block. |
| The chosen remedy is verified against a real burst or a faithful reproduction — a hang, not merely a slow response. | Met | README and comment cite recovery across real bursts (e.g., 2026-08-18: 27/27 recovered) and 133/134 recovered total; although no code changes, verification is by historical production events, which meets SC5. Evidence text embedded in README and providers.ts comments. |
Documentation impact
- updated-in-pr — This PR is documentation-only: it edits
services/reviewer/README.mdand adds an extended decision-record comment inservices/reviewer/src/providers.ts. The README explicitly updates operator guidance under “Network-call timeouts” and adds a new subsection “The 120s model timeout is settled,” correcting prior advice. No executable behavior changed, but documented behavior/guidance did — and the docs were updated in this PR accordingly.
Affected: services/reviewer/README.md
…he knob still a knob Both non-blocking findings adopted. The metrics-staleness one is squarely about this artifact's failure mode over time, so the fix says so explicitly rather than removing the numbers: they are recorded as the BASIS for the decision, nothing reads them at runtime, and what to re-measure is the reopen triggers. Keeping them is deliberate — a pointer is what the three mt#1897 passes had, and each re-derived the population and got it wrong. The README one: say plainly that REVIEWER_MODEL_TIMEOUT_MS stays configurable and is not hard-coded — the knob exists for an incident and for whoever reopens this — it just should not be tuned in normal operation. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YT3CvxJhrcGeVCv8P3DGPD
There was a problem hiding this comment.
Independent adversarial review (Chinese-wall)
Reviewer: minsky-reviewer[bot] via openai:gpt-5
Tier: 2
Verification complete. The follow-up commit confines changes to documentation-only updates: a settled-decision subsection in services/reviewer/README.md and an expanded docblock on DEFAULT_MODEL_TIMEOUT_MS in services/reviewer/src/providers.ts. Both prior R1 non-blocking notes are addressed by explicitly marking the cited figures as a dated snapshot and clarifying that the knob remains configurable but should not be tuned in normal operation. No executable code, constants, or configuration were modified. Success criteria are satisfied: option (3) is chosen and recorded with measurements; coordination with mt#2718/mt#3526 is documented; regression guard is N/A since no timeout/budget changed; mt#1897 and mem#1373 are cross-referenced; and the chosen remedy is evidenced against real bursts. I find no new defects introduced by this commit. Event: APPROVE.
Spec verification
| Criterion | Status | Evidence |
|---|---|---|
| One option above is chosen, with the choice and its basis recorded — citing the measurement, not re-deriving it. | Met | services/reviewer/README.md: “### The 120s model timeout is settled — do not tune it (mt#4996)” records option (3) with measurements; services/reviewer/src/providers.ts:46-88 docblock records the decision and cites figures as a dated snapshot. |
| SC5 of mt#1897 is honored: no change to MAX_TOOL_ROUNDS or the retry policy ships without coordinating with mt#2718 (cost) and mt#3526 (round budget). Record the coordination outcome, including "no change needed" if that is the answer. | Met | services/reviewer/README.md: the settled-decision section states no timeout, round budget, or retry policy changed and records the coordination outcome; the task spec’s SC2 text is mirrored in README prose. |
| If a timeout value or budget changes, a regression guard exists (mt#1897 AT4) keyed to the in-regime, completing-rounds percentiles — never to a population containing capped rows. | N/A | No timeout or budget change ships in this PR; README explicitly says no code changed and advises not to tune the knob. Therefore AT4/SC3 does not apply. |
| If option (3) is chosen, the justification is written into this spec and mt#1897 is cross-referenced, so a future burst does not reopen the question from scratch. | Met | Task spec body includes the full decision rationale; README section cites mt#4996 decision and mem#1373; providers.ts docblock references mt#1897/mem#1373. Cross-references present. |
| The chosen remedy is verified against a real burst or a faithful reproduction — a hang, not merely a slow response, since the two are what this whole investigation had to separate. | Met | services/reviewer/README.md settled section enumerates 134 timeout events with 133 recoveries and cites round-latency distribution and the 2026-08-18 burst. The remedy is the existing retry stack exercised by multiple real bursts; no new mechanism to reproduce needed. |
Documentation impact
- updated-in-pr — This PR is purely documentation/prose: it adds a settled-decision section in services/reviewer/README.md and a decision-record docblock on DEFAULT_MODEL_TIMEOUT_MS in services/reviewer/src/providers.ts. It corrects prior tuning advice and records the chosen option (accept the cadence) with dated measurements. No executable behavior changed, and the docs were updated in this PR to reflect the decision.
Affected: services/reviewer/README.md, services/reviewer/src/providers.ts
There was a problem hiding this comment.
Independent adversarial review (Chinese-wall)
Reviewer: minsky-reviewer[bot] via openai:gpt-5
Tier: 2
Verification pass complete. The fix commit confines changes to documentation-only updates: an expanded decision-record docblock on DEFAULT_MODEL_TIMEOUT_MS in services/reviewer/src/providers.ts and a new settled-decision subsection plus tuning-guidance correction in services/reviewer/README.md. No executable code, constants, or configuration were modified; MAX_TOOL_ROUNDS, retry logic, and timeout values remain unchanged. Success criteria are satisfied: option (3) is chosen and justified with cited measurements; coordination with mt#2718/mt#3526 is recorded; regression guard is N/A since no budget changed; cross-references to mt#1897/mem#1373 are present; the remedy is evidenced against real bursts. I find no new defects introduced by this commit. Event: APPROVE.
Spec verification
| Criterion | Status | Evidence |
|---|---|---|
| One option above is chosen, with the choice and its basis recorded — citing the measurement, not re-deriving it. | Met | services/reviewer/README.md:574-626 adds “The 120s model timeout is settled — do not tune it (mt#4996)” selecting option (3) and cites corpus-wide percentiles and recovery counts; services/reviewer/src/providers.ts:46-86 mirrors the decision and records figures as a dated snapshot. |
| SC5 of mt#1897 is honored: no change to MAX_TOOL_ROUNDS or the retry policy ships without coordinating with mt#2718 (cost) and mt#3526 (round budget). Record the coordination outcome, including "no change needed" if that is the answer. | Met | PR description and README subsection explicitly state no changes to MAX_TOOL_ROUNDS or retry policy; coordination outcome “no change needed” is recorded in the PR body under “SC2 — coordination with mt#2718 and mt#3526”. No code changes to those constants in services/reviewer/src/providers.ts (MAX_TOOL_ROUNDS remains 10; retry logic unchanged). |
| If a timeout value or budget changes, a regression guard exists (mt#1897 AT4) keyed to the in-regime, completing-rounds percentiles — never to a population containing capped rows. | N/A | No timeout or budget was changed. README and providers.ts emphasize no behavior/config change; only documentation/decision record added. |
| If option (3) is chosen, the justification is written into this spec and mt#1897 is cross-referenced, so a future burst does not reopen the question from scratch. | Met | services/reviewer/README.md:590-626 includes the justification, cites mem#1373 and mt#1897; providers.ts docblock at DEFAULT_MODEL_TIMEOUT_MS also references mt#4996 and mem#1373. |
| The chosen remedy is verified against a real burst or a faithful reproduction — a hang, not merely a slow response, since the two are what this whole investigation had to separate. | Met | README subsection summarizes recovery over 134 timeout events with 133 recovered, cites specific burst (2026-08-18) and PR #3625 sequence; providers.ts docblock repeats recovery evidence. No new tests required per amended ATs since option (3) ships no mechanism. |
Documentation impact
- updated-in-pr — This PR is documentation-only: it adds a settled-decision subsection and corrects prior guidance in services/reviewer/README.md (lines ~574-626) and expands the docblock over DEFAULT_MODEL_TIMEOUT_MS in services/reviewer/src/providers.ts (lines ~46-86). Behavior and constants remain unchanged.
Affected: services/reviewer/README.md
There was a problem hiding this comment.
Independent adversarial review (Chinese-wall)
Reviewer: minsky-reviewer[bot] via openai:gpt-5
Tier: 2
Verification complete. This fix commit confines changes to documentation: an expanded decision-record docblock adjacent to DEFAULT_MODEL_TIMEOUT_MS in services/reviewer/src/providers.ts and a new settled-decision subsection (plus corrected tuning guidance) in services/reviewer/README.md. No executable code, constants, or retry/round budgets were modified. The task’s Success Criteria are satisfied: option (3) is explicitly chosen and justified with corpus-wide measurements; coordination with mt#2718/mt#3526 is recorded as “no change needed”; regression guard is N/A since no timeout/budget moved; mt#1897/mem#1373 are cross-referenced; and the chosen remedy is evidenced against real bursts. I find no new defects introduced by these changes. Event: APPROVE.
Spec verification
| Criterion | Status | Evidence |
|---|---|---|
| One option above is chosen, with the choice and its basis recorded — citing the measurement, not re-deriving it. | Met | services/reviewer/README.md: "The 120s model timeout is settled — do not tune it (mt#4996)" subsection records option (3) and cites corpus-wide measurements (p50 7.7s, p99.9 105.0s, 133/134 recovered). services/reviewer/src/providers.ts:46-86 adds the decision-record docblock with the same basis. |
| SC5 of mt#1897 is honored: no change to MAX_TOOL_ROUNDS or the retry policy ships without coordinating with mt#2718 (cost) and mt#3526 (round budget). Record the coordination outcome, including "no change needed" if that is the answer. | Met | services/reviewer/README.md: in the settled-decision subsection, it explicitly states no code changes and defers the Responses API to mt#2718 as a cost lever; it does not alter MAX_TOOL_ROUNDS or retry policy. The PR body and spec also record "no change needed" coordination. No diffs touch those constants in services/reviewer/src/providers.ts beyond comments. |
| If a timeout value or budget changes, a regression guard exists (mt#1897 AT4) keyed to the in-regime, completing-rounds percentiles — never to a population containing capped rows. | N/A | No timeout or budget changed in this PR (comments and README only). DEFAULT_MODEL_TIMEOUT_MS remains 120_000; MAX_TOOL_ROUNDS untouched. Therefore AT4 does not apply. |
| If option (3) is chosen, the justification is written into this spec and mt#1897 is cross-referenced, so a future burst does not reopen the question from scratch. | Met | services/reviewer/README.md: the subsection cites mt#4996 and mem#1373 and explains why lower/raise are wrong; providers.ts docblock references mt#1897/mem#1373 rationale. Cross-references present and placed at the constant docblock to catch future readers. |
| The chosen remedy is verified against a real burst or a faithful reproduction — a hang, not merely a slow response, since the two are what this whole investigation had to separate. | Met | services/reviewer/README.md settled-decision subsection cites real-burst stats: 134 timeout events with 133 recovered; 2026-08-18 burst recovered 27/27; recovered retries landing in 2.7s–95.3s versus 120s capped attempts. providers.ts docblock repeats PR #3625 four-attempts evidence at round=0 supporting hang vs slowness. |
Documentation impact
- updated-in-pr — This PR intentionally ships documentation-only changes: an expanded decision-record docblock on DEFAULT_MODEL_TIMEOUT_MS in services/reviewer/src/providers.ts and a new settled-decision subsection plus corrected tuning guidance in services/reviewer/README.md. No executable behavior changed, and the docs now reflect the settled 120s timeout posture and corrected advice.
Affected: services/reviewer/README.md, services/reviewer/src/providers.ts
…ecide-and-forget ## Summary mt#4988 was filed to build burst detection plus a pipeline capturing perishable Railway deploy logs. **mt#4996 (merged today, PR #3650) removed the reason for both**, and this PR ships what survived. The task was explicitly queued behind that decision — mt#5006 filed it as *"Reconsider scope first: largely obviated if mt#4996 accepts the cadence."* The re-scope is recorded as `## AMENDMENT 2026-09-05` on the task, with superseding criteria and the originals left visible. **Retired.** The Railway-log capture lost its consumer: mt#4996's three reopen triggers are all answerable from `review_timing` alone, so nothing reads a deploy log. And surfacing every recovered burst became alert noise — a recovered burst is now documented expected behaviour at a ~13-day cadence, and mt#2719's SC5 already excludes `provider_timeout` from paging on a self-healing premise mt#4996 measured holding at 99.25%. **Shipped.** mt#4996's accept installed three thresholds and **nothing evaluated them** — a gap created today. Only one had partial coverage: `reviewer-pre-submit-failure/v1` (mt#4881) pages per-occurrence on an unrecovered timeout, covering trigger 1's single case but not its aggregate, and neither of the other two, which are distributional and would go unnoticed indefinitely. ## Key changes - **`services/reviewer/src/timeout-regime-watch.ts`** (new) — a daily in-process check over a rolling 30 days of `review_timing`, following `findings-aggregation.ts`'s scheduler shape (`enabled` flag, `parsePositiveIntEnv`, `isRunning` re-entrancy guard, cycle never throws). The evaluator is a pure function of its inputs; the query takes an injected `nowMs` with a real default. Records all three readings against their thresholds every cycle, and notifies only on a **new** crossing. - **`services/reviewer/src/server.ts`** — registration beside the other schedulers, opt-in via `TIMEOUT_REGIME_WATCH_ENABLED`. - Defaults are mt#4996's recorded values, not round numbers: 2 unrecovered events, 95.00% recovery, 115s p99.9, 30-day window, 118s completing-round cap. **Deliberately NOT an operator incident.** `ask-emitter.ts`'s operator-incident path renders *"Reviewer is down — …"* and *"Only you can clear this — the reviewer cannot recover on its own."* Both are false here: nothing is down, and the remedy is re-running mt#4996's analysis, which an agent can do. So this notifies through the existing `AlertSink` at `warn`, and the body says so outright — *"Nothing is broken and no one is paged"* — with a test asserting that sentence so the framing cannot erode later. **The `OperatorIncidentContext` union is untouched**, so the consumer set the spec enumerated for it is not disturbed and no contract propagates. Recorded on the task as `## Implementation record 2026-09-05`. ## Two edge cases the thresholds turn on - **A quiet window reports recovery as not-computable, never as a number.** Zero timeout events is the common case in this corpus. Reporting 0% would fire the trigger on every quiet window; reporting 100% would make it unfalsifiable exactly where there is nothing to measure. The value is `null`, and a null never crosses. - **The p99.9 is computed only over rounds BELOW the cap.** A round recorded at the cap is censored by the timeout mechanism rather than measured (mem#1373) — the correction that changed mt#1897's conclusion after three passes computed percentiles over cap artifacts. Encoding it here keeps the trigger from inheriting the same error. ## Correction to the spec's own burst figures The summary asserted *"37 of 47 timeouts fall on 6 of 42 days; bursts arrive roughly monthly."* Re-run under the spec's own `tool_use_active is true` filter: **7 burst days in 93** (≈ one per 13 days, so *more* frequent than monthly) and **59 of 115 timeout rows on burst days** (51%, so *less* concentrated than 79%); over the same last-42-day window, 2 burst days and 24 of 39 rows, not 6 and 37. Recorded as measured, **not diagnosed** — this pass did not establish why the inherited numbers differ, and says so rather than guessing. ## Acceptance tests AT1, AT2 and AT4 were superseded by the amendment (AT1′/AT2′/AT4′); **AT3 is retained unchanged** and is the no-duplicate-with-mt#4881 constraint. Numbering below follows the task spec. Execution evidence: ``` $ cd services/reviewer && bun test --preload ../../tests/setup.ts src/timeout-regime-watch.test.ts (pass) evaluateTimeoutRegime > AT2': the measured baseline crosses nothing (pass) evaluateTimeoutRegime > AT3: ONE unrecovered event does not cross — mt#4881 already pages per occurrence (pass) evaluateTimeoutRegime > AT4': two unrecovered events cross the aggregate trigger (pass) evaluateTimeoutRegime > AT1': recovery below 95% crosses (pass) evaluateTimeoutRegime > recovery exactly at the threshold does not cross (pass) evaluateTimeoutRegime > a quiet window reports recovery as NOT COMPUTABLE, never as zero (pass) evaluateTimeoutRegime > p99.9 above 115s crosses; at the threshold it does not (pass) evaluateTimeoutRegime > no completing rounds reports null, and null never crosses (pass) sampleTimeoutRegime > Postgres string aggregates are coerced to numbers (pass) sampleTimeoutRegime > a NULL percentile (no completing rounds) stays null rather than becoming 0 (pass) runTimeoutRegimeWatchCycle > AT1'/SC2': a crossing surfaces exactly once, and not again while it persists (pass) runTimeoutRegimeWatchCycle > a trigger that clears and re-crosses notifies again (pass) runTimeoutRegimeWatchCycle > AT2'/AT3: a baseline window with one unrecovered event surfaces nothing (pass) runTimeoutRegimeWatchCycle > a query failure is swallowed, not thrown — the watch never crashes the service (pass) runTimeoutRegimeWatchCycle > a missing alert sink does not prevent the crossing being detected (pass) buildTimeoutRegimeAlertBody > names the crossed trigger, its value, its threshold, and where to reopen (pass) buildTimeoutRegimeAlertBody > says plainly that this is not an incident (pass) loadTimeoutRegimeWatchConfig > defaults are mt#4996's recorded values, not round numbers (pass) loadTimeoutRegimeWatchConfig > the recovery rate is carried as basis points 19 pass / 0 fail / 53 expect() calls ``` **AT1′** — recovery below 95% surfaces exactly once: covered by the `AT1'` evaluator case (92.5% crosses) and the `AT1'/SC2'` cycle case, which asserts one alert on the first cycle and **no second alert** while the crossing persists. **AT2′** — the baseline window crosses nothing (evaluator + cycle). **AT3 (retained)** — one unrecovered event does not cross, so no duplicate lands alongside `reviewer-pre-submit-failure/v1`; asserted at both the evaluator and cycle level, and confirmed on REAL data below. **AT4′** — two unrecovered events cross the aggregate trigger. **SC1′** (every reading recorded with its threshold each cycle) — asserted by `result.readings` having all three entries on a healthy cycle. **SC2′** (surfaces only on a crossing, at most once per crossing) — the suppression pair above, plus the clears-and-re-crosses case proving suppression is not permanent. **SC4′** (config, defaults are mt#4996's values) — the two `loadTimeoutRegimeWatchConfig` cases. **SC3/SC5** — see Live verification. **Full reviewer suite** (`server.ts` is touched, so the whole package ran): `2584 pass / 0 fail across 98 files`. **Local checks (session `be46aa3f`):** typecheck pass, 0 errors, 8 projects including `services/reviewer` (whose tsconfig sets `noUncheckedIndexedAccess`); lint **0 errors / 0 warnings** across 4,391 files; prettier clean. `validatedWorkspace` confirmed as the session dir on both. ## Live verification The unit tests inject a fake DB, so they say nothing about whether the SQL is valid or returns what the evaluator expects. The module's query was run verbatim against production `review_timing`, twice. **1. The real 30-day window — the healthy case.** ``` reviews_with_timeout | timeout_events | unrecovered_events | p999_ms | completing_rounds 36 | 51 | 1 | 108004.608 | 27170 ``` Through the shipped thresholds: unrecovered 1 < 2 (not crossed); recovery 50/51 = **98.04%** ≥ 95% (not crossed); p99.9 **108.0s** < 115s (not crossed). **All three read healthy** — the correct verdict today, matching mt#4996's accept. This also independently confirms the figures the re-scope reasoned from: 51 events with 1 unrecovered over 30 days is exactly what mt#4996's per-day table predicts. **2. SC5 — replayed over the REAL 2026-09-04 burst, which produces a real crossing.** A healthy window cannot show that the triggers FIRE, so the same query was replayed over the burst day that started this whole investigation: ``` reviews_with_timeout | timeout_events | unrecovered_events | recovery_pct | p999_ms | completing_rounds 9 | 9 | 1 | 88.89 | 116141.456 | 1455 ``` Through the shipped thresholds: recovery **88.89% < 95% → CROSSES**; p99.9 **116.1s > 115s → CROSSES**; unrecovered **1 < 2 → does not cross**. So on real burst data the watch fires on two of three triggers **and stays silent on the one `reviewer-pre-submit-failure/v1` already paged for** — which is AT3's no-duplicate constraint holding on production data rather than on a fixture. This discharges SC5 as written ("verified against a real or replayed burst, not only a synthetic one"). Worth stating plainly so the numbers are not over-read: the shipped **window is 30 days**, and one burst day does not move a 30-day p99.9 (108.0s above). That is the intended behaviour — a single bad day should not reopen a settled decision — and the replay above is a *narrowed* window used to prove the trigger arithmetic fires on genuine degradation, not a claim that today's 30-day regime crosses. ## Deploy verification: All three changed files return `true` from `isDeploySurfaceFile`, verified by running the predicate over this PR's actual changed-file list rather than recalling a pattern set: ``` $ bun -e 'import { isDeploySurfaceFile } from "./packages/domain/src/deployment/deploy-surface.ts"; for (const f of process.argv.slice(1)) console.log(isDeploySurfaceFile(f), f);' \ services/reviewer/src/timeout-regime-watch.ts services/reviewer/src/timeout-regime-watch.test.ts \ services/reviewer/src/server.ts true services/reviewer/src/timeout-regime-watch.ts true services/reviewer/src/timeout-regime-watch.test.ts true services/reviewer/src/server.ts ``` So this is **not** `[no-deploy-impact]`. After merge I will run `deployment_wait-for-latest` against the `reviewer` service with `notBefore` set to the merge timestamp and `expectCommitSha` set to the merge SHA, read `buildIdentity`, and assert the `/health` body's `service` field is `minsky-reviewer` rather than accepting the status code. **The watch itself ships disabled** (`TIMEOUT_REGIME_WATCH_ENABLED` defaults to `false`, matching `findings-aggregation.ts`), so the deploy carries the code without starting the scheduler. That is deliberate: it makes this merge a no-op at runtime, and enabling it is a one-variable change once the deploy is confirmed healthy. **No new external-system integration** — the check reads a first-party Postgres table through the DB handle the service already holds and notifies through an alert sink it already constructs — so no credential or scope is required and no live-exercise beyond deploy health is owed. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01YT3CvxJhrcGeVCv8P3DGPD Co-Authored-By: minsky-ai[bot] <minsky-ai[bot]@users.noreply.github.com>
Summary
mt#1897 named the cause of the reviewer's
openai.chat.completions.create.toolloop120s timeoutsbut reached terminal DONE with its strategy half undelivered. mt#4996 carries that remainder:
choose the remedy (mt#1897 SC4), implement it or explicitly defer with justification (AT3), and add
a regression guard only if a timeout moves (AT4).
Chosen: option (3) — accept the cadence. Options (1) stall detection, (2) Responses API, and
(4) webhook-concurrency bound are declined; (2) is ceded to an existing owner rather than
dropped. No behaviour changes. What ships is a decision record in the two places a future reader
actually lands.
Why, from measurements re-derived this session
Every figure was queried live against
review_timing, not inherited from the spec. Populations arenamed because they differ.
Recovery, all rows (n = 8,993, 2026-05-25 → 2026-09-05): 134 timeout events, 115 reviews
carrying at least one, 1 unrecovered, all time — 99.25% event-level recovery.
Round latency, completing rounds only (n = 53,038): p50 7.7s · p95 38.2s · p99 62.3s · p99.9
105.0s · max 118.0s. Only 33 rounds (0.062%) exceed 110s.
a truncated round has already spent its reasoning tokens and must restart. This is AT2's concern,
measured over all history rather than one day.
round=0; PR feat(mt#4954): Settle which root a hook's module path resolves to, and pin the invariant #3625 burned four consecutive 120s attempts across two retry layers and completedno round. More budget lengthens each failure and recovers nothing.
chat.completions.createnon-streaming, so nobyte arrives before the whole completion does — there is no time-to-first-token to budget
against. Adopting it means converting the loop to streaming, alongside tool-call accumulation,
the mt#2828/mt#2863 emission guards, and usage accounting.
latency across 103 days (~2.6 min/day).
reviewer-pre-submit-failure/v1organically (mt#4881 SC6).inferredandarchitecturally unobservable to
withSdkRetryVisibility(mem#1373). Options (1)/(2) are derivedfrom it; option (3) rests only on the recovery rate and the completing-round distribution.
Premise-independence is a reason to prefer it.
One correction to the handoff that queued this task (mt#5006). It called 2026-09-04 "the worst
day observed" with "5 of 6 recovered". 2026-08-18 was worse on both measures — 27 timeout events
across 15 of 103 reviews (14.6%) vs 09-04's 9 across 9 of 194 (4.6%) — and recovered 27 of 27.
Bursts recur roughly every 13 days (8 days in 103 carry ≥5 events), which is evidence for
accepting: the layers have absorbed a heavier burst than the one that reopened the question.
Key changes
Two files, 69 insertions, 2 deletions, all prose — no executable statement, constant, or config
value is touched.
services/reviewer/src/providers.ts— a decision-record comment on theDEFAULT_MODEL_TIMEOUT_MSdocblock: the measurement, why neither direction is right, why stalldetection is unreachable from here, and the reopen triggers. It lives on the constant because
mt#1897 re-opened this question three times and twice argued for raising the cap from percentiles
that were artifacts of the cap itself (mem#1373).
services/reviewer/README.md— a### The 120s model timeout is settledsubsection, plus twocorrections to
### Network-call timeouts. One of those was wrong, not merely stale: thetuning advice told operators to lower
reasoning_effortwhen model timeouts fire. A timing-outround is not a round that ran long, so that would cost review quality without reducing timeouts.
SC2 — coordination with mt#2718 and mt#3526
Outcome: no change needed, and no coordination debt created — nothing moves.
MAX_TOOL_ROUNDS,the retry policy,
DEFAULT_MODEL_TIMEOUT_MSandDEFAULT_TOOLLOOP_RETRY_TIMEOUT_MSare alluntouched.
Read at source: the July 2026 reviewer-cost audit, the frame mt#2718 was built on. Two things bear
directly. Its §7 non-drivers already lists "Retries — 4.7% timeout, 1.6% full re-run. Real tail
risk, not a baseline driver" — an independent, earlier judgment from a cost investigation, on
different evidence, agreeing with this one. And its §7 stretch row already owns option (2):
"migrate Chat Completions → Responses API … 40–80% better cache utilization", effort L, status
future. So option (2) is not "never explored" — it is inventoried as an mt#2718 cost lever with
a stronger justification than the timeout question supplies. Ceded there; not duplicated here. (That
40–80% figure is the audit's relay of an OpenAI claim, not read at the vendor source by this pass —
strong-evidence, and not load-bearing, since the option is being ceded rather than adopted.)mt#3526 governs the round budget — loops that complete but never call
conclude_review. Changing itwould move this task's max-duration input, not the per-attempt cap. No conflict either direction.
Reopen triggers
Reopen the remedy question — do not re-derive the measurement — on any of, over a rolling 30 days:
≥2
timeout-unrecoveredrows (baseline 1 in 103 days); event-level recovery below 95%(baseline 99.25%); completing-round p99.9 crossing 115s (baseline 105.0s), the one condition that
would make the cap genuinely tight against real work.
Acceptance tests
AT1 and AT2 are not applicable — both describe a stall-detection mechanism, and none ships. That
is what AT3 exists to express. AT3 was amended in place rather than explained elsewhere: as
filed it read "no code changed", which taken literally forbids the documentation SC4's own
rationale asks for. The amendment preserves the original wording above it, states what actually
ships, and gives the basis. See mt#4996
## Acceptance Tests.Execution evidence:
No test file is added or modified — this PR changes prose only. The evidence the decision rests on
is the live measurement, so it is the run output pasted here.
AT3 / SC1 / SC4 — the decision and its basis, from
review_timing(2026-09-05 ~04:00Z):SC4 also requires mt#1897 to be cross-referenced so a future burst does not reopen the question
from scratch: it is cited in the spec's
## DECISION, in theproviders.tsdocblock, and in theREADME subsection.
SC2 — coordination record: written above and into the spec; no timeout, round budget, or retry
policy changed, so no before/after measurement is owed.
SC3 — regression guard: not applicable. It is conditional on a timeout value or budget changing,
and none does.
SC5 — verified against a real burst: the chosen remedy is the existing retry stack, and it has
been exercised by 8 real bursts / 134 events with 133 recoveries, including 27-of-27 on the heaviest
(2026-08-18). This is a stronger discharge than a synthetic reproduction would be.
Local checks (session workspace
df3c8366): typecheck pass, 0 errors, 8 projects includingservices/reviewer(validatedWorkspaceconfirmed as the session dir, not main); lint pass, 0errors / 0 warnings across 4,388 files; prettier clean on both files.
Deploy verification:
Both changed files return
truefromisDeploySurfaceFile— verified by running the predicate overthis PR's actual changed-file list rather than recalling a pattern set:
So this is not
[no-deploy-impact], comment-only though it is. After merge I will rundeployment_wait-for-latestagainst thereviewerservice withnotBeforeset to the mergetimestamp and
expectCommitShaset to the merge SHA, readbuildIdentity, and assert the/healthbody's
servicefield isminsky-reviewerrather than accepting the status code. No external-systemintegration changes, so no live-exercise beyond deploy health is owed.
🤖 Generated with Claude Code
https://claude.ai/code/session_01YT3CvxJhrcGeVCv8P3DGPD