can1357/oh-my-pi · error · ManualTriageConflict

ManualTriageConflict(delivery, state)

Error message

ManualTriageConflict(delivery, state)

What it means

ManualTriageConflict raised when the optimistic replace_event_if_state_in fails — the event row was concurrently changed out of the allowed inactive states between the earlier check and the write, so the enqueue lost a race. `state` is the row's current state (or "active" if the row vanished).

Source

Thrown at python/robomp/src/manual_triage.py:115

    delivery = manual_delivery_id(repo_full, number)
    existing = db.get_event(delivery)
    if existing is not None and existing.state in ("queued", "running"):
        raise ManualTriageConflict(delivery, existing.state)

    payload = await build_issues_opened_payload(github, repo_full, number)
    replaced = db.replace_event_if_state_in(
        delivery_id=delivery,
        event_type="issues",
        repo=repo_full,
        issue_key=issue_key(repo_full, number),
        payload=payload,
        state="queued",
        allowed_existing_states=INACTIVE_EVENT_STATES,
    )
    if not replaced:
        current = db.get_event(delivery)
        state = current.state if current is not None else "active"
        raise ManualTriageConflict(delivery, state)
    return delivery


_TERMINAL_STATES: tuple[str, ...] = ("done", "failed", "skipped")


async def await_terminal_state(
    db: Database,
    delivery_id: str,
    *,
    poll_interval: float = 2.0,
    timeout: float | None = None,
) -> EventRow | None:
    """Block until the event row reaches a terminal state, vanishes, or times out.

    Pure DB polling — the caller MUST NOT spawn its own ``WorkerPool``; the
    long-lived ``serve`` process is the only owner of the dispatcher loop.
    Returns the final row, or ``None`` if the row was deleted while waiting.

View on GitHub (pinned to 9690622007)

Solutions

  1. Retry enqueue_manual_triage after the competing run finishes (handle the conflict and re-enqueue later)
  2. Serialize triggers per repo#issue with a lock or single dispatcher
  3. Inspect the event's current state to decide whether to wait or abandon

Example fix

// before: bare call, races possible
await enqueue_manual_triage(github, db, repo, num)
// after: tolerate the race with one retry
try:
    await enqueue_manual_triage(github, db, repo, num)
except ManualTriageConflict:
    await asyncio.sleep(1)
    await enqueue_manual_triage(github, db, repo, num)
Defensive patterns

Strategy: retry

Try / catch

try:
    delivery = await enqueue_manual_triage(github, db, repo_full, number)
except ManualTriageConflict:
    await asyncio.sleep(poll_interval)
    delivery = await enqueue_manual_triage(github, db, repo_full, number)

Prevention

When it happens

Trigger: Two concurrent enqueue_manual_triage calls racing: one replaces the row to "queued" between the other's get_event check and replace_event_if_state_in write.

Common situations: Parallel API triggers or dashboard/API simultaneous manual triage for the same issue.

Related errors


AI-assisted analysis of can1357/oh-my-pi@9690622007 (2026-08-31). Data as JSON: /api/errors/d0c9f3700bd9f0ea. Report an issue: GitHub.