Repository navigation
[Bug]: One unresponsive provider holds all four effect workers and stalls every thread #16636
Description
Activity
Note
Grok responding on behalf of Julius.
Confirmed on current
main(517188b). One provider that stops answering can occupy every orchestration effect worker.Root cause.
-
Fixed pool of four workers.
apps/server/src/orchestration-v2/EffectWorker.ts:781setsDEFAULT_EFFECT_WORKER_CONCURRENCY = 4.runDaemonWithOptions(:784-853) starts that many identicalrunWorkerloops, andlayerDaemon(:857-858) uses the default.- The loops are not partitioned by provider. Per-thread ordering in
EffectOutbox.ts:314-353(claimableCandidatePredicate) only stops two effects on the same thread from running together, so the four slots are shared by every thread on every provider.
-
No execution deadline.
runOnce(EffectWorker.ts:681-686) awaitsEffect.raceFirst(executor.execute(...), cancellation). The only other branch is an explicit cancellation, with no timeout.provider-turn.start→ProviderTurnStartService.startandprovider-turn.interrupt→ProviderTurnControlService.interrupt(dispatch atEffectWorker.ts:152-204) add no timeout either.ProviderTurnControlService.ts:194-207has a 2s deadline, but it only covers the poll afterinterruptTurnreturns.- In the Codex adapter,
client.request("turn/start", ...)(CodexAdapterV2.ts:5890) and thethread/start/thread/resumerequests (~:6079-6110) are unbounded. packages/effect-codex-app-server/src/client.tshas no default request timeout.- Only some interrupt sub-steps are bounded:
timeoutOption("10 seconds")around waiting for completion (:6550,:6624). The bareturn/interruptrequest at:6561is not.
-
The lease is never enforced while the process is alive.
claimNext(EffectOutbox.ts:518-552) writeslease_expires_atbut only claimsstatus = 'pending'rows (:324).- Nothing compares
lease_expires_atwith the current time, and there is no renewal or reclaim of expired leases. - Expired
runningrows are only handled byreconcileAfterProcessLoss(:483-512) on the next server start. workerIdis per process (EffectWorker.ts:538), so the ownership checks insucceed/fail/retrystill pass long after the lease has expired.
In practice the 30s lease is metadata only. A fiber blocked on a silent provider holds its slot until that provider answers or the server restarts, which matches the outbox snapshot in the report.
Suggested fix direction. Both parts from the report seem right, and they work independently:
- Give provider-control effects an execution deadline that fails the effect on timeout. Either raise it above the observed p95/max for
provider-turn.start(~33s) or derive it from the lease and renew the lease while the call is healthy. On timeout, interrupt the fiber so its slot frees up, and settle the run through the existing retry/fail path. - Partition worker capacity, either with per-provider (or per-provider-instance) lanes or a per-provider cap on running effects inside the claim query. That way one wedged provider can't take every slot.
A unit test with a stub executor whose
provider-turn.startnever resolves, plus four or more threads, would reproduce this deterministically.Related.
- [Bug]: Codex app-server keeps per-thread MCP servers running after threads settle #15979 (the Codex app-server condition that triggered it)
- [Bug]: V2 run stays
startingforever afterprovider-turn.startexhausts its retries — no Stop button, settle refused, only Delete clears it (Pi provider, desktop) #13392 (closed) and Run stays "starting" when its thread's recovery state can't be read #16441, which cover runs stuck instartingwith different triggers - [Bug]: Codex sub-agents keep failing after the shared App Server is killed by SIGKILL #16502 (Codex sub-agent failures after the shared app-server dies)
I didn't find an open PR that adds a deadline or per-provider lanes.
-
- addedbugSomething is broken or behaving incorrectly.Something is broken or behaving incorrectly.via-triageFiled through npx t3 triageFiled through npx t3 triage
on Oct 7, 2026
Before submitting
Area
apps/server
Steps to reproduce
startTurnnever resolves should do the same.Expected behavior
The Claude thread starts. A stalled provider fails or times out its own effects without holding up the others.
Actual behavior
Every thread stalls, on every provider. Each unanswered
provider-turn.start(and laterprovider-turn.interrupt) holds one of the orchestration effect workers, and there are four (DEFAULT_EFFECT_WORKER_CONCURRENCYinorchestration-v2/EffectWorker.ts). Each claim has a 30-second lease, but an expired lease isn't a deadline: the effect keeps running, no worker frees up, and Claude launches, checkpoints, and deliveries wait inorchestration_v2_effect_outboxbehind it. At the first lockup, four effects (threeprovider-turn.start, oneprovider-turn.interrupt) had been running for 20 to 40 minutes, long past their leases, with eleven pending behind them: five starts, four checkpoints, a steer, and a title generation. Recovery needed restarting the Codex app-server once and the T3 backend once, which interrupted every in-flight run.Impact
Blocks work completely
Version or commit
0.0.46-nightly.20261005.2702 (first lockup) and 0.0.46-nightly.20261006.2735 (second)
Environment
Windows 11, T3 Code desktop, codex-cli 0.160.1 as the shared Codex app-server, Claude as a second provider
Logs or stack traces
Suggested fix: give provider control effects an execution deadline that fails the run, and run each provider's effects on its own workers, so one wedged provider can't take every worker.