Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 43 additions & 9 deletions .agents/scripts/pulse-issue-reconcile-actions.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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"
Expand Down Expand Up @@ -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

Expand Down
41 changes: 41 additions & 0 deletions .agents/scripts/tests/test-pulse-issue-reconcile.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
# ---------------------------------------------------------------------------
Expand Down Expand Up @@ -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"
Expand Down
Loading