# Claude and Codex sessions take one turn at a time

> Agno's ClaudeAgent and CodexAgent now take one turn per session. AgentOS answers a turn sent while another is in flight with HTTP 409 and error_type session_busy, so clients may need to handle that 409.

- Published: 2026-10-12
- Author: Kaustubh Shukla
- Categories: Changelog
- Canonical: https://www.agno.com/blog/claude-and-codex-sessions-take-one-turn-at-a-time
- Markdown: https://www.agno.com/blog/claude-and-codex-sessions-take-one-turn-at-a-time.md

Agno checks the session before a `ClaudeAgent` or `CodexAgent` turn starts. If another turn on it is still pending or running, the agent raises `SessionBusyError` without calling the SDK or writing a run row, and AgentOS turns that into a 409 whose body names the run in flight. A client that sends overlapping turns for one session needs to catch that 409, wait for the named run or cancel it, and send the turn again.

Claude Code and Codex each keep one conversation per session on their own side. In earlier releases Codex accepted the second turn and failed it on the app-server's thread lock (`already has an active writer`), which left an error run in the session, and Claude would have written two turns into one transcript.

We served a `ClaudeAgent` through AgentOS with `TestClient`, started a background turn, and sent a second one for the same session while the first was running. A stand-in `claude_agent_sdk` played Claude, since no Claude login was available (real output):

```text
first turn: 202 PENDING
second turn: 409
{
  "detail": "Session chat already has run 24b423c7-b30f-426b-92c3-8b219aac35e1 in flight. A session takes one turn at a time: wait for that run to finish, or cancel it, then send the turn again.",
  "error_id": "session_busy",
  "error_type": "session_busy"
}
second turn, streamed: 200 data: {"created_at":1792581048,"event":"RunError","agent_id":"claude","agent_name":"Claude","session_id":"chat","nested_depth":0,"content":"Session chat already has run 24b423c7-b30f-426b-92c3-8b219aac35e1 in flight. A session takes one turn at a time: wait for that run to finish, or cancel it, then send the turn again.","error_type":"session_busy","error_id":"session_busy"}
after cancel: 200
```

A streamed request gets its answer as a `RunError` event carrying the same `error_type`. The check counts the turns this process is running and, with a database, the session's pending and running run rows, so a turn accepted on another replica keeps the session busy too. A background submission gets its 409 at submission, before its pending row is written. Cancelling the run in flight frees the session at once, as the last line shows, and a run that a crash left pending or running holds the session until someone cancels it.

## Queued turns wait their turn

The durable queue keeps a waiting turn from spending its attempts on `SessionBusyError`. Each built-in queue store, Postgres, Redis and in-memory, picks the next turn for a session before it spends an attempt or a worker slot, so a second worker leaves the waiting turn alone. Two workers sharing an in-memory store showed it (real output, with the test suite's stand-in agent):

```text
worker one claims: turn-1
worker two claims while turn-1 runs: None | turn-2 attempts used: 0
turn-1 completed attempts: 1
turn-2 completed attempts: 1
```

## The 409 keeps the connection open

AgentOS registers an exception handler for `SessionBusyError` alone, which returns the 409 without re-raising it to the server. The keep-alive connection stays open for the next request and the server logs one warning line, so a pooled client such as an SDK or a load balancer keeps its connection through routine conflicts. Two busy requests on one keep-alive connection to a uvicorn server, with the same stand-in SDK, then a third after the first turn finished, printed this (real output, trimmed):

```text
WARNING SessionBusyError: 409 Session chat already has run e74be6d8-bc01-4afd-97b1-61cd43ed46ee in flight. A session takes one turn at a time: wait for that run to finish, or cancel it, then send the turn again.
turn while busy, request 1: 409 keep_alive=True
turn while busy, request 2: 409 keep_alive=True
first turn: 200
turn after it finished: 200 'one, two, three'
```

See the [cookbook](https://github.com/agno-agi/agno/blob/main/cookbook/frameworks/claude-agent-sdk/session_busy_agentos.py), and learn more about [deploying Claude and Codex agents](https://docs.agno.com/agent-os/multi-framework/deployment) in the documentation.
