refactor(coding-agent/eval): drop python warmup path

The warmup path no longer produces prelude docs, so the cached-session
warmup it implemented added no value over the create-on-first-execute
path that withKernelSession already covers. Remove warmPythonEnvironment,
the backend warm() hook, the eval-tool warmup loop, the createTools
warmup preflight, and the forcePythonWarmup option. Simplify
ExecutorBackendCallOptions into ExecutorBackendExecOptions since execute
is now the only consumer.
This commit is contained in:
can1357
2026-05-01 15:27:40 +02:00
parent 76fe4b4995
commit 6dccdf9193
10 changed files with 16 additions and 344 deletions
+2 -2
View File
@@ -391,7 +391,7 @@ A `ToolFactory` is `(session: ToolSession) => Tool | null | Promise<Tool | null>
1. Normalizes requested tool names (`toolNames`) and always injects `exit_plan_mode`.
2. Resolves eval backend allowance via `PI_PY` override (`getEvalBackendsFromEnv()`) or `eval.py` / `eval.js` settings.
3. Performs Python kernel preflight/warmup when applicable (`checkPythonKernelAvailability`, `warmPythonEnvironment`).
3. Performs Python kernel preflight when applicable (`checkPythonKernelAvailability`).
4. Computes effective gating (`isToolAllowed`) from settings and runtime state:
- feature toggles (`find.enabled`, `grep.enabled`, etc.)
- recursion guard for `task` (`task.maxRecursionDepth` vs `session.taskDepth`)
@@ -777,7 +777,7 @@ This subsystem is split into two layers:
- Entry points:
- `executePython(code, options)`
- `executePythonWithKernel(kernel, code, options)`
- warmup/session utilities (`warmPythonEnvironment`, `disposeAllKernelSessions`).
- session utilities (`disposeAllKernelSessions`, `disposeKernelSessionsByOwner`).
- Manages kernel session lifecycle in `kernelSessions: Map<string, KernelSession>` with:
- bounded session count (`MAX_KERNEL_SESSIONS`), LRU eviction (`evictOldestSession`)
- idle cleanup timer (`cleanupIdleSessions`)
+3 -9
View File
@@ -1,18 +1,14 @@
import type { ToolSession } from "../tools";
import type { EvalDisplayOutput, EvalLanguage } from "./types";
/** Per-call options shared across warm and execute. */
export interface ExecutorBackendCallOptions {
/** Per-cell execute() options. */
export interface ExecutorBackendExecOptions {
cwd: string;
sessionId: string;
sessionFile: string | undefined;
kernelOwnerId: string | undefined;
signal?: AbortSignal;
session: ToolSession;
}
/** Per-cell execute() options. */
export interface ExecutorBackendExecOptions extends ExecutorBackendCallOptions {
deadlineMs: number;
reset: boolean;
artifactPath: string | undefined;
@@ -40,10 +36,8 @@ export interface ExecutorBackend {
readonly label: string;
/** Source language identifier passed to the syntax highlighter (e.g. "python", "javascript"). */
readonly highlightLang: string;
/** Cheap availability check (no full warmup). Used by fallback resolution. */
/** Cheap availability check. Used by fallback resolution. */
isAvailable(session: ToolSession): Promise<boolean>;
/** Optional pre-warm performed once per session (no-op on backends that don't need it). */
warm?(opts: ExecutorBackendCallOptions): Promise<{ ok: boolean; reason?: string }>;
/** Execute one cell. Caller invokes once per cell and aggregates results. */
execute(code: string, opts: ExecutorBackendExecOptions): Promise<ExecutorBackendResult>;
}
@@ -453,25 +453,6 @@ async function ensureKernelAvailable(
}
}
export async function warmPythonEnvironment(
cwd: string,
sessionId?: string,
_useSharedGateway?: boolean,
_sessionFile?: string,
kernelOwnerId?: string,
signal?: AbortSignal,
): Promise<{ ok: boolean; reason?: string }> {
const resolvedSessionId = sessionId ?? `session:${cwd}`;
try {
await logger.time("warmPython:ensureKernelAvailable", ensureKernelAvailable, cwd, { signal });
} catch (err: unknown) {
const reason = err instanceof Error ? err.message : String(err);
return { ok: false, reason };
}
attachKernelOwner(resolvedSessionId, kernelOwnerId);
return { ok: true };
}
function isResourceExhaustionError(error: unknown): boolean {
const message = error instanceof Error ? error.message : String(error);
return (
+2 -20
View File
@@ -1,11 +1,6 @@
import type { ToolSession } from "../../tools";
import type {
ExecutorBackend,
ExecutorBackendCallOptions,
ExecutorBackendExecOptions,
ExecutorBackendResult,
} from "../backend";
import { executePython, type PythonExecutorOptions, warmPythonEnvironment } from "./executor";
import type { ExecutorBackend, ExecutorBackendExecOptions, ExecutorBackendResult } from "../backend";
import { executePython, type PythonExecutorOptions } from "./executor";
import { checkPythonKernelAvailability } from "./kernel";
const PYTHON_SESSION_PREFIX = "python:";
@@ -29,19 +24,6 @@ export default {
return availability.ok;
},
async warm(opts: ExecutorBackendCallOptions): Promise<{ ok: boolean; reason?: string }> {
const useSharedGateway = readSetting<boolean>(opts.session, "python.sharedGateway");
const result = await warmPythonEnvironment(
opts.cwd,
namespaceSessionId(opts.sessionId),
useSharedGateway,
opts.sessionFile,
opts.kernelOwnerId,
opts.signal,
);
return { ok: result.ok, reason: result.reason };
},
async execute(code: string, opts: ExecutorBackendExecOptions): Promise<ExecutorBackendResult> {
const useSharedGateway = readSetting<boolean>(opts.session, "python.sharedGateway");
const kernelMode = readSetting<PythonExecutorOptions["kernelMode"]>(opts.session, "python.kernelMode");
-4
View File
@@ -206,9 +206,6 @@ export interface CreateAgentSessionOptions {
enableLsp?: boolean;
/** Skip Python kernel availability check and prelude warmup */
skipPythonPreflight?: boolean;
/** Force Python prelude warmup even when test env would normally skip it */
forcePythonWarmup?: boolean;
/** Tool names explicitly requested (enables disabled-by-default tools) */
toolNames?: string[];
@@ -956,7 +953,6 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
return !requestedToolNames || requestedToolNames.includes("edit");
},
skipPythonPreflight: options.skipPythonPreflight,
forcePythonWarmup: options.forcePythonWarmup,
contextFiles,
skills,
eventBus,
-19
View File
@@ -348,8 +348,6 @@ export class EvalTool implements AgentTool<typeof evalSchema> {
});
const sessionId = sessionFile ? `session:${sessionFile}:cwd:${session.cwd}` : `cwd:${session.cwd}`;
const warmedBackends = new Set<EvalLanguage>();
for (let i = 0; i < cells.length; i++) {
const cell = cells[i];
const backend = cell.resolved.backend;
@@ -360,23 +358,6 @@ export class EvalTool implements AgentTool<typeof evalSchema> {
? AbortSignal.any([signal, timeoutSignal, sessionAbortController.signal])
: AbortSignal.any([timeoutSignal, sessionAbortController.signal]);
if (!warmedBackends.has(backend.id) && backend.warm) {
const warmup = await backend.warm({
cwd: session.cwd,
sessionId,
sessionFile: sessionFile ?? undefined,
kernelOwnerId,
signal: combinedSignal,
session,
});
if (!warmup.ok) {
if (combinedSignal.aborted) throw new ToolAbortError();
throw new ToolError(warmup.reason ?? `${backend.label} prelude helpers unavailable`);
}
session.assertEvalExecutionAllowed?.();
}
warmedBackends.add(backend.id);
const cellResult = cellResults[i];
cellResult.status = "running";
cellResult.output = "";
+2 -42
View File
@@ -1,11 +1,10 @@
import type { AgentTool } from "@oh-my-pi/pi-agent-core";
import type { ToolChoice } from "@oh-my-pi/pi-ai";
import { $env, $flag, isBunTestRuntime, logger } from "@oh-my-pi/pi-utils";
import { $env, $flag, logger } from "@oh-my-pi/pi-utils";
import type { AsyncJobManager } from "../async";
import type { PromptTemplate } from "../config/prompt-templates";
import type { Settings } from "../config/settings";
import { EditTool } from "../edit";
import { warmPythonEnvironment } from "../eval/py/executor";
import { checkPythonKernelAvailability } from "../eval/py/kernel";
import type { Skill } from "../extensibility/skills";
import type { InternalUrlRouter } from "../internal-urls";
@@ -108,8 +107,6 @@ export interface ToolSession {
hasUI: boolean;
/** Skip Python kernel availability check and warmup */
skipPythonPreflight?: boolean;
/** Force Python prelude warmup even when test env would normally skip it */
forcePythonWarmup?: boolean;
/** Pre-loaded context files (AGENTS.md, etc) */
contextFiles?: ContextFileEntry[];
/** Pre-loaded skills */
@@ -300,9 +297,7 @@ export async function createTools(session: ToolSession, toolNames?: string[]): P
let pythonAvailable = true;
const shouldCheckPython =
!skipPythonPreflight && allowPython && (requestedTools === undefined || requestedTools.includes("eval"));
const isTestEnv = isBunTestRuntime();
const forcePythonWarmup = session.forcePythonWarmup === true;
const skipPythonWarm = (isTestEnv && !forcePythonWarmup) || $flag("PI_PYTHON_SKIP_CHECK");
if (shouldCheckPython) {
const availability = await logger.time("createTools:pythonCheck", checkPythonKernelAvailability, session.cwd);
pythonAvailable = availability.ok;
@@ -310,41 +305,6 @@ export async function createTools(session: ToolSession, toolNames?: string[]): P
logger.warn("Python kernel unavailable; eval will dispatch to JavaScript backend", {
reason: availability.reason,
});
} else if (!skipPythonWarm) {
const sessionFile = session.getSessionFile?.() ?? undefined;
const kernelOwnerId = session.getEvalKernelOwnerId?.() ?? undefined;
const warmSessionId = sessionFile ? `session:${sessionFile}:cwd:${session.cwd}` : `cwd:${session.cwd}`;
const warmupAbortController = new AbortController();
try {
session.assertEvalExecutionAllowed?.();
const warmupExecution = session.trackEvalExecution
? logger.time(
"createTools:warmPython",
warmPythonEnvironment,
session.cwd,
warmSessionId,
session.settings.get("python.sharedGateway"),
sessionFile,
kernelOwnerId,
warmupAbortController.signal,
)
: logger.time(
"createTools:warmPython",
warmPythonEnvironment,
session.cwd,
warmSessionId,
session.settings.get("python.sharedGateway"),
sessionFile,
kernelOwnerId,
);
await (session.trackEvalExecution?.(warmupExecution, warmupAbortController) ?? warmupExecution);
session.assertEvalExecutionAllowed?.();
} catch (err) {
logger.warn("Failed to warm Python environment", {
error: err instanceof Error ? err.message : String(err),
});
}
}
}
@@ -95,18 +95,15 @@ const createSession = async (
slashCommands: [],
enableMCP: false,
enableLsp: false,
forcePythonWarmup: true,
toolNames: ["eval"],
})
).session;
const stubPythonWarmup = () => vi.spyOn(pythonExecutor, "warmPythonEnvironment").mockResolvedValue({ ok: true });
const createWarmupKernel = () => {
const createMockKernel = () => {
let alive = true;
return {
execute: vi.fn(async () => {
if (!alive) throw new Error("Expected warmup kernel to be restarted after shutdown");
if (!alive) throw new Error("Expected mock kernel to be restarted after shutdown");
return OK_EXECUTION;
}),
ping: vi.fn(async () => alive),
@@ -132,7 +129,7 @@ describe("AgentSession python cleanup", () => {
it("does not dispose unrelated Python owners when createAgentSession fails before session construction", async () => {
const { tempDir, cwd } = createTempProject();
tempDirs.push(tempDir);
const unrelatedKernel = createWarmupKernel();
const unrelatedKernel = createMockKernel();
const unrelatedCwd = path.join(tempDir, "unrelated-before");
const throwingExtension: ExtensionFactory = () => {
throw new Error("Extension init failed");
@@ -164,7 +161,6 @@ describe("AgentSession python cleanup", () => {
slashCommands: [],
enableMCP: false,
enableLsp: false,
forcePythonWarmup: true,
toolNames: ["eval"],
}),
).rejects.toThrow("Extension init failed");
@@ -172,7 +168,7 @@ describe("AgentSession python cleanup", () => {
expect(startSpy).toHaveBeenCalledTimes(1);
expect(unrelatedKernel.shutdown).not.toHaveBeenCalled();
const replacementKernel = createWarmupKernel();
const replacementKernel = createMockKernel();
startSpy.mockResolvedValueOnce(replacementKernel as unknown as PythonKernelInstance);
await pythonExecutor.executePython("print('fresh warmup before')", {
cwd,
@@ -198,7 +194,7 @@ describe("AgentSession python cleanup", () => {
it("does not dispose unrelated Python owners when createAgentSession fails after session construction", async () => {
const { tempDir, cwd } = createTempProject();
tempDirs.push(tempDir);
const unrelatedKernel = createWarmupKernel();
const unrelatedKernel = createMockKernel();
const unrelatedCwd = path.join(tempDir, "unrelated-after");
vi.spyOn(pythonKernel, "checkPythonKernelAvailability").mockResolvedValue({ ok: true });
const startSpy = vi
@@ -229,7 +225,6 @@ describe("AgentSession python cleanup", () => {
slashCommands: [],
enableMCP: false,
enableLsp: false,
forcePythonWarmup: true,
toolNames: ["eval"],
}),
).rejects.toThrow("Memory startup failed");
@@ -237,7 +232,7 @@ describe("AgentSession python cleanup", () => {
expect(startSpy).toHaveBeenCalledTimes(1);
expect(unrelatedKernel.shutdown).not.toHaveBeenCalled();
const replacementKernel = createWarmupKernel();
const replacementKernel = createMockKernel();
startSpy.mockResolvedValueOnce(replacementKernel as unknown as PythonKernelInstance);
await pythonExecutor.executePython("print('fresh warmup after')", {
cwd,
@@ -263,8 +258,6 @@ describe("AgentSession python cleanup", () => {
it("waits for active SDK session Python work before releasing a shared retained kernel", async () => {
const { tempDir, cwd } = createTempProject();
tempDirs.push(tempDir);
stubPythonWarmup();
const kernel = new FakeKernel();
const blockedExecution = Promise.withResolvers<typeof OK_EXECUTION>();
const blockedExecutionStarted = Promise.withResolvers<void>();
@@ -336,128 +329,9 @@ describe("AgentSession python cleanup", () => {
expect(kernel.shutdownCalls).toBe(1);
});
it("aborts tracked eval warmup during session dispose before executePython starts", async () => {
const { tempDir, cwd } = createTempProject();
tempDirs.push(tempDir);
const blockedWarmupStarted = Promise.withResolvers<void>();
const executeSpy = vi.spyOn(pythonExecutor, "executePython").mockResolvedValue({
output: "tool ok",
exitCode: 0,
cancelled: false,
truncated: false,
totalLines: 1,
totalBytes: 7,
outputLines: 1,
outputBytes: 7,
displayOutputs: [],
stdinRequested: false,
});
let warmupCallCount = 0;
const warmupSpy = vi
.spyOn(pythonExecutor, "warmPythonEnvironment")
.mockImplementation(async (_cwd, _sessionId, _useSharedGateway, _sessionFile, _kernelOwnerId, signal) => {
warmupCallCount += 1;
if (warmupCallCount === 1) {
return { ok: true };
}
blockedWarmupStarted.resolve();
return await new Promise<{ ok: boolean; reason?: string }>(resolve => {
const onAbort = () => resolve({ ok: false, reason: "Warmup aborted" });
if (signal?.aborted) {
onAbort();
return;
}
signal?.addEventListener("abort", onAbort, { once: true });
});
});
vi.spyOn(pythonKernel, "checkPythonKernelAvailability").mockResolvedValue({ ok: true });
const session = await createSession(tempDir, cwd);
const EvalTool = session.getToolByName("eval");
expect(EvalTool).toBeDefined();
let toolExecutionSettled = false;
const toolExecution = EvalTool!
.execute("call-id", { input: "```py\nprint('tool')\n```" }, undefined, undefined, undefined)
.finally(() => {
toolExecutionSettled = true;
});
await blockedWarmupStarted.promise;
let disposed = false;
const disposeSession = session.dispose().then(() => {
disposed = true;
});
await Bun.sleep(0);
expect(disposed).toBe(false);
expect(toolExecutionSettled).toBe(false);
expect(warmupSpy).toHaveBeenCalledTimes(2);
expect(executeSpy).not.toHaveBeenCalled();
await expect(toolExecution).rejects.toThrow("Operation aborted");
await disposeSession;
expect(disposed).toBe(true);
expect(toolExecutionSettled).toBe(true);
expect(executeSpy).not.toHaveBeenCalled();
});
it("rejects eval starts when warmup finishes after dispose begins", async () => {
const { tempDir, cwd } = createTempProject();
tempDirs.push(tempDir);
const blockedWarmupStarted = Promise.withResolvers<void>();
const releaseWarmup = Promise.withResolvers<void>();
const executeSpy = vi.spyOn(pythonExecutor, "executePython").mockResolvedValue({
output: "late",
exitCode: 0,
cancelled: false,
truncated: false,
totalLines: 1,
totalBytes: 4,
outputLines: 1,
outputBytes: 4,
displayOutputs: [],
stdinRequested: false,
});
let warmupCallCount = 0;
const warmupSpy = vi.spyOn(pythonExecutor, "warmPythonEnvironment").mockImplementation(async () => {
warmupCallCount += 1;
if (warmupCallCount === 1) {
return { ok: true };
}
blockedWarmupStarted.resolve();
await releaseWarmup.promise;
return { ok: true };
});
vi.spyOn(pythonKernel, "checkPythonKernelAvailability").mockResolvedValue({ ok: true });
const session = await createSession(tempDir, cwd);
const EvalTool = session.getToolByName("eval");
expect(EvalTool).toBeDefined();
const toolExecution = EvalTool!.execute(
"call-id",
{ input: "```py\nprint('tool after warmup')\n```" },
undefined,
undefined,
undefined,
);
await blockedWarmupStarted.promise;
const disposeSession = session.dispose();
releaseWarmup.resolve();
await expect(toolExecution).rejects.toThrow(
"Python execution is unavailable while session disposal is in progress",
);
await disposeSession;
expect(warmupSpy).toHaveBeenCalledTimes(2);
expect(executeSpy).not.toHaveBeenCalled();
});
it("aborts tracked eval execution during session dispose after warmup completes", async () => {
const { tempDir, cwd } = createTempProject();
tempDirs.push(tempDir);
const warmupSpy = vi.spyOn(pythonExecutor, "warmPythonEnvironment").mockImplementation(async () => {
return { ok: true };
});
const blockedExecuteStarted = Promise.withResolvers<void>();
const executeSpy = vi.spyOn(pythonExecutor, "executePython").mockImplementation(async (_code, options) => {
const signal = options?.signal;
@@ -507,14 +381,12 @@ describe("AgentSession python cleanup", () => {
expect(disposed).toBe(false);
expect(toolExecutionSettled).toBe(false);
expect(warmupSpy).toHaveBeenCalledTimes(2);
expect(executeSpy).toHaveBeenCalledTimes(1);
const [toolResult] = await Promise.all([toolExecution, disposeSession]);
expect(disposed).toBe(true);
expect(toolExecutionSettled).toBe(true);
expect(warmupSpy).toHaveBeenCalledTimes(2);
expect(executeSpy).toHaveBeenCalledTimes(1);
expect(toolResult.details?.isError).toBe(true);
expect(toolResult.content).toContainEqual(
@@ -525,8 +397,6 @@ describe("AgentSession python cleanup", () => {
it("detaches retained kernel ownership even when dispose times out waiting for Python work", async () => {
const { tempDir, cwd } = createTempProject();
tempDirs.push(tempDir);
stubPythonWarmup();
const kernel = new FakeKernel();
const blockedExecution = Promise.withResolvers<typeof OK_EXECUTION>();
const blockedExecutionStarted = Promise.withResolvers<void>();
@@ -583,7 +453,6 @@ describe("AgentSession python cleanup", () => {
it("rejects direct session Python starts once dispose begins", async () => {
const { tempDir, cwd } = createTempProject();
tempDirs.push(tempDir);
stubPythonWarmup();
const executeSpy = vi.spyOn(pythonExecutor, "executePython").mockResolvedValue({
output: "late",
exitCode: 0,
@@ -705,7 +574,6 @@ describe("AgentSession python cleanup", () => {
it("rejects eval starts once dispose begins", async () => {
const { tempDir, cwd } = createTempProject();
tempDirs.push(tempDir);
const warmupSpy = stubPythonWarmup();
const executeSpy = vi.spyOn(pythonExecutor, "executePython").mockResolvedValue({
output: "late",
exitCode: 0,
@@ -727,14 +595,12 @@ describe("AgentSession python cleanup", () => {
EvalTool!.execute("call-id", { input: "```py\nprint('late')\n```" }, undefined, undefined, undefined),
).rejects.toThrow("Python execution is unavailable while session disposal is in progress");
await disposeSession;
expect(warmupSpy).toHaveBeenCalledTimes(1);
expect(executeSpy).not.toHaveBeenCalled();
});
it("rejects eval starts that reach async preflight after dispose begins", async () => {
const { tempDir, cwd } = createTempProject();
tempDirs.push(tempDir);
const warmupSpy = stubPythonWarmup();
const executeSpy = vi.spyOn(pythonExecutor, "executePython").mockResolvedValue({
output: "late",
exitCode: 0,
@@ -771,15 +637,12 @@ describe("AgentSession python cleanup", () => {
releaseArtifact.resolve();
await expect(execution).rejects.toThrow("Python execution is unavailable while session disposal is in progress");
await disposeSession;
expect(warmupSpy).toHaveBeenCalledTimes(1);
expect(executeSpy).not.toHaveBeenCalled();
});
it("aborts every active Python execution owned by the session during dispose", async () => {
const { tempDir, cwd } = createTempProject();
tempDirs.push(tempDir);
stubPythonWarmup();
const kernel = new FakeKernel();
const blockedExecution = Promise.withResolvers<typeof OK_EXECUTION>();
const blockedExecutionStarted = Promise.withResolvers<void>();
@@ -3,7 +3,6 @@ import {
disposeAllKernelSessions,
disposeKernelSessionsByOwner,
executePython,
warmPythonEnvironment,
} from "@oh-my-pi/pi-coding-agent/eval/py/executor";
import * as gatewayCoordinator from "@oh-my-pi/pi-coding-agent/eval/py/gateway-coordinator";
import type {
@@ -13,7 +12,6 @@ import type {
} from "@oh-my-pi/pi-coding-agent/eval/py/kernel";
import * as pythonKernel from "@oh-my-pi/pi-coding-agent/eval/py/kernel";
import { PythonKernel } from "@oh-my-pi/pi-coding-agent/eval/py/kernel";
import { TempDir } from "@oh-my-pi/pi-utils";
const OK_RESULT: KernelExecuteResult = {
status: "ok",
@@ -775,80 +773,6 @@ describe("python executor owner cleanup", () => {
expect(globalKernel.shutdown).toHaveBeenCalledTimes(1);
await globalCleanup;
});
it("attaches cached warmup sessions to newly provided owners", async () => {
using tempDir = TempDir.createSync("@python-owner-warmup-");
const kernel = {
execute: vi.fn(async () => OK_RESULT),
ping: vi.fn(async () => true),
isAlive: () => true,
shutdown: vi.fn(async (): Promise<KernelShutdownResult> => ({ confirmed: true })),
};
vi.spyOn(pythonKernel, "checkPythonKernelAvailability").mockResolvedValue({ ok: true });
const startSpy = vi.spyOn(PythonKernel, "start").mockResolvedValue(kernel as unknown as PythonKernelInstance);
const firstWarmup = await warmPythonEnvironment(tempDir.path(), "warm-session", true, undefined, "owner-a");
expect(firstWarmup.ok).toBe(true);
const cachedWarmup = await warmPythonEnvironment(tempDir.path(), "warm-session", true, undefined, "owner-b");
expect(cachedWarmup.ok).toBe(true);
expect(startSpy).toHaveBeenCalledTimes(1);
await disposeKernelSessionsByOwner("owner-a");
expect(kernel.shutdown).not.toHaveBeenCalled();
await executePython("1 + 1", {
cwd: tempDir.path(),
sessionId: "warm-session",
kernelMode: "session",
kernelOwnerId: "owner-b",
});
expect(startSpy).toHaveBeenCalledTimes(1);
expect(kernel.execute).toHaveBeenCalledTimes(1);
await disposeKernelSessionsByOwner("owner-b");
expect(kernel.shutdown).toHaveBeenCalledTimes(1);
});
it("keeps cache-hit ownerless warmups provisional until an explicit owner takes over", async () => {
using tempDir = TempDir.createSync("@python-owner-fallback-");
const kernel = {
execute: vi.fn(async () => OK_RESULT),
ping: vi.fn(async () => true),
isAlive: () => true,
shutdown: vi.fn(async (): Promise<KernelShutdownResult> => ({ confirmed: true })),
};
vi.spyOn(pythonKernel, "checkPythonKernelAvailability").mockResolvedValue({ ok: true });
const startSpy = vi.spyOn(PythonKernel, "start").mockResolvedValue(kernel as unknown as PythonKernelInstance);
const firstWarmup = await warmPythonEnvironment(tempDir.path(), "warm-fallback-session", true);
expect(firstWarmup.ok).toBe(true);
const cachedWarmup = await warmPythonEnvironment(tempDir.path(), "warm-fallback-session", true);
expect(cachedWarmup.ok).toBe(true);
expect(startSpy).toHaveBeenCalledTimes(1);
await executePython("1 + 1", {
cwd: tempDir.path(),
sessionId: "warm-fallback-session",
kernelMode: "session",
kernelOwnerId: "owner-a",
});
expect(kernel.execute).toHaveBeenCalledTimes(1);
const postTakeoverWarmup = await warmPythonEnvironment(tempDir.path(), "warm-fallback-session", true);
expect(postTakeoverWarmup.ok).toBe(true);
expect(startSpy).toHaveBeenCalledTimes(1);
await disposeKernelSessionsByOwner("owner-a");
expect(kernel.shutdown).toHaveBeenCalledTimes(1);
await disposeKernelSessionsByOwner("warm-fallback-session");
expect(kernel.shutdown).toHaveBeenCalledTimes(1);
});
it("leaves per-call kernels out of owner-scoped retained cleanup and keeps global cleanup intact", async () => {
const perCallKernel = new FakeKernel();
const retainedKernel = new FakeKernel();
@@ -2,8 +2,7 @@ import { describe, expect, it } from "bun:test";
import * as fs from "node:fs";
import * as path from "node:path";
import { Settings } from "@oh-my-pi/pi-coding-agent/config/settings";
import { warmPythonEnvironment } from "@oh-my-pi/pi-coding-agent/eval/py/executor";
import { EvalTool, getEvalToolDescription } from "@oh-my-pi/pi-coding-agent/tools/eval";
import { EvalTool } from "@oh-my-pi/pi-coding-agent/tools/eval";
import { $which, getProjectDir } from "@oh-my-pi/pi-utils";
const resolvePythonPath = (): string | null => {
@@ -89,12 +88,4 @@ ${code}
expect(tool.description).toContain("read");
expect(tool.description).not.toContain("Documentation unavailable");
});
it("renders prelude docs in eval tool description", async () => {
const result = await warmPythonEnvironment(getProjectDir());
expect(result.ok).toBe(true);
const description = getEvalToolDescription();
expect(description).toContain("read");
expect(description).not.toContain("Documentation unavailable");
});
});