{"record":{"id":"8d83a0f4907691aa","repo":"ZhuLinsen/daily_stock_analysis","slug":"request-conflict","errorCode":"request_conflict","errorMessage":"This Agent request is already running","messagePattern":"This Agent request is already running","errorType":"http","errorClass":"HTTPException","httpStatus":409,"severity":"error","filePath":"api/v1/endpoints/agent.py","lineNumber":516,"sourceCode":"\n    session_id = request.session_id or str(uuid.uuid4())\n    loop = asyncio.get_running_loop()\n    queue: asyncio.Queue = asyncio.Queue()\n    cancel_event = threading.Event()\n    request_id = request.request_id or str(uuid.uuid4())\n    skill_selection = session_service.resolve_skill_selection(\n        config,\n        session_id,\n        request.effective_skills,\n    )\n    skills = skill_selection.effective_skill_ids\n    selected_skill_ids = skill_selection.selected_skill_ids_update\n    stream_ctx = _build_agent_chat_context(request, config, skills)\n\n    if backend_id == \"codex_app_server\":\n        with _ACTIVE_CODEX_STREAMS_LOCK:\n            if request_id in _ACTIVE_CODEX_STREAMS:\n                raise HTTPException(\n                    status_code=409,\n                    detail={\n                        \"error\": \"request_conflict\",\n                        \"message\": \"This Agent request is already running\",\n                    },\n                )\n            _ACTIVE_CODEX_STREAMS[request_id] = cancel_event\n\n    def progress_callback(event: dict):\n        if backend_id == \"codex_app_server\" and cancel_event.is_set():\n            return\n        # Enrich tool events with display names\n        if event.get(\"type\") in (\"tool_start\", \"tool_done\"):\n            tool = event.get(\"tool\", \"\")\n            event[\"display_name\"] = TOOL_DISPLAY_NAMES.get(tool, tool)\n        asyncio.run_coroutine_threadsafe(queue.put(event), loop)\n\n    def run_sync(executor, turn):","sourceCodeStart":498,"sourceCodeEnd":534,"githubUrl":"https://github.com/ZhuLinsen/daily_stock_analysis/blob/5159bd72e8373d215492dff122acc9d389e219c9/api/v1/endpoints/agent.py#L498-L534","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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"],"exampleFix":"// before\nconst requestId = `chat-${stockCode}`; // stable id -> second open triggers 409\nopenStream(requestId);\n\n// after\nconst requestId = crypto.randomUUID(); // unique per stream\nopenStream(requestId);\n// on retry after disconnect, also fire cancel for the old id first\nawait fetch(`/api/v1/agent/chat/stream/${oldRequestId}/cancel`, {method:'POST'});","handlingStrategy":"validation","validationCode":"// Browser client: fresh id per stream + explicit cancel of the old one\nfunction newRequestId() { return crypto.randomUUID(); }\n// before reconnecting with the same logical conversation:\nasync function reopen(oldId) {\n  await fetch(`/api/v1/agent/chat/stream/${oldId}/cancel`, { method: 'POST' }).catch(() => {});\n  return newRequestId();\n}","typeGuard":null,"tryCatchPattern":"try:\n    stream = post_chat_stream(request_id=fresh_uuid)\nexcept HTTPError as e:\n    if e.response.status_code == 409 and e.response.json()['detail']['error'] == 'request_conflict':\n        cancel_stream(request_id)  # clear the live stream, then retry once with a NEW id\n        stream = post_chat_stream(request_id=new_uuid())\n    else:\n        raise","preventionTips":["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"],"tags":["agent","http-409","sse","concurrency","single-flight"],"backgroundTag":null,"analyzedSha":"5159bd72e8373d215492dff122acc9d389e219c9","analyzedAt":"2026-08-15T01:59:36.292Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}