can1357/oh-my-pi · warning · HTTPException

{str(exc)} (ManualTriageConflict)

Error message

{str(exc)} (ManualTriageConflict)

What it means

The manual triage endpoint raised ManualTriageConflict, which api_trigger translates into HTTP 409 with the exception's message. The server throws this when the triage operation cannot proceed because the issue or delivery is already being handled or is in a state that conflicts with a new triage run (a concurrency/state conflict, not a bad request). The suffix '(ManualTriageConflict)' in the logged message marks the exception class so operators can distinguish it from other triage failures.

Source

Thrown at python/robomp/src/server.py:628

        if mode == "triage":
            if not isinstance(issue_ref, str) or not issue_ref:
                raise HTTPException(400, "triage requires 'issue' = 'owner/repo#NN'")
            try:
                repo_full, number = parse_issue_ref(issue_ref)
            except InvalidIssueRef as exc:
                raise HTTPException(400, str(exc)) from exc
            if not cfg.allows(repo_full):
                raise HTTPException(403, f"{repo_full} not in ROBOMP_REPO_ALLOWLIST")
            try:
                delivery = await enqueue_manual_triage(
                    db=db,
                    github=github,
                    repo_full=repo_full,
                    number=number,
                )
            except ManualTriageConflict as exc:
                raise HTTPException(409, str(exc)) from exc
            except ManualTriageError as exc:
                raise HTTPException(400, str(exc)) from exc
            except GitHubError as exc:
                raise HTTPException(502, f"github error: {exc.status} {exc.message}") from exc
            pool.wake()
            log.info("manual triage", extra={"delivery": delivery, "issue": f"{repo_full}#{number}"})
            return JSONResponse(
                {"delivery": delivery, "state": "queued", "mode": "triage"},
                status_code=202,
            )

        # mode == "retry"
        if isinstance(delivery_id, str) and delivery_id:
            target = delivery_id
        elif isinstance(issue_ref, str) and issue_ref:
            try:
                repo_full, number = parse_issue_ref(issue_ref)
            except InvalidIssueRef as exc:

View on GitHub (pinned to 9690622007)

Solutions

  1. Wait for the in-flight triage run on that issue/delivery to finish, then retry the trigger request
  2. Check the delivery/issue state (e.g. via the events/state API or logs) to confirm an active run before re-triggering
  3. Add client-side idempotency: dedupe by delivery_id/issue key so duplicate triggers are not sent
  4. If a previous run is wedged, cancel or reset that delivery's state, then trigger again

Example fix

// before: blind re-trigger loop
for await (const d of deliveries) await api.trigger(d);
// after: skip deliveries already in an active/conflicting state
const active = await api.listActive();
const activeKeys = new Set(active.map(a => a.issueKey));
for (const d of deliveries) if (!activeKeys.has(d.issueKey)) await api.trigger(d.deliveryId);
Defensive patterns

Strategy: try-catch

Validate before calling

// Check no active/conflicting run exists before triggering
const evt = await api.getLatestEventForIssue('acme/widgets#42');
if (evt && ['queued', 'running'].includes(evt.state)) {
  throw new Error(`triage already active for ${evt.deliveryId} (state=${evt.state})`);
}

Type guard

function isConflictResponse(res: { status: number; body?: { detail?: string } }): boolean {
  return res.status === 409 && typeof res.body?.detail === 'string';
}

Try / catch

try {
  const res = await api.trigger(payload);
} catch (err) {
  if (err.status === 409 && /ManualTriageConflict/.test(err.message)) {
    logger.warn('triage already in progress; backing off', { payload });
    await Bun.sleep(backoffMs); // re-check state, then decide whether to re-trigger
  } else throw err;
}

Prevention

When it happens

Trigger: POSTing to the trigger API (with a valid trigger token and delivery payload) while the target issue/delivery is already locked by an in-flight triage run, or re-triggering triage for an event whose state conflicts with an active run. Raised by the inner triage call inside `except ManualTriageConflict as exc: raise HTTPException(409, str(exc))`.

Common situations: Double-clicking a 'triage' button so two requests race on the same issue; a webhook redelivery firing while the first triage is still running; automated retry scripts polling the trigger endpoint faster than triage completes; a stale UI session re-submitting an already-queued item.

Related errors


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