Skip to content

Commit fe8ec1b

Browse files
fix(impl-generate): keep infrastructure failures out of the 3-attempt cap
The cap exists to stop re-running a pair the model cannot solve. It counted every failed run alike, so the Claude outage of 2026-09-02 (03:15-03:45 UTC, every run ending in `is_error:true` with an "Internal error") burned all three attempts of 27 pairs in twenty minutes, and for the rest of the 12-hour window each re-dispatch ran without any auto-retry. The failure handler now classifies the run from step outcomes: both Claude runs dying with a provider-side signature in the execution log, a Google Cloud auth/SDK/upload failure, or a GitHub API failure at PR creation or review dispatch is an infrastructure failure. Those markers carry an extra `<!-- impl-fail-cause:infra -->` tag, are excluded from the genuine count, and are retried on a separate cap of 5 per window, after which the pair is paused without `impl:<lib>:failed`. "Implementation file not found" and a missing theme render keep counting as before. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SrKzcwZBnref1sWYtdXynu
1 parent dea18f8 commit fe8ec1b

4 files changed

Lines changed: 139 additions & 24 deletions

File tree

.github/workflows/impl-generate.yml

Lines changed: 125 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -874,13 +874,15 @@ jobs:
874874
# Upload to GCS Staging
875875
# ========================================================================
876876
- name: Authenticate to GCP
877+
id: gcp_auth
877878
if: steps.pr.outputs.pr_exists == 'true'
878879
uses: google-github-actions/auth@7c6bc770dae815cd3e89ee6cdf493a5fab2cc093 # v3
879880
with:
880881
project_id: anyplot
881882
workload_identity_provider: ${{ secrets.GCP_WORKLOAD_IDENTITY_PROVIDER }}
882883

883884
- name: Set up Cloud SDK
885+
id: gcloud
884886
if: steps.pr.outputs.pr_exists == 'true'
885887
uses: google-github-actions/setup-gcloud@aa5489c8933f4cc7a4f7d45035b3b1440c9c10db # v3
886888

@@ -980,6 +982,7 @@ jobs:
980982
gh issue comment "$ISSUE" --body "$BODY"
981983
982984
- name: Trigger review workflow
985+
id: review_dispatch
983986
if: steps.pr.outputs.pr_exists == 'true'
984987
env:
985988
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
@@ -1021,9 +1024,54 @@ jobs:
10211024
# well under an hour) with room for a stalled tail, without letting
10221025
# last month's failures veto today's retries.
10231026
CAMPAIGN_WINDOW_H: '12'
1027+
# Step outcomes, to tell an infrastructure failure (provider incident,
1028+
# cloud auth, GitHub API) from the agent's own — only the latter counts
1029+
# toward the 3-attempt cap. `outcome` is the result before
1030+
# continue-on-error, so the first Claude step reports its real result.
1031+
CLAUDE_RETRY_OUTCOME: ${{ steps.claude_retry.outcome }}
1032+
CLAUDE_EXEC_FILE: ${{ steps.claude_retry.outputs.execution_file }}
1033+
GCP_AUTH_OUTCOME: ${{ steps.gcp_auth.outcome }}
1034+
GCLOUD_OUTCOME: ${{ steps.gcloud.outcome }}
1035+
GCS_OUTCOME: ${{ steps.gcs.outcome }}
1036+
PR_OUTCOME: ${{ steps.pr.outcome }}
1037+
REVIEW_DISPATCH_OUTCOME: ${{ steps.review_dispatch.outcome }}
1038+
# Infrastructure failures are retried without spending the pair's
1039+
# budget, but not forever: after this many in the window the pair is
1040+
# parked WITHOUT `impl:<lib>:failed` (it is not a capability verdict)
1041+
# and left for the next dispatch.
1042+
INFRA_WINDOW_CAP: '5'
10241043
run: |
10251044
echo "::notice::Handling generation failure for $LIBRARY/$SPEC_ID"
10261045
1046+
# Classify the failure. The retry cap exists to stop re-running a pair
1047+
# the model cannot solve; a provider incident, a cloud token refresh
1048+
# or a GitHub 5xx says nothing about the pair. During the Claude
1049+
# outage of 2026-09-02 (03:15–03:45 UTC) every run failed with
1050+
# `is_error:true` + "Internal error", 27 pairs burned all three
1051+
# attempts in twenty minutes, and for the next twelve hours each
1052+
# re-dispatch ran without any auto-retry because the markers counted.
1053+
#
1054+
# Infrastructure = both Claude runs ended in the action's own error
1055+
# path with a provider-side signature (the execution log is checked,
1056+
# so an agent that gave up — max turns, refused — still counts), or
1057+
# the GCP auth / Cloud SDK / GCS upload / PR creation / review
1058+
# dispatch step failed. "Implementation file not found" and a missing
1059+
# theme render are the agent's own and keep counting.
1060+
INFRA_CAUSE=""
1061+
if [ "${CLAUDE_RETRY_OUTCOME}" = "failure" ]; then
1062+
if [ -n "${CLAUDE_EXEC_FILE}" ] && [ -f "${CLAUDE_EXEC_FILE}" ] \
1063+
&& grep -qiE 'Internal error|overloaded|rate.?limit|too many requests|ECONNRESET|ETIMEDOUT|"status": *5[0-9][0-9]' "${CLAUDE_EXEC_FILE}"; then
1064+
INFRA_CAUSE="Claude Code action error: $(grep -oiE 'Internal error[^"]{0,60}|overloaded[^"]{0,40}|rate.?limit[^"]{0,40}|too many requests|ECONNRESET|ETIMEDOUT' "${CLAUDE_EXEC_FILE}" | head -1)"
1065+
fi
1066+
elif [ "${GCP_AUTH_OUTCOME}" = "failure" ] || [ "${GCLOUD_OUTCOME}" = "failure" ] || [ "${GCS_OUTCOME}" = "failure" ]; then
1067+
INFRA_CAUSE="Google Cloud auth/upload step failed"
1068+
elif [ "${PR_OUTCOME}" = "failure" ] || [ "${REVIEW_DISPATCH_OUTCOME}" = "failure" ]; then
1069+
INFRA_CAUSE="GitHub API step failed (PR creation or review dispatch)"
1070+
fi
1071+
if [ -n "${INFRA_CAUSE}" ]; then
1072+
echo "::notice::Classified as infrastructure failure: ${INFRA_CAUSE}"
1073+
fi
1074+
10271075
# Count previous failures via hidden marker comments (more reliable than workflow runs).
10281076
# Paginate so the marker is found even on issues with >30 comments
10291077
# (which is common because all 15 library impls land on the same issue).
@@ -1053,15 +1101,87 @@ jobs:
10531101
# on the very next manual dispatch, and 87 pairs parked repo-wide.
10541102
# A generation campaign is minutes long, so a window of hours is
10551103
# generous while still letting stale history age out on its own.
1104+
#
1105+
# INFRASTRUCTURE-AWARE: a marker that also carries INFRA_TAG records a
1106+
# failure that was not the agent's (see the classification above).
1107+
# Those are counted separately: they never spend the 3-attempt budget
1108+
# and are bounded by INFRA_WINDOW_CAP instead.
10561109
MARKER="<!-- impl-fail:${SPEC_ID}:${LIBRARY} -->"
1110+
INFRA_TAG="<!-- impl-fail-cause:infra -->"
10571111
CAMPAIGN_CUTOFF=$(date -u -d "${CAMPAIGN_WINDOW_H} hours ago" +%Y-%m-%dT%H:%M:%SZ)
1058-
if FAILURE_COUNT=$(gh api --paginate "repos/${{ github.repository }}/issues/${ISSUE}/comments?per_page=100" \
1059-
--jq "[.[] | select(.body != null and (.body | contains(\"$MARKER\")) and .created_at > \"$CAMPAIGN_CUTOFF\")] | length" \
1060-
| awk '{ sum += $1 } END { print sum + 0 }'); then
1061-
echo "::notice::Previous failures for ${LIBRARY}/${SPEC_ID} since ${CAMPAIGN_CUTOFF}: $FAILURE_COUNT"
1112+
# One paginated call, two numbers per page ("<genuine> <infra>"), summed by awk.
1113+
if COUNTS=$(gh api --paginate "repos/${{ github.repository }}/issues/${ISSUE}/comments?per_page=100" \
1114+
--jq "[.[] | select(.body != null and (.body | contains(\"$MARKER\")) and .created_at > \"$CAMPAIGN_CUTOFF\")] | \"\(map(select(.body | contains(\"$INFRA_TAG\") | not)) | length) \(map(select(.body | contains(\"$INFRA_TAG\"))) | length)\"" \
1115+
| awk '{ g += $1; i += $2 } END { print (g + 0) " " (i + 0) }'); then
1116+
FAILURE_COUNT=${COUNTS% *}
1117+
INFRA_COUNT=${COUNTS#* }
1118+
echo "::notice::Previous failures for ${LIBRARY}/${SPEC_ID} since ${CAMPAIGN_CUTOFF}: $FAILURE_COUNT (plus $INFRA_COUNT infrastructure failures, not counted)"
10621119
else
10631120
echo "::warning::Failure-count API call failed — failing closed (treating retry cap as reached, no auto-retry)"
10641121
FAILURE_COUNT=999
1122+
INFRA_COUNT=999
1123+
fi
1124+
1125+
# Retry dispatch, shared by both branches below. Forwards
1126+
# `change_request` so cross-library divergence hints from daily-regen
1127+
# pre-flight survive the retry — otherwise the first attempt has the
1128+
# hint but the retry doesn't, defeating the audit. CHANGE_REQUEST is
1129+
# read from env to keep raw quotes/$/backticks inside the hint from
1130+
# breaking shell parsing.
1131+
dispatch_retry() {
1132+
gh workflow run impl-generate.yml \
1133+
-f specification_id="${SPEC_ID}" \
1134+
-f library="${LIBRARY}" \
1135+
-f issue_number="${ISSUE}" \
1136+
-f model="${MODEL}" \
1137+
-f change_request="${CHANGE_REQUEST}"
1138+
if [ -n "${CHANGE_REQUEST}" ]; then
1139+
echo "::notice::Triggered automatic retry for ${LIBRARY}/${SPEC_ID} ($1, model=${MODEL}, change_request=present)"
1140+
else
1141+
echo "::notice::Triggered automatic retry for ${LIBRARY}/${SPEC_ID} ($1, model=${MODEL}, change_request=none)"
1142+
fi
1143+
}
1144+
1145+
if [ -n "${INFRA_CAUSE}" ]; then
1146+
INFRA_ATTEMPT=$((INFRA_COUNT + 1))
1147+
if [ "${INFRA_COUNT}" -ge $((INFRA_WINDOW_CAP - 1)) ]; then
1148+
# Parked, not failed: nothing here says the pair is impossible.
1149+
# No `impl:<lib>:failed`, so the watchdog does not treat it as a
1150+
# capability verdict either; the next backfill dispatch picks it
1151+
# up with a clean budget once the markers age out.
1152+
echo "::warning::Parking $LIBRARY/$SPEC_ID after ${INFRA_ATTEMPT} infrastructure failures in the last ${CAMPAIGN_WINDOW_H}h (cap: ${INFRA_WINDOW_CAP}) — not marked failed, no auto-retry"
1153+
gh issue edit "$ISSUE" \
1154+
--remove-label "generate:${LIBRARY},impl:${LIBRARY}:pending" 2>/dev/null || true
1155+
gh issue comment "$ISSUE" --body "${MARKER}
1156+
${INFRA_TAG}
1157+
## :pause_button: ${LIBRARY} Paused (infrastructure failures)
1158+
1159+
The **${LIBRARY}** implementation for \`${SPEC_ID}\` hit ${INFRA_ATTEMPT} infrastructure failures in the last ${CAMPAIGN_WINDOW_H}h (cap: ${INFRA_WINDOW_CAP}) and is paused. This is **not** a capability verdict — the pair keeps its 3-attempt budget.
1160+
1161+
**Last cause:** ${INFRA_CAUSE}
1162+
1163+
To retry once the incident is over:
1164+
\`\`\`
1165+
gh workflow run impl-generate.yml -f specification_id=${SPEC_ID} -f library=${LIBRARY} -f issue_number=${ISSUE} -f model=${MODEL}
1166+
\`\`\`
1167+
1168+
---
1169+
:robot: *[impl-generate](https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }})*"
1170+
else
1171+
gh issue comment "$ISSUE" --body "${MARKER}
1172+
${INFRA_TAG}
1173+
## :cloud: ${LIBRARY} Infrastructure Failure (${INFRA_ATTEMPT}/${INFRA_WINDOW_CAP} in window)
1174+
1175+
**Cause:** ${INFRA_CAUSE}
1176+
1177+
Not counted toward the 3-attempt cap. Automatically retrying...
1178+
1179+
---
1180+
:robot: *[impl-generate](https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }})*"
1181+
gh issue edit "$ISSUE" --remove-label "generate:${LIBRARY}" 2>/dev/null || true
1182+
dispatch_retry "infrastructure retry ${INFRA_ATTEMPT}/${INFRA_WINDOW_CAP}"
1183+
fi
1184+
exit 0
10651185
fi
10661186
10671187
# FAILURE_COUNT counts marker comments BEFORE this run.
@@ -1123,22 +1243,5 @@ jobs:
11231243
# Clean up generate label before retry
11241244
gh issue edit "$ISSUE" --remove-label "generate:${LIBRARY}" 2>/dev/null || true
11251245
1126-
# Automatic retry via workflow_dispatch.
1127-
# Forward `change_request` so cross-library divergence hints from
1128-
# daily-regen pre-flight survive the retry — otherwise the first
1129-
# attempt has the hint but the retry doesn't, defeating the audit.
1130-
# CHANGE_REQUEST is read from env to keep raw quotes/$/backticks
1131-
# inside the hint from breaking shell parsing.
1132-
gh workflow run impl-generate.yml \
1133-
-f specification_id="${SPEC_ID}" \
1134-
-f library="${LIBRARY}" \
1135-
-f issue_number="${ISSUE}" \
1136-
-f model="${MODEL}" \
1137-
-f change_request="${CHANGE_REQUEST}"
1138-
1139-
if [ -n "${CHANGE_REQUEST}" ]; then
1140-
echo "::notice::Triggered automatic retry for ${LIBRARY}/${SPEC_ID} (attempt $((ATTEMPT + 1)), model=${MODEL}, change_request=present)"
1141-
else
1142-
echo "::notice::Triggered automatic retry for ${LIBRARY}/${SPEC_ID} (attempt $((ATTEMPT + 1)), model=${MODEL}, change_request=none)"
1143-
fi
1246+
dispatch_retry "attempt $((ATTEMPT + 1))"
11441247
fi

CHANGELOG.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,18 @@ aggregate instead: an italic *Catalog* line at the end of the version section an
2828

2929
### Fixed
3030

31+
- **Infrastructure failures no longer spend a pair's generation budget** — the
32+
3-attempt cap in `impl-generate.yml` counted every failed run alike, so the Claude
33+
outage of 2026-09-02 (03:15–03:45 UTC, every run ending in `is_error:true` with an
34+
"Internal error") burned all three attempts of 27 pairs in twenty minutes, and for the
35+
rest of the 12-hour window each re-dispatch ran without any auto-retry. The failure
36+
handler now classifies the run from the step outcomes: both Claude runs dying with a
37+
provider-side signature in the execution log, a Google Cloud auth/SDK/upload failure,
38+
or a GitHub API failure at PR creation or review dispatch is an infrastructure failure.
39+
Those are recorded with an extra `<!-- impl-fail-cause:infra -->` tag, excluded from the
40+
genuine count, and retried on a separate cap of 5 per window, after which the pair is
41+
paused without `impl:<lib>:failed` (an incident is not a capability verdict). "Agent
42+
reports success but writes no file" and a missing theme render still count as before.
3143
- **The API image installs `libraqm0`, which is what actually restores text shaping —
3244
and unblocks a deploy pipeline that has been red since 2026-08-30**#10813 added a
3345
build-time assertion on `features.check('raqm')` on the understanding that the locked

agentic/docs/project-guide.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -789,7 +789,7 @@ uv run python -m automation.scripts.label_manager list
789789
- **`generate:all`** - Trigger all 15 libraries via bulk-generate
790790
- **`impl:{library}:pending`** - Generation in progress
791791
- **`impl:{library}:done`** - Implementation merged to main
792-
- **`impl:{library}:failed`** - Max retries exhausted (4 repair attempts)
792+
- **`impl:{library}:failed`** - Three failed generation attempts for the pair within a 12-hour window; infrastructure failures (provider incident, cloud auth, GitHub API) are retried on their own cap of 5 and never set it
793793

794794
### PR Labels (set by workflows)
795795

docs/workflows/overview.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,7 @@ impl-review.yml
9999
| `generate:{library}` | Trigger generation for library | User |
100100
| `impl:{library}:pending` | Generation in progress | Workflow |
101101
| `impl:{library}:done` | Implementation merged to main | Workflow |
102-
| `impl:{library}:failed` | Max retries exhausted | Workflow |
102+
| `impl:{library}:failed` | Three failed generation attempts for the pair within a 12-hour window. Infrastructure failures (provider incident, cloud auth, GitHub API) are retried on a separate cap of 5 and never set this label; a pair paused that way carries no label. | Workflow |
103103

104104
### PR labels (on Pull Requests)
105105

0 commit comments

Comments
 (0)