Observed 2026-09-22 on vs-7r3soq (vstim, review_gate.timeout_ms = 3,600,000). Round-2 revising passes "timed out" after about 19 and 27 minutes. The same workspace's vs-cpsreu round-1 pass timed out at exactly 60:00, so the config itself is honored.
Mechanism (coordinator-verified on origin/main):
launch_worker/5 arms Process.send_after(self(), {:timeout, attempt}, timeout_ms) at apps/arbiter/lib/arbiter/worker/review_gate.ex:3815. The timer ref isn't kept, and the file has no Process.cancel_timer.
- The handler
handle_info({:timeout, attempt}, %{attempt: attempt} = state) at :1261 escalates any timeout whose attempt equals the current one. It relies on attempt numbers being unique.
dispatch_next_review/2 resets attempt: 0 for every new round (:1915, from bd-bgeo6i, so reprompt budgets start fresh). Each round therefore numbers its passes the same way: reviewer = 1, implementer = 2.
- So round 1's implementer timer
{:timeout, 2} fires 60 minutes after round 1's implementer launched, lands while round 2's implementer is running as attempt 2, and is accepted.
The journal matches to the second (EDT):
| impl1 launched |
impl2 launched |
Timeout fired |
| 02:54:22 |
03:35:23 |
03:54:23 (impl1 + 60:01) |
| 04:36:10 |
05:09:20 |
05:36:10 (impl1 + 60:00) |
The same collision can hit a round-2+ reviewer (attempt 1) through round 1's reviewer timer. vs-7r3soq's earlier "reviewing pass timed out" parks may be this too; they weren't correlated.
Cost: long vstim reviews (a roughly 4½-minute test suite plus credo, twice per round) repeatedly lost a live revising pass mid-edit. That forced coordinator resumes and discarded work in progress.
Fix: make timer identity unique for the gate's lifetime (for example {round, attempt} or a make_ref() token in the message), or keep the timer ref and Process.cancel_timer/1 it when a pass ends or a new one launches. Keep bd-bgeo6i's per-round reprompt-budget reset.
D2: one module and one invariant, but it's the gate's liveness logic, so it needs careful tests.
Observed 2026-09-22 on vs-7r3soq (vstim,
review_gate.timeout_ms= 3,600,000). Round-2 revising passes "timed out" after about 19 and 27 minutes. The same workspace's vs-cpsreu round-1 pass timed out at exactly 60:00, so the config itself is honored.Mechanism (coordinator-verified on origin/main):
launch_worker/5armsProcess.send_after(self(), {:timeout, attempt}, timeout_ms)atapps/arbiter/lib/arbiter/worker/review_gate.ex:3815. The timer ref isn't kept, and the file has noProcess.cancel_timer.handle_info({:timeout, attempt}, %{attempt: attempt} = state)at:1261escalates any timeout whoseattemptequals the current one. It relies on attempt numbers being unique.dispatch_next_review/2resetsattempt: 0for every new round (:1915, from bd-bgeo6i, so reprompt budgets start fresh). Each round therefore numbers its passes the same way: reviewer = 1, implementer = 2.{:timeout, 2}fires 60 minutes after round 1's implementer launched, lands while round 2's implementer is running as attempt 2, and is accepted.The journal matches to the second (EDT):
The same collision can hit a round-2+ reviewer (attempt 1) through round 1's reviewer timer. vs-7r3soq's earlier "reviewing pass timed out" parks may be this too; they weren't correlated.
Cost: long vstim reviews (a roughly 4½-minute test suite plus credo, twice per round) repeatedly lost a live revising pass mid-edit. That forced coordinator resumes and discarded work in progress.
Fix: make timer identity unique for the gate's lifetime (for example
{round, attempt}or amake_ref()token in the message), or keep the timer ref andProcess.cancel_timer/1it when a pass ends or a new one launches. Keep bd-bgeo6i's per-round reprompt-budget reset.D2: one module and one invariant, but it's the gate's liveness logic, so it needs careful tests.