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
- Retry enqueue_manual_triage after the competing run finishes (handle the conflict and re-enqueue later)
- Serialize triggers per repo#issue with a lock or single dispatcher
- 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
- Route manual triage through a single serialized dispatcher per repo#issue
- Use idempotent trigger IDs client-side
- Treat conflicts as 'another run won' and monitor that run instead of retrying immediately
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
- OAuth refresh ownership was lost before persistence
- No credential with id=${id}
- session header changed during stats cleanup: ${session.path}
- Daemon ${spec.name} is already starting
- RPC message snapshot does not match current messages
AI-assisted analysis of can1357/oh-my-pi@9690622007 (2026-08-31).
Data as JSON: /api/errors/d0c9f3700bd9f0ea.
Report an issue: GitHub.