Skip to content

Handle Codex steer requests during turn finalization without losing the message #330

Description

@germanescobar

Problem

When a user sends a steer message near the end of a Codex turn, Controller can reject it with No active turn for this session even though the UI still presents the conversation as streaming and allows the steer shortcut.

There is a race between the Codex app-server lifecycle and the client SSE lifecycle:

  • CodexAppServerManager.emit() clears turnInProgress and currentTurnId as soon as run.completed arrives.
  • SessionView keeps the composer in streaming/steer mode until the later top-level done SSE event.
  • The route waits for queued event persistence, diff calculation, session updates, and stream finalization before sending done.
  • A steer submitted in that interval reaches steerSession() after the active Codex turn has already ended and is rejected.

The failed request also causes message loss/confusing UI state: handleSteer() clears the draft and inserts an optimistic local user message before the server accepts the steer. On failure, the message remains visible locally with an error but was never persisted or delivered, and disappears after reload.

The current turn/steer payload matches the installed Codex app-server schema; this is Controller lifecycle coordination rather than protocol drift.

Expected behavior

A message submitted while the UI is transitioning from an active Codex turn to its completed state must not be lost:

  • If the turn is still steerable, deliver it through native turn/steer.
  • If the turn completed before the steer could be accepted, preserve it as a queued/follow-up message for the next turn.
  • Do not clear the composer or show a delivered user message until the server has accepted responsibility for it.
  • The UI should stop offering native steering as soon as it observes terminal run.completed / run.failed state, rather than waiting only for SSE done.

Acceptance criteria

  • A Codex message submitted between run.completed and SSE done is preserved and runs as a follow-up instead of producing No active turn for this session.
  • A genuine native-steer failure does not discard the composer draft or leave a false delivered-message bubble.
  • Normal mid-turn Codex steering continues to use turn/steer and persists the accepted user message exactly once.
  • Queue promotion still behaves correctly when the active turn finishes during the steer request.
  • Regression tests cover the terminal-event/finalization race and failed-steer draft behavior.

Relevant code

  • server/lib/codex-app-server.ts: steerSession() and terminal-event state cleanup.
  • server/routes/sessions.ts: Codex SSE finalization and /steer route.
  • client/src/pages/SessionView.tsx: terminal event handling and handleSteer() optimistic state changes.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions