@oh-my-pi/pi-coding-agent 14.1.0 → 14.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +79 -0
- package/package.json +8 -8
- package/src/async/job-manager.ts +43 -10
- package/src/commit/agentic/tools/analyze-file.ts +1 -2
- package/src/config/mcp-schema.json +1 -1
- package/src/config/model-equivalence.ts +1 -0
- package/src/config/model-registry.ts +63 -34
- package/src/config/model-resolver.ts +111 -15
- package/src/config/settings-schema.ts +4 -3
- package/src/config/settings.ts +1 -1
- package/src/cursor.ts +64 -23
- package/src/edit/index.ts +254 -89
- package/src/edit/modes/chunk.ts +336 -57
- package/src/edit/modes/hashline.ts +51 -26
- package/src/edit/modes/patch.ts +16 -10
- package/src/edit/modes/replace.ts +15 -7
- package/src/edit/renderer.ts +248 -94
- package/src/export/html/template.generated.ts +1 -1
- package/src/export/html/template.js +6 -4
- package/src/extensibility/custom-tools/types.ts +0 -3
- package/src/extensibility/extensions/loader.ts +16 -0
- package/src/extensibility/extensions/runner.ts +2 -7
- package/src/extensibility/extensions/types.ts +8 -4
- package/src/internal-urls/docs-index.generated.ts +3 -3
- package/src/ipy/executor.ts +447 -52
- package/src/ipy/kernel.ts +39 -13
- package/src/lsp/client.ts +54 -0
- package/src/lsp/index.ts +8 -0
- package/src/lsp/types.ts +6 -0
- package/src/main.ts +0 -1
- package/src/modes/acp/acp-agent.ts +4 -1
- package/src/modes/components/bash-execution.ts +16 -4
- package/src/modes/components/status-line/presets.ts +17 -6
- package/src/modes/components/status-line/segments.ts +15 -0
- package/src/modes/components/status-line-segment-editor.ts +1 -0
- package/src/modes/components/status-line.ts +7 -1
- package/src/modes/components/tool-execution.ts +145 -75
- package/src/modes/controllers/command-controller.ts +24 -1
- package/src/modes/controllers/event-controller.ts +4 -1
- package/src/modes/controllers/extension-ui-controller.ts +28 -5
- package/src/modes/controllers/input-controller.ts +9 -3
- package/src/modes/controllers/selector-controller.ts +4 -1
- package/src/modes/interactive-mode.ts +19 -3
- package/src/modes/print-mode.ts +13 -4
- package/src/modes/prompt-action-autocomplete.ts +3 -5
- package/src/modes/rpc/rpc-mode.ts +8 -2
- package/src/modes/shared.ts +2 -2
- package/src/modes/types.ts +1 -0
- package/src/modes/utils/ui-helpers.ts +1 -0
- package/src/prompts/tools/bash.md +2 -2
- package/src/prompts/tools/chunk-edit.md +191 -163
- package/src/prompts/tools/hashline.md +11 -11
- package/src/prompts/tools/patch.md +10 -5
- package/src/prompts/tools/{await.md → poll.md} +1 -1
- package/src/prompts/tools/read-chunk.md +3 -3
- package/src/prompts/tools/task.md +2 -2
- package/src/prompts/tools/vim.md +98 -0
- package/src/sdk.ts +754 -724
- package/src/session/agent-session.ts +164 -34
- package/src/session/session-manager.ts +50 -4
- package/src/slash-commands/builtin-registry.ts +17 -0
- package/src/task/executor.ts +4 -4
- package/src/task/index.ts +3 -5
- package/src/task/types.ts +2 -2
- package/src/tools/bash.ts +26 -8
- package/src/tools/find.ts +5 -2
- package/src/tools/grep.ts +77 -8
- package/src/tools/index.ts +48 -19
- package/src/tools/{await-tool.ts → poll-tool.ts} +36 -30
- package/src/tools/python.ts +293 -278
- package/src/tools/submit-result.ts +5 -2
- package/src/tools/todo-write.ts +8 -2
- package/src/tools/vim.ts +966 -0
- package/src/utils/edit-mode.ts +2 -1
- package/src/utils/session-color.ts +55 -0
- package/src/utils/title-generator.ts +15 -6
- package/src/vim/buffer.ts +309 -0
- package/src/vim/commands.ts +382 -0
- package/src/vim/engine.ts +2426 -0
- package/src/vim/parser.ts +151 -0
- package/src/vim/render.ts +252 -0
- package/src/vim/types.ts +197 -0
package/src/ipy/executor.ts
CHANGED
|
@@ -16,6 +16,7 @@ import { PYTHON_PRELUDE } from "./prelude";
|
|
|
16
16
|
const IDLE_TIMEOUT_MS = 5 * 60 * 1000; // 5 minutes
|
|
17
17
|
const MAX_KERNEL_SESSIONS = 4;
|
|
18
18
|
const CLEANUP_INTERVAL_MS = 30 * 1000; // 30 seconds
|
|
19
|
+
const OWNER_CLEANUP_KERNEL_SHUTDOWN_TIMEOUT_MS = 2_000;
|
|
19
20
|
|
|
20
21
|
export type PythonKernelMode = "session" | "per-call";
|
|
21
22
|
|
|
@@ -32,6 +33,8 @@ export interface PythonExecutorOptions {
|
|
|
32
33
|
signal?: AbortSignal;
|
|
33
34
|
/** Session identifier for kernel reuse */
|
|
34
35
|
sessionId?: string;
|
|
36
|
+
/** Logical owner identifier for retained kernel cleanup */
|
|
37
|
+
kernelOwnerId?: string;
|
|
35
38
|
/** Kernel mode (session reuse vs per-call) */
|
|
36
39
|
kernelMode?: PythonKernelMode;
|
|
37
40
|
/** Restart the kernel before executing */
|
|
@@ -80,11 +83,24 @@ interface KernelSession {
|
|
|
80
83
|
queue: Promise<void>;
|
|
81
84
|
restartCount: number;
|
|
82
85
|
dead: boolean;
|
|
86
|
+
needsRestart: boolean;
|
|
87
|
+
kernelInvalidatedByRecovery: boolean;
|
|
88
|
+
disposing: boolean;
|
|
89
|
+
disposeCapacityPromise?: Promise<void>;
|
|
90
|
+
resolveDisposeCapacity?: () => void;
|
|
91
|
+
disposeAttemptPromise?: Promise<void>;
|
|
92
|
+
resolveDisposeAttempt?: () => void;
|
|
93
|
+
disposeResultPromise?: Promise<KernelDisposalResult>;
|
|
94
|
+
disposeResultTimeoutMs?: number;
|
|
95
|
+
nextDisposalRetryAt?: number;
|
|
83
96
|
lastUsedAt: number;
|
|
97
|
+
ownerIds: Set<string>;
|
|
98
|
+
hasFallbackOwner: boolean;
|
|
84
99
|
heartbeatTimer?: NodeJS.Timeout;
|
|
85
100
|
}
|
|
86
101
|
|
|
87
102
|
const kernelSessions = new Map<string, KernelSession>();
|
|
103
|
+
const disposingKernelSessions = new Set<KernelSession>();
|
|
88
104
|
let cachedPreludeDocs: PreludeHelper[] | null = null;
|
|
89
105
|
let cleanupTimer: NodeJS.Timeout | null = null;
|
|
90
106
|
|
|
@@ -93,6 +109,7 @@ interface KernelSessionExecutionOptions {
|
|
|
93
109
|
sessionFile?: string;
|
|
94
110
|
signal?: AbortSignal;
|
|
95
111
|
deadlineMs?: number;
|
|
112
|
+
kernelOwnerId?: string;
|
|
96
113
|
}
|
|
97
114
|
|
|
98
115
|
class PythonExecutionCancelledError extends Error {
|
|
@@ -142,10 +159,10 @@ function isTimedOutCancellation(error: unknown, signal?: AbortSignal): boolean {
|
|
|
142
159
|
return reason instanceof Error ? reason.name === "TimeoutError" : false;
|
|
143
160
|
}
|
|
144
161
|
|
|
145
|
-
async function
|
|
146
|
-
|
|
162
|
+
async function waitForPromiseWithCancellation<T>(
|
|
163
|
+
promise: Promise<T>,
|
|
147
164
|
options: Pick<KernelSessionExecutionOptions, "signal" | "deadlineMs">,
|
|
148
|
-
): Promise<
|
|
165
|
+
): Promise<T> {
|
|
149
166
|
if (options.signal?.aborted) {
|
|
150
167
|
throw new PythonExecutionCancelledError(isTimedOutCancellation(options.signal.reason, options.signal));
|
|
151
168
|
}
|
|
@@ -156,11 +173,10 @@ async function waitForQueueTurn(
|
|
|
156
173
|
}
|
|
157
174
|
|
|
158
175
|
if (!options.signal && remainingMs === undefined) {
|
|
159
|
-
await
|
|
160
|
-
return;
|
|
176
|
+
return await promise;
|
|
161
177
|
}
|
|
162
178
|
|
|
163
|
-
await new Promise<
|
|
179
|
+
return await new Promise<T>((resolve, reject) => {
|
|
164
180
|
const cleanups: Array<() => void> = [];
|
|
165
181
|
const finish = (callback: () => void) => {
|
|
166
182
|
while (cleanups.length > 0) {
|
|
@@ -188,13 +204,20 @@ async function waitForQueueTurn(
|
|
|
188
204
|
cleanups.push(() => clearTimeout(timeout));
|
|
189
205
|
}
|
|
190
206
|
|
|
191
|
-
|
|
192
|
-
() =>
|
|
207
|
+
promise.then(
|
|
208
|
+
value => finish(() => resolve(value)),
|
|
193
209
|
error => finish(() => reject(error)),
|
|
194
210
|
);
|
|
195
211
|
});
|
|
196
212
|
}
|
|
197
213
|
|
|
214
|
+
async function waitForQueueTurn(
|
|
215
|
+
queue: Promise<void>,
|
|
216
|
+
options: Pick<KernelSessionExecutionOptions, "signal" | "deadlineMs">,
|
|
217
|
+
): Promise<void> {
|
|
218
|
+
await waitForPromiseWithCancellation(queue, options);
|
|
219
|
+
}
|
|
220
|
+
|
|
198
221
|
function formatTimeoutAnnotation(timeoutMs?: number): string | undefined {
|
|
199
222
|
if (timeoutMs === undefined) return "Command timed out";
|
|
200
223
|
const secs = Math.max(1, Math.round(timeoutMs / 1000));
|
|
@@ -352,6 +375,131 @@ function stopCleanupTimer(): void {
|
|
|
352
375
|
}
|
|
353
376
|
}
|
|
354
377
|
|
|
378
|
+
function attachKernelOwner(sessionId: string, ownerId?: string): boolean {
|
|
379
|
+
const session = kernelSessions.get(sessionId);
|
|
380
|
+
if (!session || session.disposing) return false;
|
|
381
|
+
if (ownerId !== undefined) {
|
|
382
|
+
if (session.hasFallbackOwner) {
|
|
383
|
+
session.ownerIds.delete(sessionId);
|
|
384
|
+
session.hasFallbackOwner = false;
|
|
385
|
+
}
|
|
386
|
+
session.ownerIds.add(ownerId);
|
|
387
|
+
} else if (session.hasFallbackOwner || session.ownerIds.size === 0) {
|
|
388
|
+
session.ownerIds.add(sessionId);
|
|
389
|
+
session.hasFallbackOwner = true;
|
|
390
|
+
}
|
|
391
|
+
session.lastUsedAt = Date.now();
|
|
392
|
+
return true;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
function getRetainedKernelSessionCount(): number {
|
|
396
|
+
return kernelSessions.size + disposingKernelSessions.size;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
function syncCleanupTimer(): void {
|
|
400
|
+
if (kernelSessions.size === 0 && disposingKernelSessions.size === 0) {
|
|
401
|
+
stopCleanupTimer();
|
|
402
|
+
return;
|
|
403
|
+
}
|
|
404
|
+
startCleanupTimer();
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
function retryPendingKernelSessionDisposals(now: number = Date.now()): void {
|
|
408
|
+
for (const session of disposingKernelSessions.values()) {
|
|
409
|
+
if (session.disposeResultPromise) continue;
|
|
410
|
+
if (session.nextDisposalRetryAt !== undefined && session.nextDisposalRetryAt > now) continue;
|
|
411
|
+
session.nextDisposalRetryAt = undefined;
|
|
412
|
+
void disposeKernelSession(session);
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
function beginDisposingKernelSession(session: KernelSession): boolean {
|
|
417
|
+
if (session.disposing) return false;
|
|
418
|
+
session.disposing = true;
|
|
419
|
+
disposingKernelSessions.add(session);
|
|
420
|
+
if (kernelSessions.get(session.id) === session) {
|
|
421
|
+
kernelSessions.delete(session.id);
|
|
422
|
+
}
|
|
423
|
+
if (session.heartbeatTimer) {
|
|
424
|
+
clearInterval(session.heartbeatTimer);
|
|
425
|
+
session.heartbeatTimer = undefined;
|
|
426
|
+
}
|
|
427
|
+
syncCleanupTimer();
|
|
428
|
+
return true;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
function finishDisposingKernelSession(session: KernelSession): void {
|
|
432
|
+
disposingKernelSessions.delete(session);
|
|
433
|
+
session.resolveDisposeCapacity?.();
|
|
434
|
+
session.resolveDisposeCapacity = undefined;
|
|
435
|
+
session.disposeCapacityPromise = undefined;
|
|
436
|
+
session.resolveDisposeAttempt = undefined;
|
|
437
|
+
session.disposeAttemptPromise = undefined;
|
|
438
|
+
session.disposeResultPromise = undefined;
|
|
439
|
+
session.disposeResultTimeoutMs = undefined;
|
|
440
|
+
session.nextDisposalRetryAt = undefined;
|
|
441
|
+
session.kernelInvalidatedByRecovery = false;
|
|
442
|
+
syncCleanupTimer();
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
async function waitForDisposalCapacity(
|
|
446
|
+
options: Pick<KernelSessionExecutionOptions, "signal" | "deadlineMs">,
|
|
447
|
+
): Promise<void> {
|
|
448
|
+
retryPendingKernelSessionDisposals();
|
|
449
|
+
|
|
450
|
+
const disposalPromises: Promise<void>[] = [];
|
|
451
|
+
let nextRetryAt: number | undefined;
|
|
452
|
+
for (const session of disposingKernelSessions.values()) {
|
|
453
|
+
if (session.disposeCapacityPromise) {
|
|
454
|
+
disposalPromises.push(
|
|
455
|
+
session.disposeCapacityPromise.then(
|
|
456
|
+
() => undefined,
|
|
457
|
+
() => undefined,
|
|
458
|
+
),
|
|
459
|
+
);
|
|
460
|
+
}
|
|
461
|
+
if (session.disposeAttemptPromise) {
|
|
462
|
+
disposalPromises.push(
|
|
463
|
+
session.disposeAttemptPromise.then(
|
|
464
|
+
() => undefined,
|
|
465
|
+
() => undefined,
|
|
466
|
+
),
|
|
467
|
+
);
|
|
468
|
+
}
|
|
469
|
+
if (session.nextDisposalRetryAt !== undefined) {
|
|
470
|
+
nextRetryAt =
|
|
471
|
+
nextRetryAt === undefined
|
|
472
|
+
? session.nextDisposalRetryAt
|
|
473
|
+
: Math.min(nextRetryAt, session.nextDisposalRetryAt);
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
if (disposalPromises.length > 0) {
|
|
477
|
+
await waitForPromiseWithCancellation(Promise.race(disposalPromises), options);
|
|
478
|
+
return;
|
|
479
|
+
}
|
|
480
|
+
if (nextRetryAt === undefined) return;
|
|
481
|
+
await waitForPromiseWithCancellation(
|
|
482
|
+
Bun.sleep(Math.max(0, nextRetryAt - Date.now())).then(() => undefined),
|
|
483
|
+
options,
|
|
484
|
+
);
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
async function ensureKernelSessionCapacity(
|
|
488
|
+
options: Pick<KernelSessionExecutionOptions, "signal" | "deadlineMs">,
|
|
489
|
+
): Promise<void> {
|
|
490
|
+
while (getRetainedKernelSessionCount() >= MAX_KERNEL_SESSIONS) {
|
|
491
|
+
if (disposingKernelSessions.size > 0) {
|
|
492
|
+
await waitForDisposalCapacity(options);
|
|
493
|
+
continue;
|
|
494
|
+
}
|
|
495
|
+
if (kernelSessions.size === 0) {
|
|
496
|
+
await waitForDisposalCapacity(options);
|
|
497
|
+
continue;
|
|
498
|
+
}
|
|
499
|
+
await evictOldestSession();
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
|
|
355
503
|
async function cleanupIdleSessions(): Promise<void> {
|
|
356
504
|
const now = Date.now();
|
|
357
505
|
const toDispose: KernelSession[] = [];
|
|
@@ -367,9 +515,8 @@ async function cleanupIdleSessions(): Promise<void> {
|
|
|
367
515
|
await Promise.allSettled(toDispose.map(session => disposeKernelSession(session)));
|
|
368
516
|
}
|
|
369
517
|
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
}
|
|
518
|
+
retryPendingKernelSessionDisposals(now);
|
|
519
|
+
syncCleanupTimer();
|
|
373
520
|
}
|
|
374
521
|
|
|
375
522
|
async function evictOldestSession(): Promise<void> {
|
|
@@ -387,12 +534,29 @@ async function evictOldestSession(): Promise<void> {
|
|
|
387
534
|
|
|
388
535
|
export async function disposeAllKernelSessions(): Promise<void> {
|
|
389
536
|
stopCleanupTimer();
|
|
390
|
-
const sessions = Array.from(kernelSessions.values());
|
|
537
|
+
const sessions = Array.from(new Set([...kernelSessions.values(), ...disposingKernelSessions.values()]));
|
|
391
538
|
await Promise.allSettled(sessions.map(session => disposeKernelSession(session)));
|
|
392
539
|
}
|
|
393
540
|
|
|
394
|
-
async function
|
|
395
|
-
const
|
|
541
|
+
export async function disposeKernelSessionsByOwner(ownerId: string): Promise<void> {
|
|
542
|
+
const sessionsToDispose: KernelSession[] = [];
|
|
543
|
+
for (const session of new Set([...kernelSessions.values(), ...disposingKernelSessions.values()])) {
|
|
544
|
+
if (!session.ownerIds.delete(ownerId)) continue;
|
|
545
|
+
if (session.ownerIds.size === 0) {
|
|
546
|
+
sessionsToDispose.push(session);
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
await Promise.allSettled(
|
|
550
|
+
sessionsToDispose.map(session => disposeKernelSession(session, OWNER_CLEANUP_KERNEL_SHUTDOWN_TIMEOUT_MS)),
|
|
551
|
+
);
|
|
552
|
+
syncCleanupTimer();
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
async function ensureKernelAvailable(
|
|
556
|
+
cwd: string,
|
|
557
|
+
options: Pick<KernelSessionExecutionOptions, "signal" | "deadlineMs"> = {},
|
|
558
|
+
): Promise<void> {
|
|
559
|
+
const availability = await waitForPromiseWithCancellation(checkPythonKernelAvailability(cwd), options);
|
|
396
560
|
if (!availability.ok) {
|
|
397
561
|
throw new Error(availability.reason ?? "Python kernel unavailable");
|
|
398
562
|
}
|
|
@@ -403,10 +567,13 @@ export async function warmPythonEnvironment(
|
|
|
403
567
|
sessionId?: string,
|
|
404
568
|
useSharedGateway?: boolean,
|
|
405
569
|
sessionFile?: string,
|
|
570
|
+
kernelOwnerId?: string,
|
|
571
|
+
signal?: AbortSignal,
|
|
406
572
|
): Promise<{ ok: boolean; reason?: string; docs: PreludeHelper[] }> {
|
|
407
573
|
let cacheState: PreludeCacheState | null = null;
|
|
574
|
+
const resolvedSessionId = sessionId ?? `session:${cwd}`;
|
|
408
575
|
try {
|
|
409
|
-
await logger.time("warmPython:ensureKernelAvailable", ensureKernelAvailable, cwd);
|
|
576
|
+
await logger.time("warmPython:ensureKernelAvailable", ensureKernelAvailable, cwd, { signal });
|
|
410
577
|
} catch (err: unknown) {
|
|
411
578
|
const reason = err instanceof Error ? err.message : String(err);
|
|
412
579
|
cachedPreludeDocs = [];
|
|
@@ -418,6 +585,7 @@ export async function warmPythonEnvironment(
|
|
|
418
585
|
const cached = await readPreludeCache(cacheState);
|
|
419
586
|
if (cached) {
|
|
420
587
|
cachedPreludeDocs = cached;
|
|
588
|
+
attachKernelOwner(resolvedSessionId, kernelOwnerId);
|
|
421
589
|
return { ok: true, docs: cached };
|
|
422
590
|
}
|
|
423
591
|
} catch (err) {
|
|
@@ -426,19 +594,21 @@ export async function warmPythonEnvironment(
|
|
|
426
594
|
}
|
|
427
595
|
}
|
|
428
596
|
if (cachedPreludeDocs && cachedPreludeDocs.length > 0) {
|
|
597
|
+
attachKernelOwner(resolvedSessionId, kernelOwnerId);
|
|
429
598
|
return { ok: true, docs: cachedPreludeDocs };
|
|
430
599
|
}
|
|
431
|
-
const resolvedSessionId = sessionId ?? `session:${cwd}`;
|
|
432
600
|
try {
|
|
433
601
|
const docs = await logger.time(
|
|
434
602
|
"warmPython:withKernelSession",
|
|
435
603
|
withKernelSession,
|
|
436
604
|
resolvedSessionId,
|
|
437
605
|
cwd,
|
|
438
|
-
kernel => ensurePreludeDocsLoaded(kernel, cwd, { useSharedGateway, sessionFile }, cacheState),
|
|
606
|
+
kernel => ensurePreludeDocsLoaded(kernel, cwd, { useSharedGateway, sessionFile, signal }, cacheState),
|
|
439
607
|
{
|
|
440
608
|
useSharedGateway,
|
|
441
609
|
sessionFile,
|
|
610
|
+
kernelOwnerId,
|
|
611
|
+
signal,
|
|
442
612
|
},
|
|
443
613
|
);
|
|
444
614
|
return { ok: true, docs };
|
|
@@ -471,17 +641,57 @@ function isResourceExhaustionError(error: unknown): boolean {
|
|
|
471
641
|
);
|
|
472
642
|
}
|
|
473
643
|
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
const sessions = Array.from(kernelSessions.values());
|
|
478
|
-
for (const session of sessions) {
|
|
644
|
+
function clearSharedGatewayDisposingKernelSessionTracking(): void {
|
|
645
|
+
for (const session of Array.from(disposingKernelSessions.values())) {
|
|
646
|
+
if (!session.kernel.isSharedGateway) continue;
|
|
479
647
|
if (session.heartbeatTimer) {
|
|
480
648
|
clearInterval(session.heartbeatTimer);
|
|
649
|
+
session.heartbeatTimer = undefined;
|
|
481
650
|
}
|
|
482
|
-
|
|
651
|
+
disposingKernelSessions.delete(session);
|
|
652
|
+
session.resolveDisposeCapacity?.();
|
|
653
|
+
session.resolveDisposeCapacity = undefined;
|
|
654
|
+
session.disposeCapacityPromise = undefined;
|
|
655
|
+
session.resolveDisposeAttempt?.();
|
|
656
|
+
session.resolveDisposeAttempt = undefined;
|
|
657
|
+
session.disposeAttemptPromise = undefined;
|
|
658
|
+
session.disposeResultPromise = undefined;
|
|
659
|
+
session.disposeResultTimeoutMs = undefined;
|
|
660
|
+
session.nextDisposalRetryAt = undefined;
|
|
661
|
+
session.kernelInvalidatedByRecovery = false;
|
|
483
662
|
}
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
function markLiveKernelSessionsForRecovery(): void {
|
|
666
|
+
for (const session of kernelSessions.values()) {
|
|
667
|
+
if (session.heartbeatTimer) {
|
|
668
|
+
clearInterval(session.heartbeatTimer);
|
|
669
|
+
session.heartbeatTimer = undefined;
|
|
670
|
+
}
|
|
671
|
+
session.needsRestart = true;
|
|
672
|
+
session.kernelInvalidatedByRecovery = session.kernel.isSharedGateway;
|
|
673
|
+
session.restartCount = 0;
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
async function recoverFromResourceExhaustion(): Promise<void> {
|
|
678
|
+
logger.warn("Resource exhaustion detected, recovering by restarting shared gateway");
|
|
679
|
+
stopCleanupTimer();
|
|
680
|
+
markLiveKernelSessionsForRecovery();
|
|
681
|
+
clearSharedGatewayDisposingKernelSessionTracking();
|
|
484
682
|
await shutdownSharedGateway();
|
|
683
|
+
syncCleanupTimer();
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
function ensureKernelHeartbeat(session: KernelSession): void {
|
|
687
|
+
if (session.heartbeatTimer) return;
|
|
688
|
+
session.heartbeatTimer = setInterval(() => {
|
|
689
|
+
if (session.dead || session.needsRestart) return;
|
|
690
|
+
if (!session.kernel.isAlive()) {
|
|
691
|
+
session.dead = true;
|
|
692
|
+
}
|
|
693
|
+
}, 5000);
|
|
694
|
+
session.heartbeatTimer.unref();
|
|
485
695
|
}
|
|
486
696
|
|
|
487
697
|
async function createKernelSession(
|
|
@@ -507,21 +717,25 @@ async function createKernelSession(
|
|
|
507
717
|
throw err;
|
|
508
718
|
}
|
|
509
719
|
|
|
720
|
+
const hasFallbackOwner = options.kernelOwnerId === undefined;
|
|
721
|
+
const initialOwnerId = options.kernelOwnerId ?? sessionId;
|
|
510
722
|
const session: KernelSession = {
|
|
511
723
|
id: sessionId,
|
|
512
724
|
kernel,
|
|
513
725
|
queue: Promise.resolve(),
|
|
514
726
|
restartCount: 0,
|
|
515
727
|
dead: false,
|
|
728
|
+
needsRestart: false,
|
|
729
|
+
kernelInvalidatedByRecovery: false,
|
|
730
|
+
disposing: false,
|
|
731
|
+
disposeResultPromise: undefined,
|
|
732
|
+
nextDisposalRetryAt: undefined,
|
|
516
733
|
lastUsedAt: Date.now(),
|
|
734
|
+
ownerIds: new Set([initialOwnerId]),
|
|
735
|
+
hasFallbackOwner,
|
|
517
736
|
};
|
|
518
737
|
|
|
519
|
-
session
|
|
520
|
-
if (session.dead) return;
|
|
521
|
-
if (!session.kernel.isAlive()) {
|
|
522
|
-
session.dead = true;
|
|
523
|
-
}
|
|
524
|
-
}, 5000);
|
|
738
|
+
ensureKernelHeartbeat(session);
|
|
525
739
|
|
|
526
740
|
return session;
|
|
527
741
|
}
|
|
@@ -537,30 +751,199 @@ async function restartKernelSession(
|
|
|
537
751
|
}
|
|
538
752
|
requireRemainingTimeoutMs(options.deadlineMs);
|
|
539
753
|
try {
|
|
540
|
-
|
|
754
|
+
if (!session.kernelInvalidatedByRecovery) {
|
|
755
|
+
const deadKernel = session.dead || !session.kernel.isAlive();
|
|
756
|
+
const shutdownTimeoutMs = requireRemainingTimeoutMs(options.deadlineMs);
|
|
757
|
+
const shutdownResult = await session.kernel.shutdown({ signal: options.signal, timeoutMs: shutdownTimeoutMs });
|
|
758
|
+
if (!shutdownResult.confirmed && !deadKernel) {
|
|
759
|
+
throw new Error("Failed to confirm crashed kernel shutdown before restart");
|
|
760
|
+
}
|
|
761
|
+
if (!shutdownResult.confirmed) {
|
|
762
|
+
logger.warn("Proceeding with retained kernel restart after unconfirmed dead-kernel shutdown", {
|
|
763
|
+
sessionId: session.id,
|
|
764
|
+
});
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
const env: Record<string, string> | undefined = options.sessionFile
|
|
768
|
+
? { PI_SESSION_FILE: options.sessionFile }
|
|
769
|
+
: undefined;
|
|
770
|
+
const startOptions = buildKernelStartOptions(cwd, env, options);
|
|
771
|
+
const kernel = await PythonKernel.start(startOptions);
|
|
772
|
+
session.kernel = kernel;
|
|
773
|
+
session.dead = false;
|
|
774
|
+
session.needsRestart = false;
|
|
775
|
+
session.kernelInvalidatedByRecovery = false;
|
|
776
|
+
session.lastUsedAt = Date.now();
|
|
777
|
+
ensureKernelHeartbeat(session);
|
|
541
778
|
} catch (err) {
|
|
542
|
-
|
|
779
|
+
session.restartCount = 0;
|
|
780
|
+
logger.warn("Failed to restart kernel", { error: err instanceof Error ? err.message : String(err) });
|
|
781
|
+
throw err;
|
|
543
782
|
}
|
|
544
|
-
const env: Record<string, string> | undefined = options.sessionFile
|
|
545
|
-
? { PI_SESSION_FILE: options.sessionFile }
|
|
546
|
-
: undefined;
|
|
547
|
-
const startOptions = buildKernelStartOptions(cwd, env, options);
|
|
548
|
-
const kernel = await PythonKernel.start(startOptions);
|
|
549
|
-
session.kernel = kernel;
|
|
550
|
-
session.dead = false;
|
|
551
|
-
session.lastUsedAt = Date.now();
|
|
552
783
|
}
|
|
553
784
|
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
785
|
+
type KernelDisposalResult = { status: "confirmed" } | { status: "unconfirmed" } | { status: "failed"; err: unknown };
|
|
786
|
+
type KernelDisposalWaitResult = KernelDisposalResult | { status: "timedOut" };
|
|
787
|
+
|
|
788
|
+
function createKernelDisposalResultPromise(session: KernelSession, timeoutMs?: number): Promise<KernelDisposalResult> {
|
|
789
|
+
if (session.kernelInvalidatedByRecovery) {
|
|
790
|
+
return Promise.resolve({ status: "confirmed" as const });
|
|
557
791
|
}
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
792
|
+
return Promise.resolve()
|
|
793
|
+
.then(() => session.kernel.shutdown(timeoutMs === undefined ? undefined : { timeoutMs }))
|
|
794
|
+
.then(
|
|
795
|
+
result => (result.confirmed ? { status: "confirmed" as const } : { status: "unconfirmed" as const }),
|
|
796
|
+
(err: unknown) => ({ status: "failed" as const, err }),
|
|
797
|
+
);
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
function getOrStartKernelDisposalResultPromise(
|
|
801
|
+
session: KernelSession,
|
|
802
|
+
timeoutMs?: number,
|
|
803
|
+
): Promise<KernelDisposalResult> {
|
|
804
|
+
if (!session.disposeResultPromise) {
|
|
805
|
+
session.disposeResultTimeoutMs = timeoutMs;
|
|
806
|
+
const releaseDisposalAttempt = Promise.withResolvers<void>();
|
|
807
|
+
session.disposeAttemptPromise = releaseDisposalAttempt.promise;
|
|
808
|
+
session.resolveDisposeAttempt = releaseDisposalAttempt.resolve;
|
|
809
|
+
const disposeResultPromise = createKernelDisposalResultPromise(session, timeoutMs);
|
|
810
|
+
void disposeResultPromise.then(result => {
|
|
811
|
+
if (result.status === "confirmed") {
|
|
812
|
+
finishDisposingKernelSession(session);
|
|
813
|
+
return;
|
|
814
|
+
}
|
|
815
|
+
if (session.disposing) {
|
|
816
|
+
session.nextDisposalRetryAt = Date.now() + CLEANUP_INTERVAL_MS;
|
|
817
|
+
syncCleanupTimer();
|
|
818
|
+
}
|
|
819
|
+
});
|
|
820
|
+
const disposalAttemptPromise = disposeResultPromise.finally(() => {
|
|
821
|
+
releaseDisposalAttempt.resolve();
|
|
822
|
+
if (session.disposeResultPromise === disposalAttemptPromise) {
|
|
823
|
+
session.disposeResultPromise = undefined;
|
|
824
|
+
session.disposeResultTimeoutMs = undefined;
|
|
825
|
+
}
|
|
826
|
+
if (session.disposeAttemptPromise === releaseDisposalAttempt.promise) {
|
|
827
|
+
session.disposeAttemptPromise = undefined;
|
|
828
|
+
session.resolveDisposeAttempt = undefined;
|
|
829
|
+
}
|
|
830
|
+
});
|
|
831
|
+
session.disposeResultPromise = disposalAttemptPromise;
|
|
562
832
|
}
|
|
563
|
-
|
|
833
|
+
return session.disposeResultPromise;
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
async function waitForKernelSessionDisposal(
|
|
837
|
+
session: KernelSession,
|
|
838
|
+
timeoutMs?: number,
|
|
839
|
+
): Promise<KernelDisposalWaitResult | undefined> {
|
|
840
|
+
const disposeResultPromise = session.disposeResultPromise;
|
|
841
|
+
if (!disposeResultPromise) {
|
|
842
|
+
return undefined;
|
|
843
|
+
}
|
|
844
|
+
if (timeoutMs === undefined) {
|
|
845
|
+
return await disposeResultPromise;
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
let timeoutId: NodeJS.Timeout | undefined;
|
|
849
|
+
const result = await Promise.race([
|
|
850
|
+
disposeResultPromise,
|
|
851
|
+
new Promise<{ status: "timedOut" }>(resolve => {
|
|
852
|
+
timeoutId = setTimeout(() => resolve({ status: "timedOut" }), timeoutMs);
|
|
853
|
+
timeoutId.unref();
|
|
854
|
+
}),
|
|
855
|
+
]);
|
|
856
|
+
|
|
857
|
+
if (timeoutId) {
|
|
858
|
+
clearTimeout(timeoutId);
|
|
859
|
+
}
|
|
860
|
+
return result;
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
function retryKernelSessionDisposalInBackground(session: KernelSession): void {
|
|
864
|
+
session.nextDisposalRetryAt = undefined;
|
|
865
|
+
void disposeKernelSession(session);
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
async function disposeKernelSession(session: KernelSession, shutdownTimeoutMs?: number): Promise<void> {
|
|
869
|
+
if (!session.disposing) {
|
|
870
|
+
if (!beginDisposingKernelSession(session)) return;
|
|
871
|
+
const releaseDisposalCapacity = Promise.withResolvers<void>();
|
|
872
|
+
session.disposeCapacityPromise = releaseDisposalCapacity.promise;
|
|
873
|
+
session.resolveDisposeCapacity = releaseDisposalCapacity.resolve;
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
if (
|
|
877
|
+
shutdownTimeoutMs === undefined &&
|
|
878
|
+
session.disposeResultPromise &&
|
|
879
|
+
session.disposeResultTimeoutMs !== undefined
|
|
880
|
+
) {
|
|
881
|
+
const inheritedResult = await session.disposeResultPromise;
|
|
882
|
+
if (inheritedResult.status === "confirmed") {
|
|
883
|
+
finishDisposingKernelSession(session);
|
|
884
|
+
return;
|
|
885
|
+
}
|
|
886
|
+
session.disposeResultPromise = undefined;
|
|
887
|
+
session.disposeResultTimeoutMs = undefined;
|
|
888
|
+
logger.warn("Retained kernel shutdown was not confirmed during owner cleanup; retrying without timeout", {
|
|
889
|
+
sessionId: session.id,
|
|
890
|
+
});
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
const inheritedBackgroundRetryTimeoutMs =
|
|
894
|
+
shutdownTimeoutMs === undefined && session.disposeResultPromise && session.disposeResultTimeoutMs === undefined
|
|
895
|
+
? OWNER_CLEANUP_KERNEL_SHUTDOWN_TIMEOUT_MS
|
|
896
|
+
: shutdownTimeoutMs;
|
|
897
|
+
|
|
898
|
+
getOrStartKernelDisposalResultPromise(session, shutdownTimeoutMs);
|
|
899
|
+
const result = await waitForKernelSessionDisposal(session, inheritedBackgroundRetryTimeoutMs);
|
|
900
|
+
if (!result) {
|
|
901
|
+
return;
|
|
902
|
+
}
|
|
903
|
+
if (result.status === "timedOut") {
|
|
904
|
+
logger.warn(
|
|
905
|
+
shutdownTimeoutMs === undefined
|
|
906
|
+
? "Timed out waiting for retained kernel shutdown during global cleanup; retained capacity remains reserved"
|
|
907
|
+
: "Timed out shutting down retained kernel during owner cleanup",
|
|
908
|
+
{
|
|
909
|
+
sessionId: session.id,
|
|
910
|
+
timeoutMs: inheritedBackgroundRetryTimeoutMs,
|
|
911
|
+
},
|
|
912
|
+
);
|
|
913
|
+
if (shutdownTimeoutMs !== undefined) {
|
|
914
|
+
retryKernelSessionDisposalInBackground(session);
|
|
915
|
+
}
|
|
916
|
+
return;
|
|
917
|
+
}
|
|
918
|
+
if (result.status === "confirmed") {
|
|
919
|
+
finishDisposingKernelSession(session);
|
|
920
|
+
return;
|
|
921
|
+
}
|
|
922
|
+
if (result.status === "unconfirmed") {
|
|
923
|
+
logger.warn(
|
|
924
|
+
shutdownTimeoutMs === undefined
|
|
925
|
+
? "Kernel shutdown was not confirmed; retained capacity remains reserved"
|
|
926
|
+
: "Retained kernel shutdown was not confirmed during owner cleanup",
|
|
927
|
+
{ sessionId: session.id },
|
|
928
|
+
);
|
|
929
|
+
if (shutdownTimeoutMs !== undefined) {
|
|
930
|
+
retryKernelSessionDisposalInBackground(session);
|
|
931
|
+
}
|
|
932
|
+
return;
|
|
933
|
+
}
|
|
934
|
+
logger.warn(
|
|
935
|
+
shutdownTimeoutMs === undefined
|
|
936
|
+
? "Failed to shutdown kernel"
|
|
937
|
+
: "Failed to shutdown retained kernel during owner cleanup",
|
|
938
|
+
{
|
|
939
|
+
sessionId: session.id,
|
|
940
|
+
error: result.err instanceof Error ? result.err.message : String(result.err),
|
|
941
|
+
},
|
|
942
|
+
);
|
|
943
|
+
if (shutdownTimeoutMs !== undefined) {
|
|
944
|
+
retryKernelSessionDisposalInBackground(session);
|
|
945
|
+
}
|
|
946
|
+
return;
|
|
564
947
|
}
|
|
565
948
|
|
|
566
949
|
async function withKernelSession<T>(
|
|
@@ -570,10 +953,11 @@ async function withKernelSession<T>(
|
|
|
570
953
|
options: KernelSessionExecutionOptions = {},
|
|
571
954
|
): Promise<T> {
|
|
572
955
|
let session = kernelSessions.get(sessionId);
|
|
956
|
+
if (session?.disposing) {
|
|
957
|
+
session = undefined;
|
|
958
|
+
}
|
|
573
959
|
if (!session) {
|
|
574
|
-
|
|
575
|
-
await evictOldestSession();
|
|
576
|
-
}
|
|
960
|
+
await ensureKernelSessionCapacity(options);
|
|
577
961
|
requireRemainingTimeoutMs(options.deadlineMs);
|
|
578
962
|
if (options.signal?.aborted) {
|
|
579
963
|
throw new PythonExecutionCancelledError(isTimedOutCancellation(options.signal.reason, options.signal));
|
|
@@ -582,10 +966,15 @@ async function withKernelSession<T>(
|
|
|
582
966
|
kernelSessions.set(sessionId, session);
|
|
583
967
|
startCleanupTimer();
|
|
584
968
|
}
|
|
969
|
+
attachKernelOwner(sessionId, options.kernelOwnerId);
|
|
970
|
+
|
|
971
|
+
if (session.disposing) {
|
|
972
|
+
return await withKernelSession(sessionId, cwd, handler, options);
|
|
973
|
+
}
|
|
585
974
|
|
|
586
975
|
const run = async (): Promise<T> => {
|
|
587
976
|
session!.lastUsedAt = Date.now();
|
|
588
|
-
if (session!.dead || !session!.kernel.isAlive()) {
|
|
977
|
+
if (session!.dead || session!.needsRestart || !session!.kernel.isAlive()) {
|
|
589
978
|
await logger.time("kernel:restartKernelSession", restartKernelSession, session!, cwd, options);
|
|
590
979
|
}
|
|
591
980
|
try {
|
|
@@ -593,7 +982,7 @@ async function withKernelSession<T>(
|
|
|
593
982
|
session!.restartCount = 0;
|
|
594
983
|
return result;
|
|
595
984
|
} catch (err) {
|
|
596
|
-
if (!session!.dead && session!.kernel.isAlive()) {
|
|
985
|
+
if (!session!.dead && !session!.needsRestart && session!.kernel.isAlive()) {
|
|
597
986
|
throw err;
|
|
598
987
|
}
|
|
599
988
|
await logger.time("kernel:restartKernelSession", restartKernelSession, session!, cwd, options);
|
|
@@ -620,6 +1009,9 @@ async function withKernelSession<T>(
|
|
|
620
1009
|
|
|
621
1010
|
try {
|
|
622
1011
|
await waitForQueueTurn(queue, options);
|
|
1012
|
+
if (session.disposing) {
|
|
1013
|
+
return await withKernelSession(sessionId, cwd, handler, options);
|
|
1014
|
+
}
|
|
623
1015
|
return await run();
|
|
624
1016
|
} finally {
|
|
625
1017
|
releaseTurn?.();
|
|
@@ -741,6 +1133,9 @@ export async function executePython(code: string, options?: PythonExecutorOption
|
|
|
741
1133
|
const existing = kernelSessions.get(sessionId);
|
|
742
1134
|
if (existing) {
|
|
743
1135
|
await disposeKernelSession(existing);
|
|
1136
|
+
if (existing.disposing && existing.nextDisposalRetryAt !== undefined) {
|
|
1137
|
+
retryKernelSessionDisposalInBackground(existing);
|
|
1138
|
+
}
|
|
744
1139
|
}
|
|
745
1140
|
}
|
|
746
1141
|
return await withKernelSession(
|