Skip to content

Derive client timeouts from the token budget, not a latency percentile — and enforce the bound in CI #42

Description

@OriNachum

The finding

examples/worker_seam.py shipped REQUEST_TIMEOUT = 300.0 in front of a
cortex that generates at ~23 tok/s and thinks for 4–11.5 k tokens per turn.
Every generation past ~6,975 tokens was cut mid-flight, re-run from the top,
and after four attempts discarded — while recording truncated: false. It cost
two scored answers and censored the completion-length distribution of every
measurement taken through that seam. Full account in
#41; fixed for that
harness under pre-registration amendment 1 and deviation d4.

This issue is about the general rule, because the specific fix does not
prevent the next instance.

Why "300 was too small, use 400" is the wrong lesson

The operator asked the right question: why 300 and not 330 or 360, in case we
miss?
The answer is that no value chosen that way is safe, and the reason is
worth stating precisely.

At the measured ~23 tok/s:

timeout tokens it buys
300 s ~6,975
330 s ~7,670
360 s ~8,370
600 s ~13,900

The first call recorded after the fix was 11,458 completion tokens in
460 s
— a perfectly ordinary successful turn. A 360 s timeout would have cut
it. The largest completion ever recorded through this seam before the fix was
6,372 tokens, and that number is not a fact about the model: it is a fact about
the timeout.

Which is the trap. Sizing a timeout as "observed tail × margin" requires
knowing the tail — but the timeout censors the tail. The distribution you would
size the margin from is the one the defect has already truncated, so the
estimate is biased low by exactly the mechanism you are trying to protect
against, and the bias is invisible: nothing in the record says "a longer turn
happened here and was deleted". A margin over a censored percentile is not a
safety factor, it is a restatement of the bug.

The same circularity bit our own pre-registration. §14 computed a 1.47×
margin over a 204.3 s measured tail and flagged it as thin — good instinct,
right axis, wrong quantity. That 204.3 s came from an easier task, so the
margin was over a percentile that did not describe the workload. It correctly
predicted the failure and still could not size the fix.

The rule proposed

A client timeout in front of a token-budgeted model is derived from the
budget, not from a latency percentile:

REQUEST_TIMEOUT  >=  max_tokens / slowest_measured_generation_rate

Two properties the percentile approach does not have:

  1. It is not circular. max_tokens is a hard ceiling the model cannot
    exceed, so time-to-exhaust-budget is a true worst case that can be computed
    without observing a tail — including a tail the current timeout is hiding.
  2. It moves the binding constraint onto the one that reports itself. Above
    the bound, a turn that runs long ends as finish_reason == "length":
    visible, gradeable, and already handled as an instrument event. Below it,
    the identical turn ends as a transport retry, and nothing downstream can
    tell it happened. This is the same defect shape as
    #37 (ModelResponse
    carries no finish_reason) and the same reasoning that produced d16.

The rate belongs to the rig and should be cited, not guessed. Measured here on
2026-08-01, ten calls: 21.5 tok/s slowest, 23.2 mean
(docs/live-test-results/orchestrator-worker-preregistration.md §18).

Repo-wide audit — where every timeout sits today

Script: timeout-audit.py (to be committed with the fix, so it is re-runnable
rather than a number in an issue). Bound computed at 21.5 tok/s against the
largest token budget in each file.

file constant secs budget need verdict
examples/worker_seam.py REQUEST_TIMEOUT 300 16000 744 BELOW BOUND (0.40×) — fixed to 1200
examples/league_commander.py REQUEST_TIMEOUT 900 16000 744 OK, 1.21× — the narrowest passing
examples/league_h2h.py REQUEST_TIMEOUT 1800 16000 744 OK, 2.42×
examples/devague_legs.py REQUEST_TIMEOUT_S 600 4000 186 OK, 3.23×
examples/muse_arms.py REQUEST_TIMEOUT_S 600 3000 140 OK, 4.30×
examples/worker_throughput.py BATCH_WAIT_TIMEOUT_SECONDS 300 1200 56 OK, 5.38×

Only one harness was below the bound, and it is the one that failed — which is
mild evidence the bound is measuring the right thing rather than flagging
everything.

league_commander.py at 1.21× deserves a look. Its margin over a full
budget is 156 s. That harness produced the 2.4–4.4× hierarchy cost figure this
repo cites regularly, and if any of its turns were cut the same way, its cost
numbers are inflated and its correctness numbers are understated. Whether that
happened is checkable: its committed records carry retries per call.

Proposed work

  1. Commit the audit as a test, not a script. A test_timeout_bounds.py
    that walks the harnesses, computes max_tokens / RATE per file, and fails
    on any timeout below it. The value of the M3 discipline is that the number
    is recomputed from a committed input — a timeout that drifts below its bound
    should break CI, not wait for a series to notice.
  2. Re-examine league_commander's committed records for cut turns
    retries > 0 on any call, and any gap between wall clock and
    tokens / rate. If they are clean, say so and the 1.21× stands; if not, its
    published figures need the same treatment C1-E got.
  3. Decide whether the bound belongs in embodiment/ rather than per-harness.
    Six harnesses have each invented a timeout; the rule is the same for all of
    them and none of them is where it belongs. This needs a design decision
    about whether the loop layer owns transport policy at all — it is a genuine
    question and out of scope for a defect fix.
  4. Carry the rule to colleague. They dial the same cortex through their own
    transport and would hit this identically. Filed separately as part of the
    t19 findings post.

What this is not

Not a claim that 1200 s is right for every rig. The bound is
max_tokens / rate, and both terms are local: a faster card or a smaller
budget moves it. The transferable part is the derivation and the reason —
put the binding constraint on the quantity that reports itself.

  • embodiment (Claude)

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