From 39f487516223326ccdef217631a4aabd177a9dd6 Mon Sep 17 00:00:00 2001 From: marcusquinn <6428977+marcusquinn@users.noreply.github.com> Date: Sun, 15 Mar 2026 18:19:17 +0000 Subject: [PATCH 1/2] fix: prevent duplicate dispatch across runners via assignee check + jitter MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Root cause: multiple pulse runners evaluating the same issue simultaneously create duplicate PRs. Process-based dedup (has_worker_for_repo_issue, is-duplicate) only sees local processes — invisible across machines. Fix 1: Add is-assigned command to dispatch-dedup-helper.sh that queries GitHub assignees before dispatch. If another runner already self-assigned, skip the issue. This is the primary cross-machine dedup guard. Fix 2: Add 0-30s random startup jitter to pulse-wrapper.sh so concurrent launchd-triggered pulses don't evaluate issues at the same instant. Configurable via PULSE_JITTER_MAX (set to 0 to disable). Fix 3: Update pulse.md dispatch instructions to enforce the assignee check as a mandatory step alongside existing local process dedup. Observed: PR #4940 duplicated PR #4938 for issue #4937 because alex-solovyev's pulse dispatched 2 min after marcusquinn self-assigned, interpreting the in-progress worker as 'failed'. --- .agents/scripts/commands/pulse.md | 11 +++- .agents/scripts/dispatch-dedup-helper.sh | 81 +++++++++++++++++++++++- .agents/scripts/pulse-wrapper.sh | 20 ++++++ 3 files changed, 109 insertions(+), 3 deletions(-) diff --git a/.agents/scripts/commands/pulse.md b/.agents/scripts/commands/pulse.md index abad0107e..7ef705610 100644 --- a/.agents/scripts/commands/pulse.md +++ b/.agents/scripts/commands/pulse.md @@ -60,13 +60,20 @@ Check external contributor gate before ANY merge (see Pre-merge checks below). For each unassigned, non-blocked issue with no open PR and no active worker: ```bash -# Dedup guard (MANDATORY) +# Dedup guard (MANDATORY — all three checks required) source ~/.aidevops/agents/scripts/pulse-wrapper.sh +RUNNER_USER=$(gh api user --jq '.login' 2>/dev/null || whoami) + +# 1. Local process dedup (same machine only) if has_worker_for_repo_issue NUMBER SLUG; then continue; fi if ~/.aidevops/agents/scripts/dispatch-dedup-helper.sh is-duplicate "Issue #NUMBER: TITLE"; then continue; fi +# 2. Cross-machine assignee dedup (checks GitHub — visible to ALL runners) +# This is the primary guard against duplicate dispatch across machines. +# If another runner already assigned themselves, skip this issue. +if ~/.aidevops/agents/scripts/dispatch-dedup-helper.sh is-assigned NUMBER SLUG "$RUNNER_USER"; then continue; fi + # Assign and dispatch -RUNNER_USER=$(gh api user --jq '.login' 2>/dev/null || whoami) gh issue edit NUMBER --repo SLUG --add-assignee "$RUNNER_USER" --add-label "status:queued" 2>/dev/null || true ~/.aidevops/agents/scripts/headless-runtime-helper.sh run \ diff --git a/.agents/scripts/dispatch-dedup-helper.sh b/.agents/scripts/dispatch-dedup-helper.sh index 210f7bb33..874e1c7a8 100755 --- a/.agents/scripts/dispatch-dedup-helper.sh +++ b/.agents/scripts/dispatch-dedup-helper.sh @@ -278,6 +278,69 @@ is_duplicate() { return 1 } +####################################### +# Check if a GitHub issue is already assigned to someone else. +# +# This is the primary cross-machine dedup guard. Process-based checks +# (is_duplicate, has_worker_for_repo_issue) only see local processes — +# they miss workers running on other machines. The GitHub assignee is +# the single source of truth visible to all runners. +# +# Args: +# $1 = issue number +# $2 = repo slug (owner/repo) +# $3 = (optional) current runner login — if assigned to self, not a dup +# Returns: +# exit 0 if assigned to someone else (do NOT dispatch) +# exit 1 if unassigned or assigned to self (safe to dispatch) +# Outputs: assignee info on stdout if assigned +####################################### +is_assigned() { + local issue_number="$1" + local repo_slug="$2" + local self_login="${3:-}" + + if [[ -z "$issue_number" || -z "$repo_slug" ]]; then + # Missing args — cannot check, allow dispatch + return 1 + fi + + # Validate issue number is numeric + if [[ ! "$issue_number" =~ ^[0-9]+$ ]]; then + return 1 + fi + + # Query GitHub for current assignees + local assignees + assignees=$(gh issue view "$issue_number" --repo "$repo_slug" \ + --json assignees --jq '[.assignees[].login] | join(",")' 2>/dev/null) || assignees="" + + if [[ -z "$assignees" ]]; then + # No assignees — safe to dispatch + return 1 + fi + + # If assigned to self, not a duplicate + if [[ -n "$self_login" ]]; then + # Check if ALL assignees are self (could be multiple) + local dominated_by_self=true + local IFS=',' + for assignee in $assignees; do + if [[ "$assignee" != "$self_login" ]]; then + dominated_by_self=false + break + fi + done + unset IFS + if [[ "$dominated_by_self" == "true" ]]; then + return 1 + fi + fi + + printf 'ASSIGNED: issue #%s in %s is assigned to %s\n' "$issue_number" "$repo_slug" "$assignees" + return 0 +} + ####################################### # Show help ####################################### @@ -288,6 +351,8 @@ dispatch-dedup-helper.sh - Normalize and deduplicate worker dispatch titles (t23 Usage: dispatch-dedup-helper.sh extract-keys