Skip to content

fix(agentic): phase-eval status cleanup removes labels from an event snapshot, so a concurrent dispatch fails the run with a 404 #1566

Description

@rickylabs

Summary

openhands-phase-eval.yml's status-cleanup step removes status:* labels read from the event payload
snapshot
rather than from live state. When two dispatch runs fire seconds apart for the same head, the
first removes status:impl and the second's removeLabel returns 404 Label does not exist, failing the
whole step and turning the workflow run red — on a PR whose evaluation actually succeeded.

This is a trust defect, not a correctness defect: exactly-once dispatch held. What it produces is a
false red that costs investigation time, and it recurs on any PR that gets two near-simultaneous dispatch
events.

Observed instance — PR #1541, head 0503991ab

31596291515  completed/failure  event=pull_request  created=12:24:36Z   <- raced, red
31596293364  completed/success  event=pull_request  created=12:24:38Z   <- posted the authoritative trigger

Two seconds apart, same head. Failure log, step "Enter IMPL-EVAL status on ready transition":

DELETE /repos/rickylabs/netscript/issues/1541/labels/status%3Aimpl - 404
retry-exempt-status-codes: 400,401,403,404,422

404 is deliberately retry-exempt, which is correct — a retry cannot help — so the step fails hard.

Exactly-once was not violated. Verified: exactly one trigger marker exists for that head —
generation=29339092792 phase=impl head=0503991ab…, model openrouter/deepseek/deepseek-v4-flash-0731.
The generation-dedup mechanism did its job. Only the label cleanup raced.

Root cause

.github/workflows/openhands-phase-eval.yml, "Enter IMPL-EVAL status on ready transition":

const labels = context.payload.pull_request.labels.map((label) => label.name);
for (const label of labels.filter((name) => name.startsWith('status:'))) {
  await github.rest.issues.removeLabel({ owner, repo, issue_number, name: label });
}
await github.rest.issues.addLabels({ owner, repo, issue_number, labels: ['status:impl-eval'] });

context.payload.pull_request.labels is a snapshot taken when the event was created. The loop then
issues unconditional deletes against live state. Any concurrent path that removed a status: label between
event creation and this step makes the delete 404.

This is the same defect class as #1564acting on a payload snapshot instead of live state. There it is
pull_request.base.sha feeding a diff range; here it is pull_request.labels feeding a delete loop. Worth
noting because the two fixes rhyme: read live, or compute from something current.

Why it matters more than a stray red

A red run on a PR whose evaluation succeeded is the precise stimulus that teaches operators to discount red
runs. It has already cost real time in this milestone: this defect is what produced the apparent
"duplicate evaluator run" that had to be investigated and then corrected with the finding that extra raw
workflow entries were no-op/skip events and exactly-once was intact. The investigation was necessary
because a red run implied something had gone wrong with dispatch. Nothing had.

Fix

  1. Make status cleanup idempotent. Either fetch live labels immediately before removing
    (issues.listLabelsOnIssue) and remove only what is actually present, or tolerate the missing-label
    case — narrowly.
  2. Narrowly means narrowly. Tolerate only a 404 whose body is the missing-label error for the label
    being removed. A blanket try {} catch {} around removeLabel would swallow a 403 from a permissions
    regression or a 404 from a wrong issue_number, converting a real failure into a silent pass. Rethrow
    anything else, and keep the existing retry-exempt-status-codes behaviour unchanged.
  3. Preserve generation deduplication exactly as it is. It already works — one trigger for one head under
    a genuine race — and it is the property that keeps this defect cosmetic. Do not restructure it.

Acceptance criteria

  • A concurrent removal of status:impl between event creation and the cleanup step no longer fails the
    workflow: the step completes and status:impl-eval is applied exactly once.
  • Proven RED: a race regression test that fails before the change — the label absent at removal time
    causes the step's logic to throw — and passes after.
  • Tolerance is narrow, asserted by test: a non-missing-label error from removeLabel (for example
    403, or a 404 for a different resource) still fails the step. A blanket catch fails this box.
  • Generation deduplication is unchanged: under two dispatch events for one head, exactly one trigger
    comment is posted. Asserted by test rather than by inspection.
  • The terminal label state after cleanup is exactly one status: label, per the taxonomy's
    single-status rule.
  • gate: the phase-eval script's tests pass, and .github/scripts/ type-check, lint and format via the
    scoped wrappers.

Boundaries

Provenance

Observed on PR #1541 at head 0503991ab, 2026-08-12, runs 31596291515 (red) and 31596293364 (green).
Run facts, the 404 body, and the exactly-once verification were reproduced from the Actions API and the PR's
trigger markers by the 0.0.6 internals lane before filing. No pre-existing issue covers it (searched).

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions