diff --git a/.agents/scripts/pulse-issue-reconcile-actions.sh b/.agents/scripts/pulse-issue-reconcile-actions.sh index ab567ba65f..62527b6331 100644 --- a/.agents/scripts/pulse-issue-reconcile-actions.sh +++ b/.agents/scripts/pulse-issue-reconcile-actions.sh @@ -800,6 +800,43 @@ _pir_reopen_unstable_parent_close() { return 0 } +####################################### +# Close a parent, verify the post-close snapshot, and compensate by reopening +# when evidence changed. Args: slug parent_num live_body child_nums child_source +# Returns: 0=closed and stable, 1=close failed or compensated +####################################### +_pir_close_parent_with_postcondition() { + local slug="$1" parent_num="$2" live_parent_body="$3" child_nums="$4" child_source="$5" + gh issue close "$parent_num" --repo "$slug" >/dev/null 2>&1 || return 1 + if ! _pir_parent_close_postcondition_is_stable "$slug" "$parent_num" "$live_parent_body" \ + "$child_nums" "$child_source"; then + if [[ "$_PIR_CPT_POST_CLOSE_REOPEN_ALLOWED" -eq 1 ]]; then + _pir_reopen_unstable_parent_close "$slug" "$parent_num" || \ + echo "[pulse-wrapper] Reconcile parent-task: unable to compensate unstable close #${parent_num} in ${slug}" >>"${LOGFILE:-/dev/null}" + fi + return 1 + fi + return 0 +} + +####################################### +# Budget guard for multi-round-trip mutations (GH#34289). Reads the +# single-pass locals _t2984_start_ts/_t2984_budget via dynamic scope. A parent +# close spans several reads, the close, a stability check, an optional reopen +# and a comment; starting one near the budget risks a mid-action stage kill. +# Env: RECONCILE_MUTATION_RESERVE_SECS=reserve seconds (default 90) +# Returns: 0=enough budget (or no budget active), 1=too little remaining +####################################### +_pir_budget_allows_mutation() { + local reserve="${RECONCILE_MUTATION_RESERVE_SECS:-90}" + [[ "$reserve" =~ ^[0-9]+$ ]] || reserve=90 + local budget="${_t2984_budget:-0}" start="${_t2984_start_ts:-}" + [[ "$budget" =~ ^[0-9]+$ && "$start" =~ ^[0-9]+$ ]] || return 0 + [[ "$budget" -gt 0 ]] || return 0 + [[ $((budget - (SECONDS - start))) -ge "$reserve" ]] || return 1 + return 0 +} + ####################################### # t2138 / t3544: extract per-parent close logic. Keeps # reconcile_completed_parent_tasks under the 100-line shell-complexity @@ -813,6 +850,10 @@ _try_close_parent_tracker() { local live_child_nums="" live_child_source="" local live_parent_revision="" + if ! _pir_budget_allows_mutation; then + echo "[pulse-wrapper] Reconcile parent-task: deferred: insufficient reconcile budget for parent close #${parent_num} in ${slug}" >>"${LOGFILE:-/dev/null}" + return 1 + fi _pir_verify_parent_children_closed "$slug" "$child_nums" || return 1 child_count="$_PIR_CPT_VERIFIED_CHILD_COUNT" child_summary="$_PIR_CPT_VERIFIED_CHILD_SUMMARY" @@ -883,15 +924,8 @@ _try_close_parent_tracker() { return 1 fi local close_basis="$_PIR_PARENT_TRUST_BASIS" - gh issue close "$parent_num" --repo "$slug" >/dev/null 2>&1 || return 1 - if ! _pir_parent_close_postcondition_is_stable "$slug" "$parent_num" "$live_parent_body" \ - "$child_nums" "$child_source"; then - if [[ "$_PIR_CPT_POST_CLOSE_REOPEN_ALLOWED" -eq 1 ]]; then - _pir_reopen_unstable_parent_close "$slug" "$parent_num" || \ - echo "[pulse-wrapper] Reconcile parent-task: unable to compensate unstable close #${parent_num} in ${slug}" >>"${LOGFILE:-/dev/null}" - fi - return 1 - fi + _pir_close_parent_with_postcondition "$slug" "$parent_num" "$live_parent_body" \ + "$child_nums" "$child_source" || return 1 gh_issue_comment "$parent_num" --repo "$slug" \ --body "## All declared child tasks completed — closing parent tracker diff --git a/.agents/scripts/tests/test-pulse-issue-reconcile.sh b/.agents/scripts/tests/test-pulse-issue-reconcile.sh index 6e4656a1fc..6506cde279 100644 --- a/.agents/scripts/tests/test-pulse-issue-reconcile.sh +++ b/.agents/scripts/tests/test-pulse-issue-reconcile.sh @@ -1772,6 +1772,46 @@ EOF return 0 } +# --------------------------------------------------------------------------- +# GH#34289: a parent close must not start when the remaining reconcile budget +# is below the mutation reserve (outer stage kill would land mid-action). +# --------------------------------------------------------------------------- +test_gh34289_parent_close_deferred_near_budget() { + local actions_sh="${SCRIPT_DIR}/../pulse-issue-reconcile-actions.sh" + local logfile="" + logfile=$(mktemp) + local result="" + result=$(ACTIONS_SH="$actions_sh" LOGFILE="$logfile" bash -c ' + _PIR_SCRIPT_DIR="/nonexistent" + source "$ACTIONS_SH" + GH_CALLS=0 + gh() { GH_CALLS=$((GH_CALLS + 1)); return 1; } + _t2984_budget=360 + SECONDS=300 + _t2984_start_ts=0 + _try_close_parent_tracker owner/repo 42 43 graph "" && echo "near:ran" || echo "near:deferred" + printf "near-gh-calls:%s\n" "$GH_CALLS" + _t2984_start_ts=$SECONDS + _pir_budget_allows_mutation && echo "fresh:allowed" || echo "fresh:blocked" + _t2984_budget=0 + _pir_budget_allows_mutation && echo "nobudget:allowed" || echo "nobudget:blocked" + ' 2>/dev/null) + + local all_ok=1 + printf '%s\n' "$result" | grep -qx 'near:deferred' || all_ok=0 + printf '%s\n' "$result" | grep -qx 'near-gh-calls:0' || all_ok=0 + printf '%s\n' "$result" | grep -qx 'fresh:allowed' || all_ok=0 + printf '%s\n' "$result" | grep -qx 'nobudget:allowed' || all_ok=0 + grep -q 'deferred: insufficient reconcile budget for parent close #42' "$logfile" 2>/dev/null || all_ok=0 + rm -f "$logfile" + if [[ "$all_ok" -eq 1 ]]; then + _pass "parent close deferred (no gh calls, logged) when reconcile budget is nearly exhausted" + else + _fail "budget-deferral result mismatch: ${result}" + fi + return 0 +} + # --------------------------------------------------------------------------- # Run all tests # --------------------------------------------------------------------------- @@ -1807,6 +1847,7 @@ test_gh32640_ciw_rsd_recurrent_file_size_debt_gate test_available_feedback_worker_issue_not_assigned test_feedback_backfill_uses_label_constants test_pr_lookup_uncertainty_preserves_issue_state +test_gh34289_parent_close_deferred_near_budget echo "" echo "Results: ${pass} passed, ${fail} failed"