ZhuLinsen/daily_stock_analysis · error · HTTPException
request_conflict
request_conflict
Error message
This Agent request is already running
What it means
Starting a Codex app-server agent chat stream returns HTTP 409 code=request_conflict when the same request_id already has an active SSE stream. The module keeps an in-process dict _ACTIVE_CODEX_STREAMS (api/v1/endpoints/agent.py:47) guarded by a lock; the guard at line 514-523 registers request_id -> cancel_event on stream start and removes it at stream end (line 648-650). It is a single-flight guard: one live stream per request_id per server process.
Source
Thrown at api/v1/endpoints/agent.py:516
session_id = request.session_id or str(uuid.uuid4())
loop = asyncio.get_running_loop()
queue: asyncio.Queue = asyncio.Queue()
cancel_event = threading.Event()
request_id = request.request_id or str(uuid.uuid4())
skill_selection = session_service.resolve_skill_selection(
config,
session_id,
request.effective_skills,
)
skills = skill_selection.effective_skill_ids
selected_skill_ids = skill_selection.selected_skill_ids_update
stream_ctx = _build_agent_chat_context(request, config, skills)
if backend_id == "codex_app_server":
with _ACTIVE_CODEX_STREAMS_LOCK:
if request_id in _ACTIVE_CODEX_STREAMS:
raise HTTPException(
status_code=409,
detail={
"error": "request_conflict",
"message": "This Agent request is already running",
},
)
_ACTIVE_CODEX_STREAMS[request_id] = cancel_event
def progress_callback(event: dict):
if backend_id == "codex_app_server" and cancel_event.is_set():
return
# Enrich tool events with display names
if event.get("type") in ("tool_start", "tool_done"):
tool = event.get("tool", "")
event["display_name"] = TOOL_DISPLAY_NAMES.get(tool, tool)
asyncio.run_coroutine_threadsafe(queue.put(event), loop)
def run_sync(executor, turn):View on GitHub (pinned to 5159bd72e8)
Solutions
- Generate a fresh unique request_id (uuid) for each new stream instead of reusing one
- Wait for the previous stream's completion (or its server-sent close event) before re-issuing the same request_id
- If the previous stream is stuck, call POST /agent/chat/stream/{request_id}/cancel to end it, which lets the generator clean up and free the slot, then start a new stream
- Check server logs/stream state if 409 persists with no visible open stream — the entry may be orphaned until the connection actually closes
Example fix
// before
const requestId = `chat-${stockCode}`; // stable id -> second open triggers 409
openStream(requestId);
// after
const requestId = crypto.randomUUID(); // unique per stream
openStream(requestId);
// on retry after disconnect, also fire cancel for the old id first
await fetch(`/api/v1/agent/chat/stream/${oldRequestId}/cancel`, {method:'POST'}); Defensive patterns
Strategy: validation
Validate before calling
// Browser client: fresh id per stream + explicit cancel of the old one
function newRequestId() { return crypto.randomUUID(); }
// before reconnecting with the same logical conversation:
async function reopen(oldId) {
await fetch(`/api/v1/agent/chat/stream/${oldId}/cancel`, { method: 'POST' }).catch(() => {});
return newRequestId();
} Try / catch
try:
stream = post_chat_stream(request_id=fresh_uuid)
except HTTPError as e:
if e.response.status_code == 409 and e.response.json()['detail']['error'] == 'request_conflict':
cancel_stream(request_id) # clear the live stream, then retry once with a NEW id
stream = post_chat_stream(request_id=new_uuid())
else:
raise Prevention
- Always generate a unique request_id per stream (uuid), never derive it from conversation/stock keys
- Disable the send button while a stream for the same id is open
- Cancel before reconnecting after disconnects
- Remember the registry is per-process: use sticky routing or a single worker for agent streams
When it happens
Trigger: POSTing /agent/chat/stream (backend_id='codex_app_server') with a request_id that is still streaming — e.g. double-clicking send in the web UI, retrying a 'timed out' request while the server keeps the original SSE open, or reusing a client-generated request_id for parallel tabs. Only applies to the codex_app_server backend; other backends never register the entry and never 409 here.
Common situations: Frontend retry logic that regenerates the same request_id after a network hiccup while the original stream is alive; multiple Electron/Web clients sharing a deterministic request_id; streams that finished client-side but are still open server-side (slow downstream consumer) so the registry entry was never popped; also single-worker only — the dict is per-process, so multi-worker deployments will not detect cross-worker conflicts.
Related errors
- request_not_active
- Agent mode is not enabled
- history deletion made no progress
- unsupported_agent_arch
- Tool '{name}' not found in registry. Available: {self.list_n
AI-assisted analysis of ZhuLinsen/daily_stock_analysis@5159bd72e8 (2026-08-15).
Data as JSON: /api/errors/8d83a0f4907691aa.
Report an issue: GitHub.