fix(autofix): retry an agent timeout instead of advancing past its feedback (#7563)

A timeout evaluated NOTHING — the agent ran out of budget before finishing,
so nothing was committed and the feedback is unaddressed. It was treated as
an evaluated verdict (real ts, watermark advances), which strands that
feedback: the next scan sees "nothing new" and never retries. Observed on
#7471 (round 13/100), a heavily-reviewed 1871-line PR: rounds 11 and 13
timed out, but round 12 pushed — so a timeout is transient far more often
than not, and advancing past it left the round-13 feedback unhandled.

run-agent.mjs now drops an `agent-timeout` signal on result.timedOut, and
the handoff routes it like a pre-verdict crash: sentinel ts (feedback stays
live) and a retry, with a headline that names the real fix at the cap
(split the PR or raise the budget). A PR that PERSISTENTLY times out is
bounded by the round cap and the consecutive-failure cap, so this cannot
loop forever — it just stops treating a one-off budget blip as a verdict.

The loop guard stays terminal (a tool-call loop is a real defect, not a
budget blip). An API error still routes to its own model-key handoff; the
timeout signal is written only when NOT an API error.

Co-authored-by: wenshao <wenshao@example.com>
This commit is contained in:
Shaojin Wen 2026-07-23 13:13:35 +08:00 committed by GitHub
parent 135723a1f7
commit 5df71c8fd1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 75 additions and 13 deletions

View file

@ -2962,7 +2962,7 @@ jobs:
if git rev-parse --verify "${BRANCH}" > /dev/null 2>&1; then
git diff "origin/main...${BRANCH}" > "${WORKDIR}/pr.diff" || true
fi
for f in feedback.md address-summary.md no-action.md failure.md handoff.md gate-rejection.md agent-api-error agent-api-error-kind resolved-comments.txt pr.diff; do
for f in feedback.md address-summary.md no-action.md failure.md handoff.md gate-rejection.md agent-api-error agent-api-error-kind agent-timeout resolved-comments.txt pr.diff; do
if [[ -f "${WORKDIR}/${f}" ]]; then
echo "=============== ${f} ==============="
cat "${WORKDIR}/${f}"
@ -3264,6 +3264,16 @@ jobs:
if [[ -s "${WORKDIR}/agent-api-error-kind" ]]; then
API_ERROR_KIND="$(head -n 1 "${WORKDIR}/agent-api-error-kind" | tr -cd 'a-z')"
fi
# A timeout means the agent evaluated NOTHING before its budget ran
# out (run-agent.mjs writes this signal). Routed to retry, not an
# evaluated advance — same as a pre-verdict crash. Bounded by the
# round cap and the consecutive-failure cap, so a PR that keeps
# timing out still stops; a one-off (usually followed by a good
# round) recovers on the next scan instead of stranding its feedback.
AGENT_TIMEOUT=''
if [[ -s "${WORKDIR}/agent-timeout" ]]; then
AGENT_TIMEOUT="$(head -n 1 "${WORKDIR}/agent-timeout" | cut -c1-120 | iconv -f utf-8 -t utf-8 -c || true)"
fi
# If feedback was actually read (prepare ran), stamp its newest ts so
# the watermark advances and the same feedback is not re-selected next
# scan. If the crash happened before prepare, NEWEST is empty and the
@ -3291,13 +3301,14 @@ jobs:
MARK_TS="${NEWEST:-${WATERMARK:-9999-12-31T23:59:59Z}}"
if [[ -n "${NEWEST:-}" ]]; then
MARK_ROUND="$(( ROUND + 1 ))"
if [[ -z "${DETAIL_FILE}" || -n "${API_ERROR_DETAIL}" || "${GATE_CRASHED}" == 'true' ]]; then
# Prepare ran (NEWEST is set) but no verdict was reached. Three
# ways that happens, and in ALL of them the agent evaluated
# NOTHING: it produced no output at all (crashed before any
# verdict — a staged runner that fails to boot), it died on a
# model [API Error] (access/quota/5xx/transport), or the gate
# crashed after the agent wrote its summary. So the watermark
if [[ -z "${DETAIL_FILE}" || -n "${API_ERROR_DETAIL}" || -n "${AGENT_TIMEOUT}" || "${GATE_CRASHED}" == 'true' ]]; then
# Prepare ran (NEWEST is set) but no verdict was reached. Ways
# that happens, and in ALL of them the agent evaluated NOTHING:
# it produced no output at all (crashed before any verdict — a
# staged runner that fails to boot), it died on a model
# [API Error] (access/quota/5xx/transport), it TIMED OUT before
# finishing, or the gate crashed after the agent wrote its
# summary. So the watermark
# must NOT advance past this feedback: an advance makes the next
# scan see "nothing new" and never retry, stranding the PR on a
# transient failure (an infra blip, a quota reset minutes away, a
@ -3319,6 +3330,13 @@ jobs:
if [[ -n "${API_ERROR_DETAIL}" ]]; then
CAUSE="could not reach the model — ${API_ERROR_DETAIL}"
LAST_FIX="a maintainer should check the autofix model key/access, then re-arm"
elif [[ -n "${AGENT_TIMEOUT}" ]]; then
# A timeout evaluated nothing, so the feedback is unaddressed
# and stays live for the retry. On a big / heavily-reviewed PR
# this is usually a one-off; the last automatic attempt names
# the real fix (split the PR or raise the budget).
CAUSE="ran out of time before finishing (${AGENT_TIMEOUT})"
LAST_FIX="a human should split the PR or raise the agent time budget, then re-arm"
elif [[ -z "${DETAIL_FILE}" ]]; then
CAUSE="crashed before it could evaluate the feedback"
LAST_FIX="a human should take over this PR"