diff --git a/src/agents/embedded-agent-runner/run/attempt-stream-runtime-prepare.test.ts b/src/agents/embedded-agent-runner/run/attempt-stream-runtime-prepare.test.ts index 73195aa139f3..28f6ce50e08d 100644 --- a/src/agents/embedded-agent-runner/run/attempt-stream-runtime-prepare.test.ts +++ b/src/agents/embedded-agent-runner/run/attempt-stream-runtime-prepare.test.ts @@ -228,6 +228,25 @@ describe("prepareEmbeddedAttemptStreamRuntime", () => { expect(mocks.withOwnedSessionTranscriptWrites).toHaveBeenCalledOnce(); }); + it.each([ + { label: "external cancellation", message: "run cancelled" }, + { label: "run timeout", message: "run timed out" }, + ])("does not start a prompt after $label", async ({ message }) => { + const fixture = createFixture(); + const runtime = await prepareEmbeddedAttemptStreamRuntime(fixture.input); + const reason = new Error(message); + const abortError = new Error(message, { cause: reason }); + abortError.name = "AbortError"; + fixture.input.runAbortController.abort(reason); + mocks.abortable.mockImplementationOnce((_signal, _promise) => Promise.reject(abortError)); + + await expect(runtime.promptActiveSession("must not start")).rejects.toBe(abortError); + + expect(fixture.activeSession.prompt).not.toHaveBeenCalled(); + expect(fixture.trackPromptSettlePromise).not.toHaveBeenCalled(); + expect(mocks.abortable).toHaveBeenCalledOnce(); + }); + it("flushes pending tool results and disposes the session when history preparation fails", async () => { const fixture = createFixture({ aborted: true }); const failure = new Error("history failed"); diff --git a/src/agents/embedded-agent-runner/run/attempt-stream-runtime-prepare.ts b/src/agents/embedded-agent-runner/run/attempt-stream-runtime-prepare.ts index edcfe89e09bf..3458e5ff8ed1 100644 --- a/src/agents/embedded-agent-runner/run/attempt-stream-runtime-prepare.ts +++ b/src/agents/embedded-agent-runner/run/attempt-stream-runtime-prepare.ts @@ -137,9 +137,14 @@ export async function prepareEmbeddedAttemptStreamRuntime(input: { prompt: string, options?: Parameters[1], ): Promise => - withOwnedSessionTranscriptWrites(input.ownedTranscriptWriteContext, async () => - abortable(input.trackPromptSettlePromise(activeSession.prompt(prompt, options))), - ); + withOwnedSessionTranscriptWrites(input.ownedTranscriptWriteContext, async () => { + // Prompting starts its own agent loop; reject before creating a loop that + // an already-aborted attempt can no longer cancel. + if (input.runAbortController.signal.aborted) { + return abortable(Promise.resolve()); + } + return abortable(input.trackPromptSettlePromise(activeSession.prompt(prompt, options))); + }); const onBlockReply = attempt.onBlockReply ? bindOwnedSessionTranscriptWrites(input.ownedTranscriptWriteContext, attempt.onBlockReply) : undefined;