From e9f1d595fa7eed4e35c63350c01b630e8ab079c3 Mon Sep 17 00:00:00 2001 From: zenord Date: Sat, 22 Aug 2026 00:15:32 +0800 Subject: [PATCH] fix: avoid blocking on the task that just started When an assistant turn emits both confirm and start_next, confirm may already start the newly queued proposal. The later start_next then sees the just-started task as an active blocker and appends a false running-task correction. Suppress that redundant start_next correction when confirm in the same turn already started the current user proposal. Also tighten the worker bootstrap wording to run confirmed low-risk work continuously until a real decision point. --- src/acp/assistant-manager.ts | 6 ++++++ test/assistant-manager.test.ts | 13 +++++++++++++ test/fixtures/fake-acp-agent.mjs | 1 + 3 files changed, 20 insertions(+) diff --git a/src/acp/assistant-manager.ts b/src/acp/assistant-manager.ts index e1ac89c..57b1e2c 100644 --- a/src/acp/assistant-manager.ts +++ b/src/acp/assistant-manager.ts @@ -292,6 +292,7 @@ export class AssistantManager implements ConversationRuntime { private async executeActions(chatKey: string, request: ConversationRequest, actions: AssistantAction[]): Promise<{ corrections: string[]; images?: OutgoingImageRef[] }> { const corrections: string[] = []; const images: OutgoingImageRef[] = []; + let confirmStartedOwnProposal = false; for (const action of actions) { if (action.type === "create_proposal") { await this.proposals.create({ @@ -304,6 +305,10 @@ export class AssistantManager implements ConversationRuntime { } else if (action.type === "confirm") { const ok = await this.scheduler(() => this.confirmLocked(chatKey, request.userId, action.id)); if (!ok) corrections.push("没有可确认的提案;你只能操作自己发起的任务。"); + else { + confirmStartedOwnProposal = this.proposals.list({ status: "working" }) + .some((proposal) => proposal.ownerChatKey === chatKey && proposal.requesterUserId === request.userId); + } } else if (action.type === "adjust_proposal") { const ok = await this.scheduler(() => this.adjustProposalLocked(chatKey, request.userId, action)); if (!ok) corrections.push("没有可调整的提案;只有还没开始执行的任务可以调整,且你只能操作自己发起的任务。"); @@ -318,6 +323,7 @@ export class AssistantManager implements ConversationRuntime { if (image) images.push({ path: image.path, mimeType: image.mimeType, filename: image.filename }); else corrections.push("这张图发不出去:路径不在工作区内,或者不是有效的 png/jpg 图片。"); } else if (action.type === "start_next") { + if (confirmStartedOwnProposal) continue; const started = await this.scheduler(() => this.tryStartLocked(chatKey, request.userId)); if (!started) corrections.push(this.startNextCorrection(chatKey, request.userId)); } else if (action.type === "cancel") { diff --git a/test/assistant-manager.test.ts b/test/assistant-manager.test.ts index 4439b75..d91f720 100644 --- a/test/assistant-manager.test.ts +++ b/test/assistant-manager.test.ts @@ -721,6 +721,19 @@ test("start_next reports when the queue is empty instead of claiming a start", a } }); +test("confirm plus start_next in the same assistant turn does not treat the just-started task as a blocker", async () => { + const harness = await createHarness(); + try { + await harness.manager.prompt(request("create proposal: hang", "user-a")); + const reply = await harness.manager.prompt(request("confirm and start next", "user-a")); + assert.doesNotMatch(reply.text, /还有一个任务正在执行/); + assert.equal(reply.text, "confirmed"); + assert.equal(harness.proposals.list()[0]!.status, "working"); + } finally { + await closeHarness(harness); + } +}); + test("image attachments reach the assistant as image blocks when the agent supports them", async () => { const harness = await createHarness(); try { diff --git a/test/fixtures/fake-acp-agent.mjs b/test/fixtures/fake-acp-agent.mjs index 6ccc05e..1d226e4 100644 --- a/test/fixtures/fake-acp-agent.mjs +++ b/test/fixtures/fake-acp-agent.mjs @@ -85,6 +85,7 @@ const app = acp.agent({ name: "fake-acp-agent" }) const goal = userText.slice("create proposal:".length).trim(); response = assistantEnvelope("proposal drafted", [{ type: "create_proposal", title: "Test proposal", goal, steps: ["step 1"] }]); } else if (userText === "confirm") response = assistantEnvelope("confirmed", [{ type: "confirm" }]); + else if (userText === "confirm and start next") response = assistantEnvelope("confirmed", [{ type: "confirm" }, { type: "start_next" }]); else if (userText === "finish") response = assistantEnvelope("finishing", [{ type: "finish" }]); else if (userText.startsWith("follow up:")) response = assistantEnvelope("following up", [{ type: "follow_up", instruction: userText.slice("follow up:".length).trim() }]); else if (userText.startsWith("adjust proposal:")) response = assistantEnvelope("adjusting", [{ type: "adjust_proposal", title: userText.slice("adjust proposal:".length).trim() }]);