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.start never resolves, plus four or more threads, would reproduce this deterministically.
Related.
- #15979 (the Codex app-server condition that triggered it)
- #13392 (closed) and #16441, which cover runs stuck in
startingwith different triggers - #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.