修正无头应答器收 stdin 的时机
Project CI / Repository checks (pull_request) Failing after 9s
Project CI / Backend tests (pull_request) Failing after 12s
Project CI / Frontend tests (pull_request) Successful in 2m58s
Project CI / Native shell tests (pull_request) Failing after 11m48s

CLI 的 REPL 是「先打印提示符再读行」,所以第一个「你>」出现时本轮还没开始跑:它
就是用来读我们这条任务的。在那里收 stdin,等确认卡弹出来已经是 EOF,自动应答器
根本没机会回 approve,一轮真实跑就这样收在 pending-confirmation。改成按提示符计
数,投递之后的下一个提示符才收口。

同时补上原本缺的那条:总控可能判定直接回复而不起持久 Run,那条路径没有 turn 回
执,只等回执会一直干等到超时——实测白等了十分钟。提示符收口对两种情况都成立。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-20 08:49:40 +00:00
parent e45c6760b6
commit 276606fc70
2 changed files with 51 additions and 2 deletions
@@ -946,6 +946,20 @@ export function nextSwarmAutoPilotReply(output) {
return null;
}
// CLI 的 REPL 是「先打印提示符再读行」,所以第一个「你>」出现时本轮还没开始跑:
// 它就是用来读我们这条任务的。收 stdin 必须等到投递之后的下一个提示符——那才是
// 本轮结束、CLI 回到待输入状态。绝大多数情况下此前已经打印过 turn 回执,但总控也
// 可能判定直接回复而不起持久 Run,那条路径没有回执,只等回执会一直干等到超时。
const swarmChatPromptPattern = /(^|\n)你> $/u;
export function swarmAutoPilotSitsAtPrompt(output) {
return swarmChatPromptPattern.test(output);
}
export function swarmAutoPilotShouldCloseInput(output, promptsAfterSubmit) {
return swarmAutoPilotSitsAtPrompt(output) && promptsAfterSubmit >= 1;
}
async function runTaskCargo(
cliArguments,
task,
@@ -960,6 +974,9 @@ async function runTaskCargo(
const reportLines = [];
let pendingLine = '';
let settled = false;
let taskSubmitted = false;
let promptsSeen = 0;
let sittingAtPrompt = false;
child.stdout.setEncoding('utf8');
child.stdout.on('data', (chunk) => {
process.stdout.write(chunk);
@@ -974,8 +991,15 @@ async function runTaskCargo(
}
}
if (!autoPilot || child.stdin.writableEnded) return;
// turn 已给出回执后不再应答,直接收 stdin 让 CLI 正常退出。
if (settled) {
const atPrompt = swarmAutoPilotSitsAtPrompt(pendingLine);
if (atPrompt && !sittingAtPrompt) promptsSeen += 1;
sittingAtPrompt = atPrompt;
// turn 已给出回执、或 CLI 回到了投递之后的下一个提示符,都说明本轮结束。
if (
settled ||
(taskSubmitted &&
swarmAutoPilotShouldCloseInput(pendingLine, promptsSeen - 1))
) {
child.stdin.end();
return;
}
@@ -987,6 +1011,7 @@ async function runTaskCargo(
});
if (autoPilot) {
child.stdin.write(`${task}\n`);
taskSubmitted = true;
} else {
child.stdin.end(`${task}\n`);
}
@@ -46,6 +46,8 @@ import {
resolveSwarmTestTimeoutMs,
runnerEndpointFileName,
shouldStartPersistentPreview,
swarmAutoPilotShouldCloseInput,
swarmAutoPilotSitsAtPrompt,
terminateChildTree,
testProjectPrefix,
testProjectSentinelName,
@@ -715,6 +717,28 @@ describe('Swarm test argument parsing', () => {
).toBeNull();
});
it('closes the driven stdin only at the prompt that follows submission', () => {
// CLI 先打印提示符再读行:第一个「你>」是用来读我们这条任务的,此时本轮还没
// 开始跑。在那里收 stdin,确认卡弹出来时已经是 EOF,自动应答器根本没机会回
// approve——实测就这样把一轮跑成了 pending-confirmation。
expect(swarmAutoPilotSitsAtPrompt('你> ')).toBe(true);
expect(swarmAutoPilotShouldCloseInput('你> ', 0)).toBe(false);
expect(swarmAutoPilotShouldCloseInput('你> ', 1)).toBe(true);
// 总控判定直接回复那条路径没有 turn 回执,同样靠这个提示符收口。
expect(swarmAutoPilotShouldCloseInput('[意图] reply\n\n你> ', 1)).toBe(
true,
);
// 提示符不在缓冲末尾、或本轮还在推进时不得收 stdin。
expect(
swarmAutoPilotSitsAtPrompt(
'你> [决策] Agent 正在判断直接回复或调用持久能力。',
),
).toBe(false);
expect(
swarmAutoPilotSitsAtPrompt('[状态] project-supervisor running/planning'),
).toBe(false);
});
it('keeps persistent preview only for manual chat mode', () => {
expect(shouldStartPersistentPreview(parseSwarmTestArguments([]))).toBe(
true,