{"record":{"id":"bd91ee10067ea4bf","repo":"can1357/oh-my-pi","slug":"delivery-target-is-event-state-only-inactive","errorCode":null,"errorMessage":"delivery {target} is {event.state}; only inactive events can be retried","messagePattern":"delivery (.+?) is (.+?); only inactive events can be retried","errorType":"http","errorClass":"HTTPException","httpStatus":409,"severity":"warning","filePath":"python/robomp/src/server.py","lineNumber":661,"sourceCode":"        elif isinstance(issue_ref, str) and issue_ref:\n            try:\n                repo_full, number = parse_issue_ref(issue_ref)\n            except InvalidIssueRef as exc:\n                raise HTTPException(400, str(exc)) from exc\n            if not cfg.allows(repo_full):\n                raise HTTPException(403, f\"{repo_full} not in ROBOMP_REPO_ALLOWLIST\")\n            row = db.latest_event_for_issue(make_issue_key(repo_full, number))\n            if row is None:\n                raise HTTPException(404, f\"no retryable stored event for {repo_full}#{number}\")\n            target = row.delivery_id\n        else:\n            raise HTTPException(400, \"retry requires 'delivery_id' or 'issue'\")\n\n        event = db.get_event(target)\n        if event is None:\n            raise HTTPException(404, f\"unknown delivery {target}\")\n        if not db.requeue_event(target, from_states=INACTIVE_EVENT_STATES):\n            raise HTTPException(409, f\"delivery {target} is {event.state}; only inactive events can be retried\")\n        pool.wake()\n        log.info(\"manual retry\", extra={\"delivery\": target})\n        return JSONResponse(\n            {\"delivery\": target, \"state\": \"queued\", \"mode\": \"retry\"},\n            status_code=202,\n        )\n\n    @app.post(\"/api/cancel\")\n    async def api_cancel(\n        request: Request,\n        payload: dict[str, Any] = Body(...),\n        x_robomp_token: str | None = Header(None, alias=\"X-Robomp-Replay-Token\"),\n    ) -> JSONResponse:\n        \"\"\"Stop a running event. The omp subprocess is killed; the row lands in\n        `failed` with `cancelled by operator` as the error.\n        \"\"\"\n        bag = request.app.state.bag\n        cfg: Settings = bag[\"settings\"]","sourceCodeStart":643,"sourceCodeEnd":679,"githubUrl":"https://github.com/can1357/oh-my-pi/blob/969062200754ea02cfac922e5ebb8c608c079e15/python/robomp/src/server.py#L643-L679","documentation":"The delivery exists but is still active, so the retry endpoint returns HTTP 409 'delivery <target> is <state>; only inactive events can be retried'. db.requeue_event(target, from_states=INACTIVE_EVENT_STATES) only transitions events whose current state is inactive (e.g. failed/skipped); when the conditional requeue returns False the server surfaces the event's actual state so you know why the retry was refused. Retrying a running or queued delivery would duplicate work.","triggerScenarios":"POSTing to the retry endpoint with a delivery_id whose event.state is still 'running', 'queued', or otherwise outside INACTIVE_EVENT_STATES — requeue_event returns False and the server raises the 409 with the live state embedded in the message.","commonSituations":"Clicking 'retry' on a job that is actually still executing (dashboard state stale); a slow agent run that looks hung but is still active; automation retrying on a timer without checking state first; double-firing the retry while the first attempt is mid-flight.","solutions":["Wait for the delivery to reach an inactive state (finished/failed), then retry","Inspect the event's current state (message body tells you; or query events/logs) before retrying","If the run is genuinely hung, cancel it via the cancel endpoint (requires state 'running'), then retry once inactive","Add polling with backoff: only re-issue the retry after the state endpoint reports an inactive state"],"exampleFix":"// before: fire-and-forget retry\nawait api.retry(deliveryId);\n// after: retry only when inactive\nconst evt = await api.getEvent(deliveryId);\nif (!['running', 'queued'].includes(evt.state)) await api.retry(deliveryId);","handlingStrategy":"retry","validationCode":"// Only retry when the event is in an inactive state\nconst evt = await api.getEvent(deliveryId);\nconst INACTIVE = ['failed', 'skipped', 'done', 'cancelled']; // match server INACTIVE_EVENT_STATES\nif (!evt || !INACTIVE.includes(evt.state)) {\n  throw new Error(`delivery ${deliveryId} is ${evt?.state ?? 'unknown'}; not retryable yet`);\n}","typeGuard":"function isInactiveEvent(e: { state: string }): boolean {\n  return !['queued', 'running'].includes(e.state);\n}","tryCatchPattern":"try {\n  await api.retry({ delivery_id: id });\n} catch (err) {\n  if (err.status === 409 && /only inactive events can be retried/.test(err.message)) {\n    logger.info('still active; will retry after it becomes inactive', { id, state: err.message });\n    await pollUntilInactive(id, { timeoutMs: 15 * 60_000 });\n    await api.retry({ delivery_id: id });\n  } else throw err;\n}","preventionTips":["Poll the event state and only issue retries on inactive states","Use backoff — active runs can take minutes","If a run is truly stuck, cancel it (cancel requires state 'running') before retrying","Never fire retries from multiple clients concurrently for the same delivery"],"tags":["http-409","conflict","state-machine","retry","concurrency"],"backgroundTag":"invalid-state-transition","analyzedSha":"969062200754ea02cfac922e5ebb8c608c079e15","analyzedAt":"2026-08-31T10:29:35.737Z","schemaVersion":2},"datasetVersion":"2026-08-31T14:17:45.589Z"}