diff --git a/packages/coding-agent/src/modes/acp/acp-agent.ts b/packages/coding-agent/src/modes/acp/acp-agent.ts index 247147a8d79..f0e9acc3a6e 100644 --- a/packages/coding-agent/src/modes/acp/acp-agent.ts +++ b/packages/coding-agent/src/modes/acp/acp-agent.ts @@ -5274,7 +5274,9 @@ export class AcpAgent implements Agent { const { outcome } = waiter.terminal; this.#rememberSettledPromptCorrelation(id, record, waiter.correlation); if (outcome.kind === "stopped") { - waiter.resolve({ stopReason: outcome.reason }); + // Once the SDK has acknowledged a client cancel, the cancellation owns the + // turn even if a stale terminal frame races in with another stopped reason. + waiter.resolve({ stopReason: waiter.cancelAcknowledged ? "cancelled" : outcome.reason }); return; } // `phase` is the host's own claim, and a host that omits it leaves `terminalOutcome` diff --git a/packages/coding-agent/test/acp/acp-cancel-settlement.test.ts b/packages/coding-agent/test/acp/acp-cancel-settlement.test.ts index 5fcab0154f0..64e296032f6 100644 --- a/packages/coding-agent/test/acp/acp-cancel-settlement.test.ts +++ b/packages/coding-agent/test/acp/acp-cancel-settlement.test.ts @@ -3296,3 +3296,20 @@ test("successful remote session retirement releases an unresolved abort owner", fixture.dispose(); } }); + +test("cancel 100ms after background prompt start settles cancelled and follow-up end_turn", async () => { + const fixture = await createFixture({ cancelSettlementGraceMs: 25 }); + try { + const background = prompt(fixture, "sleep 5000"); + await bounded(fixture.promptDelivered, "background prompt delivery"); + await Bun.sleep(100); + await bounded(fixture.agent.cancel({ sessionId: fixture.sessionId }), "background cancel acknowledgement"); + expect(await bounded(background, "background cancelled settlement")).toEqual({ stopReason: "cancelled" }); + const followUp = prompt(fixture, "follow-up"); + await waitFor(() => fixture.promptDeliveryCount() === 2, "follow-up prompt delivery"); + fixture.sendStopped("end_turn"); + expect(await bounded(followUp, "follow-up completion")).toEqual({ stopReason: "end_turn" }); + } finally { + fixture.dispose(); + } +});