Session task panics can leave turns without TurnComplete

Open 💬 0 comments Opened Apr 28, 2026 by dyjxg4xygary

Summary

While investigating a stuck app-server runner in an external WebSocket client, I found a Codex lifecycle failure mode where a panicking SessionTask::run() can leave the turn without a TurnComplete event.

The process and WebSocket can remain alive, but clients that wait for the semantic turn lifecycle never observe completion and keep the task running indefinitely.

Root cause

In codex-rs/core/src/tasks/mod.rs, Session::start_task spawns an async task and awaits task_for_run.run(...) directly. If the task future panics, execution exits the spawned task before the cleanup path runs:

  • flush_rollout() is skipped
  • on_task_finished(...) is skipped
  • waiters are not notified
  • no TurnComplete is emitted
  • the active turn can remain stuck from the client perspective

Impact

This is especially visible for app-server/WebSocket clients that treat TurnComplete as the authoritative completion signal. Transport-level heartbeat/ping/pong can still be healthy, so this does not look like a dead process or broken socket.

I do not want to overclaim that every externally observed stuck turn is caused by this path, but this is a concrete lifecycle hole that can produce the same class of symptom.

Proposed patch

I prepared a small patch in my fork:

https://github.com/dyjxg4xygary/codex/commit/c51d3c56f0efdcbbf494b5a65e4309a8410d99c5

Compare link:

https://github.com/openai/codex/compare/main...dyjxg4xygary:fix/websocket-semantic-idle?expand=1

The patch wraps SessionTask::run() with catch_unwind(). On panic it emits an internal error event, then continues through the existing finish path so rollout flush, task finish handling, waiter notification, and TurnComplete still happen.

I could not open a PR because this repository currently limits PR creation to collaborators.

Regression test

The patch adds panicking_task_emits_error_and_completes_turn, which creates a synthetic SessionTask that panics in run() and asserts that Codex emits:

  1. EventMsg::Error with CodexErrorInfo::InternalServerError
  2. EventMsg::TurnComplete
  3. a cleared active_turn

Verification

Targeted tests passed locally:

CARGO_NET_GIT_FETCH_WITH_CLI=true cargo test -p codex-core session::tests::panicking_task_emits_error_and_completes_turn
CARGO_NET_GIT_FETCH_WITH_CLI=true cargo test -p codex-core session::tests::spawn_task_turn_span_inherits_dispatch_trace_context
just fmt
git diff --check

I also ran the full cargo test -p codex-core. It completed with unrelated local failures in existing agent init / shell snapshot / unified exec timing tests: 1617 passed; 9 failed; 3 ignored.

View original on GitHub ↗