{"record":{"id":"c1be19ad3fe5d4b5","repo":"can1357/oh-my-pi","slug":"delivery-id-did-not-reach-a-terminal-state-withi","errorCode":null,"errorMessage":"{delivery_id} did not reach a terminal state within {timeout_seconds:g}s (state={state})","messagePattern":"(.+?) did not reach a terminal state within (.+?)s \\(state=(.+?)\\)","errorType":"exception","errorClass":"ManualTriageTimeout","httpStatus":null,"severity":"warning","filePath":"python/robomp/src/manual_triage.py","lineNumber":149,"sourceCode":"    Pure DB polling — the caller MUST NOT spawn its own ``WorkerPool``; the\n    long-lived ``serve`` process is the only owner of the dispatcher loop.\n    Returns the final row, or ``None`` if the row was deleted while waiting.\n    Raises ``ManualTriageTimeout`` if ``timeout`` elapses first.\n    \"\"\"\n    deadline = None if timeout is None else time.monotonic() + timeout\n    while True:\n        row = db.get_event(delivery_id)\n        if row is None:\n            return None\n        if row.state in _TERMINAL_STATES:\n            return row\n\n        sleep_for = poll_interval\n        if deadline is not None:\n            remaining = deadline - time.monotonic()\n            if remaining <= 0:\n                assert timeout is not None\n                raise ManualTriageTimeout(delivery_id, row.state, timeout)\n            sleep_for = min(poll_interval, remaining)\n        await asyncio.sleep(sleep_for)\n\n\n__all__ = [\n    \"InvalidIssueRef\",\n    \"ManualTriageError\",\n    \"ManualTriageConflict\",\n    \"ManualTriageTimeout\",\n    \"await_terminal_state\",\n    \"build_issues_opened_payload\",\n    \"enqueue_manual_triage\",\n    \"manual_delivery_id\",\n    \"parse_issue_ref\",\n]\n","sourceCodeStart":131,"sourceCodeEnd":165,"githubUrl":"https://github.com/can1357/oh-my-pi/blob/969062200754ea02cfac922e5ebb8c608c079e15/python/robomp/src/manual_triage.py#L131-L165","documentation":"ManualTriageTimeout raised by await_terminal_state when the delivery event did not reach a terminal state (done/failed/skipped) within the given timeout. The message includes the elapsed timeout and the state last observed, so callers know whether it is still queued or running.","triggerScenarios":"Polling with await_terminal_state (from _go or _wait) while the triage run is slow, stuck, or the poll deadline expires before a terminal transition.","commonSituations":"Triage agent taking longer than the configured timeout, worker crashed leaving the event in \"running\" forever, too-short timeout for large repos.","solutions":["Increase the `timeout` (or extend the deadline) and poll again","Inspect the event row's state and logs to see if the run is progressing or wedged","If the run is stuck in a non-terminal state, cancel/reset it and re-enqueue"],"exampleFix":"// before\nawait await_terminal_state(db, delivery_id, timeout=30)\n// after\nawait await_terminal_state(db, delivery_id, timeout=300)","handlingStrategy":"try-catch","validationCode":"# ensure the delivery exists and is progressing before waiting\nrow = db.get_event(delivery_id)\nif row is None or row.state in (\"done\", \"failed\", \"skipped\"):\n    return row.state if row else \"missing\"","typeGuard":"TERMINAL = {\"done\", \"failed\", \"skipped\"}\ndef is_terminal(state: str) -> bool:\n    return state in TERMINAL","tryCatchPattern":"try:\n    await await_terminal_state(db, delivery_id, timeout=timeout_s)\nexcept ManualTriageTimeout as e:\n    log.warning(\"triage %s still %s after %ss\", delivery_id, e.args, timeout_s)\n    # inspect/cancel stuck run, or re-poll with a longer timeout","preventionTips":["Set timeout generously relative to expected triage duration","Monitor worker liveness so crashed runs don't stay 'running' forever","Cancel/reset events stuck in non-terminal states before re-enqueueing"],"tags":["timeout","polling","async"],"backgroundTag":"operation-timeout","analyzedSha":"969062200754ea02cfac922e5ebb8c608c079e15","analyzedAt":"2026-08-31T10:29:35.737Z","schemaVersion":2},"datasetVersion":"2026-08-31T14:17:45.589Z"}