feat: overhauled stream timeout and eval session management

- Replaced external watchdog timers with per-request SDK timeouts for first-event budget across OpenAI, Anthropic, and Azure providers.
- Keyed Python shared kernels by (sessionId, cwd) to prevent cross-directory state bleed.
- Deduplicated concurrent cold-start session acquisition for JS and Python executors.
- Moved `isOpenAIResponsesProgressEvent` to shared module and scoped display output routing per run for interleaved async cells.
This commit is contained in:
can1357
2026-05-26 16:21:34 +02:00
parent 8da054f4cc
commit 796c437dc1
30 changed files with 1600 additions and 539 deletions
@@ -681,24 +681,30 @@ describe("AgentSession python cleanup", () => {
expect(executeSpy).not.toHaveBeenCalled();
});
it("aborts every active Python execution owned by the session during dispose", async () => {
it("aborts every active concurrent Python execution owned by the session during dispose", async () => {
const { tempDir, cwd } = createTempProject();
tempDirs.push(tempDir);
const kernel = new FakeKernel();
const blockedExecution = Promise.withResolvers<typeof OK_EXECUTION>();
const blockedExecutionStarted = Promise.withResolvers<void>();
kernel.blockedCode = "print('first')";
const bothStarted = Promise.withResolvers<void>();
let starts = 0;
kernel.blockedCode = "print('blocked')";
kernel.blockedExecution = blockedExecution.promise;
kernel.blockedExecutionStarted = () => blockedExecutionStarted.resolve();
kernel.blockedExecutionStarted = () => {
starts += 1;
if (starts >= 2) bothStarted.resolve();
};
vi.spyOn(pythonKernel, "checkPythonKernelAvailability").mockResolvedValue({ ok: true });
vi.spyOn(pythonKernel.PythonKernel, "start").mockResolvedValue(kernel as unknown as PythonKernelInstance);
const session = await createSession(tempDir, cwd);
const firstExecution = session.executePython("print('first')");
await blockedExecutionStarted.promise;
const secondExecution = session.executePython("print('second')");
// Two concurrent blocked executions on the shared kernel session: both must
// be tracked when dispose runs so abortEval cancels every signal.
const firstExecution = session.executePython("print('blocked')");
const secondExecution = session.executePython("print('blocked')");
await bothStarted.promise;
const sleepSpy = mockPositiveSleepsImmediate();
await session.dispose();
@@ -707,7 +713,7 @@ describe("AgentSession python cleanup", () => {
expect(firstResult.cancelled).toBe(true);
expect(secondResult.cancelled).toBe(true);
expect(kernel.executeCalls).toEqual(["print('first')"]);
expect(kernel.executeCalls).toEqual(["print('blocked')", "print('blocked')"]);
expect(kernel.shutdownCalls).toBe(1);
});
});