-
Notifications
You must be signed in to change notification settings - Fork 3k
fix(ci): stream autofix agent progress #8895
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
816292a
9b304a2
2fffcac
bff179d
5045d8a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -22,10 +22,11 @@ const QWEN_TIMEOUT_MS = Number(process.env.QWEN_TIMEOUT_MS) || 50 * 60 * 1000; | |||||||||||||||||||
| // Idle watchdog: a wedged sandbox produces NOTHING — four observed hangs | ||||||||||||||||||||
| // (#8663 x2, #8761 r3, #8763 r4) each printed their last byte at docker | ||||||||||||||||||||
| // container entry and then sat silent for the whole absolute budget, | ||||||||||||||||||||
| // burning 2 hours per round for zero work. Legitimate runs are never that | ||||||||||||||||||||
| // quiet: the longest silence the fleet tolerates elsewhere is the review | ||||||||||||||||||||
| // pipeline's 10-minute stream-idle window for thinking phases on ~1M-token | ||||||||||||||||||||
| // contexts, so twice that is the default. Distinct from QWEN_TIMEOUT_MS so | ||||||||||||||||||||
| // burning 2 hours per round for zero work. Streamed agent events keep active | ||||||||||||||||||||
| // runs observable; the longest silence the fleet tolerates elsewhere is the | ||||||||||||||||||||
| // review pipeline's 10-minute stream-idle window for thinking phases on | ||||||||||||||||||||
| // ~1M-token contexts, so twice that is the default. Distinct from | ||||||||||||||||||||
| // QWEN_TIMEOUT_MS so | ||||||||||||||||||||
| // the failure comment says which limit fired; a leg whose absolute budget is | ||||||||||||||||||||
| // shorter than this window (the review workflow's 18-minute repair pass) | ||||||||||||||||||||
| // always reaches the absolute timer first. | ||||||||||||||||||||
|
|
@@ -37,6 +38,9 @@ const QWEN_IDLE_TIMEOUT_MS = | |||||||||||||||||||
| Number.isFinite(parsedIdleTimeoutMs) && parsedIdleTimeoutMs > 0 | ||||||||||||||||||||
| ? parsedIdleTimeoutMs | ||||||||||||||||||||
| : 20 * 60 * 1000; | ||||||||||||||||||||
| 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'; | ||||||||||||||||||||
| const specs = { | ||||||||||||||||||||
| 'assess-candidates': { | ||||||||||||||||||||
| inputs: ['candidates.json'], | ||||||||||||||||||||
|
|
@@ -176,6 +180,19 @@ function isLoopGuardOutput(output) { | |||||||||||||||||||
| ); | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
|
||||||||||||||||||||
| function streamResultOutput(event) { | ||||||||||||||||||||
| if (!event || event.type !== 'result') return ''; | ||||||||||||||||||||
| return [event.error?.message, event.result] | ||||||||||||||||||||
| .filter((value) => typeof value === 'string') | ||||||||||||||||||||
| .join('\n'); | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
|
||||||||||||||||||||
| function isLoopGuardResult(event) { | ||||||||||||||||||||
| return ( | ||||||||||||||||||||
| event?.is_error === true && isLoopGuardOutput(streamResultOutput(event)) | ||||||||||||||||||||
| ); | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
Comment on lines
+190
to
+194
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [Suggestion] R2-4: The deliberate 中文说明此处有意添加的 — qwen3.8-max via Qwen Code /review (v0.21.9) |
||||||||||||||||||||
|
|
||||||||||||||||||||
| function killQwen(child, signal) { | ||||||||||||||||||||
| try { | ||||||||||||||||||||
| process.kill(-child.pid, signal); | ||||||||||||||||||||
|
|
@@ -190,8 +207,10 @@ function runQwen(options, prompt) { | |||||||||||||||||||
| flags: 'w', | ||||||||||||||||||||
| }); | ||||||||||||||||||||
| log.on('error', () => {}); | ||||||||||||||||||||
| let outputTail = ''; | ||||||||||||||||||||
| let loopDetected = false; | ||||||||||||||||||||
| let diagnosticTail = ''; | ||||||||||||||||||||
| let stdoutCarry = ''; | ||||||||||||||||||||
| let discardingOversizedStdoutLine = false; | ||||||||||||||||||||
| let terminalResult; | ||||||||||||||||||||
| let settled = false; | ||||||||||||||||||||
| let timedOut = false; | ||||||||||||||||||||
| let idleTimedOut = false; | ||||||||||||||||||||
|
|
@@ -207,23 +226,93 @@ function runQwen(options, prompt) { | |||||||||||||||||||
| let sandboxRemoval = null; | ||||||||||||||||||||
|
|
||||||||||||||||||||
| return new Promise((resolve) => { | ||||||||||||||||||||
| const child = spawn(options.qwenBin, ['--yolo', '--prompt', prompt], { | ||||||||||||||||||||
| stdio: ['inherit', 'pipe', 'pipe'], | ||||||||||||||||||||
| detached: true, | ||||||||||||||||||||
| }); | ||||||||||||||||||||
| const child = spawn( | ||||||||||||||||||||
| options.qwenBin, | ||||||||||||||||||||
| [ | ||||||||||||||||||||
| '--yolo', | ||||||||||||||||||||
| '--output-format', | ||||||||||||||||||||
| 'stream-json', | ||||||||||||||||||||
| '--include-partial-messages', | ||||||||||||||||||||
|
Comment on lines
+233
to
+235
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [Critical] stream-json puts every message — including Failure scenario: an autofix round reads a file containing either literal, then hits a mundane transient 429 / timeout / non-zero exit → the round is classified as terminal tool-call-loop: the watermark advances past the feedback item (consumed, never retried) and the PR comment posts the false diagnosis "needs a human", where the sentinel path would have retried with the cause-aware budget. Probe-confirmed A/B: identical 429+exit-1 inputs differing only in one marker-bearing tool_result envelope — without it the run is transient-retryable with sentinels; with it, all sentinels absent. Complements the exit-0-gate finding above: that one fabricates retries; this one suppresses a warranted retry. Suggested fix: derive loop detection from the structured stream — parse stdout line-wise as JSON and set 中文说明stream-json 会把每一条消息——包括携带 agent 所读取的文件/shell/PR 全文内容的 失败场景:某一轮 autofix 读取了包含任一 marker 的文件,随后遇到普通的瞬时 429 / 超时 / 非零退出 → 该轮被归类为终止性 tool-call-loop:水位线越过该 feedback 条目(被消费、不再重试),PR 评论给出"需要人工接管"的错误诊断;而走哨兵路径本可以按原因分类的预算重试。已通过 A/B 探针确认:完全相同的 429+exit-1 输入,唯一差异是一个携带 marker 的 tool_result 消息——没有它时运行被归为可重试的瞬时错误并写出哨兵;有它时所有哨兵缺失。与上面 exit-0 分支的问题互为镜像:那一个伪造重试,这一个压制本应发生的重试。 建议修复:从结构化流推导循环检测——把 stdout 按行解析为 JSON,仅在终端 — qwen3.8-max via Qwen Code /review (v0.21.9)
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 已修复 + 验证证据:聚焦分类用例 10/10 通过;完整 workflow 154/154 断言通过。
Comment on lines
+233
to
+235
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [Suggestion] Suggested fix — forward only non- // in record(), replace the unconditional stream.write(chunk) for stdout:
if (source === 'stderr') stream.write(chunk);
// stdout echo happens per parsed line inside consumeStreamJson,
// skipping lines whose parsed event type is 'stream_event'中文说明
建议修复——只把非 — qwen3.8-max via Qwen Code /review (v0.21.9)
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 已修复 + 验证证据:stream_event 仅保留在 agent.log,不再复制到 step stdout;相关用例 10/10、完整 workflow 158/158 断言通过。 |
||||||||||||||||||||
| '--prompt', | ||||||||||||||||||||
| prompt, | ||||||||||||||||||||
| ], | ||||||||||||||||||||
| { | ||||||||||||||||||||
| stdio: ['inherit', 'pipe', 'pipe'], | ||||||||||||||||||||
| detached: true, | ||||||||||||||||||||
| }, | ||||||||||||||||||||
| ); | ||||||||||||||||||||
|
|
||||||||||||||||||||
| const appendDiagnostic = (text) => { | ||||||||||||||||||||
| diagnosticTail = (diagnosticTail + text).slice(-20_000); | ||||||||||||||||||||
| }; | ||||||||||||||||||||
|
|
||||||||||||||||||||
| const consumeStreamJsonLine = (line, terminated) => { | ||||||||||||||||||||
| if (!line.trim()) return; | ||||||||||||||||||||
| try { | ||||||||||||||||||||
| const event = JSON.parse(line); | ||||||||||||||||||||
| lastOutputAt = Date.now(); | ||||||||||||||||||||
| if (event?.type === 'result') terminalResult = event; | ||||||||||||||||||||
| if (event?.type !== 'stream_event') { | ||||||||||||||||||||
| process.stdout.write(`${line}${terminated ? '\n' : ''}`); | ||||||||||||||||||||
| } | ||||||||||||||||||||
| } catch { | ||||||||||||||||||||
| appendDiagnostic(`${line}${terminated ? '\n' : ''}`); | ||||||||||||||||||||
| process.stdout.write(`${line}${terminated ? '\n' : ''}`); | ||||||||||||||||||||
| } | ||||||||||||||||||||
| }; | ||||||||||||||||||||
|
|
||||||||||||||||||||
| const consumeStreamJson = (text, final = false) => { | ||||||||||||||||||||
| const parts = text.split('\n'); | ||||||||||||||||||||
| for (const [index, part] of parts.entries()) { | ||||||||||||||||||||
| const terminated = index < parts.length - 1; | ||||||||||||||||||||
| if (!discardingOversizedStdoutLine) { | ||||||||||||||||||||
| const remaining = MAX_STREAM_JSON_LINE_LENGTH - stdoutCarry.length; | ||||||||||||||||||||
| if (part.length <= remaining) { | ||||||||||||||||||||
| stdoutCarry += part; | ||||||||||||||||||||
| } else { | ||||||||||||||||||||
| stdoutCarry = ''; | ||||||||||||||||||||
| discardingOversizedStdoutLine = true; | ||||||||||||||||||||
| } | ||||||||||||||||||||
| } | ||||||||||||||||||||
| if (terminated) { | ||||||||||||||||||||
| if (discardingOversizedStdoutLine) { | ||||||||||||||||||||
| process.stdout.write(OVERSIZED_STREAM_JSON_LINE_NOTICE); | ||||||||||||||||||||
| } else { | ||||||||||||||||||||
| consumeStreamJsonLine(stdoutCarry, true); | ||||||||||||||||||||
| } | ||||||||||||||||||||
| stdoutCarry = ''; | ||||||||||||||||||||
| discardingOversizedStdoutLine = false; | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
Comment on lines
+277
to
+285
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [Suggestion] R3-7: An oversized stdout line is dropped with zero breadcrumb in step output — even non-JSON garbage gets echoed (catch branch), but a discarded line produces nothing. If the dropped line was the terminal result event, 中文说明超长 stdout 行被丢弃时在 step 输出中不留任何痕迹——连无法解析的 JSON 垃圾都会被回显(catch 分支),但被丢弃的行什么都不产生。如果被丢弃的正是终端 result 事件, — qwen3.8-max via Qwen Code /review (v0.21.9)
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 已修复 + 验证证据:5045d8a370;丢弃超大行时现输出单条可观测提示,相关 4/4 通过。 |
||||||||||||||||||||
| } | ||||||||||||||||||||
| if (final) { | ||||||||||||||||||||
| if (discardingOversizedStdoutLine) { | ||||||||||||||||||||
| process.stdout.write(OVERSIZED_STREAM_JSON_LINE_NOTICE); | ||||||||||||||||||||
| } else { | ||||||||||||||||||||
| consumeStreamJsonLine(stdoutCarry, false); | ||||||||||||||||||||
| } | ||||||||||||||||||||
| stdoutCarry = ''; | ||||||||||||||||||||
| discardingOversizedStdoutLine = false; | ||||||||||||||||||||
| } | ||||||||||||||||||||
| }; | ||||||||||||||||||||
|
|
||||||||||||||||||||
| const finish = (result) => { | ||||||||||||||||||||
| if (settled) return; | ||||||||||||||||||||
| settled = true; | ||||||||||||||||||||
| clearTimeout(timer); | ||||||||||||||||||||
| clearTimeout(killTimer); | ||||||||||||||||||||
| clearInterval(idleTimer); | ||||||||||||||||||||
| const apiErrorInfo = recoverableApiError(outputTail); | ||||||||||||||||||||
| consumeStreamJson('', true); | ||||||||||||||||||||
| const terminalOutput = streamResultOutput(terminalResult); | ||||||||||||||||||||
|
Comment on lines
+304
to
+305
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [Suggestion] No test exercises Suggested fix — next to the other exit-0 tests in it('classifies a chunk-split result line on exit zero', () => {
withRunnerDir((dir) => {
writeFileSync(join(dir, 'feedback.md'), 'feedback\n');
const stub = writeWorkdirStub(dir, [
"const line = qwenResultLine({ result: '[API Error: 429 quota exceeded]' });",
'process.stdout.write(line.slice(0, 20));',
'setTimeout(() => {',
' process.stdout.write(line.slice(20));',
' process.exit(0);',
'}, 300);',
]);
const result = runAddressReview(dir, stub);
expect(result.status).toBe(1);
expect(existsSync(join(dir, 'agent-api-error'))).toBe(true);
});
});中文说明没有任何测试覆盖 建议修复——在 — qwen3.8-max via Qwen Code /review (v0.21.9)
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 已修复 + 验证证据:跨 chunk 与无换行终止 result 均有回归覆盖;相关用例 10/10、完整 workflow 158/158 断言通过。 |
||||||||||||||||||||
| const apiErrorInfo = recoverableApiError( | ||||||||||||||||||||
| result.status === 0 | ||||||||||||||||||||
| ? terminalOutput | ||||||||||||||||||||
| : `${diagnosticTail}\n${terminalOutput}`, | ||||||||||||||||||||
| ); | ||||||||||||||||||||
|
Comment on lines
+306
to
+310
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [Suggestion] The exit-0 branch's deliberate exclusion of Suggested fix: it('keeps an exit-zero run with a stderr-only marker out of the retry path', () => {
withRunnerDir((dir) => {
writeFileSync(join(dir, 'feedback.md'), 'feedback\n');
const stub = writeWorkdirStub(dir, [
"process.stderr.write('[API Error: 429 quota exceeded]\\n');",
'process.exit(0);',
]);
const result = runAddressReview(dir, stub);
expect(result.status).toBe(1);
expect(existsSync(join(dir, 'agent-api-error'))).toBe(false);
expect(readFileSync(join(dir, 'failure.md'), 'utf8')).toContain(
'finished without required output file(s)',
);
});
});中文说明exit-0 分支刻意排除 建议修复——补上面的用例。 — qwen3.8-max via Qwen Code /review (v0.21.9) |
||||||||||||||||||||
| const payload = { | ||||||||||||||||||||
| ...result, | ||||||||||||||||||||
| timedOut, | ||||||||||||||||||||
| idleTimedOut, | ||||||||||||||||||||
| loopDetected: loopDetected || isLoopGuardOutput(outputTail), | ||||||||||||||||||||
| loopDetected: isLoopGuardResult(terminalResult), | ||||||||||||||||||||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [Suggestion] R3-8: The runner this PR reworks is one of two copy-forked 中文说明本 PR 重构的 runner 是两个复制分叉的 — qwen3.8-max via Qwen Code /review (v0.21.9) |
||||||||||||||||||||
| // A RECOVERABLE model error means qwen never evaluated the feedback — | ||||||||||||||||||||
| // the workflow retries it rather than advancing the watermark. | ||||||||||||||||||||
| apiError: apiErrorInfo.error, | ||||||||||||||||||||
|
|
@@ -237,8 +326,7 @@ function runQwen(options, prompt) { | |||||||||||||||||||
| } | ||||||||||||||||||||
| }; | ||||||||||||||||||||
|
|
||||||||||||||||||||
| const record = (chunk, stream) => { | ||||||||||||||||||||
| lastOutputAt = Date.now(); | ||||||||||||||||||||
| const record = (chunk, stream, source) => { | ||||||||||||||||||||
| const text = chunk.toString('utf8'); | ||||||||||||||||||||
| if (!sandboxName) { | ||||||||||||||||||||
| lineCarry += text; | ||||||||||||||||||||
|
|
@@ -256,14 +344,18 @@ function runQwen(options, prompt) { | |||||||||||||||||||
| } | ||||||||||||||||||||
| } | ||||||||||||||||||||
| } | ||||||||||||||||||||
| outputTail = (outputTail + text).slice(-20_000); | ||||||||||||||||||||
| if (!loopDetected && isLoopGuardOutput(outputTail)) loopDetected = true; | ||||||||||||||||||||
| if (source === 'stdout') { | ||||||||||||||||||||
| consumeStreamJson(text); | ||||||||||||||||||||
| } else { | ||||||||||||||||||||
| lastOutputAt = Date.now(); | ||||||||||||||||||||
| appendDiagnostic(text); | ||||||||||||||||||||
| stream.write(chunk); | ||||||||||||||||||||
| } | ||||||||||||||||||||
| log.write(chunk); | ||||||||||||||||||||
| stream.write(chunk); | ||||||||||||||||||||
| }; | ||||||||||||||||||||
|
|
||||||||||||||||||||
| child.stdout.on('data', (chunk) => record(chunk, process.stdout)); | ||||||||||||||||||||
| child.stderr.on('data', (chunk) => record(chunk, process.stderr)); | ||||||||||||||||||||
| child.stdout.on('data', (chunk) => record(chunk, process.stdout, 'stdout')); | ||||||||||||||||||||
| child.stderr.on('data', (chunk) => record(chunk, process.stderr, 'stderr')); | ||||||||||||||||||||
| child.on('error', (error) => finish({ error, status: null, signal: null })); | ||||||||||||||||||||
| child.on('close', (status, signal) => | ||||||||||||||||||||
| finish({ error: null, status, signal }), | ||||||||||||||||||||
|
|
@@ -394,7 +486,24 @@ const result = await runQwen(options, prompt); | |||||||||||||||||||
| // timeout) so the leak warning and the removal itself settle before this | ||||||||||||||||||||
| // process exits and the next step inspects the host. | ||||||||||||||||||||
| if (result.sandboxRemoval) await result.sandboxRemoval; | ||||||||||||||||||||
| if (result.error || result.signal || result.status !== 0) { | ||||||||||||||||||||
| const missingOutputs = missing(options.workdir, spec.outputs); | ||||||||||||||||||||
| const presentOutputs = spec.outputs.filter( | ||||||||||||||||||||
| (name) => !missingOutputs.includes(name), | ||||||||||||||||||||
| ); | ||||||||||||||||||||
| const hasOutputVerdict = spec.anyOutput | ||||||||||||||||||||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [Suggestion]
Suggested change
— deepseek-v4-flash via Qwen Code /review (v0.21.8) 中文说明
— deepseek-v4-flash via Qwen Code /review (v0.21.8)
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 已修复 + 验证证据:聚焦分类用例 10/10 通过;完整 workflow 154/154 断言通过。 |
||||||||||||||||||||
| ? presentOutputs.length > 0 | ||||||||||||||||||||
| : missingOutputs.length === 0; | ||||||||||||||||||||
|
Comment on lines
+493
to
+495
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [Suggestion] The output-contract predicate is now spelled twice in this file in two formulations: Concrete cost: any future change to the output contract (a fourth mode, an output that may legitimately be empty, altered Suggested fix: reuse the hoisted value at the later site — replace the 中文说明输出契约谓词现在在本文件中以两种写法出现了两次:此处的 具体代价:未来任何对输出契约的修改(第四种模式、某个允许为空的输出、 建议修复:在后面一处复用已提升的值——把 — qwen3.8-max via Qwen Code /review (v0.21.9)
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 已修复 + 验证证据:聚焦分类用例 10/10 通过;完整 workflow 154/154 断言通过。 |
||||||||||||||||||||
| const apiErrorWithoutVerdict = | ||||||||||||||||||||
| result.status === 0 && | ||||||||||||||||||||
| result.apiError && | ||||||||||||||||||||
|
Comment on lines
+496
to
+498
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [Critical] The new exit-0 gate trusts Failure scenario: a PR under autofix contains the marker text in a file the agent reads late in the run (within the 20 KB tail), and the round exits 0 without complete outputs → the workflow takes the cause-aware retry branch instead of the terminal branch: the watermark does not advance, the marker's first line is echoed into the PR failure comment ( Suggested fix: corroborate before treating an exit-0 run as an API failure — parse the tail line-wise as JSON and only accept the marker from assistant/system-emitted content (excluding 中文说明新的 exit-0 分支依赖 失败场景:被 autofix 托管的 PR 在某个文件中埋有该 marker 文本,agent 在运行后期(尾部 20 KB 窗口内)读到它,且该轮以退出码 0 结束、没有产出完整输出 → workflow 进入按原因分类的重试分支而不是终止分支:水位线不前进,marker 首行被回显到 PR 失败评论( 建议修复:在把 exit-0 运行当作 API 失败之前增加佐证——将尾部按行做 JSON 解析,只接受来自 assistant/system 输出内容的 marker(排除 — qwen3.8-max via Qwen Code /review (v0.21.9)
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 已修复 + 验证证据:聚焦分类用例 10/10 通过;完整 workflow 154/154 断言通过。
Comment on lines
+496
to
+498
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [Suggestion] The exit-0 API-error gate authorizes the retryable classification by pattern-matching the Suggested fix — make the trade-off explicit at the gate:
Suggested change
中文说明exit-0 API 错误分支是靠在模型自己控制的文本里做模式匹配来批准"可重试"分类的——失败场景:exit 0 时 建议修复——在该分支处把这一取舍写明(见上方 suggestion)。 — qwen3.8-max via Qwen Code /review (v0.21.9) |
||||||||||||||||||||
| !hasOutputVerdict && | ||||||||||||||||||||
| !existsSync(file(options.workdir, 'failure.md')); | ||||||||||||||||||||
| if ( | ||||||||||||||||||||
| result.error || | ||||||||||||||||||||
| result.signal || | ||||||||||||||||||||
| result.status !== 0 || | ||||||||||||||||||||
| apiErrorWithoutVerdict | ||||||||||||||||||||
| ) { | ||||||||||||||||||||
| const detail = result.error | ||||||||||||||||||||
| ? result.error.message | ||||||||||||||||||||
| : result.idleTimedOut | ||||||||||||||||||||
|
|
@@ -403,7 +512,9 @@ if (result.error || result.signal || result.status !== 0) { | |||||||||||||||||||
| ? `timeout (${QWEN_TIMEOUT_MS}ms)` | ||||||||||||||||||||
| : result.signal | ||||||||||||||||||||
| ? `signal ${result.signal}` | ||||||||||||||||||||
| : `status ${String(result.status)}`; | ||||||||||||||||||||
| : apiErrorWithoutVerdict | ||||||||||||||||||||
| ? 'recoverable API error without an agent verdict' | ||||||||||||||||||||
| : `status ${String(result.status)}`; | ||||||||||||||||||||
| if (!existsSync(file(options.workdir, 'failure.md'))) { | ||||||||||||||||||||
| if (result.loopDetected) { | ||||||||||||||||||||
| writeFailure( | ||||||||||||||||||||
|
|
@@ -462,7 +573,7 @@ if (result.error || result.signal || result.status !== 0) { | |||||||||||||||||||
| `Qwen failed during ${options.mode}: ${detail}; preserving agent-written failure.md.`, | ||||||||||||||||||||
| ); | ||||||||||||||||||||
| } | ||||||||||||||||||||
| process.exit(result.status ?? 1); | ||||||||||||||||||||
| process.exit(result.status === 0 ? 1 : (result.status ?? 1)); | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
|
||||||||||||||||||||
| if (existsSync(file(options.workdir, 'failure.md'))) { | ||||||||||||||||||||
|
|
@@ -475,18 +586,12 @@ if (existsSync(file(options.workdir, 'failure.md'))) { | |||||||||||||||||||
| process.exit(0); | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
|
||||||||||||||||||||
| const missingOutputs = missing(options.workdir, spec.outputs); | ||||||||||||||||||||
| const presentOutputs = spec.outputs.filter( | ||||||||||||||||||||
| (name) => !missingOutputs.includes(name), | ||||||||||||||||||||
| ); | ||||||||||||||||||||
| if (spec.exclusiveOutput && presentOutputs.length > 1) { | ||||||||||||||||||||
| const message = `Autofix agent wrote mutually exclusive output files: ${presentOutputs.join(', ')}.`; | ||||||||||||||||||||
| writeFailure(options.workdir, message); | ||||||||||||||||||||
| fail(message); | ||||||||||||||||||||
| } | ||||||||||||||||||||
| const ok = spec.anyOutput | ||||||||||||||||||||
| ? missingOutputs.length < spec.outputs.length | ||||||||||||||||||||
| : missingOutputs.length === 0; | ||||||||||||||||||||
| const ok = hasOutputVerdict; | ||||||||||||||||||||
| if (!ok) { | ||||||||||||||||||||
| const message = `Autofix agent finished without required output file(s): ${missingOutputs.join(', ')}.`; | ||||||||||||||||||||
| writeFailure(options.workdir, message); | ||||||||||||||||||||
|
|
||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[Suggestion] The
is_error === trueguard inisLoopGuardResult— the deliberate narrowing from round 1 — has no pinning test: deleting the condition survives the whole suite (all 154 tests), because every loop-guard test usesisError: true. — Failure scenario: an agent whose final SUCCESS text quotes a loop marker (e.g. a summary echoing grepped log output containingLoop detection halted the run) combined with a non-zero exit would then be classified loop-guard — writing the terminalhandoff.md/human-takeover instead of the ordinary retryable failure path. Probe-confirmed: the mutant survives 154/154, and the test below passes on current code and flips against the mutant.Suggested fix:
中文说明
isLoopGuardResult中的is_error === true守卫——第 1 轮修复时刻意做的收窄——没有锁定测试:删掉这个条件后整套测试(154 个)依然全部通过,因为所有 loop-guard 测试都使用isError: true。— 失败场景:一个最终 SUCCESS 文本里引用了 loop marker 的 agent(例如总结里复述 grep 到的日志输出,其中含有Loop detection halted the run),再叠加非零退出,就会被归类为 loop-guard——写出终止性的handoff.md/人工接管,而不是走普通的可重试失败路径。已用探针确认:该突变体在 154/154 下存活,而上面的用例在当前代码上通过、对突变体翻转。建议修复——补上面的用例。
— qwen3.8-max via Qwen Code /review (v0.21.9)