Skip to content
Closed
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
2 changes: 1 addition & 1 deletion .github/workflows/.size-baseline
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@
6495 pr-self-report-label.yml
9646 qwen-autofix-fork-bridge.yml
5942 qwen-autofix-fork-signal.yml
397656 qwen-autofix.yml
399717 qwen-autofix.yml
7061 qwen-ci-flaky-rerun.yml
151937 qwen-code-pr-review.yml
79041 qwen-fleet-shepherd.yml
Expand Down
67 changes: 67 additions & 0 deletions .github/workflows/qwen-autofix.md
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,7 @@ task-oriented guides — what a maintainer types and what happens next — see:
- [145. review-address · Report dry-run / failure — CUMULATIVE timeout breaker — the sibling of the consecutive one above, for the…](#af-145)
- [146. review-address · Report dry-run / failure — The agent committed (verify recorded committed=true before any gate could fail),…](#af-146)
- [147. review-address · Report dry-run / failure — Same byte-budget hygiene as the English excerpt above. 3000 bytes ≈ 1000 CJK…](#af-147)
- [148. review-address · Repair deterministic rejection — Budget the repair pass so it can finish, and keep its exhaustions out of…](#af-148)

---

Expand Down Expand Up @@ -3717,3 +3718,69 @@ forbids HTML in failure.zh.md), but must not be able to open
or close a <details>/<summary> that swallows the closing tag
the workflow emits below.
```

<a id="af-148"></a>

### 148. review-address · Repair deterministic rejection — Budget the repair pass so it can finish, and keep its exhaustions out of the timeout cap.

In `review-address` · `Repair deterministic rejection` and `Report dry-run / failure`.

```text
Two defects, one cause. The repair pass ran on an 18-minute budget
under a 20-minute step, and every exhaustion it produced was counted
into TIMEOUT_WINDOW_CAP as though the ROUND had run out of time.

The budget could not work. This pass has to re-run the same
verification the gate just finished before its own verdict counts, and
that verification measured 6.6-10.6 minutes across the sampled runs,
leaving the model roughly 8 minutes to read the rejection, fix it, and
land a commit — on a repository where the primary pass averages 41
minutes of its 120. Measured 2026-08-22 across 5 PRs (#9394, #9576,
#9340, #9602 and, unselected, #9474): 20 repair passes ran, 20 hit the
wall at exactly 18.0 minutes, none succeeded. That sample is drawn
from runs already known to contain a repair timeout, so read the rate
as indicative rather than fleet-wide — but the arithmetic above does
not depend on the sample. Raised to 40 minutes under a 45-minute
backstop. That does not fit the old 300-minute job ceiling — the four
long steps plus the 25-minute setup/report reserve now sum to 320 — so
the job cap moved to 325, the largest value that stays under
review-scan's PENDING_STALE_MIN of 330 while keeping the same
5-minute slack the 295/300 pair had. Nothing else was squeezed to make
room: the two verification gates keep their 60-minute caps even though
they measured 6.6-10.6, because that headroom is their safety margin,
not spare change. The real cost is bounded occupancy, not real time:
the worst round actually observed spent 88 minutes end to end.

Counting those exhaustions was the af-073 mistake in a second form.
The cap's remedy is "split or reduce the PR (or raise the agent time
budget AND its step backstop)", and none of it applies here: the
primary pass FINISHED and produced the commit the gate rejected, so
the round was not too big for its budget, and the repair sub-budget is
pinned independently of the round budget the remedy names. Over the 14
days to 2026-08-21 this class was 30 of the 119 timeouts (25%) and 30
of the 61 non-idle ones (49%) — so after af-073 removed idle, it was
what kept parking PRs: #9394, #9576 and #9340 were all stopped on it
within hours of af-073 landing.

The escape hatch is unchanged and is what makes the exclusion safe: a
repair timeout pushes nothing and matches none of CONSEC_FAIL's
streak-reset needles, so a PR that keeps failing this way still
terminates at CONSECUTIVE_FAILURE_CAP. What no longer terminates is a
PR that is making progress and occasionally loses a round to a
sub-budget that is not the one the operator would raise.

Telling the two apart needed a name. run-agent.mjs's timeout detail
opened with a bare "timeout (Nms)" for every caller, so the census had
only the millisecond count to go on — and pinning a census to a
literal budget value breaks the moment anyone retunes it. It now emits
QWEN_TIMEOUT_LABEL as that opening token (validated lowercase-dashed,
defaulting to "timeout"), the repair step sets it to "repair-timeout",
and the census greps that. A test pins the step's literal against the
census needle: a typo in either silently returns repair timeouts to
the cap, which is exactly the bug this entry removes.

The same exclusion applies to the prompt-narrowing census (af-049),
whose warning tells the agent it "ran out of time before finishing
anything" — false for a repair timeout, where the primary pass
finished.
```
49 changes: 37 additions & 12 deletions .github/workflows/qwen-autofix.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3445,7 +3445,7 @@ jobs:
# write+ (internal) authors at scan AND address time. That is an
# Full rationale → qwen-autofix.md#af-038
runs-on: '${{ (github.repository == ''QwenLM/qwen-code'' && vars.MAINTAINER_ECS_RUNNER_DISABLED != ''true'' && (github.event_name != ''pull_request'' && github.event_name != ''pull_request_review'' || github.event.pull_request.head.repo.full_name == github.repository || contains(fromJSON(''["OWNER","MEMBER","COLLABORATOR"]''), github.event.pull_request.author_association))) && fromJSON(''["self-hosted", "linux", "x64", "ecs-qwen"]'') || fromJSON(''["ubuntu-latest"]'') }}'
timeout-minutes: 300
timeout-minutes: 325
permissions:
contents: 'read'
strategy:
Expand Down Expand Up @@ -4682,6 +4682,8 @@ jobs:
# Idle (silent-sandbox) timeouts are excluded like in the cap
# census: the narrowing advice targets budget exhaustion, and an
# infra-killed round never had any budget to exhaust (af-073).
# Repair-pass exhaustions are excluded on the same reading: this
# warning's "before finishing anything" is false there (af-148).
PRIOR_TIMEOUTS="$(jq -r --arg ab "${AUTOFIX_BOT}" --arg key "${LIVE_REARM_KEY}" '
[ .[] | select((.user.login // "") == $ab)
| select((.body // "") | contains("<!-- autofix-eval "))
Expand All @@ -4690,7 +4692,7 @@ jobs:
] | sort_by(.created_at)
| map((.body | gsub("\r"; "") | split("\n")[0]))
| (map(test("Addressed the latest review feedback|no changes needed")) | rindex(true) // -1) as $lastok
| [ .[($lastok + 1):][] | select(contains("AutoFix ran out of time before finishing") and (contains("AutoFix ran out of time before finishing (idle-timeout") | not)) ] | length' "${WORKDIR}/ic.json" 2> /dev/null || true)"
| [ .[($lastok + 1):][] | select(contains("AutoFix ran out of time before finishing") and (contains("AutoFix ran out of time before finishing (idle-timeout") | not) and (contains("AutoFix ran out of time before finishing (repair-timeout") | not)) ] | length' "${WORKDIR}/ic.json" 2> /dev/null || true)"
if [[ "${PRIOR_TIMEOUTS}" -ge 1 ]]; then
echo
echo '## Budget warning: previous round(s) ran out of time'
Expand Down Expand Up @@ -4944,9 +4946,11 @@ jobs:
# The outcome check fails closed: a FAILED resolver leaves the image
# binding below empty, and an empty QWEN_SANDBOX_IMAGE relaunches
# the CLI without any sandbox (#9527 review).
# Budget 40m under a 45m backstop; the old 18m/20m pair could never
# finish. Full rationale → qwen-autofix.md#af-148
if: |-
${{ always() && steps.verify.outputs.retryable == 'true' && steps.sandbox_image.outcome == 'success' }}
timeout-minutes: 20
timeout-minutes: 45
env:
QWEN_SANDBOX_IMAGE: '${{ steps.sandbox_image.outputs.image }}'
DOCKER_HOST: ''
Expand All @@ -4958,7 +4962,10 @@ jobs:
OPENAI_MODEL: '${{ vars.QWEN_AUTOFIX_MODEL || vars.QWEN_PR_REVIEW_MODEL }}'
NO_PROXY: '127.0.0.1,localhost,::1'
QWEN_HOME: '${{ runner.temp }}/qwen-autofix-review-home'
QWEN_TIMEOUT_MS: '1080000'
QWEN_TIMEOUT_MS: '2400000'
# Names the budget in run-agent.mjs's timeout detail so the census
# can tell this pass apart; the cap's needle pins this literal.
QWEN_TIMEOUT_LABEL: 'repair-timeout'
CONFLICT: '${{ steps.prepare.outputs.conflict }}'
BASE: 'main'
SETTINGS_JSON: |-
Expand Down Expand Up @@ -5778,12 +5785,24 @@ jobs:
if [[ "${AGENT_TIMEOUT:-}" == 'idle-timeout'* ]]; then
IDLE_N=$(( IDLE_N + 1 ))
fi
# Excluding idle from the cap must not hide it. The job log is
# Repair-pass exhaustions are EXCLUDED for the same reason: the
# primary pass FINISHED and only the smaller repair sub-budget
# ran out, so splitting the PR is not the fix. Still bounded by
# CONSECUTIVE_FAILURE_CAP.
# Full rationale → qwen-autofix.md#af-148
REPAIR_N="$(grep -c 'AutoFix ran out of time before finishing (repair-timeout' <<< "${PRIOR_HEADS}" || true)"
if [[ "${AGENT_TIMEOUT:-}" == 'repair-timeout'* ]]; then
REPAIR_N=$(( REPAIR_N + 1 ))
fi
# Excluding a class from the cap must not hide it. The job log is
# the right surface: it reaches the operator without spending a
# PR comment on infra noise.
if [[ "${IDLE_N}" -gt 0 ]]; then
echo "::warning::#${PR}: ${IDLE_N} silent-sandbox (idle) timeout(s) this counting window — excluded from the ${TIMEOUT_WINDOW_CAP}-timeout cap; check the sandbox image and the runner docker daemon"
fi
if [[ "${REPAIR_N}" -gt 0 ]]; then
echo "::warning::#${PR}: ${REPAIR_N} repair-pass timeout(s) this counting window — excluded from the ${TIMEOUT_WINDOW_CAP}-timeout cap; the repair sub-budget, not the round budget, is what ran out"
fi
if [[ "${MARK_ROUND}" != "${MAX_ROUNDS}" ]]; then
# Needle matches the emitted headline verbatim — first lines
# can embed provider error text (API_ERROR_DETAIL puts up to
Expand All @@ -5793,11 +5812,11 @@ jobs:
if [[ -n "${AGENT_TIMEOUT:-}" ]]; then
TIMEOUT_N=$(( TIMEOUT_N + 1 ))
fi
# IDLE_N's needle is TIMEOUT_N's needle extended with the
# idle cause's opening token — every line matching it also
# matches TIMEOUT_N's, so IDLE_N can never exceed TIMEOUT_N
# and the subtraction below can never go negative.
BUDGET_TIMEOUT_N=$(( TIMEOUT_N - IDLE_N ))
# Both excluded needles are TIMEOUT_N's needle extended with
# their cause's opening token — every line matching either
# also matches TIMEOUT_N's, and the two causes are mutually
# exclusive, so the subtraction below can never go negative.
BUDGET_TIMEOUT_N=$(( TIMEOUT_N - IDLE_N - REPAIR_N ))
if [[ "${BUDGET_TIMEOUT_N}" -ge "${TIMEOUT_WINDOW_CAP}" ]]; then
MARK_ROUND="${MAX_ROUNDS}"
# The headline states what the census MEASURED — the
Expand All @@ -5817,8 +5836,14 @@ jobs:
IDLE_CLAUSE=" The window also holds ${IDLE_N} silent-sandbox (idle) timeout(s), which no budget increase can cure and which do NOT count toward this cap — investigate the sandbox image and runner docker daemon separately."
IDLE_CLAUSE_ZH="本窗口另有 ${IDLE_N} 次静默 sandbox(idle)超时,提高预算也治不了,且不计入本上限 —— 请另行排查 sandbox 镜像与 runner 的 docker daemon。"
fi
HEADLINE="🤖 AutoFix stopped: this counting window now contains ${BUDGET_TIMEOUT_N} agent time-budget exhaustions (pushed rounds in between included; this round itself may have failed differently). That is ${BUDGET_TIMEOUT_N} full agent runs that pushed nothing.${IDLE_CLAUSE} A human should split or reduce the PR (or raise the agent time budget AND its step backstop together), then comment \`${RETRY_COMMAND}\` to re-arm. Until then future scans will skip this PR."
HEADLINE_ZH="🤖 AutoFix 已停止:当前计数窗口内已累计 ${BUDGET_TIMEOUT_N} 次时间预算耗尽(含其间推送过的轮次;本轮本身可能以别的方式失败)。即 ${BUDGET_TIMEOUT_N} 次完整 agent 运行没有推送任何内容。${IDLE_CLAUSE_ZH}应由人工拆分或缩减该 PR(或同时提高 agent 时间预算与其步骤兜底),然后评论 \`${RETRY_COMMAND}\` 重新武装。在此之前,后续扫描将跳过本 PR。"
REPAIR_CLAUSE=''
REPAIR_CLAUSE_ZH=''
if [[ "${REPAIR_N}" -gt 0 ]]; then
REPAIR_CLAUSE=" It also holds ${REPAIR_N} repair-pass timeout(s), which do NOT count toward this cap either — there the primary pass finished and only the smaller repair sub-budget ran out."
REPAIR_CLAUSE_ZH="另有 ${REPAIR_N} 次 repair pass 超时,同样不计入本上限 —— 那些轮次的主 pass 已经跑完,耗尽的只是更小的 repair 子预算。"
fi
HEADLINE="🤖 AutoFix stopped: this counting window now contains ${BUDGET_TIMEOUT_N} agent time-budget exhaustions (pushed rounds in between included; this round itself may have failed differently). That is ${BUDGET_TIMEOUT_N} full agent runs that pushed nothing.${IDLE_CLAUSE}${REPAIR_CLAUSE} A human should split or reduce the PR (or raise the agent time budget AND its step backstop together), then comment \`${RETRY_COMMAND}\` to re-arm. Until then future scans will skip this PR."
HEADLINE_ZH="🤖 AutoFix 已停止:当前计数窗口内已累计 ${BUDGET_TIMEOUT_N} 次时间预算耗尽(含其间推送过的轮次;本轮本身可能以别的方式失败)。即 ${BUDGET_TIMEOUT_N} 次完整 agent 运行没有推送任何内容。${IDLE_CLAUSE_ZH}${REPAIR_CLAUSE_ZH}应由人工拆分或缩减该 PR(或同时提高 agent 时间预算与其步骤兜底),然后评论 \`${RETRY_COMMAND}\` 重新武装。在此之前,后续扫描将跳过本 PR。"
fi
fi
fi
Expand Down
14 changes: 13 additions & 1 deletion .qwen/skills/autofix/scripts/run-agent.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,18 @@ const QWEN_IDLE_TIMEOUT_MS =
Number.isFinite(parsedIdleTimeoutMs) && parsedIdleTimeoutMs > 0
? parsedIdleTimeoutMs
: 20 * 60 * 1000;
// Names WHICH budget ran out, because callers run this script for passes with
// very different budgets and the consumer treats them differently: the autofix
// timeout census counts a primary-pass exhaustion ("the PR is too big for a
// round") but not a repair pass hitting its own smaller sub-budget. The token
// is the first thing in the emitted detail, so it is what a census greps on;
// keep it lowercase-dashed and short, and fall back to the neutral 'timeout'
// for any caller that does not set one.
const QWEN_TIMEOUT_LABEL = /^[a-z][a-z-]{0,23}$/.test(
process.env.QWEN_TIMEOUT_LABEL ?? '',
)
? process.env.QWEN_TIMEOUT_LABEL
: 'timeout';
const MAX_STREAM_JSON_LINE_LENGTH = 1024 * 1024;
const OVERSIZED_STREAM_JSON_LINE_NOTICE =
'[run-agent] dropped oversized stream-json line; full bytes in agent.log\n';
Expand Down Expand Up @@ -523,7 +535,7 @@ if (
: result.idleTimedOut
? `idle-timeout (no output for ${QWEN_IDLE_TIMEOUT_MS}ms — the sandbox likely hung at startup)`
: result.timedOut
? `timeout (${QWEN_TIMEOUT_MS}ms)`
? `${QWEN_TIMEOUT_LABEL} (${QWEN_TIMEOUT_MS}ms)`
: result.signal
? `signal ${result.signal}`
: apiErrorWithoutVerdict
Expand Down
Loading
Loading