@gajae-code/coding-agent 0.18.8 → 0.18.10

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.
Files changed (113) hide show
  1. package/CHANGELOG.md +100 -0
  2. package/dist/types/async/endpoint-id.d.ts +16 -0
  3. package/dist/types/async/support.d.ts +0 -14
  4. package/dist/types/commands/mcp-serve.d.ts +4 -0
  5. package/dist/types/discovery/mcp-json.d.ts +1 -0
  6. package/dist/types/eval/py/kernel.d.ts +18 -0
  7. package/dist/types/export/html/html-export-destination.d.ts +2 -0
  8. package/dist/types/extensibility/extensions/function-hooks-internal.d.ts +5 -0
  9. package/dist/types/extensibility/skill-management.d.ts +1 -0
  10. package/dist/types/gjc-runtime/psmux-detect.d.ts +3 -3
  11. package/dist/types/gjc-runtime/sidecar-bootstrap-env.d.ts +6 -0
  12. package/dist/types/gjc-runtime/state-writer.d.ts +3 -0
  13. package/dist/types/gjc-runtime/tmux-common.d.ts +8 -5
  14. package/dist/types/lsp/config.d.ts +8 -5
  15. package/dist/types/runtime-mcp/manager.d.ts +2 -1
  16. package/dist/types/runtime-mcp/mcp-redirect.d.ts +9 -0
  17. package/dist/types/runtime-mcp/oauth-public-url.d.ts +3 -1
  18. package/dist/types/runtime-mcp/project-host-values.d.ts +27 -0
  19. package/dist/types/sdk/broker/broker.d.ts +6 -1
  20. package/dist/types/sdk/broker/doctor-restart.d.ts +5 -0
  21. package/dist/types/sdk/broker/ensure.d.ts +14 -1
  22. package/dist/types/session/session-storage.d.ts +7 -0
  23. package/dist/types/skill-state/workflow-mutation-guard.d.ts +3 -0
  24. package/dist/types/task/isolation-dir.d.ts +26 -0
  25. package/dist/types/tools/ast-edit-write-path.d.ts +13 -0
  26. package/dist/types/tools/browser/cdp-url.d.ts +2 -0
  27. package/dist/types/tools/github-side-effect.d.ts +22 -0
  28. package/dist/types/tools/monitor-mutation.d.ts +9 -0
  29. package/package.json +7 -7
  30. package/src/async/endpoint-id.ts +26 -0
  31. package/src/async/job-manager.ts +19 -10
  32. package/src/async/support.ts +0 -25
  33. package/src/capability/index.ts +14 -16
  34. package/src/cli/doctor/repairs.ts +4 -1
  35. package/src/cli/doctor-supervisor.ts +3 -0
  36. package/src/cli/public-command-registry.ts +28 -6
  37. package/src/commands/mcp-serve.ts +7 -0
  38. package/src/commands/sdk.ts +453 -415
  39. package/src/config/file-lock.ts +9 -11
  40. package/src/config/model-profile-activation.ts +5 -5
  41. package/src/coordinator-mcp/server.ts +2 -0
  42. package/src/customization/inventory.ts +1 -13
  43. package/src/dap/config.ts +2 -2
  44. package/src/defaults/gjc/extensions/grok-cli-vendor/src/provider/register.ts +10 -6
  45. package/src/discovery/builtin.ts +3 -1
  46. package/src/discovery/helpers.ts +12 -9
  47. package/src/discovery/mcp-json.ts +20 -12
  48. package/src/eval/py/executor.ts +459 -77
  49. package/src/eval/py/kernel.ts +79 -13
  50. package/src/eval/py/tool-bridge.ts +4 -0
  51. package/src/export/html/html-export-destination.ts +16 -0
  52. package/src/export/html/index.ts +2 -6
  53. package/src/extensibility/extensions/function-hooks-internal.ts +27 -0
  54. package/src/extensibility/extensions/runner.ts +11 -1
  55. package/src/extensibility/gjc-plugins/mcp-policy.ts +2 -1
  56. package/src/extensibility/gjc-plugins/runtime-adapters.ts +27 -15
  57. package/src/extensibility/skill-management.ts +3 -0
  58. package/src/gjc-runtime/launch-worktree.ts +56 -5
  59. package/src/gjc-runtime/psmux-detect.ts +28 -4
  60. package/src/gjc-runtime/session-state-sidecar.ts +3 -2
  61. package/src/gjc-runtime/sidecar-bootstrap-env.ts +19 -0
  62. package/src/gjc-runtime/state-writer.ts +22 -8
  63. package/src/gjc-runtime/tmux-common.ts +8 -5
  64. package/src/internal-urls/docs-index.generated.ts +7 -7
  65. package/src/lsp/config.ts +12 -63
  66. package/src/memories/index.ts +88 -7
  67. package/src/modes/components/hook-editor.ts +1 -1
  68. package/src/modes/components/hook-selector.ts +1 -1
  69. package/src/modes/controllers/input-controller.ts +2 -2
  70. package/src/modes/controllers/runtime-mcp-command-controller.ts +21 -13
  71. package/src/modes/controllers/selector-controller.ts +3 -7
  72. package/src/modes/controllers/todo-command-controller.ts +1 -1
  73. package/src/modes/interactive-mode.ts +1 -1
  74. package/src/runtime-mcp/client.ts +4 -2
  75. package/src/runtime-mcp/json-rpc.ts +2 -1
  76. package/src/runtime-mcp/manager.ts +29 -11
  77. package/src/runtime-mcp/mcp-redirect.ts +73 -0
  78. package/src/runtime-mcp/oauth-flow.ts +11 -4
  79. package/src/runtime-mcp/oauth-public-url.ts +8 -2
  80. package/src/runtime-mcp/pool.ts +6 -0
  81. package/src/runtime-mcp/project-host-values.ts +46 -0
  82. package/src/runtime-mcp/transports/http.ts +2 -1
  83. package/src/sdk/acp/adapter.ts +2 -0
  84. package/src/sdk/broker/broker.ts +6 -0
  85. package/src/sdk/broker/doctor-restart.ts +4 -0
  86. package/src/sdk/broker/ensure.ts +29 -2
  87. package/src/sdk/host/session-runtime.test.ts +58 -9
  88. package/src/sdk/host/session-runtime.ts +65 -35
  89. package/src/sdk/router/session-router.ts +7 -6
  90. package/src/sdk/session.ts +2 -7
  91. package/src/session/agent-session.ts +59 -17
  92. package/src/session/artifacts.ts +3 -2
  93. package/src/session/internal/managed-session-scope.ts +403 -81
  94. package/src/session/internal/managed-session-storage.ts +125 -17
  95. package/src/session/session-manager.ts +12 -0
  96. package/src/session/session-storage.ts +30 -1
  97. package/src/skill-state/active-state.ts +13 -0
  98. package/src/skill-state/workflow-mutation-guard.ts +102 -10
  99. package/src/slash-commands/builtin-registry.ts +1 -1
  100. package/src/slash-commands/helpers/mcp.ts +8 -4
  101. package/src/task/isolation-dir.ts +143 -0
  102. package/src/task/worktree.ts +16 -7
  103. package/src/tools/ast-edit-write-path.ts +68 -0
  104. package/src/tools/ast-edit.ts +7 -2
  105. package/src/tools/browser/cdp-url.ts +18 -0
  106. package/src/tools/browser.ts +8 -1
  107. package/src/tools/gh.ts +10 -0
  108. package/src/tools/github-side-effect.ts +112 -0
  109. package/src/tools/monitor-mutation.ts +27 -0
  110. package/src/tools/monitor.ts +7 -0
  111. package/src/tools/python.ts +7 -6
  112. package/src/utils/external-editor.ts +3 -2
  113. package/src/web/insane/url-guard.ts +2 -1
@@ -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
- async function shutdownOrRetainKernel(kernel: PythonKernel, ownerIds: Set<string>): Promise<void> {
200
- let result: KernelShutdownResult;
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
- result = await callKernelShutdown(kernel);
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
- session.cleanupStart = () => void confirmedKernelShutdown(session.kernel).then(resolve, reject);
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(kernel: PythonKernel): [Promise<KernelShutdownResult>, () => void] {
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 ownerId =
327
- capturedOptions.kernelOwnerId ??
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(cwd: string, options: PythonExecutorOptions): Promise<PythonKernel> {
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
- async function acquireSession(sessionId: string, cwd: string, options: PythonExecutorOptions): Promise<PythonSession> {
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
- attachOwner(existing, sessionId, options.kernelOwnerId);
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
- if (
547
- isCancellationError(error) &&
548
- !options.signal?.aborted &&
549
- (remainingMs === undefined || remainingMs > 0)
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
- throwIfExecutionCancelled(options);
566
- const kernel = await startKernel(cwd, options);
567
- initializing.kernel = kernel;
568
- if (initializing.cancelled || options.signal?.aborted) {
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 shutdownOrRetainKernel(kernel, initializing.ownerIds);
809
+ await shutdownInitializingKernel(initializing, kernel);
571
810
  } catch (error) {
572
- initializing.cleanupError = error;
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(isTimedOutCancellation(options.signal?.reason, options.signal))
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 shutdownOrRetainKernel(kernel, initializing.ownerIds);
823
+ await shutdownInitializingKernel(initializing, kernel);
584
824
  } catch (error) {
585
- initializing.cleanupError = error;
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
- attachOwner(initializing, sessionId, options.kernelOwnerId);
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
- if (initializing.cancelled) return;
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
- await old
666
- .shutdown(remaining !== undefined ? { timeoutMs: Math.max(0, remaining) } : undefined)
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?.shutdown().catch(() => undefined);
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.catch(() => undefined);
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
- for (const entry of initializing) entry.retirementOwnerIds.add(ownerId);
795
- for (const [id, entry] of entries)
796
- if (isInitializingSession(entry) && entry.ownerIds.size === 1) sessions.delete(id);
797
- for (const entry of initializing) entry.ownerIds.delete(ownerId);
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 = [...retiringKernels]
808
- .filter(kernel => retiringKernelOwners.get(kernel)?.has(ownerId))
809
- .map(kernel => [kernel, ...prepareRetiringKernelShutdown(kernel)] as const);
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) || options?.signal?.aborted) {
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(code: string, cwd: string, options: PythonExecutorOptions): Promise<PythonResult> {
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 kernel = await startKernel(cwd, options);
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
- return await executeWithKernel(kernel, code, options);
1018
- } finally {
1019
- await kernel.shutdown().catch(() => undefined);
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(code: string, cwd: string, options: PythonExecutorOptions): Promise<PythonResult> {
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) || tracked.options.signal?.aborted) {
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 (isCancellationError(err) || executionOptions.signal?.aborted) {
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;