can1357/oh-my-pi · info · HTTPException
delivery {delivery_id} is {event.state}; only running delive
Error message
delivery {delivery_id} is {event.state}; only running deliveries can be cancelled What it means
FastAPI raises HTTP 409 with 'delivery {id} is {state}; only running deliveries can be cancelled' from POST /api/cancel when the event exists but its recorded state is not 'running'. Cancelling only applies to in-flight tasks; completed, failed, or queued deliveries cannot be cancelled, only retried (inactive states) via /api/trigger retry mode.
Source
Thrown at python/robomp/src/server.py:691
x_robomp_token: str | None = Header(None, alias="X-Robomp-Replay-Token"),
) -> JSONResponse:
"""Stop a running event. The omp subprocess is killed; the row lands in
`failed` with `cancelled by operator` as the error.
"""
bag = request.app.state.bag
cfg: Settings = bag["settings"]
_require_trigger_token(cfg, x_robomp_token)
delivery_id = payload.get("delivery_id")
if not isinstance(delivery_id, str) or not delivery_id:
raise HTTPException(400, "cancel requires 'delivery_id'")
db: Database = bag["db"]
event = db.get_event(delivery_id)
if event is None:
raise HTTPException(404, f"unknown delivery {delivery_id}")
if event.state != "running":
raise HTTPException(
409, f"delivery {delivery_id} is {event.state}; only running deliveries can be cancelled"
)
pool: WorkerPool = bag["pool"]
fired = await pool.cancel_event(delivery_id)
log.info(
"manual cancel",
extra={"delivery": delivery_id, "fired": fired, "state": event.state},
)
return JSONResponse(
{"delivery": delivery_id, "fired": fired, "previous_state": event.state},
status_code=202,
)
@app.get("/events")
async def events(request: Request, limit: int = 50) -> dict[str, Any]:
rows = request.app.state.bag["db"].list_events(limit=limit)
return {View on GitHub (pinned to 9690622007)
Solutions
- Check GET /events for the delivery's current state; if it is done/failed, nothing needs cancelling.
- For queued deliveries use POST /api/trigger with mode 'retry' (requeues inactive events) rather than cancel.
- Refresh the dashboard and retry only if the event is genuinely still running; the state row may have been stale.
- Treat the 409 as informational — the task already reached a terminal state; inspect last_error in /events if it failed.
Defensive patterns
Strategy: validation
Validate before calling
const { events } = await (await fetch('/events')).json();
const ev = events.find(e => e.delivery_id === deliveryId);
if (!ev) throw new Error('unknown delivery');
if (ev.state !== 'running') throw new Error(`delivery is ${ev.state}; only running deliveries can be cancelled`); Try / catch
try {
await api.cancel(deliveryId);
} catch (e) {
if (e instanceof ApiError && e.status === 409) {
console.info('already finished — nothing to cancel; use trigger/retry if needed');
} else throw e;
} Prevention
- Check the delivery state via GET /events immediately before cancelling.
- Refresh the dashboard view; stale listings cause double-cancels.
- Use POST /api/trigger (retry mode) for inactive deliveries instead of cancel.
- Handle double-click/multiple operators with optimistic UI disable after the first cancel.
When it happens
Trigger: POST /api/cancel for a delivery whose event.state is 'done' (already finished), 'failed' (already errored or previously cancelled), or 'queued' (not yet claimed by the dispatcher); double-clicking cancel after the first cancel already flipped the row to failed.
Common situations: Operator sees a stale dashboard listing and cancels an already-finished task; two maintainers cancel concurrently; racing the worker which completed the task between fetch and cancel; trying to cancel a queued event that should be retried/cancelled via trigger instead.
Understand the failure class
Background: "Invalid state transition" errors: "status must be X, actually Y", "already rejected/charging/uninstalled", "cannot ... while running" — what they mean when a library rejects your call — this error's family across 31 libraries.
Related errors
- delivery {target} is {event.state}; only inactive events can
- {str(exc)} (ManualTriageConflict)
- Managed skill "${name}" already exists. Use action "update"
- Call rewrite before approve: there is no draft to accept
- Draft ${draft.round} has not been reviewed yet. End this tur
AI-assisted analysis of can1357/oh-my-pi@9690622007 (2026-08-31).
Data as JSON: /api/errors/d84bad10f2c64b89.
Report an issue: GitHub.