fix(agents): prevent cancelled prompts from spawning zombie runs (#115011)

This commit is contained in:
Peter Steinberger
2026-07-28 03:43:20 -04:00
committed by GitHub
parent 0d7fb8eb39
commit fe19f8f54b
2 changed files with 27 additions and 3 deletions

View File

@@ -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");

View File

@@ -137,9 +137,14 @@ export async function prepareEmbeddedAttemptStreamRuntime(input: {
prompt: string,
options?: Parameters<typeof activeSession.prompt>[1],
): Promise<void> =>
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;