@oh-my-pi/pi-coding-agent 15.0.2 → 15.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +56 -1
- package/examples/custom-tools/README.md +11 -7
- package/examples/custom-tools/hello/index.ts +2 -2
- package/examples/extensions/README.md +19 -8
- package/examples/extensions/api-demo.ts +15 -19
- package/examples/extensions/hello.ts +5 -6
- package/examples/extensions/plan-mode.ts +1 -1
- package/examples/extensions/reload-runtime.ts +4 -3
- package/examples/extensions/with-deps/index.ts +4 -3
- package/examples/sdk/06-extensions.ts +4 -2
- package/package.json +7 -17
- package/src/autoresearch/tools/init-experiment.ts +38 -41
- package/src/autoresearch/tools/log-experiment.ts +32 -41
- package/src/autoresearch/tools/run-experiment.ts +3 -3
- package/src/autoresearch/tools/update-notes.ts +11 -11
- package/src/commit/agentic/tools/analyze-file.ts +4 -4
- package/src/commit/agentic/tools/git-file-diff.ts +4 -4
- package/src/commit/agentic/tools/git-hunk.ts +5 -5
- package/src/commit/agentic/tools/git-overview.ts +4 -4
- package/src/commit/agentic/tools/propose-changelog.ts +13 -13
- package/src/commit/agentic/tools/propose-commit.ts +6 -6
- package/src/commit/agentic/tools/recent-commits.ts +3 -3
- package/src/commit/agentic/tools/schemas.ts +28 -28
- package/src/commit/agentic/tools/split-commit.ts +22 -21
- package/src/commit/analysis/summary.ts +4 -4
- package/src/commit/changelog/generate.ts +7 -11
- package/src/commit/shared-llm.ts +22 -34
- package/src/config/config-file.ts +35 -13
- package/src/config/model-registry.ts +9 -190
- package/src/config/models-config-schema.ts +166 -0
- package/src/config/settings-schema.ts +18 -0
- package/src/edit/index.ts +2 -2
- package/src/edit/modes/apply-patch.ts +7 -6
- package/src/edit/modes/patch.ts +18 -25
- package/src/edit/modes/replace.ts +18 -20
- package/src/eval/js/shared/rewrite-imports.ts +131 -10
- package/src/eval/py/executor.ts +233 -623
- package/src/eval/py/kernel.ts +27 -2
- package/src/exa/factory.ts +5 -4
- package/src/exa/mcp-client.ts +1 -1
- package/src/exa/researcher.ts +9 -20
- package/src/exa/search.ts +26 -52
- package/src/exa/types.ts +1 -1
- package/src/exa/websets.ts +54 -53
- package/src/exec/bash-executor.ts +2 -1
- package/src/extensibility/custom-commands/loader.ts +5 -3
- package/src/extensibility/custom-commands/types.ts +4 -2
- package/src/extensibility/custom-tools/loader.ts +5 -3
- package/src/extensibility/custom-tools/types.ts +7 -6
- package/src/extensibility/custom-tools/wrapper.ts +1 -1
- package/src/extensibility/extensions/loader.ts +7 -3
- package/src/extensibility/extensions/types.ts +9 -5
- package/src/extensibility/extensions/wrapper.ts +1 -2
- package/src/extensibility/hooks/loader.ts +3 -1
- package/src/extensibility/hooks/tool-wrapper.ts +1 -1
- package/src/extensibility/hooks/types.ts +4 -2
- package/src/extensibility/plugins/legacy-pi-compat.ts +30 -0
- package/src/extensibility/shared-events.ts +1 -1
- package/src/extensibility/typebox.ts +391 -0
- package/src/goals/tools/goal-tool.ts +6 -12
- package/src/hashline/types.ts +4 -4
- package/src/hindsight/state.ts +2 -2
- package/src/index.ts +0 -2
- package/src/internal-urls/docs-index.generated.ts +7 -7
- package/src/lsp/types.ts +30 -38
- package/src/mcp/manager.ts +1 -1
- package/src/mcp/tool-bridge.ts +1 -1
- package/src/modes/components/session-observer-overlay.ts +12 -1
- package/src/modes/components/status-line/segments.ts +2 -1
- package/src/modes/controllers/command-controller.ts +27 -2
- package/src/modes/controllers/event-controller.ts +3 -4
- package/src/modes/interactive-mode.ts +1 -1
- package/src/modes/rpc/host-tools.ts +1 -1
- package/src/modes/rpc/rpc-client.ts +1 -1
- package/src/modes/rpc/rpc-types.ts +1 -1
- package/src/modes/theme/theme.ts +111 -117
- package/src/modes/types.ts +1 -1
- package/src/modes/utils/context-usage.ts +2 -2
- package/src/sdk.ts +31 -8
- package/src/session/agent-session.ts +74 -104
- package/src/session/messages.ts +16 -51
- package/src/session/session-manager.ts +22 -2
- package/src/session/streaming-output.ts +16 -6
- package/src/task/executor.ts +208 -86
- package/src/task/index.ts +15 -11
- package/src/task/render.ts +32 -5
- package/src/task/types.ts +54 -39
- package/src/tools/ask.ts +12 -12
- package/src/tools/ast-edit.ts +11 -15
- package/src/tools/ast-grep.ts +9 -10
- package/src/tools/bash.ts +9 -23
- package/src/tools/browser.ts +39 -53
- package/src/tools/calculator.ts +12 -11
- package/src/tools/checkpoint.ts +7 -7
- package/src/tools/debug.ts +40 -43
- package/src/tools/eval.ts +6 -8
- package/src/tools/find.ts +10 -13
- package/src/tools/gh.ts +71 -128
- package/src/tools/hindsight-recall.ts +4 -6
- package/src/tools/hindsight-reflect.ts +5 -5
- package/src/tools/hindsight-retain.ts +15 -17
- package/src/tools/image-gen.ts +31 -81
- package/src/tools/index.ts +4 -1
- package/src/tools/inspect-image.ts +8 -9
- package/src/tools/irc.ts +15 -27
- package/src/tools/job.ts +14 -21
- package/src/tools/read.ts +7 -8
- package/src/tools/recipe/index.ts +7 -9
- package/src/tools/render-mermaid.ts +12 -12
- package/src/tools/report-tool-issue.ts +4 -4
- package/src/tools/resolve.ts +11 -11
- package/src/tools/review.ts +14 -26
- package/src/tools/search-tool-bm25.ts +7 -9
- package/src/tools/search.ts +19 -22
- package/src/tools/ssh.ts +7 -7
- package/src/tools/todo-write.ts +26 -34
- package/src/tools/vim.ts +10 -26
- package/src/tools/write.ts +5 -5
- package/src/tools/yield.ts +100 -54
- package/src/web/search/index.ts +9 -24
- package/src/prompts/compaction/branch-summary-context.md +0 -5
- package/src/prompts/compaction/branch-summary-preamble.md +0 -2
- package/src/prompts/compaction/branch-summary.md +0 -30
- package/src/prompts/compaction/compaction-short-summary.md +0 -9
- package/src/prompts/compaction/compaction-summary-context.md +0 -5
- package/src/prompts/compaction/compaction-summary.md +0 -38
- package/src/prompts/compaction/compaction-turn-prefix.md +0 -17
- package/src/prompts/compaction/compaction-update-summary.md +0 -45
- package/src/prompts/system/auto-handoff-threshold-focus.md +0 -1
- package/src/prompts/system/file-operations.md +0 -10
- package/src/prompts/system/handoff-document.md +0 -49
- package/src/prompts/system/summarization-system.md +0 -3
- package/src/session/compaction/branch-summarization.ts +0 -324
- package/src/session/compaction/compaction.ts +0 -1420
- package/src/session/compaction/errors.ts +0 -31
- package/src/session/compaction/index.ts +0 -8
- package/src/session/compaction/pruning.ts +0 -91
- package/src/session/compaction/utils.ts +0 -184
package/src/eval/py/executor.ts
CHANGED
|
@@ -13,11 +13,6 @@ import {
|
|
|
13
13
|
} from "./kernel";
|
|
14
14
|
import { ensurePyToolBridge, registerPyToolBridge } from "./tool-bridge";
|
|
15
15
|
|
|
16
|
-
const IDLE_TIMEOUT_MS = 5 * 60 * 1000; // 5 minutes
|
|
17
|
-
const MAX_KERNEL_SESSIONS = 4;
|
|
18
|
-
const CLEANUP_INTERVAL_MS = 30 * 1000; // 30 seconds
|
|
19
|
-
const OWNER_CLEANUP_KERNEL_SHUTDOWN_TIMEOUT_MS = 2_000;
|
|
20
|
-
|
|
21
16
|
export type PythonKernelMode = "session" | "per-call";
|
|
22
17
|
|
|
23
18
|
export interface PythonExecutorOptions {
|
|
@@ -94,42 +89,27 @@ export interface PythonResult {
|
|
|
94
89
|
stdinRequested: boolean;
|
|
95
90
|
}
|
|
96
91
|
|
|
97
|
-
|
|
98
|
-
|
|
92
|
+
// ---------------------------------------------------------------------------
|
|
93
|
+
// Session bookkeeping
|
|
94
|
+
//
|
|
95
|
+
// One PythonKernel subprocess per session id. Sessions are reused until they
|
|
96
|
+
// die or are explicitly disposed. Multiple agent owners can register against
|
|
97
|
+
// the same session id; the kernel stays alive until the last owner detaches.
|
|
98
|
+
// ---------------------------------------------------------------------------
|
|
99
|
+
|
|
100
|
+
interface PythonSession {
|
|
101
|
+
sessionId: string;
|
|
99
102
|
kernel: PythonKernel;
|
|
100
|
-
queue: Promise<void>;
|
|
101
|
-
restartCount: number;
|
|
102
|
-
dead: boolean;
|
|
103
|
-
needsRestart: boolean;
|
|
104
|
-
disposing: boolean;
|
|
105
|
-
disposeCapacityPromise?: Promise<void>;
|
|
106
|
-
resolveDisposeCapacity?: () => void;
|
|
107
|
-
disposeAttemptPromise?: Promise<void>;
|
|
108
|
-
resolveDisposeAttempt?: () => void;
|
|
109
|
-
disposeResultPromise?: Promise<KernelDisposalResult>;
|
|
110
|
-
disposeResultTimeoutMs?: number;
|
|
111
|
-
nextDisposalRetryAt?: number;
|
|
112
|
-
lastUsedAt: number;
|
|
113
103
|
ownerIds: Set<string>;
|
|
114
104
|
hasFallbackOwner: boolean;
|
|
115
|
-
|
|
105
|
+
queue: Promise<void>;
|
|
116
106
|
}
|
|
117
107
|
|
|
118
|
-
const
|
|
119
|
-
const disposingKernelSessions = new Set<KernelSession>();
|
|
120
|
-
let cleanupTimer: NodeJS.Timeout | null = null;
|
|
108
|
+
const sessions = new Map<string, PythonSession>();
|
|
121
109
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
signal?: AbortSignal;
|
|
126
|
-
deadlineMs?: number;
|
|
127
|
-
kernelOwnerId?: string;
|
|
128
|
-
/** Bridge session identifier exported into the kernel env as PI_TOOL_BRIDGE_SESSION. */
|
|
129
|
-
bridgeSessionId?: string;
|
|
130
|
-
/** Cached bridge connection info. When present, env vars for tool.<name>() get injected. */
|
|
131
|
-
bridge?: { url: string; token: string };
|
|
132
|
-
}
|
|
110
|
+
// ---------------------------------------------------------------------------
|
|
111
|
+
// Cancellation plumbing
|
|
112
|
+
// ---------------------------------------------------------------------------
|
|
133
113
|
|
|
134
114
|
class PythonExecutionCancelledError extends Error {
|
|
135
115
|
readonly timedOut: boolean;
|
|
@@ -147,29 +127,6 @@ function getExecutionDeadlineMs(options?: Pick<PythonExecutorOptions, "deadlineM
|
|
|
147
127
|
return Date.now() + options.timeoutMs;
|
|
148
128
|
}
|
|
149
129
|
|
|
150
|
-
/**
|
|
151
|
-
* Build the env block exposed to the Python kernel. Includes the session file
|
|
152
|
-
* (for things that need the raw session path) and the effective artifacts
|
|
153
|
-
* directory (preferred by the prelude when resolving output IDs, so subagents
|
|
154
|
-
* see the parent's flat dir instead of a non-existent sibling).
|
|
155
|
-
*/
|
|
156
|
-
function buildKernelEnv(options: {
|
|
157
|
-
sessionFile?: string;
|
|
158
|
-
artifactsDir?: string;
|
|
159
|
-
bridgeSessionId?: string;
|
|
160
|
-
bridge?: { url: string; token: string };
|
|
161
|
-
}): Record<string, string> | undefined {
|
|
162
|
-
const env: Record<string, string> = {};
|
|
163
|
-
if (options.sessionFile) env.PI_SESSION_FILE = options.sessionFile;
|
|
164
|
-
if (options.artifactsDir) env.PI_ARTIFACTS_DIR = options.artifactsDir;
|
|
165
|
-
if (options.bridge && options.bridgeSessionId) {
|
|
166
|
-
env.PI_TOOL_BRIDGE_URL = options.bridge.url;
|
|
167
|
-
env.PI_TOOL_BRIDGE_TOKEN = options.bridge.token;
|
|
168
|
-
env.PI_TOOL_BRIDGE_SESSION = options.bridgeSessionId;
|
|
169
|
-
}
|
|
170
|
-
return Object.keys(env).length > 0 ? env : undefined;
|
|
171
|
-
}
|
|
172
|
-
|
|
173
130
|
function getRemainingTimeoutMs(deadlineMs?: number): number | undefined {
|
|
174
131
|
if (deadlineMs === undefined) return undefined;
|
|
175
132
|
return deadlineMs - Date.now();
|
|
@@ -203,62 +160,48 @@ function isTimedOutCancellation(error: unknown, signal?: AbortSignal): boolean {
|
|
|
203
160
|
|
|
204
161
|
async function waitForPromiseWithCancellation<T>(
|
|
205
162
|
promise: Promise<T>,
|
|
206
|
-
options: Pick<
|
|
163
|
+
options: Pick<PythonExecutorOptions, "signal" | "deadlineMs">,
|
|
207
164
|
): Promise<T> {
|
|
208
165
|
if (options.signal?.aborted) {
|
|
209
166
|
throw new PythonExecutionCancelledError(isTimedOutCancellation(options.signal.reason, options.signal));
|
|
210
167
|
}
|
|
211
|
-
|
|
212
168
|
const remainingMs = getRemainingTimeoutMs(options.deadlineMs);
|
|
213
169
|
if (remainingMs !== undefined && remainingMs <= 0) {
|
|
214
170
|
throw new PythonExecutionCancelledError(true);
|
|
215
171
|
}
|
|
216
|
-
|
|
217
172
|
if (!options.signal && remainingMs === undefined) {
|
|
218
173
|
return await promise;
|
|
219
174
|
}
|
|
220
175
|
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
const onAbort = () => {
|
|
176
|
+
const { promise: resultPromise, resolve, reject } = Promise.withResolvers<T>();
|
|
177
|
+
const cleanups: Array<() => void> = [];
|
|
178
|
+
const finish = (cb: () => void): void => {
|
|
179
|
+
while (cleanups.length > 0) cleanups.pop()?.();
|
|
180
|
+
cb();
|
|
181
|
+
};
|
|
182
|
+
if (options.signal) {
|
|
183
|
+
const onAbort = (): void =>
|
|
231
184
|
finish(() =>
|
|
232
185
|
reject(new PythonExecutionCancelledError(isTimedOutCancellation(options.signal?.reason, options.signal))),
|
|
233
186
|
);
|
|
234
|
-
};
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
promise.then(
|
|
250
|
-
value => finish(() => resolve(value)),
|
|
251
|
-
error => finish(() => reject(error)),
|
|
252
|
-
);
|
|
253
|
-
});
|
|
187
|
+
options.signal.addEventListener("abort", onAbort, { once: true });
|
|
188
|
+
cleanups.push(() => options.signal?.removeEventListener("abort", onAbort));
|
|
189
|
+
}
|
|
190
|
+
if (remainingMs !== undefined) {
|
|
191
|
+
const timer = setTimeout(() => finish(() => reject(new PythonExecutionCancelledError(true))), remainingMs);
|
|
192
|
+
timer.unref();
|
|
193
|
+
cleanups.push(() => clearTimeout(timer));
|
|
194
|
+
}
|
|
195
|
+
promise.then(
|
|
196
|
+
value => finish(() => resolve(value)),
|
|
197
|
+
err => finish(() => reject(err)),
|
|
198
|
+
);
|
|
199
|
+
return await resultPromise;
|
|
254
200
|
}
|
|
255
201
|
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
): Promise<void> {
|
|
260
|
-
await waitForPromiseWithCancellation(queue, options);
|
|
261
|
-
}
|
|
202
|
+
// ---------------------------------------------------------------------------
|
|
203
|
+
// Result formatting
|
|
204
|
+
// ---------------------------------------------------------------------------
|
|
262
205
|
|
|
263
206
|
function formatTimeoutAnnotation(timeoutMs?: number): string | undefined {
|
|
264
207
|
if (timeoutMs === undefined) return "Command timed out";
|
|
@@ -284,534 +227,172 @@ function createCancelledPythonResult(timedOut: boolean, timeoutMs?: number): Pyt
|
|
|
284
227
|
};
|
|
285
228
|
}
|
|
286
229
|
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
options: KernelSessionExecutionOptions,
|
|
291
|
-
) {
|
|
292
|
-
return {
|
|
293
|
-
cwd,
|
|
294
|
-
env,
|
|
295
|
-
signal: options.signal,
|
|
296
|
-
deadlineMs: options.deadlineMs,
|
|
297
|
-
};
|
|
298
|
-
}
|
|
230
|
+
// ---------------------------------------------------------------------------
|
|
231
|
+
// Kernel start helpers
|
|
232
|
+
// ---------------------------------------------------------------------------
|
|
299
233
|
|
|
300
|
-
function
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
234
|
+
function buildKernelEnv(options: {
|
|
235
|
+
sessionFile?: string;
|
|
236
|
+
artifactsDir?: string;
|
|
237
|
+
bridgeSessionId?: string;
|
|
238
|
+
bridge?: { url: string; token: string };
|
|
239
|
+
}): Record<string, string> | undefined {
|
|
240
|
+
const env: Record<string, string> = {};
|
|
241
|
+
if (options.sessionFile) env.PI_SESSION_FILE = options.sessionFile;
|
|
242
|
+
if (options.artifactsDir) env.PI_ARTIFACTS_DIR = options.artifactsDir;
|
|
243
|
+
if (options.bridge && options.bridgeSessionId) {
|
|
244
|
+
env.PI_TOOL_BRIDGE_URL = options.bridge.url;
|
|
245
|
+
env.PI_TOOL_BRIDGE_TOKEN = options.bridge.token;
|
|
246
|
+
env.PI_TOOL_BRIDGE_SESSION = options.bridgeSessionId;
|
|
247
|
+
}
|
|
248
|
+
return Object.keys(env).length > 0 ? env : undefined;
|
|
306
249
|
}
|
|
307
250
|
|
|
308
|
-
function
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
251
|
+
async function startKernel(cwd: string, options: PythonExecutorOptions): Promise<PythonKernel> {
|
|
252
|
+
requireRemainingTimeoutMs(options.deadlineMs);
|
|
253
|
+
return await PythonKernel.start({
|
|
254
|
+
cwd,
|
|
255
|
+
env: buildKernelEnv(options),
|
|
256
|
+
signal: options.signal,
|
|
257
|
+
deadlineMs: options.deadlineMs,
|
|
258
|
+
});
|
|
313
259
|
}
|
|
314
260
|
|
|
315
|
-
function
|
|
316
|
-
const session = kernelSessions.get(sessionId);
|
|
317
|
-
if (!session || session.disposing) return false;
|
|
261
|
+
function attachOwner(session: PythonSession, sessionId: string, ownerId: string | undefined): void {
|
|
318
262
|
if (ownerId !== undefined) {
|
|
319
263
|
if (session.hasFallbackOwner) {
|
|
320
264
|
session.ownerIds.delete(sessionId);
|
|
321
265
|
session.hasFallbackOwner = false;
|
|
322
266
|
}
|
|
323
267
|
session.ownerIds.add(ownerId);
|
|
324
|
-
} else if (session.hasFallbackOwner || session.ownerIds.size === 0) {
|
|
325
|
-
session.ownerIds.add(sessionId);
|
|
326
|
-
session.hasFallbackOwner = true;
|
|
327
|
-
}
|
|
328
|
-
session.lastUsedAt = Date.now();
|
|
329
|
-
return true;
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
function getRetainedKernelSessionCount(): number {
|
|
333
|
-
return kernelSessions.size + disposingKernelSessions.size;
|
|
334
|
-
}
|
|
335
|
-
|
|
336
|
-
function syncCleanupTimer(): void {
|
|
337
|
-
if (kernelSessions.size === 0 && disposingKernelSessions.size === 0) {
|
|
338
|
-
stopCleanupTimer();
|
|
339
|
-
return;
|
|
340
|
-
}
|
|
341
|
-
startCleanupTimer();
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
function retryPendingKernelSessionDisposals(now: number = Date.now()): void {
|
|
345
|
-
for (const session of disposingKernelSessions.values()) {
|
|
346
|
-
if (session.disposeResultPromise) continue;
|
|
347
|
-
if (session.nextDisposalRetryAt !== undefined && session.nextDisposalRetryAt > now) continue;
|
|
348
|
-
session.nextDisposalRetryAt = undefined;
|
|
349
|
-
void disposeKernelSession(session);
|
|
350
|
-
}
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
function beginDisposingKernelSession(session: KernelSession): boolean {
|
|
354
|
-
if (session.disposing) return false;
|
|
355
|
-
session.disposing = true;
|
|
356
|
-
disposingKernelSessions.add(session);
|
|
357
|
-
if (kernelSessions.get(session.id) === session) {
|
|
358
|
-
kernelSessions.delete(session.id);
|
|
359
|
-
}
|
|
360
|
-
if (session.heartbeatTimer) {
|
|
361
|
-
clearInterval(session.heartbeatTimer);
|
|
362
|
-
session.heartbeatTimer = undefined;
|
|
363
|
-
}
|
|
364
|
-
syncCleanupTimer();
|
|
365
|
-
return true;
|
|
366
|
-
}
|
|
367
|
-
|
|
368
|
-
function finishDisposingKernelSession(session: KernelSession): void {
|
|
369
|
-
disposingKernelSessions.delete(session);
|
|
370
|
-
session.resolveDisposeCapacity?.();
|
|
371
|
-
session.resolveDisposeCapacity = undefined;
|
|
372
|
-
session.disposeCapacityPromise = undefined;
|
|
373
|
-
session.resolveDisposeAttempt = undefined;
|
|
374
|
-
session.disposeAttemptPromise = undefined;
|
|
375
|
-
session.disposeResultPromise = undefined;
|
|
376
|
-
session.disposeResultTimeoutMs = undefined;
|
|
377
|
-
session.nextDisposalRetryAt = undefined;
|
|
378
|
-
syncCleanupTimer();
|
|
379
|
-
}
|
|
380
|
-
|
|
381
|
-
async function waitForDisposalCapacity(
|
|
382
|
-
options: Pick<KernelSessionExecutionOptions, "signal" | "deadlineMs">,
|
|
383
|
-
): Promise<void> {
|
|
384
|
-
retryPendingKernelSessionDisposals();
|
|
385
|
-
|
|
386
|
-
const disposalPromises: Promise<void>[] = [];
|
|
387
|
-
let nextRetryAt: number | undefined;
|
|
388
|
-
for (const session of disposingKernelSessions.values()) {
|
|
389
|
-
if (session.disposeCapacityPromise) {
|
|
390
|
-
disposalPromises.push(
|
|
391
|
-
session.disposeCapacityPromise.then(
|
|
392
|
-
() => undefined,
|
|
393
|
-
() => undefined,
|
|
394
|
-
),
|
|
395
|
-
);
|
|
396
|
-
}
|
|
397
|
-
if (session.disposeAttemptPromise) {
|
|
398
|
-
disposalPromises.push(
|
|
399
|
-
session.disposeAttemptPromise.then(
|
|
400
|
-
() => undefined,
|
|
401
|
-
() => undefined,
|
|
402
|
-
),
|
|
403
|
-
);
|
|
404
|
-
}
|
|
405
|
-
if (session.nextDisposalRetryAt !== undefined) {
|
|
406
|
-
nextRetryAt =
|
|
407
|
-
nextRetryAt === undefined
|
|
408
|
-
? session.nextDisposalRetryAt
|
|
409
|
-
: Math.min(nextRetryAt, session.nextDisposalRetryAt);
|
|
410
|
-
}
|
|
411
|
-
}
|
|
412
|
-
if (disposalPromises.length > 0) {
|
|
413
|
-
await waitForPromiseWithCancellation(Promise.race(disposalPromises), options);
|
|
414
268
|
return;
|
|
415
269
|
}
|
|
416
|
-
if (
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
options,
|
|
420
|
-
);
|
|
421
|
-
}
|
|
422
|
-
|
|
423
|
-
async function ensureKernelSessionCapacity(
|
|
424
|
-
options: Pick<KernelSessionExecutionOptions, "signal" | "deadlineMs">,
|
|
425
|
-
): Promise<void> {
|
|
426
|
-
while (getRetainedKernelSessionCount() >= MAX_KERNEL_SESSIONS) {
|
|
427
|
-
if (disposingKernelSessions.size > 0) {
|
|
428
|
-
await waitForDisposalCapacity(options);
|
|
429
|
-
continue;
|
|
430
|
-
}
|
|
431
|
-
if (kernelSessions.size === 0) {
|
|
432
|
-
await waitForDisposalCapacity(options);
|
|
433
|
-
continue;
|
|
434
|
-
}
|
|
435
|
-
await evictOldestSession();
|
|
436
|
-
}
|
|
437
|
-
}
|
|
438
|
-
|
|
439
|
-
async function cleanupIdleSessions(): Promise<void> {
|
|
440
|
-
const now = Date.now();
|
|
441
|
-
const toDispose: KernelSession[] = [];
|
|
442
|
-
|
|
443
|
-
for (const session of kernelSessions.values()) {
|
|
444
|
-
if (session.dead || now - session.lastUsedAt > IDLE_TIMEOUT_MS) {
|
|
445
|
-
toDispose.push(session);
|
|
446
|
-
}
|
|
447
|
-
}
|
|
448
|
-
|
|
449
|
-
if (toDispose.length > 0) {
|
|
450
|
-
logger.debug("Cleaning up idle kernel sessions", { count: toDispose.length });
|
|
451
|
-
await Promise.allSettled(toDispose.map(session => disposeKernelSession(session)));
|
|
452
|
-
}
|
|
453
|
-
|
|
454
|
-
retryPendingKernelSessionDisposals(now);
|
|
455
|
-
syncCleanupTimer();
|
|
456
|
-
}
|
|
457
|
-
|
|
458
|
-
async function evictOldestSession(): Promise<void> {
|
|
459
|
-
let oldest: KernelSession | null = null;
|
|
460
|
-
for (const session of kernelSessions.values()) {
|
|
461
|
-
if (!oldest || session.lastUsedAt < oldest.lastUsedAt) {
|
|
462
|
-
oldest = session;
|
|
463
|
-
}
|
|
464
|
-
}
|
|
465
|
-
if (oldest) {
|
|
466
|
-
logger.debug("Evicting oldest kernel session", { id: oldest.id });
|
|
467
|
-
await disposeKernelSession(oldest);
|
|
468
|
-
}
|
|
469
|
-
}
|
|
470
|
-
|
|
471
|
-
export async function disposeAllKernelSessions(): Promise<void> {
|
|
472
|
-
stopCleanupTimer();
|
|
473
|
-
const sessions = Array.from(new Set([...kernelSessions.values(), ...disposingKernelSessions.values()]));
|
|
474
|
-
await Promise.allSettled(sessions.map(session => disposeKernelSession(session)));
|
|
475
|
-
}
|
|
476
|
-
|
|
477
|
-
export async function disposeKernelSessionsByOwner(ownerId: string): Promise<void> {
|
|
478
|
-
const sessionsToDispose: KernelSession[] = [];
|
|
479
|
-
for (const session of new Set([...kernelSessions.values(), ...disposingKernelSessions.values()])) {
|
|
480
|
-
if (!session.ownerIds.delete(ownerId)) continue;
|
|
481
|
-
if (session.ownerIds.size === 0) {
|
|
482
|
-
sessionsToDispose.push(session);
|
|
483
|
-
}
|
|
270
|
+
if (session.hasFallbackOwner || session.ownerIds.size === 0) {
|
|
271
|
+
session.ownerIds.add(sessionId);
|
|
272
|
+
session.hasFallbackOwner = true;
|
|
484
273
|
}
|
|
485
|
-
await Promise.allSettled(
|
|
486
|
-
sessionsToDispose.map(session => disposeKernelSession(session, OWNER_CLEANUP_KERNEL_SHUTDOWN_TIMEOUT_MS)),
|
|
487
|
-
);
|
|
488
|
-
syncCleanupTimer();
|
|
489
274
|
}
|
|
490
275
|
|
|
491
|
-
async function
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
if (!availability.ok) {
|
|
497
|
-
throw new Error(availability.reason ?? "Python kernel unavailable");
|
|
276
|
+
async function acquireSession(sessionId: string, cwd: string, options: PythonExecutorOptions): Promise<PythonSession> {
|
|
277
|
+
const existing = sessions.get(sessionId);
|
|
278
|
+
if (existing) {
|
|
279
|
+
attachOwner(existing, sessionId, options.kernelOwnerId);
|
|
280
|
+
return existing;
|
|
498
281
|
}
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
if (session.heartbeatTimer) return;
|
|
503
|
-
session.heartbeatTimer = setInterval(() => {
|
|
504
|
-
if (session.dead || session.needsRestart) return;
|
|
505
|
-
if (!session.kernel.isAlive()) {
|
|
506
|
-
session.dead = true;
|
|
507
|
-
}
|
|
508
|
-
}, 5000);
|
|
509
|
-
session.heartbeatTimer.unref();
|
|
510
|
-
}
|
|
511
|
-
|
|
512
|
-
async function createKernelSession(
|
|
513
|
-
sessionId: string,
|
|
514
|
-
cwd: string,
|
|
515
|
-
options: KernelSessionExecutionOptions = {},
|
|
516
|
-
): Promise<KernelSession> {
|
|
517
|
-
requireRemainingTimeoutMs(options.deadlineMs);
|
|
518
|
-
const env = buildKernelEnv(options);
|
|
519
|
-
const startOptions = buildKernelStartOptions(cwd, env, options);
|
|
520
|
-
|
|
521
|
-
const kernel = await logger.time("createKernelSession:PythonKernel.start", PythonKernel.start, startOptions);
|
|
522
|
-
|
|
523
|
-
const hasFallbackOwner = options.kernelOwnerId === undefined;
|
|
524
|
-
const initialOwnerId = options.kernelOwnerId ?? sessionId;
|
|
525
|
-
const session: KernelSession = {
|
|
526
|
-
id: sessionId,
|
|
282
|
+
const kernel = await startKernel(cwd, options);
|
|
283
|
+
const session: PythonSession = {
|
|
284
|
+
sessionId,
|
|
527
285
|
kernel,
|
|
286
|
+
ownerIds: new Set(),
|
|
287
|
+
hasFallbackOwner: false,
|
|
528
288
|
queue: Promise.resolve(),
|
|
529
|
-
restartCount: 0,
|
|
530
|
-
dead: false,
|
|
531
|
-
needsRestart: false,
|
|
532
|
-
disposing: false,
|
|
533
|
-
disposeResultPromise: undefined,
|
|
534
|
-
nextDisposalRetryAt: undefined,
|
|
535
|
-
lastUsedAt: Date.now(),
|
|
536
|
-
ownerIds: new Set([initialOwnerId]),
|
|
537
|
-
hasFallbackOwner,
|
|
538
289
|
};
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
290
|
+
attachOwner(session, sessionId, options.kernelOwnerId);
|
|
291
|
+
sessions.set(sessionId, session);
|
|
542
292
|
return session;
|
|
543
293
|
}
|
|
544
294
|
|
|
545
|
-
async function
|
|
546
|
-
session:
|
|
295
|
+
async function replaceSessionKernel(
|
|
296
|
+
session: PythonSession,
|
|
547
297
|
cwd: string,
|
|
548
|
-
options:
|
|
298
|
+
options: PythonExecutorOptions,
|
|
549
299
|
): Promise<void> {
|
|
550
|
-
session.
|
|
551
|
-
|
|
552
|
-
|
|
300
|
+
const old = session.kernel;
|
|
301
|
+
const remaining = getRemainingTimeoutMs(options.deadlineMs);
|
|
302
|
+
await old
|
|
303
|
+
.shutdown(remaining !== undefined ? { timeoutMs: Math.max(0, remaining) } : undefined)
|
|
304
|
+
.catch(() => undefined);
|
|
305
|
+
if (sessions.get(session.sessionId) !== session) {
|
|
306
|
+
throw new PythonExecutionCancelledError(false);
|
|
553
307
|
}
|
|
554
308
|
requireRemainingTimeoutMs(options.deadlineMs);
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
if (!shutdownResult.confirmed && !deadKernel) {
|
|
560
|
-
throw new Error("Failed to confirm crashed kernel shutdown before restart");
|
|
561
|
-
}
|
|
562
|
-
if (!shutdownResult.confirmed) {
|
|
563
|
-
logger.warn("Proceeding with retained kernel restart after unconfirmed dead-kernel shutdown", {
|
|
564
|
-
sessionId: session.id,
|
|
565
|
-
});
|
|
566
|
-
}
|
|
567
|
-
const env = buildKernelEnv(options);
|
|
568
|
-
const startOptions = buildKernelStartOptions(cwd, env, options);
|
|
569
|
-
const kernel = await PythonKernel.start(startOptions);
|
|
570
|
-
session.kernel = kernel;
|
|
571
|
-
session.dead = false;
|
|
572
|
-
session.needsRestart = false;
|
|
573
|
-
session.lastUsedAt = Date.now();
|
|
574
|
-
ensureKernelHeartbeat(session);
|
|
575
|
-
} catch (err) {
|
|
576
|
-
session.restartCount = 0;
|
|
577
|
-
logger.warn("Failed to restart kernel", { error: err instanceof Error ? err.message : String(err) });
|
|
578
|
-
throw err;
|
|
309
|
+
const next = await startKernel(cwd, options);
|
|
310
|
+
if (sessions.get(session.sessionId) !== session) {
|
|
311
|
+
await next.shutdown().catch(() => undefined);
|
|
312
|
+
throw new PythonExecutionCancelledError(false);
|
|
579
313
|
}
|
|
314
|
+
session.kernel = next;
|
|
580
315
|
}
|
|
581
316
|
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
.then(() => session.kernel.shutdown(timeoutMs === undefined ? undefined : { timeoutMs }))
|
|
588
|
-
.then(
|
|
589
|
-
result => (result.confirmed ? { status: "confirmed" as const } : { status: "unconfirmed" as const }),
|
|
590
|
-
(err: unknown) => ({ status: "failed" as const, err }),
|
|
591
|
-
);
|
|
317
|
+
async function resetSession(sessionId: string): Promise<void> {
|
|
318
|
+
const existing = sessions.get(sessionId);
|
|
319
|
+
if (!existing) return;
|
|
320
|
+
sessions.delete(sessionId);
|
|
321
|
+
await existing.kernel.shutdown().catch(() => undefined);
|
|
592
322
|
}
|
|
593
323
|
|
|
594
|
-
function
|
|
595
|
-
session:
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
}
|
|
613
|
-
});
|
|
614
|
-
const disposalAttemptPromise = disposeResultPromise.finally(() => {
|
|
615
|
-
releaseDisposalAttempt.resolve();
|
|
616
|
-
if (session.disposeResultPromise === disposalAttemptPromise) {
|
|
617
|
-
session.disposeResultPromise = undefined;
|
|
618
|
-
session.disposeResultTimeoutMs = undefined;
|
|
619
|
-
}
|
|
620
|
-
if (session.disposeAttemptPromise === releaseDisposalAttempt.promise) {
|
|
621
|
-
session.disposeAttemptPromise = undefined;
|
|
622
|
-
session.resolveDisposeAttempt = undefined;
|
|
623
|
-
}
|
|
624
|
-
});
|
|
625
|
-
session.disposeResultPromise = disposalAttemptPromise;
|
|
626
|
-
}
|
|
627
|
-
return session.disposeResultPromise;
|
|
628
|
-
}
|
|
629
|
-
|
|
630
|
-
async function waitForKernelSessionDisposal(
|
|
631
|
-
session: KernelSession,
|
|
632
|
-
timeoutMs?: number,
|
|
633
|
-
): Promise<KernelDisposalWaitResult | undefined> {
|
|
634
|
-
const disposeResultPromise = session.disposeResultPromise;
|
|
635
|
-
if (!disposeResultPromise) {
|
|
636
|
-
return undefined;
|
|
637
|
-
}
|
|
638
|
-
if (timeoutMs === undefined) {
|
|
639
|
-
return await disposeResultPromise;
|
|
640
|
-
}
|
|
641
|
-
|
|
642
|
-
let timeoutId: NodeJS.Timeout | undefined;
|
|
643
|
-
const result = await Promise.race([
|
|
644
|
-
disposeResultPromise,
|
|
645
|
-
new Promise<{ status: "timedOut" }>(resolve => {
|
|
646
|
-
timeoutId = setTimeout(() => resolve({ status: "timedOut" }), timeoutMs);
|
|
647
|
-
timeoutId.unref();
|
|
648
|
-
}),
|
|
649
|
-
]);
|
|
650
|
-
|
|
651
|
-
if (timeoutId) {
|
|
652
|
-
clearTimeout(timeoutId);
|
|
324
|
+
async function runQueued<T>(
|
|
325
|
+
session: PythonSession,
|
|
326
|
+
options: Pick<PythonExecutorOptions, "signal" | "deadlineMs">,
|
|
327
|
+
work: () => Promise<T>,
|
|
328
|
+
): Promise<T> {
|
|
329
|
+
const previous = session.queue;
|
|
330
|
+
const { promise: ourSlot, resolve: releaseSlot } = Promise.withResolvers<void>();
|
|
331
|
+
// Keep the queue chained even if WE bail out: future runs must still wait
|
|
332
|
+
// for `previous` to finish before they touch the kernel.
|
|
333
|
+
session.queue = previous.catch(() => undefined).then(() => ourSlot);
|
|
334
|
+
try {
|
|
335
|
+
await waitForPromiseWithCancellation(
|
|
336
|
+
previous.catch(() => undefined),
|
|
337
|
+
options,
|
|
338
|
+
);
|
|
339
|
+
return await work();
|
|
340
|
+
} finally {
|
|
341
|
+
releaseSlot();
|
|
653
342
|
}
|
|
654
|
-
return result;
|
|
655
343
|
}
|
|
656
344
|
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
}
|
|
345
|
+
// ---------------------------------------------------------------------------
|
|
346
|
+
// Public dispose entry points
|
|
347
|
+
// ---------------------------------------------------------------------------
|
|
661
348
|
|
|
662
|
-
async function
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
session.disposeCapacityPromise = releaseDisposalCapacity.promise;
|
|
667
|
-
session.resolveDisposeCapacity = releaseDisposalCapacity.resolve;
|
|
668
|
-
}
|
|
669
|
-
|
|
670
|
-
if (
|
|
671
|
-
shutdownTimeoutMs === undefined &&
|
|
672
|
-
session.disposeResultPromise &&
|
|
673
|
-
session.disposeResultTimeoutMs !== undefined
|
|
674
|
-
) {
|
|
675
|
-
const inheritedResult = await session.disposeResultPromise;
|
|
676
|
-
if (inheritedResult.status === "confirmed") {
|
|
677
|
-
finishDisposingKernelSession(session);
|
|
678
|
-
return;
|
|
679
|
-
}
|
|
680
|
-
session.disposeResultPromise = undefined;
|
|
681
|
-
session.disposeResultTimeoutMs = undefined;
|
|
682
|
-
logger.warn("Retained kernel shutdown was not confirmed during owner cleanup; retrying without timeout", {
|
|
683
|
-
sessionId: session.id,
|
|
684
|
-
});
|
|
685
|
-
}
|
|
686
|
-
|
|
687
|
-
const inheritedBackgroundRetryTimeoutMs =
|
|
688
|
-
shutdownTimeoutMs === undefined && session.disposeResultPromise && session.disposeResultTimeoutMs === undefined
|
|
689
|
-
? OWNER_CLEANUP_KERNEL_SHUTDOWN_TIMEOUT_MS
|
|
690
|
-
: shutdownTimeoutMs;
|
|
691
|
-
|
|
692
|
-
getOrStartKernelDisposalResultPromise(session, shutdownTimeoutMs);
|
|
693
|
-
const result = await waitForKernelSessionDisposal(session, inheritedBackgroundRetryTimeoutMs);
|
|
694
|
-
if (!result) {
|
|
695
|
-
return;
|
|
696
|
-
}
|
|
697
|
-
if (result.status === "timedOut") {
|
|
698
|
-
logger.warn(
|
|
699
|
-
shutdownTimeoutMs === undefined
|
|
700
|
-
? "Timed out waiting for retained kernel shutdown during global cleanup; retained capacity remains reserved"
|
|
701
|
-
: "Timed out shutting down retained kernel during owner cleanup",
|
|
702
|
-
{
|
|
703
|
-
sessionId: session.id,
|
|
704
|
-
timeoutMs: inheritedBackgroundRetryTimeoutMs,
|
|
705
|
-
},
|
|
706
|
-
);
|
|
707
|
-
if (shutdownTimeoutMs !== undefined) {
|
|
708
|
-
retryKernelSessionDisposalInBackground(session);
|
|
709
|
-
}
|
|
710
|
-
return;
|
|
711
|
-
}
|
|
712
|
-
if (result.status === "confirmed") {
|
|
713
|
-
finishDisposingKernelSession(session);
|
|
714
|
-
return;
|
|
715
|
-
}
|
|
716
|
-
if (result.status === "unconfirmed") {
|
|
717
|
-
logger.warn(
|
|
718
|
-
shutdownTimeoutMs === undefined
|
|
719
|
-
? "Kernel shutdown was not confirmed; retained capacity remains reserved"
|
|
720
|
-
: "Retained kernel shutdown was not confirmed during owner cleanup",
|
|
721
|
-
{ sessionId: session.id },
|
|
722
|
-
);
|
|
723
|
-
if (shutdownTimeoutMs !== undefined) {
|
|
724
|
-
retryKernelSessionDisposalInBackground(session);
|
|
725
|
-
}
|
|
726
|
-
return;
|
|
349
|
+
export async function disposeAllKernelSessions(): Promise<void> {
|
|
350
|
+
const all = [...sessions.entries()];
|
|
351
|
+
for (const [id, session] of all) {
|
|
352
|
+
if (sessions.get(id) === session) sessions.delete(id);
|
|
727
353
|
}
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
);
|
|
737
|
-
if (shutdownTimeoutMs !== undefined) {
|
|
738
|
-
retryKernelSessionDisposalInBackground(session);
|
|
354
|
+
const results = await Promise.allSettled(all.map(([, session]) => session.kernel.shutdown()));
|
|
355
|
+
for (let i = 0; i < all.length; i += 1) {
|
|
356
|
+
const [id, session] = all[i];
|
|
357
|
+
const result = results[i];
|
|
358
|
+
if (result.status === "fulfilled" && result.value?.confirmed !== false) continue;
|
|
359
|
+
const reason = result.status === "rejected" ? result.reason : "not confirmed";
|
|
360
|
+
logger.warn("Python kernel shutdown not confirmed", { sessionId: id, reason });
|
|
361
|
+
if (!sessions.has(id)) sessions.set(id, session);
|
|
739
362
|
}
|
|
740
|
-
return;
|
|
741
363
|
}
|
|
742
364
|
|
|
743
|
-
async function
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
)
|
|
749
|
-
|
|
750
|
-
if (session?.disposing) {
|
|
751
|
-
session = undefined;
|
|
752
|
-
}
|
|
753
|
-
if (!session) {
|
|
754
|
-
await ensureKernelSessionCapacity(options);
|
|
755
|
-
requireRemainingTimeoutMs(options.deadlineMs);
|
|
756
|
-
if (options.signal?.aborted) {
|
|
757
|
-
throw new PythonExecutionCancelledError(isTimedOutCancellation(options.signal.reason, options.signal));
|
|
365
|
+
export async function disposeKernelSessionsByOwner(ownerId: string): Promise<void> {
|
|
366
|
+
const toShutdown: PythonSession[] = [];
|
|
367
|
+
for (const session of [...sessions.values()]) {
|
|
368
|
+
if (!session.ownerIds.has(ownerId)) continue;
|
|
369
|
+
if (session.ownerIds.size === 1) {
|
|
370
|
+
toShutdown.push(session);
|
|
371
|
+
continue;
|
|
758
372
|
}
|
|
759
|
-
session
|
|
760
|
-
kernelSessions.set(sessionId, session);
|
|
761
|
-
startCleanupTimer();
|
|
373
|
+
session.ownerIds.delete(ownerId);
|
|
762
374
|
}
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
if (session.disposing) {
|
|
766
|
-
return await withKernelSession(sessionId, cwd, handler, options);
|
|
375
|
+
for (const session of toShutdown) {
|
|
376
|
+
if (sessions.get(session.sessionId) === session) sessions.delete(session.sessionId);
|
|
767
377
|
}
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
session
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
const result = await logger.time("kernel:withSession:handler", handler, session!.kernel);
|
|
776
|
-
session!.restartCount = 0;
|
|
777
|
-
return result;
|
|
778
|
-
} catch (err) {
|
|
779
|
-
if (!session!.dead && !session!.needsRestart && session!.kernel.isAlive()) {
|
|
780
|
-
throw err;
|
|
781
|
-
}
|
|
782
|
-
await logger.time("kernel:restartKernelSession", restartKernelSession, session!, cwd, options);
|
|
783
|
-
const result = await logger.time("kernel:postRestart:handler", handler, session!.kernel);
|
|
784
|
-
session!.restartCount = 0;
|
|
785
|
-
return result;
|
|
786
|
-
}
|
|
787
|
-
};
|
|
788
|
-
|
|
789
|
-
const queue = session.queue;
|
|
790
|
-
let releaseTurn: (() => void) | undefined;
|
|
791
|
-
const turn = new Promise<void>(resolve => {
|
|
792
|
-
releaseTurn = resolve;
|
|
793
|
-
});
|
|
794
|
-
session.queue = queue
|
|
795
|
-
.then(
|
|
796
|
-
() => turn,
|
|
797
|
-
() => turn,
|
|
798
|
-
)
|
|
799
|
-
.then(
|
|
800
|
-
() => undefined,
|
|
801
|
-
() => undefined,
|
|
802
|
-
);
|
|
803
|
-
|
|
804
|
-
try {
|
|
805
|
-
await waitForQueueTurn(queue, options);
|
|
806
|
-
if (session.disposing) {
|
|
807
|
-
return await withKernelSession(sessionId, cwd, handler, options);
|
|
378
|
+
const results = await Promise.allSettled(toShutdown.map(session => session.kernel.shutdown()));
|
|
379
|
+
for (let i = 0; i < toShutdown.length; i += 1) {
|
|
380
|
+
const session = toShutdown[i];
|
|
381
|
+
const result = results[i];
|
|
382
|
+
if (result.status === "fulfilled" && result.value?.confirmed !== false) {
|
|
383
|
+
session.ownerIds.delete(ownerId);
|
|
384
|
+
continue;
|
|
808
385
|
}
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
386
|
+
const reason = result.status === "rejected" ? result.reason : "not confirmed";
|
|
387
|
+
logger.warn("Python kernel shutdown not confirmed", { sessionId: session.sessionId, reason });
|
|
388
|
+
if (!sessions.has(session.sessionId)) sessions.set(session.sessionId, session);
|
|
812
389
|
}
|
|
813
390
|
}
|
|
814
391
|
|
|
392
|
+
// ---------------------------------------------------------------------------
|
|
393
|
+
// Execution
|
|
394
|
+
// ---------------------------------------------------------------------------
|
|
395
|
+
|
|
815
396
|
async function executeWithKernel(
|
|
816
397
|
kernel: PythonKernelExecutor,
|
|
817
398
|
code: string,
|
|
@@ -900,6 +481,76 @@ async function executeWithKernel(
|
|
|
900
481
|
}
|
|
901
482
|
}
|
|
902
483
|
|
|
484
|
+
async function ensureKernelAvailable(cwd: string, options: PythonExecutorOptions): Promise<void> {
|
|
485
|
+
const availability = await waitForPromiseWithCancellation(checkPythonKernelAvailability(cwd), options);
|
|
486
|
+
if (!availability.ok) {
|
|
487
|
+
throw new Error(availability.reason ?? "Python kernel unavailable");
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
async function ensureToolBridge(options: PythonExecutorOptions): Promise<void> {
|
|
492
|
+
if (!options.toolSession || options.bridge) return;
|
|
493
|
+
try {
|
|
494
|
+
options.bridge = await ensurePyToolBridge();
|
|
495
|
+
} catch (err) {
|
|
496
|
+
logger.warn("Failed to start Python tool bridge", {
|
|
497
|
+
error: err instanceof Error ? err.message : String(err),
|
|
498
|
+
});
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
async function executePerCall(code: string, cwd: string, options: PythonExecutorOptions): Promise<PythonResult> {
|
|
503
|
+
if (options.bridge && !options.bridgeSessionId) {
|
|
504
|
+
options.bridgeSessionId = `py-bridge:${crypto.randomUUID()}`;
|
|
505
|
+
}
|
|
506
|
+
const kernel = await startKernel(cwd, options);
|
|
507
|
+
try {
|
|
508
|
+
return await executeWithKernel(kernel, code, options);
|
|
509
|
+
} finally {
|
|
510
|
+
await kernel.shutdown().catch(() => undefined);
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
async function executeOnSession(code: string, cwd: string, options: PythonExecutorOptions): Promise<PythonResult> {
|
|
515
|
+
const sessionId = options.sessionId ?? `session:${cwd}`;
|
|
516
|
+
if (options.bridge && !options.bridgeSessionId) {
|
|
517
|
+
options.bridgeSessionId = sessionId;
|
|
518
|
+
}
|
|
519
|
+
if (options.reset) {
|
|
520
|
+
await resetSession(sessionId);
|
|
521
|
+
}
|
|
522
|
+
const session = await acquireSession(sessionId, cwd, options);
|
|
523
|
+
return await runQueued(session, options, async () => {
|
|
524
|
+
if (options.signal?.aborted) {
|
|
525
|
+
throw new PythonExecutionCancelledError(isTimedOutCancellation(options.signal.reason, options.signal));
|
|
526
|
+
}
|
|
527
|
+
if (sessions.get(session.sessionId) !== session) {
|
|
528
|
+
throw new PythonExecutionCancelledError(false);
|
|
529
|
+
}
|
|
530
|
+
if (!session.kernel.isAlive()) {
|
|
531
|
+
await replaceSessionKernel(session, cwd, options);
|
|
532
|
+
if (sessions.get(session.sessionId) !== session) {
|
|
533
|
+
throw new PythonExecutionCancelledError(false);
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
try {
|
|
537
|
+
return await executeWithKernel(session.kernel, code, options);
|
|
538
|
+
} catch (err) {
|
|
539
|
+
if (isCancellationError(err) || options.signal?.aborted) throw err;
|
|
540
|
+
if (session.kernel.isAlive()) throw err;
|
|
541
|
+
if (sessions.get(session.sessionId) !== session) {
|
|
542
|
+
throw new PythonExecutionCancelledError(false);
|
|
543
|
+
}
|
|
544
|
+
// Kernel died during execute. Replace it and retry once on a fresh one.
|
|
545
|
+
await replaceSessionKernel(session, cwd, options);
|
|
546
|
+
if (sessions.get(session.sessionId) !== session) {
|
|
547
|
+
throw new PythonExecutionCancelledError(false);
|
|
548
|
+
}
|
|
549
|
+
return await executeWithKernel(session.kernel, code, options);
|
|
550
|
+
}
|
|
551
|
+
});
|
|
552
|
+
}
|
|
553
|
+
|
|
903
554
|
export async function executePythonWithKernel(
|
|
904
555
|
kernel: PythonKernelExecutor,
|
|
905
556
|
code: string,
|
|
@@ -923,55 +574,14 @@ export async function executePython(code: string, options?: PythonExecutorOption
|
|
|
923
574
|
isTimedOutCancellation(executionOptions.signal.reason, executionOptions.signal),
|
|
924
575
|
);
|
|
925
576
|
}
|
|
926
|
-
|
|
927
|
-
await
|
|
577
|
+
await ensureKernelAvailable(cwd, executionOptions);
|
|
578
|
+
await ensureToolBridge(executionOptions);
|
|
928
579
|
|
|
929
580
|
const kernelMode = executionOptions.kernelMode ?? "session";
|
|
930
|
-
|
|
931
|
-
if (executionOptions.toolSession && !executionOptions.bridge) {
|
|
932
|
-
try {
|
|
933
|
-
executionOptions.bridge = await ensurePyToolBridge();
|
|
934
|
-
} catch (err) {
|
|
935
|
-
logger.warn("Failed to start Python tool bridge", {
|
|
936
|
-
error: err instanceof Error ? err.message : String(err),
|
|
937
|
-
});
|
|
938
|
-
}
|
|
939
|
-
}
|
|
940
|
-
|
|
941
581
|
if (kernelMode === "per-call") {
|
|
942
|
-
|
|
943
|
-
executionOptions.bridgeSessionId = `py-bridge:${crypto.randomUUID()}`;
|
|
944
|
-
}
|
|
945
|
-
const env = buildKernelEnv(executionOptions);
|
|
946
|
-
requireRemainingTimeoutMs(deadlineMs);
|
|
947
|
-
const startOptions = buildKernelStartOptions(cwd, env, executionOptions);
|
|
948
|
-
const kernel = await PythonKernel.start(startOptions);
|
|
949
|
-
try {
|
|
950
|
-
return await executeWithKernel(kernel, code, executionOptions);
|
|
951
|
-
} finally {
|
|
952
|
-
await kernel.shutdown();
|
|
953
|
-
}
|
|
954
|
-
}
|
|
955
|
-
|
|
956
|
-
const sessionId = executionOptions.sessionId ?? `session:${cwd}`;
|
|
957
|
-
if (executionOptions.bridge && !executionOptions.bridgeSessionId) {
|
|
958
|
-
executionOptions.bridgeSessionId = sessionId;
|
|
959
|
-
}
|
|
960
|
-
if (executionOptions.reset) {
|
|
961
|
-
const existing = kernelSessions.get(sessionId);
|
|
962
|
-
if (existing) {
|
|
963
|
-
await disposeKernelSession(existing);
|
|
964
|
-
if (existing.disposing && existing.nextDisposalRetryAt !== undefined) {
|
|
965
|
-
retryKernelSessionDisposalInBackground(existing);
|
|
966
|
-
}
|
|
967
|
-
}
|
|
582
|
+
return await executePerCall(code, cwd, executionOptions);
|
|
968
583
|
}
|
|
969
|
-
return await
|
|
970
|
-
sessionId,
|
|
971
|
-
cwd,
|
|
972
|
-
async kernel => executeWithKernel(kernel, code, executionOptions),
|
|
973
|
-
executionOptions,
|
|
974
|
-
);
|
|
584
|
+
return await executeOnSession(code, cwd, executionOptions);
|
|
975
585
|
} catch (err) {
|
|
976
586
|
if (isCancellationError(err) || executionOptions.signal?.aborted) {
|
|
977
587
|
return createCancelledPythonResult(isTimedOutCancellation(err, executionOptions.signal));
|