From 088fb7fb75ad080781730dde3cb3c6562357b596 Mon Sep 17 00:00:00 2001 From: can1357 Date: Mon, 8 Jun 2026 02:04:59 +0200 Subject: [PATCH] fix(eval): resolved JS/Python resets by awaiting in-flight operations - Coalesced concurrent JS and Python reset requests by awaiting in-flight promises instead of throwing. - Aligned non-reset execution calls to wait for in-progress resets before running on a recreated session. --- .../src/eval/js/context-manager.ts | 33 ++++++++++++------ packages/coding-agent/src/eval/py/executor.ts | 34 +++++++++++++------ .../core/python-executor-lifecycle.test.ts | 23 +++++++++++++ 3 files changed, 68 insertions(+), 22 deletions(-) diff --git a/packages/coding-agent/src/eval/js/context-manager.ts b/packages/coding-agent/src/eval/js/context-manager.ts index c1dcef642..764edb660 100644 --- a/packages/coding-agent/src/eval/js/context-manager.ts +++ b/packages/coding-agent/src/eval/js/context-manager.ts @@ -52,7 +52,7 @@ interface JsSession { const sessions = new Map(); const startingSessions = new Map>(); -const resettingSessions = new Set(); +const resettingSessions = new Map>(); // Worker startup (module-graph import + WorkerCore construction) is infrastructure // cost, not user compute. Floor it independently of Bun's 5s default per-test timeout // so a slow cold-start under load isn't aborted mid-init — terminating a still- @@ -73,17 +73,28 @@ export async function executeInVmContext(options: { runState: VmRunState; }): Promise<{ value: unknown }> { if (options.reset) { - if (resettingSessions.has(options.sessionKey)) { - throw new ToolError("JS context reset already in progress"); + // Coalesce concurrent resets: an existing in-flight reset already + // produces a fresh context, so a follow-up `reset: true` cell should + // just wait for it rather than failing the user-visible call. + const inFlight = resettingSessions.get(options.sessionKey); + if (inFlight) await inFlight.catch(() => undefined); + else { + const resetPromise = resetVmContext(options.sessionKey); + resettingSessions.set( + options.sessionKey, + resetPromise.then(() => undefined), + ); + try { + await resetPromise; + } finally { + resettingSessions.delete(options.sessionKey); + } } - resettingSessions.add(options.sessionKey); - try { - await resetVmContext(options.sessionKey); - } finally { - resettingSessions.delete(options.sessionKey); - } - } else if (resettingSessions.has(options.sessionKey)) { - throw new ToolError("JS context reset in progress"); + } else { + // Internal coordination: wait for any in-flight reset to settle and + // then run on the freshly-rebuilt context. + const inFlight = resettingSessions.get(options.sessionKey); + if (inFlight) await inFlight.catch(() => undefined); } const session = await acquireSession( options.sessionKey, diff --git a/packages/coding-agent/src/eval/py/executor.ts b/packages/coding-agent/src/eval/py/executor.ts index 6f678527c..37d1c1b05 100644 --- a/packages/coding-agent/src/eval/py/executor.ts +++ b/packages/coding-agent/src/eval/py/executor.ts @@ -126,7 +126,7 @@ interface PythonSession { const sessions = new Map(); const startingSessions = new Map>(); -const resettingSessions = new Set(); +const resettingSessions = new Map>(); function normalizeSessionCwd(cwd: string): string { return path.resolve(cwd); @@ -611,17 +611,29 @@ async function executeOnSession(code: string, cwd: string, options: PythonExecut options.bridgeSessionId = sessionId; } if (options.reset) { - if (resettingSessions.has(sessionKey)) { - throw new Error("Python kernel reset already in progress"); + // Coalesce concurrent resets: if another reset is in flight for this + // session, await it instead of throwing — the caller's intent ("start + // from a clean kernel") is satisfied once that reset settles. + const inFlight = resettingSessions.get(sessionKey); + if (inFlight) await inFlight.catch(() => undefined); + else { + const resetPromise = resetSession(sessionKey); + resettingSessions.set( + sessionKey, + resetPromise.then(() => undefined), + ); + try { + await resetPromise; + } finally { + resettingSessions.delete(sessionKey); + } } - resettingSessions.add(sessionKey); - try { - await resetSession(sessionKey); - } finally { - resettingSessions.delete(sessionKey); - } - } else if (resettingSessions.has(sessionKey)) { - throw new Error("Python kernel reset in progress"); + } else { + // A reset already in progress is an internal coordination state, not a + // user-visible failure. Wait for it to clear, then proceed with the + // requested execution on the freshly-restarted kernel. + const inFlight = resettingSessions.get(sessionKey); + if (inFlight) await inFlight.catch(() => undefined); } const session = await acquireSession(sessionKey, sessionId, cwd, options); if (options.signal?.aborted) { diff --git a/packages/coding-agent/test/core/python-executor-lifecycle.test.ts b/packages/coding-agent/test/core/python-executor-lifecycle.test.ts index c8fc90762..ad0fcbc47 100644 --- a/packages/coding-agent/test/core/python-executor-lifecycle.test.ts +++ b/packages/coding-agent/test/core/python-executor-lifecycle.test.ts @@ -119,4 +119,27 @@ describe("executePython lifecycle", () => { expect(kernel.execute).toHaveBeenCalledTimes(0); expect(kernelNext.execute).toHaveBeenCalledTimes(2); }); + + it("coalesces concurrent reset requests instead of throwing 'reset already in progress'", async () => { + // Two cells from the same session asking for reset in flight at once + // previously crashed the second one with "Python kernel reset already + // in progress" — the user reported this as eval returning only the + // status line and no executed output. The executor now waits for the + // in-flight reset and then proceeds. + const kernelA = new FakeKernel(OK_RESULT); + const kernelB = new FakeKernel(OK_RESULT); + vi.spyOn(pythonKernel, "checkPythonKernelAvailability").mockResolvedValue({ ok: true }); + vi.spyOn(pythonKernel.PythonKernel, "start") + .mockResolvedValueOnce(kernelA as unknown as pythonKernel.PythonKernel) + .mockResolvedValueOnce(kernelB as unknown as pythonKernel.PythonKernel); + // Seed a live session that both reset cells will tear down. + await executePython("1 + 1", { kernelMode: "session", sessionId: "coalesce", cwd: getProjectDir() }); + + const [r1, r2] = await Promise.all([ + executePython("2 + 2", { kernelMode: "session", sessionId: "coalesce", reset: true, cwd: getProjectDir() }), + executePython("3 + 3", { kernelMode: "session", sessionId: "coalesce", reset: true, cwd: getProjectDir() }), + ]); + expect(r1.exitCode).toBe(0); + expect(r2.exitCode).toBe(0); + }); });