Skip to content

daemon: adapter Closed can overtake queued events, losing the tail and mislabeling failed sessions as done #1082

Description

@edwin-zvs

Summary

When an adapter emits events and then exits immediately, the daemon can drop the final events and mark the session terminal from the adapter process's exit code instead. The user-visible result is a session that shows done ✓ while its last events — including Error and Status{Errored} — never reach the transcript.

Why it happens

Two independent tasks feed one mpsc::Sender<AdapterMessage>:

  • crates/daemon/src/adapter.rs — the reader task turns adapter stdout notifications into AdapterMessage::Event.
  • crates/daemon/src/adapter.rs — a separate wait task sends AdapterMessage::Closed { exit_code } as soon as child.wait() returns.

Ordering between two senders on one channel isn't guaranteed, so Closed can overtake events the reader hasn't dispatched yet. In crates/daemon/src/session.rs, drain_adapter's Closed arm ends in break, so anything still queued behind it is discarded. The same arm derives the terminal state from the adapter process's exit code — which is 0 whenever the adapter returns Ok(()), regardless of what the session actually did — so the session lands on Done rather than Errored.

Repro

Any adapter that emits an event and then returns promptly. Concretely, with a headless codex adapter that emits Error + Status{Errored} + Done{1} and then exits (the shape proposed in #1073), 10 runs of the identical scenario gave:

  • 4/10 — errored, error event present in the transcript
  • 6/10 — done ✓, zero error events, transcript truncated at the last stdout line

Inserting a 300 ms sleep after the final Done emit makes it 6/6 correct; removing it restores the flakiness. That timing dependence is the race.

Impact

  • Failure signaling from any adapter that exits right after reporting is unreliable — the failure is silently reported as success.
  • It also means transcript tails can be truncated for short-lived adapter runs generally, not just error paths.

Suggested fix

Make Closed a drain barrier rather than a cut-off: have the reader task signal completion and have drain_adapter process everything the reader dispatched before applying Closed (e.g. Closed flows through the reader task after EOF, or the wait task waits on the reader's join handle before sending). Deriving a session's terminal state from the adapter process's exit code is also worth revisiting — an adapter that reports a failed session still exits 0.

Found while reviewing #1073.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions