@oh-my-pi/pi-coding-agent 15.0.2 → 15.1.1

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