You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
[Bug]: Shared Codex app-server backlogs or dies silently; thread/start hangs for hours and new Codex threads never start #24300
T3 runs one shared Codex app-server per environment (provider-session:provider-instance:codex:shared). On long-running servers, that process's message stream falls hours behind or stops entirely. When that happens, thread/start requests wait hours or never get an answer. New Codex threads then sit at "Workspace ready" with the provider thread stuck in pending, and no turn ever starts. T3 doesn't detect any of this: there is no timeout, no restart, and no error in the UI.
A second case: if the app-server process is killed (for example by stopping its systemd scope), T3 keeps routing new threads to the dead session. The only trace is CodexAppServerTransportError: Codex App Server transport operation 'read-process-exit-status' failed from ProviderSessionManager.unloadIdleThread.
Environment
T3 0.1.2-preview.20261022.3511, Linux server (headless, 4 vCPU / 16 GB)
codex-cli 0.163.0
Several Codex threads at once (up to 4 leads, each delegating step tasks), with the server up 1–2 days
thread/start latency on one server, request (outgoing id) to its response:
2026-10-22 08:15–08:19: 0 s
2026-10-22 09:50: 938 s
2026-10-22 10:30: 1510 s
2026-10-22 18:37 onward: 13,000–69,000 s (4–19 h)
from 2026-10-23 18:06: no response at all (11 requests)
Example: request id 88 was sent at 2026-10-23T11:53:51.883Z, and the response arrived at 2026-10-23T20:43:19.075Z.
Notification lag: receive time minus the app-server's own emittedAtMs.
About 20 min on 10-22.
2–5 h on 10-23 (for example, 7,000–9,000 s throughout 10:25–11:34).
The app-server emits on time, but the messages reach T3 hours later. That points at the shared stdio stream or T3's reader, not at model latency. The process itself is idle (0% CPU).
Across 40 servers running the same workload, counting thread/start requests since 2026-10-23:
61/61 unanswered on two servers.
21/26 unanswered on another.
46–55 answered late (over 5 min) on two more.
Five servers whose app-server had been killed: every request unanswered.
Reconnecting... 5/5 errors show up in the same stream around the stalls.
Expected
A thread/start (and turn start) that gets no response within a bounded time fails the run with a visible error, instead of hanging.
T3 detects when the shared app-server is dead or backlogged (process exit, or message lag past a threshold) and restarts it. It doesn't keep routing new threads to it.
Ideally, one busy or wedged thread can't stall thread/start for every other thread in the environment.
Workaround
Restart t3code.service, which starts a fresh app-server.
Still happening on stable 0.1.7 (322f6544df) on Windows, which already includes #24600, #26188 and #27924. This instance differs from the original report in one useful way: the app-server's stdout stayed open and its notifications arrived on time. The process stopped answering requests, while T3's reader kept up.
Setup
Windows 11 Pro build 26300, AMD Ryzen 9 9950X3D, 125 GB RAM. Load during the incident was about 20% CPU, with 34 GB RAM free.
T3 Code desktop 0.1.7 running as an always-on server. Used remotely from a Mac and a phone through T3 Connect (linked environment, "approval-required" access).
codex-cli 0.166.0 (WinGet). One shared app-server (provider-session:provider-instance:codex:shared) for every Codex thread in the environment.
The workload is heavy orchestration. Lead threads delegate to up to 20 concurrent delegate_task children (gpt-6-luna, gpt-6.1-sol), and several leads run at once. The environment has 528 threads, and statev2.sqlite is 2.3 GB.
The app-server was started at 22:58 local after a reboot. It had been up about an hour when it stopped answering.
2026-11-10T23:52:43Z id 158 thread/unsubscribe -> answered in 7 ms # last answered request
2026-11-11T00:00:08Z id 159 thread/resume (excludeTurns: true) # never answered
2026-11-11T08:15:12Z id 160 turn/steer (user: "U stuck?!") # never answered
2026-11-11T08:29:43Z id 161 thread/start (brand-new thread) # never answered
2026-11-11T08:33 .. 09:49 incoming skills/changed every ~5 min, emittedAtMs == receive time
After id 158 at 23:52, no incoming message carried an id. Only skills/changed notifications arrived, all on time.
The app-server process (PID 6256) was alive and idle. Its CPU time moved 0.016 s in 5 s. It held 553 OS threads and 3,849 handles, and had about 50 sets of MCP children under it (node, node_repl, 1password-mcp, cmd).
Nothing reached the UI. Every Codex thread on the machine sat at "starting" or "Working" for about 10 hours. A new thread showed "Workspace ready" and never produced a turn.
Network was fine. DNS and HTTPS to api.openai.com and chatgpt.com answered from the same host.
Recovery
Stopping PID 6256 made T3 spawn a new app-server within seconds. The new thread waiting since 08:29 answered at 09:53, and the thread whose thread/resume had hung resumed. Its running children were marked failed, which is correct. One lead thread stayed stuck on its old run. I'm filing that as a separate issue, because it happens after the process is replaced and needs a different fix.
What would have helped
The original report already asks for a bounded timeout on thread/start, thread/resume and turn requests that fails the run visibly, and for restarting the shared app-server when it stops answering. This case adds one thing: a liveness check can't rely on stdout closing or on notification lag. Here both looked healthy. It has to check that requests are still being answered, for example by pinging with a cheap request and treating no response within N seconds as dead.
I can send a PR with a request timeout and a test using a fake app-server that keeps sending notifications but never answers a request. Would you take it?
CodexAdapterV2.ts: thread/resume (6971-6986 and 7336-7357, the excludeTurns: true path that hung here), thread/start (7304-7316), turn/start (7023), turn/steer (7500) and initialize (1816-1846) all wait without a deadline. ProviderSessionManager.ts:1985-2045 and ProviderTurnStartService.ts:1268 add none either.
Only interruptTurn has one (ProviderSessionManager.ts:2140-2153, 15 s). Its comment says: "A provider starved of memory can stop answering, so no caller may wait on it unbounded." Resume, start and steer don't follow that rule.
Liveness of the shared app-server is detected only through client.awaitTermination (CodexAdapterV2.ts:1858-1870). That requires the process to exit.
On T3's side, one hung request doesn't block other threads' requests. The second test below shows another thread's thread/start answered while thread/resume hangs. So in this incident ids 160 and 161 went unanswered because Codex itself stopped answering.
One thing I couldn't tell from the logs: outgoing frames are logged when they're enqueued (protocol.ts:237-248), before they're written. If Codex had stopped reading stdin, the writer would block (_internal/stdio.ts:25-28) and the log would look the same. Either way, nothing in T3 notices.
Failing test
The test goes into apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.test.ts, next to the process-exit tests. The fake app-server answers initialize and thread/start, never answers thread/resume, and keeps sending skills/changed every 5 minutes while the TestClock advances 60 minutes.
Test code
describe("CodexAdapterV2 unanswered app-server request",()=>{// A live shared app-server whose stdout stays open and keeps sending// notifications, but which never answers one request. Every other request is// answered from the replay preamble with the request's own id.constmakeSilentResumeSession=(scenario: string)=>Effect.gen(function*(){constnativeThreadId=`native-${scenario}`;constinput=yield*Queue.unbounded<Uint8Array,Cause.Done>();constemit=(frame: unknown)=>Queue.offer(input,newTextEncoder().encode(JSON.stringify(frame)+""));constresumeSent=yield*Deferred.make<void>();constpreamble=codexReplayPreamble({ nativeThreadId,nativeTurnId: "unused",prompt: ""});constclient=yield*CodexClient.make(Stdio.make({args: Effect.succeed([]),stdin: Stream.fromQueue(input),stdout: ()=>Sink.forEach((chunk: string|Uint8Array)=>Effect.gen(function*(){constframe=JSON.parse(typeofchunk==="string" ? chunk : newTextDecoder().decode(chunk),)as{readonlyid?: number;readonlymethod: string};if(frame.method==="thread/resume"){// Codex accepted the request and never replies.yield*Deferred.succeed(resumeSent,undefined);return;}constresponse=preamble.find((entry)=>entry.type==="emit_inbound"&&entry.label===frame.method&&Predicate.isObject(entry.frame)&&"id"inentry.frame,);if(response?.type==="emit_inbound"&&Predicate.isObject(response.frame)){yield*emit({ ...response.frame,id: frame.id});}}),),stderr: ()=>Sink.drain,}),);constadapter=yield*CodexAdapterV2.makeCodexAdapterV2({instanceId: CodexAdapterV2.CODEX_DEFAULT_INSTANCE_ID,settings: DEFAULT_CODEX_SETTINGS,environment: {},clientFactory: {open: ()=>Effect.succeed(client)},digest: yield*Digest.Digest,fileSystem: yield*FileSystem.FileSystem,idAllocator: yield*IdAllocator.IdAllocatorV2,serverConfig: yield*ProviderReplayHarness.makeReplayServerConfig(scenario).pipe(Effect.orDie,),});constthreadId=ThreadId.make(`thread-${scenario}`);construntime=yield*adapter.openSession({
threadId,providerSessionId: ProviderSessionId.make(`session-${scenario}`),modelSelection: CODEX_TEST_MODEL_SELECTION,runtimePolicy: CODEX_TEST_RUNTIME_POLICY,});constproviderThread=yield*runtime.ensureThread({
threadId,modelSelection: CODEX_TEST_MODEL_SELECTION,runtimePolicy: CODEX_TEST_RUNTIME_POLICY,});constresume=yield*runtime.resumeThread({
providerThread,modelSelection: CODEX_TEST_MODEL_SELECTION,runtimePolicy: CODEX_TEST_RUNTIME_POLICY,}).pipe(Effect.forkScoped);yield*Deferred.await(resumeSent);// The process stays healthy-looking: it keeps notifying every 5 minutes.constkeepNotifying=(minutes: number)=>Effect.gen(function*(){for(letelapsed=0;elapsed<minutes;elapsed+=5){yield*emit({method: "skills/changed",params: {}});yield*TestClock.adjust("5 minutes");}});return{ runtime, resume, keepNotifying, scenario };});it.effect("fails thread/resume within a bounded time while the app-server stays alive",()=>Effect.gen(function*(){const{ resume, keepNotifying }=yield*makeSilentResumeSession("silent-resume");yield*keepNotifying(60);// Without a request deadline the resume is still pending after an hour, so// the run sits in "starting" with no error until the process is killed.constexit=resume.pollUnsafe();assert.isDefined(exit,"thread/resume is still waiting for a response after 60 minutes");assert.isTrue(exit!==undefined&&Exit.isFailure(exit));consterror=exit!==undefined&&Exit.isFailure(exit) ? Cause.squash(exit.cause) : null;assert.nestedPropertyVal(error,"_tag","ProviderAdapterResumeThreadError");}).pipe(Effect.scoped,Effect.provide(Layer.mergeAll(IdAllocator.layer,McpProviderSessions.layer,NodeServices.layer,NodeCrypto.layer,),),),);it.effect("keeps serving other threads' requests while one request is unanswered",()=>Effect.gen(function*(){const{ runtime, resume }=yield*makeSilentResumeSession("silent-resume-concurrent");constotherThreadId=ThreadId.make("thread-silent-resume-concurrent-other");// T3 does not serialize requests on the shared client: a second thread's// thread/start is written and its response routed while resume hangs.constother=yield*runtime.ensureThread({threadId: otherThreadId,modelSelection: CODEX_TEST_MODEL_SELECTION,runtimePolicy: CODEX_TEST_RUNTIME_POLICY,});assert.equal(other.appThreadId,otherThreadId);assert.isUndefined(resume.pollUnsafe());}).pipe(Effect.scoped,Effect.provide(Layer.mergeAll(IdAllocator.layer,McpProviderSessions.layer,NodeServices.layer,NodeCrypto.layer,),),),);});
$ cd apps/server && npx vp test run src/orchestration-v2/Adapters/CodexAdapterV2.test.ts -t "unanswered app-server request"
× fails thread/resume within a bounded time while the app-server stays alive 19ms
AssertionError: thread/resume is still waiting for a response after 60 minutes: expected undefined to not equal undefined
Tests 1 failed | 1 passed | 151 skipped (153)
Fix I have locally
A withRunRequestDeadline in CodexAdapterV2.ts applied to thread/resume, thread/start, turn/start and turn/steer. The limit is 2 minutes, chosen without measurements and open to discussion.
When the deadline passes, it fails the run with a visible error (Codex did not answer thread/resume within 2 minutes, surfaced as ProviderAdapterResumeThreadError). It also fails the session's event stream with ProviderAdapterEventStreamError, which the process-exit tests already use to retire the shared app-server.
I kept the deadline out of the protocol layer, because some requests, such as command/exec, are legitimately long.
With this change both tests pass, the full CodexAdapterV2.test.ts file passes (153 tests), and effect-tsgo typecheck for apps/server is clean.
Not verified yet:
That ProviderSessionManager then respawns the app-server, end to end with a real Codex binary.
Another cause of unanswered thread/start, with a repro that does not need T3: Codex blocks forever on a locked Linux Secret Service keyring.
Setup: T3 Code nightly 0.1.10-nightly.20261113.4656, systemd user service on headless Linux x64 (Ubuntu; only the GDM greeter on seat0, no desktop login). codex-cli 0.169.0, an OAuth MCP server (Sentry), default mcp_oauth_credentials_store.
What happened: both shared app-servers (codex_personal, codex) answered initialize, then never answered thread/resume (id 2) or thread/start (ids 2, 3, 4, 6, 7) between 02:18 and 02:34 UTC. Each app-server was idle at 0% CPU, with open connections to the session D-Bus. Codex threads hung with no error in the UI. In the same window:
codex mcp list failed; text generation keeps its MCP servers { reason: 'timeout' }, and its codex exec children stayed hung
orchestration-v2.driver-session.detach-unload-failed: TimeoutError: Operation timed out after '10s'
Cause: the GNOME keyring login collection was locked: busctl --user get-property org.freedesktop.secrets /org/freedesktop/secrets/collection/login org.freedesktop.Secret.Collection Locked → b true.
Codex asks the Secret Service for the MCP OAuth token. The keyring needs an unlock prompt that cannot appear, so the call never returns.
codex exec -c mcp_oauth_credentials_store=auto "Reply with exactly: ok" hangs (killed at 45 s). With -c mcp_oauth_credentials_store=file it answers in 5 s. codex mcp list behaves the same way (hangs vs. 0.01 s).
Workaround: add mcp_oauth_credentials_store = "file" to ~/.codex/config.toml, then restart the T3 server to replace the stuck app-servers. On shadow-home installs this can lead to #26811 through .credentials.json.
This is one more reason for the time limit on thread/start proposed above: T3 had no sign that Codex was stuck.
Posted by Claude Code (Opus 5.5) via T3 Code triage.
Activity
rafiki270 commentedon Nov 11, 2026
Still happening on stable 0.1.7 (
322f6544df) on Windows, which already includes #24600, #26188 and #27924. This instance differs from the original report in one useful way: the app-server's stdout stayed open and its notifications arrived on time. The process stopped answering requests, while T3's reader kept up.Setup
codex-cli 0.166.0(WinGet). One shared app-server (provider-session:provider-instance:codex:shared) for every Codex thread in the environment.delegate_taskchildren (gpt-6-luna,gpt-6.1-sol), and several leads run at once. The environment has 528 threads, andstatev2.sqliteis 2.3 GB.Evidence (
~/.t3/userdata/logs/provider/events.*.log, UTC)id. Onlyskills/changednotifications arrived, all on time.node,node_repl,1password-mcp,cmd).api.openai.comandchatgpt.comanswered from the same host.Recovery
Stopping PID 6256 made T3 spawn a new app-server within seconds. The new thread waiting since 08:29 answered at 09:53, and the thread whose
thread/resumehad hung resumed. Its running children were marked failed, which is correct. One lead thread stayed stuck on its old run. I'm filing that as a separate issue, because it happens after the process is replaced and needs a different fix.What would have helped
The original report already asks for a bounded timeout on
thread/start,thread/resumeand turn requests that fails the run visibly, and for restarting the shared app-server when it stops answering. This case adds one thing: a liveness check can't rely on stdout closing or on notification lag. Here both looked healthy. It has to check that requests are still being answered, for example by pinging with a cheap request and treating no response within N seconds as dead.I can send a PR with a request timeout and a test using a fake app-server that keeps sending notifications but never answers a request. Would you take it?
rafiki270 commentedon Nov 11, 2026
Follow-up: here's where it happens in the code, with a test that fails on main (
8d236998d0).Where it happens
Nothing puts a time limit on a request to the app-server. A request ends only when Codex answers or the connection terminates.
packages/effect-codex-app-server/src/protocol.ts:497-530:requestregisters a pending entry, enqueues the frame and then callsDeferred.await(deferred)(line 524) with no timeout. The only ways out arehandleResponse(290-308) andfailAllPending(197-205).failAllPendingruns fromhandleTermination(207-229), which fires only on stdout end, a read failure or a write failure (447-495). That's the path fix(codex): bound pending requests after app-server stdout closes #24600, fix(server): Codex works again after its app-server is killed #26188 and fix(codex): a failed write to the app-server no longer leaves requests hanging #27924 fixed. In this incident none of those happened.CodexAdapterV2.ts:thread/resume(6971-6986 and 7336-7357, theexcludeTurns: truepath that hung here),thread/start(7304-7316),turn/start(7023),turn/steer(7500) andinitialize(1816-1846) all wait without a deadline.ProviderSessionManager.ts:1985-2045andProviderTurnStartService.ts:1268add none either.interruptTurnhas one (ProviderSessionManager.ts:2140-2153, 15 s). Its comment says: "A provider starved of memory can stop answering, so no caller may wait on it unbounded." Resume, start and steer don't follow that rule.client.awaitTermination(CodexAdapterV2.ts:1858-1870). That requires the process to exit.On T3's side, one hung request doesn't block other threads' requests. The second test below shows another thread's
thread/startanswered whilethread/resumehangs. So in this incident ids 160 and 161 went unanswered because Codex itself stopped answering.One thing I couldn't tell from the logs: outgoing frames are logged when they're enqueued (
protocol.ts:237-248), before they're written. If Codex had stopped reading stdin, the writer would block (_internal/stdio.ts:25-28) and the log would look the same. Either way, nothing in T3 notices.Failing test
The test goes into
apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.test.ts, next to the process-exit tests. The fake app-server answersinitializeandthread/start, never answersthread/resume, and keeps sendingskills/changedevery 5 minutes while theTestClockadvances 60 minutes.Test code
Fix I have locally
withRunRequestDeadlineinCodexAdapterV2.tsapplied tothread/resume,thread/start,turn/startandturn/steer. The limit is 2 minutes, chosen without measurements and open to discussion.Codex did not answer thread/resume within 2 minutes, surfaced asProviderAdapterResumeThreadError). It also fails the session's event stream withProviderAdapterEventStreamError, which the process-exit tests already use to retire the shared app-server.command/exec, are legitimately long.With this change both tests pass, the full
CodexAdapterV2.test.tsfile passes (153 tests), andeffect-tsgotypecheck forapps/serveris clean.Not verified yet:
ProviderSessionManagerthen respawns the app-server, end to end with a real Codex binary.initializeshould get the same deadline.I'm happy to open the PR if you want it.
SacredAI commentedon Nov 14, 2026
Another cause of unanswered
thread/start, with a repro that does not need T3: Codex blocks forever on a locked Linux Secret Service keyring.Setup: T3 Code nightly
0.1.10-nightly.20261113.4656, systemd user service on headless Linux x64 (Ubuntu; only the GDM greeter on seat0, no desktop login). codex-cli 0.169.0, an OAuth MCP server (Sentry), defaultmcp_oauth_credentials_store.What happened: both shared app-servers (
codex_personal,codex) answeredinitialize, then never answeredthread/resume(id 2) orthread/start(ids 2, 3, 4, 6, 7) between 02:18 and 02:34 UTC. Each app-server was idle at 0% CPU, with open connections to the session D-Bus. Codex threads hung with no error in the UI. In the same window:codex mcp list failed; text generation keeps its MCP servers { reason: 'timeout' }, and itscodex execchildren stayed hungorchestration-v2.driver-session.detach-unload-failed:TimeoutError: Operation timed out after '10s'Cause: the GNOME keyring
logincollection was locked:busctl --user get-property org.freedesktop.secrets /org/freedesktop/secrets/collection/login org.freedesktop.Secret.Collection Locked→b true.Codex asks the Secret Service for the MCP OAuth token. The keyring needs an unlock prompt that cannot appear, so the call never returns.
Repro:
logincollection locked.codex exec -c mcp_oauth_credentials_store=auto "Reply with exactly: ok"hangs (killed at 45 s). With-c mcp_oauth_credentials_store=fileit answers in 5 s.codex mcp listbehaves the same way (hangs vs. 0.01 s).Workaround: add
mcp_oauth_credentials_store = "file"to~/.codex/config.toml, then restart the T3 server to replace the stuck app-servers. On shadow-home installs this can lead to #26811 through.credentials.json.This is one more reason for the time limit on
thread/startproposed above: T3 had no sign that Codex was stuck.Posted by Claude Code (Opus 5.5) via T3 Code triage.