fix(coding-agent): scoped python kernel cleanup to sessions

scoped retained-kernel ownership to agent sessions and cleaned it up on session disposal

cleaned up warmed python owners on session startup failure and rejected new direct and tool-based python starts during disposal, including async hook, preflight, and warmup races
This commit is contained in:
Carl
2026-04-12 13:29:17 +02:00
parent f96422d911
commit 379064a0c9
18 changed files with 3593 additions and 1156 deletions
@@ -294,13 +294,49 @@ describe("PythonKernel (external gateway)", () => {
expect(chunks.join("")).toContain("result");
expect(displays).toEqual([{ type: "json", data: { answer: 42 } }]);
await kernel.shutdown();
const shutdown = await kernel.shutdown();
expect(shutdown).toEqual({ confirmed: true });
expect(fetchMock).toHaveBeenCalledWith("http://gateway.test/api/kernels/kernel-1", {
method: "DELETE",
headers: {},
});
});
it("returns an unconfirmed shutdown result when kernel deletion is not acknowledged", async () => {
let deleteAttempts = 0;
const fetchMock = vi.fn(async (url: string, init?: RequestInit) => {
if (url.endsWith("/api/kernels") && init?.method === "POST") {
return new Response(JSON.stringify({ id: "kernel-delete-failure" }), { status: 201 });
}
if (url.includes("/api/kernels/") && init?.method === "DELETE") {
deleteAttempts += 1;
if (deleteAttempts === 1) {
return new Response("delete failed", { status: 500, statusText: "Server Error" });
}
return new Response("already gone", { status: 404, statusText: "Not Found" });
}
return new Response("", { status: 200 });
});
using _hook = hookFetch((input, init) => fetchMock(String(input), init));
const kernelPromise = PythonKernel.start({ cwd: "/" });
await Bun.sleep(10);
const ws = FakeWebSocket.lastInstance;
if (!ws) throw new Error("WebSocket not initialized");
ws.setSendHandler(data => {
const msg = typeof data === "string" ? (JSON.parse(data) as JupyterMessage) : decodeMessage(data);
sendOkExecution(ws, msg.header.msg_id);
});
const kernel = await kernelPromise;
await expect(kernel.shutdown()).resolves.toEqual({ confirmed: false });
expect(deleteAttempts).toBe(1);
await expect(kernel.shutdown()).resolves.toEqual({ confirmed: true });
expect(deleteAttempts).toBe(2);
await expect(kernel.shutdown()).resolves.toEqual({ confirmed: true });
expect(deleteAttempts).toBe(2);
});
it("initializes the IPython prelude", async () => {
const fetchMock = vi.fn(async (url: string, init?: RequestInit) => {
if (url.endsWith("/api/kernels") && init?.method === "POST") {