From 86793327567c9eceaffac7c34bf41fcd15c273e5 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 9 Apr 2026 14:45:30 +0000 Subject: [PATCH] =?UTF-8?q?fix:=20bug:=20dev-poll=20stale=20detection=20ra?= =?UTF-8?q?ces=20with=20issue=5Fclaim=20=E2=80=94=20blocks=20freshly=20cla?= =?UTF-8?q?imed=20issues=20(#471)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add 60-second grace period to stale in-progress detection in dev-poll.sh. When a poller sees an in-progress issue with no assignee/PR/lock, it now checks the timeline API for when the label was added. If <60s ago, it skips stale detection to allow the claiming agent time to finish its assign+label sequence. Also documents the intentional assign-before-label ordering in issue_claim() that minimizes the race window. Co-Authored-By: Claude Opus 4.6 (1M context) --- dev/dev-poll.sh | 43 +++++++++++++++++++++++++++++++++++++++--- lib/issue-lifecycle.sh | 4 +++- 2 files changed, 43 insertions(+), 4 deletions(-) diff --git a/dev/dev-poll.sh b/dev/dev-poll.sh index 83cfa9f..ffd8613 100755 --- a/dev/dev-poll.sh +++ b/dev/dev-poll.sh @@ -103,6 +103,37 @@ is_blocked() { # STALENESS DETECTION FOR IN-PROGRESS ISSUES # ============================================================================= +# Check if in-progress label was added recently (within grace period). +# Prevents race where a poller marks an issue as stale before the claiming +# agent's assign + label sequence has fully propagated. See issue #471. +# Args: issue_number [grace_seconds] +# Returns: 0 if recently added (within grace period), 1 if not +in_progress_recently_added() { + local issue="$1" grace="${2:-60}" + local now label_ts delta + + now=$(date +%s) + + # Query issue timeline for the most recent in-progress label event + label_ts=$(curl -sf -H "Authorization: token ${FORGE_TOKEN}" \ + "${API}/issues/${issue}/timeline" | \ + jq -r '[.[] | select(.type == "label") | select(.label.name == "in-progress")] | last | .created_at // empty') || true + + if [ -z "$label_ts" ]; then + return 1 # no label event found — not recently added + fi + + # Convert ISO timestamp to epoch and compare + local label_epoch + label_epoch=$(date -d "$label_ts" +%s 2>/dev/null || echo 0) + delta=$(( now - label_epoch )) + + if [ "$delta" -lt "$grace" ]; then + return 0 # within grace period + fi + return 1 +} + # Check if there's an open PR for a specific issue # Args: issue_number # Returns: 0 if open PR exists, 1 if not @@ -460,9 +491,15 @@ if [ "$ORPHAN_COUNT" -gt 0 ]; then fi if [ "$OPEN_PR" = false ] && [ "$BLOCKED_BY_INPROGRESS" = false ]; then - log "issue #${ISSUE_NUM} is stale (no assignee, no open PR, no agent lock) — relabeling to blocked" - relabel_stale_issue "$ISSUE_NUM" "no_assignee_no_open_pr_no_lock" - BLOCKED_BY_INPROGRESS=true + # Grace period: skip if in-progress label was added <60s ago (issue #471) + if in_progress_recently_added "$ISSUE_NUM" 60; then + log "issue #${ISSUE_NUM} in-progress label added <60s ago — skipping stale detection (grace period)" + BLOCKED_BY_INPROGRESS=true + else + log "issue #${ISSUE_NUM} is stale (no assignee, no open PR, no agent lock) — relabeling to blocked" + relabel_stale_issue "$ISSUE_NUM" "no_assignee_no_open_pr_no_lock" + BLOCKED_BY_INPROGRESS=true + fi fi # Formula guard: formula-labeled issues should not be worked on by dev-agent. diff --git a/lib/issue-lifecycle.sh b/lib/issue-lifecycle.sh index 3792d3f..ed56c02 100644 --- a/lib/issue-lifecycle.sh +++ b/lib/issue-lifecycle.sh @@ -102,7 +102,9 @@ issue_claim() { return 1 fi - # Assign to self (Forgejo rejects if already assigned differently) + # Assign to self BEFORE adding in-progress label (issue #471). + # This ordering ensures the assignee is set by the time other pollers + # see the in-progress label, reducing the stale-detection race window. curl -sf -X PATCH \ -H "Authorization: token ${FORGE_TOKEN}" \ -H "Content-Type: application/json" \