@gajae-code/coding-agent 0.18.8 → 0.18.9
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 +98 -0
- package/dist/types/async/endpoint-id.d.ts +16 -0
- package/dist/types/async/support.d.ts +0 -14
- package/dist/types/commands/mcp-serve.d.ts +4 -0
- package/dist/types/discovery/mcp-json.d.ts +1 -0
- package/dist/types/eval/py/kernel.d.ts +18 -0
- package/dist/types/export/html/html-export-destination.d.ts +2 -0
- package/dist/types/extensibility/extensions/function-hooks-internal.d.ts +5 -0
- package/dist/types/extensibility/skill-management.d.ts +1 -0
- package/dist/types/gjc-runtime/psmux-detect.d.ts +3 -3
- package/dist/types/gjc-runtime/sidecar-bootstrap-env.d.ts +6 -0
- package/dist/types/gjc-runtime/state-writer.d.ts +3 -0
- package/dist/types/gjc-runtime/tmux-common.d.ts +8 -5
- package/dist/types/lsp/config.d.ts +8 -5
- package/dist/types/runtime-mcp/manager.d.ts +2 -1
- package/dist/types/runtime-mcp/mcp-redirect.d.ts +9 -0
- package/dist/types/runtime-mcp/oauth-public-url.d.ts +3 -1
- package/dist/types/runtime-mcp/project-host-values.d.ts +27 -0
- package/dist/types/sdk/broker/broker.d.ts +6 -1
- package/dist/types/sdk/broker/doctor-restart.d.ts +5 -0
- package/dist/types/sdk/broker/ensure.d.ts +14 -1
- package/dist/types/session/session-storage.d.ts +7 -0
- package/dist/types/skill-state/workflow-mutation-guard.d.ts +3 -0
- package/dist/types/task/isolation-dir.d.ts +26 -0
- package/dist/types/tools/ast-edit-write-path.d.ts +13 -0
- package/dist/types/tools/browser/cdp-url.d.ts +2 -0
- package/dist/types/tools/github-side-effect.d.ts +22 -0
- package/dist/types/tools/monitor-mutation.d.ts +9 -0
- package/package.json +7 -7
- package/src/async/endpoint-id.ts +26 -0
- package/src/async/job-manager.ts +19 -10
- package/src/async/support.ts +0 -25
- package/src/capability/index.ts +14 -16
- package/src/cli/doctor/repairs.ts +4 -1
- package/src/cli/doctor-supervisor.ts +3 -0
- package/src/cli/public-command-registry.ts +28 -6
- package/src/commands/mcp-serve.ts +7 -0
- package/src/commands/sdk.ts +453 -415
- package/src/config/file-lock.ts +9 -11
- package/src/config/model-profile-activation.ts +5 -5
- package/src/coordinator-mcp/server.ts +2 -0
- package/src/customization/inventory.ts +1 -13
- package/src/dap/config.ts +2 -2
- package/src/defaults/gjc/extensions/grok-cli-vendor/src/provider/register.ts +10 -6
- package/src/discovery/builtin.ts +3 -1
- package/src/discovery/helpers.ts +12 -9
- package/src/discovery/mcp-json.ts +20 -12
- package/src/eval/py/executor.ts +459 -77
- package/src/eval/py/kernel.ts +79 -13
- package/src/eval/py/tool-bridge.ts +4 -0
- package/src/export/html/html-export-destination.ts +16 -0
- package/src/export/html/index.ts +2 -6
- package/src/extensibility/extensions/function-hooks-internal.ts +27 -0
- package/src/extensibility/extensions/runner.ts +11 -1
- package/src/extensibility/gjc-plugins/mcp-policy.ts +2 -1
- package/src/extensibility/gjc-plugins/runtime-adapters.ts +27 -15
- package/src/extensibility/skill-management.ts +3 -0
- package/src/gjc-runtime/launch-worktree.ts +56 -5
- package/src/gjc-runtime/psmux-detect.ts +28 -4
- package/src/gjc-runtime/session-state-sidecar.ts +3 -2
- package/src/gjc-runtime/sidecar-bootstrap-env.ts +19 -0
- package/src/gjc-runtime/state-writer.ts +22 -8
- package/src/gjc-runtime/tmux-common.ts +8 -5
- package/src/internal-urls/docs-index.generated.ts +7 -7
- package/src/lsp/config.ts +12 -63
- package/src/memories/index.ts +88 -7
- package/src/modes/components/hook-editor.ts +1 -1
- package/src/modes/components/hook-selector.ts +1 -1
- package/src/modes/controllers/input-controller.ts +2 -2
- package/src/modes/controllers/runtime-mcp-command-controller.ts +21 -13
- package/src/modes/controllers/selector-controller.ts +3 -7
- package/src/modes/controllers/todo-command-controller.ts +1 -1
- package/src/modes/interactive-mode.ts +1 -1
- package/src/runtime-mcp/client.ts +4 -2
- package/src/runtime-mcp/json-rpc.ts +2 -1
- package/src/runtime-mcp/manager.ts +29 -11
- package/src/runtime-mcp/mcp-redirect.ts +73 -0
- package/src/runtime-mcp/oauth-flow.ts +11 -4
- package/src/runtime-mcp/oauth-public-url.ts +8 -2
- package/src/runtime-mcp/pool.ts +6 -0
- package/src/runtime-mcp/project-host-values.ts +46 -0
- package/src/runtime-mcp/transports/http.ts +2 -1
- package/src/sdk/acp/adapter.ts +2 -0
- package/src/sdk/broker/broker.ts +6 -0
- package/src/sdk/broker/doctor-restart.ts +4 -0
- package/src/sdk/broker/ensure.ts +29 -2
- package/src/sdk/host/session-runtime.test.ts +58 -9
- package/src/sdk/host/session-runtime.ts +65 -35
- package/src/sdk/router/session-router.ts +7 -6
- package/src/sdk/session.ts +2 -7
- package/src/session/agent-session.ts +59 -17
- package/src/session/artifacts.ts +3 -2
- package/src/session/internal/managed-session-scope.ts +403 -81
- package/src/session/internal/managed-session-storage.ts +125 -17
- package/src/session/session-manager.ts +12 -0
- package/src/session/session-storage.ts +30 -1
- package/src/skill-state/active-state.ts +13 -0
- package/src/skill-state/workflow-mutation-guard.ts +102 -10
- package/src/slash-commands/builtin-registry.ts +1 -1
- package/src/slash-commands/helpers/mcp.ts +8 -4
- package/src/task/isolation-dir.ts +143 -0
- package/src/task/worktree.ts +16 -7
- package/src/tools/ast-edit-write-path.ts +68 -0
- package/src/tools/ast-edit.ts +7 -2
- package/src/tools/browser/cdp-url.ts +18 -0
- package/src/tools/browser.ts +8 -1
- package/src/tools/gh.ts +10 -0
- package/src/tools/github-side-effect.ts +112 -0
- package/src/tools/monitor-mutation.ts +27 -0
- package/src/tools/monitor.ts +7 -0
- package/src/tools/python.ts +7 -6
- package/src/utils/external-editor.ts +3 -2
- package/src/web/insane/url-guard.ts +2 -1
package/src/eval/py/executor.ts
CHANGED
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
type KernelExecuteResult,
|
|
14
14
|
type KernelShutdownResult,
|
|
15
15
|
PythonKernel,
|
|
16
|
+
PythonKernelStartError,
|
|
16
17
|
} from "./kernel";
|
|
17
18
|
import type { PythonRuntimeOptions } from "./runtime";
|
|
18
19
|
import { ensurePyToolBridge, registerPyToolBridge } from "./tool-bridge";
|
|
@@ -115,6 +116,8 @@ interface PythonSession {
|
|
|
115
116
|
kernelInstanceId: string;
|
|
116
117
|
bridgeCapability?: string;
|
|
117
118
|
ownerIds: Set<string>;
|
|
119
|
+
durableOwnerIds: Set<string>;
|
|
120
|
+
provisionalOwnerCounts: Map<string, number>;
|
|
118
121
|
hasFallbackOwner: boolean;
|
|
119
122
|
queue: Promise<void>;
|
|
120
123
|
cleanupPromise?: Promise<KernelShutdownResult>;
|
|
@@ -122,20 +125,32 @@ interface PythonSession {
|
|
|
122
125
|
}
|
|
123
126
|
|
|
124
127
|
interface InitializingPythonSession {
|
|
128
|
+
sessionId: string;
|
|
125
129
|
promise: Promise<PythonSession>;
|
|
130
|
+
startupController: AbortController;
|
|
131
|
+
requests: Set<ActivePythonRequest>;
|
|
126
132
|
ownerIds: Set<string>;
|
|
127
133
|
retirementOwnerIds: Set<string>;
|
|
128
134
|
hasFallbackOwner: boolean;
|
|
129
135
|
cancelled?: PythonExecutionCancelledError;
|
|
130
136
|
kernel?: PythonKernel;
|
|
137
|
+
cleanupAttempt?: Promise<KernelShutdownResult>;
|
|
131
138
|
cleanupError?: unknown;
|
|
132
139
|
cleanupFailed?: boolean;
|
|
133
140
|
}
|
|
134
141
|
|
|
135
142
|
interface ActivePythonRequest {
|
|
136
143
|
readonly ownerId?: string;
|
|
144
|
+
readonly sessionId?: string;
|
|
145
|
+
readonly fallbackOwner: boolean;
|
|
137
146
|
readonly controller: AbortController;
|
|
147
|
+
signal?: AbortSignal;
|
|
148
|
+
initializer?: InitializingPythonSession;
|
|
149
|
+
initializerAcquired?: boolean;
|
|
150
|
+
provisionalSession?: PythonSession;
|
|
138
151
|
readonly completion: Promise<void>;
|
|
152
|
+
readonly kernels: Set<PythonKernel>;
|
|
153
|
+
readonly cleanupAttempts: Map<PythonKernel, Promise<KernelShutdownResult>>;
|
|
139
154
|
readonly finish: () => void;
|
|
140
155
|
}
|
|
141
156
|
|
|
@@ -196,19 +211,28 @@ function retainKernelForOwners(kernel: PythonKernel, ownerIds: Set<string>): voi
|
|
|
196
211
|
retiringKernelOwners.set(kernel, owners);
|
|
197
212
|
}
|
|
198
213
|
|
|
199
|
-
|
|
200
|
-
|
|
214
|
+
function retainKernelForOwner(kernel: PythonKernel | undefined, ownerId: string): void {
|
|
215
|
+
if (!kernel) return;
|
|
216
|
+
retiringKernels.add(kernel);
|
|
217
|
+
const owners = retiringKernelOwners.get(kernel) ?? new Set<string>();
|
|
218
|
+
owners.add(ownerId);
|
|
219
|
+
retiringKernelOwners.set(kernel, owners);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function removeKernelOwner(kernel: PythonKernel | undefined, ownerId: string): void {
|
|
223
|
+
if (kernel) retiringKernelOwners.get(kernel)?.delete(ownerId);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
async function shutdownOrRetainKernel(kernel: PythonKernel, ownerIds: Set<string>, timeoutMs?: number): Promise<void> {
|
|
227
|
+
retainKernelForOwners(kernel, ownerIds);
|
|
228
|
+
const [shutdown, start] = prepareRetiringKernelShutdown(kernel, timeoutMs);
|
|
229
|
+
start();
|
|
201
230
|
try {
|
|
202
|
-
|
|
231
|
+
await shutdown;
|
|
203
232
|
} catch (error) {
|
|
204
|
-
retainKernelForOwners(kernel, ownerIds);
|
|
205
233
|
logger.warn("Python kernel shutdown not confirmed", { kernelId: kernel.id, reason: error });
|
|
206
234
|
throw error;
|
|
207
235
|
}
|
|
208
|
-
if (result.confirmed) return;
|
|
209
|
-
retainKernelForOwners(kernel, ownerIds);
|
|
210
|
-
logger.warn("Python kernel shutdown not confirmed", { kernelId: kernel.id });
|
|
211
|
-
throw unconfirmedShutdownError(kernel);
|
|
212
236
|
}
|
|
213
237
|
|
|
214
238
|
function prepareSessionShutdown(session: PythonSession): Promise<KernelShutdownResult> {
|
|
@@ -216,7 +240,12 @@ function prepareSessionShutdown(session: PythonSession): Promise<KernelShutdownR
|
|
|
216
240
|
const { promise: cleanup, resolve, reject } = Promise.withResolvers<KernelShutdownResult>();
|
|
217
241
|
retiringSessions.add(session);
|
|
218
242
|
session.cleanupPromise = cleanup;
|
|
219
|
-
|
|
243
|
+
retainKernelForOwners(session.kernel, session.ownerIds);
|
|
244
|
+
const [kernelCleanup, startKernelCleanup] = prepareRetiringKernelShutdown(session.kernel);
|
|
245
|
+
session.cleanupStart = () => {
|
|
246
|
+
startKernelCleanup();
|
|
247
|
+
void kernelCleanup.then(resolve, reject);
|
|
248
|
+
};
|
|
220
249
|
void cleanup.then(
|
|
221
250
|
result => {
|
|
222
251
|
if (result.confirmed) retiringSessions.delete(session);
|
|
@@ -243,22 +272,25 @@ function startSessionShutdown(session: PythonSession): void {
|
|
|
243
272
|
start();
|
|
244
273
|
}
|
|
245
274
|
|
|
246
|
-
function callKernelShutdown(kernel: PythonKernel): Promise<KernelShutdownResult> {
|
|
275
|
+
function callKernelShutdown(kernel: PythonKernel, timeoutMs?: number): Promise<KernelShutdownResult> {
|
|
247
276
|
try {
|
|
248
|
-
return kernel.shutdown();
|
|
277
|
+
return kernel.shutdown(timeoutMs === undefined ? undefined : { timeoutMs });
|
|
249
278
|
} catch (error) {
|
|
250
279
|
return Promise.reject(error);
|
|
251
280
|
}
|
|
252
281
|
}
|
|
253
282
|
|
|
254
|
-
function confirmedKernelShutdown(kernel: PythonKernel): Promise<KernelShutdownResult> {
|
|
255
|
-
return callKernelShutdown(kernel).then(result => {
|
|
283
|
+
function confirmedKernelShutdown(kernel: PythonKernel, timeoutMs?: number): Promise<KernelShutdownResult> {
|
|
284
|
+
return callKernelShutdown(kernel, timeoutMs).then(result => {
|
|
256
285
|
if (!result.confirmed) throw unconfirmedShutdownError(kernel);
|
|
257
286
|
return result;
|
|
258
287
|
});
|
|
259
288
|
}
|
|
260
289
|
|
|
261
|
-
function prepareRetiringKernelShutdown(
|
|
290
|
+
function prepareRetiringKernelShutdown(
|
|
291
|
+
kernel: PythonKernel,
|
|
292
|
+
timeoutMs?: number,
|
|
293
|
+
): [Promise<KernelShutdownResult>, () => void] {
|
|
262
294
|
const existing = retiringKernelShutdowns.get(kernel);
|
|
263
295
|
if (existing) return [existing, () => {}];
|
|
264
296
|
const { promise, resolve, reject } = Promise.withResolvers<KernelShutdownResult>();
|
|
@@ -281,7 +313,7 @@ function prepareRetiringKernelShutdown(kernel: PythonKernel): [Promise<KernelShu
|
|
|
281
313
|
() => {
|
|
282
314
|
if (started) return;
|
|
283
315
|
started = true;
|
|
284
|
-
void confirmedKernelShutdown(kernel).then(resolve, reject);
|
|
316
|
+
void confirmedKernelShutdown(kernel, timeoutMs).then(resolve, reject);
|
|
285
317
|
},
|
|
286
318
|
];
|
|
287
319
|
}
|
|
@@ -306,6 +338,16 @@ class PythonExecutionCancelledError extends Error {
|
|
|
306
338
|
}
|
|
307
339
|
}
|
|
308
340
|
|
|
341
|
+
class PythonKernelCleanupError extends Error {
|
|
342
|
+
constructor(
|
|
343
|
+
kernel: PythonKernel,
|
|
344
|
+
readonly cleanupError: unknown,
|
|
345
|
+
) {
|
|
346
|
+
super(`Python kernel cleanup failed: ${kernel.id}`, { cause: cleanupError });
|
|
347
|
+
this.name = "PythonKernelCleanupError";
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
309
351
|
function getExecutionDeadlineMs(options?: Pick<PythonExecutorOptions, "deadlineMs" | "timeoutMs">): number | undefined {
|
|
310
352
|
if (options?.deadlineMs !== undefined) return options.deadlineMs;
|
|
311
353
|
if (options?.timeoutMs === undefined) return undefined;
|
|
@@ -323,24 +365,30 @@ function beginPythonRequest(
|
|
|
323
365
|
ensurePythonResourceCleanup();
|
|
324
366
|
const capturedOptions = { ...(options ?? {}) };
|
|
325
367
|
const cwd = useSessionFallback ? (capturedOptions.cwd ?? getProjectDir()) : undefined;
|
|
326
|
-
const
|
|
327
|
-
capturedOptions.
|
|
328
|
-
(useSessionFallback && capturedOptions.kernelMode !== "per-call"
|
|
368
|
+
const sessionId =
|
|
369
|
+
useSessionFallback && capturedOptions.kernelMode !== "per-call"
|
|
329
370
|
? scopedSessionId(
|
|
330
371
|
capturedOptions.sessionId ?? `session:${cwd}`,
|
|
331
372
|
capturedOptions.settings,
|
|
332
373
|
capturedOptions.sessionId !== undefined,
|
|
333
374
|
)
|
|
334
|
-
: undefined
|
|
375
|
+
: undefined;
|
|
376
|
+
const ownerId = capturedOptions.kernelOwnerId ?? sessionId;
|
|
335
377
|
const deadlineMs = getExecutionDeadlineMs(capturedOptions);
|
|
336
378
|
const controller = new AbortController();
|
|
337
379
|
const { promise: completion, resolve } = Promise.withResolvers<void>();
|
|
338
380
|
let deadlineTimer: NodeJS.Timeout | undefined;
|
|
339
381
|
const request: ActivePythonRequest = {
|
|
340
382
|
ownerId,
|
|
383
|
+
sessionId,
|
|
384
|
+
fallbackOwner: capturedOptions.kernelOwnerId === undefined,
|
|
341
385
|
controller,
|
|
342
386
|
completion,
|
|
387
|
+
kernels: new Set(),
|
|
388
|
+
cleanupAttempts: new Map(),
|
|
343
389
|
finish: () => {
|
|
390
|
+
detachInitializerRequest(request, false);
|
|
391
|
+
detachProvisionalSessionRequest(request);
|
|
344
392
|
activeRequests.delete(request);
|
|
345
393
|
if (deadlineTimer) clearTimeout(deadlineTimer);
|
|
346
394
|
resolve();
|
|
@@ -351,6 +399,11 @@ function beginPythonRequest(
|
|
|
351
399
|
const signals = [controller.signal];
|
|
352
400
|
if (capturedOptions.signal) signals.push(capturedOptions.signal);
|
|
353
401
|
const signal = AbortSignal.any(signals);
|
|
402
|
+
request.signal = signal;
|
|
403
|
+
if (sessionId !== undefined) {
|
|
404
|
+
const session = sessions.get(sessionId);
|
|
405
|
+
if (session && isInitializingSession(session)) enrollInitializerRequest(session, request);
|
|
406
|
+
}
|
|
354
407
|
const remainingMs = getRemainingTimeoutMs(deadlineMs);
|
|
355
408
|
deadlineTimer =
|
|
356
409
|
remainingMs !== undefined
|
|
@@ -502,7 +555,14 @@ function buildKernelEnv(options: {
|
|
|
502
555
|
return Object.keys(env).length > 0 ? env : undefined;
|
|
503
556
|
}
|
|
504
557
|
|
|
505
|
-
async function startKernel(
|
|
558
|
+
async function startKernel(
|
|
559
|
+
cwd: string,
|
|
560
|
+
options: PythonExecutorOptions,
|
|
561
|
+
registration?: {
|
|
562
|
+
onKernelCreated: (kernel: PythonKernel) => void;
|
|
563
|
+
onStartupFailure: (kernel: PythonKernel) => Promise<void>;
|
|
564
|
+
},
|
|
565
|
+
): Promise<PythonKernel> {
|
|
506
566
|
throwIfExecutionCancelled(options);
|
|
507
567
|
return await PythonKernel.start({
|
|
508
568
|
cwd,
|
|
@@ -511,6 +571,8 @@ async function startKernel(cwd: string, options: PythonExecutorOptions): Promise
|
|
|
511
571
|
runtimeOptions: options.runtimeOptions,
|
|
512
572
|
signal: options.signal,
|
|
513
573
|
deadlineMs: options.deadlineMs,
|
|
574
|
+
onKernelCreated: registration?.onKernelCreated,
|
|
575
|
+
onStartupFailure: registration?.onStartupFailure,
|
|
514
576
|
});
|
|
515
577
|
}
|
|
516
578
|
|
|
@@ -522,75 +584,246 @@ function attachOwner(
|
|
|
522
584
|
if (ownerId !== undefined) {
|
|
523
585
|
if (session.hasFallbackOwner) {
|
|
524
586
|
session.ownerIds.delete(sessionId);
|
|
587
|
+
if (!isInitializingSession(session)) session.durableOwnerIds.delete(sessionId);
|
|
588
|
+
removeKernelOwner(session.kernel, sessionId);
|
|
525
589
|
session.hasFallbackOwner = false;
|
|
526
590
|
}
|
|
527
591
|
session.ownerIds.add(ownerId);
|
|
592
|
+
if (!isInitializingSession(session)) session.durableOwnerIds.add(ownerId);
|
|
593
|
+
retainKernelForOwner(session.kernel, ownerId);
|
|
528
594
|
return;
|
|
529
595
|
}
|
|
530
596
|
if (session.hasFallbackOwner || session.ownerIds.size === 0) {
|
|
531
597
|
session.ownerIds.add(sessionId);
|
|
598
|
+
if (!isInitializingSession(session)) session.durableOwnerIds.add(sessionId);
|
|
532
599
|
session.hasFallbackOwner = true;
|
|
600
|
+
retainKernelForOwner(session.kernel, sessionId);
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
function enrollInitializerRequest(initializing: InitializingPythonSession, request: ActivePythonRequest): boolean {
|
|
605
|
+
if (request.initializer === initializing) return true;
|
|
606
|
+
if (request.signal?.aborted || initializing.cancelled) return false;
|
|
607
|
+
if (request.sessionId !== initializing.sessionId) return false;
|
|
608
|
+
if (request.initializer) detachInitializerRequest(request, false);
|
|
609
|
+
request.initializer = initializing;
|
|
610
|
+
initializing.requests.add(request);
|
|
611
|
+
if (request.fallbackOwner || !initializing.hasFallbackOwner) {
|
|
612
|
+
attachOwner(initializing, initializing.sessionId, request.fallbackOwner ? undefined : request.ownerId);
|
|
613
|
+
} else if (request.ownerId !== undefined) {
|
|
614
|
+
initializing.ownerIds.add(request.ownerId);
|
|
615
|
+
}
|
|
616
|
+
return true;
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
function initializerRequestOwnerId(request: ActivePythonRequest): string | undefined {
|
|
620
|
+
return request.ownerId;
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
function detachInitializerRequest(request: ActivePythonRequest, timedOut: boolean): void {
|
|
624
|
+
const initializing = request.initializer;
|
|
625
|
+
if (!initializing) return;
|
|
626
|
+
request.initializer = undefined;
|
|
627
|
+
request.initializerAcquired = false;
|
|
628
|
+
initializing.requests.delete(request);
|
|
629
|
+
const ownerId = initializerRequestOwnerId(request);
|
|
630
|
+
if (ownerId !== undefined) {
|
|
631
|
+
let hasOwnerRequest = false;
|
|
632
|
+
for (const other of initializing.requests) {
|
|
633
|
+
if (other.ownerId === ownerId && !other.signal?.aborted) {
|
|
634
|
+
hasOwnerRequest = true;
|
|
635
|
+
break;
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
if (!hasOwnerRequest) {
|
|
639
|
+
initializing.ownerIds.delete(ownerId);
|
|
640
|
+
if (request.fallbackOwner || initializing.cleanupFailed) {
|
|
641
|
+
initializing.retirementOwnerIds.add(ownerId);
|
|
642
|
+
}
|
|
643
|
+
if (initializing.ownerIds.size > 0) removeKernelOwner(initializing.kernel, ownerId);
|
|
644
|
+
}
|
|
533
645
|
}
|
|
646
|
+
if (initializing.ownerIds.size !== 0 || initializing.cancelled) return;
|
|
647
|
+
initializing.cancelled = new PythonExecutionCancelledError(timedOut);
|
|
648
|
+
if (initializing.kernel && !initializing.cleanupAttempt && !initializing.cleanupFailed) {
|
|
649
|
+
const [cleanup, start] = prepareRetiringKernelShutdown(initializing.kernel);
|
|
650
|
+
initializing.cleanupAttempt = cleanup;
|
|
651
|
+
start();
|
|
652
|
+
}
|
|
653
|
+
initializing.startupController.abort(initializing.cancelled);
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
function promoteInitializerRequest(request: ActivePythonRequest): void {
|
|
657
|
+
const initializing = request.initializer;
|
|
658
|
+
if (!initializing) return;
|
|
659
|
+
initializing.requests.delete(request);
|
|
660
|
+
request.initializer = undefined;
|
|
534
661
|
}
|
|
535
662
|
|
|
536
|
-
|
|
663
|
+
function promoteProvisionalSessionRequest(request: ActivePythonRequest): void {
|
|
664
|
+
const session = request.provisionalSession;
|
|
665
|
+
if (!session) return;
|
|
666
|
+
request.provisionalSession = undefined;
|
|
667
|
+
const ownerId = request.ownerId;
|
|
668
|
+
if (ownerId === undefined) return;
|
|
669
|
+
session.durableOwnerIds.add(ownerId);
|
|
670
|
+
const count = session.provisionalOwnerCounts.get(ownerId) ?? 0;
|
|
671
|
+
if (count <= 1) session.provisionalOwnerCounts.delete(ownerId);
|
|
672
|
+
else session.provisionalOwnerCounts.set(ownerId, count - 1);
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
function detachProvisionalSessionRequest(request: ActivePythonRequest): void {
|
|
676
|
+
const session = request.provisionalSession;
|
|
677
|
+
if (!session) return;
|
|
678
|
+
request.provisionalSession = undefined;
|
|
679
|
+
const ownerId = request.ownerId;
|
|
680
|
+
if (ownerId === undefined) return;
|
|
681
|
+
const count = session.provisionalOwnerCounts.get(ownerId) ?? 0;
|
|
682
|
+
if (count <= 1) session.provisionalOwnerCounts.delete(ownerId);
|
|
683
|
+
else session.provisionalOwnerCounts.set(ownerId, count - 1);
|
|
684
|
+
if (count > 1 || session.durableOwnerIds.has(ownerId)) return;
|
|
685
|
+
if (session.ownerIds.size === 1 && session.ownerIds.has(ownerId)) {
|
|
686
|
+
retainKernelForOwner(session.kernel, ownerId);
|
|
687
|
+
if (sessions.get(session.sessionId) === session) sessions.delete(session.sessionId);
|
|
688
|
+
const cleanup = prepareSessionShutdown(session);
|
|
689
|
+
startSessionShutdown(session);
|
|
690
|
+
void cleanup.catch(error =>
|
|
691
|
+
logger.warn("Python kernel shutdown not confirmed", { kernelId: session.kernel.id, reason: error }),
|
|
692
|
+
);
|
|
693
|
+
return;
|
|
694
|
+
}
|
|
695
|
+
session.ownerIds.delete(ownerId);
|
|
696
|
+
removeKernelOwner(session.kernel, ownerId);
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
function initializingOwnerIds(initializing: InitializingPythonSession): Set<string> {
|
|
700
|
+
return new Set(initializing.ownerIds.size > 0 ? initializing.ownerIds : initializing.retirementOwnerIds);
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
async function shutdownInitializingKernel(
|
|
704
|
+
initializing: InitializingPythonSession,
|
|
705
|
+
kernel: PythonKernel,
|
|
706
|
+
): Promise<void> {
|
|
707
|
+
try {
|
|
708
|
+
if (initializing.kernel === kernel && initializing.cleanupAttempt) {
|
|
709
|
+
await initializing.cleanupAttempt;
|
|
710
|
+
} else {
|
|
711
|
+
await shutdownOrRetainKernel(kernel, initializingOwnerIds(initializing));
|
|
712
|
+
}
|
|
713
|
+
} catch (error) {
|
|
714
|
+
initializing.cleanupError = error;
|
|
715
|
+
initializing.cleanupFailed = true;
|
|
716
|
+
throw error;
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
async function acquireSession(
|
|
721
|
+
sessionId: string,
|
|
722
|
+
cwd: string,
|
|
723
|
+
options: PythonExecutorOptions,
|
|
724
|
+
request: ActivePythonRequest,
|
|
725
|
+
): Promise<PythonSession> {
|
|
726
|
+
throwIfExecutionCancelled(options);
|
|
537
727
|
const existing = sessions.get(sessionId);
|
|
728
|
+
if (request.provisionalSession && request.provisionalSession !== existing) {
|
|
729
|
+
detachProvisionalSessionRequest(request);
|
|
730
|
+
}
|
|
538
731
|
if (existing) {
|
|
539
|
-
|
|
732
|
+
if (isInitializingSession(existing)) {
|
|
733
|
+
if (enrollInitializerRequest(existing, request)) {
|
|
734
|
+
if (!request.fallbackOwner) attachOwner(existing, sessionId, options.kernelOwnerId);
|
|
735
|
+
request.initializerAcquired = true;
|
|
736
|
+
}
|
|
737
|
+
} else {
|
|
738
|
+
promoteProvisionalSessionRequest(request);
|
|
739
|
+
attachOwner(existing, sessionId, options.kernelOwnerId);
|
|
740
|
+
}
|
|
741
|
+
if (isInitializingSession(existing) && existing.cleanupFailed && existing.kernel) {
|
|
742
|
+
await shutdownOrRetainKernel(existing.kernel, initializingOwnerIds(existing));
|
|
743
|
+
promoteInitializerRequest(request);
|
|
744
|
+
existing.cleanupFailed = false;
|
|
745
|
+
existing.cleanupError = undefined;
|
|
746
|
+
existing.ownerIds.clear();
|
|
747
|
+
existing.retirementOwnerIds.clear();
|
|
748
|
+
existing.hasFallbackOwner = false;
|
|
749
|
+
if (sessions.get(sessionId) === existing) sessions.delete(sessionId);
|
|
750
|
+
throwIfExecutionCancelled(options);
|
|
751
|
+
return await acquireSession(sessionId, cwd, options, request);
|
|
752
|
+
}
|
|
540
753
|
let session: PythonSession;
|
|
541
754
|
if (isInitializingSession(existing)) {
|
|
542
755
|
try {
|
|
543
756
|
session = await waitForPromiseWithCancellation(existing.promise, options);
|
|
544
757
|
} catch (error) {
|
|
758
|
+
if (isCancellationError(error))
|
|
759
|
+
detachInitializerRequest(request, isTimedOutCancellation(error, options.signal));
|
|
545
760
|
const remainingMs = getRemainingTimeoutMs(options.deadlineMs);
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
return await acquireSession(sessionId, cwd, options);
|
|
761
|
+
const mayRetry = !options.signal?.aborted && (remainingMs === undefined || remainingMs > 0);
|
|
762
|
+
if (isCancellationError(error) && mayRetry && !existing.cleanupFailed) {
|
|
763
|
+
if (existing.cancelled) await existing.promise.catch(() => undefined);
|
|
764
|
+
return await acquireSession(sessionId, cwd, options, request);
|
|
765
|
+
}
|
|
552
766
|
throw error;
|
|
553
767
|
}
|
|
554
768
|
} else {
|
|
555
769
|
session = existing;
|
|
556
770
|
}
|
|
771
|
+
if (isInitializingSession(existing)) promoteInitializerRequest(request);
|
|
557
772
|
return session;
|
|
558
773
|
}
|
|
559
774
|
|
|
775
|
+
const startupController = new AbortController();
|
|
560
776
|
const initializing: InitializingPythonSession = {
|
|
777
|
+
sessionId,
|
|
778
|
+
startupController,
|
|
779
|
+
requests: new Set(),
|
|
561
780
|
ownerIds: new Set(),
|
|
562
781
|
retirementOwnerIds: new Set(),
|
|
563
782
|
hasFallbackOwner: false,
|
|
564
783
|
promise: Promise.resolve().then(async () => {
|
|
565
|
-
|
|
566
|
-
const
|
|
567
|
-
|
|
568
|
-
|
|
784
|
+
if (initializing.cancelled) throw initializing.cancelled;
|
|
785
|
+
const startupOptions = { ...options, signal: startupController.signal, deadlineMs: undefined };
|
|
786
|
+
const kernel = await startKernel(cwd, startupOptions, {
|
|
787
|
+
onKernelCreated: created => {
|
|
788
|
+
initializing.kernel = created;
|
|
789
|
+
retainKernelForOwners(created, initializingOwnerIds(initializing));
|
|
790
|
+
},
|
|
791
|
+
onStartupFailure: async failed => {
|
|
792
|
+
try {
|
|
793
|
+
await shutdownInitializingKernel(initializing, failed);
|
|
794
|
+
} catch (error) {
|
|
795
|
+
logger.warn("Python kernel shutdown not confirmed", {
|
|
796
|
+
kernelId: failed.id,
|
|
797
|
+
reason: error,
|
|
798
|
+
});
|
|
799
|
+
}
|
|
800
|
+
},
|
|
801
|
+
});
|
|
802
|
+
for (const route of [...initializing.requests]) {
|
|
803
|
+
if (!route.initializerAcquired && route.signal?.aborted) {
|
|
804
|
+
detachInitializerRequest(route, isTimedOutCancellation(route.signal.reason, route.signal));
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
if (initializing.cancelled || startupController.signal.aborted) {
|
|
569
808
|
try {
|
|
570
|
-
await
|
|
809
|
+
await shutdownInitializingKernel(initializing, kernel);
|
|
571
810
|
} catch (error) {
|
|
572
|
-
|
|
573
|
-
initializing.cleanupFailed = true;
|
|
811
|
+
logger.warn("Python kernel shutdown not confirmed", { kernelId: kernel.id, reason: error });
|
|
574
812
|
}
|
|
575
813
|
throw (
|
|
576
814
|
initializing.cancelled ??
|
|
577
|
-
new PythonExecutionCancelledError(
|
|
815
|
+
new PythonExecutionCancelledError(
|
|
816
|
+
isTimedOutCancellation(startupController.signal.reason, startupController.signal),
|
|
817
|
+
)
|
|
578
818
|
);
|
|
579
819
|
}
|
|
580
820
|
const current = sessions.get(sessionId);
|
|
581
821
|
if (current !== initializing) {
|
|
582
822
|
try {
|
|
583
|
-
await
|
|
823
|
+
await shutdownInitializingKernel(initializing, kernel);
|
|
584
824
|
} catch (error) {
|
|
585
|
-
|
|
586
|
-
initializing.cleanupFailed = true;
|
|
825
|
+
logger.warn("Python kernel shutdown not confirmed", { kernelId: kernel.id, reason: error });
|
|
587
826
|
}
|
|
588
|
-
const winner = current
|
|
589
|
-
? isInitializingSession(current)
|
|
590
|
-
? await waitForPromiseWithCancellation(current.promise, options)
|
|
591
|
-
: current
|
|
592
|
-
: undefined;
|
|
593
|
-
if (winner) return winner;
|
|
594
827
|
throw new PythonExecutionCancelledError(false);
|
|
595
828
|
}
|
|
596
829
|
const session: PythonSession = {
|
|
@@ -599,21 +832,35 @@ async function acquireSession(sessionId: string, cwd: string, options: PythonExe
|
|
|
599
832
|
kernelInstanceId: crypto.randomUUID(),
|
|
600
833
|
bridgeCapability: options.bridge?.capability,
|
|
601
834
|
ownerIds: new Set(initializing.ownerIds),
|
|
835
|
+
durableOwnerIds: new Set(),
|
|
836
|
+
provisionalOwnerCounts: new Map(),
|
|
602
837
|
hasFallbackOwner: initializing.hasFallbackOwner,
|
|
603
838
|
queue: Promise.resolve(),
|
|
604
839
|
};
|
|
840
|
+
for (const route of initializing.requests) {
|
|
841
|
+
const ownerId = route.ownerId;
|
|
842
|
+
const acquired = route.initializerAcquired === true;
|
|
843
|
+
route.initializer = undefined;
|
|
844
|
+
route.initializerAcquired = false;
|
|
845
|
+
if (ownerId === undefined || !session.ownerIds.has(ownerId)) continue;
|
|
846
|
+
if (acquired) {
|
|
847
|
+
session.durableOwnerIds.add(ownerId);
|
|
848
|
+
} else {
|
|
849
|
+
route.provisionalSession = session;
|
|
850
|
+
session.provisionalOwnerCounts.set(ownerId, (session.provisionalOwnerCounts.get(ownerId) ?? 0) + 1);
|
|
851
|
+
}
|
|
852
|
+
}
|
|
853
|
+
initializing.requests.clear();
|
|
605
854
|
sessions.set(sessionId, session);
|
|
606
855
|
return session;
|
|
607
856
|
}),
|
|
608
857
|
};
|
|
609
|
-
|
|
858
|
+
if (enrollInitializerRequest(initializing, request)) request.initializerAcquired = true;
|
|
610
859
|
initializingSessions.add(initializing);
|
|
611
860
|
sessions.set(sessionId, initializing);
|
|
612
861
|
let cancellationTimer: NodeJS.Timeout | undefined;
|
|
613
862
|
const retireCancelledInitialization = (timedOut: boolean): void => {
|
|
614
|
-
|
|
615
|
-
initializing.cancelled = new PythonExecutionCancelledError(timedOut);
|
|
616
|
-
if (sessions.get(sessionId) === initializing) sessions.delete(sessionId);
|
|
863
|
+
detachInitializerRequest(request, timedOut);
|
|
617
864
|
};
|
|
618
865
|
const onAbort = (): void => {
|
|
619
866
|
options.signal?.removeEventListener("abort", onAbort);
|
|
@@ -636,18 +883,20 @@ async function acquireSession(sessionId: string, cwd: string, options: PythonExe
|
|
|
636
883
|
void initializing.promise
|
|
637
884
|
.finally(() => {
|
|
638
885
|
initializingSessions.delete(initializing);
|
|
886
|
+
if (!initializing.cleanupFailed && sessions.get(sessionId) === initializing) sessions.delete(sessionId);
|
|
639
887
|
options.signal?.removeEventListener("abort", onAbort);
|
|
640
888
|
if (cancellationTimer) clearTimeout(cancellationTimer);
|
|
641
889
|
})
|
|
642
890
|
.catch(() => undefined);
|
|
643
891
|
try {
|
|
644
892
|
const session = await waitForPromiseWithCancellation(initializing.promise, options);
|
|
893
|
+
promoteInitializerRequest(request);
|
|
645
894
|
return session;
|
|
646
895
|
} catch (err) {
|
|
647
896
|
if (isCancellationError(err)) {
|
|
648
897
|
retireCancelledInitialization(isTimedOutCancellation(err, options.signal));
|
|
649
|
-
await initializing.promise.catch(() => undefined);
|
|
650
|
-
} else if (sessions.get(sessionId) === initializing) {
|
|
898
|
+
if (initializing.cancelled) await initializing.promise.catch(() => undefined);
|
|
899
|
+
} else if (sessions.get(sessionId) === initializing && !initializing.cleanupFailed) {
|
|
651
900
|
sessions.delete(sessionId);
|
|
652
901
|
}
|
|
653
902
|
throw err;
|
|
@@ -662,9 +911,8 @@ async function replaceSessionKernel(
|
|
|
662
911
|
throwIfExecutionCancelled(options);
|
|
663
912
|
const old = session.kernel;
|
|
664
913
|
const remaining = getRemainingTimeoutMs(options.deadlineMs);
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
.catch(() => undefined);
|
|
914
|
+
const ownerIds = new Set(session.ownerIds);
|
|
915
|
+
await shutdownOrRetainKernel(old, ownerIds, remaining === undefined ? undefined : Math.max(0, remaining));
|
|
668
916
|
throwIfExecutionCancelled(options);
|
|
669
917
|
if (sessions.get(session.sessionId) !== session) {
|
|
670
918
|
throw new PythonExecutionCancelledError(false);
|
|
@@ -675,9 +923,17 @@ async function replaceSessionKernel(
|
|
|
675
923
|
const nextCapability = bridge ? crypto.randomUUID() : undefined;
|
|
676
924
|
if (bridge && nextCapability) bridge.capability = nextCapability;
|
|
677
925
|
let next: PythonKernel | undefined;
|
|
926
|
+
let startSucceeded = false;
|
|
678
927
|
try {
|
|
679
928
|
throwIfExecutionCancelled(options);
|
|
680
|
-
next = await startKernel(cwd, options
|
|
929
|
+
next = await startKernel(cwd, options, {
|
|
930
|
+
onKernelCreated: created => {
|
|
931
|
+
next = created;
|
|
932
|
+
retainKernelForOwners(created, ownerIds);
|
|
933
|
+
},
|
|
934
|
+
onStartupFailure: failed => shutdownOrRetainKernel(failed, ownerIds),
|
|
935
|
+
});
|
|
936
|
+
startSucceeded = true;
|
|
681
937
|
throwIfExecutionCancelled(options);
|
|
682
938
|
if (sessions.get(session.sessionId) !== session) {
|
|
683
939
|
throw new PythonExecutionCancelledError(false);
|
|
@@ -686,7 +942,7 @@ async function replaceSessionKernel(
|
|
|
686
942
|
session.kernelInstanceId = crypto.randomUUID();
|
|
687
943
|
session.bridgeCapability = nextCapability;
|
|
688
944
|
} catch (err) {
|
|
689
|
-
await next
|
|
945
|
+
if (startSucceeded && next) await shutdownOrRetainKernel(next, ownerIds).catch(() => undefined);
|
|
690
946
|
if (bridge && previousCapability && bridge.capability === nextCapability) {
|
|
691
947
|
bridge.capability = previousCapability;
|
|
692
948
|
}
|
|
@@ -697,14 +953,26 @@ async function replaceSessionKernel(
|
|
|
697
953
|
async function resetSession(sessionId: string): Promise<void> {
|
|
698
954
|
const existing = sessions.get(sessionId);
|
|
699
955
|
if (!existing) return;
|
|
700
|
-
if (sessions.get(sessionId) === existing) sessions.delete(sessionId);
|
|
701
956
|
if (isInitializingSession(existing)) {
|
|
957
|
+
existing.cancelled ??= new PythonExecutionCancelledError(false);
|
|
958
|
+
let cleanup: Promise<KernelShutdownResult> | undefined;
|
|
959
|
+
if (existing.kernel) {
|
|
960
|
+
const prepared = prepareRetiringKernelShutdown(existing.kernel);
|
|
961
|
+
cleanup = prepared[0];
|
|
962
|
+
existing.cleanupAttempt = cleanup;
|
|
963
|
+
prepared[1]();
|
|
964
|
+
}
|
|
965
|
+
if (!existing.startupController.signal.aborted) existing.startupController.abort(existing.cancelled);
|
|
702
966
|
await existing.promise.catch(() => undefined);
|
|
967
|
+
if (existing.cleanupFailed) throw existing.cleanupError;
|
|
968
|
+
if (cleanup) await cleanup;
|
|
703
969
|
return;
|
|
704
970
|
}
|
|
971
|
+
const kernel = existing.kernel;
|
|
705
972
|
const shutdown = prepareSessionShutdown(existing);
|
|
706
973
|
startSessionShutdown(existing);
|
|
707
|
-
await shutdown
|
|
974
|
+
await shutdown;
|
|
975
|
+
if (sessions.get(sessionId) === existing && existing.kernel === kernel) sessions.delete(sessionId);
|
|
708
976
|
}
|
|
709
977
|
|
|
710
978
|
async function runQueued<T>(
|
|
@@ -743,6 +1011,17 @@ export function disposeAllKernelSessions(): Promise<void> {
|
|
|
743
1011
|
]);
|
|
744
1012
|
const shutdowns = [...targets].map(session => [session, prepareSessionShutdown(session)] as const);
|
|
745
1013
|
const retiring = [...retiringKernels].map(kernel => [kernel, ...prepareRetiringKernelShutdown(kernel)] as const);
|
|
1014
|
+
for (const request of requests) {
|
|
1015
|
+
for (const kernel of request.kernels) {
|
|
1016
|
+
const cleanup = retiring.find(([retiringKernel]) => retiringKernel === kernel)?.[1];
|
|
1017
|
+
if (cleanup) request.cleanupAttempts.set(kernel, cleanup);
|
|
1018
|
+
}
|
|
1019
|
+
}
|
|
1020
|
+
for (const entry of initializing) {
|
|
1021
|
+
if (!entry.kernel) continue;
|
|
1022
|
+
const cleanup = retiring.find(([kernel]) => kernel === entry.kernel)?.[1];
|
|
1023
|
+
if (cleanup) entry.cleanupAttempt = cleanup;
|
|
1024
|
+
}
|
|
746
1025
|
for (const [id, entry] of entries) {
|
|
747
1026
|
if (sessions.get(id) === entry) sessions.delete(id);
|
|
748
1027
|
}
|
|
@@ -750,6 +1029,10 @@ export function disposeAllKernelSessions(): Promise<void> {
|
|
|
750
1029
|
for (const request of requests) {
|
|
751
1030
|
if (!request.controller.signal.aborted) request.controller.abort(new PythonExecutionCancelledError(false));
|
|
752
1031
|
}
|
|
1032
|
+
for (const entry of initializing) {
|
|
1033
|
+
entry.cancelled ??= new PythonExecutionCancelledError(false);
|
|
1034
|
+
if (!entry.startupController.signal.aborted) entry.startupController.abort(entry.cancelled);
|
|
1035
|
+
}
|
|
753
1036
|
for (const [session] of shutdowns) startSessionShutdown(session);
|
|
754
1037
|
for (const [, , start] of retiring) start();
|
|
755
1038
|
return (async () => {
|
|
@@ -791,23 +1074,61 @@ export function disposeKernelSessionsByOwner(ownerId: string): Promise<void> {
|
|
|
791
1074
|
),
|
|
792
1075
|
]);
|
|
793
1076
|
const toShutdown: PythonSession[] = [];
|
|
794
|
-
|
|
795
|
-
for (const
|
|
796
|
-
|
|
797
|
-
|
|
1077
|
+
const initializersToAbort: InitializingPythonSession[] = [];
|
|
1078
|
+
for (const entry of initializing) {
|
|
1079
|
+
entry.retirementOwnerIds.add(ownerId);
|
|
1080
|
+
let hasOwnerRequest = false;
|
|
1081
|
+
for (const request of entry.requests) {
|
|
1082
|
+
if (request.ownerId === ownerId && !request.signal?.aborted) {
|
|
1083
|
+
hasOwnerRequest = true;
|
|
1084
|
+
break;
|
|
1085
|
+
}
|
|
1086
|
+
}
|
|
1087
|
+
if (!hasOwnerRequest) {
|
|
1088
|
+
entry.ownerIds.delete(ownerId);
|
|
1089
|
+
if (entry.ownerIds.size > 0) removeKernelOwner(entry.kernel, ownerId);
|
|
1090
|
+
}
|
|
1091
|
+
if (![...entry.ownerIds].some(id => id !== ownerId)) {
|
|
1092
|
+
entry.cancelled ??= new PythonExecutionCancelledError(false);
|
|
1093
|
+
initializersToAbort.push(entry);
|
|
1094
|
+
}
|
|
1095
|
+
}
|
|
798
1096
|
for (const session of sessionsToRetire) {
|
|
799
1097
|
if (session.ownerIds.size === 1) {
|
|
800
1098
|
toShutdown.push(session);
|
|
801
1099
|
if (sessions.get(session.sessionId) === session) sessions.delete(session.sessionId);
|
|
802
1100
|
} else {
|
|
803
1101
|
session.ownerIds.delete(ownerId);
|
|
1102
|
+
session.durableOwnerIds.delete(ownerId);
|
|
1103
|
+
removeKernelOwner(session.kernel, ownerId);
|
|
804
1104
|
}
|
|
805
1105
|
}
|
|
806
1106
|
const shutdowns = toShutdown.map(session => [session, prepareSessionShutdown(session)] as const);
|
|
807
|
-
const retiring = [
|
|
808
|
-
|
|
809
|
-
|
|
1107
|
+
const retiring: Array<readonly [PythonKernel, Promise<KernelShutdownResult>, () => void]> = [];
|
|
1108
|
+
for (const kernel of retiringKernels) {
|
|
1109
|
+
const owners = retiringKernelOwners.get(kernel);
|
|
1110
|
+
if (!owners?.has(ownerId)) continue;
|
|
1111
|
+
if (owners.size > 1) {
|
|
1112
|
+
owners.delete(ownerId);
|
|
1113
|
+
continue;
|
|
1114
|
+
}
|
|
1115
|
+
retiring.push([kernel, ...prepareRetiringKernelShutdown(kernel)]);
|
|
1116
|
+
}
|
|
1117
|
+
for (const request of requests) {
|
|
1118
|
+
for (const kernel of request.kernels) {
|
|
1119
|
+
const cleanup = retiring.find(([retiringKernel]) => retiringKernel === kernel)?.[1];
|
|
1120
|
+
if (cleanup) request.cleanupAttempts.set(kernel, cleanup);
|
|
1121
|
+
}
|
|
1122
|
+
}
|
|
1123
|
+
for (const entry of initializing) {
|
|
1124
|
+
if (!entry.kernel) continue;
|
|
1125
|
+
const cleanup = retiring.find(([kernel]) => kernel === entry.kernel)?.[1];
|
|
1126
|
+
if (cleanup) entry.cleanupAttempt = cleanup;
|
|
1127
|
+
}
|
|
810
1128
|
// All cleanup futures are visible before an abort listener can reenter this API.
|
|
1129
|
+
for (const entry of initializersToAbort) {
|
|
1130
|
+
if (!entry.startupController.signal.aborted) entry.startupController.abort(entry.cancelled);
|
|
1131
|
+
}
|
|
811
1132
|
for (const request of requests) {
|
|
812
1133
|
if (!request.controller.signal.aborted) request.controller.abort(new PythonExecutionCancelledError(false));
|
|
813
1134
|
}
|
|
@@ -830,6 +1151,7 @@ export function disposeKernelSessionsByOwner(ownerId: string): Promise<void> {
|
|
|
830
1151
|
const result = results[index];
|
|
831
1152
|
if (result.status === "fulfilled" && result.value.confirmed) {
|
|
832
1153
|
session.ownerIds.delete(ownerId);
|
|
1154
|
+
session.durableOwnerIds.delete(ownerId);
|
|
833
1155
|
continue;
|
|
834
1156
|
}
|
|
835
1157
|
if (!sessions.has(session.sessionId)) sessions.set(session.sessionId, session);
|
|
@@ -843,6 +1165,19 @@ export function disposeKernelSessionsByOwner(ownerId: string): Promise<void> {
|
|
|
843
1165
|
if (result.status === "fulfilled" && result.value.confirmed) continue;
|
|
844
1166
|
failures.push(result.status === "rejected" ? result.reason : unconfirmedShutdownError(kernel));
|
|
845
1167
|
}
|
|
1168
|
+
for (const [id, entry] of sessions) {
|
|
1169
|
+
if (
|
|
1170
|
+
isInitializingSession(entry) &&
|
|
1171
|
+
entry.retirementOwnerIds.has(ownerId) &&
|
|
1172
|
+
entry.cleanupFailed &&
|
|
1173
|
+
entry.kernel &&
|
|
1174
|
+
!retiringKernels.has(entry.kernel)
|
|
1175
|
+
) {
|
|
1176
|
+
entry.cleanupFailed = false;
|
|
1177
|
+
entry.cleanupError = undefined;
|
|
1178
|
+
if (sessions.get(id) === entry) sessions.delete(id);
|
|
1179
|
+
}
|
|
1180
|
+
}
|
|
846
1181
|
if (failures.length) throw failures[0];
|
|
847
1182
|
})();
|
|
848
1183
|
}
|
|
@@ -954,7 +1289,7 @@ async function executeWithKernel(
|
|
|
954
1289
|
...(await sink.dump()),
|
|
955
1290
|
};
|
|
956
1291
|
} catch (err) {
|
|
957
|
-
if (isCancellationError(err)
|
|
1292
|
+
if (isCancellationError(err)) {
|
|
958
1293
|
const timedOut = isTimedOutCancellation(err, options?.signal);
|
|
959
1294
|
return {
|
|
960
1295
|
exitCode: undefined,
|
|
@@ -1006,21 +1341,67 @@ async function ensureToolBridge(options: PythonExecutorOptions): Promise<void> {
|
|
|
1006
1341
|
}
|
|
1007
1342
|
}
|
|
1008
1343
|
|
|
1009
|
-
async function executePerCall(
|
|
1344
|
+
async function executePerCall(
|
|
1345
|
+
code: string,
|
|
1346
|
+
cwd: string,
|
|
1347
|
+
options: PythonExecutorOptions,
|
|
1348
|
+
request: ActivePythonRequest,
|
|
1349
|
+
): Promise<PythonResult> {
|
|
1010
1350
|
throwIfExecutionCancelled(options);
|
|
1011
1351
|
if (options.bridge && !options.bridgeSessionId) {
|
|
1012
1352
|
options.bridgeSessionId = `py-bridge:${crypto.randomUUID()}`;
|
|
1013
1353
|
}
|
|
1014
|
-
const
|
|
1354
|
+
const ownerIds = new Set(options.kernelOwnerId === undefined ? [] : [options.kernelOwnerId]);
|
|
1355
|
+
let physicalKernel: PythonKernel | undefined;
|
|
1356
|
+
const kernel = await startKernel(cwd, options, {
|
|
1357
|
+
onKernelCreated: created => {
|
|
1358
|
+
physicalKernel = created;
|
|
1359
|
+
request.kernels.add(created);
|
|
1360
|
+
retainKernelForOwners(created, ownerIds);
|
|
1361
|
+
},
|
|
1362
|
+
onStartupFailure: async failed => {
|
|
1363
|
+
const cleanup = request.cleanupAttempts.get(failed);
|
|
1364
|
+
if (cleanup) await cleanup;
|
|
1365
|
+
else await shutdownOrRetainKernel(failed, ownerIds);
|
|
1366
|
+
},
|
|
1367
|
+
});
|
|
1368
|
+
const createdKernel = physicalKernel;
|
|
1369
|
+
if (!createdKernel || createdKernel !== kernel) {
|
|
1370
|
+
await shutdownOrRetainKernel(kernel, ownerIds);
|
|
1371
|
+
throw new Error("Python kernel creation was not registered");
|
|
1372
|
+
}
|
|
1373
|
+
let execution:
|
|
1374
|
+
| { readonly succeeded: true; readonly result: PythonResult }
|
|
1375
|
+
| { readonly succeeded: false; readonly error: unknown };
|
|
1015
1376
|
try {
|
|
1016
1377
|
throwIfExecutionCancelled(options);
|
|
1017
|
-
|
|
1018
|
-
}
|
|
1019
|
-
|
|
1378
|
+
execution = { succeeded: true, result: await executeWithKernel(kernel, code, options) };
|
|
1379
|
+
} catch (error) {
|
|
1380
|
+
execution = { succeeded: false, error };
|
|
1381
|
+
}
|
|
1382
|
+
|
|
1383
|
+
let cleanup: { readonly succeeded: true } | { readonly succeeded: false; readonly error: unknown };
|
|
1384
|
+
try {
|
|
1385
|
+
const cleanupAttempt = request.cleanupAttempts.get(createdKernel);
|
|
1386
|
+
if (cleanupAttempt) await cleanupAttempt;
|
|
1387
|
+
else await shutdownOrRetainKernel(createdKernel, ownerIds);
|
|
1388
|
+
cleanup = { succeeded: true };
|
|
1389
|
+
} catch (error) {
|
|
1390
|
+
cleanup = { succeeded: false, error: new PythonKernelCleanupError(createdKernel, error) };
|
|
1020
1391
|
}
|
|
1392
|
+
|
|
1393
|
+
if (!execution.succeeded && !isCancellationError(execution.error)) throw execution.error;
|
|
1394
|
+
if (!cleanup.succeeded) throw cleanup.error;
|
|
1395
|
+
if (!execution.succeeded) throw execution.error;
|
|
1396
|
+
return execution.result;
|
|
1021
1397
|
}
|
|
1022
1398
|
|
|
1023
|
-
async function executeOnSession(
|
|
1399
|
+
async function executeOnSession(
|
|
1400
|
+
code: string,
|
|
1401
|
+
cwd: string,
|
|
1402
|
+
options: PythonExecutorOptions,
|
|
1403
|
+
request: ActivePythonRequest,
|
|
1404
|
+
): Promise<PythonResult> {
|
|
1024
1405
|
throwIfExecutionCancelled(options);
|
|
1025
1406
|
const sessionId = scopedSessionId(
|
|
1026
1407
|
options.sessionId ?? `session:${cwd}`,
|
|
@@ -1035,7 +1416,7 @@ async function executeOnSession(code: string, cwd: string, options: PythonExecut
|
|
|
1035
1416
|
await resetSession(sessionId);
|
|
1036
1417
|
throwIfExecutionCancelled(options);
|
|
1037
1418
|
}
|
|
1038
|
-
const session = await acquireSession(sessionId, cwd, options);
|
|
1419
|
+
const session = await acquireSession(sessionId, cwd, options, request);
|
|
1039
1420
|
throwIfExecutionCancelled(options);
|
|
1040
1421
|
if (options.bridge && session.bridgeCapability) {
|
|
1041
1422
|
options.bridge.capability = session.bridgeCapability;
|
|
@@ -1086,7 +1467,7 @@ export async function executePythonWithKernel(
|
|
|
1086
1467
|
try {
|
|
1087
1468
|
return await executeWithKernel(kernel, code, tracked.options);
|
|
1088
1469
|
} catch (err) {
|
|
1089
|
-
if (isCancellationError(err)
|
|
1470
|
+
if (isCancellationError(err)) {
|
|
1090
1471
|
return createCancelledPythonResult(
|
|
1091
1472
|
isTimedOutCancellation(err, tracked.options.signal),
|
|
1092
1473
|
tracked.options.timeoutMs,
|
|
@@ -1111,11 +1492,12 @@ export async function executePython(code: string, options?: PythonExecutorOption
|
|
|
1111
1492
|
|
|
1112
1493
|
const kernelMode = executionOptions.kernelMode ?? "session";
|
|
1113
1494
|
if (kernelMode === "per-call") {
|
|
1114
|
-
return await executePerCall(code, cwd, executionOptions);
|
|
1495
|
+
return await executePerCall(code, cwd, executionOptions, tracked.request);
|
|
1115
1496
|
}
|
|
1116
|
-
return await executeOnSession(code, cwd, executionOptions);
|
|
1497
|
+
return await executeOnSession(code, cwd, executionOptions, tracked.request);
|
|
1117
1498
|
} catch (err) {
|
|
1118
|
-
if (
|
|
1499
|
+
if (err instanceof PythonKernelStartError || err instanceof PythonKernelCleanupError) throw err;
|
|
1500
|
+
if (isCancellationError(err)) {
|
|
1119
1501
|
return createCancelledPythonResult(isTimedOutCancellation(err, executionOptions.signal));
|
|
1120
1502
|
}
|
|
1121
1503
|
throw err;
|